Example #1
0
class GroupBase(models.Model):
    """Base class for groups in Mozillians."""
    name = models.CharField(db_index=True, max_length=100,
                            unique=True, verbose_name=_lazy(u'Name'))
    url = models.SlugField(blank=True)

    objects = GroupBaseManager.from_queryset(GroupQuerySet)()

    class Meta:
        abstract = True
        ordering = ['name']

    def get_absolute_url(self):
        cls_name = self.__class__.__name__
        url_pattern = 'groups:show_{0}'.format(cls_name.lower())
        return absolutify(reverse(url_pattern, args=[self.url]))

    def clean(self):
        """Verify that name is unique in ALIAS_MODEL."""

        super(GroupBase, self).clean()
        query = self.ALIAS_MODEL.objects.filter(name=self.name)
        if self.pk:
            query = query.exclude(alias=self)
        if query.exists():
            raise ValidationError({'name': _('This name already exists.')})
        return self.name

    @classmethod
    def search(cls, query):
        query = query.lower()
        results = cls.objects.filter(aliases__name__contains=query)
        results = results.distinct()
        return results

    def save(self, *args, **kwargs):
        """Override save method."""

        self.name = self.name.lower()
        super(GroupBase, self).save()
        if not self.url:
            alias = self.ALIAS_MODEL.objects.create(name=self.name, alias=self)
            self.url = alias.url
            super(GroupBase, self).save()

    def __unicode__(self):
        return self.name

    def merge_groups(self, group_list):
        """Merge two groups."""
        for group in group_list:
            map(lambda x: self.add_member(x), group.members.all())
            group.aliases.update(alias=self)
            group.delete()

    def user_can_leave(self, userprofile):
        """Checks if a member of a group can leave."""

        curators = self.curators.all()
        return (
            # some groups don't allow leaving
            getattr(self, 'members_can_leave', True) and
            # We need at least one curator
            (curators.count() > 1 or userprofile not in curators) and
            # only makes sense to leave a group they belong to (at least pending)
            (self.has_member(userprofile=userprofile) or
             self.has_pending_member(userprofile=userprofile))
        )

    def user_can_join(self, userprofile):
        """Checks if a user can join a group."""
        return (
            # Must be vouched
            userprofile.is_vouched and
            # some groups don't allow
            (getattr(self, 'accepting_new_members', 'yes') != 'no') and
            # only makes sense to join if not already a member (full or pending)
            not (self.has_member(userprofile=userprofile) or
                 self.has_pending_member(userprofile=userprofile))
        )

    # Read-only properties so clients don't care which subclasses have some fields
    @property
    def is_visible(self):
        """Checks if a group is visible."""
        return getattr(self, 'visible', True)

    def add_member(self, userprofile):
        """Adds a method to a group."""
        self.members.add(userprofile)

    def remove_member(self, userprofile):
        """Removes a member from a group."""
        self.members.remove(userprofile)

    def has_member(self, userprofile):
        """Checks membership status."""
        return self.members.filter(user=userprofile.user).exists()

    def has_pending_member(self, userprofile):
        """Checks if a membership is pending.

        Skills have no pending members, just members
        """
        return False
