source: asadb/forms/views.py @ e0ed952

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

FYSM: don't request or link slides (ASA-#242)

I haven't removed the infrastructure to *display* them, because I feel a little
silly making old slides inaccessible. Perhaps ASA-#23 can make slides from the
appropriate years get linked.

  • Property mode set to 100644
File size: 25.2 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.involved_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        msg="The list below contains only groups that list you as being involved. You must be an administrator of a group to submit an update.",
264    )
265
266class Form_GroupMembershipUpdate(ModelForm):
267    def __init__(self, *args, **kwargs):
268        super(Form_GroupMembershipUpdate, self).__init__(*args, **kwargs)
269        self.fields['no_hazing'].required = True
270
271    class Meta:
272        model = forms.models.GroupMembershipUpdate
273        fields = [
274            'updater_title',
275            'group_email',
276            'officer_email',
277            'email_preface',
278            'no_hazing',
279            'no_discrimination',
280            'membership_definition',
281            'num_undergrads',
282            'num_grads',
283            'num_alum',
284            'num_other_affiliate',
285            'num_other',
286            'membership_list',
287        ]
288
289@login_required
290def group_membership_update(request, cycle_slug, pk, ):
291    cycle = get_object_or_404(forms.models.GroupConfirmationCycle, slug=cycle_slug)
292    group_obj = get_object_or_404(groups.models.Group, pk=pk)
293    if not request.user.has_perm('groups.admin_group', group_obj):
294        raise PermissionDenied
295
296    try:
297        update_obj = forms.models.GroupMembershipUpdate.objects.get(group=group_obj, cycle=cycle, )
298    except forms.models.GroupMembershipUpdate.DoesNotExist:
299        update_obj = None
300
301    confirm_uri = request.build_absolute_uri(reverse('membership-confirm'))
302    submitted_uri = request.build_absolute_uri(reverse('membership-submitted'))
303
304    if request.method == 'POST':
305        form = Form_GroupMembershipUpdate(request.POST, request.FILES, instance=update_obj) # A form bound to the POST data
306
307        if form.is_valid(): # All validation rules pass
308            # Update the updater info
309            form.instance.group = group_obj
310            form.instance.cycle = cycle
311            form.instance.update_time  = datetime.datetime.now()
312            form.instance.updater_name = request.user.username
313            request_obj = form.save()
314
315            # Send email
316            tmpl = get_template('membership/anti-hazing.txt')
317            ctx = Context({
318                'update': request_obj,
319                'group': group_obj,
320                'submitter': request.user,
321            })
322            body = tmpl.render(ctx)
323            email = EmailMessage(
324                subject='Anti-Hazing and Non-Discrimination Acknowledgement for %s' % (
325                    group_obj.name,
326                ),
327                body=body,
328                from_email=request.user.email,
329                to=[request_obj.group_email, ],
330                cc=[request_obj.officer_email, ],
331                bcc=['asa-db-outgoing@mit.edu', ],
332            )
333            email.send()
334
335            # Send email
336            tmpl = get_template('membership/submit-confirm-email.txt')
337            ctx = Context({
338                'update': request_obj,
339                'group': group_obj,
340                'submitter': request.user,
341                'confirm_uri': confirm_uri,
342                'submitted_uri': submitted_uri,
343            })
344            body = tmpl.render(ctx)
345            email = EmailMessage(
346                subject='ASA Membership Information for %s' % (
347                    group_obj.name,
348                ),
349                body=body,
350                from_email=request.user.email,
351                to=[request_obj.officer_email, ],
352                bcc=['asa-db-outgoing@mit.edu', ],
353            )
354            email.send()
355
356            return HttpResponseRedirect(reverse('membership-thanks', )) # Redirect after POST
357
358    else:
359        form = Form_GroupMembershipUpdate(instance=update_obj)
360
361    context = {
362        'form':form,
363        'group':group_obj,
364        'cycle':cycle,
365        'confirm_uri': confirm_uri,
366        'pagename':'groups',
367    }
368    return render_to_response('membership/update.html', context, context_instance=RequestContext(request), )
369
370class Form_PersonMembershipUpdate(ModelForm):
371    groups = ModelMultipleChoiceField(queryset=membership_update_qs)
372    class Meta:
373        model = forms.models.PersonMembershipUpdate
374        fields = [
375            'groups',
376        ]
377
378@login_required
379def person_membership_update(request, ):
380    initial = {
381    }
382    cycle = forms.models.GroupConfirmationCycle.latest()
383
384    # Initialize/find the PersonMembershipUpdate for this user
385    try:
386        update_obj = forms.models.PersonMembershipUpdate.objects.get(
387            username=request.user.username,
388            deleted__isnull=True,
389            cycle=cycle,
390        )
391        selected_groups = update_obj.groups.all()
392    except forms.models.PersonMembershipUpdate.DoesNotExist:
393        update_obj = forms.models.PersonMembershipUpdate()
394        update_obj.update_time  = datetime.datetime.now()
395        update_obj.username = request.user.username
396        update_obj.cycle = cycle
397        selected_groups = []
398
399    # Determine whether the submitter is a student or not
400    accounts = groups.models.AthenaMoiraAccount
401    try:
402        person = accounts.active_accounts.get(username=request.user.username)
403        if person.is_student():
404            update_obj.valid = forms.models.VALID_AUTOVALIDATED
405        else:
406            update_obj.valid = forms.models.VALID_AUTOREJECTED
407    except accounts.DoesNotExist:
408        pass
409        update_obj.valid = forms.models.VALID_AUTOREJECTED
410
411    update_obj.save()
412
413    # Find groups that list a role for the user
414    office_holders = groups.models.OfficeHolder.current_holders.filter(person=request.user.username)
415    role_groups = {}
416    for office_holder in office_holders:
417        if office_holder.group.pk not in role_groups:
418            role_groups[office_holder.group.pk] = (office_holder.group, set())
419        role_groups[office_holder.group.pk][1].add(office_holder.role.display_name)
420
421    # Find groups the user searched for
422    filterset = groups.views.GroupFilter(request.GET, membership_update_qs)
423    filtered_groups = filterset.qs.all()
424    show_filtered_groups = ('search' in request.GET)
425
426    message = ""
427    message_type = "info"
428
429    if update_obj.valid <= forms.models.VALID_UNSET:
430        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."
431        message_type = "warn"
432
433    # Handle the single group add/remove forms
434    # * removing previously confirmed groups
435    # * add/remove groups that list the user in a role
436    # * add/remove groups the user searched for
437    if request.method == 'POST' and 'add-remove' in request.POST:
438        group = groups.models.Group.objects.get(id=request.POST['group'])
439        if request.POST['action'] == 'remove':
440            if group in update_obj.groups.all():
441                update_obj.groups.remove(group)
442                message = "You have successfully unconfirmed membership in %s." % (group, )
443            else:
444                message = "Removal failed because you had not confirmed membership in %s." % (group, )
445                message_type = "warn"
446        elif request.POST['action'] == 'add':
447            if group in update_obj.groups.all():
448                message = "Membership in %s already confirmed." % (group, )
449                message_type = "warn"
450            else:
451                update_obj.groups.add(group)
452                message = "You have successfully confirmed membership in %s." % (group, )
453        else:
454            message = "Uh, somehow you tried to do something besides adding and removing..."
455            message_type = "alert"
456
457    # Handle the big list of groups
458    if request.method == 'POST' and 'list' in request.POST:
459        form = Form_PersonMembershipUpdate(request.POST, request.FILES, instance=update_obj)
460        if form.is_valid():
461            request_obj = form.save()
462            message = "Update saved"
463    else:
464        form = Form_PersonMembershipUpdate(initial=initial, instance=update_obj, )
465    form.fields['groups'].widget.attrs['size'] = 20
466
467    # Render the page
468    context = {
469        'role_groups':role_groups,
470        'form':form,
471        'filter':filterset,
472        'show_filtered_groups':show_filtered_groups,
473        'filtered_groups':filtered_groups,
474        'member_groups':selected_groups,
475        'message': message,
476        'message_type': message_type,
477        'pagename':'groups',
478    }
479    return render_to_response('membership/confirm.html', context, context_instance=RequestContext(request), )
480
481
482class View_GroupMembershipList(ListView):
483    context_object_name = "group_list"
484    template_name = "membership/submitted.html"
485
486    def get_queryset(self):
487        cycle = forms.models.GroupConfirmationCycle.latest()
488        group_updates = forms.models.GroupMembershipUpdate.objects.all()
489        group_updates = group_updates.filter(
490            cycle=cycle,
491            group__personmembershipupdate__cycle=cycle,
492            group__personmembershipupdate__deleted__isnull=True,
493            group__personmembershipupdate__valid__gt=0,
494        )
495        group_updates = group_updates.annotate(num_confirms=Count('group__personmembershipupdate'))
496        #print len(list(group_updates))
497        #for query in connection.queries: print query
498        return group_updates
499
500
501@permission_required('groups.view_group_private_info')
502def group_confirmation_issues(request, ):
503    account_numbers = ("accounts" in request.GET) and request.GET['accounts'] == "1"
504
505    active_groups = groups.models.Group.active_groups
506    group_updates = forms.models.GroupMembershipUpdate.objects.all()
507    people_confirmations = forms.models.PersonMembershipUpdate.objects.filter(
508        deleted__isnull=True,
509        valid__gt=0,
510    )
511
512    buf = StringIO.StringIO()
513    output = csv.writer(buf)
514    fields = ['group_id', 'group_name', 'issue', 'num_confirm', 'officer_email', ]
515    if account_numbers: fields.append("main_account")
516    output.writerow(fields)
517
518    q_present = Q(id__in=group_updates.values('group'))
519    missing_groups = active_groups.filter(~q_present)
520    #print len(list(group_updates))
521    for group in missing_groups:
522        num_confirms = len(people_confirmations.filter(groups=group))
523        fields = [
524            group.id,
525            group.name,
526            'unsubmitted',
527            num_confirms,
528            group.officer_email,
529        ]
530        if account_numbers: fields.append(group.main_account_id)
531        output.writerow(fields)
532
533    for group_update in group_updates:
534        group = group_update.group
535        num_confirms = len(people_confirmations.filter(groups=group))
536        problems = []
537
538        if num_confirms < 5:
539            problems.append("confirmations")
540
541        num_students = group_update.num_undergrads + group_update.num_grads
542        num_other = group_update.num_alum + group_update.num_other_affiliate + group_update.num_other
543        if num_students < num_other:
544            problems.append("50%")
545
546        for problem in problems:
547            fields = [
548                group.id,
549                group.name,
550                problem,
551                num_confirms,
552                group.officer_email,
553            ]
554            if account_numbers: fields.append(group.main_account_id)
555            output.writerow(fields)
556
557
558    return HttpResponse(buf.getvalue(), mimetype='text/csv', )
559
560
561
562##########
563# Midway #
564##########
565
566
567class View_Midways(ListView):
568    context_object_name = "midway_list"
569    template_name = "midway/midway_list.html"
570
571    def get_queryset(self):
572        midways = forms.models.Midway.objects.order_by('date')
573        return midways
574
575    def get_context_data(self, **kwargs):
576        context = super(View_Midways, self).get_context_data(**kwargs)
577        context['pagename'] = 'midway'
578        return context
579
580def midway_map_latest(request, ):
581    midways = forms.models.Midway.objects.order_by('-date')[:1]
582    if len(midways) == 0:
583        raise Http404("No midways found.")
584    else:
585        url = reverse('midway-map', args=(midways[0].slug, ))
586        return HttpResponseRedirect(url)
587
588
589class MidwayAssignmentFilter(django_filters.FilterSet):
590    name = django_filters.CharFilter(name='group__name', lookup_type='icontains', label="Name contains")
591    abbreviation = django_filters.CharFilter(name='group__abbreviation', lookup_type='iexact', label="Abbreviation is")
592    activity_category = django_filters.ModelChoiceFilter(
593        label='Activity category',
594        name='group__activity_category',
595        queryset=groups.models.ActivityCategory.objects,
596    )
597
598    class Meta:
599        model = forms.models.MidwayAssignment
600        fields = [
601            'name',
602            'abbreviation',
603            'activity_category',
604        ]
605        order_by = (
606            ('group__name', 'Name', ),
607            ('group__abbreviation', 'Abbreviation', ),
608            ('group__activity_category__name', 'Activity category', ),
609            ('location', 'Location', ),
610        )
611
612
613class MidwayMapView(DetailView):
614    context_object_name = "midway"
615    model = forms.models.Midway
616    template_name = 'midway/map.html'
617
618    def get_context_data(self, **kwargs):
619        # Call the base implementation first to get a context
620        context = super(MidwayMapView, self).get_context_data(**kwargs)
621       
622        filterset = MidwayAssignmentFilter(self.request.GET)
623        context['assignments'] = filterset.qs
624        context['filter'] = filterset
625        context['pagename'] = 'midway'
626
627        return context
628
629
630class MidwayAssignmentsUploadForm(Form):
631    def validate_csv_fields(upload_file):
632        reader = csv.reader(upload_file)
633        row = reader.next()
634        for col in ('Group', 'officers', 'Table', ):
635            if col not in row:
636                raise ValidationError('Please upload a CSV file with (at least) columns "Group", "officers", and "Table". (Missing at least "%s".)' % (col, ))
637
638    assignments = FileField(validators=[validate_csv_fields])
639
640@permission_required('forms.add_midwayassignment')
641def midway_assignment_upload(request, slug, ):
642    midway = get_object_or_404(forms.models.Midway, slug=slug, )
643
644    uploaded = False
645    found = []
646    issues = collections.defaultdict(list)
647
648    if request.method == 'POST': # If the form has been submitted...
649        form = MidwayAssignmentsUploadForm(request.POST, request.FILES, ) # A form bound to the POST data
650
651        if form.is_valid(): # All validation rules pass
652            uploaded = True
653            reader = csv.DictReader(request.FILES['assignments'])
654            for row in reader:
655                group_name = row['Group']
656                group_officers = row['officers']
657                table = row['Table']
658                issue = False
659                try:
660                    group = groups.models.Group.objects.get(name=group_name)
661                    assignment = forms.models.MidwayAssignment(
662                        midway=midway,
663                        location=table,
664                        group=group,
665                    )
666                    assignment.save()
667                    found.append(assignment)
668                    status = group.group_status.slug
669                    if status != 'active':
670                        issue = 'status=%s (added anyway)' % (status, )
671                except groups.models.Group.DoesNotExist:
672                    issue = 'unknown group (ignored)'
673                except groups.models.Group.MultipleObjectsReturned:
674                    issue = 'multiple groups found (ignored)'
675                if issue:
676                    issues[issue].append((group_name, group_officers, table))
677            for issue in issues:
678                issues[issue] = sorted(issues[issue], key=lambda x: x[0])
679
680    else:
681        form = MidwayAssignmentsUploadForm() # An unbound form
682
683    context = {
684        'midway':midway,
685        'form':form,
686        'uploaded': uploaded,
687        'found': found,
688        'issues': dict(issues),
689        'pagename':'midway',
690    }
691    return render_to_response('midway/upload.html', context, context_instance=RequestContext(request), )
Note: See TracBrowser for help on using the repository browser.