view savane/svmain/models.py @ 323:01ffc3eb965a

Ask search engines not to follow links with 'next=' params
author Sylvain Beucler <beuc@beuc.net>
date Sat, 21 Aug 2010 10:07:11 +0200
parents e80f63d8ddc3
children 3324f74d22b2
line wrap: on
line source

# User/group extra attributes
# Copyright (C) 2002-2006 Mathieu Roy <yeupou--gnu.org>
# Copyright (C) 2007, 2008, 2009, 2010  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.

However this means all SvGroupInfo fields have a default value or can
be NULL, which means the rest of the code will have to handle NULL
cases :/

There are also reports of issues when used with the South framework.

Also this code:
  group.svgroupinfo.type = ...
  group.svgroupinfo.save()
Currently fails: type remains NULL, probably because the result of the
first invocation of 'group.svgroupinfo' return a different result
thant the second one.  I submitted a fix:
http://bitbucket.org/offline/django-annoying/issue/16/cache-issue-with-autoonetoonefield

Currently we use a modified and fixed version (for the South and the
above issue) in savane.utils.fields, because upstream is not
responsive.
"""

from django.db import models
from django.contrib.auth import models as auth_models
from django.utils.translation import ugettext, ugettext_lazy as _
import datetime
from savane.utils import markup

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 savane.utils.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)

    # Confirm that user owns the e-mail the hash was sent to
    email_hash_confirm = models.BigIntegerField(blank=True, null=True)
    # Allow a user to counter-act an account hijack
    email_hash_cancel = models.BigIntegerField(blank=True, null=True)
    # The new e-mail to use if validated
    email_new = models.EmailField(_("new e-mail address"), blank=True, null=True)

    # Keys
    gpg_key = models.TextField(blank=True,
                               help_text=_("You can write down here your (ASCII) public key (gpg --export --armor keyid)"))
    gpg_key_count = models.IntegerField(null=True, blank=True)
    # SSH keys: cf. SshKey above

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

    superuser_is_enabled = models.BooleanField(default=False)

    # 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'"
                          )

    def get_full_name_display(self):
        if self.user.get_full_name() != "":
            return self.user.get_full_name()
        else:
            return self.user.username

    def __unicode__(self):
        return "Savane information on user %s" % (self.user.username)


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 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)
    url_cvs_viewcvs_homepage = models.CharField(max_length=255)
    url_cvs_viewcvs          = models.CharField(max_length=255)
    url_arch_viewcvs         = models.CharField(max_length=255)
    url_svn_viewcvs          = models.CharField(max_length=255)
    url_git_viewcvs          = models.CharField(max_length=255)
    url_hg_viewcvs           = models.CharField(max_length=255)
    url_bzr_viewcvs          = models.CharField(max_length=255)
    url_download             = models.CharField(max_length=255)
    url_mailing_list_listinfo         = models.CharField(max_length=255)
    url_mailing_list_archives         = models.CharField(max_length=255)
    url_mailing_list_archives_private = models.CharField(max_length=255)
    url_mailing_list_admin            = models.CharField(max_length=255)
    # Old majordomo
    #url_mailing_list_subscribe        = models.CharField(max_length=255)
    #url_mailing_list_unsubscribe      = models.CharField(max_length=255)
    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']
        verbose_name = _("project information")

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

    type = models.ForeignKey(GroupConfiguration,
      null=True)  # NULL when object initially created by AutoOneToOneField
    full_name = models.CharField(max_length=255, blank=True,
      verbose_name=_("full name"),
      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(_("short description"), max_length=255, blank=True)
    long_description = models.TextField(_("long description"), blank=True)
    license = models.ForeignKey(License, blank=True, null=True, verbose_name=_("license"))
    license_other = models.TextField(_("license (other)"), blank=True)

    devel_status = models.ForeignKey(DevelopmentStatus,
      null=True,  # NULL when object initially created by AutoOneToOneField
      verbose_name=_("development status"))

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

    register_time = models.DateTimeField(default=datetime.datetime.now)
    #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_download                = models.BooleanField(default=False)
    use_extralink_documentation = 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_bugs                    = models.BooleanField(default=False)
    use_patch                   = models.BooleanField(default=False)
    use_support                 = models.BooleanField(default=False)
    use_task                    = models.BooleanField(default=False)

    # blank means 'use default'
    url_homepage                = models.CharField(max_length=255, blank=True)
    url_mail                    = models.CharField(max_length=255, blank=True)
    url_download                = models.CharField(max_length=255, blank=True)
    url_extralink_documentation = 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_patch                   = models.CharField(max_length=255, blank=True)
    url_support                 = models.CharField(max_length=255, blank=True)
    url_task                    = 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 get_full_name_display(self):
        if self.full_name != "":
            return self.full_name
        else:
            return self.group.name

    def get_admin_memberships(self):
        return self.group.membership_set.filter(admin_flags='A')

    def get_active_memberships(self):
        return self.group.membership_set.exclude(admin_flags='P')

    # Download
    def get_url_download(self):
        return (self.url_download
                or self.type.url_download.replace('%PROJECT', self.group.name))

    # Homepage
    def uses_homepage(self):  return self.type.can_use_homepage and self.use_homepage

    def get_url_homepage(self):
        return (self.url_homepage
                or self.type.url_homepage.replace('%PROJECT', self.group.name))

    def uses_cvs_for_homepage (self):  return self.uses_homepage() and self.type.homepage_scm == 'cvs'
    def uses_svn_for_homepage (self):  return self.uses_homepage() and self.type.homepage_scm == 'svn'
    def uses_arch_for_homepage(self):  return self.uses_homepage() and self.type.homepage_scm == 'arch'
    def uses_git_for_homepage (self):  return self.uses_homepage() and self.type.homepage_scm == 'git'
    def uses_hg_for_homepage  (self):  return self.uses_homepage() and self.type.homepage_scm == 'hg'
    def uses_bzr_for_homepage (self):  return self.uses_homepage() and self.type.homepage_scm == 'bzr'

    def get_url_homepage_vcs_browser(self):
        return (self.url_cvs_viewcvs_homepage
                or self.type.url_cvs_viewcvs_homepage.replace('%PROJECT', self.group.name))

    # VCS
    def uses_cvs (self):  return self.type.can_use_cvs  and self.use_cvs
    def uses_svn (self):  return self.type.can_use_svn  and self.use_svn
    def uses_arch(self):  return self.type.can_use_arch and self.use_arch
    def uses_git (self):  return self.type.can_use_git  and self.use_git
    def uses_hg  (self):  return self.type.can_use_hg   and self.use_hg
    def uses_bzr (self):  return self.type.can_use_bzr  and self.use_bzr

    def get_url_cvs_browser(self):
        return (self.url_cvs_viewcvs
                or self.type.url_cvs_viewcvs.replace('%PROJECT', self.group.name))
    def get_url_svn_browser(self):
        return (self.url_svn_viewcvs
                or self.type.url_svn_viewcvs.replace('%PROJECT', self.group.name))
    def get_url_arch_browser(self):
        return (self.url_arch_viewcvs
                or self.type.url_arch_viewcvs.replace('%PROJECT', self.group.name))
    def get_url_git_browser(self):
        return (self.url_git_viewcvs
                or self.type.url_git_viewcvs.replace('%PROJECT', self.group.name))
    def get_url_hg_browser(self):
        return (self.url_hg_viewcvs
                or self.type.url_hg_viewcvs.replace('%PROJECT', self.group.name))
    def get_url_bzr_browser(self):
        return (self.url_bzr_viewcvs
                or self.type.url_bzr_viewcvs.replace('%PROJECT', 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__name', 'user__last_name', 'user__first_name',)

    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"))
    since = models.DateField(default=datetime.datetime.now, blank=True, null=True)

    # 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

    def save(self, force_insert=False, force_update=False):
        """
        Update the matching User<->Group relationship
        """
        if self.admin_flags != 'P':
            self.group.user_set.add(self.user)
        if self.admin_flags == 'P':
            self.group.user_set.remove(self.user)
        super(self.__class__, self).save(force_insert, force_update)
    def delete(self, using=None):
        self.group.user_set.remove(self.user)
        super(self.__class__, self).delete(using)

    @staticmethod
    def is_member(user, group):
        return ((user.is_superuser and user.svuserinfo.superuser_is_enabled) or
                (not user.is_anonymous()
                 and group.user_set.filter(pk=user.pk).count() > 0)
                and user.svuserinfo.status == 'A'
                and group.svgroupinfo.status == 'A')

    @staticmethod
    def is_admin(user, group):
        return ((user.is_superuser and user.svuserinfo.superuser_is_enabled) or
                Membership.is_nonsuper_admin(user, group))

    @staticmethod
    def is_nonsuper_admin(user, group):
        return (not user.is_anonymous()
                and Membership.is_member(user, group)
                and Membership.objects
                .filter(user=user, group=group, admin_flags='A').count() > 0)

    @staticmethod
    def tidy(user=None, group=None):
        """
        If using a non-Savane users&groups base, create missing
        Membership relationships.
        """
        if group is not None:
            # If using a non-Savane groups base, prepare membership metadata
            user_pks = Membership.objects.filter(group=group).values_list('user__pk', flat=True)
            missing_members = group.user_set.exclude(pk__in=user_pks)
            for member in missing_members:
                Membership(user=member, group=group, admin_flags='').save()
        
            # If a membership does not have a matching User<->Group relationship, remove it
            user_pks = group.user_set.values_list('pk', flat=True)
            invalid_memberships = Membership.objects.filter(group=group).exclude(user__in=user_pks).exclude(admin_flags='P')
            invalid_memberships.delete()
        if user is not None:
            # If using a non-Savane groups base, prepare membership metadata
            group_pks = Membership.objects.filter(user=user).values_list('group__pk', flat=True)
            missing_groups = user.groups.exclude(pk__in=group_pks)
            for group in missing_groups:
                Membership(user=member, group=group, admin_flags='').save()
        
            # If a membership does not have a matching User<->Group relationship, remove it
            group_pks = user.groups.values_list('pk', flat=True)
            invalid_memberships = Membership.objects.filter(user=user).exclude(group__in=group_pks).exclude(admin_flags='P')
            invalid_memberships.delete()

    @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)


class MailingList(models.Model):
    status_CHOICES = (
        ('0', _('Deleted')),
        ('1', _('To be created')),
        ('2', _('To be reconfigured')),
        ('5', _('Created')),
        )
    group = models.ForeignKey(auth_models.Group)
    list_name = models.CharField(max_length=255)
    is_public = models.BooleanField(default=True)
    # password
    # list_admin
    status = models.CharField(max_length=1, choices=status_CHOICES, default='1')
    description = models.CharField(max_length=255)

    def get_address(self):
        return (self.group.svgroupinfo.type.mailing_list_address.replace('%LIST', self.list_name))
    def get_url_listinfo(self):
        return (self.group.svgroupinfo.type.url_mailing_list_listinfo.replace('%LIST', self.list_name))
    def get_url_archives(self):
        return (self.group.svgroupinfo.type.url_mailing_list_archives.replace('%LIST', self.list_name))
    def get_url_archives_private(self):
        return (self.group.svgroupinfo.type.url_mailing_list_archives_private.replace('%LIST', self.list_name))
    def get_url_admin(self):
        return (self.group.svgroupinfo.type.url_mailing_list_admin.replace('%LIST', self.list_name))