changeset 111:8aa5c05316ed

Merge branch 'master' of ssh://git.sv.gnu.org/srv/git/savane-cleanup/framework
author Jonathan Gonzalez V <zeus@gnu.org>
date Mon, 27 Jul 2009 09:23:06 -0400
parents f5b5694f118b (current diff) 17623d457b70 (diff)
children 7e1da1c3afae
files template/savane/my/conf.html template/savane/my/index.html template/savane/my/resume_skill.html template/savane/my/ssh_gpg.html
diffstat 19 files changed, 604 insertions(+), 159 deletions(-) [+]
line wrap: on
line diff
--- a/INSTALL
+++ b/INSTALL
@@ -9,7 +9,7 @@
 
 * Dependencies as Debian packages
 
-apt-get install mysql-server python-django python-mysqldb 
+apt-get install mysql-server python-django python-mysqldb python-decorator
 
 
 * Install process
--- a/migrate_old_savane.sql
+++ b/migrate_old_savane.sql
@@ -3,9 +3,11 @@
 -- Import all users except for the 'None' user (#100)
 INSERT INTO auth_user
     (id, username, first_name, last_name, email,
-     password, last_login, date_joined, is_active)
-  SELECT user_id, user_name, trim(convert(realname using latin1)), '', email,
-      CONCAT('md5$$', user_pw), now(), FROM_UNIXTIME(add_date), status='A'
+     password, last_login, date_joined, is_active,
+     is_superuser, is_staff)
+  SELECT user_id, user_name, realname, '', email,
+      CONCAT('md5$$', user_pw), now(), FROM_UNIXTIME(add_date), status='A',
+      0, 0
     FROM savane_old.user
     WHERE user_id != 100;
 
@@ -16,7 +18,7 @@
      timezone, theme, email_hide, gpg_key, gpg_key_count)
   SELECT user_id, status, spamscore, authorized_keys,
       authorized_keys_count, people_view_skills,
-      CONVERT(people_resume USING latin1), timezone, theme,
+      people_resume, timezone, theme,
       email_hide, gpg_key, gpg_key_count
     FROM savane_old.user
     WHERE user_id != 100;
new file mode 100644
--- /dev/null
+++ b/src/savane/my/fixtures/README
@@ -0,0 +1,2 @@
+The fixtures subdirectory is used by the Django framework for test
+suites.
new file mode 100644
--- /dev/null
+++ b/src/savane/my/fixtures/developmentstatus.yaml
@@ -0,0 +1,36 @@
+- fields:
+    name: '0 - Undefined'
+  model: my.developmentstatus
+  pk: 1
+- fields:
+    name: '1 - Planning'
+  model: my.developmentstatus
+  pk: 2
+- fields:
+    name: '2 - Pre-Alpha'
+  model: my.developmentstatus
+  pk: 3
+- fields:
+    name: '3 - Alpha'
+  model: my.developmentstatus
+  pk: 4
+- fields:
+    name: '4 - Beta'
+  model: my.developmentstatus
+  pk: 5
+- fields:
+    name: '5 - Production/Stable'
+  model: my.developmentstatus
+  pk: 6
+- fields:
+    name: '6 - Mature'
+  model: my.developmentstatus
+  pk: 7
+- fields:
+    name: 'N/A'
+  model: my.developmentstatus
+  pk: 8
+- fields:
+    name: '? - Orphaned/Unmaintained'
+  model: my.developmentstatus
+  pk: 9
new file mode 100644
--- /dev/null
+++ b/src/savane/my/fixtures/license.yaml
@@ -0,0 +1,88 @@
+- fields:
+    slug: 'gpl'
+    name: 'GNU General Public License V2 or later'
+    url: 'http://www.gnu.org/copyleft/gpl.html'
+  model: my.license
+  pk: 1
+- fields:
+    slug: 'lgpl'
+    name: 'GNU Lesser General Public License'
+    url: 'http://www.gnu.org/copyleft/lesser.html'
+  model: my.license
+  pk: 2
+- fields:
+    slug: 'fdl'
+    name: 'GNU Free Documentation License'
+    url: 'http://www.gnu.org/copyleft/fdl.html'
+  model: my.license
+  pk: 3
+- fields:
+    slug: 'mbsd'
+    name: 'Modified BSD License'
+    url: 'http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5'
+  model: my.license
+  pk: 4
+- fields:
+    slug: 'x11'
+    name: 'X11 license'
+    url: 'http://www.x.org/terms.htm'
+  model: my.license
+  pk: 5
+- fields:
+    slug: 'cryptix'
+    name: 'Cryptix General License'
+    url: 'http://www.cryptix.org/docs/license.html'
+  model: my.license
+  pk: 6
+- fields:
+    slug: 'zlib'
+    name: 'The license of ZLib'
+    url: 'ftp://ftp.freesoftware.com/pub/infozip/zlib/zlib_license.html'
+  model: my.license
+  pk: 7
+- fields:
+    slug: 'imatrix'
+    name: 'The license of the iMatix Standard Function Library'
+  model: my.license
+  pk: 8
+- fields:
+    slug: 'w3c'
+    name: 'The W3C Software Notice and License'
+    url: 'http://www.w3.org/Consortium/Legal/copyright-software.html'
+  model: my.license
+  pk: 9
+- fields:
+    slug: 'berkeley'
+    name: 'The Berkeley Database License'
+    url: 'http://www.sleepycat.com/license.net'
+  model: my.license
+  pk: 10
+- fields:
+    slug: 'python16'
+    name: 'The License of Python 1.6a2 and earlier versions'
+    url: 'http://www.python.org/doc/Copyright.html'
+  model: my.license
+  pk: 11
+- fields:
+    slug: 'cartistic'
+    name: 'The Clarified Artistic License'
+    url: 'http://www.statistica.unimib.it/utenti/dellavedova/software/artistic2.html'
+  model: my.license
+  pk: 12
+- fields:
+    slug: 'expat'
+    name: 'Expat License (sometime refered to as MIT License)'
+    url: 'http://www.gnu.org/licenses/license-list.html#Expat'
+  model: my.license
+  pk: 13
+- fields:
+    slug: 'affero'
+    name: 'Affero General Public License V1 or later'
+    url: 'http://www.affero.org/oagpl.html'
+  model: my.license
+  pk: 14
+- fields:
+    slug: 'website'
+    name: 'WebSite Only'
+  model: my.license
+  pk: 15
--- a/src/savane/my/models.py
+++ b/src/savane/my/models.py
@@ -18,14 +18,25 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from django.db import models
-from django.contrib.auth.models import User, UserManager
+from django.contrib.auth import models as auth_models
+
+# TODO: these models probably don't belong to the 'my' application
 
