source: asadb/groups/views.py @ 04a46ed

space-accessstablestage
Last change on this file since 04a46ed was 04a46ed, checked in by MIT Association of Student Activities <asa@…>, 14 years ago

Clearer text for people bulk update

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