view savane/svmain/models.py @ 185:3d1a07772d4d

Rework title/icon template infrastructure
author Sylvain Beucler <beuc@beuc.net>
date Sun, 25 Jul 2010 09:59:57 +0200
parents 27559c1989f9
children 6bc5e698e9c4
line wrap: on
line source

# User/group extra attributes
# Copyright (C) 2002-2006 Mathieu Roy <yeupou--gnu.org>
# Copyright (C) 2007, 2008, 2009  Sylvain Beucler
# Copyright (C) 2008  Aleix Conchillo Flaque
# Copyright (C) 2009  Jonathan Gonzalez V.
#
# This file is part of Savane.
#
# Savane is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Savane is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
User/group extra attributes

This may look like reinventing the User.get_profile() that comes with
Django;
http://www.b-list.org/weblog/2006/jun/06/django-tips-extending-user-model/
http://mirobetm.blogspot.com/2007/04/en-django-extending-user_2281.html

However profiles were mainly useful in Django < 1.0 where you couldn't
subclass User.

Profiles also have a few drawbacks, namely they are site-specific,
which means you cannot have multiple applications have different
profiles in the same website, while with subclassing you only need to
user different class names (to avoid parent->child fieldname clash).

Moreover splitting the information in two different models can be
cumbersome when using ModelForms.

Subclassing drawback: there's apparently a technique to use a
vhost-based profile class (with django.contrib.site and multiples
settings.py). But it's not useful for Savane IMHO.

In addition, it seems impossible to convert an existing User to a
derived class from Python (this can be done through DB but that's
ugly). This apparently prevents auto-creating our derived class when a
new User is directly created (and sends a post_save signal).

Profiles vs. inheritance is also described at
http://scottbarnham.com/blog/2008/08/21/extending-the-django-user-model-with-inheritance/

Note that Scott's authentication backend has the same issue than
profiles: only one profile class can be used on a single website, so
we don't use it.