-class ExtendedUser(User):
+class ExtendedUser(auth_models.User):
+    """Django base User class + extra Savane fields"""
+
     # Migrated to 'firstname' in auth.User
     #realname = models.CharField(max_length=96)
 
     # Old Savane can be Active/Deleted/Pending/Suspended/SQuaD
-    status = models.CharField(max_length=48)
+    status_CHOICES = (
+        ('A', 'Active'),
+        ('D', 'Deleted'),
+        ('P', 'Pending'),
+        ('S', 'Suspended'),
+        #('SQD', 'Squad'), # TODO: implement squads more cleanly
+        )
+    status = models.CharField(max_length=3)
 
     # Used by trackers only but it could be used more widely
     spamscore = models.IntegerField(null=True, blank=True)
@@ -50,4 +61,273 @@
 
 
     # Inherit specialized models.Manager with convenience functions
-    objects = UserManager()
+    objects = auth_models.UserManager()
+
+class License(models.Model):
+    """
+    Main license used by a project
+
+    TODO: support several licenses per project (mixed-licensed code,
+    documentation, ...)
+    """
+    slug = models.CharField(max_length=32)
+    name = models.CharField(max_length=255)
+    url = models.CharField(max_length=255)
+
+class DevelopmentStatus(models.Model):
+    """Describe the development status of a project"""
+    name = models.CharField(max_length=255)
+
+class GroupConfiguration(models.Model):
+    """Group configuration and main category (previously group_type)"""
+    name = models.CharField(max_length=255)
+    # Text added to each project page
+    description = models.TextField(blank=True)
+
+    #admin_email_adress = models.CharField(max_length=128, null=True) # unused
+
+    # Redirect to this host when visiting project page
+    base_host = models.CharField(max_length=128, null=True)
+    # Mailing lists are hosted there
+    mailing_list_host = models.CharField(max_length=255, null=True)
+
+    # Permissions
+    can_use_homepage     = models.BooleanField(default=True)
+    can_use_download     = models.BooleanField(default=True)
+    can_use_cvs          = models.BooleanField(default=True)
+    can_use_arch         = models.BooleanField(default=False)
+    can_use_svn          = models.BooleanField(default=False)
+    can_use_git          = models.BooleanField(default=False)
+    can_use_hg           = models.BooleanField(default=False)
+    can_use_bzr          = models.BooleanField(default=False)
+    can_use_license      = models.BooleanField(default=True)
+    can_use_devel_status = models.BooleanField(default=True)
+    can_use_forum        = models.BooleanField(default=False)
+    can_use_mailing_list = models.BooleanField(default=True)
+    can_use_patch        = models.BooleanField(default=False)
+    can_use_task         = models.BooleanField(default=True)
+    can_use_news         = models.BooleanField(default=True)
+    can_use_support      = models.BooleanField(default=True)
+    can_use_bug          = models.BooleanField(default=True)
+    is_menu_configurable_homepage                = models.BooleanField(default=False)
+    is_menu_configurable_download                = models.BooleanField(default=False)
+    is_menu_configurable_forum                   = models.BooleanField(default=False)
+    is_menu_configurable_support                 = models.BooleanField(default=False)
+    is_menu_configurable_mail                    = models.BooleanField(default=False)
+    is_menu_configurable_cvs                     = models.BooleanField(default=False)
+    is_menu_configurable_cvs_viewcvs             = models.BooleanField(default=False)
+    is_menu_configurable_cvs_viewcvs_homepage    = models.BooleanField(default=False)
+    is_menu_configurable_arch                    = models.BooleanField(default=False)
+    is_menu_configurable_arch_viewcvs            = models.BooleanField(default=False)
+    is_menu_configurable_svn                     = models.BooleanField(default=False)
+    is_menu_configurable_svn_viewcvs             = models.BooleanField(default=False)
+    is_menu_configurable_git                     = models.BooleanField(default=False)
+    is_menu_configurable_git_viewcvs             = models.BooleanField(default=False)
+    is_menu_configurable_hg                      = models.BooleanField(default=False)
+    is_menu_configurable_hg_viewcvs              = models.BooleanField(default=False)
+    is_menu_configurable_bzr                     = models.BooleanField(default=False)
+    is_menu_configurable_bzr_viewcvs             = models.BooleanField(default=False)
+    is_menu_configurable_bugs                    = models.BooleanField(default=False)
+    is_menu_configurable_task                    = models.BooleanField(default=False)
+    is_menu_configurable_patch                   = models.BooleanField(default=False)
+    is_menu_configurable_extralink_documentation = models.BooleanField(default=False)
+    is_configurable_download_dir                 = models.BooleanField(default=False)
+
+    # Directory creation config
+    SCM_CHOICES = (
+        ('cvs', 'CVS'),
+        ('svn' , 'Subversion'),
+        ('arch' , 'GNU Arch'),
+        ('git' , 'Git'),
+        ('hg' , 'Mercurial'),
+        ('bzr' , 'Bazaar'),
+        )
+    homepage_scm = models.CharField(max_length=4, choices=SCM_CHOICES, default='cvs')
+    DIR_TYPE_CHOICES = (
+        ('basicdirectory', 'Basic directory'),
+        ('basiccvs', 'Basic CVS directory'),
+        ('basicsvn', 'Basic Subversion directory'),
+        ('basicgit', 'Basic Git directory'),
+        ('basichg', 'Basic Mercurial directory'),
+        ('basicbzr', 'Basic Bazaar directory'),
+        ('cvsattic', 'CVS Attic/Gna!'),
+        ('svnattic', 'Subversion Attic/Gna!'),
+        ('svnatticwebsite', 'Subversion Subdirectory Attic/Gna!'),
+        ('savannah-gnu', 'CVS Savannah GNU'),
+        ('savannah-nongnu', 'CVS Savannah non-GNU'),
+        )
+    dir_type_cvs      = models.CharField(max_length=15, choices=DIR_TYPE_CHOICES, default='basiccvs')
+    dir_type_arch     = models.CharField(max_length=15, choices=DIR_TYPE_CHOICES, default='basicdirectory')
+    dir_type_svn      = models.CharField(max_length=15, choices=DIR_TYPE_CHOICES, default='basicsvn')
+    dir_type_git      = models.CharField(max_length=15, choices=DIR_TYPE_CHOICES, default='basicgit')
+    dir_type_hg       = models.CharField(max_length=15, choices=DIR_TYPE_CHOICES, default='basichg')
+    dir_type_bzr      = models.CharField(max_length=15, choices=DIR_TYPE_CHOICES, default='basicbzr')
+    dir_type_homepage = models.CharField(max_length=15, choices=DIR_TYPE_CHOICES, default='basicdirectory')
+    dir_type_download = models.CharField(max_length=15, choices=DIR_TYPE_CHOICES, default='basicdirectory')
+    dir_homepage = models.CharField(max_length=255, default='/'),
+    dir_cvs      = models.CharField(max_length=255, default='/')
+    dir_arch     = models.CharField(max_length=255, default='/')
+    dir_svn      = models.CharField(max_length=255, default='/')
+    dir_git      = models.CharField(max_length=255, default='/')
+    dir_hg       = models.CharField(max_length=255, default='/')
+    dir_bzr      = models.CharField(max_length=255, default='/')
+    dir_download = models.CharField(max_length=255, default='/')
+
+    # Default URLs
+    url_homepage             = models.CharField(max_length=255, default='http://'),
+    url_download             = models.CharField(max_length=255, default='http://')
+    url_cvs_viewcvs          = models.CharField(max_length=255, default='http://')
+    url_arch_viewcvs         = models.CharField(max_length=255, default='http://') 
+    url_svn_viewcvs          = models.CharField(max_length=255, default='http://')
+    url_git_viewcvs          = models.CharField(max_length=255, default='http://')
+    url_hg_viewcvs           = models.CharField(max_length=255, default='http://')
+    url_bzr_viewcvs          = models.CharField(max_length=255, default='http://')
+    url_cvs_viewcvs_homepage = models.CharField(max_length=255, default='http://')
+    url_mailing_list_listinfo         = models.CharField(max_length=255, default='http://'),
+    url_mailing_list_subscribe        = models.CharField(max_length=255, default='http://')
+    url_mailing_list_unsubscribe      = models.CharField(max_length=255, default='http://')
+    url_mailing_list_archives         = models.CharField(max_length=255, default='http://')
+    url_mailing_list_archives_private = models.CharField(max_length=255, default='http://')
+    url_mailing_list_admin            = models.CharField(max_length=255, default='http://')
+    url_extralink_documentation = models.CharField(max_length=255, blank=True)
+
+    # Unused
+    #license_array = models.TextField()
+
+    devel_status_array = models.ForeignKey(DevelopmentStatus),
+
+    mailing_list_address = models.CharField(max_length=255, default='@'),
+    mailing_list_virtual_host = models.CharField(max_length=255, default=''),
+    mailing_list_format = models.CharField(max_length=255, default='%NAME'),
+
+    # TODO: split forum and news config
+    #forum_flags     = IntegerField(default='2')
+    #news_flags      = IntegerField(default='3')
+    #forum_rflags    = IntegerField(default='2')
+    #news_rflags     = IntegerField(default='2')
+
+    # TODO: split tracker config
+    #bugs_flags      = IntegerField(default='2')
+    #task_flags      = IntegerField(default='2')
+    #patch_flags     = IntegerField(default='2')
+    #cookbook_flags  = IntegerField(default='2')
+    #support_flags   = IntegerField(default='2')
+    #bugs_rflags     = IntegerField(default='2')
+    #task_rflags     = IntegerField(default='5')
+    #patch_rflags    = IntegerField(default='2')
+    #cookbook_rflags = IntegerField(default='5')
+    #support_rflags  = IntegerField(default='2')
+
+
+class ExtendedGroup(auth_models.Group):
+    """Django base Group class + extra Savane fields"""
+    
+    type = models.ForeignKey(GroupConfiguration)
+    name = models.CharField(max_length=30, blank=True)
+    is_public = models.BooleanField(default=False)
+    status_CHOICES = (
+        ('A', 'Active'),
+        ('P', 'Pending'),
+        ('D', 'Deleted'),
+        ('M', 'Maintenance (accessible only to superuser)'),
+        ('I', 'Incomplete (failure during registration)'),
+        )
+    status = models.CharField(max_length=1, default='A')
+    short_description = models.CharField(max_length=255, blank=True)
+    long_description = models.TextField()
+    license = models.ForeignKey(License, null=True)
+    license_other = models.TextField()
+
+    devel_status = models.ForeignKey(DevelopmentStatus),
+
+    # Registration-specific
+    register_purpose = models.TextField()
+    required_software = models.TextField()
+    other_comments = models.TextField()
+
+    register_time = models.DateTimeField()
+    #rand_hash text,
+    
+    registered_gpg_keys = models.TextField()
+
+    # Project "Features"
+    use_homepage                = models.BooleanField(default=False)
+    use_mail                    = models.BooleanField(default=False)
+    use_patch                   = models.BooleanField(default=False)
+    use_task                    = models.BooleanField(default=False)
+    use_forum                   = models.BooleanField(default=False)
+    use_cvs                     = models.BooleanField(default=False)
+    use_arch                    = models.BooleanField(default=False)
+    use_svn                     = models.BooleanField(default=False)
+    use_git                     = models.BooleanField(default=False)
+    use_hg                      = models.BooleanField(default=False)
+    use_bzr                     = models.BooleanField(default=False)
+    use_news                    = models.BooleanField(default=False)
+    use_support                 = models.BooleanField(default=False)
+    use_download                = models.BooleanField(default=False)
+    use_bugs                    = models.BooleanField(default=False)
+    use_extralink_documentation = models.BooleanField(default=False)
+
+    # 'null' means 'use default'
+    url_homepage                = models.CharField(max_length=255, null=True)
+    url_download                = models.CharField(max_length=255, null=True)
+    url_forum                   = models.CharField(max_length=255, null=True)
+    url_support                 = models.CharField(max_length=255, null=True)
+    url_mail                    = models.CharField(max_length=255, null=True)
+    url_cvs                     = models.CharField(max_length=255, null=True)
+    url_cvs_viewcvs             = models.CharField(max_length=255, null=True)
+    url_cvs_viewcvs_homepage    = models.CharField(max_length=255, null=True)
+    url_arch                    = models.CharField(max_length=255, null=True)
+    url_arch_viewcvs            = models.CharField(max_length=255, null=True)
+    url_svn                     = models.CharField(max_length=255, null=True)
+    url_svn_viewcvs             = models.CharField(max_length=255, null=True)
+    url_git                     = models.CharField(max_length=255, null=True)
+    url_git_viewcvs             = models.CharField(max_length=255, null=True)
+    url_hg                      = models.CharField(max_length=255, null=True)
+    url_hg_viewcvs              = models.CharField(max_length=255, null=True)
+    url_bzr                     = models.CharField(max_length=255, null=True)
+    url_bzr_viewcvs             = models.CharField(max_length=255, null=True)
+    url_bugs                    = models.CharField(max_length=255, null=True)
+    url_task                    = models.CharField(max_length=255, null=True)
+    url_patch                   = models.CharField(max_length=255, null=True)
+    url_extralink_documentation = models.CharField(max_length=255, null=True)
+
+    # Admin override (unused)
+    #dir_cvs = models.CharField(max_length=255, null=True)
+    #dir_arch = models.CharField(max_length=255, null=True)
+    #dir_svn = models.CharField(max_length=255, null=True)
+    #dir_git = models.CharField(max_length=255, null=True)
+    #dir_hg = models.CharField(max_length=255, null=True)
+    #dir_bzr = models.CharField(max_length=255, null=True)
+    #dir_homepage = models.CharField(max_length=255, null=True)
+    #dir_download = models.CharField(max_length=255, null=True)
+
+    # TODO: split trackers configuration
+    #bugs_preamble = models.TextField()
+    #task_preamble = models.TextField()
+    #patch_preamble = models.TextField()
+    #support_preamble = models.TextField()
+    #cookbook_preamble = models.TextField()
+
+    #new_bugs_address text NOT NULL
+    #new_patch_address text NOT NULL
+    #new_support_address text NOT NULL
+    #new_task_address text NOT NULL
+    #new_news_address text NOT NULL
+    #new_cookbook_address text NOT NULL
+
+    #bugs_glnotif int(11) NOT NULL default '1'
+    #support_glnotif int(11) NOT NULL default '1'
+    #task_glnotif int(11) NOT NULL default '1'
+    #patch_glnotif int(11) NOT NULL default '1'
+    #cookbook_glnotif int(11) NOT NULL default '1'
+    #send_all_bugs int(11) NOT NULL default '0'
+    #send_all_patch int(11) NOT NULL default '0'
+    #send_all_support int(11) NOT NULL default '0'
+    #send_all_task int(11) NOT NULL default '0'
+    #send_all_cookbook int(11) NOT NULL default '0'
+    #bugs_private_exclude_address text
+    #task_private_exclude_address text
+    #support_private_exclude_address text
+    #patch_private_exclude_address text
+    #cookbook_private_exclude_address text
--- a/src/savane/my/urls.py
+++ b/src/savane/my/urls.py
@@ -20,24 +20,38 @@
 from django.conf.urls.defaults import *
 from django.contrib.auth.decorators import login_required
 from django.views.generic.simple import direct_to_template