Example #2
0
class Group(GroupBase):
    ALIAS_MODEL = GroupAlias

    # Has a steward taken ownership of this group?
    description = models.TextField(max_length=1024,
                                   verbose_name=_lazy(u'Description'),
                                   default='',
                                   blank=True)
    curators = models.ManyToManyField('users.UserProfile',
                                      related_name='groups_curated')
    irc_channel = models.CharField(
        max_length=63,
        verbose_name=_lazy(u'IRC Channel'),
        help_text=_lazy(
            u'An IRC channel where this group is discussed (optional).'),
        default='',
        blank=True)
    website = models.URLField(
        max_length=200,
        verbose_name=_lazy(u'Website'),
        help_text=_lazy(
            u'A URL of a web site with more information about this group (optional).'
        ),
        default='',
        blank=True)
    wiki = models.URLField(
        max_length=200,
        verbose_name=_lazy(u'Wiki'),
        help_text=_lazy(
            u'A URL of a wiki with more information about this group (optional).'
        ),
        default='',
        blank=True)
    members_can_leave = models.BooleanField(default=True)
    accepting_new_members = models.CharField(
        verbose_name=_lazy(u'Accepting new members'),
        choices=(
            ('yes', _lazy(u'Open')),
            ('by_request', _lazy(u'Reviewed')),
            ('no', _lazy(u'Closed')),
        ),
        default='yes',
        max_length=10)
    new_member_criteria = models.TextField(
        max_length=1024,
        default='',
        blank=True,
        verbose_name=_lazy(u'New Member Criteria'),
        help_text=_lazy(
            u'Specify the criteria you will use to decide whether or not '
            u'you will accept a membership request.'))
    functional_area = models.BooleanField(default=False)
    visible = models.BooleanField(
        default=True,
        help_text=_lazy(
            u'Whether group is shown on the UI (in group lists, search, etc). Mainly '
            u'intended to keep system groups like "staff" from cluttering up the '
            u'interface.'))
    max_reminder = models.IntegerField(
        default=0,
        help_text=
        (u'The max PK of pending membership requests the last time we sent the '
         u'curator a reminder'))

    terms = models.TextField(default='', verbose_name=_('Terms'), blank=True)
    invalidation_days = models.PositiveIntegerField(
        null=True,
        default=None,
        blank=True,
        verbose_name=_('Invalidation days'))
    invites = models.ManyToManyField('users.UserProfile',
                                     related_name='invites_received',
                                     through='Invite',
                                     through_fields=('group', 'redeemer'))
    invite_email_text = models.TextField(
        max_length=2048,
        default='',
        blank=True,
        help_text=_('Please enter any additional text for the '
                    'invitation email'))
    objects = GroupBaseManager.from_queryset(GroupQuerySet)()

    @classmethod
    def get_functional_areas(cls):
        """Return all visible groups that are functional areas."""
        return cls.objects.visible().filter(functional_area=True)

    @classmethod
    def get_non_functional_areas(cls, **kwargs):
        """
        Return all visible groups that are not functional areas.

        Use kwargs to apply additional filtering to the groups.
        """
        return cls.objects.visible().filter(functional_area=False, **kwargs)

    @classmethod
    def get_curated(cls):
        """Return all non-functional areas that are curated."""
        return cls.get_non_functional_areas(curators__isnull=False)

    @classmethod
    def search(cls, query):
        return super(Group, cls).search(query).visible()

    def merge_groups(self, group_list):
        for membership in GroupMembership.objects.filter(group__in=group_list):
            # add_member will never demote someone, so just add them with the current membership
            # level from the merging group and they'll end up with the highest level from
            # either group.
            self.add_member(membership.userprofile, membership.status)

        for group in group_list:
            group.aliases.update(alias=self)
            group.delete()

    def add_member(self, userprofile, status=GroupMembership.MEMBER):
        """
        Add a user to this group. Optionally specify status other than member.

        If user is already in the group with the given status, this is a no-op.

        If user is already in the group with a different status, their status will
        be updated if the change is a promotion. Otherwise, their status will not change.
        """
        defaults = dict(status=status, date_joined=now())
        membership, created = GroupMembership.objects.get_or_create(
            userprofile=userprofile, group=self, defaults=defaults)
        if created:
            if status == GroupMembership.MEMBER:
                # Joined
                # Group is functional area, we want to sent this update to Basket
                if self.functional_area:
                    update_basket_task.delay(userprofile.id)
        else:
            if membership.status != status:
                # Status changed
                # The only valid status change states are:
                # PENDING to MEMBER
                # PENDING to PENDING_TERMS
                # PENDING_TERMS to MEMBER

                old_status = membership.status
                membership.status = status
                statuses = [
                    (GroupMembership.PENDING, GroupMembership.MEMBER),
                    (GroupMembership.PENDING, GroupMembership.PENDING_TERMS),
                    (GroupMembership.PENDING_TERMS, GroupMembership.MEMBER)
                ]
                if (old_status, status) in statuses:
                    # Status changed
                    membership.save()
                    if membership.status in [
                            GroupMembership.PENDING, GroupMembership.MEMBER
                    ]:
                        if self.functional_area:
                            # Group is functional area, we want to sent this update to Basket.
                            update_basket_task.delay(userprofile.id)
                        email_membership_change.delay(self.pk,
                                                      userprofile.user.pk,
                                                      old_status, status)

    def remove_member(self, userprofile, send_email=True):
        try:
            membership = GroupMembership.objects.get(group=self,
                                                     userprofile=userprofile)
        except GroupMembership.DoesNotExist:
            return
        old_status = membership.status
        membership.delete()
        # If group is functional area, we want to sent this update to Basket
        if self.functional_area:
            update_basket_task.delay(userprofile.id)

        if old_status == GroupMembership.PENDING and send_email:
            # Request denied
            email_membership_change.delay(self.pk, userprofile.user.pk,
                                          old_status, None)
        elif old_status == GroupMembership.MEMBER and send_email:
            # Member removed
            member_removed_email.delay(self.pk, userprofile.user.pk)

        # delete the invitation to the group if exists
        Invite.objects.filter(group=self, redeemer=userprofile).delete()

    def has_member(self, userprofile):
        """
        Return True if this user is in this group with status MEMBER.
        """
        return self.groupmembership_set.filter(
            userprofile=userprofile, status=GroupMembership.MEMBER).exists()

    def has_pending_member(self, userprofile):
        """
        Return True if this user is in this group with status PENDING.
        """
        return self.groupmembership_set.filter(
            userprofile=userprofile, status=GroupMembership.PENDING).exists()
