source: asadb/groups/views.py @ 68c93e8

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

Correctly linkify constitution URL

We previously linkified any non-empty constitution URL. We now only
linkify constitution URLs starting with "http://" or "https://".

  • Property mode set to 100644
File size: 45.0 KB
Line 
1# Create your views here.
2
3import collections
4import csv
5import datetime
6
7import groups.models
8
9from django.contrib.auth.decorators import user_passes_test, login_required, permission_required
10from django.contrib.contenttypes.models import ContentType
11from django.core.exceptions import PermissionDenied
12from django.views.generic import ListView, DetailView
13from django.shortcuts import render_to_response, get_object_or_404, redirect
14from django.template import RequestContext
15from django.template import Context, Template
16from django.template.loader import get_template
17from django.http import HttpResponse, Http404, HttpResponseRedirect
18from django.core.urlresolvers import reverse
19from django.core.validators import URLValidator, EmailValidator, email_re
20from django.core.mail import EmailMessage, mail_admins
21from django import forms
22from django.forms import ValidationError
23from django.db import connection
24from django.db.models import Q
25from django.utils import html
26from django.utils.safestring import mark_safe
27
28import form_utils.forms
29import reversion.models
30import django_filters
31
32from util.db_form_utils import StaticWidget
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
71
72
73################
74# Single group #
75################
76
77class GroupDetailView(DetailView):
78    context_object_name = "group"
79    model = groups.models.Group
80    def get_context_data(self, **kwargs):
81        # Call the base implementation first to get a context
82        context = super(GroupDetailView, self).get_context_data(**kwargs)
83        group = context['group']
84
85        const_link = None
86        const_url = group.constitution_url
87        if const_url.startswith("http://") or const_url.startswith("https://"):
88            const_link = const_url
89        context['constitution_link'] = const_link
90
91        # Indicate whether this person should be able to see "private" info
92        context['viewpriv'] = self.request.user.has_perm('groups.view_group_private_info', group)
93        context['adminpriv'] = self.request.user.has_perm('groups.admin_group', group)
94        context['notes'] = group.viewable_notes(self.request.user)
95
96        # People involved in the group
97        just_roles = groups.models.OfficerRole.objects.all()
98        if context['viewpriv'] or self.request.user.has_perm('groups.view_signatories'):
99            # Can see the non-public stuff
100            pass
101        else:
102            just_roles = just_roles.filter(publicly_visible=True)
103        roles = []
104        for role in just_roles:
105            roles.append((role.display_name, role, group.officers(role=role), ))
106        context['roles'] = roles
107        context['my_roles'] = []
108        if self.request.user.is_authenticated():
109            context['my_roles'] = group.officers(person=self.request.user.username).select_related('role')
110
111        return context
112
113
114class GroupChangeMainForm(form_utils.forms.BetterModelForm):
115    def __init__(self, *args, **kwargs):
116        change_restricted = False
117        if 'change_restricted' in kwargs:
118            change_restricted = kwargs['change_restricted']
119            del kwargs['change_restricted']
120        super(GroupChangeMainForm, self).__init__(*args, **kwargs)
121        restricted_fields = list(self.nobody_fields)
122        if change_restricted:
123            restricted_fields.extend(self.exec_only_fields)
124        for field_name in restricted_fields:
125            formfield = self.fields[field_name]
126            value = getattr(self.instance, field_name)
127            StaticWidget.replace_widget(formfield, value)
128        for field in self.force_required:
129            self.fields[field].required = True
130        self.fields['constitution_url'].help_text = mark_safe("Please put your current constitution URL, if you have one.<br>If your constitution is currently an AFS path, you can either use the corresponding web.mit.edu (e.g., http://web.mit.edu/locker/path/to/const.html) or stuff.mit.edu path, or just use http://asa.mit.edu/const/afs/your-afs-path.<br>If you don't currently know where your constitution is, put http://mit.edu/asa/start/constitution-req.html.<br>(In either of these last two cases, we'll get in touch with you later about putting something better in.)")
131
132    exec_only_fields = [
133        'name', 'abbreviation',
134        'group_status', 'group_class',
135        'group_funding', 'main_account_id', 'funding_account_id',
136    ]
137    nobody_fields = [
138        'recognition_date',
139    ]
140    force_required = [
141        'activity_category', 'description',
142        'num_undergrads', 'num_grads', 'num_community', 'num_other',
143        'website_url', 'officer_email', 'group_email',
144        'constitution_url', 'athena_locker',
145    ]
146
147
148    class Meta:
149        fieldsets = [
150            ('basic', {
151                'legend': 'Basic Information',
152                'fields': ['name', 'abbreviation', 'activity_category', 'description', ],
153            }),
154            ('size', {
155                'legend':'Membership Numbers',
156                '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.',
157                'fields': ['num_undergrads', 'num_grads', 'num_community', 'num_other',],
158            }),
159            ('contact', {
160                'legend': 'Contact Information',
161                'fields': ['website_url', 'meeting_times', 'officer_email', 'group_email', ],
162            }),
163            ('recognition', {
164                'legend': 'Recognition',
165                'fields': ['group_status', 'group_class', 'recognition_date', ],
166            }),
167            ('financial', {
168                'legend': 'Financial Information',
169                'fields': ['group_funding', 'main_account_id', 'funding_account_id', ],
170            }),
171            ('more-info', {
172                'legend': 'Additional Information',
173                'fields': ['constitution_url', 'advisor_name', 'athena_locker', ],
174            }),
175        ]
176        model = groups.models.Group
177
178@login_required
179def manage_main(request, pk, ):
180    group = get_object_or_404(groups.models.Group, pk=pk)
181
182    if not request.user.has_perm('groups.admin_group', group):
183        raise PermissionDenied
184    change_restricted = True
185    if request.user.has_perm('groups.change_group', group):
186        change_restricted = False
187
188    msg = None
189
190    initial = {}
191    if request.method == 'POST': # If the form has been submitted...
192        # A form bound to the POST data
193        form = GroupChangeMainForm(
194            request.POST, request.FILES,
195            change_restricted=change_restricted,
196            instance=group,
197        )
198
199        if form.is_valid(): # All validation rules pass
200            request_obj = form.save(commit=False)
201            request_obj.set_updater(request.user)
202            request_obj.save()
203            form.save_m2m()
204            msg = "Thanks for editing!"
205        else:
206            msg = "Validation failed. See below for details."
207
208    else:
209        form = GroupChangeMainForm(change_restricted=change_restricted, instance=group, initial=initial, ) # An unbound form
210
211    context = {
212        'group': group,
213        'form':  form,
214        'msg':   msg,
215    }
216    return render_to_response('groups/group_change_main.html', context, context_instance=RequestContext(request), )
217
218# Helper for manage_officers view
219def manage_officers_load_officers(group, ):
220    officers = group.officers()
221    people = list(set([ officer.person for officer in officers ]))
222    roles  = groups.models.OfficerRole.objects.all()
223
224    name_map = {}
225    for name in people:
226        name_map[name] = groups.models.AthenaMoiraAccount.try_format_by_username(name)
227    officers_map = {}
228
229    for officer in officers:
230        officers_map[(officer.person, officer.role)] = officer
231
232    return people, roles, name_map, officers_map
233
234# Helper for manager_officers view
235def manage_officers_load_accounts(max_new, people, request, msgs, ):
236    new_people = {}
237    moira_accounts = {}
238
239    for i in range(max_new):
240        key = "extra.%d" % (i, )
241        if key in request.POST and request.POST[key] != "":
242            username = request.POST[key]
243            try:
244                moira_accounts[username] = groups.models.AthenaMoiraAccount.active_accounts.get(username=username)
245                new_people[i] = username
246            except groups.models.AthenaMoiraAccount.DoesNotExist:
247                msgs.append('Athena account "%s" appears not to exist. Changes involving them have been ignored.' % (username, ))
248    for person in people:
249        try:
250            moira_accounts[person] = groups.models.AthenaMoiraAccount.active_accounts.get(username=person)
251        except groups.models.AthenaMoiraAccount.DoesNotExist:
252            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, ))
253
254    return new_people, moira_accounts
255
256# Helper for manager_officers view
257def manage_officers_sync_role_people(
258    group, role, new_holders,
259    msgs, changes,
260    officers_map, people, moira_accounts, new_people, max_new, ):
261    """
262    Sync a set of new holders of a role with the database.
263
264    Arguments:
265    Function-specific:
266        role: the role object the changes center around
267        new_holders: The desired final set of people who should have the role
268    Output arguments --- information messages
269        msgs: warning message list. Output argument.
270        changes: list of changes made. [(verb, color, person, role)]
271    Background info arguments:
272        officers_map: (username, role) -> OfficeHolder
273        people: list of all potentially-affected people (who were previously involved)
274        moira_accounts: username -> AthenaMoiraAccount
275        new_people: potentially-affected people (who are newly involved) --- key -> username
276        max_new: highest index to use in keys for new_people
277    """
278
279    kept = 0
280    kept_not = 0
281
282    for person in people:
283        if person in new_holders:
284            if (person, role) in officers_map:
285                if person not in moira_accounts:
286                    pass # already errored above
287                elif role.require_student and not moira_accounts[person].is_student():
288                    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, ))
289                #changes.append(("Kept", "yellow", person, role))
290                kept += 1
291            else:
292                if person not in moira_accounts:
293                    pass # already errored above
294                elif role.require_student and not moira_accounts[person].is_student():
295                    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, ))
296                else:
297                    holder = groups.models.OfficeHolder(person=person, role=role, group=group,)
298                    holder.save()
299                    changes.append(("Added", "green", person, role))
300        else:
301            if (person, role) in officers_map:
302                officers_map[(person, role)].expire()
303                changes.append(("Removed", "red", person, role))
304            else:
305                kept_not += 1
306                pass
307    for i in range(max_new):
308        if "extra.%d" % (i, ) in new_holders:
309            if i in new_people:
310                person = new_people[i]
311                assert person in moira_accounts
312                if role.require_student and not moira_accounts[person].is_student():
313                    msgs.append('Only students can have the %s role, and %s does not appear to be a student.' % (role, person, ))
314                else:
315                    holder = groups.models.OfficeHolder(person=person, role=role, group=group,)
316                    holder.save()
317                    changes.append(("Added", "green", person, role))
318
319    return kept, kept_not
320
321# Helper for manager_officers view
322def manage_officers_table_update(
323    group,
324    request, context, msgs, changes,
325    people, roles, officers_map, max_new, ):
326
327    context['kept'] = 0
328    context['kept_not'] = 0
329
330    # Fill out moira_accounts with AthenaMoiraAccount objects for relevant people
331    new_people, moira_accounts = manage_officers_load_accounts(max_new, people, request, msgs)
332
333    # Process changes
334    for role in roles:
335        key = "holders.%s" % (role.slug, )
336        new_holders = set()
337        if key in request.POST:
338            new_holders = set(request.POST.getlist(key, ))
339        if len(new_holders) > role.max_count:
340            msgs.append("You selected %d people for %s; only %d are allowed. No changes to %s have been carried out in this update." %
341                (len(new_holders), role.display_name, role.max_count, role.display_name, )
342            )
343        else:
344            kept_delta, kept_not_delta = manage_officers_sync_role_people(
345                group, role, new_holders,   # input arguments
346                msgs, changes,              # output arguments
347                officers_map, people, moira_accounts,   # ~background data
348                new_people, max_new,                    # new people data
349            )
350            context['kept'] += kept_delta
351            context['kept_not'] += kept_not_delta
352
353
354class OfficersBulkManageForm(forms.Form):
355    mode_choices = [
356        ('add', 'Add new people', ),
357        ('remove', 'Remove old people', ),
358        ('sync', 'Set people to list provided', ),
359    ]
360    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.'
361    mode = forms.ChoiceField(choices=mode_choices, help_text=mode_help, )
362    role = forms.ChoiceField(initial='office-access', )
363    people = forms.CharField(
364        help_text='Usernames of people, one per line.',
365        widget=forms.Textarea,
366    )
367
368    def __init__(self, *args, **kwargs):
369        self._roles = kwargs['roles']
370        del kwargs['roles']
371        super(OfficersBulkManageForm, self).__init__(*args, **kwargs)
372        role_choices = [ (role.slug, role.display_name) for role in self._roles ]
373        self.fields['role'].choices = role_choices
374
375    def get_role(self, ):
376        role_slug = self.cleaned_data['role']
377        for role in self._roles:
378            if role.slug == role_slug:
379                return role
380        raise groups.OfficerRole.DoesNotExist
381
382def manage_officers_bulk_update(
383        group, bulk_form,
384        msgs, changes,
385        officers_map, ):
386
387    # Load parameters
388    mode = bulk_form.cleaned_data['mode']
389    role = bulk_form.get_role()
390    people_lst = bulk_form.cleaned_data['people'].split('\n')
391    people_set = set([p.strip() for p in people_lst])
392    if '' in people_set: people_set.remove('')
393
394    # Fill out moira_accounts
395    moira_accounts = {}
396    for username in people_set:
397        try:
398            moira_accounts[username] = groups.models.AthenaMoiraAccount.active_accounts.get(username=username)
399        except groups.models.AthenaMoiraAccount.DoesNotExist:
400            msgs.append('Athena account "%s" appears not to exist. Changes involving them have been ignored.' % (username, ))
401
402    # Find our target sets
403    cur_holders = [user for user, map_role in officers_map if role == map_role]
404    people = people_set.union(cur_holders)
405    if mode == 'add':
406        new_holders = people
407    elif mode == 'remove':
408        new_holders = people-people_set
409    elif mode == 'sync':
410        new_holders = people_set
411    else:
412        raise NotImplementedError("Unknown operation '%s'" % (mode, ))
413
414    # Make changes
415    if len(new_holders) <= role.max_count:
416        new_people = dict()
417        max_new = 0
418        manage_officers_sync_role_people(
419            group, role, new_holders,
420            msgs, changes,
421            officers_map, people, moira_accounts, new_people, max_new,
422        )
423    else:
424        too_many_tmpl = "You selected %d people for %s; only %d are allowed. No changes have been made in this update."
425        error = too_many_tmpl % (len(new_holders), role.display_name, role.max_count, )
426        msgs.append(error)
427
428@login_required
429def manage_officers(request, pk, ):
430    group = get_object_or_404(groups.models.Group, pk=pk)
431
432    if not request.user.has_perm('groups.admin_group', group):
433        raise PermissionDenied
434
435    people, roles, name_map, officers_map = manage_officers_load_officers(group)
436
437    max_new = 4
438    msgs = []
439    changes = []
440
441    context = {
442        'group': group,
443        'roles': roles,
444        'people': people,
445        'changes':   changes,
446        'msgs': msgs,
447    }
448
449    if request.method == 'POST' and 'opt-mode' in request.POST: # If the form has been submitted
450        edited = True
451
452        # Do the changes
453        if request.POST['opt-mode'] == 'table':
454            context['bulk_form'] = OfficersBulkManageForm(roles=roles, )
455            manage_officers_table_update(
456                group,
457                request, context, msgs, changes,
458                people, roles, officers_map, max_new,
459            )
460        elif request.POST['opt-mode'] == 'bulk':
461            bulk_form = OfficersBulkManageForm(request.POST, roles=roles, )
462            context['bulk_form'] = bulk_form
463            if bulk_form.is_valid():
464                manage_officers_bulk_update(
465                    group, bulk_form,
466                    msgs, changes,
467                    officers_map, )
468        else:
469            raise NotImplementedError("Update mode must be table or bulk, was '%s'" % (request.POST['opt-mode'], ))
470
471        # mark as changed and reload the data
472        if changes:
473            group.set_updater(request.user)
474            group.save()
475        people, roles, name_map, officers_map = manage_officers_load_officers(group)
476    else:
477        context['bulk_form'] = OfficersBulkManageForm(roles=roles, )
478
479    officers_data = []
480    for person in people:
481        role_list = []
482        for role in roles:
483            if (person, role) in officers_map:
484                role_list.append((role, True))
485            else:
486                role_list.append((role, False))
487        officers_data.append((False, person, name_map[person], role_list))
488    null_role_list = [(role, False) for role in roles]
489    for i in range(max_new):
490        officers_data.append((True, "extra.%d" % (i, ), "", null_role_list))
491    context['officers'] = officers_data
492
493    return render_to_response('groups/group_change_officers.html', context, context_instance=RequestContext(request), )
494
495
496
497##################
498# ACCOUNT LOOKUP #
499##################
500
501class AccountLookupForm(forms.Form):
502    account_number = forms.IntegerField()
503    username = forms.CharField(help_text="Athena username of person to check")
504
505def account_lookup(request, ):
506    msg = None
507    msg_type = ""
508    account_number = None
509    username = None
510    group = None
511    office_holders = []
512
513    visible_roles  = groups.models.OfficerRole.objects.filter(publicly_visible=True)
514
515    initial = {}
516
517    if 'search' in request.GET: # If the form has been submitted...
518        # A form bound to the POST data
519        form = AccountLookupForm(request.GET)
520
521        if form.is_valid(): # All validation rules pass
522            account_number = form.cleaned_data['account_number']
523            username = form.cleaned_data['username']
524            account_q = Q(main_account_id=account_number) | Q(funding_account_id=account_number)
525            try:
526                group = groups.models.Group.objects.get(account_q)
527                office_holders = group.officers(person=username)
528                office_holders = office_holders.filter(role__in=visible_roles)
529            except groups.models.Group.DoesNotExist:
530                msg = "Group not found"
531                msg_type = "error"
532
533    else:
534        form = AccountLookupForm()
535
536    context = {
537        'username':     username,
538        'account_number': account_number,
539        'group':        group,
540        'office_holders': office_holders,
541        'form':         form,
542        'msg':          msg,
543        'msg_type':     msg_type,
544        'visible_roles':    visible_roles,
545    }
546    return render_to_response('groups/account_lookup.html', context, context_instance=RequestContext(request), )
547
548
549
550##################
551# GROUP CREATION #
552##################
553
554def validate_athena(username, student=False, ):
555    try:
556        person = groups.models.AthenaMoiraAccount.active_accounts.get(username=username)
557        if student and not person.is_student():
558            raise ValidationError('This must be a current student.')
559    except groups.models.AthenaMoiraAccount.DoesNotExist:
560        raise ValidationError('This must be a valid Athena username.')
561
562
563class GroupCreateForm(form_utils.forms.BetterModelForm):
564    create_officer_list = forms.BooleanField(required=False)
565    create_group_list = forms.BooleanField(required=False)
566    create_athena_locker = forms.BooleanField(required=False)
567
568    president_name = forms.CharField(max_length=50, )
569    president_kerberos = forms.CharField(min_length=3, max_length=8, )
570    treasurer_name = forms.CharField(max_length=50)
571    treasurer_kerberos = forms.CharField(min_length=3, max_length=8, )
572    def clean_president(self, ):
573        username = self.cleaned_data['president_kerberos']
574        validate_athena(username, True, )
575        return username
576
577    def clean_treasurer(self, ):
578        username = self.cleaned_data['treasurer_kerberos']
579        validate_athena(username, True, )
580        return username
581
582    class Meta:
583        fieldsets = [
584            ('basic', {
585                'legend': 'Basic Information',
586                'fields': ['name', 'abbreviation', 'description', ],
587            }),
588            ('officers', {
589                'legend': 'Officers',
590                'fields': ['president_name', 'president_kerberos', 'treasurer_name', 'treasurer_kerberos', ],
591            }),
592            ('type', {
593                'legend': 'Type',
594                'fields': ['activity_category', 'group_class', 'group_funding', ],
595            }),
596            ('technical', {
597                'legend': 'Technical Information',
598                'fields': [
599                    'officer_email', 'create_officer_list',
600                    'group_email', 'create_group_list',
601                    'athena_locker', 'create_athena_locker',
602                ],
603            }),
604            ('financial', {
605                'legend': 'Financial Information',
606                'fields': ['main_account_id', 'funding_account_id', ],
607            }),
608            ('constitution', {
609                'legend': 'Constitution',
610                'fields': ['constitution_url', ],
611            }),
612        ]
613        model = groups.models.Group
614
615
616class GroupCreateNgeForm(GroupCreateForm):
617    def __init__(self, *args, **kwargs):
618        super(GroupCreateNgeForm, self).__init__(*args, **kwargs)
619        self.fields['treasurer_name'].required = False
620        self.fields['treasurer_kerberos'].required = False
621
622
623class GroupCreateStartupForm(GroupCreateForm):
624    def __init__(self, *args, **kwargs):
625        super(GroupCreateStartupForm, self).__init__(*args, **kwargs)
626        self.fields['activity_category'].required = True
627        self.fields['constitution_url'].required = True
628        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."
629
630    class Meta(GroupCreateForm.Meta):
631        fieldsets = filter(
632            lambda fieldset: fieldset[0] not in ['financial', ],
633            GroupCreateForm.Meta.fieldsets
634        )
635
636def create_group_get_emails(group, group_startup, officer_emails, ):
637    # Figure out all the accounts mail parameters
638    accounts_count = 0
639    create_officer_list = False
640    if group_startup.create_officer_list and group.officer_email:
641        create_officer_list = True
642        accounts_count += 1
643    create_group_list = False
644    if group_startup.create_group_list and group.group_email:
645        create_group_list = True
646        accounts_count += 1
647    create_athena_locker = False
648    if group_startup.create_athena_locker and group.athena_locker:
649        create_athena_locker = True
650        accounts_count += 1
651    officer_list, _, officer_domain = group.officer_email.partition('@')
652    group_list, _, group_domain = group.group_email.partition('@')
653
654    # Fill out the Context
655    mail_context = Context({
656        'group': group,
657        'group_startup': group_startup,
658        'create_officer_list': create_officer_list,
659        'create_group_list': create_group_list,
660        'create_athena_locker': create_athena_locker,
661        'officer_list': officer_list,
662        'group_list': group_list,
663        'officer_emails': officer_emails,
664    })
665
666    # Welcome mail
667    welcome_mail = email_from_template(
668        tmpl='groups/diffs/new-group-announce.txt',
669        context=mail_context,
670        subject='ASA Group Recognition: %s' % (group.name, ),
671        to=officer_emails,
672        cc=['asa-new-group-announce@mit.edu'],
673        from_email='asa-exec@mit.edu',
674    )
675
676    # Accounts mail
677    if accounts_count > 0:
678        accounts_mail = email_from_template(
679            tmpl='groups/diffs/new-group-accounts.txt',
680            context=mail_context,
681            subject='New Student Activity: %s' % (group.name, ),
682            to=['accounts@mit.edu'],
683            cc=officer_emails+['asa-admin@mit.edu'],
684            from_email='asa-admin@mit.edu',
685        )
686        # XXX: Handle this better
687        if officer_domain != 'mit.edu' or (create_group_list and group_domain != 'mit.edu'):
688            accounts_mail.to = ['asa-groups@mit.edu']
689            accounts_mail.cc = ['asa-db@mit.edu']
690            accounts_mail.subject = "ERROR: " + accounts_mail.subject
691            accounts_mail.body = "Bad domain on officer or group list\n\n" + accounts_mail.body
692
693    else:
694        accounts_mail = None
695    return welcome_mail, accounts_mail
696
697def create_group_officers(group, formdata, save=True, ):
698    officer_emails = [ ]
699    for officer in ('president', 'treasurer', ):
700        username = formdata[officer+'_kerberos']
701        if username:
702            if save: groups.models.OfficeHolder(
703                person=username,
704                role=groups.models.OfficerRole.objects.get(slug=officer),
705                group=group,
706            ).save()
707            officer_emails.append('%s@mit.edu' % (formdata[officer+'_kerberos'], ))
708    return officer_emails
709
710@permission_required('groups.recognize_nge')
711def recognize_nge(request, ):
712    msg = None
713
714    initial = {
715        'create_officer_list': False,
716        'create_group_list': False,
717        'create_athena_locker': True,
718    }
719    group = groups.models.Group()
720    group.group_status = groups.models.GroupStatus.objects.get(slug='nge', )
721    group.recognition_date  = datetime.datetime.now()
722    if request.method == 'POST': # If the form has been submitted...
723        # A form bound to the POST data
724        form = GroupCreateNgeForm(
725            request.POST, request.FILES,
726            initial=initial,
727            instance=group,
728        )
729
730        if form.is_valid(): # All validation rules pass
731            group.set_updater(request.user)
732            form.save()
733            officer_emails = create_group_officers(group, form.cleaned_data, save=True, )
734
735            return redirect(reverse('groups:group-detail', args=[group.pk]))
736        else:
737            msg = "Validation failed. See below for details."
738
739    else:
740        form = GroupCreateNgeForm(initial=initial, instance=group, ) # An unbound form
741
742    context = {
743        'form':  form,
744        'msg':   msg,
745        'pagename':   'groups',
746    }
747    return render_to_response('groups/create/nge.html', context, context_instance=RequestContext(request), )
748
749@login_required
750def startup_form(request, ):
751    msg = None
752
753    initial = {
754        'create_officer_list': True,
755        'create_group_list': True,
756        'create_athena_locker': True,
757    }
758    group = groups.models.Group()
759    group.group_status = groups.models.GroupStatus.objects.get(slug='applying', )
760    group.recognition_date  = datetime.datetime.now()
761    if request.method == 'POST': # If the form has been submitted...
762        # A form bound to the POST data
763        form = GroupCreateStartupForm(
764            request.POST, request.FILES,
765            initial=initial,
766            instance=group,
767        )
768
769        if form.is_valid(): # All validation rules pass
770            group.set_updater(request.user)
771            form.save()
772
773            group_startup = groups.models.GroupStartup()
774            group_startup.group = group
775            group_startup.stage = groups.models.GROUP_STARTUP_STAGE_SUBMITTED
776            group_startup.submitter = request.user.username
777
778            group_startup.create_officer_list = form.cleaned_data['create_officer_list']
779            group_startup.create_group_list = form.cleaned_data['create_group_list']
780            group_startup.create_athena_locker = form.cleaned_data['create_athena_locker']
781
782            group_startup.president_name = form.cleaned_data['president_name']
783            group_startup.president_kerberos = form.cleaned_data['president_kerberos']
784            group_startup.treasurer_name = form.cleaned_data['treasurer_name']
785            group_startup.treasurer_kerberos = form.cleaned_data['treasurer_kerberos']
786
787            group_startup.save()
788
789            context = {
790                'group':            group,
791                'group_startup':    group_startup,
792                'pagename':         'groups',
793            }
794
795            email_from_template(
796                tmpl='groups/create/startup-submitted-email.txt',
797                context=context,
798                subject='ASA Startup Application: %s' % (group.name, ),
799                to=[request.user.email] + create_group_officers(group, form.cleaned_data, save=False, ),
800                cc=['asa-groups@mit.edu'],
801                from_email='asa-groups@mit.edu',
802            ).send()
803
804            return render_to_response('groups/create/startup_thanks.html', context, context_instance=RequestContext(request), )
805        else:
806            msg = "Validation failed. See below for details."
807
808    else:
809        form = GroupCreateStartupForm(initial=initial, instance=group, ) # An unbound form
810
811    context = {
812        'form':  form,
813        'msg':   msg,
814        'pagename':   'groups',
815    }
816    return render_to_response('groups/create/startup.html', context, context_instance=RequestContext(request), )
817
818@permission_required('groups.recognize_group')
819def recognize_normal_group(request, pk, ):
820    group_startup = get_object_or_404(groups.models.GroupStartup, pk=pk, )
821    group = group_startup.group
822
823    context = {
824        'startup': group_startup,
825        'group': group,
826        'pagename' : 'groups',
827    }
828
829    if group.group_status.slug != 'applying':
830        return render_to_response('groups/create/err.not-applying.html', context, context_instance=RequestContext(request), )
831    if group_startup.stage != groups.models.GROUP_STARTUP_STAGE_SUBMITTED:
832        return render_to_response('groups/create/err.not-applying.html', context, context_instance=RequestContext(request), )
833
834    context['msg'] = ""
835    if request.method == 'POST':
836        if 'approve' in request.POST:
837            group_startup.stage = groups.models.GROUP_STARTUP_STAGE_APPROVED
838            group_startup.save()
839
840            group.group_status = groups.models.GroupStatus.objects.get(slug='active')
841            group.constitution_url = ""
842            group.recognition_date = datetime.datetime.now()
843            group.set_updater(request.user)
844
845            group.save()
846            officer_emails = create_group_officers(group, group_startup.__dict__, )
847            welcome_mail, accounts_mail = create_group_get_emails(group, group_startup, officer_emails, )
848            welcome_mail.send()
849            if accounts_mail:
850                accounts_mail.send()
851            context['msg'] = 'Group approved.'
852            context['msg_type'] = 'info'
853        elif 'reject' in request.POST:
854            group_startup.stage = groups.models.GROUP_STARTUP_STAGE_REJECTED
855            group_startup.save()
856            group.group_status = groups.models.GroupStatus.objects.get(slug='derecognized')
857            group.save()
858            note = groups.models.GroupNote(
859                author=request.user.username,
860                body="Group rejected during recognition process.",
861                acl_read_group=True,
862                acl_read_offices=True,
863                group=group,
864            ).save()
865            context['msg'] = 'Group rejected.'
866            context['msg_type'] = 'info'
867        else:
868            context['disp_form'] = True
869    else:
870        context['disp_form'] = True
871
872    return render_to_response('groups/create/startup_review.html', context, context_instance=RequestContext(request), )
873
874class GroupStartupListView(ListView):
875    model = groups.models.GroupStartup
876    template_object_name = 'startup'
877
878    def get_queryset(self, ):
879        qs = super(GroupStartupListView, self).get_queryset()
880        qs = qs.filter(stage=groups.models.GROUP_STARTUP_STAGE_SUBMITTED)
881        qs = qs.select_related('group')
882        return qs
883
884    def get_context_data(self, **kwargs):
885        context = super(GroupStartupListView, self).get_context_data(**kwargs)
886        context['pagename'] = 'groups'
887        return context
888
889
890
891##################
892# Multiple group #
893##################
894
895class GroupFilter(django_filters.FilterSet):
896    name = django_filters.CharFilter(lookup_type='icontains', label="Name contains")
897    abbreviation = django_filters.CharFilter(lookup_type='iexact', label="Abbreviation is")
898
899    class Meta:
900        model = groups.models.Group
901        fields = [
902            'name',
903            'abbreviation',
904            'activity_category',
905            'group_class',
906            'group_status',
907            'group_funding',
908        ]
909
910    def __init__(self, data=None, *args, **kwargs):
911        if not data: data = None
912        super(GroupFilter, self).__init__(data, *args, **kwargs)
913        active_pk = groups.models.GroupStatus.objects.get(slug='active').pk
914        self.form.initial['group_status'] = active_pk
915
916
917class GroupListView(ListView):
918    model = groups.models.Group
919    template_object_name = 'group'
920
921    def get(self, *args, **kwargs):
922        qs = super(GroupListView, self).get_queryset()
923        self.filterset = GroupFilter(self.request.GET, qs)
924        return super(GroupListView, self).get(*args, **kwargs)
925
926    def get_queryset(self, ):
927        qs = self.filterset.qs
928        return qs
929
930    def get_context_data(self, **kwargs):
931        context = super(GroupListView, self).get_context_data(**kwargs)
932        # Add in the publisher
933        context['pagename'] = 'groups'
934        context['filter'] = self.filterset
935        return context
936
937
938@permission_required('groups.view_signatories')
939def view_signatories(request, ):
940    # TODO:
941    # * limit which columns (roles) get displayed
942    # This might want to wait for the generic reporting infrastructure, since
943    # I'd imagine some of it can be reused.
944
945    the_groups = groups.models.Group.objects.all()
946    groups_filterset = GroupFilter(request.GET, the_groups)
947    the_groups = groups_filterset.qs
948    officers = groups.models.OfficeHolder.objects.filter(start_time__lte=datetime.datetime.now(), end_time__gte=datetime.datetime.now())
949    officers = officers.filter(group__in=the_groups)
950    officers = officers.select_related(depth=1)
951    roles = groups.models.OfficerRole.objects.all()
952    officers_map = collections.defaultdict(lambda: collections.defaultdict(set))
953    for officer in officers:
954        officers_map[officer.group][officer.role].add(officer.person)
955    officers_data = []
956    for group in the_groups:
957        role_list = []
958        for role in roles:
959            role_list.append(officers_map[group][role])
960        officers_data.append((group, role_list))
961
962    context = {
963        'roles': roles,
964        'officers': officers_data,
965        'filter': groups_filterset,
966        'pagename': 'groups',
967    }
968    return render_to_response('groups/groups_signatories.html', context, context_instance=RequestContext(request), )
969
970def search_groups(request, ):
971    the_groups = groups.models.Group.objects.all()
972    groups_filterset = GroupFilter(request.GET, the_groups)
973
974    dest = None
975    if 'signatories' in request.GET:
976        dest = reverse('groups:signatories')
977        print dest
978    elif 'group-goto' in request.GET:
979        if len(groups_filterset.qs) == 1:
980            group = groups_filterset.qs[0]
981            return redirect(reverse('groups:group-detail', kwargs={'pk':group.pk}))
982        else:
983            dest = reverse('groups:list')
984    elif 'group-list' in request.GET:
985        dest = reverse('groups:list')
986
987    if dest:
988        return redirect(dest + "?" + request.META['QUERY_STRING'])
989    else:
990        context = {
991            'filter': groups_filterset,
992            'pagename': 'groups',
993        }
994        return render_to_response('groups/group_search.html', context, context_instance=RequestContext(request), )
995
996
997class GroupHistoryView(ListView):
998    context_object_name = "version_list"
999    template_name = "groups/group_version.html"
1000
1001    def get_queryset(self):
1002        history_entries = None
1003        if 'pk' in self.kwargs:
1004            group = get_object_or_404(groups.models.Group, pk=self.kwargs['pk'])
1005            history_entries = reversion.models.Version.objects.get_for_object(group)
1006        else:
1007            history_entries = reversion.models.Version.objects.all()
1008            group_content_type = ContentType.objects.get_for_model(groups.models.Group)
1009            history_entries = history_entries.filter(content_type=group_content_type)
1010        length = len(history_entries)
1011        if length > 150:
1012            history_entries = history_entries[length-100:]
1013        return history_entries
1014
1015    def get_context_data(self, **kwargs):
1016        context = super(GroupHistoryView, self).get_context_data(**kwargs)
1017        if 'pk' in self.kwargs:
1018            group = get_object_or_404(groups.models.Group, pk=self.kwargs['pk'])
1019            context['title'] = "History for %s" % (group.name, )
1020            context['adminpriv'] = self.request.user.has_perm('groups.admin_group', group)
1021            context['group'] = group
1022        else:
1023            context['title'] = "Recent Changes"
1024        return context
1025
1026
1027
1028#######################
1029# REPORTING COMPONENT #
1030#######################
1031
1032class ReportingForm(form_utils.forms.BetterForm):
1033    basic_fields_choices = groups.models.Group.reporting_fields()
1034    basic_fields_labels = dict(basic_fields_choices) # name -> verbose_name
1035    basic_fields = forms.fields.MultipleChoiceField(
1036        choices=basic_fields_choices,
1037        widget=forms.CheckboxSelectMultiple,
1038        initial = ['id', 'name'],
1039    )
1040
1041    people_fields = forms.models.ModelMultipleChoiceField(
1042        queryset=groups.models.OfficerRole.objects.all(),
1043        widget=forms.CheckboxSelectMultiple,
1044        required=False,
1045    )
1046    show_as_emails = forms.BooleanField(
1047        help_text='Append "@mit.edu" to each value of people fields to allow use as email addresses?',
1048        required=False,
1049    )
1050
1051    _format_choices = [
1052        ('html/inline',     "Web (HTML)", ),
1053        ('csv/inline',      "Spreadsheet (CSV) --- in browser", ),
1054        ('csv/download',    "Spreadsheet (CSV) --- download", ),
1055    ]
1056    output_format = forms.fields.ChoiceField(choices=_format_choices, widget=forms.RadioSelect, initial='html/inline')
1057
1058    class Meta:
1059        fieldsets = [
1060            ('filter', {
1061                'legend': 'Filter Groups',
1062                'fields': ['name', 'abbreviation', 'activity_category', 'group_class', 'group_status', 'group_funding', ],
1063            }),
1064            ('fields', {
1065                'legend': 'Data to display',
1066                'fields': ['basic_fields', 'people_fields', 'show_as_emails', ],
1067            }),
1068            ('final', {
1069                'legend': 'Final options',
1070                'fields': ['o', 'output_format', ],
1071            }),
1072        ]
1073
1074class GroupReportingFilter(GroupFilter):
1075    class Meta(GroupFilter.Meta):
1076        form = ReportingForm
1077        order_by = True # we customize the field, so the value needs to be true-like but doesn't matter otherwise
1078
1079    def get_ordering_field(self):
1080        return forms.ChoiceField(label="Ordering", required=False, choices=ReportingForm.basic_fields_choices)
1081
1082    def __init__(self, data=None, *args, **kwargs):
1083        super(GroupReportingFilter, self).__init__(data, *args, **kwargs)
1084
1085def format_id(pk):
1086    url = reverse('groups:group-detail', kwargs={'pk':pk})
1087    return mark_safe("<a href='%s'>%d</a>" % (url, pk))
1088
1089def format_url(url):
1090    try:
1091        urlvalidator(url)
1092    except ValidationError:
1093        return url
1094    else:
1095        escaped = html.escape(url)
1096        return mark_safe("<a href='%s'>%s</a>" % (escaped, escaped))
1097
1098def format_email(email):
1099    try:
1100        emailvalidator(email)
1101    except ValidationError:
1102        return email
1103    else:
1104        escaped = html.escape(email)
1105        return mark_safe("<a href='mailto:%s'>%s</a>" % (escaped, escaped))
1106
1107reporting_html_formatters = {
1108    'id': format_id,
1109    'website_url': format_url,
1110    'constitution_url': format_url,
1111    'group_email': format_email,
1112    'officer_email': format_email,
1113}
1114
1115@permission_required('groups.view_group_private_info')
1116def reporting(request, ):
1117    the_groups = groups.models.Group.objects.all()
1118    groups_filterset = GroupReportingFilter(request.GET, the_groups)
1119    form = groups_filterset.form
1120
1121    col_labels = []
1122    report_groups = []
1123    run_report = 'go' in request.GET and form.is_valid()
1124    if run_report:
1125        basic_fields = form.cleaned_data['basic_fields']
1126        output_format, output_disposition = form.cleaned_data['output_format'].split('/')
1127        col_labels = [form.basic_fields_labels[field] for field in basic_fields]
1128
1129        # Set up query
1130        qs = groups_filterset.qs
1131        # Prefetch foreign keys
1132        prefetch_fields = groups.models.Group.reporting_prefetch()
1133        prefetch_fields = prefetch_fields.intersection(basic_fields)
1134        if prefetch_fields:
1135            qs = qs.select_related(*list(prefetch_fields))
1136
1137        # Set up people
1138        people_fields = form.cleaned_data['people_fields']
1139        people_data = groups.models.OfficeHolder.current_holders.filter(group__in=qs, role__in=people_fields)
1140        # Group.pk -> (OfficerRole.pk -> set(username))
1141        people_map = collections.defaultdict(lambda: collections.defaultdict(set))
1142        for holder in people_data:
1143            people_map[holder.group_id][holder.role_id].add(holder.person)
1144        for field in people_fields:
1145            col_labels.append(field.display_name)
1146
1147        # Assemble data
1148        if output_format == 'html':
1149            formatters = reporting_html_formatters
1150        else:
1151            formatters = {}
1152        show_as_emails = form.cleaned_data['show_as_emails']
1153        def fetch_item(group, field):
1154            val = getattr(group, field)
1155            if field in formatters:
1156                val = formatters[field](val)
1157            return val
1158        for group in qs:
1159            group_data = [fetch_item(group, field) for field in basic_fields]
1160            for field in people_fields:
1161                people = people_map[group.pk][field.pk]
1162                if show_as_emails: people = ["%s@mit.edu" % p for p in people]
1163                group_data.append(", ".join(people))
1164
1165            report_groups.append(group_data)
1166
1167        # Handle output as CSV
1168        if output_format == 'csv':
1169            if output_disposition == 'download':
1170                mimetype = 'text/csv'
1171            else:
1172                # Firefox, at least, downloads text/csv regardless
1173                mimetype = 'text/plain'
1174            response = HttpResponse(mimetype=mimetype)
1175            if output_disposition == 'download':
1176                response['Content-Disposition'] = 'attachment; filename=asa-db-report.csv'
1177            writer = csv.writer(response)
1178            writer.writerow(col_labels)
1179            for row in report_groups: writer.writerow(row)
1180            return response
1181
1182    # Handle output as HTML
1183    context = {
1184        'form': form,
1185        'run_report': run_report,
1186        'column_labels': col_labels,
1187        'report_groups': report_groups,
1188        'pagename': 'groups',
1189    }
1190    return render_to_response('groups/reporting.html', context, context_instance=RequestContext(request), )
Note: See TracBrowser for help on using the repository browser.