+from django.views.generic.list_detail import object_list
 import views
+import models as my_models
+from decorator import decorator
+
+@decorator
+def only_mine(f, *args, **kwargs):
+    """Filter a generic query_set to only display objets related to
+    the current user"""
+    request = args[0]
+    user = request.user
+    print kwargs
+    kwargs['queryset'] = kwargs['queryset'].filter(user=user.id)
+    return f(*args, **kwargs)
+
+@only_mine
+def object_list__only_mine(*args, **kwargs):
+    return object_list(*args, **kwargs)
 
 @login_required
 def direct_to_template__login_required(*args, **kwargs):
     return direct_to_template(*args, **kwargs)
 
-
 urlpatterns = patterns ('',
   url(r'^$', direct_to_template__login_required,
-      { 'template' : 'savane/my/index.html' },
-      name='my.views.index'),
-  url('^conf/$',
-      views.sv_conf,
-      ),
-  url('^conf/resume_skill$',
-      views.sv_resume_skill,
-      ),
-  url('^conf/ssh_gpg$',
-      views.sv_ssh_gpg,
-      ),
+      { 'template' : 'my/index.html' },
+      name='savane.my.views.index'),
+  url('^conf/$', views.sv_conf),
+  url('^conf/resume_skill$', views.sv_resume_skill),
+  url('^conf/ssh_gpg$', views.sv_ssh_gpg),
+  url('^conf/ssh_gpg$', views.sv_ssh_gpg),
+  url(r'^groups/$', object_list__only_mine,
+      { 'queryset' : my_models.ExtendedGroup.objects.all() },
+      name='savane.my.generic.group_list'),
 )
