source: asadb/forms/views.py @ beb46fb

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

Make issues.csv cycle-aware (ASA-#191)

  • Property mode set to 100644
File size: 25.5 KB
Line 
1import collections
2import csv
3import datetime
4import StringIO
5
6from django.conf import settings
7from django.contrib.auth.decorators import user_passes_test, login_required, permission_required
8from django.core.exceptions import PermissionDenied
9from django.views.generic import list_detail, ListView, DetailView
10from django.shortcuts import render_to_response, get_object_or_404
11from django.template import RequestContext
12from django.template import Context, Template
13from django.template.loader import get_template
14from django.http import Http404, HttpResponseRedirect, HttpResponse
15from django.core.urlresolvers import reverse
16from django.core.mail import EmailMessage, mail_admins
17from django.forms import FileField
18from django.forms import Form
19from django.forms import ModelForm
20from django.forms import ModelChoiceField, ModelMultipleChoiceField
21from django.forms import ValidationError
22from django.db import connection
23from django.db.models import Q, Count
24
25import django_filters
26
27import forms.models
28import groups.models
29import groups.views
30import util.emails
31
32#################
33# GENERIC VIEWS #
34#################
35
36class SelectGroupForm(Form):
37    group = ModelChoiceField(queryset=groups.models.Group.objects.all())
38    def __init__(self, *args, **kwargs):
39        queryset = None
40        if 'queryset' in kwargs:
41            queryset = kwargs['queryset']
42            del kwargs['queryset']
43        super(SelectGroupForm, self).__init__(*args, **kwargs)
44        if queryset is not None:
45            self.fields["group"].queryset = queryset
46
47def select_group(request, url_name_after, url_args=[], pagename='homepage', queryset=None, title="", msg=""):
48    if request.method == 'POST': # If the form has been submitted...
49        # A form bound to the POST data
50        form = SelectGroupForm(request.POST, queryset=queryset, )
51        if form.is_valid(): # All validation rules pass
52            group = form.cleaned_data['group'].id
53            return HttpResponseRedirect(reverse(url_name_after, args=url_args+[group],)) # Redirect after POST
54    else:
55        form = SelectGroupForm(queryset=queryset, ) # An unbound form
56
57    if not title: title = "Select group"
58    context = {
59        'form':form,
60        'title':title,
61        'msg':msg,
62        'pagename':pagename,
63    }
64    return render_to_response('forms/select.html', context, context_instance=RequestContext(request), )
65
66#############################
67# FIRST-YEAR SUMMER MAILING #
68#############################
69
70@login_required
71def fysm_by_years(request, year, category, ):
72    if year is None: year = datetime.date.today().year
73    queryset = forms.models.FYSM.objects.filter(year=year).order_by('group__name')
74    category_obj = None
75    category_name = 'main'
76    if category != None:
77        category_obj = get_object_or_404(forms.models.FYSMCategory, slug=category)
78        category_name = category_obj.name
79        queryset = queryset.filter(categories=category_obj)
80    forms.models.FYSMView.record_metric(request=request, fysm=None, year=year, page=category_name, )
81    categories = forms.models.FYSMCategory.objects.all()
82    return list_detail.object_list(
83        request,
84        queryset=queryset,
85        template_name="fysm/fysm_listing.html",
86        template_object_name="fysm",
87        extra_context={
88            "year": year,
89            "pagename": "fysm",
90            "category": category_obj,
91            "categories": categories,
92        }
93    )
94
95@login_required
96def fysm_view(request, year, submission, ):
97    submit_obj = get_object_or_404(forms.models.FYSM, pk=submission,)
98    all = forms.models.FYSM.objects.only("id", "display_name", )
99    try:
100        prev = all.filter(display_name__lt=submit_obj.display_name).order_by("-display_name")[0]
101    except IndexError:
102        prev = None
103    try:
104        next = all.filter(display_name__gt=submit_obj.display_name).order_by("display_name")[0]
105    except IndexError:
106        next = None
107    forms.models.FYSMView.record_metric(request=request, fysm=submit_obj, year=year, page="detail", )
108    return list_detail.object_detail(
109        request,
110        forms.models.FYSM.objects,
111        object_id=submission,
112        template_name="fysm/fysm_detail.html",
113        template_object_name="fysm",
114        extra_context={
115            "year": year,
116            "pagename": "fysm",
117            "prev": prev,
118            "next": next,
119        },
120    )
121
122def fysm_link(request, year, link_type, submission, ):
123    submit_obj = get_object_or_404(forms.models.FYSM, pk=submission,)
124    if submit_obj.year != int(year):
125        raise Http404("Year mismatch: fysm.year='%s', request's year='%s'" % (submit_obj.year, year, ))
126    if link_type == 'join':
127        url = submit_obj.join_url
128    elif link_type == 'website':
129        url = submit_obj.website
130    else:
131        raise Http404("Unknown link type")
132    forms.models.FYSMView.record_metric(request=request, fysm=submit_obj, year=year, page=link_type, )
133    return HttpResponseRedirect(url)
134
135def select_group_fysm(request, ):
136    qobj = Q(activity_category__isnull = True) | ~(Q(activity_category__name='Dorm') | Q(activity_category__name='FSILG'))
137    queryset = groups.models.Group.active_groups.filter(qobj)
138    return select_group(
139        request,
140        url_name_after='fysm-manage',
141        pagename='fysm',
142        queryset=queryset,
143    )
144
145class FYSMRequestForm(ModelForm):
146    class Meta:
147        model = forms.models.FYSM
148        fields = (
149            'display_name',
150            'website',
151            'join_url',
152            'contact_email',
153            'description',
154            'logo',
155            'tags',
156            'categories',
157        )
158
159    def clean_display_name(self, ):
160        name = self.cleaned_data['display_name']
161        if ',' in name:
162            raise ValidationError("""In general, commas in a display name are a mistake and will look bad (group names like "Punctuation Society, MIT" should probably be "Punctuation Society"). If you do want a comma, contact asa-fysm@mit.edu and we'll put it in for you.""")
163        return name
164
165    def clean_description(self, ):
166        description = self.cleaned_data['description']
167        length = len(description)
168        if length > 400:
169            raise ValidationError("Descriptions are capped at 400 characters, and this one is %d characters." % (length, ))
170        return description
171
172@login_required
173def fysm_manage(request, group, ):
174    year = datetime.date.today().year
175    group_obj = get_object_or_404(groups.models.Group, pk=group)
176
177    initial = {}
178    try:
179        fysm_obj = forms.models.FYSM.objects.get(group=group_obj, year=year, )
180    except forms.models.FYSM.DoesNotExist:
181        fysm_obj = forms.models.FYSM()
182        fysm_obj.group = group_obj
183        fysm_obj.year = year
184        initial['display_name'] = group_obj.name
185        initial['year'] = year
186        initial['website'] = group_obj.website_url
187        initial['join_url'] = group_obj.website_url
188        initial['contact_email'] = group_obj.officer_email
189
190    if request.method == 'POST': # If the form has been submitted...
191        form = FYSMRequestForm(request.POST, request.FILES, instance=fysm_obj, ) # A form bound to the POST data
192
193        if form.is_valid(): # All validation rules pass
194            request_obj = form.save()
195
196            view_uri = request.build_absolute_uri(reverse('fysm-view', args=[year, request_obj.pk, ]))
197
198            # Send email
199            email = util.emails.email_from_template(
200                tmpl='fysm/update_email.txt',
201                context = Context({
202                    'group': group_obj,
203                    'fysm': fysm_obj,
204                    'view_uri': view_uri,
205                    'submitter': request.user,
206                    'request': request,
207                    'sender': "ASA FYSM team",
208                }),
209                subject='FYSM entry for "%s" updated by "%s"' % (
210                    group_obj.name,
211                    request.user,
212                ),
213                to=[group_obj.officer_email, request.user.email, ],
214                from_email='asa-fysm@mit.edu',
215            )
216            email.bcc = ['asa-fysm-submissions@mit.edu']
217            email.send()
218            return HttpResponseRedirect(reverse('fysm-thanks', args=[fysm_obj.pk],)) # Redirect after POST
219
220    else:
221        form = FYSMRequestForm(instance=fysm_obj, initial=initial, ) # An unbound form
222
223    context = {
224        'group':group_obj,
225        'fysm':fysm_obj,
226        'form':form,
227        'categories':forms.models.FYSMCategory.objects.all(),
228        'pagename':'fysm',
229    }
230    return render_to_response('fysm/submit.html', context, context_instance=RequestContext(request), )
231
232
233def fysm_thanks(request, fysm, ):
234    year = datetime.date.today().year
235    fysm_obj = get_object_or_404(forms.models.FYSM, pk=fysm)
236
237    context = {
238        'group':fysm_obj.group,
239        'fysm':fysm_obj,
240        'pagename':'fysm',
241    }
242    return render_to_response('fysm/thanks.html', context, context_instance=RequestContext(request), )
243
244#####################
245# Membership update #
246#####################
247
248membership_update_qs = groups.models.Group.objects.filter(group_status__slug__in=['active', 'suspended', ])
249
250@login_required
251def group_membership_update_select_group(request, ):
252    cycle = forms.models.GroupConfirmationCycle.latest()
253
254    users_groups = groups.models.Group.admin_groups(request.user.username)
255    qs = membership_update_qs.filter(pk__in=users_groups)
256
257    return select_group(request=request,
258        url_name_after='membership-update-group',
259        url_args=[cycle.slug],
260        pagename='groups',
261        queryset=qs,
262        title="Submit membership update for...",
263    )
264
265class Form_GroupMembershipUpdate(ModelForm):
266    def __init__(self, *args, **kwargs):
267        super(Form_GroupMembershipUpdate, self).__init__(*args, **kwargs)
268        self.fields['no_hazing'].required = True
269
270    class Meta:
271        model = forms.models.GroupMembershipUpdate
272        fields = [
273            'updater_title',
274            'group_email',
275            'officer_email',
276            'email_preface',
277            'no_hazing',
278            'no_discrimination',
279            'membership_definition',
280            'num_undergrads',
281            'num_grads',
282            'num_alum',
283            'num_other_affiliate',
284            'num_other',
285            'membership_list',
286        ]
287
288@login_required
289def group_membership_update(request, cycle_slug, pk, ):
290    cycle = get_object_or_404(forms.models.GroupConfirmationCycle, slug=cycle_slug)
291    group_obj = get_object_or_404(groups.models.Group, pk=pk)
292    if not request.user.has_perm('groups.admin_group', group_obj):
293        raise PermissionDenied
294
295    try:
296        update_obj = forms.models.GroupMembershipUpdate.objects.get(group=group_obj, cycle=cycle, )
297    except forms.models.GroupMembershipUpdate.DoesNotExist:
298        update_obj = None
299
300    confirm_uri = request.build_absolute_uri(reverse('membership-confirm'))
301    submitted_uri = request.build_absolute_uri(reverse('membership-submitted'))
302
303    if request.method == 'POST':
304        form = Form_GroupMembershipUpdate(request.POST, request.FILES, instance=update_obj) # A form bound to the POST data
305
306        if form.is_valid(): # All validation rules pass
307            # Update the updater info
308            form.instance.group = group_obj
309            form.instance.cycle = cycle
310            form.instance.update_time  = datetime.datetime.now()
311            form.instance.updater_name = request.user.username
312            request_obj = form.save()
313
314            # Send email
315            tmpl = get_template('membership/anti-hazing.txt')
316            ctx = Context({
317                'update': request_obj,
318                'group': group_obj,
319                'submitter': request.user,
320            })
321            body = tmpl.render(ctx)
322            email = EmailMessage(
323                subject='Anti-Hazing and Non-Discrimination Acknowledgement for %s' % (
324                    group_obj.name,
325                ),
326                body=body,
327                from_email=request.user.email,
328                to=[request_obj.group_email, ],
329                cc=[request_obj.officer_email, ],
330                bcc=['asa-db-outgoing@mit.edu', ],
331            )
332            email.send()
333
334            # Send email
335            tmpl = get_template('membership/submit-confirm-email.txt')
336            ctx = Context({
337                'update': request_obj,
338                'group': group_obj,
339                'submitter': request.user,
340                'confirm_uri': confirm_uri,
341                'submitted_uri': submitted_uri,
342            })
343            body = tmpl.render(ctx)
344            email = EmailMessage(
345                subject='ASA Membership Information for %s' % (
346                    group_obj.name,
347                ),
348                body=body,
349                from_email=request.user.email,
350                to=[request_obj.officer_email, ],
351                bcc=['asa-db-outgoing@mit.edu', ],
352            )
353            email.send()
354
355            return HttpResponseRedirect(reverse('membership-thanks', )) # Redirect after POST
356
357    else:
358        form = Form_GroupMembershipUpdate(instance=update_obj)
359
360    context = {
361        'form':form,
362        'group':group_obj,
363        'cycle':cycle,
364        'confirm_uri': confirm_uri,
365        'pagename':'groups',
366    }
367    return render_to_response('membership/update.html', context, context_instance=RequestContext(request), )
368
369class Form_PersonMembershipUpdate(ModelForm):
370    groups = ModelMultipleChoiceField(queryset=membership_update_qs)
371    class Meta:
372        model = forms.models.PersonMembershipUpdate
373        fields = [
374            'groups',
375        ]
376
377@login_required
378def person_membership_update(request, ):
379    initial = {
380    }
381    cycle = forms.models.GroupConfirmationCycle.latest()
382
383    # Initialize/find the PersonMembershipUpdate for this user
384    try:
385        update_obj = forms.models.PersonMembershipUpdate.objects.get(
386            username=request.user.username,
387            deleted__isnull=True,
388            cycle=cycle,
389        )
390        selected_groups = update_obj.groups.all()
391    except forms.models.PersonMembershipUpdate.DoesNotExist:
392        update_obj = forms.models.PersonMembershipUpdate()
393        update_obj.update_time  = datetime.datetime.now()
394        update_obj.username = request.user.username
395        update_obj.cycle = cycle
396        selected_groups = []
397
398    # Determine whether the submitter is a student or not
399    accounts = groups.models.AthenaMoiraAccount
400    try:
401        person = accounts.active_accounts.get(username=request.user.username)
402        if person.is_student():
403            update_obj.valid = forms.models.VALID_AUTOVALIDATED
404        else:
405            update_obj.valid = forms.models.VALID_AUTOREJECTED
406    except accounts.DoesNotExist:
407        pass
408        update_obj.valid = forms.models.VALID_AUTOREJECTED
409
410    update_obj.save()
411
412    # Find groups that list a role for the user
413    office_holders = groups.models.OfficeHolder.current_holders.filter(person=request.user.username)
414    role_groups = {}
415    for office_holder in office_holders:
416        if office_holder.group.pk not in role_groups:
417            role_groups[office_holder.group.pk] = (office_holder.group, set())
418        role_groups[office_holder.group.pk][1].add(office_holder.role.display_name)
419
420    # Find groups the user searched for
421    filterset = groups.views.GroupFilter(request.GET, membership_update_qs)
422    filtered_groups = filterset.qs.all()
423    show_filtered_groups = ('search' in request.GET)
424
425    message = ""
426    message_type = "info"
427
428    if update_obj.valid <= forms.models.VALID_UNSET:
429        message = "You are not listed as a student. While you're welcome to confirm your membership in groups anyway, you will not count towards the five student member requirement. If you are a student, please contact asa-exec so that we can correct our records."
430        message_type = "warn"
431
432    # Handle the single group add/remove forms
433    # * removing previously confirmed groups
434    # * add/remove groups that list the user in a role
435    # * add/remove groups the user searched for
436    if request.method == 'POST' and 'add-remove' in request.POST:
437        group = groups.models.Group.objects.get(id=request.POST['group'])
438        if request.POST['action'] == 'remove':
439            if group in update_obj.groups.all():
440                update_obj.groups.remove(group)
441                message = "You have successfully unconfirmed membership in %s." % (group, )
442            else:
443                message = "Removal failed because you had not confirmed membership in %s." % (group, )
444                message_type = "warn"
445        elif request.POST['action'] == 'add':
446            if group in update_obj.groups.all():
447                message = "Membership in %s already confirmed." % (group, )
448                message_type = "warn"
449            else:
450                update_obj.groups.add(group)
451                message = "You have successfully confirmed membership in %s." % (group, )
452        else:
453            message = "Uh, somehow you tried to do something besides adding and removing..."
454            message_type = "alert"
455
456    # Handle the big list of groups
457    if request.method == 'POST' and 'list' in request.POST:
458        form = Form_PersonMembershipUpdate(request.POST, request.FILES, instance=update_obj)
459        if form.is_valid():
460            request_obj = form.save()
461            message = "Update saved"
462    else:
463        form = Form_PersonMembershipUpdate(initial=initial, instance=update_obj, )
464    form.fields['groups'].widget.attrs['size'] = 20
465
466    # Render the page
467    context = {
468        'role_groups':role_groups,
469        'form':form,
470        'filter':filterset,
471        'show_filtered_groups':show_filtered_groups,
472        'filtered_groups':filtered_groups,
473        'member_groups':selected_groups,
474        'message': message,
475        'message_type': message_type,
476        'pagename':'groups',
477    }
478    return render_to_response('membership/confirm.html', context, context_instance=RequestContext(request), )
479
480
481class View_GroupMembershipList(ListView):
482    context_object_name = "group_list"
483    template_name = "membership/submitted.html"
484
485    def get_queryset(self):
486        cycle = forms.models.GroupConfirmationCycle.latest()
487        group_updates = forms.models.GroupMembershipUpdate.objects.all()
488        group_updates = group_updates.filter(
489            cycle=cycle,
490            group__personmembershipupdate__cycle=cycle,
491            group__personmembershipupdate__deleted__isnull=True,
492            group__personmembershipupdate__valid__gt=0,
493        )
494        group_updates = group_updates.annotate(num_confirms=Count('group__personmembershipupdate'))
495        #print len(list(group_updates))
496        #for query in connection.queries: print query
497        return group_updates
498
499
500class View_GroupConfirmationCyclesList(ListView):
501    context_object_name = "cycle_list"
502    template_name = "membership/admin.html"
503    model = forms.models.GroupConfirmationCycle
504
505    def get_context_data(self, **kwargs):
506        context = super(View_GroupConfirmationCyclesList, self).get_context_data(**kwargs)
507        context['pagename'] = 'groups'
508        return context
509
510
511@permission_required('groups.view_group_private_info')
512def group_confirmation_issues(request, slug, ):
513    account_numbers = ("accounts" in request.GET) and request.GET['accounts'] == "1"
514
515    active_groups = groups.models.Group.active_groups
516    group_updates = forms.models.GroupMembershipUpdate.objects.filter(cycle__slug=slug, )
517    people_confirmations = forms.models.PersonMembershipUpdate.objects.filter(
518        deleted__isnull=True,
519        valid__gt=0,
520        cycle__slug=slug,
521    )
522
523    buf = StringIO.StringIO()
524    output = csv.writer(buf)
525    fields = ['group_id', 'group_name', 'issue', 'num_confirm', 'officer_email', ]
526    if account_numbers: fields.append("main_account")
527    output.writerow(fields)
528
529    q_present = Q(id__in=group_updates.values('group'))
530    missing_groups = active_groups.filter(~q_present)
531    #print len(list(group_updates))
532    for group in missing_groups:
533        num_confirms = len(people_confirmations.filter(groups=group))
534        fields = [
535            group.id,
536            group.name,
537            'unsubmitted',
538            num_confirms,
539            group.officer_email,
540        ]
541        if account_numbers: fields.append(group.main_account_id)
542        output.writerow(fields)
543
544    for group_update in group_updates:
545        group = group_update.group
546        num_confirms = len(people_confirmations.filter(groups=group))
547        problems = []
548
549        if num_confirms < 5:
550            problems.append("confirmations")
551
552        num_students = group_update.num_undergrads + group_update.num_grads
553        num_other = group_update.num_alum + group_update.num_other_affiliate + group_update.num_other
554        if num_students < num_other:
555            problems.append("50%")
556
557        for problem in problems:
558            fields = [
559                group.id,
560                group.name,
561                problem,
562                num_confirms,
563                group.officer_email,
564            ]
565            if account_numbers: fields.append(group.main_account_id)
566            output.writerow(fields)
567
568
569    return HttpResponse(buf.getvalue(), mimetype='text/csv', )
570
571
572
573##########
574# Midway #
575##########
576
577
578class View_Midways(ListView):
579    context_object_name = "midway_list"
580    template_name = "midway/midway_list.html"
581
582    def get_queryset(self):
583        midways = forms.models.Midway.objects.order_by('date')
584        return midways
585
586    def get_context_data(self, **kwargs):
587        context = super(View_Midways, self).get_context_data(**kwargs)
588        context['pagename'] = 'midway'
589        return context
590
591def midway_map_latest(request, ):
592    midways = forms.models.Midway.objects.order_by('-date')[:1]
593    if len(midways) == 0:
594        raise Http404("No midways found.")
595    else:
596        url = reverse('midway-map', args=(midways[0].slug, ))
597        return HttpResponseRedirect(url)
598
599
600class MidwayAssignmentFilter(django_filters.FilterSet):
601    name = django_filters.CharFilter(name='group__name', lookup_type='icontains', label="Name contains")
602    abbreviation = django_filters.CharFilter(name='group__abbreviation', lookup_type='iexact', label="Abbreviation is")
603    activity_category = django_filters.ModelChoiceFilter(
604        label='Activity category',
605        name='group__activity_category',
606        queryset=groups.models.ActivityCategory.objects,
607    )
608
609    class Meta:
610        model = forms.models.MidwayAssignment
611        fields = [
612            'name',
613            'abbreviation',
614            'activity_category',
615        ]
616        order_by = (
617            ('group__name', 'Name', ),
618            ('group__abbreviation', 'Abbreviation', ),
619            ('group__activity_category__name', 'Activity category', ),
620            ('location', 'Location', ),
621        )
622
623
624class MidwayMapView(DetailView):
625    context_object_name = "midway"
626    model = forms.models.Midway
627    template_name = 'midway/map.html'
628
629    def get_context_data(self, **kwargs):
630        # Call the base implementation first to get a context
631        context = super(MidwayMapView, self).get_context_data(**kwargs)
632       
633        filterset = MidwayAssignmentFilter(self.request.GET)
634        context['assignments'] = filterset.qs
635        context['filter'] = filterset
636        context['pagename'] = 'midway'
637
638        return context
639
640
641class MidwayAssignmentsUploadForm(Form):
642    def validate_csv_fields(upload_file):
643        reader = csv.reader(upload_file)
644        row = reader.next()
645        for col in ('Group', 'officers', 'Table', ):
646            if col not in row:
647                raise ValidationError('Please upload a CSV file with (at least) columns "Group", "officers", and "Table". (Missing at least "%s".)' % (col, ))
648
649    assignments = FileField(validators=[validate_csv_fields])
650
651@permission_required('forms.add_midwayassignment')
652def midway_assignment_upload(request, slug, ):
653    midway = get_object_or_404(forms.models.Midway, slug=slug, )
654
655    uploaded = False
656    found = []
657    issues = collections.defaultdict(list)
658
659    if request.method == 'POST': # If the form has been submitted...
660        form = MidwayAssignmentsUploadForm(request.POST, request.FILES, ) # A form bound to the POST data
661
662        if form.is_valid(): # All validation rules pass
663            uploaded = True
664            reader = csv.DictReader(request.FILES['assignments'])
665            for row in reader:
666                group_name = row['Group']
667                group_officers = row['officers']
668                table = row['Table']
669                issue = False
670                try:
671                    group = groups.models.Group.objects.get(name=group_name)
672                    assignment = forms.models.MidwayAssignment(
673                        midway=midway,
674                        location=table,
675                        group=group,
676                    )
677                    assignment.save()
678                    found.append(assignment)
679                    status = group.group_status.slug
680                    if status != 'active':
681                        issue = 'status=%s (added anyway)' % (status, )
682                except groups.models.Group.DoesNotExist:
683                    issue = 'unknown group (ignored)'
684                except groups.models.Group.MultipleObjectsReturned:
685                    issue = 'multiple groups found (ignored)'
686                if issue:
687                    issues[issue].append((group_name, group_officers, table))
688            for issue in issues:
689                issues[issue] = sorted(issues[issue], key=lambda x: x[0])
690
691    else:
692        form = MidwayAssignmentsUploadForm() # An unbound form
693
694    context = {
695        'midway':midway,
696        'form':form,
697        'uploaded': uploaded,
698        'found': found,
699        'issues': dict(issues),
700        'pagename':'midway',
701    }
702    return render_to_response('midway/upload.html', context, context_instance=RequestContext(request), )
Note: See TracBrowser for help on using the repository browser.