The current solution is to use AutoOneToOneField: OneToOneField is
similar to extending a model class (at the SQL tables level), and
AutoOneToOneField is a trick from django-annoying to automatically
create the extended data on first access.
"""

from django.db import models
from django.contrib.auth import models as auth_models


class SshKey(models.Model):
    user = models.ForeignKey(auth_models.User)
    # Could a CharField with max_length=3000 or something similar, as
    # it's a single line of text, but it sounds safer to use a
    # TextField for such a long text.  Too bad for the admin/ area.
    ssh_key = models.TextField(blank=False)

from annoying.fields import AutoOneToOneField
class SvUserInfo(models.Model):
    """
    Django base User class + extra Savane fields

    Since it adds a field to Django's User objects, we prefix it by
    'sv' to avoid clashes with other packages, C-style (ahem).

    Using AutoOneToOneField to automatically create this extra data
    for new users as soon as the field is accessed.
    """

    class Meta:
        ordering = ['user__username']

    user = AutoOneToOneField(auth_models.User, primary_key=True)

    # Migrated to 'first_name' and 'last_name' in auth.User
    #realname = models.CharField(max_length=96)

    # Old Savane can be Active/Deleted/Pending/Suspended/SQuaD
    status_CHOICES = (
        ('A', 'Active'),
        ('D', 'Deleted'),
        ('P', 'Pending'),
        ('S', 'Suspended'),
        #('SQD', 'Squad'), # TODO: implement squads more cleanly
        )
    status = models.CharField(max_length=3, choices=status_CHOICES, default='A')

    # Unix mapping, used when populating a LDAP directory
    uidNumber = models.IntegerField(default=0)
    gidNumber = models.IntegerField(default=0)

    # Used by trackers only but it could be used more widely
    spamscore = models.IntegerField(null=True, blank=True)
    # Previously used for e-mail changes and password recovery, Django
    # does it different with a auth.tokens
    #confirm_hash = models.CharField(max_length=96, blank=True, null=True)

    # Keys
    gpg_key = models.TextField(blank=True)
    gpg_key_count = models.IntegerField(null=True, blank=True)
    # SSH keys: cf. SshKey above

    # Personal info
    people_resume = models.TextField()

    # Preferences - /!\ some are also in the user_preferences table
    people_view_skills = models.BooleanField(default=False)
    email_hide = models.BooleanField(default=False)
    timezone = models.CharField(max_length=192, blank=True)
    theme = models.CharField(max_length=45, blank=True)

    # Inherit specialized models.Manager with convenience functions
    objects = auth_models.UserManager()

    @staticmethod
    def query_active_users_raw(conn, fields):
        """
        Return efficient query with all the users; used by LDIF export
        """
        return conn.query("SELECT "
                          + ",".join(fields)
                          + " FROM auth_user JOIN svmain_extendeduser"
                          + " ON auth_user.id = svmain_extendeduser.user_ptr_id"
                          + " WHERE status = 'A'"
                          )


class License(models.Model):
    """
    Main license used by a project

    TODO: support several licenses per project (mixed-licensed code,
    documentation, ...)
    """
    slug = models.SlugField(max_length=32)
    name = models.CharField(max_length=255)
    url = models.CharField(max_length=255, blank=True)

    def get_group_names(self):
        """
        Return a list of groups with only the 'name' attribute
        retrieved for efficiency (retrieving all informations, namely
        long_description, is quite long).  Used by the license template.
        """
        return self.svgroupinfo_set.values_list('group__name', flat=True)

    def __unicode__(self):
        return self.slug + ": " + self.name

    @models.permalink
    def get_absolute_url(self):
        return ('savane.svmain.license_detail', [self.slug])

    class Meta:
        ordering = ['slug']

class DevelopmentStatus(models.Model):
    """Describe the development status of a project"""
    name = models.CharField(max_length=255)

    def __unicode__(self):
        return self.name

    class Meta:
        ordering = ['name']
        verbose_name_plural='Development statuses'

class GroupConfiguration(models.Model):
    """Group configuration and main category (previously group_type)"""
    name = models.CharField(max_length=255)
    description = models.TextField(blank=True,
      help_text='Will be added on each project main page')

    #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, blank=True)

    # Mailing lists
    mailing_list_address = models.CharField(max_length=255, default='@',
      help_text='would be %LIST@gnu.org for GNU projects at sv.gnu.org')
    mailing_list_virtual_host = models.CharField(max_length=255, blank=True,
      help_text='would be lists.gnu.org or lists.nongnu.org at sv.gnu.org [BACKEND SPECIFIC]')
    mailing_list_format = models.CharField(max_length=255, default='%NAME',
      help_text='With this, you can force projects to follow a specific policy'
        + ' for the name of the %LIST. Here you should use the special wildcard'
        + ' %NAME, which is the part the of the mailing list name that the'
        + ' project admin can define (would be %PROJECT-%NAME for non-GNU'
        + ' projects at sv.gnu.org). Do no add any @hostname here!'
        + ' You can specify multiple formats separated by a "," comma.')
    #mailing_list_host = models.CharField(max_length=255, help_text='DEPRECATED')

    # Permissions
    can_use_homepage     = models.BooleanField(default=True)
    can_use_download     = models.BooleanField(default=True,
      help_text='This is useful if you provide directly download areas (created'
        + ' by the backend) or if you want to allow projects to configure the'
        + ' related menu entry (see below).')
    can_use_cvs          = models.BooleanField(default=False)
    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,
      help_text='This is useful if you want project to select a license'
        + ' on submission.')
    can_use_devel_status = models.BooleanField(default=True,
      help_text='This is useful if you want project to be able to defines their'
        + ' development status that will be shown on their main page. This is'
        + ' purely a matter of cosmetics. This option is mainly here just to'
        + ' remove this content in case it is useless (it does not makes sense'
        + ' for organizational projects).')
    can_use_mailing_list = models.BooleanField(default=True,
      help_text='This is one of the main issue tracker of Savane.'
        + ' Projects are supposed to use it as primary interface with end user.')
    can_use_support      = models.BooleanField(default=True)
    can_use_bug          = models.BooleanField(default=True)
    can_use_task         = models.BooleanField(default=True)
    can_use_patch        = models.BooleanField(default=False)
    can_use_news         = models.BooleanField(default=True)
    is_menu_configurable_homepage                = models.BooleanField(default=False,
      help_text='the homepage link can be modified')
    is_menu_configurable_download                = 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,
      help_text="the download _directory_ can be modified -- beware, if the"
        + " backend is running and creating download dir, it can be used"
        + " maliciously. don't activate this feature unless you truly know"
        + "what you're doing")

    # 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_cvs_viewcvs_homepage = 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_download             = 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)

    # Deprecated
    # "Forum is a deprecated feature of Savane. We do not recommend
    #  using it and we do not maintain this code any longer."
    #can_use_forum = models.BooleanField(default=False)
    #is_menu_configurable_forum = models.BooleanField(default=False)
    #forum_flags = IntegerField(default='2')
    #forum_rflags = IntegerField(default='2')

    # Unused
    #license_array = models.TextField()
    #devel_status_array = models.TextField()

    # TODO: split forum and news config
    #news_flags      = IntegerField(default='3')
    #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')

    def __unicode__(self):
        return self.name


class SvGroupInfo(models.Model):
    """
    Django base Group class + extra Savane fields

    Cf. SvUserInfo for concepts.
    """

    class Meta:
        ordering = ['group__name']

    group = AutoOneToOneField(auth_models.Group, primary_key=True)

    type = models.ForeignKey(GroupConfiguration)
    full_name = models.CharField(max_length=255, blank=True,
      help_text="Full project name (not Unix system name)")
    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, choices=status_CHOICES, default='A')
    gidNumber = models.IntegerField(default=0)

    short_description = models.CharField(max_length=255, blank=True)
    long_description = models.TextField(blank=True)
    license = models.ForeignKey(License, blank=True, null=True)
    license_other = models.TextField(blank=True)

    devel_status = models.ForeignKey(DevelopmentStatus)

    # Registration-specific
    register_purpose = models.TextField(blank=True)
    required_software = models.TextField(blank=True)
    other_comments = models.TextField(blank=True)

    register_time = models.DateTimeField()
    #rand_hash text,

    registered_gpg_keys = models.TextField(blank=True)

    # 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_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)

    # blank means 'use default'
    url_homepage                = models.CharField(max_length=255, blank=True)
    url_download                = models.CharField(max_length=255, blank=True)
    url_support                 = models.CharField(max_length=255, blank=True)
    url_mail                    = models.CharField(max_length=255, blank=True)
    url_cvs                     = models.CharField(max_length=255, blank=True)
    url_cvs_viewcvs             = models.CharField(max_length=255, blank=True)
    url_cvs_viewcvs_homepage    = models.CharField(max_length=255, blank=True)
    url_arch                    = models.CharField(max_length=255, blank=True)
    url_arch_viewcvs            = models.CharField(max_length=255, blank=True)
    url_svn                     = models.CharField(max_length=255, blank=True)
    url_svn_viewcvs             = models.CharField(max_length=255, blank=True)
    url_git                     = models.CharField(max_length=255, blank=True)
    url_git_viewcvs             = models.CharField(max_length=255, blank=True)
    url_hg                      = models.CharField(max_length=255, blank=True)
    url_hg_viewcvs              = models.CharField(max_length=255, blank=True)
    url_bzr                     = models.CharField(max_length=255, blank=True)
    url_bzr_viewcvs             = models.CharField(max_length=255, blank=True)
    url_bugs                    = models.CharField(max_length=255, blank=True)
    url_task                    = models.CharField(max_length=255, blank=True)
    url_patch                   = models.CharField(max_length=255, blank=True)
    url_extralink_documentation = models.CharField(max_length=255, blank=True)

    # Admin override (unused)
    #dir_cvs = models.CharField(max_length=255)
    #dir_arch = models.CharField(max_length=255)
    #dir_svn = models.CharField(max_length=255)
    #dir_git = models.CharField(max_length=255)
    #dir_hg = models.CharField(max_length=255)
    #dir_bzr = models.CharField(max_length=255)
    #dir_homepage = models.CharField(max_length=255)
    #dir_download = models.CharField(max_length=255)

    # Deprecated
    #url_forum = models.CharField(max_length=255, blank=True)
    #use_forum = models.BooleanField(default=False)

    # 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

    def full_name_display(self):
        if self.full_name != "":
            return self.full_name
        else:
            return self.group.name

    @staticmethod
    def query_active_groups_raw(conn, fields):
        """
        Return efficient query with all the users; used by LDIF export
        """
        return conn.query("SELECT "
                          + ",".join(fields)
                          + " FROM auth_group JOIN svmain_svgroupinfo"
                          + " ON auth_group.id = svmain_svgroupinfo.group_id"
                          + " WHERE status = 'A'"
                          )

    def __unicode__(self):
        return "%s (%s)" % (self.group.name, self.status)


class Membership(models.Model):
    """
    Extra attributes about a User<->Group relationship
    (e.g. "is the user an admin?")

    Consider this as metadata about an existing
    django.contrib.auth.User.groups relationship; or a potential
    relationship (e.g. pending membership waiting for admin approval).

    The group membership is defined by the underlying User.groups
    relationship, not this one.
    """

    class Meta:
        unique_together = (('user', 'group'),)
        ordering = ('group', 'user', )

    user = models.ForeignKey(auth_models.User)
    group = models.ForeignKey(auth_models.Group)

    admin_flags_CHOICES = (
        ('A', 'Admin'),
        # IMHO we need to put 'P' in a separate table, like 'pending
        # membership', otherwise it's too easy to make mistakes
        ('P', 'Pending moderation'),
        ('SQD', 'Squad'), # FIXME: I dislike squad=user
        )
    admin_flags = models.CharField(max_length=3, choices=admin_flags_CHOICES,
      blank=True, help_text="membership properties")
    onduty = models.BooleanField(default=True,
      help_text="Untick to hide emeritous members from the project page")

    # TODO: split news params
    #news_flags int(11) default NULL

    # Trackers-related
    #privacy_flags = models.BooleanField(default=True)
    #bugs_flags int(11) default NULL
    #task_flags int(11) default NULL
    #patch_flags int(11) default NULL
    #support_flags int(11) default NULL
    #cookbook_flags int(11) default NULL

    # Deprecated
    #forum_flags int(11) default NULL

    @staticmethod
    def query_active_memberships_raw(conn, fields):
        """
        Return efficient query with all the users; used by LDIF export
        """
        return conn.query("SELECT "
                          + ",".join(fields)
                          + " FROM svmain_membership JOIN auth_user"
                          + " ON user_id = auth_user.id"
                          + " WHERE admin_flags<>'P'"
                          )

    def __unicode__(self):
        return "[%s is a member of %s]" % (self.user.username, self.group.name)