source: asadb/forms/views.py @ bda4d86

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

Finish support for group membership update cycles

The revised workflow is that you visit a group selection page, and select a
group to update. That checks authz (ASA-#169) and then loads any old update in
the appropriate cycle (ASA-#27).

  • Property mode set to 100644
File size: 20.2 KB
Line 
1import forms.models
2import groups.models
3import groups.views
4import settings
5import util.emails
6
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 Form
18from django.forms import ModelForm
19from django.forms import ModelChoiceField, ModelMultipleChoiceField
20from django.forms import ValidationError
21from django.db import connection
22from django.db.models import Q, Count
23
24import csv
25import datetime
26import StringIO
27
28#################
29# GENERIC VIEWS #
30#################
31
32class SelectGroupForm(Form):
33    group = ModelChoiceField(queryset=groups.models.Group.objects.all())
34    def __init__(self, *args, **kwargs):
35        queryset = None
36        if 'queryset' in kwargs:
37            queryset = kwargs['queryset']
38            del kwargs['queryset']
39        super(SelectGroupForm, self).__init__(*args, **kwargs)
40        if queryset is not None:
41            self.fields["group"].queryset = queryset
42
43def select_group(request, url_name_after, url_args=[], pagename='homepage', queryset=None, title="", msg=""):
44    if request.method == 'POST': # If the form has been submitted...
45        # A form bound to the POST data
46        form = SelectGroupForm(request.POST, queryset=queryset, )
47        if form.is_valid(): # All validation rules pass
48            group = form.cleaned_data['group'].id
49            return HttpResponseRedirect(reverse(url_name_after, args=url_args+[group],)) # Redirect after POST
50    else:
51        form = SelectGroupForm(queryset=queryset, ) # An unbound form
52
53    if not title: title = "Select group"
54    context = {
55        'form':form,
56        'title':title,
57        'msg':msg,
58        'pagename':pagename,
59    }
60    return render_to_response('forms/select.html', context, context_instance=RequestContext(request), )
61
62#############################
63# FIRST-YEAR SUMMER MAILING #
64#############################
65
66@login_required
67def fysm_by_years(request, year, category, ):
68    if year is None: year = datetime.date.today().year
69    queryset = forms.models.FYSM.objects.filter(year=year).order_by('group__name')
70    category_obj = None
71    category_name = 'main'
72    if category != None:
73        category_obj = get_object_or_404(forms.models.FYSMCategory, slug=category)
74        category_name = category_obj.name
75        queryset = queryset.filter(categories=category_obj)
76    forms.models.FYSMView.record_metric(request=request, fysm=None, year=year, page=category_name, )
77    categories = forms.models.FYSMCategory.objects.all()
78    return list_detail.object_list(
79        request,
80        queryset=queryset,
81        template_name="fysm/fysm_listing.html",
82        template_object_name="fysm",
83        extra_context={
84            "year": year,
85            "pagename": "fysm",
86            "category": category_obj,
87            "categories": categories,
88        }
89    )
90
91@login_required
92def fysm_view(request, year, submission, ):
93    submit_obj = get_object_or_404(forms.models.FYSM, pk=submission,)
94    all = forms.models.FYSM.objects.only("id", "display_name", )
95    try:
96        prev = all.filter(display_name__lt=submit_obj.display_name).order_by("-display_name")[0]
97    except IndexError:
98        prev = None
99    try:
100        next = all.filter(display_name__gt=submit_obj.display_name).order_by("display_name")[0]
101    except IndexError:
102        next = None
103    forms.models.FYSMView.record_metric(request=request, fysm=submit_obj, year=year, page="detail", )
104    return list_detail.object_detail(
105        request,
106        forms.models.FYSM.objects,
107        object_id=submission,
108        template_name="fysm/fysm_detail.html",
109        template_object_name="fysm",
110        extra_context={
111            "year": year,
112            "pagename": "fysm",
113            "prev": prev,
114            "next": next,
115        },
116    )
117
118def fysm_link(request, year, link_type, submission, ):
119    submit_obj = get_object_or_404(forms.models.FYSM, pk=submission,)
120    if submit_obj.year != int(year):
121        raise Http404("Year mismatch: fysm.year='%s', request's year='%s'" % (submit_obj.year, year, ))
122    if link_type == 'join':
123        url = submit_obj.join_url
124    elif link_type == 'website':
125        url = submit_obj.website
126    else:
127        raise Http404("Unknown link type")
128    forms.models.FYSMView.record_metric(request=request, fysm=submit_obj, year=year, page=link_type, )
129    return HttpResponseRedirect(url)
130
131def select_group_fysm(request, ):
132    qobj = Q(activity_category__isnull = True) | ~(Q(activity_category__name='Dorm') | Q(activity_category__name='FSILG'))
133    queryset = groups.models.Group.active_groups.filter(qobj)
134    return select_group(
135        request,
136        url_name_after='fysm-manage',
137        pagename='fysm',
138        queryset=queryset,
139    )
140
141class FYSMRequestForm(ModelForm):
142    class Meta:
143        model = forms.models.FYSM
144        fields = (
145            'display_name',
146            'website',
147            'join_url',
148            'contact_email',
149            'description',
150            'logo',
151            'slide',
152            'tags',
153            'categories',
154        )
155
156    def clean_display_name(self, ):
157        name = self.cleaned_data['display_name']
158        if ',' in name:
159            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.""")
160        return name
161
162    def clean_description(self, ):
163        description = self.cleaned_data['description']
164        length = len(description)
165        if length > 400:
166            raise ValidationError("Descriptions are capped at 400 characters, and this one is %d characters." % (length, ))
167        return description
168
169@login_required
170def fysm_manage(request, group, ):
171    year = datetime.date.today().year
172    group_obj = get_object_or_404(groups.models.Group, pk=group)
173
174    initial = {}
175    try:
176        fysm_obj = forms.models.FYSM.objects.get(group=group_obj, year=year, )
177    except forms.models.FYSM.DoesNotExist:
178        fysm_obj = forms.models.FYSM()
179        fysm_obj.group = group_obj
180        fysm_obj.year = year
181        initial['display_name'] = group_obj.name
182        initial['year'] = year
183        initial['website'] = group_obj.website_url
184        initial['join_url'] = group_obj.website_url
185        initial['contact_email'] = group_obj.officer_email
186
187    if request.method == 'POST': # If the form has been submitted...
188        form = FYSMRequestForm(request.POST, request.FILES, instance=fysm_obj, ) # A form bound to the POST data
189
190        if form.is_valid(): # All validation rules pass
191            request_obj = form.save()
192
193            view_path = reverse('fysm-view', args=[year, request_obj.pk, ])
194            view_uri = '%s://%s%s' % (request.is_secure() and 'https' or 'http',
195                 request.get_host(), view_path)
196
197            # Send email
198            email = util.emails.email_from_template(
199                tmpl='fysm/update_email.txt',
200                context = Context({
201                    'group': group_obj,
202                    'fysm': fysm_obj,
203                    'view_uri': view_uri,
204                    'submitter': request.user,
205                    'request': request,
206                    'sender': "ASA FYSM team",
207                }),
208                subject='FYSM entry for "%s" updated by "%s"' % (
209                    group_obj.name,
210                    request.user,
211                ),
212                to=[group_obj.officer_email, request.user.email, ],
213                from_email='asa-fysm@mit.edu',
214            )
215            email.bcc = ['asa-fysm-submissions@mit.edu']
216            email.send()
217            return HttpResponseRedirect(reverse('fysm-thanks', args=[fysm_obj.pk],)) # Redirect after POST
218
219    else:
220        form = FYSMRequestForm(instance=fysm_obj, initial=initial, ) # An unbound form
221
222    context = {
223        'group':group_obj,
224        'fysm':fysm_obj,
225        'form':form,
226        'categories':forms.models.FYSMCategory.objects.all(),
227        'pagename':'fysm',
228    }
229    return render_to_response('fysm/submit.html', context, context_instance=RequestContext(request), )
230
231
232def fysm_thanks(request, fysm, ):
233    year = datetime.date.today().year
234    fysm_obj = get_object_or_404(forms.models.FYSM, pk=fysm)
235
236    context = {
237        'group':fysm_obj.group,
238        'fysm':fysm_obj,
239        'pagename':'fysm',
240    }
241    return render_to_response('fysm/thanks.html', context, context_instance=RequestContext(request), )
242
243#####################
244# Membership update #
245#####################
246
247membership_update_qs = groups.models.Group.objects.filter(group_status__slug__in=['active', 'suspended', ])
248
249@login_required
250def group_membership_update_select_group(request, ):
251    cycle = forms.models.GroupConfirmationCycle.latest()
252
253    users_groups = groups.models.Group.involved_groups(request.user.username)
254    qs = membership_update_qs.filter(pk__in=users_groups)
255
256    return select_group(request=request,
257        url_name_after='membership-update-group',
258        url_args=[cycle.slug],
259        pagename='groups',
260        queryset=qs,
261        title="Submit membership update for...",
262        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.",
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_path = reverse('membership-confirm', )
301    confirm_uri = '%s://%s%s' % (request.is_secure() and 'https' or 'http',
302         request.get_host(), confirm_path)
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            })
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        'confirm_uri': confirm_uri,
364        'pagename':'groups',
365    }
366    return render_to_response('membership/update.html', context, context_instance=RequestContext(request), )
367
368class Form_PersonMembershipUpdate(ModelForm):
369    groups = ModelMultipleChoiceField(queryset=membership_update_qs)
370    class Meta:
371        model = forms.models.PersonMembershipUpdate
372        fields = [
373            'groups',
374        ]
375
376@login_required
377def person_membership_update(request, ):
378    initial = {
379    }
380    cycle = forms.models.GroupConfirmationCycle.latest()
381
382    # Initialize/find the PersonMembershipUpdate for this user
383    try:
384        update_obj = forms.models.PersonMembershipUpdate.objects.get(
385            username=request.user.username,
386            deleted__isnull=True,
387            cycle=cycle,
388        )
389        selected_groups = update_obj.groups.all()
390    except forms.models.PersonMembershipUpdate.DoesNotExist:
391        update_obj = forms.models.PersonMembershipUpdate()
392        update_obj.update_time  = datetime.datetime.now()
393        update_obj.username = request.user.username
394        update_obj.cycle = cycle
395        selected_groups = []
396
397    # Determine whether the submitter is a student or not
398    accounts = groups.models.AthenaMoiraAccount
399    try:
400        person = accounts.active_accounts.get(username=request.user.username)
401        if person.is_student():
402            update_obj.valid = forms.models.VALID_AUTOVALIDATED
403        else:
404            update_obj.valid = forms.models.VALID_AUTOREJECTED
405    except accounts.DoesNotExist:
406        pass
407        update_obj.valid = forms.models.VALID_AUTOREJECTED
408
409    update_obj.save()
410
411    # Find groups that list a role for the user
412    office_holders = groups.models.OfficeHolder.current_holders.filter(person=request.user.username)
413    role_groups = {}
414    for office_holder in office_holders:
415        if office_holder.group.pk not in role_groups:
416            role_groups[office_holder.group.pk] = (office_holder.group, set())
417        role_groups[office_holder.group.pk][1].add(office_holder.role.display_name)
418
419    # Find groups the user searched for
420    filterset = groups.views.GroupFilter(request.GET, membership_update_qs)
421    filtered_groups = filterset.qs.all()
422    show_filtered_groups = ('search' in request.GET)
423
424    message = ""
425    message_type = "info"
426
427    if update_obj.valid <= forms.models.VALID_UNSET:
428        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."
429        message_type = "warn"
430
431    # Handle the single group add/remove forms
432    # * removing previously confirmed groups
433    # * add/remove groups that list the user in a role
434    # * add/remove groups the user searched for
435    if request.method == 'POST' and 'add-remove' in request.POST:
436        group = groups.models.Group.objects.get(id=request.POST['group'])
437        if request.POST['action'] == 'remove':
438            if group in update_obj.groups.all():
439                update_obj.groups.remove(group)
440                message = "You have successfully unconfirmed membership in %s." % (group, )
441            else:
442                message = "Removal failed because you had not confirmed membership in %s." % (group, )
443                message_type = "warn"
444        elif request.POST['action'] == 'add':
445            if group in update_obj.groups.all():
446                message = "Membership in %s already confirmed." % (group, )
447                message_type = "warn"
448            else:
449                update_obj.groups.add(group)
450                message = "You have successfully confirmed membership in %s." % (group, )
451        else:
452            message = "Uh, somehow you tried to do something besides adding and removing..."
453            message_type = "alert"
454
455    # Handle the big list of groups
456    if request.method == 'POST' and 'list' in request.POST:
457        form = Form_PersonMembershipUpdate(request.POST, request.FILES, instance=update_obj)
458        if form.is_valid():
459            request_obj = form.save()
460            message = "Update saved"
461    else:
462        form = Form_PersonMembershipUpdate(initial=initial, instance=update_obj, )
463
464    # Render the page
465    context = {
466        'role_groups':role_groups,
467        'form':form,
468        'filter':filterset,
469        'show_filtered_groups':show_filtered_groups,
470        'filtered_groups':filtered_groups,
471        'member_groups':selected_groups,
472        'message': message,
473        'message_type': message_type,
474        'pagename':'groups',
475    }
476    return render_to_response('membership/confirm.html', context, context_instance=RequestContext(request), )
477
478
479class View_GroupMembershipList(ListView):
480    context_object_name = "group_list"
481    template_name = "membership/submitted.html"
482
483    def get_queryset(self):
484        cycle = forms.models.GroupConfirmationCycle.latest()
485        group_updates = forms.models.GroupMembershipUpdate.objects.all()
486        group_updates = group_updates.filter(
487            cycle=cycle,
488            group__personmembershipupdate__cycle=cycle,
489            group__personmembershipupdate__deleted__isnull=True,
490            group__personmembershipupdate__valid__gt=0,
491        )
492        group_updates = group_updates.annotate(num_confirms=Count('group__personmembershipupdate'))
493        #print len(list(group_updates))
494        #for query in connection.queries: print query
495        return group_updates
496
497
498@permission_required('groups.view_group_private_info')
499def group_confirmation_issues(request, ):
500    active_groups = groups.models.Group.active_groups
501    group_updates = forms.models.GroupMembershipUpdate.objects.all()
502    people_confirmations = forms.models.PersonMembershipUpdate.objects.filter(
503        deleted__isnull=True,
504        valid__gt=0,
505    )
506
507    buf = StringIO.StringIO()
508    output = csv.writer(buf)
509    output.writerow(['group_id', 'group_name', 'issue', 'num_confirm', 'officer_email', ])
510
511    q_present = Q(id__in=group_updates.values('group'))
512    missing_groups = active_groups.filter(~q_present)
513    #print len(list(group_updates))
514    for group in missing_groups:
515        num_confirms = len(people_confirmations.filter(groups=group))
516        output.writerow([
517            group.id,
518            group.name,
519            'unsubmitted',
520            num_confirms,
521            group.officer_email,
522        ])
523
524    for group_update in group_updates:
525        group = group_update.group
526        num_confirms = len(people_confirmations.filter(groups=group))
527        problems = []
528
529        if num_confirms < 5:
530            problems.append("confirmations")
531
532        num_students = group_update.num_undergrads + group_update.num_grads
533        num_other = group_update.num_alum + group_update.num_other_affiliate + group_update.num_other
534        if num_students < num_other:
535            problems.append("50%")
536
537        for problem in problems:
538            output.writerow([
539                group.id,
540                group.name,
541                problem,
542                num_confirms,
543                group.officer_email,
544            ])
545
546
547    return HttpResponse(buf.getvalue(), mimetype='text/csv', )
Note: See TracBrowser for help on using the repository browser.