source: asadb/groups/views.py @ f612be5

stablestage
Last change on this file since f612be5 was f612be5, checked in by Alex Dehnert <adehnert@…>, 12 years ago

Reporting: Handle weird casing better when finding names

  • Property mode set to 100644
File size: 53.8 KB
Line 
1# Create your views here.
2
3import collections
4import csv
5import datetime
6
7from django.contrib.auth.decorators import user_passes_test, login_required, permission_required
8from django.contrib.contenttypes.models import ContentType
9from django.core.exceptions import PermissionDenied
10from django.views.generic import ListView, DetailView
11from django.shortcuts import render_to_response, get_object_or_404, redirect
12from django.template import RequestContext
13from django.template import Context, Template
14from django.template.loader import get_template
15from django.http import HttpResponse, Http404, HttpResponseRedirect
16from django.core.urlresolvers import reverse
17from django.core.validators import URLValidator, EmailValidator, email_re
18from django.core.mail import EmailMessage, mail_admins
19from django import forms
20from django.forms import ValidationError
21from django.db import connection
22from django.db.models import Q
23from django.utils import html
24from django.utils.safestring import mark_safe
25
26import form_utils.forms
27import reversion.models
28import django_filters
29
30import groups.models
31from util.db_form_utils import StaticWidget
32import util.db_filters
33from util.emails import email_from_template
34
35urlvalidator = URLValidator()
36emailvalidator = EmailValidator(email_re)
37
38
39
40############
41# Homepage #
42############
43
44def view_homepage(request, ):
45    users_groups = []
46    groupmsg = ""
47    has_perms = []
48    if request.user.is_authenticated():
49        username = request.user.username
50        current_officers = groups.models.OfficeHolder.current_holders.filter(person=username)
51        users_groups = groups.models.Group.objects.filter(officeholder__in=current_officers).distinct()
52
53        perms = []
54        perms.extend(groups.models.Group._meta.permissions)
55        perms.extend(groups.models.GroupNote._meta.permissions)
56        perms += (
57            ('change_group', 'Change arbitrary group information', ),
58        )
59        for perm_name, perm_desc in perms:
60            if request.user.has_perm('groups.%s' % (perm_name, )):
61                has_perms.append((perm_name, perm_desc, ))
62
63    context = {
64        'groups': users_groups,
65        'groupmsg': groupmsg,
66        'has_perms': has_perms,
67        'pagename': 'homepage',
68    }
69    return render_to_response('index.html', context, context_instance=RequestContext(request), )
70
71def view_roles_descriptions(request, ):
72    roles  = groups.models.OfficerRole.objects.all()
73    context = {
74        'pagename': 'about',
75        'roles': roles,
76    }
77    return render_to_response('about/roles_descriptions.html', context, context_instance=RequestContext(request), )
78
79################
80# Single group #
81################
82
83class GroupDetailView(DetailView):
84    context_object_name = "group"
85    model = groups.models.Group
86    def get_context_data(self, **kwargs):
87        # Call the base implementation first to get a context
88        context = super(GroupDetailView, self).get_context_data(**kwargs)
89        group = context['group']
90        context['pagename'] = "groups"
91
92        # Indicate whether this person should be able to see "private" info
93        context['viewpriv'] = self.request.user.has_perm('groups.view_group_private_info', group)
94        context['adminpriv'] = self.request.user.has_perm('groups.admin_group', group)
95        context['notes'] = group.viewable_notes(self.request.user)
96
97        # People involved in the group
98        just_roles = groups.models.OfficerRole.objects.all()
99        if context['viewpriv'] or self.request.user.has_perm('groups.view_signatories'):
100            # Can see the non-public stuff
101            pass
102        else:
103            just_roles = just_roles.filter(publicly_visible=True)
104        roles = []
105        for role in just_roles:
106            roles.append((role.display_name, role, group.officers(role=role), ))
107        context['roles'] = roles
108        context['my_roles'] = []
109        if self.request.user.is_authenticated():
110            context['my_roles'] = group.officers(person=self.request.user.username).select_related('role')
111
112        return context
113
114
115class GroupChangeMainForm(form_utils.forms.BetterModelForm):
116    def __init__(self, *args, **kwargs):
117        change_restricted = False
118        if 'change_restricted' in kwargs:
119            change_restricted = kwargs['change_restricted']
120            del kwargs['change_restricted']
121        super(GroupChangeMainForm, self).__init__(*args, **kwargs)
122        restricted_fields = list(self.nobody_fields)
123        if change_restricted:
124            restricted_fields.extend(self.exec_only_fields)
125        for field_name in restricted_fields:
126            formfield = self.fields[field_name]
127            value = getattr(self.instance, field_name)
128            StaticWidget.replace_widget(formfield, value)
129        for field in self.force_required:
130            self.fields[field].required = True
131        self.fields['constitution_url'].help_text = mark_safe("Please put your current constitution URL or AFS path.")
132
133    exec_only_fields = [
134        'name', 'abbreviation',
135        'group_status', 'group_class',
136        'group_funding', 'main_account_id', 'funding_account_id',
137    ]
138    nobody_fields = [
139        'recognition_date',
140    ]
141    force_required = [
142        'activity_category', 'description',
143        'num_undergrads', 'num_grads', 'num_community', 'num_other',
144        'website_url', 'officer_email', 'group_email',
145        'constitution_url', 'athena_locker',
146    ]
147
148
149    class Meta:
150        fieldsets = [
151            ('basic', {
152                'legend': 'Basic Information',
153                'fields': ['name', 'abbreviation', 'activity_category', 'description', ],
154            }),
155            ('size', {
156                'legend':'Membership Numbers',
157                'description':'Count each person in your group exactly once. Count only MIT students as "undergrads" or "grads". "Community" should be MIT community members who are not students, such as alums and staff.',
158                'fields': ['num_undergrads', 'num_grads', 'num_community', 'num_other',],
159            }),
160            ('contact', {
161                'legend': 'Contact Information',
162                'fields': ['website_url', 'meeting_times', 'officer_email', 'group_email', ],
163            }),
164            ('recognition', {
165                'legend': 'Recognition',
166                'fields': ['group_status', 'group_class', 'recognition_date', ],
167            }),
168            ('financial', {
169                'legend': 'Financial Information',
170                'fields': ['group_funding', 'main_account_id', 'funding_account_id', ],
171            }),
172            ('more-info', {
173                'legend': 'Additional Information',
174                'fields': ['constitution_url', 'advisor_name', 'athena_locker', ],
175            }),
176        ]
177        model = groups.models.Group
178
179@login_required
180def manage_main(request, pk, ):
181    group = get_object_or_404(groups.models.Group, pk=pk)
182
183    if not request.user.has_perm('groups.admin_group', group):
184        raise PermissionDenied
185    change_restricted = True
186    if request.user.has_perm('groups.change_group', group):
187        change_restricted = False
188
189    msg = None
190
191    initial = {}
192    if request.method == 'POST': # If the form has been submitted...
193        # A form bound to the POST data
194        form = GroupChangeMainForm(
195            request.POST, request.FILES,
196            change_restricted=change_restricted,
197            instance=group,
198        )
199
200        if form.is_valid(): # All validation rules pass
201            request_obj = form.save(commit=False)
202            request_obj.set_updater(request.user)
203            request_obj.save()
204            form.save_m2m()
205            msg = "Thanks for editing!"
206        else:
207            msg = "Validation failed. See below for details."
208
209    else:
210        form = GroupChangeMainForm(change_restricted=change_restricted, instance=group, initial=initial, ) # An unbound form
211
212    context = {
213        'group': group,
214        'form':  form,
215        'msg':   msg,
216        'pagename': "groups",
217    }
218    return render_to_response('groups/group_change_main.html', context, context_instance=RequestContext(request), )
219
220# Helper for manage_officers view
221def manage_officers_load_officers(group, ):
222    officers = group.officers()
223    people = sorted(set([ officer.person for officer in officers ]))
224    roles  = groups.models.OfficerRole.objects.all()
225
226    name_map = {}
227    for name in people:
228        name_map[name] = groups.models.AthenaMoiraAccount.try_format_by_username(name)
229    officers_map = {}
230
231    for officer in officers:
232        officers_map[(officer.person, officer.role)] = officer
233
234    return people, roles, name_map, officers_map
235
236# Helper for manager_officers view
237def manage_officers_load_accounts(max_new, people, request, msgs, ):
238    new_people = {}
239    moira_accounts = {}
240
241    for i in range(max_new):
242        key = "extra.%d" % (i, )
243        if key in request.POST and request.POST[key] != "":
244            username = request.POST[key]
245            try:
246                moira_accounts[username] = groups.models.AthenaMoiraAccount.active_accounts.get(username=username)
247                new_people[i] = username
248            except groups.models.AthenaMoiraAccount.DoesNotExist:
249                msgs.append('Athena account "%s" appears not to exist. Changes involving them have been ignored.' % (username, ))
250    for person in people:
251        try:
252            moira_accounts[person] = groups.models.AthenaMoiraAccount.active_accounts.get(username=person)
253        except groups.models.AthenaMoiraAccount.DoesNotExist:
254            msgs.append('Athena account "%s" appears not to exist. They can not be added to new roles. You should remove them from any roles they hold, if you have not already.' % (person, ))
255
256    return new_people, moira_accounts
257
258# Helper for manager_officers view
259def manage_officers_sync_role_people(
260    group, role, new_holders,
261    msgs, changes,
262    officers_map, people, moira_accounts, new_people, max_new, ):
263    """
264    Sync a set of new holders of a role with the database.
265
266    Arguments:
267    Function-specific:
268        role: the role object the changes center around
269        new_holders: The desired final set of people who should have the role
270    Output arguments --- information messages
271        msgs: warning message list. Output argument.
272        changes: list of changes made. [(verb, color, person, role)]
273    Background info arguments:
274        officers_map: (username, role) -> OfficeHolder
275        people: list of all potentially-affected people (who were previously involved)
276        moira_accounts: username -> AthenaMoiraAccount
277        new_people: potentially-affected people (who are newly involved) --- key -> username
278        max_new: highest index to use in keys for new_people
279    """
280
281    kept = 0
282    kept_not = 0
283
284    for person in people:
285        if person in new_holders:
286            if (person, role) in officers_map:
287                if person not in moira_accounts:
288                    pass # already errored above
289                elif role.require_student and not moira_accounts[person].is_student():
290                    msgs.append('Only students can have the %s role, and %s does not appear to be a student. (If this is not the case, please contact us.) You should replace this person ASAP.' % (role, person, ))
291                #changes.append(("Kept", "yellow", person, role))
292                kept += 1
293            else:
294                if person not in moira_accounts:
295                    pass # already errored above
296                elif role.require_student and not moira_accounts[person].is_student():
297                    msgs.append('Only students can have the %s role, and %s does not appear to be a student. (If this is not the case, please contact us.)' % (role, person, ))
298                else:
299                    holder = groups.models.OfficeHolder(person=person, role=role, group=group,)
300                    holder.save()
301                    changes.append(("Added", "green", person, role))
302        else:
303            if (person, role) in officers_map:
304                officers_map[(person, role)].expire()
305                changes.append(("Removed", "red", person, role))
306            else:
307                kept_not += 1
308                pass
309    for i in range(max_new):
310        if "extra.%d" % (i, ) in new_holders:
311            if i in new_people:
312                person = new_people[i]
313                assert person in moira_accounts
314                if role.require_student and not moira_accounts[person].is_student():
315                    msgs.append('Only students can have the %s role, and %s does not appear to be a student.' % (role, person, ))
316                else:
317                    holder = groups.models.OfficeHolder(person=person, role=role, group=group,)
318                    holder.save()
319                    changes.append(("Added", "green", person, role))
320
321    return kept, kept_not
322
323# Helper for manager_officers view
324def manage_officers_table_update(
325    group,
326    request, context, msgs, changes,
327    people, roles, officers_map, max_new, ):
328
329    context['kept'] = 0
330    context['kept_not'] = 0
331
332    # Fill out moira_accounts with AthenaMoiraAccount objects for relevant people
333    new_people, moira_accounts = manage_officers_load_accounts(max_new, people, request, msgs)
334
335    # Process changes
336    for role in roles:
337        key = "holders.%s" % (role.slug, )
338        new_holders = set()
339        if key in request.POST:
340            new_holders = set(request.POST.getlist(key, ))
341        if len(new_holders) > role.max_count:
342            msgs.append("You selected %d people for %s; only %d are allowed. No changes to %s have been carried out in this update." %
343                (len(new_holders), role.display_name, role.max_count, role.display_name, )
344            )
345        else:
346            kept_delta, kept_not_delta = manage_officers_sync_role_people(
347                group, role, new_holders,   # input arguments
348                msgs, changes,              # output arguments
349                officers_map, people, moira_accounts,   # ~background data
350                new_people, max_new,                    # new people data
351            )
352            context['kept'] += kept_delta
353            context['kept_not'] += kept_not_delta
354
355
356class OfficersBulkManageForm(forms.Form):
357    mode_choices = [
358        ('add', 'Add new people', ),
359        ('remove', 'Remove old people', ),
360        ('sync', 'Set people to list provided', ),
361    ]
362    mode_help = '"Set people to list provided" will add people not listed in the grid above, and remove people not listed in the textbox below. You must always specify at least one username, and thus cannot use "Set people" to remove all people.'
363    mode = forms.ChoiceField(choices=mode_choices, help_text=mode_help, )
364    role = forms.ChoiceField(initial='office-access', )
365    people = forms.CharField(
366        help_text='Usernames of people, one per line.',
367        widget=forms.Textarea,
368    )
369
370    def __init__(self, *args, **kwargs):
371        self._roles = kwargs['roles']
372        del kwargs['roles']
373        super(OfficersBulkManageForm, self).__init__(*args, **kwargs)
374        role_choices = [ (role.slug, role.display_name) for role in self._roles ]
375        self.fields['role'].choices = role_choices
376
377    def get_role(self, ):
378        role_slug = self.cleaned_data['role']
379        for role in self._roles:
380            if role.slug == role_slug:
381                return role
382        raise groups.OfficerRole.DoesNotExist
383
384def manage_officers_bulk_update(
385        group, bulk_form,
386        msgs, changes,
387        officers_map, ):
388
389    # Load parameters
390    mode = bulk_form.cleaned_data['mode']
391    role = bulk_form.get_role()
392    people_lst = bulk_form.cleaned_data['people'].split('\n')
393    people_set = set([p.strip() for p in people_lst])
394    if '' in people_set: people_set.remove('')
395
396    # Fill out moira_accounts
397    moira_accounts = {}
398    for username in people_set:
399        try:
400            moira_accounts[username] = groups.models.AthenaMoiraAccount.active_accounts.get(username=username)
401        except groups.models.AthenaMoiraAccount.DoesNotExist:
402            msgs.append('Athena account "%s" appears not to exist. Changes involving them have been ignored.' % (username, ))
403
404    # Find our target sets
405    cur_holders = [user for user, map_role in officers_map if role == map_role]
406    people = people_set.union(cur_holders)
407    if mode == 'add':
408        new_holders = people
409    elif mode == 'remove':
410        new_holders = people-people_set
411    elif mode == 'sync':
412        new_holders = people_set
413    else:
414        raise NotImplementedError("Unknown operation '%s'" % (mode, ))
415
416    # Make changes
417    if len(new_holders) <= role.max_count:
418        new_people = dict()
419        max_new = 0
420        manage_officers_sync_role_people(
421            group, role, new_holders,
422            msgs, changes,
423            officers_map, people, moira_accounts, new_people, max_new,
424        )
425    else:
426        too_many_tmpl = "You selected %d people for %s; only %d are allowed. No changes have been made in this update."
427        error = too_many_tmpl % (len(new_holders), role.display_name, role.max_count, )
428        msgs.append(error)
429
430@login_required
431def manage_officers(request, pk, ):
432    group = get_object_or_404(groups.models.Group, pk=pk)
433
434    if not request.user.has_perm('groups.admin_group', group):
435        raise PermissionDenied
436
437    people, roles, name_map, officers_map = manage_officers_load_officers(group)
438
439    max_new = 4
440    msgs = []
441    changes = []
442
443    context = {
444        'group': group,
445        'roles': roles,
446        'people': people,
447        'changes':   changes,
448        'msgs': msgs,
449    }
450
451    if request.method == 'POST' and 'opt-mode' in request.POST: # If the form has been submitted
452        edited = True
453
454        # Do the changes
455        if request.POST['opt-mode'] == 'table':
456            context['bulk_form'] = OfficersBulkManageForm(roles=roles, )
457            manage_officers_table_update(
458                group,
459                request, context, msgs, changes,
460                people, roles, officers_map, max_new,
461            )
462        elif request.POST['opt-mode'] == 'bulk':
463            bulk_form = OfficersBulkManageForm(request.POST, roles=roles, )
464            context['bulk_form'] = bulk_form
465            if bulk_form.is_valid():
466                manage_officers_bulk_update(
467                    group, bulk_form,
468                    msgs, changes,
469                    officers_map, )
470        else:
471            raise NotImplementedError("Update mode must be table or bulk, was '%s'" % (request.POST['opt-mode'], ))
472
473        # mark as changed and reload the data
474        if changes:
475            group.set_updater(request.user)
476            group.save()
477        people, roles, name_map, officers_map = manage_officers_load_officers(group)
478    else:
479        context['bulk_form'] = OfficersBulkManageForm(roles=roles, )
480
481    officers_data = []
482    for person in people:
483        role_list = []
484        for role in roles:
485            if (person, role) in officers_map:
486                role_list.append((role, True))
487            else:
488                role_list.append((role, False))
489        officers_data.append((False, person, name_map[person], role_list))
490    null_role_list = [(role, False) for role in roles]
491    for i in range(max_new):
492        officers_data.append((True, "extra.%d" % (i, ), "", null_role_list))
493    context['officers'] = officers_data
494    context['pagename'] = "groups"
495
496    return render_to_response('groups/group_change_officers.html', context, context_instance=RequestContext(request), )
497
498
499
500##################
501# ACCOUNT LOOKUP #
502##################
503
504class AccountLookupForm(forms.Form):
505    account_number = forms.IntegerField()
506    username = forms.CharField(help_text="Athena username of person to check")
507
508def account_lookup(request, ):
509    msg = None
510    msg_type = ""
511    account_number = None
512    username = None
513    group = None
514    office_holders = []
515
516    visible_roles  = groups.models.OfficerRole.objects.filter(publicly_visible=True)
517
518    initial = {}
519
520    if 'search' in request.GET: # If the form has been submitted...
521        # A form bound to the POST data
522        form = AccountLookupForm(request.GET)
523
524        if form.is_valid(): # All validation rules pass
525            account_number = form.cleaned_data['account_number']
526            username = form.cleaned_data['username']
527            account_q = Q(main_account_id=account_number) | Q(funding_account_id=account_number)
528            try:
529                group = groups.models.Group.objects.get(account_q)
530                office_holders = group.officers(person=username)
531                office_holders = office_holders.filter(role__in=visible_roles)
532            except groups.models.Group.DoesNotExist:
533                msg = "Group not found"
534                msg_type = "error"
535
536    else:
537        form = AccountLookupForm()
538
539    context = {
540        'username':     username,
541        'account_number': account_number,
542        'group':        group,
543        'office_holders': office_holders,
544        'form':         form,
545        'msg':          msg,
546        'msg_type':     msg_type,
547        'visible_roles':    visible_roles,
548    }
549    return render_to_response('groups/account_lookup.html', context, context_instance=RequestContext(request), )
550
551
552
553##################
554# GROUP CREATION #
555##################
556
557def validate_athena(username, student=False, ):
558    try:
559        person = groups.models.AthenaMoiraAccount.active_accounts.get(username=username)
560        if student and not person.is_student():
561            raise ValidationError('This must be a current student.')
562    except groups.models.AthenaMoiraAccount.DoesNotExist:
563        raise ValidationError('This must be a valid Athena username.')
564
565
566class GroupCreateForm(form_utils.forms.BetterModelForm):
567    create_officer_list = forms.BooleanField(required=False)
568    create_group_list = forms.BooleanField(required=False)
569    create_athena_locker = forms.BooleanField(required=False)
570
571    president_name = forms.CharField(max_length=50, )
572    president_kerberos = forms.CharField(min_length=3, max_length=8, )
573    treasurer_name = forms.CharField(max_length=50)
574    treasurer_kerberos = forms.CharField(min_length=3, max_length=8, )
575    def clean_president_kerberos(self, ):
576        username = self.cleaned_data['president_kerberos']
577        validate_athena(username, True, )
578        return username
579
580    def clean_treasurer_kerberos(self, ):
581        username = self.cleaned_data['treasurer_kerberos']
582        validate_athena(username, True, )
583        return username
584
585    class Meta:
586        fieldsets = [
587            ('basic', {
588                'legend': 'Basic Information',
589                'fields': ['name', 'abbreviation', 'description', ],
590            }),
591            ('officers', {
592                'legend': 'Officers',
593                'fields': ['president_name', 'president_kerberos', 'treasurer_name', 'treasurer_kerberos', ],
594            }),
595            ('type', {
596                'legend': 'Type',
597                'fields': ['activity_category', 'group_class', 'group_funding', ],
598            }),
599            ('technical', {
600                'legend': 'Technical Information',
601                'fields': [
602                    'officer_email', 'create_officer_list',
603                    'group_email', 'create_group_list',
604                    'athena_locker', 'create_athena_locker',
605                ],
606            }),
607            ('financial', {
608                'legend': 'Financial Information',
609                'fields': ['main_account_id', 'funding_account_id', ],
610            }),
611            ('constitution', {
612                'legend': 'Constitution',
613                'fields': ['constitution_url', ],
614            }),
615        ]
616        model = groups.models.Group
617
618
619class GroupCreateNgeForm(GroupCreateForm):
620    def __init__(self, *args, **kwargs):
621        super(GroupCreateNgeForm, self).__init__(*args, **kwargs)
622        self.fields['treasurer_name'].required = False
623        self.fields['treasurer_kerberos'].required = False
624
625
626class GroupCreateStartupForm(GroupCreateForm):
627    def __init__(self, *args, **kwargs):
628        super(GroupCreateStartupForm, self).__init__(*args, **kwargs)
629        self.fields['activity_category'].required = True
630        self.fields['constitution_url'].required = True
631        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
633        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.
641
642    class Meta(GroupCreateForm.Meta):
643        fieldsets = filter(
644            lambda fieldset: fieldset[0] not in ['financial', ],
645            GroupCreateForm.Meta.fieldsets
646        )
647
648def create_group_get_emails(group, group_startup, officer_emails, ):
649    # Figure out all the accounts mail parameters
650    accounts_count = 0
651    create_officer_list = False
652    if group_startup.create_officer_list and group.officer_email:
653        create_officer_list = True
654        accounts_count += 1
655    create_group_list = False
656    if group_startup.create_group_list and group.group_email:
657        create_group_list = True
658        accounts_count += 1
659    create_athena_locker = False
660    if group_startup.create_athena_locker and group.athena_locker:
661        create_athena_locker = True
662        accounts_count += 1
663    officer_list, _, officer_domain = group.officer_email.partition('@')
664    group_list, _, group_domain = group.group_email.partition('@')
665
666    # Fill out the Context
667    mail_context = Context({
668        'group': group,
669        'group_startup': group_startup,
670        'create_officer_list': create_officer_list,
671        'create_group_list': create_group_list,
672        'create_athena_locker': create_athena_locker,
673        'officer_list': officer_list,
674        'group_list': group_list,
675        'officer_emails': officer_emails,
676    })
677
678    # Welcome mail
679    welcome_mail = email_from_template(
680        tmpl='groups/diffs/new-group-announce.txt',
681        context=mail_context,
682        subject='ASA Group Recognition: %s' % (group.name, ),
683        to=officer_emails,
684        cc=['asa-new-group-announce@mit.edu'],
685        from_email='asa-exec@mit.edu',
686    )
687
688    # Accounts mail
689    if accounts_count > 0:
690        accounts_mail = email_from_template(
691            tmpl='groups/diffs/new-group-accounts.txt',
692            context=mail_context,
693            subject='New Student Activity: %s' % (group.name, ),
694            to=['accounts@mit.edu'],
695            cc=officer_emails+['asa-admin@mit.edu'],
696            from_email='asa-admin@mit.edu',
697        )
698
699    else:
700        accounts_mail = None
701    return welcome_mail, accounts_mail
702
703def create_group_officers(group, formdata, save=True, ):
704    officer_emails = [ ]
705    for officer in ('president', 'treasurer', ):
706        username = formdata[officer+'_kerberos']
707        if username:
708            if save: groups.models.OfficeHolder(
709                person=username,
710                role=groups.models.OfficerRole.objects.get(slug=officer),
711                group=group,
712            ).save()
713            officer_emails.append('%s@mit.edu' % (formdata[officer+'_kerberos'], ))
714    return officer_emails
715
716@permission_required('groups.recognize_nge')
717def recognize_nge(request, ):
718    msg = None
719
720    initial = {
721        'create_officer_list': False,
722        'create_group_list': False,
723        'create_athena_locker': True,
724    }
725    group = groups.models.Group()
726    group.group_status = groups.models.GroupStatus.objects.get(slug='nge', )
727    group.recognition_date  = datetime.datetime.now()
728    if request.method == 'POST': # If the form has been submitted...
729        # A form bound to the POST data
730        form = GroupCreateNgeForm(
731            request.POST, request.FILES,
732            initial=initial,
733            instance=group,
734        )
735
736        if form.is_valid(): # All validation rules pass
737            group.set_updater(request.user)
738            form.save()
739            officer_emails = create_group_officers(group, form.cleaned_data, save=True, )
740
741            return redirect(reverse('groups:group-detail', args=[group.pk]))
742        else:
743            msg = "Validation failed. See below for details."
744
745    else:
746        form = GroupCreateNgeForm(initial=initial, instance=group, ) # An unbound form
747
748    context = {
749        'form':  form,
750        'msg':   msg,
751        'pagename':   'groups',
752    }
753    return render_to_response('groups/create/nge.html', context, context_instance=RequestContext(request), )
754
755@login_required
756def startup_form(request, ):
757    msg = None
758
759    initial = {
760        'create_officer_list': True,
761        'create_group_list': True,
762        'create_athena_locker': True,
763    }
764    group = groups.models.Group()
765    group.group_status = groups.models.GroupStatus.objects.get(slug='applying', )
766    group.recognition_date  = datetime.datetime.now()
767    if request.method == 'POST': # If the form has been submitted...
768        # A form bound to the POST data
769        form = GroupCreateStartupForm(
770            request.POST, request.FILES,
771            initial=initial,
772            instance=group,
773        )
774
775        if form.is_valid(): # All validation rules pass
776            group.set_updater(request.user)
777            form.save()
778
779            group_startup = groups.models.GroupStartup()
780            group_startup.group = group
781            group_startup.stage = groups.models.GROUP_STARTUP_STAGE_SUBMITTED
782            group_startup.submitter = request.user.username
783
784            group_startup.create_officer_list = form.cleaned_data['create_officer_list']
785            group_startup.create_group_list = form.cleaned_data['create_group_list']
786            group_startup.create_athena_locker = form.cleaned_data['create_athena_locker']
787
788            group_startup.president_name = form.cleaned_data['president_name']
789            group_startup.president_kerberos = form.cleaned_data['president_kerberos']
790            group_startup.treasurer_name = form.cleaned_data['treasurer_name']
791            group_startup.treasurer_kerberos = form.cleaned_data['treasurer_kerberos']
792
793            group_startup.save()
794
795            context = {
796                'group':            group,
797                'group_startup':    group_startup,
798                'pagename':         'groups',
799            }
800
801            email_from_template(
802                tmpl='groups/create/startup-submitted-email.txt',
803                context=context,
804                subject='ASA Startup Application: %s' % (group.name, ),
805                to=[request.user.email] + create_group_officers(group, form.cleaned_data, save=False, ),
806                cc=['asa-groups@mit.edu'],
807                from_email='asa-groups@mit.edu',
808            ).send()
809
810            return render_to_response('groups/create/startup_thanks.html', context, context_instance=RequestContext(request), )
811        else:
812            msg = "Validation failed. See below for details."
813
814    else:
815        form = GroupCreateStartupForm(initial=initial, instance=group, ) # An unbound form
816
817    context = {
818        'form':  form,
819        'msg':   msg,
820        'pagename':   'groups',
821    }
822    return render_to_response('groups/create/startup.html', context, context_instance=RequestContext(request), )
823
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
851@permission_required('groups.recognize_group')
852def recognize_normal_group(request, pk, ):
853    group_startup = get_object_or_404(groups.models.GroupStartup, pk=pk, )
854    group = group_startup.group
855
856    context = {
857        'startup': group_startup,
858        'group': group,
859        'pagename' : 'groups',
860    }
861
862    if group.group_status.slug != 'applying':
863        return render_to_response('groups/create/err.not-applying.html', context, context_instance=RequestContext(request), )
864    if group_startup.stage != groups.models.GROUP_STARTUP_STAGE_SUBMITTED:
865        return render_to_response('groups/create/err.not-applying.html', context, context_instance=RequestContext(request), )
866
867    context['warnings'] = review_group_check_warnings(group_startup, group)
868
869    context['msg'] = ""
870    if request.method == 'POST':
871        if 'approve' in request.POST:
872            group_startup.stage = groups.models.GROUP_STARTUP_STAGE_APPROVED
873            group_startup.save()
874
875            group.group_status = groups.models.GroupStatus.objects.get(slug='suspended')
876            group.constitution_url = ""
877            group.recognition_date = datetime.datetime.now()
878            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()
887
888            group.save()
889            officer_emails = create_group_officers(group, group_startup.__dict__, )
890            welcome_mail, accounts_mail = create_group_get_emails(group, group_startup, officer_emails, )
891            welcome_mail.send()
892            if accounts_mail:
893                accounts_mail.send()
894            context['msg'] = 'Group approved.'
895            context['msg_type'] = 'info'
896        elif 'reject' in request.POST:
897            group_startup.stage = groups.models.GROUP_STARTUP_STAGE_REJECTED
898            group_startup.save()
899            group.group_status = groups.models.GroupStatus.objects.get(slug='derecognized')
900            group.save()
901            note = groups.models.GroupNote(
902                author=request.user.username,
903                body="Group rejected during recognition process.",
904                acl_read_group=True,
905                acl_read_offices=True,
906                group=group,
907            ).save()
908            context['msg'] = 'Group rejected.'
909            context['msg_type'] = 'info'
910        else:
911            context['disp_form'] = True
912    else:
913        context['disp_form'] = True
914
915    return render_to_response('groups/create/startup_review.html', context, context_instance=RequestContext(request), )
916
917class GroupStartupListView(ListView):
918    model = groups.models.GroupStartup
919    template_object_name = 'startup'
920
921    def get_queryset(self, ):
922        qs = super(GroupStartupListView, self).get_queryset()
923        qs = qs.filter(stage=groups.models.GROUP_STARTUP_STAGE_SUBMITTED)
924        qs = qs.select_related('group')
925        return qs
926
927    def get_context_data(self, **kwargs):
928        context = super(GroupStartupListView, self).get_context_data(**kwargs)
929        context['pagename'] = 'groups'
930        return context
931
932
933
934##################
935# Multiple group #
936##################
937
938class GroupFilter(django_filters.FilterSet):
939    name = django_filters.CharFilter(lookup_type='icontains', label="Name contains")
940    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    )
947
948    class Meta:
949        model = groups.models.Group
950        fields = [
951            'name',
952            'abbreviation',
953            'officer_email',
954            'activity_category',
955            'group_class',
956            'group_status',
957            'group_funding',
958            'account_filter',
959        ]
960
961    def __init__(self, data=None, *args, **kwargs):
962        if not data: data = None
963        super(GroupFilter, self).__init__(data, *args, **kwargs)
964        active_pk = groups.models.GroupStatus.objects.get(slug='active').pk
965        self.form.initial['group_status'] = active_pk
966
967
968class GroupListView(ListView):
969    model = groups.models.Group
970    template_object_name = 'group'
971
972    def get(self, *args, **kwargs):
973        qs = super(GroupListView, self).get_queryset()
974        self.filterset = GroupFilter(self.request.GET, qs)
975        return super(GroupListView, self).get(*args, **kwargs)
976
977    def get_queryset(self, ):
978        qs = self.filterset.qs
979        return qs
980
981    def get_context_data(self, **kwargs):
982        context = super(GroupListView, self).get_context_data(**kwargs)
983        # Add in the publisher
984        context['pagename'] = 'groups'
985        context['filter'] = self.filterset
986        return context
987
988
989@permission_required('groups.view_signatories')
990def view_signatories(request, ):
991    the_groups = groups.models.Group.objects.all()
992    groups_filterset = GroupFilter(request.GET, the_groups)
993    the_groups = groups_filterset.qs
994
995    officers = groups.models.OfficeHolder.objects.filter(start_time__lte=datetime.datetime.now(), end_time__gte=datetime.datetime.now())
996    officers = officers.filter(group__in=the_groups)
997    officers = officers.select_related(depth=1)
998
999    role_slugs = ['president', 'treasurer', 'financial', 'reservation']
1000    roles = groups.models.OfficerRole.objects.filter(slug__in=role_slugs)
1001    roles = sorted(roles, key=lambda r: role_slugs.index(r.slug))
1002
1003    officers_map = collections.defaultdict(lambda: collections.defaultdict(set))
1004    for officer in officers:
1005        officers_map[officer.group][officer.role].add(officer.person)
1006    officers_data = []
1007    for group in the_groups:
1008        role_list = []
1009        for role in roles:
1010            role_list.append(sorted(officers_map[group][role]))
1011        officers_data.append((group, role_list))
1012
1013    context = {
1014        'roles': roles,
1015        'officers': officers_data,
1016        'filter': groups_filterset,
1017        'pagename': 'groups',
1018    }
1019    return render_to_response('groups/groups_signatories.html', context, context_instance=RequestContext(request), )
1020
1021def search_groups(request, ):
1022    the_groups = groups.models.Group.objects.all()
1023    groups_filterset = GroupFilter(request.GET, the_groups)
1024
1025    dest = None
1026    if 'signatories' in request.GET:
1027        dest = reverse('groups:signatories')
1028        print dest
1029    elif 'group-goto' in request.GET:
1030        if len(groups_filterset.qs) == 1:
1031            group = groups_filterset.qs[0]
1032            return redirect(reverse('groups:group-detail', kwargs={'pk':group.pk}))
1033        else:
1034            dest = reverse('groups:list')
1035    elif 'group-list' in request.GET:
1036        dest = reverse('groups:list')
1037
1038    if dest:
1039        return redirect(dest + "?" + request.META['QUERY_STRING'])
1040    else:
1041        context = {
1042            'filter': groups_filterset,
1043            'pagename': 'groups',
1044        }
1045        return render_to_response('groups/group_search.html', context, context_instance=RequestContext(request), )
1046
1047
1048class GroupHistoryView(ListView):
1049    context_object_name = "version_list"
1050    template_name = "groups/group_version.html"
1051
1052    def get_queryset(self):
1053        history_entries = None
1054        if 'pk' in self.kwargs:
1055            group = get_object_or_404(groups.models.Group, pk=self.kwargs['pk'])
1056            history_entries = reversion.get_for_object(group)
1057        else:
1058            history_entries = reversion.models.Version.objects.all()
1059            group_content_type = ContentType.objects.get_for_model(groups.models.Group)
1060            history_entries = history_entries.filter(content_type=group_content_type)
1061        length = len(history_entries)
1062        if length > 150:
1063            history_entries = history_entries[length-100:]
1064        return history_entries
1065
1066    def get_context_data(self, **kwargs):
1067        context = super(GroupHistoryView, self).get_context_data(**kwargs)
1068        if 'pk' in self.kwargs:
1069            group = get_object_or_404(groups.models.Group, pk=self.kwargs['pk'])
1070            context['title'] = "History for %s" % (group.name, )
1071            context['adminpriv'] = self.request.user.has_perm('groups.admin_group', group)
1072            context['group'] = group
1073        else:
1074            context['title'] = "Recent Changes"
1075        context['pagename'] = 'groups'
1076        return context
1077
1078
1079@permission_required('groups.view_group_private_info')
1080def downloaded_constitutions(request, ):
1081    constitutions = groups.models.GroupConstitution.objects
1082    constitutions = constitutions.order_by('failure_reason', 'status_msg', 'failure_date', 'group__name', ).select_related('group', 'group__group_status', )
1083    failures = collections.defaultdict(list)
1084    successes = collections.defaultdict(list)
1085    for const in constitutions:
1086        if const.failure_reason:
1087            failures[const.failure_reason].append(const)
1088        else:
1089            successes[const.status_msg].append(const)
1090    context = {}
1091    context['failures']  = sorted(failures.items(),  key=lambda x: x[0])
1092    context['successes'] = sorted(successes.items(), key=lambda x: x[0])
1093    context['pagename'] = 'groups'
1094    return render_to_response('groups/groups_constitutions.html', context, context_instance=RequestContext(request), )
1095
1096
1097@permission_required('groups.view_group_private_info')
1098def downloaded_constitutions_csv(request, ):
1099    active_groups = groups.models.Group.active_groups.all()
1100    constitutions = groups.models.GroupConstitution.objects.filter(group__in=active_groups)
1101    constitutions = constitutions.order_by('failure_reason', 'status_msg', 'failure_date', 'group__name', ).select_related('group', 'group__group_status')
1102
1103    response = HttpResponse(mimetype='text/csv')
1104    writer = csv.writer(response)
1105
1106    writer.writerow([
1107        'failure_date',
1108        'status_msg',
1109        'name', 'id', 'group_status', 'email',
1110        'constitution_url',
1111    ])
1112    for const in constitutions:
1113        writer.writerow([
1114            const.failure_date,
1115            const.status_msg.strip(),
1116            const.group.name,
1117            const.group.pk,
1118            const.group.group_status.slug,
1119            const.group.officer_email,
1120            const.source_url,
1121        ])
1122    return response
1123
1124
1125
1126#######################
1127# REPORTING COMPONENT #
1128#######################
1129
1130name_formats = collections.OrderedDict()
1131name_formats['username']    = ('username',                      False,  '%(user)s')
1132name_formats['email']       = ('username@mit.edu',              False,  '%(user)s@mit.edu')
1133name_formats['name']        = ('First Last',                    True,   '%(first)s %(last)s')
1134name_formats['name-email']  = ('First Last <username@mit.edu>', True,   '%(first)s %(last)s <%(user)s@mit.edu>')
1135
1136class ReportingForm(form_utils.forms.BetterForm):
1137    special_filters = forms.fields.MultipleChoiceField(
1138        choices=[],
1139        widget=forms.SelectMultiple(attrs={'size': 10}),
1140        validators=[groups.models.filter_registry.validate_filter_slug],
1141        required=False,
1142    )
1143
1144    basic_fields_choices = groups.models.Group.reporting_fields()
1145    basic_fields_labels = dict(basic_fields_choices) # name -> verbose_name
1146    basic_fields = forms.fields.MultipleChoiceField(
1147        choices=basic_fields_choices,
1148        widget=forms.CheckboxSelectMultiple,
1149        initial = ['id', 'name'],
1150    )
1151
1152    people_fields = forms.models.ModelMultipleChoiceField(
1153        queryset=groups.models.OfficerRole.objects.all(),
1154        widget=forms.CheckboxSelectMultiple,
1155        required=False,
1156    )
1157    name_format = forms.ChoiceField(
1158        help_text='How to format the names of the President, Treasurer, etc., if displayed',
1159        choices=[(k, v[0]) for k,v in name_formats.items()],
1160        initial='username',
1161        required=True,
1162    )
1163
1164    special_fields_choices = (
1165        ('option_entry', '<option> entry', ),
1166    )
1167    special_fields = forms.fields.MultipleChoiceField(
1168        choices=special_fields_choices,
1169        widget=forms.CheckboxSelectMultiple,
1170        required=False,
1171    )
1172
1173    _format_choices = [
1174        ('html/inline',     "Web (HTML)", ),
1175        ('csv/inline',      "Spreadsheet (CSV) --- in browser", ),
1176        ('csv/download',    "Spreadsheet (CSV) --- download", ),
1177    ]
1178    output_format = forms.fields.ChoiceField(choices=_format_choices, widget=forms.RadioSelect, initial='html/inline')
1179
1180    class Meta:
1181        fieldsets = [
1182            ('filter', {
1183                'legend': 'Filter Groups',
1184                'fields': [
1185                    'name', 'abbreviation',
1186                    'activity_category', 'group_class', 'group_status', 'group_funding',
1187                    'special_filters',
1188                ],
1189            }),
1190            ('fields', {
1191                'legend': 'Data to display',
1192                'fields': ['basic_fields', 'people_fields', 'name_format', 'special_fields', ],
1193            }),
1194            ('final', {
1195                'legend': 'Final options',
1196                'fields': ['o', 'output_format', ],
1197            }),
1198        ]
1199
1200    def __init__(self, *args, **kwargs):
1201        super(ReportingForm, self).__init__(*args, **kwargs)
1202
1203        registry = groups.models.filter_registry
1204        self.fields['special_filters'].choices = registry.get_choices()
1205
1206class GroupReportingFilter(GroupFilter):
1207    class Meta(GroupFilter.Meta):
1208        form = ReportingForm
1209        order_by = True # we customize the field, so the value needs to be true-like but doesn't matter otherwise
1210
1211    def get_ordering_field(self):
1212        return forms.ChoiceField(label="Ordering", required=False, choices=ReportingForm.basic_fields_choices)
1213
1214    def __init__(self, data=None, *args, **kwargs):
1215        super(GroupReportingFilter, self).__init__(data, *args, **kwargs)
1216
1217def format_id(pk):
1218    url = reverse('groups:group-detail', kwargs={'pk':pk})
1219    return mark_safe("<a href='%s'>%d</a>" % (url, pk))
1220
1221def format_url(url):
1222    try:
1223        urlvalidator(url)
1224    except ValidationError:
1225        return url
1226    else:
1227        escaped = html.escape(url)
1228        return mark_safe("<a href='%s'>%s</a>" % (escaped, escaped))
1229
1230def format_email(email):
1231    try:
1232        emailvalidator(email)
1233    except ValidationError:
1234        return email
1235    else:
1236        escaped = html.escape(email)
1237        return mark_safe("<a href='mailto:%s'>%s</a>" % (escaped, escaped))
1238
1239def format_option_entry(group):
1240    name = html.escape(group.name)
1241    return '<option value="%s">%s</option>' % (name, name, )
1242
1243reporting_html_formatters = {
1244    'id': format_id,
1245    'website_url': format_url,
1246    'constitution_url': format_url,
1247    'group_email': format_email,
1248    'officer_email': format_email,
1249}
1250
1251@permission_required('groups.view_group_private_info')
1252def reporting(request, ):
1253    the_groups = groups.models.Group.objects.all()
1254    groups_filterset = GroupReportingFilter(request.GET, the_groups)
1255    form = groups_filterset.form
1256
1257    col_labels = []
1258    report_groups = []
1259    run_report = 'go' in request.GET and form.is_valid()
1260    if run_report:
1261        basic_fields = form.cleaned_data['basic_fields']
1262        output_format, output_disposition = form.cleaned_data['output_format'].split('/')
1263        col_labels = [form.basic_fields_labels[field] for field in basic_fields]
1264
1265        # Set up query
1266        qs = groups_filterset.qs
1267        for fltr_slug in form.cleaned_data['special_filters']:
1268            fltr = groups.models.filter_registry.get(fltr_slug)
1269            qs = qs.filter(pk__in=fltr.queryset())
1270
1271        # Prefetch foreign keys
1272        prefetch_fields = groups.models.Group.reporting_prefetch()
1273        prefetch_fields = prefetch_fields.intersection(basic_fields)
1274        if prefetch_fields:
1275            qs = qs.select_related(*list(prefetch_fields))
1276
1277        # Set up people
1278        people_fields = form.cleaned_data['people_fields']
1279        people_data = groups.models.OfficeHolder.current_holders.filter(group__in=qs, role__in=people_fields)
1280        # Group.pk -> (OfficerRole.pk -> set(username))
1281        people_map = collections.defaultdict(lambda: collections.defaultdict(set))
1282        for holder in people_data:
1283            people_map[holder.group_id][holder.role_id].add(holder.person)
1284        for field in people_fields:
1285            col_labels.append(field.display_name)
1286        # Figure out the format, and if necessary fetch human names
1287        nf_slug = form.cleaned_data['name_format']
1288        nf_label, nf_need_name, nf_fmt = name_formats[nf_slug]
1289        name_map = collections.defaultdict(lambda: ('???', '???'))
1290        if nf_need_name:
1291            people_usernames = [p.person.lower() for p in people_data]
1292            name_data = groups.models.AthenaMoiraAccount.objects.filter(username__in=people_usernames)
1293            for account in name_data:
1294                name_map[account.username.lower()] = (account.first_name, account.last_name)
1295
1296        # Set up special fields
1297        special_formatters = []
1298        if 'option_entry' in form.cleaned_data['special_fields']:
1299            col_labels.append('option_entry')
1300            special_formatters.append(format_option_entry)
1301
1302        # Assemble data
1303        if output_format == 'html':
1304            formatters = reporting_html_formatters
1305        else:
1306            formatters = {}
1307        def fetch_item(group, field):
1308            val = getattr(group, field)
1309            if field in formatters:
1310                val = formatters[field](val)
1311            return val
1312
1313        for group in qs:
1314            group_data = [fetch_item(group, field) for field in basic_fields]
1315            for field in people_fields:
1316                people = people_map[group.pk][field.pk]
1317                if nf_need_name:
1318                    def fmt(p):
1319                        first, last = name_map[p.lower()]
1320                        ctx = {'user': p, 'first': first, 'last': last}
1321                        return nf_fmt % ctx
1322                    people = [fmt(p) for p in people]
1323                else:
1324                    people = [nf_fmt % {'user': p} for p in people]
1325                group_data.append(", ".join(people))
1326
1327            for formatter in special_formatters:
1328                group_data.append(formatter(group))
1329
1330            report_groups.append(group_data)
1331
1332        # Handle output as CSV
1333        if output_format == 'csv':
1334            if output_disposition == 'download':
1335                mimetype = 'text/csv'
1336            else:
1337                # Firefox, at least, downloads text/csv regardless
1338                mimetype = 'text/plain'
1339            response = HttpResponse(mimetype=mimetype)
1340            if output_disposition == 'download':
1341                response['Content-Disposition'] = 'attachment; filename=asa-db-report.csv'
1342            writer = csv.writer(response)
1343            writer.writerow(col_labels)
1344            for row in report_groups:
1345                utf8_row = [unicode(cell).encode("utf-8") for cell in row]
1346                writer.writerow(utf8_row)
1347            return response
1348
1349    # Handle output as HTML
1350    context = {
1351        'form': form,
1352        'run_report': run_report,
1353        'column_labels': col_labels,
1354        'report_groups': report_groups,
1355        'pagename': 'groups',
1356    }
1357    return render_to_response('groups/reporting.html', context, context_instance=RequestContext(request), )
1358
1359
1360@permission_required('groups.view_group_private_info')
1361def show_nonstudent_officers(request, ):
1362    student_roles  = groups.models.OfficerRole.objects.filter(require_student=True, )
1363    student_q = groups.models.AthenaMoiraAccount.student_q()
1364    students = groups.models.AthenaMoiraAccount.active_accounts.filter(student_q)
1365    office_holders = groups.models.OfficeHolder.current_holders.order_by('group__name', 'role', )
1366    office_holders = office_holders.filter(role__in=student_roles)
1367    office_holders = office_holders.exclude(person__in=students.values('username'))
1368    office_holders = office_holders.select_related('group', 'group__group_status', 'role')
1369
1370    msg = None
1371    msg_type = ""
1372    if 'sort' in request.GET:
1373        if request.GET['sort'] == 'group':
1374            office_holders = office_holders.order_by('group__name', 'group__group_status', 'role', 'person', )
1375        elif request.GET['sort'] == 'status':
1376            office_holders = office_holders.order_by('group__group_status', 'group__name', 'role', 'person', )
1377        elif request.GET['sort'] == 'role':
1378            office_holders = office_holders.order_by('role', 'group__group_status', 'group__name', 'person', )
1379        elif request.GET['sort'] == 'person':
1380            office_holders = office_holders.order_by('person', 'group__group_status', 'group__name', 'role', )
1381        else:
1382            msg = 'Unknown sort key "%s".' % (request.GET['sort'], )
1383            msg_type = 'error'
1384
1385    context = {
1386        'pagename': 'groups',
1387        'roles': student_roles,
1388        'holders': office_holders,
1389        'msg': msg,
1390        'msg_type': msg_type,
1391    }
1392
1393    return render_to_response('groups/reporting/non-students.html', context, context_instance=RequestContext(request), )
1394
1395
Note: See TracBrowser for help on using the repository browser.