Example #3
0
class Group(GroupBase):
    """Group class."""
    ALIAS_MODEL = GroupAlias

    # Possible group types
    OPEN = u'yes'
    REVIEWED = u'by_request'
    CLOSED = u'no'

    GROUP_TYPES = (
        (OPEN, _lazy(u'Open')),
        (REVIEWED, _lazy(u'Reviewed')),
        (CLOSED, _lazy(u'Closed')),
    )

    # Has a steward taken ownership of this group?
    description = models.TextField(max_length=1024,
                                   verbose_name=_lazy(u'Description'),
                                   default='', blank=True)
    curators = models.ManyToManyField('users.UserProfile', related_name='groups_curated')
    irc_channel = models.CharField(
        max_length=63,
        verbose_name=_lazy(u'IRC Channel'),
        help_text=_lazy(u'An IRC channel where this group is discussed (optional).'),
        default='', blank=True)
    website = models.URLField(
        max_length=200,
        verbose_name=_lazy(u'Website'),
        help_text=_lazy(u'A URL of a web site with more information about this group (optional).'),
        default='', blank=True)
    wiki = models.URLField(
        max_length=200,
        verbose_name=_lazy(u'Wiki'),
        help_text=_lazy(u'A URL of a wiki with more information about this group (optional).'),
        default='', blank=True)
    members_can_leave = models.BooleanField(default=True)
    accepting_new_members = models.CharField(verbose_name=_lazy(u'Accepting new members'),
                                             choices=GROUP_TYPES,
                                             default=OPEN,
                                             max_length=10)
    new_member_criteria = models.TextField(
        max_length=1024,
        default='',
        blank=True,
        verbose_name=_lazy(u'New Member Criteria'),
        help_text=_lazy(u'Specify the criteria you will use to decide whether or not '
                        u'you will accept a membership request.'))
    functional_area = models.BooleanField(default=False)
    visible = models.BooleanField(
        default=True,
        help_text=_lazy(u'Whether group is shown on the UI (in group lists, search, etc). Mainly '
                        u'intended to keep system groups like "staff" from cluttering up the '
                        u'interface.')
    )
    max_reminder = models.IntegerField(
        default=0,
        help_text=(u'The max PK of pending membership requests the last time we sent the '
                   u'curator a reminder')
    )

    terms = models.TextField(default='', verbose_name=_('Terms'), blank=True)
    invalidation_days = models.PositiveIntegerField(null=True,
                                                    default=None,
                                                    blank=True,
                                                    verbose_name=_('Invalidation days'))
    invites = models.ManyToManyField('users.UserProfile',
                                     related_name='invites_received',
                                     through='Invite',
                                     through_fields=('group', 'redeemer'))
    invite_email_text = models.TextField(max_length=2048,
                                         default='',
                                         blank=True,
                                         help_text=_('Please enter any additional text for the '
                                                     'invitation email'))
    objects = GroupBaseManager.from_queryset(GroupQuerySet)()

    @classmethod
    def get_functional_areas(cls):
        """Return all visible groups that are functional areas."""
        return cls.objects.visible().filter(functional_area=True)

    @classmethod
    def get_non_functional_areas(cls, **kwargs):
        """
        Return all visible groups that are not functional areas.

        Use kwargs to apply additional filtering to the groups.
        """
        return cls.objects.visible().filter(functional_area=False, **kwargs)

    @classmethod
    def get_curated(cls):
        """Return all non-functional areas that are curated."""
        return cls.get_non_functional_areas(curators__isnull=False)

    @classmethod
    def search(cls, query):
        return super(Group, cls).search(query).visible()

    def merge_groups(self, group_list):
        for membership in GroupMembership.objects.filter(group__in=group_list):
            # add_member will never demote someone, so just add them with the current membership
            # level from the merging group and they'll end up with the highest level from
            # either group.
            self.add_member(membership.userprofile, membership.status)

        for group in group_list:
            group.aliases.update(alias=self)
            group.delete()

    def add_member(self, userprofile, status=GroupMembership.MEMBER):
        """
        Add a user to this group. Optionally specify status other than member.

        If user is already in the group with the given status, this is a no-op.

        If user is already in the group with a different status, their status will
        be updated if the change is a promotion. Otherwise, their status will not change.

        If the group in question is the NDA group, also add the user to the NDA newsletter.
        """
        defaults = dict(status=status, date_joined=now())
        membership, _ = GroupMembership.objects.get_or_create(userprofile=userprofile,
                                                              group=self,
                                                              defaults=defaults)
        # Remove the need_removal flag in any case
        # We have a renewal, let's save the object.
        if membership.needs_renewal:
            membership.needs_renewal = False
            membership.save()

        if membership.status != status:
            # Status changed
            # The only valid status change states are:
            # PENDING to MEMBER
            # PENDING to PENDING_TERMS
            # PENDING_TERMS to MEMBER

            old_status = membership.status
            membership.status = status
            statuses = [(GroupMembership.PENDING, GroupMembership.MEMBER),
                        (GroupMembership.PENDING, GroupMembership.PENDING_TERMS),
                        (GroupMembership.PENDING_TERMS, GroupMembership.MEMBER)]

            if (old_status, status) in statuses:
                # Status changed
                membership.save()
                if membership.status in [GroupMembership.PENDING, GroupMembership.MEMBER]:
                    email_membership_change.delay(self.pk, userprofile.user.pk, old_status, status)
                # Since there is no demotion, we can check if the new status is MEMBER and
                # subscribe the user to the NDA newsletter if the group is NDA
                if self.name == settings.NDA_GROUP and status == GroupMembership.MEMBER:
                    subscribe_user_to_basket.delay(userprofile.id,
                                                   [settings.BASKET_NDA_NEWSLETTER])

    def remove_member(self, userprofile, status=None, send_email=False):
        """Change membership status for a group.

        If user is a member of an open group, then the user is removed.

        If a user is a member of a reviewed or closed group,
        then the membership is in a pending state.
        """
        try:
            membership = GroupMembership.objects.get(group=self, userprofile=userprofile)
        except GroupMembership.DoesNotExist:
            return
        old_status = membership.status

        # If the group is of type Group.OPEN, delete membership
        # If no status is given, delete membership,
        # If the current membership is PENDING*, delete membership
        if (not status or self.accepting_new_members == Group.OPEN or
                old_status != GroupMembership.MEMBER):
            # We have either an open group or the request to join a reviewed group is denied
            # or the curator manually declined a user in a pending state.
            membership.delete()
            # delete the invitation to the group if exists
            Invite.objects.filter(group=self, redeemer=userprofile).delete()
            send_email = True

        # Group is either of Group.REVIEWED or Group.CLOSED, change membership to `status`
        else:
            # if we are here, there is a new status for our user
            membership.status = status
            membership.needs_renewal = False
            membership.save()
            send_email = True

        # If group is the NDA group, unsubscribe user from the newsletter.
        if self.name == settings.NDA_GROUP:
            unsubscribe_from_basket_task.delay(userprofile.email, [settings.BASKET_NDA_NEWSLETTER])

        if send_email:
            email_membership_change.delay(self.pk, userprofile.user.pk, old_status, status)

    def has_member(self, userprofile):
        """
        Return True if this user is in this group with status MEMBER.
        """
        return self.groupmembership_set.filter(userprofile=userprofile,
                                               status=GroupMembership.MEMBER).exists()

    def has_pending_member(self, userprofile):
        """
        Return True if this user is in this group with status PENDING or
        there is a flag marking the profile ready for a renewal
        """
        return (self.groupmembership_set.filter(userprofile=userprofile)
                                        .filter(Q(status=GroupMembership.PENDING) |
                                                Q(needs_renewal=True))).exists()
