Changeset ef118cf for asadb/groups


Ignore:
Timestamp:
Feb 11, 2014, 2:22:25 AM (12 years ago)
Author:
Alex Dehnert <adehnert@…>
Branches:
master, stable, stage
Children:
d4f571c
Parents:
2563230 (diff), e5c5180 (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent.
git-author:
Alex Dehnert <adehnert@…> (02/11/14 02:22:25)
git-committer:
Alex Dehnert <adehnert@…> (02/11/14 02:22:25)
Message:

Merge branch 'master' into filters

  • master: (107 commits) People lookup: Handle more name formats (ASA-#253) People lookup: specify intended format (ASA-#253) People lookup: handle round numbers (ASA-#253) People lookup: force names to lowercase (ASA-#252) Add hazing and non-discrimination links (ASA-#255) Correct description of submitted link Add missing column heading (ASA-#258) Re-wrap anti-hazing email (ASA-#256) Add recognition date to issues.csv People lookup: fix *small* groups... Add docs for setting up chosen.js Link people lookup tool from membership update page People lookup: mention the data source People lookup: Support large numbers of people People lookup: make the UI more reasonable Basic people lookup support (ASA-#186) Add pre-group-approval warnings Reword membership definition text More thoroughly purge mention of membership lists Standarize on UK-style ndashes ...

Conflicts:

asadb/groups/models.py

Location:
asadb/groups
Files:
3 added
6 edited

Legend:

Unmodified
Added
Removed
  • asadb/groups/admin.py

    rcbffe98 r89165c1  
     1import datetime
     2
     3from django.contrib import admin
     4from django.utils.translation import ugettext_lazy
     5
     6from reversion.admin import VersionAdmin
     7
    18import groups.models
    2 from django.contrib import admin
    3 from reversion.admin import VersionAdmin
     9import util.admin
    410
    511class GroupAdmin(VersionAdmin):
     
    98104
    99105class OfficeHolderAdmin(VersionAdmin):
     106    class OfficeHolderPeriodFilter(util.admin.TimePeriodFilter):
     107        start_field = 'start_time'
     108        end_field = 'end_time'
     109
     110    def expire_holders(self, request, queryset):
     111        rows_updated = queryset.update(end_time=datetime.datetime.now())
     112        if rows_updated == 1:
     113            message_bit = "1 entry was"
     114        else:
     115            message_bit = "%s entries were" % rows_updated
     116        self.message_user(request, "%s successfully expired." % message_bit)
     117    expire_holders.short_description = ugettext_lazy("Expire selected %(verbose_name_plural)s")
     118
     119    actions = ['expire_holders']
     120
    100121    list_display = (
    101122        'id',
     
    121142    list_filter = [
    122143        'role',
     144        OfficeHolderPeriodFilter,
    123145    ]
    124146admin.site.register(groups.models.OfficeHolder, OfficeHolderAdmin)
     
    181203        'last_name',
    182204        'account_class',
     205        'affiliation_basic',
     206        'loose_student',
    183207        'mutable',
    184208        'add_date',
     
    188212    list_display_links = ( 'id', 'username', )
    189213    search_fields = ( 'username', 'mit_id', 'first_name', 'last_name', 'account_class', )
     214    list_filter = (
     215        'account_class',
     216        'affiliation_basic', 'affiliation_detailed',
     217        'loose_student',
     218        'mutable',
     219    )
    190220admin.site.register(groups.models.AthenaMoiraAccount, Admin_AthenaMoiraAccount)
  • asadb/groups/diffs.py

    rafc5348 r9af1bb4  
    244244
    245245def default_active_pred():
    246     status_objs = groups.models.GroupStatus.objects.filter(slug__in=['active', 'suspended', 'nge'])
     246    status_objs = groups.models.GroupStatus.objects.filter(slug__in=['active', 'suspended', ])
    247247    status_pks = [status.pk for status in status_objs]
    248248    def pred(version, fields):
  • asadb/groups/load_people.py

    r161ce5f r89165c1  
    2626    'last_name',
    2727    'account_class',
     28    'affiliation_basic',
     29    'affiliation_detailed',
    2830]
    2931
  • asadb/groups/models.py

    r62f73df ref118cf  
    11# -*- coding: utf8 -*-
    22
    3 from django.conf import settings
    4 from django.db import models
    5 from django.core.exceptions import ValidationError
    6 from django.core.validators import RegexValidator
    7 from django.contrib.auth.models import User
    8 from django.template.defaultfilters import slugify
    9 import reversion
    10 
     3import collections
    114import datetime
    125import filecmp
     
    2013import urllib2
    2114
     15from django.conf import settings
     16from django.db import models
     17from django.db.models import Q
     18from django.core.exceptions import ValidationError
     19from django.core.validators import RegexValidator
     20from django.contrib.auth.models import User, Permission
     21from django.contrib.contenttypes.models import ContentType
     22from django.template.defaultfilters import slugify
     23
     24import reversion
     25
    2226import mit
    2327
     
    3034        )
    3135
    32 locker_validator = RegexValidator(regex=r'^[-A-Za-z0-9_.]+$', message='Enter a valid Athena locker.')
     36locker_validator = RegexValidator(regex=r'^[-A-Za-z0-9_.]+$', message='Enter a valid Athena locker. This should be the single "word" that appears in "/mit/word/" or "web.mit.edu/word/", with no slashes, spaces, etc..')
    3337
    3438class Group(models.Model):
     
    100104            if as_of == "now": as_of = datetime.datetime.now()
    101105            office_holders = office_holders.filter(start_time__lte=as_of, end_time__gte=as_of)
     106        office_holders = office_holders.order_by('role', 'person')
    102107        return office_holders
    103108
     
    112117        current_officers = OfficeHolder.current_holders.filter(person=username)
    113118        users_groups = Group.objects.filter(officeholder__in=current_officers).distinct()
     119
     120    @staticmethod
     121    def admin_groups(username, codename='admin_group'):
     122        holders = OfficeHolder.current_holders.filter_perm(codename=codename).filter(person=username)
     123        users_groups = Group.objects.filter(officeholder__in=holders).distinct()
    114124        return users_groups
    115125
     
    395405
    396406    @classmethod
     407    def getRolesGrantingPerm(cls, perm=None, model=Group, codename=None, ):
     408        """Get all OfficerRole objects granting a permission
     409
     410        Either `perm` or `codename` must be supplied, but not both. If
     411        `codename` is provided (and `perm` is None), then `perm` the
     412        permission corresponding to `model` (default: `Group`) and `codename`
     413        will be found and used."""
     414
     415        if perm is None:
     416            ct = ContentType.objects.get_for_model(model)
     417            print ct
     418            print Permission.objects.filter(content_type=ct)
     419            perm = Permission.objects.get(content_type=ct, codename=codename)
     420
     421        Q_user = Q(user_permissions=perm)
     422        Q_group = Q(groups__permissions=perm)
     423        users = User.objects.filter(Q_user|Q_group)
     424        roles = cls.objects.filter(grant_user__in=users)
     425        return roles
     426
     427    @classmethod
    397428    def retrieve(cls, slug, ):
    398429        return cls.objects.get(slug=slug)
     430
    399431reversion.register(OfficerRole)
    400432
     
    407439        )
    408440
     441    def filter_perm(self, perm=None, model=Group, codename=None, ):
     442        roles = OfficerRole.getRolesGrantingPerm(perm=perm, model=model, codename=codename)
     443        return self.get_query_set().filter(role__in=roles)
     444
    409445class OfficeHolder(models.Model):
    410446    EXPIRE_OFFSET   = datetime.timedelta(seconds=1)
    411447    END_NEVER       = datetime.datetime.max
    412448
    413     person = models.CharField(max_length=30, db_index=True, )
     449    person = models.CharField(max_length=30, db_index=True, help_text='Athena username')
    414450    role = models.ForeignKey('OfficerRole', db_index=True, )
    415451    group = models.ForeignKey('Group', db_index=True, )
     
    433469    def __repr__(self, ):
    434470        return str(self)
     471
    435472reversion.register(OfficeHolder)
    436473
     
    506543
    507544    def __str__(self, ):
    508         active = ""
    509         if not self.is_active:
    510             active = " (inactive)"
    511         return "%s%s" % (self.name, active, )
     545        return self.name
    512546
    513547    class Meta:
     
    528562    def get_query_set(self, ):
    529563        return super(AthenaMoiraAccount_ActiveManager, self).get_query_set().filter(del_date=None)
     564
     565def student_account_classes():
     566    year = datetime.datetime.now().year
     567    return ["G"] + [str(yr) for yr in range(year-5, year+10)]
    530568
    531569class AthenaMoiraAccount(models.Model):
     
    535573    last_name       = models.CharField(max_length=45)
    536574    account_class   = models.CharField(max_length=10)
     575    affiliation_basic       = models.CharField(max_length=10)
     576    affiliation_detailed    = models.CharField(max_length=40)
     577    loose_student   = models.BooleanField(default=False, help_text='Whether to use loose or strict determination of student status. Loose means that either the account class or the affiliation should indicate student status; strict means that the affiliation must be student. In general, we use strict; for some people ("secret people") directory information is suppressed and the affiliation will be misleading.')
    537578    mutable         = models.BooleanField(default=True)
    538579    add_date        = models.DateField(help_text="Date when this person was added to the dump.", )
     
    544585
    545586    def is_student(self, ):
    546         # XXX: Is this... right?
    547         return self.account_class == 'G' or self.account_class.isdigit()
     587        student_affiliation = (self.affiliation_basic == 'student')
     588        student_class = (self.account_class in student_account_classes())
     589        return student_affiliation or (student_class and self.loose_student)
     590
     591    @staticmethod
     592    def student_q():
     593        q_affiliation = Q(affiliation_basic='student')
     594        q_class = Q(account_class__in=student_account_classes())
     595        return q_affiliation | (q_class & Q(loose_student=True))
    548596
    549597    def format(self, ):
  • asadb/groups/urls.py

    r532a8e9 ra7c08e4  
    11from django.conf.urls.defaults import *
     2
     3import django.shortcuts
    24
    35import groups.views
    46import space.views
    57
     8def redirect_view(to):
     9    def _redirect_view(request, **kwargs):
     10        return django.shortcuts.redirect(to, **kwargs)
     11    return _redirect_view
     12
    613group_patterns = patterns('',
    714    url(r'^$', groups.views.GroupDetailView.as_view(), name='group-detail', ),
    815    url(r'^edit/main$', groups.views.manage_main, name='group-manage-main', ),
    9     url(r'^edit/officers$', groups.views.manage_officers, name='group-manage-officers', ),
     16    url(r'^edit/people$', groups.views.manage_officers, name='group-manage-officers', ),
     17    url(r'^edit/officers$', redirect_view('groups:group-manage-officers'), ),
    1018    url(r'^history/$', groups.views.GroupHistoryView.as_view(), name='group-manage-history', ),
    1119    url(r'^space/$', space.views.manage_access, name='group-space-access', ),
  • asadb/groups/views.py

    rd7557b8 ref118cf  
    44import csv
    55import datetime
    6 
    7 import groups.models
    86
    97from django.contrib.auth.decorators import user_passes_test, login_required, permission_required
     
    3028import django_filters
    3129
     30import groups.models
    3231from util.db_form_utils import StaticWidget
     32import util.db_filters
    3333from util.emails import email_from_template
    3434
     
    129129        for field in self.force_required:
    130130            self.fields[field].required = True
    131         self.fields['constitution_url'].help_text = mark_safe("""Please put your current constitution URL or AFS path.<br>If you don't currently know where your constitution is, put "http://mit.edu/asa/start/constitution-req.html" and draft a constitution soon.""")
     131        self.fields['constitution_url'].help_text = mark_safe("Please put your current constitution URL or AFS path.")
    132132
    133133    exec_only_fields = [
     
    221221def manage_officers_load_officers(group, ):
    222222    officers = group.officers()
    223     people = list(set([ officer.person for officer in officers ]))
     223    people = sorted(set([ officer.person for officer in officers ]))
    224224    roles  = groups.models.OfficerRole.objects.all()
    225225
     
    573573    treasurer_name = forms.CharField(max_length=50)
    574574    treasurer_kerberos = forms.CharField(min_length=3, max_length=8, )
    575     def clean_president(self, ):
     575    def clean_president_kerberos(self, ):
    576576        username = self.cleaned_data['president_kerberos']
    577577        validate_athena(username, True, )
    578578        return username
    579579
    580     def clean_treasurer(self, ):
     580    def clean_treasurer_kerberos(self, ):
    581581        username = self.cleaned_data['treasurer_kerberos']
    582582        validate_athena(username, True, )
     
    630630        self.fields['constitution_url'].required = True
    631631        self.fields['constitution_url'].help_text = "Please put a copy of your finalized constitution on a publicly-accessible website (e.g. your group's, or your own, Public folder), and link to it in the box above."
     632        self.fields['group_email'].required = True
    632633        self.fields['athena_locker'].required = True
     634        self.fields['athena_locker'].help_text = "In general, this is limited to twelve characters. You should stick to letters, numbers, and hyphens. (Underscores and dots are also acceptable, but may cause problems in some situations.)"
     635
     636        # Specifically, if the group ends up wanting to use scripts.mit.edu,
     637        # they will currently be assigned locker.scripts.mit.edu. If they try
     638        # to use foo.bar, then https://foo.bar.scripts.mit.edu/ will produce a
     639        # certificate name mismatch. Officially, underscores are not allowed in
     640        # hostnames, so foo_.scripts.mit.edu may fail with some software.
    633641
    634642    class Meta(GroupCreateForm.Meta):
     
    688696            from_email='asa-admin@mit.edu',
    689697        )
    690         # XXX: Handle this better
    691         if officer_domain != 'mit.edu' or (create_group_list and group_domain != 'mit.edu'):
    692             accounts_mail.to = ['asa-groups@mit.edu']
    693             accounts_mail.cc = ['asa-db@mit.edu']
    694             accounts_mail.subject = "ERROR: " + accounts_mail.subject
    695             accounts_mail.body = "Bad domain on officer or group list\n\n" + accounts_mail.body
    696698
    697699    else:
     
    820822    return render_to_response('groups/create/startup.html', context, context_instance=RequestContext(request), )
    821823
     824def review_group_check_warnings(group_startup, group, ):
     825    warnings = []
     826
     827    if group.name.startswith("MIT "):
     828        warnings.append('Group name starts with "MIT". Generally, we prefer "Foo, MIT" instead.')
     829    if "mit" in group.athena_locker.lower():
     830        warnings.append('Athena locker name contains "mit", which may be redundant with paths like "http://web.mit.edu/mitfoo" or "/mit/foo/".')
     831
     832    if group_startup.president_kerberos == group_startup.treasurer_kerberos:
     833        warnings.append('President matches Treasurer.')
     834    if "%s@mit.edu" % (group_startup.president_kerberos, ) in (group.officer_email, group.group_email):
     835        warnings.append('President email matches officer and/or group email.')
     836    if group.officer_email == group.group_email:
     837        warnings.append('Officer email matches group email.')
     838
     839    if '@mit.edu' not in group.officer_email or '@mit.edu' not in group.group_email:
     840        warnings.append('Officer and/or group email are non-MIT. Ensure that they are not requesting the addresses be created, and consider suggesting they use an MIT list instead.')
     841
     842    if '.' in group.athena_locker:
     843        warnings.append('Athena locker contains a ".". This is not compatible with scripts.mit.edu\'s wildcard certificate, and may cause other problems.')
     844    if '_' in group.athena_locker:
     845        warnings.append('Athena locker contains a "_". If this locker name gets used in a URL (for example, locker.scripts.mit.edu), it will technically violate the hostname specification and may not work in some clients.')
     846    if len(group.athena_locker) > 12:
     847        warnings.append('Athena locker is more than twelve characters long. In general, twelve characters is the longest Athena locker an ASA-recognized group can get.')
     848
     849    return warnings
     850
    822851@permission_required('groups.recognize_group')
    823852def recognize_normal_group(request, pk, ):
     
    836865        return render_to_response('groups/create/err.not-applying.html', context, context_instance=RequestContext(request), )
    837866
     867    context['warnings'] = review_group_check_warnings(group_startup, group)
     868
    838869    context['msg'] = ""
    839870    if request.method == 'POST':
     
    842873            group_startup.save()
    843874
    844             group.group_status = groups.models.GroupStatus.objects.get(slug='active')
     875            group.group_status = groups.models.GroupStatus.objects.get(slug='suspended')
    845876            group.constitution_url = ""
    846877            group.recognition_date = datetime.datetime.now()
    847878            group.set_updater(request.user)
     879
     880            note = groups.models.GroupNote(
     881                author=request.user.username,
     882                body="Approved group for recognition.",
     883                acl_read_group=True,
     884                acl_read_offices=True,
     885                group=group,
     886            ).save()
    848887
    849888            group.save()
     
    900939    name = django_filters.CharFilter(lookup_type='icontains', label="Name contains")
    901940    abbreviation = django_filters.CharFilter(lookup_type='iexact', label="Abbreviation is")
     941    officer_email = django_filters.CharFilter(lookup_type='icontains', label="Officers' list contains")
     942
     943    account_filter = util.db_filters.MultiNumberFilter(
     944        lookup_type='exact', label="Account number",
     945        names=('main_account_id', 'funding_account_id', ),
     946    )
    902947
    903948    class Meta:
     
    906951            'name',
    907952            'abbreviation',
     953            'officer_email',
    908954            'activity_category',
    909955            'group_class',
    910956            'group_status',
    911957            'group_funding',
     958            'account_filter',
    912959        ]
    913960
     
    945992    groups_filterset = GroupFilter(request.GET, the_groups)
    946993    the_groups = groups_filterset.qs
     994
    947995    officers = groups.models.OfficeHolder.objects.filter(start_time__lte=datetime.datetime.now(), end_time__gte=datetime.datetime.now())
    948996    officers = officers.filter(group__in=the_groups)
    949997    officers = officers.select_related(depth=1)
     998
    950999    role_slugs = ['president', 'treasurer', 'financial', 'reservation']
    9511000    roles = groups.models.OfficerRole.objects.filter(slug__in=role_slugs)
    9521001    roles = sorted(roles, key=lambda r: role_slugs.index(r.slug))
     1002
    9531003    officers_map = collections.defaultdict(lambda: collections.defaultdict(set))
    9541004    for officer in officers:
     
    9581008        role_list = []
    9591009        for role in roles:
    960             role_list.append(officers_map[group][role])
     1010            role_list.append(sorted(officers_map[group][role]))
    9611011        officers_data.append((group, role_list))
    9621012
     
    10041054        if 'pk' in self.kwargs:
    10051055            group = get_object_or_404(groups.models.Group, pk=self.kwargs['pk'])
    1006             history_entries = reversion.models.Version.objects.get_for_object(group)
     1056            history_entries = reversion.get_for_object(group)
    10071057        else:
    10081058            history_entries = reversion.models.Version.objects.all()
     
    12881338def show_nonstudent_officers(request, ):
    12891339    student_roles  = groups.models.OfficerRole.objects.filter(require_student=True, )
    1290     year = datetime.datetime.now().year
    1291     account_classes = ["G"] + [str(yr) for yr in range(year-5, year+10)]
    1292     students = groups.models.AthenaMoiraAccount.active_accounts.filter(account_class__in=account_classes)
     1340    student_q = groups.models.AthenaMoiraAccount.student_q()
     1341    students = groups.models.AthenaMoiraAccount.active_accounts.filter(student_q)
    12931342    office_holders = groups.models.OfficeHolder.current_holders.order_by('group__name', 'role', )
    12941343    office_holders = office_holders.filter(role__in=student_roles)
    12951344    office_holders = office_holders.exclude(person__in=students.values('username'))
    1296     office_holders = office_holders.select_related('group', 'role')
     1345    office_holders = office_holders.select_related('group', 'group__group_status', 'role')
    12971346
    12981347    msg = None
     
    13001349    if 'sort' in request.GET:
    13011350        if request.GET['sort'] == 'group':
    1302             office_holders = office_holders.order_by('group__name', 'role', 'person', )
     1351            office_holders = office_holders.order_by('group__name', 'group__group_status', 'role', 'person', )
     1352        elif request.GET['sort'] == 'status':
     1353            office_holders = office_holders.order_by('group__group_status', 'group__name', 'role', 'person', )
    13031354        elif request.GET['sort'] == 'role':
    1304             office_holders = office_holders.order_by('role', 'group__name', 'person', )
     1355            office_holders = office_holders.order_by('role', 'group__group_status', 'group__name', 'person', )
    13051356        elif request.GET['sort'] == 'person':
    1306             office_holders = office_holders.order_by('person', 'group__name', 'role', )
     1357            office_holders = office_holders.order_by('person', 'group__group_status', 'group__name', 'role', )
    13071358        else:
    13081359            msg = 'Unknown sort key "%s".' % (request.GET['sort'], )
Note: See TracChangeset for help on using the changeset viewer.