source: asadb/groups/views.py

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

Allow specifying @mit.edu addresses (ASA-#219)

  • Property mode set to 100644
File size: 54.1 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
246            local, at, domain = username.partition('@')
247            if at and domain.lower() == 'mit.edu':
248                msgs.append('In general, you should specify just the Athena username (for example, %s), not the whole email address (like %s).' % (local, username))
249                username = local
250
251            try:
252                moira_accounts[username] = groups.models.AthenaMoiraAccount.active_accounts.get(username=username)
253                new_people[i] = username
254            except groups.models.AthenaMoiraAccount.DoesNotExist:
255                msgs.append('Athena account "%s" appears not to exist. Changes involving them have been ignored.' % (username, ))
256    for person in people:
257        try:
258            moira_accounts[person] = groups.models.AthenaMoiraAccount.active_accounts.get(username=person)
259        except groups.models.AthenaMoiraAccount.DoesNotExist:
260            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, ))
261
262    return new_people, moira_accounts
263
264# Helper for manager_officers view
265def manage_officers_sync_role_people(
266    group, role, new_holders,
267    msgs, changes,
268    officers_map, people, moira_accounts, new_people, max_new, ):
269    """
270    Sync a set of new holders of a role with the database.
271
272    Arguments:
273    Function-specific:
274        role: the role object the changes center around
275        new_holders: The desired final set of people who should have the role
276    Output arguments --- information messages
277        msgs: warning message list. Output argument.
278        changes: list of changes made. [(verb, color, person, role)]
279    Background info arguments:
280        officers_map: (username, role) -> OfficeHolder
281        people: list of all potentially-affected people (who were previously involved)
282        moira_accounts: username -> AthenaMoiraAccount
283        new_people: potentially-affected people (who are newly involved) --- key -> username
284        max_new: highest index to use in keys for new_people
285    """
286
287    kept = 0
288    kept_not = 0
289
290    for person in people:
291        if person in new_holders:
292            if (person, role) in officers_map:
293                if person not in moira_accounts:
294                    pass # already errored above
295                elif role.require_student and not moira_accounts[person].is_student():
296                    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, ))
297                #changes.append(("Kept", "yellow", person, role))
298                kept += 1
299            else:
300                if person not in moira_accounts:
301                    pass # already errored above
302                elif role.require_student and not moira_accounts[person].is_student():
303                    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, ))
304                else:
305                    holder = groups.models.OfficeHolder(person=person, role=role, group=group,)
306                    holder.save()
307                    changes.append(("Added", "green", person, role))
308        else:
309            if (person, role) in officers_map:
310                officers_map[(person, role)].expire()
311                changes.append(("Removed", "red", person, role))
312            else:
313                kept_not += 1
314                pass
315    for i in range(max_new):
316        if "extra.%d" % (i, ) in new_holders:
317            if i in new_people:
318                person = new_people[i]
319                assert person in moira_accounts
320                if role.require_student and not moira_accounts[person].is_student():
321                    msgs.append('Only students can have the %s role, and %s does not appear to be a student.' % (role, person, ))
322                else:
323                    holder = groups.models.OfficeHolder(person=person, role=role, group=group,)
324                    holder.save()
325                    changes.append(("Added", "green", person, role))
326
327    return kept, kept_not
328
329# Helper for manager_officers view
330def manage_officers_table_update(
331    group,
332    request, context, msgs, changes,
333    people, roles, officers_map, max_new, ):
334
335    context['kept'] = 0
336    context['kept_not'] = 0
337
338    # Fill out moira_accounts with AthenaMoiraAccount objects for relevant people
339    new_people, moira_accounts = manage_officers_load_accounts(max_new, people, request, msgs)
340
341    # Process changes
342    for role in roles:
343        key = "holders.%s" % (role.slug, )
344        new_holders = set()
345        if key in request.POST:
346            new_holders = set(request.POST.getlist(key, ))
347        if len(new_holders) > role.max_count:
348            msgs.append("You selected %d people for %s; only %d are allowed. No changes to %s have been carried out in this update." %
349                (len(new_holders), role.display_name, role.max_count, role.display_name, )
350            )
351        else:
352            kept_delta, kept_not_delta = manage_officers_sync_role_people(
353                group, role, new_holders,   # input arguments
354                msgs, changes,              # output arguments
355                officers_map, people, moira_accounts,   # ~background data
356                new_people, max_new,                    # new people data
357            )
358            context['kept'] += kept_delta
359            context['kept_not'] += kept_not_delta
360
361
362class OfficersBulkManageForm(forms.Form):
363    mode_choices = [
364        ('add', 'Add new people', ),
365        ('remove', 'Remove old people', ),
366        ('sync', 'Set people to list provided', ),
367    ]
368    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.'
369    mode = forms.ChoiceField(choices=mode_choices, help_text=mode_help, )
370    role = forms.ChoiceField(initial='office-access', )
371    people = forms.CharField(
372        help_text='Usernames of people, one per line.',
373        widget=forms.Textarea,
374    )
375
376    def __init__(self, *args, **kwargs):
377        self._roles = kwargs['roles']
378        del kwargs['roles']
379        super(OfficersBulkManageForm, self).__init__(*args, **kwargs)
380        role_choices = [ (role.slug, role.display_name) for role in self._roles ]
381        self.fields['role'].choices = role_choices
382
383    def get_role(self, ):
384        role_slug = self.cleaned_data['role']
385        for role in self._roles:
386            if role.slug == role_slug:
387                return role
388        raise groups.OfficerRole.DoesNotExist
389
390def manage_officers_bulk_update(
391        group, bulk_form,
392        msgs, changes,
393        officers_map, ):
394
395    # Load parameters
396    mode = bulk_form.cleaned_data['mode']
397    role = bulk_form.get_role()
398    people_lst = bulk_form.cleaned_data['people'].split('\n')
399    people_set = set([p.strip() for p in people_lst])
400    if '' in people_set: people_set.remove('')
401
402    # Fill out moira_accounts
403    moira_accounts = {}
404    for username in people_set:
405        try:
406            moira_accounts[username] = groups.models.AthenaMoiraAccount.active_accounts.get(username=username)
407        except groups.models.AthenaMoiraAccount.DoesNotExist:
408            msgs.append('Athena account "%s" appears not to exist. Changes involving them have been ignored.' % (username, ))
409
410    # Find our target sets
411    cur_holders = [user for user, map_role in officers_map if role == map_role]
412    people = people_set.union(cur_holders)
413    if mode == 'add':
414        new_holders = people
415    elif mode == 'remove':
416        new_holders = people-people_set
417    elif mode == 'sync':
418        new_holders = people_set
419    else:
420        raise NotImplementedError("Unknown operation '%s'" % (mode, ))
421
422    # Make changes
423    if len(new_holders) <= role.max_count:
424        new_people = dict()
425        max_new = 0
426        manage_officers_sync_role_people(
427            group, role, new_holders,
428            msgs, changes,
429            officers_map, people, moira_accounts, new_people, max_new,
430        )
431    else:
432        too_many_tmpl = "You selected %d people for %s; only %d are allowed. No changes have been made in this update."
433        error = too_many_tmpl % (len(new_holders), role.display_name, role.max_count, )
434        msgs.append(error)
435
436@login_required
437def manage_officers(request, pk, ):
438    group = get_object_or_404(groups.models.Group, pk=pk)
439
440    if not request.user.has_perm('groups.admin_group', group):
441        raise PermissionDenied
442
443    people, roles, name_map, officers_map = manage_officers_load_officers(group)
444
445    max_new = 4
446    msgs = []
447    changes = []
448
449    context = {
450        'group': group,
451        'roles': roles,
452        'people': people,
453        'changes':   changes,
454        'msgs': msgs,
455    }
456
457    if request.method == 'POST' and 'opt-mode' in request.POST: # If the form has been submitted
458        edited = True
459
460        # Do the changes
461        if request.POST['opt-mode'] == 'table':
462            context['bulk_form'] = OfficersBulkManageForm(roles=roles, )
463            manage_officers_table_update(
464                group,
465                request, context, msgs, changes,
466                people, roles, officers_map, max_new,
467            )
468        elif request.POST['opt-mode'] == 'bulk':
469            bulk_form = OfficersBulkManageForm(request.POST, roles=roles, )
470            context['bulk_form'] = bulk_form
471            if bulk_form.is_valid():
472                manage_officers_bulk_update(
473                    group, bulk_form,
474                    msgs, changes,
475                    officers_map, )
476        else:
477            raise NotImplementedError("Update mode must be table or bulk, was '%s'" % (request.POST['opt-mode'], ))
478
479        # mark as changed and reload the data
480        if changes:
481            group.set_updater(request.user)
482            group.save()
483        people, roles, name_map, officers_map = manage_officers_load_officers(group)
484    else:
485        context['bulk_form'] = OfficersBulkManageForm(roles=roles, )
486
487    officers_data = []
488    for person in people:
489        role_list = []
490        for role in roles:
491            if (person, role) in officers_map:
492                role_list.append((role, True))
493            else:
494                role_list.append((role, False))
495        officers_data.append((False, person, name_map[person], role_list))
496    null_role_list = [(role, False) for role in roles]
497    for i in range(max_new):
498        officers_data.append((True, "extra.%d" % (i, ), "", null_role_list))
499    context['officers'] = officers_data
500    context['pagename'] = "groups"
501
502    return render_to_response('groups/group_change_officers.html', context, context_instance=RequestContext(request), )
503
504
505
506##################
507# ACCOUNT LOOKUP #
508##################
509
510class AccountLookupForm(forms.Form):
511    account_number = forms.IntegerField()
512    username = forms.CharField(help_text="Athena username of person to check")
513
514def account_lookup(request, ):
515    msg = None
516    msg_type = ""
517    account_number = None
518    username = None
519    group = None
520    office_holders = []
521
522    visible_roles  = groups.models.OfficerRole.objects.filter(publicly_visible=True)
523
524    initial = {}
525
526    if 'search' in request.GET: # If the form has been submitted...
527        # A form bound to the POST data
528        form = AccountLookupForm(request.GET)
529
530        if form.is_valid(): # All validation rules pass
531            account_number = form.cleaned_data['account_number']
532            username = form.cleaned_data['username']
533            account_q = Q(main_account_id=account_number) | Q(funding_account_id=account_number)
534            try:
535                group = groups.models.Group.objects.get(account_q)
536                office_holders = group.officers(person=username)
537                office_holders = office_holders.filter(role__in=visible_roles)
538            except groups.models.Group.DoesNotExist:
539                msg = "Group not found"
540                msg_type = "error"
541
542    else:
543        form = AccountLookupForm()
544
545    context = {
546        'username':     username,
547        'account_number': account_number,
548        'group':        group,
549        'office_holders': office_holders,
550        'form':         form,
551        'msg':          msg,
552        'msg_type':     msg_type,
553        'visible_roles':    visible_roles,
554    }
555    return render_to_response('groups/account_lookup.html', context, context_instance=RequestContext(request), )
556
557
558
559##################
560# GROUP CREATION #
561##################
562
563def validate_athena(username, student=False, ):
564    try:
565        person = groups.models.AthenaMoiraAccount.active_accounts.get(username=username)
566        if student and not person.is_student():
567            raise ValidationError('This must be a current student.')
568    except groups.models.AthenaMoiraAccount.DoesNotExist:
569        raise ValidationError('This must be a valid Athena username.')
570
571
572class GroupCreateForm(form_utils.forms.BetterModelForm):
573    create_officer_list = forms.BooleanField(required=False)
574    create_group_list = forms.BooleanField(required=False)
575    create_athena_locker = forms.BooleanField(required=False)
576
577    president_name = forms.CharField(max_length=50, )
578    president_kerberos = forms.CharField(min_length=3, max_length=8, )
579    treasurer_name = forms.CharField(max_length=50)
580    treasurer_kerberos = forms.CharField(min_length=3, max_length=8, )
581    def clean_president_kerberos(self, ):
582        username = self.cleaned_data['president_kerberos']
583        validate_athena(username, True, )
584        return username
585
586    def clean_treasurer_kerberos(self, ):
587        username = self.cleaned_data['treasurer_kerberos']
588        validate_athena(username, True, )
589        return username
590
591    class Meta:
592        fieldsets = [
593            ('basic', {
594                'legend': 'Basic Information',
595                'fields': ['name', 'abbreviation', 'description', ],
596            }),
597            ('officers', {
598                'legend': 'Officers',
599                'fields': ['president_name', 'president_kerberos', 'treasurer_name', 'treasurer_kerberos', ],
600            }),
601            ('type', {
602                'legend': 'Type',
603                'fields': ['activity_category', 'group_class', 'group_funding', ],
604            }),
605            ('technical', {
606                'legend': 'Technical Information',
607                'fields': [
608                    'officer_email', 'create_officer_list',
609                    'group_email', 'create_group_list',
610                    'athena_locker', 'create_athena_locker',
611                ],
612            }),
613            ('financial', {
614                'legend': 'Financial Information',
615                'fields': ['main_account_id', 'funding_account_id', ],
616            }),
617            ('constitution', {
618                'legend': 'Constitution',
619                'fields': ['constitution_url', ],
620            }),
621        ]
622        model = groups.models.Group
623
624
625class GroupCreateNgeForm(GroupCreateForm):
626    def __init__(self, *args, **kwargs):
627        super(GroupCreateNgeForm, self).__init__(*args, **kwargs)
628        self.fields['treasurer_name'].required = False
629        self.fields['treasurer_kerberos'].required = False
630
631
632class GroupCreateStartupForm(GroupCreateForm):
633    def __init__(self, *args, **kwargs):
634        super(GroupCreateStartupForm, self).__init__(*args, **kwargs)
635        self.fields['activity_category'].required = True
636        self.fields['constitution_url'].required = True
637        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."
638        self.fields['group_email'].required = True
639        self.fields['athena_locker'].required = True
640        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.)"
641
642        # Specifically, if the group ends up wanting to use scripts.mit.edu,
643        # they will currently be assigned locker.scripts.mit.edu. If they try
644        # to use foo.bar, then https://foo.bar.scripts.mit.edu/ will produce a
645        # certificate name mismatch. Officially, underscores are not allowed in
646        # hostnames, so foo_.scripts.mit.edu may fail with some software.
647
648    class Meta(GroupCreateForm.Meta):
649        fieldsets = filter(
650            lambda fieldset: fieldset[0] not in ['financial', ],
651            GroupCreateForm.Meta.fieldsets
652        )
653
654def create_group_get_emails(group, group_startup, officer_emails, ):
655    # Figure out all the accounts mail parameters
656    accounts_count = 0
657    create_officer_list = False
658    if group_startup.create_officer_list and group.officer_email:
659        create_officer_list = True
660        accounts_count += 1
661    create_group_list = False
662    if group_startup.create_group_list and group.group_email:
663        create_group_list = True
664        accounts_count += 1
665    create_athena_locker = False
666    if group_startup.create_athena_locker and group.athena_locker:
667        create_athena_locker = True
668        accounts_count += 1
669    officer_list, _, officer_domain = group.officer_email.partition('@')
670    group_list, _, group_domain = group.group_email.partition('@')
671
672    # Fill out the Context
673    mail_context = Context({
674        'group': group,
675        'group_startup': group_startup,
676        'create_officer_list': create_officer_list,
677        'create_group_list': create_group_list,
678        'create_athena_locker': create_athena_locker,
679        'officer_list': officer_list,
680        'group_list': group_list,
681        'officer_emails': officer_emails,
682    })
683
684    # Welcome mail
685    welcome_mail = email_from_template(
686        tmpl='groups/diffs/new-group-announce.txt',
687        context=mail_context,
688        subject='ASA Group Recognition: %s' % (group.name, ),
689        to=officer_emails,
690        cc=['asa-new-group-announce@mit.edu'],
691        from_email='asa-exec@mit.edu',
692    )
693
694    # Accounts mail
695    if accounts_count > 0:
696        accounts_mail = email_from_template(
697            tmpl='groups/diffs/new-group-accounts.txt',
698            context=mail_context,
699            subject='New Student Activity: %s' % (group.name, ),
700            to=['accounts@mit.edu'],
701            cc=officer_emails+['asa-admin@mit.edu'],
702            from_email='asa-admin@mit.edu',
703        )
704
705    else:
706        accounts_mail = None
707    return welcome_mail, accounts_mail
708
709def create_group_officers(group, formdata, save=True, ):
710    officer_emails = [ ]
711    for officer in ('president', 'treasurer', ):
712        username = formdata[officer+'_kerberos']
713        if username:
714            if save: groups.models.OfficeHolder(
715                person=username,
716                role=groups.models.OfficerRole.objects.get(slug=officer),
717                group=group,
718            ).save()
719            officer_emails.append('%s@mit.edu' % (formdata[officer+'_kerberos'], ))
720    return officer_emails
721
722@permission_required('groups.recognize_nge')
723def recognize_nge(request, ):
724    msg = None
725
726    initial = {
727        'create_officer_list': False,
728        'create_group_list': False,
729        'create_athena_locker': True,
730    }
731    group = groups.models.Group()
732    group.group_status = groups.models.GroupStatus.objects.get(slug='nge', )
733    group.recognition_date  = datetime.datetime.now()
734    if request.method == 'POST': # If the form has been submitted...
735        # A form bound to the POST data
736        form = GroupCreateNgeForm(
737            request.POST, request.FILES,
738            initial=initial,
739            instance=group,
740        )
741
742        if form.is_valid(): # All validation rules pass
743            group.set_updater(request.user)
744            form.save()
745            officer_emails = create_group_officers(group, form.cleaned_data, save=True, )
746
747            return redirect(reverse('groups:group-detail', args=[group.pk]))
748        else:
749            msg = "Validation failed. See below for details."
750
751    else:
752        form = GroupCreateNgeForm(initial=initial, instance=group, ) # An unbound form
753
754    context = {
755        'form':  form,
756        'msg':   msg,
757        'pagename':   'groups',
758    }
759    return render_to_response('groups/create/nge.html', context, context_instance=RequestContext(request), )
760
761@login_required
762def startup_form(request, ):
763    msg = None
764
765    initial = {
766        'create_officer_list': True,
767        'create_group_list': True,
768        'create_athena_locker': True,
769    }
770    group = groups.models.Group()
771    group.group_status = groups.models.GroupStatus.objects.get(slug='applying', )
772    group.recognition_date  = datetime.datetime.now()
773    if request.method == 'POST': # If the form has been submitted...
774        # A form bound to the POST data
775        form = GroupCreateStartupForm(
776            request.POST, request.FILES,
777            initial=initial,
778            instance=group,
779        )
780
781        if form.is_valid(): # All validation rules pass
782            group.set_updater(request.user)
783            form.save()
784
785            group_startup = groups.models.GroupStartup()
786            group_startup.group = group
787            group_startup.stage = groups.models.GROUP_STARTUP_STAGE_SUBMITTED
788            group_startup.submitter = request.user.username
789
790            group_startup.create_officer_list = form.cleaned_data['create_officer_list']
791            group_startup.create_group_list = form.cleaned_data['create_group_list']
792            group_startup.create_athena_locker = form.cleaned_data['create_athena_locker']
793
794            group_startup.president_name = form.cleaned_data['president_name']
795            group_startup.president_kerberos = form.cleaned_data['president_kerberos']
796            group_startup.treasurer_name = form.cleaned_data['treasurer_name']
797            group_startup.treasurer_kerberos = form.cleaned_data['treasurer_kerberos']
798
799            group_startup.save()
800
801            context = {
802                'group':            group,
803                'group_startup':    group_startup,
804                'pagename':         'groups',
805            }
806
807            email_from_template(
808                tmpl='groups/create/startup-submitted-email.txt',
809                context=context,
810                subject='ASA Startup Application: %s' % (group.name, ),
811                to=[request.user.email] + create_group_officers(group, form.cleaned_data, save=False, ),
812                cc=['asa-groups@mit.edu'],
813                from_email='asa-groups@mit.edu',
814            ).send()
815
816            return render_to_response('groups/create/startup_thanks.html', context, context_instance=RequestContext(request), )
817        else:
818            msg = "Validation failed. See below for details."
819
820    else:
821        form = GroupCreateStartupForm(initial=initial, instance=group, ) # An unbound form
822
823    context = {
824        'form':  form,
825        'msg':   msg,
826        'pagename':   'groups',
827    }
828    return render_to_response('groups/create/startup.html', context, context_instance=RequestContext(request), )
829
830def review_group_check_warnings(group_startup, group, ):
831    warnings = []
832
833    if group.name.startswith("MIT "):
834        warnings.append('Group name starts with "MIT". Generally, we prefer "Foo, MIT" instead.')
835    if "mit" in group.athena_locker.lower():
836        warnings.append('Athena locker name contains "mit", which may be redundant with paths like "http://web.mit.edu/mitfoo" or "/mit/foo/".')
837
838    if group_startup.president_kerberos == group_startup.treasurer_kerberos:
839        warnings.append('President matches Treasurer.')
840    if "%s@mit.edu" % (group_startup.president_kerberos, ) in (group.officer_email, group.group_email):
841        warnings.append('President email matches officer and/or group email.')
842    if group.officer_email == group.group_email:
843        warnings.append('Officer email matches group email.')
844
845    if '@mit.edu' not in group.officer_email or '@mit.edu' not in group.group_email:
846        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.')
847
848    if '.' in group.athena_locker:
849        warnings.append('Athena locker contains a ".". This is not compatible with scripts.mit.edu\'s wildcard certificate, and may cause other problems.')
850    if '_' in group.athena_locker:
851        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.')
852    if len(group.athena_locker) > 12:
853        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.')
854
855    return warnings
856
857@permission_required('groups.recognize_group')
858def recognize_normal_group(request, pk, ):
859    group_startup = get_object_or_404(groups.models.GroupStartup, pk=pk, )
860    group = group_startup.group
861
862    context = {
863        'startup': group_startup,
864        'group': group,
865        'pagename' : 'groups',
866    }
867
868    if group.group_status.slug != 'applying':
869        return render_to_response('groups/create/err.not-applying.html', context, context_instance=RequestContext(request), )
870    if group_startup.stage != groups.models.GROUP_STARTUP_STAGE_SUBMITTED:
871        return render_to_response('groups/create/err.not-applying.html', context, context_instance=RequestContext(request), )
872
873    context['warnings'] = review_group_check_warnings(group_startup, group)
874
875    context['msg'] = ""
876    if request.method == 'POST':
877        if 'approve' in request.POST:
878            group_startup.stage = groups.models.GROUP_STARTUP_STAGE_APPROVED
879            group_startup.save()
880
881            group.group_status = groups.models.GroupStatus.objects.get(slug='suspended')
882            group.constitution_url = ""
883            group.recognition_date = datetime.datetime.now()
884            group.set_updater(request.user)
885
886            note = groups.models.GroupNote(
887                author=request.user.username,
888                body="Approved group for recognition.",
889                acl_read_group=True,
890                acl_read_offices=True,
891                group=group,
892            ).save()
893
894            group.save()
895            officer_emails = create_group_officers(group, group_startup.__dict__, )
896            welcome_mail, accounts_mail = create_group_get_emails(group, group_startup, officer_emails, )
897            welcome_mail.send()
898            if accounts_mail:
899                accounts_mail.send()
900            context['msg'] = 'Group approved.'
901            context['msg_type'] = 'info'
902        elif 'reject' in request.POST:
903            group_startup.stage = groups.models.GROUP_STARTUP_STAGE_REJECTED
904            group_startup.save()
905            group.group_status = groups.models.GroupStatus.objects.get(slug='derecognized')
906            group.save()
907            note = groups.models.GroupNote(
908                author=request.user.username,
909                body="Group rejected during recognition process.",
910                acl_read_group=True,
911                acl_read_offices=True,
912                group=group,
913            ).save()
914            context['msg'] = 'Group rejected.'
915            context['msg_type'] = 'info'
916        else:
917            context['disp_form'] = True
918    else:
919        context['disp_form'] = True
920
921    return render_to_response('groups/create/startup_review.html', context, context_instance=RequestContext(request), )
922
923class GroupStartupListView(ListView):
924    model = groups.models.GroupStartup
925    template_object_name = 'startup'
926
927    def get_queryset(self, ):
928        qs = super(GroupStartupListView, self).get_queryset()
929        qs = qs.filter(stage=groups.models.GROUP_STARTUP_STAGE_SUBMITTED)
930        qs = qs.select_related('group')
931        return qs
932
933    def get_context_data(self, **kwargs):
934        context = super(GroupStartupListView, self).get_context_data(**kwargs)
935        context['pagename'] = 'groups'
936        return context
937
938
939
940##################
941# Multiple group #
942##################
943
944class GroupFilter(django_filters.FilterSet):
945    name = django_filters.CharFilter(lookup_type='icontains', label="Name contains")
946    abbreviation = django_filters.CharFilter(lookup_type='iexact', label="Abbreviation is")
947    officer_email = django_filters.CharFilter(lookup_type='icontains', label="Officers' list contains")
948
949    account_filter = util.db_filters.MultiNumberFilter(
950        lookup_type='exact', label="Account number",
951        names=('main_account_id', 'funding_account_id', ),
952    )
953
954    class Meta:
955        model = groups.models.Group
956        fields = [
957            'name',
958            'abbreviation',
959            'officer_email',
960            'activity_category',
961            'group_class',
962            'group_status',
963            'group_funding',
964            'account_filter',
965        ]
966
967    def __init__(self, data=None, *args, **kwargs):
968        if not data: data = None
969        super(GroupFilter, self).__init__(data, *args, **kwargs)
970        active_pk = groups.models.GroupStatus.objects.get(slug='active').pk
971        self.form.initial['group_status'] = active_pk
972
973
974class GroupListView(ListView):
975    model = groups.models.Group
976    template_object_name = 'group'
977
978    def get(self, *args, **kwargs):
979        qs = super(GroupListView, self).get_queryset()
980        self.filterset = GroupFilter(self.request.GET, qs)
981        return super(GroupListView, self).get(*args, **kwargs)
982
983    def get_queryset(self, ):
984        qs = self.filterset.qs
985        return qs
986
987    def get_context_data(self, **kwargs):
988        context = super(GroupListView, self).get_context_data(**kwargs)
989        # Add in the publisher
990        context['pagename'] = 'groups'
991        context['filter'] = self.filterset
992        return context
993
994
995@permission_required('groups.view_signatories')
996def view_signatories(request, ):
997    the_groups = groups.models.Group.objects.all()
998    groups_filterset = GroupFilter(request.GET, the_groups)
999    the_groups = groups_filterset.qs
1000
1001    officers = groups.models.OfficeHolder.objects.filter(start_time__lte=datetime.datetime.now(), end_time__gte=datetime.datetime.now())
1002    officers = officers.filter(group__in=the_groups)
1003    officers = officers.select_related(depth=1)
1004
1005    role_slugs = ['president', 'treasurer', 'financial', 'reservation']
1006    roles = groups.models.OfficerRole.objects.filter(slug__in=role_slugs)
1007    roles = sorted(roles, key=lambda r: role_slugs.index(r.slug))
1008
1009    officers_map = collections.defaultdict(lambda: collections.defaultdict(set))
1010    for officer in officers:
1011        officers_map[officer.group][officer.role].add(officer.person)
1012    officers_data = []
1013    for group in the_groups:
1014        role_list = []
1015        for role in roles:
1016            role_list.append(sorted(officers_map[group][role]))
1017        officers_data.append((group, role_list))
1018
1019    context = {
1020        'roles': roles,
1021        'officers': officers_data,
1022        'filter': groups_filterset,
1023        'pagename': 'groups',
1024    }
1025    return render_to_response('groups/groups_signatories.html', context, context_instance=RequestContext(request), )
1026
1027def search_groups(request, ):
1028    the_groups = groups.models.Group.objects.all()
1029    groups_filterset = GroupFilter(request.GET, the_groups)
1030
1031    dest = None
1032    if 'signatories' in request.GET:
1033        dest = reverse('groups:signatories')
1034        print dest
1035    elif 'group-goto' in request.GET:
1036        if len(groups_filterset.qs) == 1:
1037            group = groups_filterset.qs[0]
1038            return redirect(reverse('groups:group-detail', kwargs={'pk':group.pk}))
1039        else:
1040            dest = reverse('groups:list')
1041    elif 'group-list' in request.GET:
1042        dest = reverse('groups:list')
1043
1044    if dest:
1045        return redirect(dest + "?" + request.META['QUERY_STRING'])
1046    else:
1047        context = {
1048            'filter': groups_filterset,
1049            'pagename': 'groups',
1050        }
1051        return render_to_response('groups/group_search.html', context, context_instance=RequestContext(request), )
1052
1053
1054class GroupHistoryView(ListView):
1055    context_object_name = "version_list"
1056    template_name = "groups/group_version.html"
1057
1058    def get_queryset(self):
1059        history_entries = None
1060        if 'pk' in self.kwargs:
1061            group = get_object_or_404(groups.models.Group, pk=self.kwargs['pk'])
1062            history_entries = reversion.get_for_object(group)
1063        else:
1064            history_entries = reversion.models.Version.objects.all()
1065            group_content_type = ContentType.objects.get_for_model(groups.models.Group)
1066            history_entries = history_entries.filter(content_type=group_content_type)
1067        length = len(history_entries)
1068        if length > 150:
1069            history_entries = history_entries[length-100:]
1070        return history_entries
1071
1072    def get_context_data(self, **kwargs):
1073        context = super(GroupHistoryView, self).get_context_data(**kwargs)
1074        if 'pk' in self.kwargs:
1075            group = get_object_or_404(groups.models.Group, pk=self.kwargs['pk'])
1076            context['title'] = "History for %s" % (group.name, )
1077            context['adminpriv'] = self.request.user.has_perm('groups.admin_group', group)
1078            context['group'] = group
1079        else:
1080            context['title'] = "Recent Changes"
1081        context['pagename'] = 'groups'
1082        return context
1083
1084
1085@permission_required('groups.view_group_private_info')
1086def downloaded_constitutions(request, ):
1087    constitutions = groups.models.GroupConstitution.objects
1088    constitutions = constitutions.order_by('failure_reason', 'status_msg', 'failure_date', 'group__name', ).select_related('group', 'group__group_status', )
1089    failures = collections.defaultdict(list)
1090    successes = collections.defaultdict(list)
1091    for const in constitutions:
1092        if const.failure_reason:
1093            failures[const.failure_reason].append(const)
1094        else:
1095            successes[const.status_msg].append(const)
1096    context = {}
1097    context['failures']  = sorted(failures.items(),  key=lambda x: x[0])
1098    context['successes'] = sorted(successes.items(), key=lambda x: x[0])
1099    context['pagename'] = 'groups'
1100    return render_to_response('groups/groups_constitutions.html', context, context_instance=RequestContext(request), )
1101
1102
1103@permission_required('groups.view_group_private_info')
1104def downloaded_constitutions_csv(request, ):
1105    active_groups = groups.models.Group.active_groups.all()
1106    constitutions = groups.models.GroupConstitution.objects.filter(group__in=active_groups)
1107    constitutions = constitutions.order_by('failure_reason', 'status_msg', 'failure_date', 'group__name', ).select_related('group', 'group__group_status')
1108
1109    response = HttpResponse(mimetype='text/csv')
1110    writer = csv.writer(response)
1111
1112    writer.writerow([
1113        'failure_date',
1114        'status_msg',
1115        'name', 'id', 'group_status', 'email',
1116        'constitution_url',
1117    ])
1118    for const in constitutions:
1119        writer.writerow([
1120            const.failure_date,
1121            const.status_msg.strip(),
1122            const.group.name,
1123            const.group.pk,
1124            const.group.group_status.slug,
1125            const.group.officer_email,
1126            const.source_url,
1127        ])
1128    return response
1129
1130
1131
1132#######################
1133# REPORTING COMPONENT #
1134#######################
1135
1136name_formats = collections.OrderedDict()
1137name_formats['username']    = ('username',                      False,  '%(user)s')
1138name_formats['email']       = ('username@mit.edu',              False,  '%(user)s@mit.edu')
1139name_formats['name']        = ('First Last',                    True,   '%(first)s %(last)s')
1140name_formats['name-email']  = ('First Last <username@mit.edu>', True,   '%(first)s %(last)s <%(user)s@mit.edu>')
1141
1142class ReportingForm(form_utils.forms.BetterForm):
1143    special_filters = forms.fields.MultipleChoiceField(
1144        choices=[],
1145        widget=forms.SelectMultiple(attrs={'size': 10}),
1146        validators=[groups.models.filter_registry.validate_filter_slug],
1147        required=False,
1148    )
1149
1150    basic_fields_choices = groups.models.Group.reporting_fields()
1151    basic_fields_labels = dict(basic_fields_choices) # name -> verbose_name
1152    basic_fields = forms.fields.MultipleChoiceField(
1153        choices=basic_fields_choices,
1154        widget=forms.CheckboxSelectMultiple,
1155        initial = ['id', 'name'],
1156    )
1157
1158    people_fields = forms.models.ModelMultipleChoiceField(
1159        queryset=groups.models.OfficerRole.objects.all(),
1160        widget=forms.CheckboxSelectMultiple,
1161        required=False,
1162    )
1163    name_format = forms.ChoiceField(
1164        help_text='How to format the names of the President, Treasurer, etc., if displayed',
1165        choices=[(k, v[0]) for k,v in name_formats.items()],
1166        initial='username',
1167        required=True,
1168    )
1169
1170    special_fields_choices = (
1171        ('option_entry', '<option> entry', ),
1172    )
1173    special_fields = forms.fields.MultipleChoiceField(
1174        choices=special_fields_choices,
1175        widget=forms.CheckboxSelectMultiple,
1176        required=False,
1177    )
1178
1179    _format_choices = [
1180        ('html/inline',     "Web (HTML)", ),
1181        ('csv/inline',      "Spreadsheet (CSV) --- in browser", ),
1182        ('csv/download',    "Spreadsheet (CSV) --- download", ),
1183    ]
1184    output_format = forms.fields.ChoiceField(choices=_format_choices, widget=forms.RadioSelect, initial='html/inline')
1185
1186    class Meta:
1187        fieldsets = [
1188            ('filter', {
1189                'legend': 'Filter Groups',
1190                'fields': [
1191                    'name', 'abbreviation',
1192                    'activity_category', 'group_class', 'group_status', 'group_funding',
1193                    'special_filters',
1194                ],
1195            }),
1196            ('fields', {
1197                'legend': 'Data to display',
1198                'fields': ['basic_fields', 'people_fields', 'name_format', 'special_fields', ],
1199            }),
1200            ('final', {
1201                'legend': 'Final options',
1202                'fields': ['o', 'output_format', ],
1203            }),
1204        ]
1205
1206    def __init__(self, *args, **kwargs):
1207        super(ReportingForm, self).__init__(*args, **kwargs)
1208
1209        registry = groups.models.filter_registry
1210        self.fields['special_filters'].choices = registry.get_choices()
1211
1212class GroupReportingFilter(GroupFilter):
1213    class Meta(GroupFilter.Meta):
1214        form = ReportingForm
1215        order_by = True # we customize the field, so the value needs to be true-like but doesn't matter otherwise
1216
1217    def get_ordering_field(self):
1218        return forms.ChoiceField(label="Ordering", required=False, choices=ReportingForm.basic_fields_choices)
1219
1220    def __init__(self, data=None, *args, **kwargs):
1221        super(GroupReportingFilter, self).__init__(data, *args, **kwargs)
1222
1223def format_id(pk):
1224    url = reverse('groups:group-detail', kwargs={'pk':pk})
1225    return mark_safe("<a href='%s'>%d</a>" % (url, pk))
1226
1227def format_url(url):
1228    try:
1229        urlvalidator(url)
1230    except ValidationError:
1231        return url
1232    else:
1233        escaped = html.escape(url)
1234        return mark_safe("<a href='%s'>%s</a>" % (escaped, escaped))
1235
1236def format_email(email):
1237    try:
1238        emailvalidator(email)
1239    except ValidationError:
1240        return email
1241    else:
1242        escaped = html.escape(email)
1243        return mark_safe("<a href='mailto:%s'>%s</a>" % (escaped, escaped))
1244
1245def format_option_entry(group):
1246    name = html.escape(group.name)
1247    return '<option value="%s">%s</option>' % (name, name, )
1248
1249reporting_html_formatters = {
1250    'id': format_id,
1251    'website_url': format_url,
1252    'constitution_url': format_url,
1253    'group_email': format_email,
1254    'officer_email': format_email,
1255}
1256
1257@permission_required('groups.view_group_private_info')
1258def reporting(request, ):
1259    the_groups = groups.models.Group.objects.all()
1260    groups_filterset = GroupReportingFilter(request.GET, the_groups)
1261    form = groups_filterset.form
1262
1263    col_labels = []
1264    report_groups = []
1265    run_report = 'go' in request.GET and form.is_valid()
1266    if run_report:
1267        basic_fields = form.cleaned_data['basic_fields']
1268        output_format, output_disposition = form.cleaned_data['output_format'].split('/')
1269        col_labels = [form.basic_fields_labels[field] for field in basic_fields]
1270
1271        # Set up query
1272        qs = groups_filterset.qs
1273        for fltr_slug in form.cleaned_data['special_filters']:
1274            fltr = groups.models.filter_registry.get(fltr_slug)
1275            qs = qs.filter(pk__in=fltr.queryset())
1276
1277        # Prefetch foreign keys
1278        prefetch_fields = groups.models.Group.reporting_prefetch()
1279        prefetch_fields = prefetch_fields.intersection(basic_fields)
1280        if prefetch_fields:
1281            qs = qs.select_related(*list(prefetch_fields))
1282
1283        # Set up people
1284        people_fields = form.cleaned_data['people_fields']
1285        people_data = groups.models.OfficeHolder.current_holders.filter(group__in=qs, role__in=people_fields)
1286        # Group.pk -> (OfficerRole.pk -> set(username))
1287        people_map = collections.defaultdict(lambda: collections.defaultdict(set))
1288        for holder in people_data:
1289            people_map[holder.group_id][holder.role_id].add(holder.person)
1290        for field in people_fields:
1291            col_labels.append(field.display_name)
1292        # Figure out the format, and if necessary fetch human names
1293        nf_slug = form.cleaned_data['name_format']
1294        nf_label, nf_need_name, nf_fmt = name_formats[nf_slug]
1295        name_map = collections.defaultdict(lambda: ('???', '???'))
1296        if nf_need_name:
1297            people_usernames = [p.person.lower() for p in people_data]
1298            name_data = groups.models.AthenaMoiraAccount.objects.filter(username__in=people_usernames)
1299            for account in name_data:
1300                name_map[account.username.lower()] = (account.first_name, account.last_name)
1301
1302        # Set up special fields
1303        special_formatters = []
1304        if 'option_entry' in form.cleaned_data['special_fields']:
1305            col_labels.append('option_entry')
1306            special_formatters.append(format_option_entry)
1307
1308        # Assemble data
1309        if output_format == 'html':
1310            formatters = reporting_html_formatters
1311        else:
1312            formatters = {}
1313        def fetch_item(group, field):
1314            val = getattr(group, field)
1315            if field in formatters:
1316                val = formatters[field](val)
1317            return val
1318
1319        for group in qs:
1320            group_data = [fetch_item(group, field) for field in basic_fields]
1321            for field in people_fields:
1322                people = people_map[group.pk][field.pk]
1323                if nf_need_name:
1324                    def fmt(p):
1325                        first, last = name_map[p.lower()]
1326                        ctx = {'user': p, 'first': first, 'last': last}
1327                        return nf_fmt % ctx
1328                    people = [fmt(p) for p in people]
1329                else:
1330                    people = [nf_fmt % {'user': p} for p in people]
1331                group_data.append(", ".join(people))
1332
1333            for formatter in special_formatters:
1334                group_data.append(formatter(group))
1335
1336            report_groups.append(group_data)
1337
1338        # Handle output as CSV
1339        if output_format == 'csv':
1340            if output_disposition == 'download':
1341                mimetype = 'text/csv'
1342            else:
1343                # Firefox, at least, downloads text/csv regardless
1344                mimetype = 'text/plain'
1345            response = HttpResponse(mimetype=mimetype)
1346            if output_disposition == 'download':
1347                response['Content-Disposition'] = 'attachment; filename=asa-db-report.csv'
1348            writer = csv.writer(response)
1349            writer.writerow(col_labels)
1350            for row in report_groups:
1351                utf8_row = [unicode(cell).encode("utf-8") for cell in row]
1352                writer.writerow(utf8_row)
1353            return response
1354
1355    # Handle output as HTML
1356    context = {
1357        'form': form,
1358        'run_report': run_report,
1359        'column_labels': col_labels,
1360        'report_groups': report_groups,
1361        'pagename': 'groups',
1362    }
1363    return render_to_response('groups/reporting.html', context, context_instance=RequestContext(request), )
1364
1365
1366@permission_required('groups.view_group_private_info')
1367def show_nonstudent_officers(request, ):
1368    student_roles  = groups.models.OfficerRole.objects.filter(require_student=True, )
1369    student_q = groups.models.AthenaMoiraAccount.student_q()
1370    students = groups.models.AthenaMoiraAccount.active_accounts.filter(student_q)
1371    office_holders = groups.models.OfficeHolder.current_holders.order_by('group__name', 'role', )
1372    office_holders = office_holders.filter(role__in=student_roles)
1373    office_holders = office_holders.exclude(person__in=students.values('username'))
1374    office_holders = office_holders.select_related('group', 'group__group_status', 'role')
1375
1376    msg = None
1377    msg_type = ""
1378    if 'sort' in request.GET:
1379        if request.GET['sort'] == 'group':
1380            office_holders = office_holders.order_by('group__name', 'group__group_status', 'role', 'person', )
1381        elif request.GET['sort'] == 'status':
1382            office_holders = office_holders.order_by('group__group_status', 'group__name', 'role', 'person', )
1383        elif request.GET['sort'] == 'role':
1384            office_holders = office_holders.order_by('role', 'group__group_status', 'group__name', 'person', )
1385        elif request.GET['sort'] == 'person':
1386            office_holders = office_holders.order_by('person', 'group__group_status', 'group__name', 'role', )
1387        else:
1388            msg = 'Unknown sort key "%s".' % (request.GET['sort'], )
1389            msg_type = 'error'
1390
1391    context = {
1392        'pagename': 'groups',
1393        'roles': student_roles,
1394        'holders': office_holders,
1395        'msg': msg,
1396        'msg_type': msg_type,
1397    }
1398
1399    return render_to_response('groups/reporting/non-students.html', context, context_instance=RequestContext(request), )
1400
1401
Note: See TracBrowser for help on using the repository browser.