--- a/src/savane/my/views.py
+++ b/src/savane/my/views.py
@@ -69,7 +69,7 @@
                 success_msg = 'Personal information changed.'
                 form_identity = IdentityForm()
 
-    return render_to_response('savane/my/conf.html',
+    return render_to_response('my/conf.html',
                               { 'form_pass' : form_pass,
                                 'form_mail' : form_mail,
                                 'form_identity' : form_identity,
@@ -80,7 +80,7 @@
 
 @login_required()
 def sv_resume_skill( request ):
-    return render_to_response('savane/my/resume_skill.html',
+    return render_to_response('my/resume_skill.html',
                                context_instance=RequestContext(request))
 @login_required()
 def sv_ssh_gpg( request ):
@@ -136,7 +136,7 @@
             form_gpg = GPGForm()
 
 
-    return render_to_response('savane/my/ssh_gpg.html',
+    return render_to_response('my/ssh_gpg.html',
                               { 'form_gpg' : form_gpg,
                                 'form_ssh' : form_ssh,
                                 },
--- a/src/settings.py
+++ b/src/settings.py
@@ -84,12 +84,13 @@
 LOGIN_URL = '/accounts/login/'
 LOGIN_REDIRECT_URL = '/my/'
 
+# Used by syncdb, etc.
 INSTALLED_APPS = (
     'django.contrib.auth',
     'django.contrib.contenttypes',
     'django.contrib.sessions',
     'django.contrib.sites',
 #    'django.contrib.admin',
-#    'savane.my',
-#    'savane.main',
+    'savane.my',
+    'savane.main',
 )
--- a/template/README
+++ b/template/README
@@ -1,7 +1,6 @@
 Django's generic views look for templates with a path built after the
-application name, e.g. '<template_dir>/savane/my/' <- 'savane.my'.
-
-Let's store the Savane templates in 'savane/'.
+application label (Model._meta.app_label), e.g.
+'<template_dir>/my/' <- 'savane.my'.
 
 
 Base templates (base.html, etc.) are specific to each installation and
new file mode 100644
--- /dev/null
+++ b/template/my/conf.html
@@ -0,0 +1,74 @@
+{% extends "base.html" %}
+
+{% block content %}
+<ul class="section">
+  <li><a href="{% url savane.my.views.sv_conf %}">General Configuration</a></li>
+  <li><a href="{% url savane.my.views.sv_resume_skill %}">Resume &amp; Skill</a></li>
+  <li><a href="{% url savane.my.views.sv_ssh_gpg %}">SSH/GPG Keys</a></li>
+</ul>
+
+<div class="box">
+  <div class="boxtitle">Change Password</div>
+  <div class="boxitem">
+    <form method="post">
+      <dl>
+        {% for field in form_pass %}
+        {% if field.is_hidden %}
+        {{field}}
+        {% else %}
+        <dt>{{ field.label_tag }} {{ field.errors }}</dt>
+        <dd>{{ field }}</dd>
+        {% endif %}
+        {% endfor %}
+        <dd><input type="submit" value="Update" name="Update" /></dd>
+      </dl>
+    </form>
+  </div>
+</div>
+
+<div class="box">
+  <div class="boxtitle">Change E-Mail</div>
+  <div class="boxitem">
+    <form method="post">
+      <dl>
+        {% for field in form_mail %}
+        {% if field.is_hidden %}
+        {{field}}
+        {% else %}
+        <dt>{{ field.label_tag }} {{ field.errors }}</dt>
+        <dd>{{ field }}</dd>
+        {% endif %}
+        {% endfor %}
+        <dd><input type="submit" value="Update" name="Update" /></dd>
+      </dl>
+    </form>
+  </div>
+</div>
+
+<div class="box">
+  <div class="boxtitle">Change Name</div>
+  <div class="boxitem">
+    <form method="post">
+      <dl>
+        {% for field in form_identity %}
+        {% if field.is_hidden %}
+        {{field}}
+        {% else %}
+        <dt>{{ field.label_tag }} {{ field.errors }}</dt>
+        <dd>{{ field }}</dd>
+        {% endif %}
+        {% endfor %}
+        <dd><input type="submit" value="Update" name="Update" /></dd>
+      </dl>
+    </form>
+  </div>
+</div>
+
+{% endblock %}
+{% comment %}
+Local Variables: **
+mode: django-html **
+tab-width: 4 **
+indent-tabs-mode: nil **
+End: **
+{% endcomment %}
new file mode 100644
--- /dev/null
+++ b/template/my/extendedgroup_list.html
@@ -0,0 +1,23 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+{% if object_list %}
+    <ul>
+    {% for object in object_list %}
+        <li><a href="{{object.id}}">{{ object }}</a></li>
+    {% endfor %}
+    </ul>
+{% else %}
+    <p>You are not part of any group.</p>
+{% endif %}
+
+{% endblock %}
+
+{% comment %}
+Local Variables: **
+mode: django-html **
+tab-width: 4 **
+indent-tabs-mode: nil **
+End: **
+{% endcomment %}
new file mode 100644
--- /dev/null
+++ b/template/my/index.html
@@ -0,0 +1,6 @@
+{# -*- Mode: django-html; -*- #}
+{% extends "base.html" %}
+
+{% block content %}
+My nice personal page.
+{% endblock %}
new file mode 100644
--- /dev/null
+++ b/template/my/resume_skill.html
@@ -0,0 +1,18 @@
+{% extends "base.html" %}
+
+{% block content %}
+<ul class="section">
+  <li><a href="{% url savane.my.views.sv_conf %}">General Configuration</a></li>
+  <li><a href="{% url savane.my.views.sv_resume_skill %}">Resume &amp; Skill</a></li>
+  <li><a href="{% url savane.my.views.sv_ssh_gpg %}">SSH/GPG Keys</a></li>
+</ul>
+
+
+{% endblock %}
+{% comment %}
+Local Variables: **
+mode: django-html **
+tab-width: 4 **
+indent-tabs-mode: nil **
+End: **
+{% endcomment %}
new file mode 100644
--- /dev/null
+++ b/template/my/ssh_gpg.html
@@ -0,0 +1,32 @@
+{% extends "base.html" %}
+
+{% block content %}
+<ul class="section">
+  <li><a href="{% url savane.my.views.sv_conf %}">General Configuration</a></li>
+  <li><a href="{% url savane.my.views.sv_resume_skill %}">Resume &amp; Skill</a></li>
+  <li><a href="{% url savane.my.views.sv_ssh_gpg %}">SSH/GPG Keys</a></li>
+</ul>
+
+<h3>SSH Keys</h3>
+<form method="post">
+  {{ form_ssh.as_p }}
+  <br /><input type="submit" value="Save" name="Save" />
+</form>
+
+
+
+<h3>GPG Key</h3>
+<form method="post">
+  {{ form_gpg.as_p }}
+  <br /><input type="submit" value="Update" name="Update" />
+</form>
+
+
+{% endblock %}
+{% comment %}
+Local Variables: **
+mode: django-html **
+tab-width: 4 **
+indent-tabs-mode: nil **
+End: **
+{% endcomment %}
deleted file mode 100644
--- a/template/savane/my/conf.html
+++ /dev/null
@@ -1,74 +0,0 @@
-{% extends "base.html" %}
-
-{% block content %}
-<ul class="section">
-  <li><a href="{% url savane.my.views.sv_conf %}">General Configuration</a></li>
-  <li><a href="{% url savane.my.views.sv_resume_skill %}">Resume &amp; Skill</a></li>
-  <li><a href="{% url savane.my.views.sv_ssh_gpg %}">SSH/GPG Keys</a></li>
-</ul>
-
-<div class="box">
-  <div class="boxtitle">Change Password</div>
-  <div class="boxitem">
-    <form method="post">
-      <dl>
-        {% for field in form_pass %}
-        {% if field.is_hidden %}
-        {{field}}
-        {% else %}
-        <dt>{{ field.label_tag }} {{ field.errors }}</dt>
-        <dd>{{ field }}</dd>
-        {% endif %}
-        {% endfor %}
-        <dd><input type="submit" value="Update" name="Update" /></dd>
-      </dl>
-    </form>
-  </div>
-</div>
-
-<div class="box">
-  <div class="boxtitle">Change E-Mail</div>
-  <div class="boxitem">
-    <form method="post">
-      <dl>
-        {% for field in form_mail %}
-        {% if field.is_hidden %}
-        {{field}}
-        {% else %}
-        <dt>{{ field.label_tag }} {{ field.errors }}</dt>
-        <dd>{{ field }}</dd>
-        {% endif %}
-        {% endfor %}
-        <dd><input type="submit" value="Update" name="Update" /></dd>
-      </dl>
-    </form>
-  </div>
-</div>
-
-<div class="box">
-  <div class="boxtitle">Change Name</div>
-  <div class="boxitem">
-    <form method="post">
-      <dl>
-        {% for field in form_identity %}
-        {% if field.is_hidden %}
-        {{field}}
-        {% else %}
-        <dt>{{ field.label_tag }} {{ field.errors }}</dt>
-        <dd>{{ field }}</dd>
-        {% endif %}
-        {% endfor %}
-        <dd><input type="submit" value="Update" name="Update" /></dd>
-      </dl>
-    </form>
-  </div>
-</div>
-
-{% endblock %}
-{% comment %}
-Local Variables: **
-mode: django-html **
-tab-width: 4 **
-indent-tabs-mode: nil **
-End: **
-{% endcomment %}
deleted file mode 100644
--- a/template/savane/my/index.html
+++ /dev/null
@@ -1,6 +0,0 @@
-{# -*- Mode: django-html; -*- #}
-{% extends "base.html" %}
-
-{% block content %}
-My nice personal page.
-{% endblock %}
deleted file mode 100644
--- a/template/savane/my/resume_skill.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{% extends "base.html" %}
-
-{% block content %}
-<ul class="section">
-  <li><a href="{% url savane.my.views.sv_conf %}">General Configuration</a></li>
-  <li><a href="{% url savane.my.views.sv_resume_skill %}">Resume &amp; Skill</a></li>
-  <li><a href="{% url savane.my.views.sv_ssh_gpg %}">SSH/GPG Keys</a></li>
-</ul>
-
-
-{% endblock %}
-{% comment %}
-Local Variables: **
-mode: django-html **
-tab-width: 4 **
-indent-tabs-mode: nil **
-End: **
-{% endcomment %}
deleted file mode 100644
--- a/template/savane/my/ssh_gpg.html
+++ /dev/null
@@ -1,32 +0,0 @@
-{% extends "base.html" %}
-
-{% block content %}
-<ul class="section">
-  <li><a href="{% url savane.my.views.sv_conf %}">General Configuration</a></li>
-  <li><a href="{% url savane.my.views.sv_resume_skill %}">Resume &amp; Skill</a></li>
-  <li><a href="{% url savane.my.views.sv_ssh_gpg %}">SSH/GPG Keys</a></li>
-</ul>
-
-<h3>SSH Keys</h3>
-<form method="post">
-  {{ form_ssh.as_p }}
-  <br /><input type="submit" value="Save" name="Save" />
-</form>
-
-
-
-<h3>GPG Key</h3>
-<form method="post">
-  {{ form_gpg.as_p }}
-  <br /><input type="submit" value="Update" name="Update" />
-</form>
-
-
-{% endblock %}
-{% comment %}
-Local Variables: **
-mode: django-html **
-tab-width: 4 **
-indent-tabs-mode: nil **
-End: **
-{% endcomment %}