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