source: asadb/forms/views.py @ 0e2426b

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

Save some unnecessary queries with select_related

Computing how many people have confirmed still requires O(n) queries, but at
least this gets rid of a few of the things that required O(n) queries.

  • Property mode set to 100644
File size: 25.3 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    message = ""
421    message_type = "info"
422
423    if update_obj.valid <= forms.models.VALID_UNSET:
424        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."
425        message_type = "warn"
426
427    # Handle the single group add/remove forms
428    # * removing previously confirmed groups
429    # * add/remove groups that list the user in a role
430    # * add/remove groups the user searched for
431    if request.method == 'POST' and 'add-remove' in request.POST:
432        group = groups.models.Group.objects.get(id=request.POST['group'])
433        if request.POST['action'] == 'remove':
434            if group in update_obj.groups.all():
435                update_obj.groups.remove(group)
436                message = "You have successfully unconfirmed membership in %s." % (group, )
437            else:
438                message = "Removal failed because you had not confirmed membership in %s." % (group, )
439                message_type = "warn"
440        elif request.POST['action'] == 'add':
441            if group in update_obj.groups.all():
442                message = "Membership in %s already confirmed." % (group, )
443                message_type = "warn"
444            else:
445                update_obj.groups.add(group)
446                message = "You have successfully confirmed membership in %s." % (group, )
447        else:
448            message = "Uh, somehow you tried to do something besides adding and removing..."
449            message_type = "alert"
450
451    # Handle the big list of groups
452    if request.method == 'POST' and 'list' in request.POST:
453        form = Form_PersonMembershipUpdate(request.POST, request.FILES, instance=update_obj)
454        if form.is_valid():
455            request_obj = form.save()
456            message = "Update saved"
457    else:
458        form = Form_PersonMembershipUpdate(initial=initial, instance=update_obj, )
459    form.fields['groups'].widget.attrs['size'] = 20
460
461    # Render the page
462    context = {
463        'role_groups':role_groups,
464        'form':form,
465        'member_groups':selected_groups,
466        'message': message,
467        'message_type': message_type,
468        'pagename':'groups',
469    }
470    return render_to_response('membership/confirm.html', context, context_instance=RequestContext(request), )
471
472
473class View_GroupMembershipList(ListView):
474    context_object_name = "group_list"
475    template_name = "membership/submitted.html"
476
477    def get_queryset(self):
478        cycle = forms.models.GroupConfirmationCycle.latest()
479        group_updates = forms.models.GroupMembershipUpdate.objects.all()
480        group_updates = group_updates.filter(
481            cycle=cycle,
482            group__personmembershipupdate__cycle=cycle,
483            group__personmembershipupdate__deleted__isnull=True,
484            group__personmembershipupdate__valid__gt=0,
485        )
486        group_updates = group_updates.annotate(num_confirms=Count('group__personmembershipupdate'))
487        #print len(list(group_updates))
488        #for query in connection.queries: print query
489        return group_updates
490
491
492class View_GroupConfirmationCyclesList(ListView):
493    context_object_name = "cycle_list"
494    template_name = "membership/admin.html"
495    model = forms.models.GroupConfirmationCycle
496
497    def get_context_data(self, **kwargs):
498        context = super(View_GroupConfirmationCyclesList, self).get_context_data(**kwargs)
499        context['pagename'] = 'groups'
500        return context
501
502
503@permission_required('groups.view_group_private_info')
504def group_confirmation_issues(request, slug, ):
505    account_numbers = ("accounts" in request.GET) and request.GET['accounts'] == "1"
506
507    check_groups = groups.models.Group.objects.filter(group_status__slug__in=('active', 'suspended', ))
508    check_groups = check_groups.select_related('group_status')
509    group_updates = forms.models.GroupMembershipUpdate.objects.filter(cycle__slug=slug, )
510    group_updates = group_updates.select_related('group', 'group__group_status')
511    people_confirmations = forms.models.PersonMembershipUpdate.objects.filter(
512        deleted__isnull=True,
513        valid__gt=0,
514        cycle__slug=slug,
515    )
516
517    buf = StringIO.StringIO()
518    output = csv.writer(buf)
519    fields = ['group_id', 'group_name', 'group_status', 'issue', 'num_confirm', 'officer_email', ]
520    if account_numbers: fields.append("main_account")
521    output.writerow(fields)
522
523    def output_issue(group, issue, num_confirms):
524        fields = [
525            group.id,
526            group.name,
527            group.group_status.slug,
528            issue,
529            num_confirms,
530            group.officer_email,
531        ]
532        if account_numbers: fields.append(group.main_account_id)
533        output.writerow(fields)
534
535    q_present = Q(id__in=group_updates.values('group'))
536    missing_groups = check_groups.filter(~q_present)
537    #print len(list(group_updates))
538    for group in missing_groups:
539        #num_confirms = len(people_confirmations.filter(groups=group))
540        output_issue(group, 'unsubmitted', '')
541
542    for group_update in group_updates:
543        group = group_update.group
544        num_confirms = len(people_confirmations.filter(groups=group))
545        problems = []
546
547        if num_confirms < 5:
548            problems.append("confirmations")
549
550        num_students = group_update.num_undergrads + group_update.num_grads
551        num_other = group_update.num_alum + group_update.num_other_affiliate + group_update.num_other
552        if num_students < num_other:
553            problems.append("50%")
554
555        for problem in problems:
556            output_issue(group, problem, num_confirms)
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.