source: asadb/groups/views.py @ d3b0d83

space-accessstablestage
Last change on this file since d3b0d83 was d3b0d83, checked in by Alex Dehnert <adehnert@…>, 13 years ago

Sort the signatories page (partial ASA-#232)

  • Property mode set to 100644
File size: 49.6 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 = list(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['athena_locker'].required = True
633
634    class Meta(GroupCreateForm.Meta):
635        fieldsets = filter(
636            lambda fieldset: fieldset[0] not in ['financial', ],
637            GroupCreateForm.Meta.fieldsets
638        )
639
640def create_group_get_emails(group, group_startup, officer_emails, ):
641    # Figure out all the accounts mail parameters
642    accounts_count = 0
643    create_officer_list = False
644    if group_startup.create_officer_list and group.officer_email:
645        create_officer_list = True
646        accounts_count += 1
647    create_group_list = False
648    if group_startup.create_group_list and group.group_email:
649        create_group_list = True
650        accounts_count += 1
651    create_athena_locker = False
652    if group_startup.create_athena_locker and group.athena_locker:
653        create_athena_locker = True
654        accounts_count += 1
655    officer_list, _, officer_domain = group.officer_email.partition('@')
656    group_list, _, group_domain = group.group_email.partition('@')
657
658    # Fill out the Context
659    mail_context = Context({
660        'group': group,
661        'group_startup': group_startup,
662        'create_officer_list': create_officer_list,
663        'create_group_list': create_group_list,
664        'create_athena_locker': create_athena_locker,
665        'officer_list': officer_list,
666        'group_list': group_list,
667        'officer_emails': officer_emails,
668    })
669
670    # Welcome mail
671    welcome_mail = email_from_template(
672        tmpl='groups/diffs/new-group-announce.txt',
673        context=mail_context,
674        subject='ASA Group Recognition: %s' % (group.name, ),
675        to=officer_emails,
676        cc=['asa-new-group-announce@mit.edu'],
677        from_email='asa-exec@mit.edu',
678    )
679
680    # Accounts mail
681    if accounts_count > 0:
682        accounts_mail = email_from_template(
683            tmpl='groups/diffs/new-group-accounts.txt',
684            context=mail_context,
685            subject='New Student Activity: %s' % (group.name, ),
686            to=['accounts@mit.edu'],
687            cc=officer_emails+['asa-admin@mit.edu'],
688            from_email='asa-admin@mit.edu',
689        )
690        # XXX: Handle this better
691        if officer_domain != 'mit.edu' or (create_group_list and group_domain != 'mit.edu'):
692            accounts_mail.to = ['asa-groups@mit.edu']
693            accounts_mail.cc = ['asa-db@mit.edu']
694            accounts_mail.subject = "ERROR: " + accounts_mail.subject
695            accounts_mail.body = "Bad domain on officer or group list\n\n" + accounts_mail.body
696
697    else:
698        accounts_mail = None
699    return welcome_mail, accounts_mail
700
701def create_group_officers(group, formdata, save=True, ):
702    officer_emails = [ ]
703    for officer in ('president', 'treasurer', ):
704        username = formdata[officer+'_kerberos']
705        if username:
706            if save: groups.models.OfficeHolder(
707                person=username,
708                role=groups.models.OfficerRole.objects.get(slug=officer),
709                group=group,
710            ).save()
711            officer_emails.append('%s@mit.edu' % (formdata[officer+'_kerberos'], ))
712    return officer_emails
713
714@permission_required('groups.recognize_nge')
715def recognize_nge(request, ):
716    msg = None
717
718    initial = {
719        'create_officer_list': False,
720        'create_group_list': False,
721        'create_athena_locker': True,
722    }
723    group = groups.models.Group()
724    group.group_status = groups.models.GroupStatus.objects.get(slug='nge', )
725    group.recognition_date  = datetime.datetime.now()
726    if request.method == 'POST': # If the form has been submitted...
727        # A form bound to the POST data
728        form = GroupCreateNgeForm(
729            request.POST, request.FILES,
730            initial=initial,
731            instance=group,
732        )
733
734        if form.is_valid(): # All validation rules pass
735            group.set_updater(request.user)
736            form.save()
737            officer_emails = create_group_officers(group, form.cleaned_data, save=True, )
738
739            return redirect(reverse('groups:group-detail', args=[group.pk]))
740        else:
741            msg = "Validation failed. See below for details."
742
743    else:
744        form = GroupCreateNgeForm(initial=initial, instance=group, ) # An unbound form
745
746    context = {
747        'form':  form,
748        'msg':   msg,
749        'pagename':   'groups',
750    }
751    return render_to_response('groups/create/nge.html', context, context_instance=RequestContext(request), )
752
753@login_required
754def startup_form(request, ):
755    msg = None
756
757    initial = {
758        'create_officer_list': True,
759        'create_group_list': True,
760        'create_athena_locker': True,
761    }
762    group = groups.models.Group()
763    group.group_status = groups.models.GroupStatus.objects.get(slug='applying', )
764    group.recognition_date  = datetime.datetime.now()
765    if request.method == 'POST': # If the form has been submitted...
766        # A form bound to the POST data
767        form = GroupCreateStartupForm(
768            request.POST, request.FILES,
769            initial=initial,
770            instance=group,
771        )
772
773        if form.is_valid(): # All validation rules pass
774            group.set_updater(request.user)
775            form.save()
776
777            group_startup = groups.models.GroupStartup()
778            group_startup.group = group
779            group_startup.stage = groups.models.GROUP_STARTUP_STAGE_SUBMITTED
780            group_startup.submitter = request.user.username
781
782            group_startup.create_officer_list = form.cleaned_data['create_officer_list']
783            group_startup.create_group_list = form.cleaned_data['create_group_list']
784            group_startup.create_athena_locker = form.cleaned_data['create_athena_locker']
785
786            group_startup.president_name = form.cleaned_data['president_name']
787            group_startup.president_kerberos = form.cleaned_data['president_kerberos']
788            group_startup.treasurer_name = form.cleaned_data['treasurer_name']
789            group_startup.treasurer_kerberos = form.cleaned_data['treasurer_kerberos']
790
791            group_startup.save()
792
793            context = {
794                'group':            group,
795                'group_startup':    group_startup,
796                'pagename':         'groups',
797            }
798
799            email_from_template(
800                tmpl='groups/create/startup-submitted-email.txt',
801                context=context,
802                subject='ASA Startup Application: %s' % (group.name, ),
803                to=[request.user.email] + create_group_officers(group, form.cleaned_data, save=False, ),
804                cc=['asa-groups@mit.edu'],
805                from_email='asa-groups@mit.edu',
806            ).send()
807
808            return render_to_response('groups/create/startup_thanks.html', context, context_instance=RequestContext(request), )
809        else:
810            msg = "Validation failed. See below for details."
811
812    else:
813        form = GroupCreateStartupForm(initial=initial, instance=group, ) # An unbound form
814
815    context = {
816        'form':  form,
817        'msg':   msg,
818        'pagename':   'groups',
819    }
820    return render_to_response('groups/create/startup.html', context, context_instance=RequestContext(request), )
821
822@permission_required('groups.recognize_group')
823def recognize_normal_group(request, pk, ):
824    group_startup = get_object_or_404(groups.models.GroupStartup, pk=pk, )
825    group = group_startup.group
826
827    context = {
828        'startup': group_startup,
829        'group': group,
830        'pagename' : 'groups',
831    }
832
833    if group.group_status.slug != 'applying':
834        return render_to_response('groups/create/err.not-applying.html', context, context_instance=RequestContext(request), )
835    if group_startup.stage != groups.models.GROUP_STARTUP_STAGE_SUBMITTED:
836        return render_to_response('groups/create/err.not-applying.html', context, context_instance=RequestContext(request), )
837
838    context['msg'] = ""
839    if request.method == 'POST':
840        if 'approve' in request.POST:
841            group_startup.stage = groups.models.GROUP_STARTUP_STAGE_APPROVED
842            group_startup.save()
843
844            group.group_status = groups.models.GroupStatus.objects.get(slug='active')
845            group.constitution_url = ""
846            group.recognition_date = datetime.datetime.now()
847            group.set_updater(request.user)
848
849            group.save()
850            officer_emails = create_group_officers(group, group_startup.__dict__, )
851            welcome_mail, accounts_mail = create_group_get_emails(group, group_startup, officer_emails, )
852            welcome_mail.send()
853            if accounts_mail:
854                accounts_mail.send()
855            context['msg'] = 'Group approved.'
856            context['msg_type'] = 'info'
857        elif 'reject' in request.POST:
858            group_startup.stage = groups.models.GROUP_STARTUP_STAGE_REJECTED
859            group_startup.save()
860            group.group_status = groups.models.GroupStatus.objects.get(slug='derecognized')
861            group.save()
862            note = groups.models.GroupNote(
863                author=request.user.username,
864                body="Group rejected during recognition process.",
865                acl_read_group=True,
866                acl_read_offices=True,
867                group=group,
868            ).save()
869            context['msg'] = 'Group rejected.'
870            context['msg_type'] = 'info'
871        else:
872            context['disp_form'] = True
873    else:
874        context['disp_form'] = True
875
876    return render_to_response('groups/create/startup_review.html', context, context_instance=RequestContext(request), )
877
878class GroupStartupListView(ListView):
879    model = groups.models.GroupStartup
880    template_object_name = 'startup'
881
882    def get_queryset(self, ):
883        qs = super(GroupStartupListView, self).get_queryset()
884        qs = qs.filter(stage=groups.models.GROUP_STARTUP_STAGE_SUBMITTED)
885        qs = qs.select_related('group')
886        return qs
887
888    def get_context_data(self, **kwargs):
889        context = super(GroupStartupListView, self).get_context_data(**kwargs)
890        context['pagename'] = 'groups'
891        return context
892
893
894
895##################
896# Multiple group #
897##################
898
899class GroupFilter(django_filters.FilterSet):
900    name = django_filters.CharFilter(lookup_type='icontains', label="Name contains")
901    abbreviation = django_filters.CharFilter(lookup_type='iexact', label="Abbreviation is")
902    officer_email = django_filters.CharFilter(lookup_type='icontains', label="Officers' list contains")
903
904    account_filter = util.db_filters.MultiNumberFilter(
905        lookup_type='exact', label="Account number",
906        names=('main_account_id', 'funding_account_id', ),
907    )
908
909    class Meta:
910        model = groups.models.Group
911        fields = [
912            'name',
913            'abbreviation',
914            'officer_email',
915            'activity_category',
916            'group_class',
917            'group_status',
918            'group_funding',
919            'account_filter',
920        ]
921
922    def __init__(self, data=None, *args, **kwargs):
923        if not data: data = None
924        super(GroupFilter, self).__init__(data, *args, **kwargs)
925        active_pk = groups.models.GroupStatus.objects.get(slug='active').pk
926        self.form.initial['group_status'] = active_pk
927
928
929class GroupListView(ListView):
930    model = groups.models.Group
931    template_object_name = 'group'
932
933    def get(self, *args, **kwargs):
934        qs = super(GroupListView, self).get_queryset()
935        self.filterset = GroupFilter(self.request.GET, qs)
936        return super(GroupListView, self).get(*args, **kwargs)
937
938    def get_queryset(self, ):
939        qs = self.filterset.qs
940        return qs
941
942    def get_context_data(self, **kwargs):
943        context = super(GroupListView, self).get_context_data(**kwargs)
944        # Add in the publisher
945        context['pagename'] = 'groups'
946        context['filter'] = self.filterset
947        return context
948
949
950@permission_required('groups.view_signatories')
951def view_signatories(request, ):
952    the_groups = groups.models.Group.objects.all()
953    groups_filterset = GroupFilter(request.GET, the_groups)
954    the_groups = groups_filterset.qs
955
956    officers = groups.models.OfficeHolder.objects.filter(start_time__lte=datetime.datetime.now(), end_time__gte=datetime.datetime.now())
957    officers = officers.filter(group__in=the_groups)
958    officers = officers.select_related(depth=1)
959
960    role_slugs = ['president', 'treasurer', 'financial', 'reservation']
961    roles = groups.models.OfficerRole.objects.filter(slug__in=role_slugs)
962    roles = sorted(roles, key=lambda r: role_slugs.index(r.slug))
963
964    officers_map = collections.defaultdict(lambda: collections.defaultdict(set))
965    for officer in officers:
966        officers_map[officer.group][officer.role].add(officer.person)
967    officers_data = []
968    for group in the_groups:
969        role_list = []
970        for role in roles:
971            role_list.append(sorted(officers_map[group][role]))
972        officers_data.append((group, role_list))
973
974    context = {
975        'roles': roles,
976        'officers': officers_data,
977        'filter': groups_filterset,
978        'pagename': 'groups',
979    }
980    return render_to_response('groups/groups_signatories.html', context, context_instance=RequestContext(request), )
981
982def search_groups(request, ):
983    the_groups = groups.models.Group.objects.all()
984    groups_filterset = GroupFilter(request.GET, the_groups)
985
986    dest = None
987    if 'signatories' in request.GET:
988        dest = reverse('groups:signatories')
989        print dest
990    elif 'group-goto' in request.GET:
991        if len(groups_filterset.qs) == 1:
992            group = groups_filterset.qs[0]
993            return redirect(reverse('groups:group-detail', kwargs={'pk':group.pk}))
994        else:
995            dest = reverse('groups:list')
996    elif 'group-list' in request.GET:
997        dest = reverse('groups:list')
998
999    if dest:
1000        return redirect(dest + "?" + request.META['QUERY_STRING'])
1001    else:
1002        context = {
1003            'filter': groups_filterset,
1004            'pagename': 'groups',
1005        }
1006        return render_to_response('groups/group_search.html', context, context_instance=RequestContext(request), )
1007
1008
1009class GroupHistoryView(ListView):
1010    context_object_name = "version_list"
1011    template_name = "groups/group_version.html"
1012
1013    def get_queryset(self):
1014        history_entries = None
1015        if 'pk' in self.kwargs:
1016            group = get_object_or_404(groups.models.Group, pk=self.kwargs['pk'])
1017            history_entries = reversion.get_for_object(group)
1018        else:
1019            history_entries = reversion.models.Version.objects.all()
1020            group_content_type = ContentType.objects.get_for_model(groups.models.Group)
1021            history_entries = history_entries.filter(content_type=group_content_type)
1022        length = len(history_entries)
1023        if length > 150:
1024            history_entries = history_entries[length-100:]
1025        return history_entries
1026
1027    def get_context_data(self, **kwargs):
1028        context = super(GroupHistoryView, self).get_context_data(**kwargs)
1029        if 'pk' in self.kwargs:
1030            group = get_object_or_404(groups.models.Group, pk=self.kwargs['pk'])
1031            context['title'] = "History for %s" % (group.name, )
1032            context['adminpriv'] = self.request.user.has_perm('groups.admin_group', group)
1033            context['group'] = group
1034        else:
1035            context['title'] = "Recent Changes"
1036        context['pagename'] = 'groups'
1037        return context
1038
1039
1040@permission_required('groups.view_group_private_info')
1041def downloaded_constitutions(request, ):
1042    constitutions = groups.models.GroupConstitution.objects
1043    constitutions = constitutions.order_by('failure_reason', 'status_msg', 'failure_date', 'group__name', ).select_related('group', 'group__group_status', )
1044    failures = collections.defaultdict(list)
1045    successes = collections.defaultdict(list)
1046    for const in constitutions:
1047        if const.failure_reason:
1048            failures[const.failure_reason].append(const)
1049        else:
1050            successes[const.status_msg].append(const)
1051    context = {}
1052    context['failures']  = sorted(failures.items(),  key=lambda x: x[0])
1053    context['successes'] = sorted(successes.items(), key=lambda x: x[0])
1054    context['pagename'] = 'groups'
1055    return render_to_response('groups/groups_constitutions.html', context, context_instance=RequestContext(request), )
1056
1057
1058@permission_required('groups.view_group_private_info')
1059def downloaded_constitutions_csv(request, ):
1060    active_groups = groups.models.Group.active_groups.all()
1061    constitutions = groups.models.GroupConstitution.objects.filter(group__in=active_groups)
1062    constitutions = constitutions.order_by('failure_reason', 'status_msg', 'failure_date', 'group__name', ).select_related('group', 'group__group_status')
1063
1064    response = HttpResponse(mimetype='text/csv')
1065    writer = csv.writer(response)
1066
1067    writer.writerow([
1068        'failure_date',
1069        'status_msg',
1070        'name', 'id', 'group_status', 'email',
1071        'constitution_url',
1072    ])
1073    for const in constitutions:
1074        writer.writerow([
1075            const.failure_date,
1076            const.status_msg.strip(),
1077            const.group.name,
1078            const.group.pk,
1079            const.group.group_status.slug,
1080            const.group.officer_email,
1081            const.source_url,
1082        ])
1083    return response
1084
1085
1086
1087#######################
1088# REPORTING COMPONENT #
1089#######################
1090
1091class ReportingForm(form_utils.forms.BetterForm):
1092    basic_fields_choices = groups.models.Group.reporting_fields()
1093    basic_fields_labels = dict(basic_fields_choices) # name -> verbose_name
1094    basic_fields = forms.fields.MultipleChoiceField(
1095        choices=basic_fields_choices,
1096        widget=forms.CheckboxSelectMultiple,
1097        initial = ['id', 'name'],
1098    )
1099
1100    people_fields = forms.models.ModelMultipleChoiceField(
1101        queryset=groups.models.OfficerRole.objects.all(),
1102        widget=forms.CheckboxSelectMultiple,
1103        required=False,
1104    )
1105    show_as_emails = forms.BooleanField(
1106        help_text='Append "@mit.edu" to each value of people fields to allow use as email addresses?',
1107        required=False,
1108    )
1109
1110    special_fields_choices = (
1111        ('option_entry', '<option> entry', ),
1112    )
1113    special_fields = forms.fields.MultipleChoiceField(
1114        choices=special_fields_choices,
1115        widget=forms.CheckboxSelectMultiple,
1116        required=False,
1117    )
1118
1119    _format_choices = [
1120        ('html/inline',     "Web (HTML)", ),
1121        ('csv/inline',      "Spreadsheet (CSV) --- in browser", ),
1122        ('csv/download',    "Spreadsheet (CSV) --- download", ),
1123    ]
1124    output_format = forms.fields.ChoiceField(choices=_format_choices, widget=forms.RadioSelect, initial='html/inline')
1125
1126    class Meta:
1127        fieldsets = [
1128            ('filter', {
1129                'legend': 'Filter Groups',
1130                'fields': ['name', 'abbreviation', 'activity_category', 'group_class', 'group_status', 'group_funding', ],
1131            }),
1132            ('fields', {
1133                'legend': 'Data to display',
1134                'fields': ['basic_fields', 'people_fields', 'show_as_emails', 'special_fields', ],
1135            }),
1136            ('final', {
1137                'legend': 'Final options',
1138                'fields': ['o', 'output_format', ],
1139            }),
1140        ]
1141
1142class GroupReportingFilter(GroupFilter):
1143    class Meta(GroupFilter.Meta):
1144        form = ReportingForm
1145        order_by = True # we customize the field, so the value needs to be true-like but doesn't matter otherwise
1146
1147    def get_ordering_field(self):
1148        return forms.ChoiceField(label="Ordering", required=False, choices=ReportingForm.basic_fields_choices)
1149
1150    def __init__(self, data=None, *args, **kwargs):
1151        super(GroupReportingFilter, self).__init__(data, *args, **kwargs)
1152
1153def format_id(pk):
1154    url = reverse('groups:group-detail', kwargs={'pk':pk})
1155    return mark_safe("<a href='%s'>%d</a>" % (url, pk))
1156
1157def format_url(url):
1158    try:
1159        urlvalidator(url)
1160    except ValidationError:
1161        return url
1162    else:
1163        escaped = html.escape(url)
1164        return mark_safe("<a href='%s'>%s</a>" % (escaped, escaped))
1165
1166def format_email(email):
1167    try:
1168        emailvalidator(email)
1169    except ValidationError:
1170        return email
1171    else:
1172        escaped = html.escape(email)
1173        return mark_safe("<a href='mailto:%s'>%s</a>" % (escaped, escaped))
1174
1175def format_option_entry(group):
1176    name = html.escape(group.name)
1177    return '<option value="%s">%s</option>' % (name, name, )
1178
1179reporting_html_formatters = {
1180    'id': format_id,
1181    'website_url': format_url,
1182    'constitution_url': format_url,
1183    'group_email': format_email,
1184    'officer_email': format_email,
1185}
1186
1187@permission_required('groups.view_group_private_info')
1188def reporting(request, ):
1189    the_groups = groups.models.Group.objects.all()
1190    groups_filterset = GroupReportingFilter(request.GET, the_groups)
1191    form = groups_filterset.form
1192
1193    col_labels = []
1194    report_groups = []
1195    run_report = 'go' in request.GET and form.is_valid()
1196    if run_report:
1197        basic_fields = form.cleaned_data['basic_fields']
1198        output_format, output_disposition = form.cleaned_data['output_format'].split('/')
1199        col_labels = [form.basic_fields_labels[field] for field in basic_fields]
1200
1201        # Set up query
1202        qs = groups_filterset.qs
1203        # Prefetch foreign keys
1204        prefetch_fields = groups.models.Group.reporting_prefetch()
1205        prefetch_fields = prefetch_fields.intersection(basic_fields)
1206        if prefetch_fields:
1207            qs = qs.select_related(*list(prefetch_fields))
1208
1209        # Set up people
1210        people_fields = form.cleaned_data['people_fields']
1211        people_data = groups.models.OfficeHolder.current_holders.filter(group__in=qs, role__in=people_fields)
1212        # Group.pk -> (OfficerRole.pk -> set(username))
1213        people_map = collections.defaultdict(lambda: collections.defaultdict(set))
1214        for holder in people_data:
1215            people_map[holder.group_id][holder.role_id].add(holder.person)
1216        for field in people_fields:
1217            col_labels.append(field.display_name)
1218
1219        # Set up special fields
1220        special_formatters = []
1221        if 'option_entry' in form.cleaned_data['special_fields']:
1222            col_labels.append('option_entry')
1223            special_formatters.append(format_option_entry)
1224
1225        # Assemble data
1226        if output_format == 'html':
1227            formatters = reporting_html_formatters
1228        else:
1229            formatters = {}
1230        show_as_emails = form.cleaned_data['show_as_emails']
1231        def fetch_item(group, field):
1232            val = getattr(group, field)
1233            if field in formatters:
1234                val = formatters[field](val)
1235            return val
1236
1237        for group in qs:
1238            group_data = [fetch_item(group, field) for field in basic_fields]
1239            for field in people_fields:
1240                people = people_map[group.pk][field.pk]
1241                if show_as_emails: people = ["%s@mit.edu" % p for p in people]
1242                group_data.append(", ".join(people))
1243
1244            for formatter in special_formatters:
1245                group_data.append(formatter(group))
1246
1247            report_groups.append(group_data)
1248
1249        # Handle output as CSV
1250        if output_format == 'csv':
1251            if output_disposition == 'download':
1252                mimetype = 'text/csv'
1253            else:
1254                # Firefox, at least, downloads text/csv regardless
1255                mimetype = 'text/plain'
1256            response = HttpResponse(mimetype=mimetype)
1257            if output_disposition == 'download':
1258                response['Content-Disposition'] = 'attachment; filename=asa-db-report.csv'
1259            writer = csv.writer(response)
1260            writer.writerow(col_labels)
1261            for row in report_groups:
1262                utf8_row = [unicode(cell).encode("utf-8") for cell in row]
1263                writer.writerow(utf8_row)
1264            return response
1265
1266    # Handle output as HTML
1267    context = {
1268        'form': form,
1269        'run_report': run_report,
1270        'column_labels': col_labels,
1271        'report_groups': report_groups,
1272        'pagename': 'groups',
1273    }
1274    return render_to_response('groups/reporting.html', context, context_instance=RequestContext(request), )
1275
1276
1277@permission_required('groups.view_group_private_info')
1278def show_nonstudent_officers(request, ):
1279    student_roles  = groups.models.OfficerRole.objects.filter(require_student=True, )
1280    year = datetime.datetime.now().year
1281    account_classes = ["G"] + [str(yr) for yr in range(year-5, year+10)]
1282    students = groups.models.AthenaMoiraAccount.active_accounts.filter(account_class__in=account_classes)
1283    office_holders = groups.models.OfficeHolder.current_holders.order_by('group__name', 'role', )
1284    office_holders = office_holders.filter(role__in=student_roles)
1285    office_holders = office_holders.exclude(person__in=students.values('username'))
1286    office_holders = office_holders.select_related('group', 'group__group_status', 'role')
1287
1288    msg = None
1289    msg_type = ""
1290    if 'sort' in request.GET:
1291        if request.GET['sort'] == 'group':
1292            office_holders = office_holders.order_by('group__name', 'group__group_status', 'role', 'person', )
1293        elif request.GET['sort'] == 'status':
1294            office_holders = office_holders.order_by('group__group_status', 'group__name', 'role', 'person', )
1295        elif request.GET['sort'] == 'role':
1296            office_holders = office_holders.order_by('role', 'group__group_status', 'group__name', 'person', )
1297        elif request.GET['sort'] == 'person':
1298            office_holders = office_holders.order_by('person', 'group__group_status', 'group__name', 'role', )
1299        else:
1300            msg = 'Unknown sort key "%s".' % (request.GET['sort'], )
1301            msg_type = 'error'
1302
1303    context = {
1304        'pagename': 'groups',
1305        'roles': student_roles,
1306        'holders': office_holders,
1307        'msg': msg,
1308        'msg_type': msg_type,
1309    }
1310
1311    return render_to_response('groups/reporting/non-students.html', context, context_instance=RequestContext(request), )
1312
1313
Note: See TracBrowser for help on using the repository browser.