Example #4
0
class GroupBase(models.Model):
    name = models.CharField(db_index=True, max_length=50, unique=True)
    url = models.SlugField(blank=True)

    objects = GroupBaseManager()

    class Meta:
        abstract = True
        ordering = ['name']

    def clean(self):
        """Verify that name is unique in ALIAS_MODEL.

        We have to duplicate code here and in
        forms.GroupForm.clean_name due to bug
        https://code.djangoproject.com/ticket/16986. To update when we
        upgrade to Django 1.7.

        """
        super(GroupBase, self).clean()
        query = self.ALIAS_MODEL.objects.filter(name=self.name)
        if self.pk:
            query = query.exclude(alias=self)
        if query.exists():
            raise ValidationError({'name': _('This name already exists.')})
        return self.name

    @classmethod
    def search(cls, query):
        query = query.lower()
        results = cls.objects.filter(aliases__name__contains=query)
        results = results.distinct()
        return results

    def save(self, *args, **kwargs):
        self.name = self.name.lower()
        super(GroupBase, self).save()
        if not self.url:
            alias = self.ALIAS_MODEL.objects.create(name=self.name, alias=self)
            self.url = alias.url
            super(GroupBase, self).save()

    def __unicode__(self):
        return self.name

    def merge_groups(self, group_list):
        for group in group_list:
            map(lambda x: self.add_member(x),
                group.members.all())
            group.aliases.update(alias=self)
            group.delete()

    def user_can_leave(self, userprofile):
        return (
            # some groups don't allow leaving
            getattr(self, 'members_can_leave', True)
            and
            # curators cannot leave their own groups
            getattr(self, 'curator', None) != userprofile
            and
            # only makes sense to leave a group they belong to (at least pending)
            (self.has_member(userprofile=userprofile)
             or self.has_pending_member(userprofile=userprofile))
        )

    def user_can_join(self, userprofile):
        return (
            # Must be vouched
            userprofile.is_vouched
            and
            # some groups don't allow
            (getattr(self, 'accepting_new_members', 'yes') != 'no')
            and
            # only makes sense to join if not already a member (full or pending)
            not (self.has_member(userprofile=userprofile)
                 or self.has_pending_member(userprofile=userprofile))
        )

    # Read-only properties so clients don't care which subclasses have some fields
    @property
    def is_visible(self):
        return getattr(self, 'visible', True)

    def add_member(self, userprofile):
        self.members.add(userprofile)

    def remove_member(self, userprofile):
        self.members.remove(userprofile)

    def has_member(self, userprofile):
        return self.members.filter(user=userprofile.user).exists()

    def has_pending_member(self, userprofile):
        # skills have no pending members, just members
        return False