1 | # Create your views here. |
---|
2 | |
---|
3 | import collections |
---|
4 | import csv |
---|
5 | import datetime |
---|
6 | |
---|
7 | from django.contrib.auth.decorators import user_passes_test, login_required, permission_required |
---|
8 | from django.contrib.contenttypes.models import ContentType |
---|
9 | from django.core.exceptions import PermissionDenied |
---|
10 | from django.views.generic import ListView, DetailView |
---|
11 | from django.shortcuts import render_to_response, get_object_or_404, redirect |
---|
12 | from django.template import RequestContext |
---|
13 | from django.template import Context, Template |
---|
14 | from django.template.loader import get_template |
---|
15 | from django.http import HttpResponse, Http404, HttpResponseRedirect |
---|
16 | from django.core.urlresolvers import reverse |
---|
17 | from django.core.validators import URLValidator, EmailValidator, email_re |
---|
18 | from django.core.mail import EmailMessage, mail_admins |
---|
19 | from django import forms |
---|
20 | from django.forms import ValidationError |
---|
21 | from django.db import connection |
---|
22 | from django.db.models import Q |
---|
23 | from django.utils import html |
---|
24 | from django.utils.safestring import mark_safe |
---|
25 | |
---|
26 | import form_utils.forms |
---|
27 | import reversion.models |
---|
28 | import django_filters |
---|
29 | |
---|
30 | import groups.models |
---|
31 | from util.db_form_utils import StaticWidget |
---|
32 | import util.db_filters |
---|
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 | def view_roles_descriptions(request, ): |
---|
72 | roles = groups.models.OfficerRole.objects.all() |
---|
73 | context = { |
---|
74 | 'pagename': 'about', |
---|
75 | 'roles': roles, |
---|
76 | } |
---|
77 | return render_to_response('about/roles_descriptions.html', context, context_instance=RequestContext(request), ) |
---|
78 | |
---|
79 | ################ |
---|
80 | # Single group # |
---|
81 | ################ |
---|
82 | |
---|
83 | class GroupDetailView(DetailView): |
---|
84 | context_object_name = "group" |
---|
85 | model = groups.models.Group |
---|
86 | def get_context_data(self, **kwargs): |
---|
87 | # Call the base implementation first to get a context |
---|
88 | context = super(GroupDetailView, self).get_context_data(**kwargs) |
---|
89 | group = context['group'] |
---|
90 | context['pagename'] = "groups" |
---|
91 | |
---|
92 | # Indicate whether this person should be able to see "private" info |
---|
93 | context['viewpriv'] = self.request.user.has_perm('groups.view_group_private_info', group) |
---|
94 | context['adminpriv'] = self.request.user.has_perm('groups.admin_group', group) |
---|
95 | context['notes'] = group.viewable_notes(self.request.user) |
---|
96 | |
---|
97 | # People involved in the group |
---|
98 | just_roles = groups.models.OfficerRole.objects.all() |
---|
99 | if context['viewpriv'] or self.request.user.has_perm('groups.view_signatories'): |
---|
100 | # Can see the non-public stuff |
---|
101 | pass |
---|
102 | else: |
---|
103 | just_roles = just_roles.filter(publicly_visible=True) |
---|
104 | roles = [] |
---|
105 | for role in just_roles: |
---|
106 | roles.append((role.display_name, role, group.officers(role=role), )) |
---|
107 | context['roles'] = roles |
---|
108 | context['my_roles'] = [] |
---|
109 | if self.request.user.is_authenticated(): |
---|
110 | context['my_roles'] = group.officers(person=self.request.user.username).select_related('role') |
---|
111 | |
---|
112 | return context |
---|
113 | |
---|
114 | |
---|
115 | class GroupChangeMainForm(form_utils.forms.BetterModelForm): |
---|
116 | def __init__(self, *args, **kwargs): |
---|
117 | change_restricted = False |
---|
118 | if 'change_restricted' in kwargs: |
---|
119 | change_restricted = kwargs['change_restricted'] |
---|
120 | del kwargs['change_restricted'] |
---|
121 | super(GroupChangeMainForm, self).__init__(*args, **kwargs) |
---|
122 | restricted_fields = list(self.nobody_fields) |
---|
123 | if change_restricted: |
---|
124 | restricted_fields.extend(self.exec_only_fields) |
---|
125 | for field_name in restricted_fields: |
---|
126 | formfield = self.fields[field_name] |
---|
127 | value = getattr(self.instance, field_name) |
---|
128 | StaticWidget.replace_widget(formfield, value) |
---|
129 | for field in self.force_required: |
---|
130 | self.fields[field].required = True |
---|
131 | self.fields['constitution_url'].help_text = mark_safe("Please put your current constitution URL or AFS path.") |
---|
132 | |
---|
133 | exec_only_fields = [ |
---|
134 | 'name', 'abbreviation', |
---|
135 | 'group_status', 'group_class', |
---|
136 | 'group_funding', 'main_account_id', 'funding_account_id', |
---|
137 | ] |
---|
138 | nobody_fields = [ |
---|
139 | 'recognition_date', |
---|
140 | ] |
---|
141 | force_required = [ |
---|
142 | 'activity_category', 'description', |
---|
143 | 'num_undergrads', 'num_grads', 'num_community', 'num_other', |
---|
144 | 'website_url', 'officer_email', 'group_email', |
---|
145 | 'constitution_url', 'athena_locker', |
---|
146 | ] |
---|
147 | |
---|
148 | |
---|
149 | class Meta: |
---|
150 | fieldsets = [ |
---|
151 | ('basic', { |
---|
152 | 'legend': 'Basic Information', |
---|
153 | 'fields': ['name', 'abbreviation', 'activity_category', 'description', ], |
---|
154 | }), |
---|
155 | ('size', { |
---|
156 | 'legend':'Membership Numbers', |
---|
157 | '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.', |
---|
158 | 'fields': ['num_undergrads', 'num_grads', 'num_community', 'num_other',], |
---|
159 | }), |
---|
160 | ('contact', { |
---|
161 | 'legend': 'Contact Information', |
---|
162 | 'fields': ['website_url', 'meeting_times', 'officer_email', 'group_email', ], |
---|
163 | }), |
---|
164 | ('recognition', { |
---|
165 | 'legend': 'Recognition', |
---|
166 | 'fields': ['group_status', 'group_class', 'recognition_date', ], |
---|
167 | }), |
---|
168 | ('financial', { |
---|
169 | 'legend': 'Financial Information', |
---|
170 | 'fields': ['group_funding', 'main_account_id', 'funding_account_id', ], |
---|
171 | }), |
---|
172 | ('more-info', { |
---|
173 | 'legend': 'Additional Information', |
---|
174 | 'fields': ['constitution_url', 'advisor_name', 'athena_locker', ], |
---|
175 | }), |
---|
176 | ] |
---|
177 | model = groups.models.Group |
---|
178 | |
---|
179 | @login_required |
---|
180 | def manage_main(request, pk, ): |
---|
181 | group = get_object_or_404(groups.models.Group, pk=pk) |
---|
182 | |
---|
183 | if not request.user.has_perm('groups.admin_group', group): |
---|
184 | raise PermissionDenied |
---|
185 | change_restricted = True |
---|
186 | if request.user.has_perm('groups.change_group', group): |
---|
187 | change_restricted = False |
---|
188 | |
---|
189 | msg = None |
---|
190 | |
---|
191 | initial = {} |
---|
192 | if request.method == 'POST': # If the form has been submitted... |
---|
193 | # A form bound to the POST data |
---|
194 | form = GroupChangeMainForm( |
---|
195 | request.POST, request.FILES, |
---|
196 | change_restricted=change_restricted, |
---|
197 | instance=group, |
---|
198 | ) |
---|
199 | |
---|
200 | if form.is_valid(): # All validation rules pass |
---|
201 | request_obj = form.save(commit=False) |
---|
202 | request_obj.set_updater(request.user) |
---|
203 | request_obj.save() |
---|
204 | form.save_m2m() |
---|
205 | msg = "Thanks for editing!" |
---|
206 | else: |
---|
207 | msg = "Validation failed. See below for details." |
---|
208 | |
---|
209 | else: |
---|
210 | form = GroupChangeMainForm(change_restricted=change_restricted, instance=group, initial=initial, ) # An unbound form |
---|
211 | |
---|
212 | context = { |
---|
213 | 'group': group, |
---|
214 | 'form': form, |
---|
215 | 'msg': msg, |
---|
216 | 'pagename': "groups", |
---|
217 | } |
---|
218 | return render_to_response('groups/group_change_main.html', context, context_instance=RequestContext(request), ) |
---|
219 | |
---|
220 | # Helper for manage_officers view |
---|
221 | def manage_officers_load_officers(group, ): |
---|
222 | officers = group.officers() |
---|
223 | people = sorted(set([ officer.person for officer in officers ])) |
---|
224 | roles = groups.models.OfficerRole.objects.all() |
---|
225 | |
---|
226 | name_map = {} |
---|
227 | for name in people: |
---|
228 | name_map[name] = groups.models.AthenaMoiraAccount.try_format_by_username(name) |
---|
229 | officers_map = {} |
---|
230 | |
---|
231 | for officer in officers: |
---|
232 | officers_map[(officer.person, officer.role)] = officer |
---|
233 | |
---|
234 | return people, roles, name_map, officers_map |
---|
235 | |
---|
236 | # Helper for manager_officers view |
---|
237 | def manage_officers_load_accounts(max_new, people, request, msgs, ): |
---|
238 | new_people = {} |
---|
239 | moira_accounts = {} |
---|
240 | |
---|
241 | for i in range(max_new): |
---|
242 | key = "extra.%d" % (i, ) |
---|
243 | if key in request.POST and request.POST[key] != "": |
---|
244 | username = request.POST[key] |
---|
245 | |
---|
246 | local, at, domain = username.partition('@') |
---|
247 | if at and domain.lower() == 'mit.edu': |
---|
248 | msgs.append('In general, you should specify just the Athena username (for example, %s), not the whole email address (like %s).' % (local, username)) |
---|
249 | username = local |
---|
250 | |
---|
251 | try: |
---|
252 | moira_accounts[username] = groups.models.AthenaMoiraAccount.active_accounts.get(username=username) |
---|
253 | new_people[i] = username |
---|
254 | except groups.models.AthenaMoiraAccount.DoesNotExist: |
---|
255 | msgs.append('Athena account "%s" appears not to exist. Changes involving them have been ignored.' % (username, )) |
---|
256 | for person in people: |
---|
257 | try: |
---|
258 | moira_accounts[person] = groups.models.AthenaMoiraAccount.active_accounts.get(username=person) |
---|
259 | except groups.models.AthenaMoiraAccount.DoesNotExist: |
---|
260 | 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, )) |
---|
261 | |
---|
262 | return new_people, moira_accounts |
---|
263 | |
---|
264 | # Helper for manager_officers view |
---|
265 | def manage_officers_sync_role_people( |
---|
266 | group, role, new_holders, |
---|
267 | msgs, changes, |
---|
268 | officers_map, people, moira_accounts, new_people, max_new, ): |
---|
269 | """ |
---|
270 | Sync a set of new holders of a role with the database. |
---|
271 | |
---|
272 | Arguments: |
---|
273 | Function-specific: |
---|
274 | role: the role object the changes center around |
---|
275 | new_holders: The desired final set of people who should have the role |
---|
276 | Output arguments --- information messages |
---|
277 | msgs: warning message list. Output argument. |
---|
278 | changes: list of changes made. [(verb, color, person, role)] |
---|
279 | Background info arguments: |
---|
280 | officers_map: (username, role) -> OfficeHolder |
---|
281 | people: list of all potentially-affected people (who were previously involved) |
---|
282 | moira_accounts: username -> AthenaMoiraAccount |
---|
283 | new_people: potentially-affected people (who are newly involved) --- key -> username |
---|
284 | max_new: highest index to use in keys for new_people |
---|
285 | """ |
---|
286 | |
---|
287 | kept = 0 |
---|
288 | kept_not = 0 |
---|
289 | |
---|
290 | for person in people: |
---|
291 | if person in new_holders: |
---|
292 | if (person, role) in officers_map: |
---|
293 | if person not in moira_accounts: |
---|
294 | pass # already errored above |
---|
295 | elif role.require_student and not moira_accounts[person].is_student(): |
---|
296 | 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, )) |
---|
297 | #changes.append(("Kept", "yellow", person, role)) |
---|
298 | kept += 1 |
---|
299 | else: |
---|
300 | if person not in moira_accounts: |
---|
301 | pass # already errored above |
---|
302 | elif role.require_student and not moira_accounts[person].is_student(): |
---|
303 | 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, )) |
---|
304 | else: |
---|
305 | holder = groups.models.OfficeHolder(person=person, role=role, group=group,) |
---|
306 | holder.save() |
---|
307 | changes.append(("Added", "green", person, role)) |
---|
308 | else: |
---|
309 | if (person, role) in officers_map: |
---|
310 | officers_map[(person, role)].expire() |
---|
311 | changes.append(("Removed", "red", person, role)) |
---|
312 | else: |
---|
313 | kept_not += 1 |
---|
314 | pass |
---|
315 | for i in range(max_new): |
---|
316 | if "extra.%d" % (i, ) in new_holders: |
---|
317 | if i in new_people: |
---|
318 | person = new_people[i] |
---|
319 | assert person in moira_accounts |
---|
320 | if role.require_student and not moira_accounts[person].is_student(): |
---|
321 | msgs.append('Only students can have the %s role, and %s does not appear to be a student.' % (role, person, )) |
---|
322 | else: |
---|
323 | holder = groups.models.OfficeHolder(person=person, role=role, group=group,) |
---|
324 | holder.save() |
---|
325 | changes.append(("Added", "green", person, role)) |
---|
326 | |
---|
327 | return kept, kept_not |
---|
328 | |
---|
329 | # Helper for manager_officers view |
---|
330 | def manage_officers_table_update( |
---|
331 | group, |
---|
332 | request, context, msgs, changes, |
---|
333 | people, roles, officers_map, max_new, ): |
---|
334 | |
---|
335 | context['kept'] = 0 |
---|
336 | context['kept_not'] = 0 |
---|
337 | |
---|
338 | # Fill out moira_accounts with AthenaMoiraAccount objects for relevant people |
---|
339 | new_people, moira_accounts = manage_officers_load_accounts(max_new, people, request, msgs) |
---|
340 | |
---|
341 | # Process changes |
---|
342 | for role in roles: |
---|
343 | key = "holders.%s" % (role.slug, ) |
---|
344 | new_holders = set() |
---|
345 | if key in request.POST: |
---|
346 | new_holders = set(request.POST.getlist(key, )) |
---|
347 | if len(new_holders) > role.max_count: |
---|
348 | msgs.append("You selected %d people for %s; only %d are allowed. No changes to %s have been carried out in this update." % |
---|
349 | (len(new_holders), role.display_name, role.max_count, role.display_name, ) |
---|
350 | ) |
---|
351 | else: |
---|
352 | kept_delta, kept_not_delta = manage_officers_sync_role_people( |
---|
353 | group, role, new_holders, # input arguments |
---|
354 | msgs, changes, # output arguments |
---|
355 | officers_map, people, moira_accounts, # ~background data |
---|
356 | new_people, max_new, # new people data |
---|
357 | ) |
---|
358 | context['kept'] += kept_delta |
---|
359 | context['kept_not'] += kept_not_delta |
---|
360 | |
---|
361 | |
---|
362 | class OfficersBulkManageForm(forms.Form): |
---|
363 | mode_choices = [ |
---|
364 | ('add', 'Add new people', ), |
---|
365 | ('remove', 'Remove old people', ), |
---|
366 | ('sync', 'Set people to list provided', ), |
---|
367 | ] |
---|
368 | 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.' |
---|
369 | mode = forms.ChoiceField(choices=mode_choices, help_text=mode_help, ) |
---|
370 | role = forms.ChoiceField(initial='office-access', ) |
---|
371 | people = forms.CharField( |
---|
372 | help_text='Usernames of people, one per line.', |
---|
373 | widget=forms.Textarea, |
---|
374 | ) |
---|
375 | |
---|
376 | def __init__(self, *args, **kwargs): |
---|
377 | self._roles = kwargs['roles'] |
---|
378 | del kwargs['roles'] |
---|
379 | super(OfficersBulkManageForm, self).__init__(*args, **kwargs) |
---|
380 | role_choices = [ (role.slug, role.display_name) for role in self._roles ] |
---|
381 | self.fields['role'].choices = role_choices |
---|
382 | |
---|
383 | def get_role(self, ): |
---|
384 | role_slug = self.cleaned_data['role'] |
---|
385 | for role in self._roles: |
---|
386 | if role.slug == role_slug: |
---|
387 | return role |
---|
388 | raise groups.OfficerRole.DoesNotExist |
---|
389 | |
---|
390 | def manage_officers_bulk_update( |
---|
391 | group, bulk_form, |
---|
392 | msgs, changes, |
---|
393 | officers_map, ): |
---|
394 | |
---|
395 | # Load parameters |
---|
396 | mode = bulk_form.cleaned_data['mode'] |
---|
397 | role = bulk_form.get_role() |
---|
398 | people_lst = bulk_form.cleaned_data['people'].split('\n') |
---|
399 | people_set = set([p.strip() for p in people_lst]) |
---|
400 | if '' in people_set: people_set.remove('') |
---|
401 | |
---|
402 | # Fill out moira_accounts |
---|
403 | moira_accounts = {} |
---|
404 | for username in people_set: |
---|
405 | try: |
---|
406 | moira_accounts[username] = groups.models.AthenaMoiraAccount.active_accounts.get(username=username) |
---|
407 | except groups.models.AthenaMoiraAccount.DoesNotExist: |
---|
408 | msgs.append('Athena account "%s" appears not to exist. Changes involving them have been ignored.' % (username, )) |
---|
409 | |
---|
410 | # Find our target sets |
---|
411 | cur_holders = [user for user, map_role in officers_map if role == map_role] |
---|
412 | people = people_set.union(cur_holders) |
---|
413 | if mode == 'add': |
---|
414 | new_holders = people |
---|
415 | elif mode == 'remove': |
---|
416 | new_holders = people-people_set |
---|
417 | elif mode == 'sync': |
---|
418 | new_holders = people_set |
---|
419 | else: |
---|
420 | raise NotImplementedError("Unknown operation '%s'" % (mode, )) |
---|
421 | |
---|
422 | # Make changes |
---|
423 | if len(new_holders) <= role.max_count: |
---|
424 | new_people = dict() |
---|
425 | max_new = 0 |
---|
426 | manage_officers_sync_role_people( |
---|
427 | group, role, new_holders, |
---|
428 | msgs, changes, |
---|
429 | officers_map, people, moira_accounts, new_people, max_new, |
---|
430 | ) |
---|
431 | else: |
---|
432 | too_many_tmpl = "You selected %d people for %s; only %d are allowed. No changes have been made in this update." |
---|
433 | error = too_many_tmpl % (len(new_holders), role.display_name, role.max_count, ) |
---|
434 | msgs.append(error) |
---|
435 | |
---|
436 | @login_required |
---|
437 | def manage_officers(request, pk, ): |
---|
438 | group = get_object_or_404(groups.models.Group, pk=pk) |
---|
439 | |
---|
440 | if not request.user.has_perm('groups.admin_group', group): |
---|
441 | raise PermissionDenied |
---|
442 | |
---|
443 | people, roles, name_map, officers_map = manage_officers_load_officers(group) |
---|
444 | |
---|
445 | max_new = 4 |
---|
446 | msgs = [] |
---|
447 | changes = [] |
---|
448 | |
---|
449 | context = { |
---|
450 | 'group': group, |
---|
451 | 'roles': roles, |
---|
452 | 'people': people, |
---|
453 | 'changes': changes, |
---|
454 | 'msgs': msgs, |
---|
455 | } |
---|
456 | |
---|
457 | if request.method == 'POST' and 'opt-mode' in request.POST: # If the form has been submitted |
---|
458 | edited = True |
---|
459 | |
---|
460 | # Do the changes |
---|
461 | if request.POST['opt-mode'] == 'table': |
---|
462 | context['bulk_form'] = OfficersBulkManageForm(roles=roles, ) |
---|
463 | manage_officers_table_update( |
---|
464 | group, |
---|
465 | request, context, msgs, changes, |
---|
466 | people, roles, officers_map, max_new, |
---|
467 | ) |
---|
468 | elif request.POST['opt-mode'] == 'bulk': |
---|
469 | bulk_form = OfficersBulkManageForm(request.POST, roles=roles, ) |
---|
470 | context['bulk_form'] = bulk_form |
---|
471 | if bulk_form.is_valid(): |
---|
472 | manage_officers_bulk_update( |
---|
473 | group, bulk_form, |
---|
474 | msgs, changes, |
---|
475 | officers_map, ) |
---|
476 | else: |
---|
477 | raise NotImplementedError("Update mode must be table or bulk, was '%s'" % (request.POST['opt-mode'], )) |
---|
478 | |
---|
479 | # mark as changed and reload the data |
---|
480 | if changes: |
---|
481 | group.set_updater(request.user) |
---|
482 | group.save() |
---|
483 | people, roles, name_map, officers_map = manage_officers_load_officers(group) |
---|
484 | else: |
---|
485 | context['bulk_form'] = OfficersBulkManageForm(roles=roles, ) |
---|
486 | |
---|
487 | officers_data = [] |
---|
488 | for person in people: |
---|
489 | role_list = [] |
---|
490 | for role in roles: |
---|
491 | if (person, role) in officers_map: |
---|
492 | role_list.append((role, True)) |
---|
493 | else: |
---|
494 | role_list.append((role, False)) |
---|
495 | officers_data.append((False, person, name_map[person], role_list)) |
---|
496 | null_role_list = [(role, False) for role in roles] |
---|
497 | for i in range(max_new): |
---|
498 | officers_data.append((True, "extra.%d" % (i, ), "", null_role_list)) |
---|
499 | context['officers'] = officers_data |
---|
500 | context['pagename'] = "groups" |
---|
501 | |
---|
502 | return render_to_response('groups/group_change_officers.html', context, context_instance=RequestContext(request), ) |
---|
503 | |
---|
504 | |
---|
505 | |
---|
506 | ################## |
---|
507 | # ACCOUNT LOOKUP # |
---|
508 | ################## |
---|
509 | |
---|
510 | class AccountLookupForm(forms.Form): |
---|
511 | account_number = forms.IntegerField() |
---|
512 | username = forms.CharField(help_text="Athena username of person to check") |
---|
513 | |
---|
514 | def account_lookup(request, ): |
---|
515 | msg = None |
---|
516 | msg_type = "" |
---|
517 | account_number = None |
---|
518 | username = None |
---|
519 | group = None |
---|
520 | office_holders = [] |
---|
521 | |
---|
522 | visible_roles = groups.models.OfficerRole.objects.filter(publicly_visible=True) |
---|
523 | |
---|
524 | initial = {} |
---|
525 | |
---|
526 | if 'search' in request.GET: # If the form has been submitted... |
---|
527 | # A form bound to the POST data |
---|
528 | form = AccountLookupForm(request.GET) |
---|
529 | |
---|
530 | if form.is_valid(): # All validation rules pass |
---|
531 | account_number = form.cleaned_data['account_number'] |
---|
532 | username = form.cleaned_data['username'] |
---|
533 | account_q = Q(main_account_id=account_number) | Q(funding_account_id=account_number) |
---|
534 | try: |
---|
535 | group = groups.models.Group.objects.get(account_q) |
---|
536 | office_holders = group.officers(person=username) |
---|
537 | office_holders = office_holders.filter(role__in=visible_roles) |
---|
538 | except groups.models.Group.DoesNotExist: |
---|
539 | msg = "Group not found" |
---|
540 | msg_type = "error" |
---|
541 | |
---|
542 | else: |
---|
543 | form = AccountLookupForm() |
---|
544 | |
---|
545 | context = { |
---|
546 | 'username': username, |
---|
547 | 'account_number': account_number, |
---|
548 | 'group': group, |
---|
549 | 'office_holders': office_holders, |
---|
550 | 'form': form, |
---|
551 | 'msg': msg, |
---|
552 | 'msg_type': msg_type, |
---|
553 | 'visible_roles': visible_roles, |
---|
554 | } |
---|
555 | return render_to_response('groups/account_lookup.html', context, context_instance=RequestContext(request), ) |
---|
556 | |
---|
557 | |
---|
558 | |
---|
559 | ################## |
---|
560 | # GROUP CREATION # |
---|
561 | ################## |
---|
562 | |
---|
563 | def validate_athena(username, student=False, ): |
---|
564 | try: |
---|
565 | person = groups.models.AthenaMoiraAccount.active_accounts.get(username=username) |
---|
566 | if student and not person.is_student(): |
---|
567 | raise ValidationError('This must be a current student.') |
---|
568 | except groups.models.AthenaMoiraAccount.DoesNotExist: |
---|
569 | raise ValidationError('This must be a valid Athena username.') |
---|
570 | |
---|
571 | |
---|
572 | class GroupCreateForm(form_utils.forms.BetterModelForm): |
---|
573 | create_officer_list = forms.BooleanField(required=False) |
---|
574 | create_group_list = forms.BooleanField(required=False) |
---|
575 | create_athena_locker = forms.BooleanField(required=False) |
---|
576 | |
---|
577 | president_name = forms.CharField(max_length=50, ) |
---|
578 | president_kerberos = forms.CharField(min_length=3, max_length=8, ) |
---|
579 | treasurer_name = forms.CharField(max_length=50) |
---|
580 | treasurer_kerberos = forms.CharField(min_length=3, max_length=8, ) |
---|
581 | def clean_president_kerberos(self, ): |
---|
582 | username = self.cleaned_data['president_kerberos'] |
---|
583 | validate_athena(username, True, ) |
---|
584 | return username |
---|
585 | |
---|
586 | def clean_treasurer_kerberos(self, ): |
---|
587 | username = self.cleaned_data['treasurer_kerberos'] |
---|
588 | validate_athena(username, True, ) |
---|
589 | return username |
---|
590 | |
---|
591 | class Meta: |
---|
592 | fieldsets = [ |
---|
593 | ('basic', { |
---|
594 | 'legend': 'Basic Information', |
---|
595 | 'fields': ['name', 'abbreviation', 'description', ], |
---|
596 | }), |
---|
597 | ('officers', { |
---|
598 | 'legend': 'Officers', |
---|
599 | 'fields': ['president_name', 'president_kerberos', 'treasurer_name', 'treasurer_kerberos', ], |
---|
600 | }), |
---|
601 | ('type', { |
---|
602 | 'legend': 'Type', |
---|
603 | 'fields': ['activity_category', 'group_class', 'group_funding', ], |
---|
604 | }), |
---|
605 | ('technical', { |
---|
606 | 'legend': 'Technical Information', |
---|
607 | 'fields': [ |
---|
608 | 'officer_email', 'create_officer_list', |
---|
609 | 'group_email', 'create_group_list', |
---|
610 | 'athena_locker', 'create_athena_locker', |
---|
611 | ], |
---|
612 | }), |
---|
613 | ('financial', { |
---|
614 | 'legend': 'Financial Information', |
---|
615 | 'fields': ['main_account_id', 'funding_account_id', ], |
---|
616 | }), |
---|
617 | ('constitution', { |
---|
618 | 'legend': 'Constitution', |
---|
619 | 'fields': ['constitution_url', ], |
---|
620 | }), |
---|
621 | ] |
---|
622 | model = groups.models.Group |
---|
623 | |
---|
624 | |
---|
625 | class GroupCreateNgeForm(GroupCreateForm): |
---|
626 | def __init__(self, *args, **kwargs): |
---|
627 | super(GroupCreateNgeForm, self).__init__(*args, **kwargs) |
---|
628 | self.fields['treasurer_name'].required = False |
---|
629 | self.fields['treasurer_kerberos'].required = False |
---|
630 | |
---|
631 | |
---|
632 | class GroupCreateStartupForm(GroupCreateForm): |
---|
633 | def __init__(self, *args, **kwargs): |
---|
634 | super(GroupCreateStartupForm, self).__init__(*args, **kwargs) |
---|
635 | self.fields['activity_category'].required = True |
---|
636 | self.fields['constitution_url'].required = True |
---|
637 | 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." |
---|
638 | self.fields['group_email'].required = True |
---|
639 | self.fields['athena_locker'].required = True |
---|
640 | self.fields['athena_locker'].help_text = "In general, this is limited to twelve characters. You should stick to letters, numbers, and hyphens. (Underscores and dots are also acceptable, but may cause problems in some situations.)" |
---|
641 | |
---|
642 | # Specifically, if the group ends up wanting to use scripts.mit.edu, |
---|
643 | # they will currently be assigned locker.scripts.mit.edu. If they try |
---|
644 | # to use foo.bar, then https://foo.bar.scripts.mit.edu/ will produce a |
---|
645 | # certificate name mismatch. Officially, underscores are not allowed in |
---|
646 | # hostnames, so foo_.scripts.mit.edu may fail with some software. |
---|
647 | |
---|
648 | class Meta(GroupCreateForm.Meta): |
---|
649 | fieldsets = filter( |
---|
650 | lambda fieldset: fieldset[0] not in ['financial', ], |
---|
651 | GroupCreateForm.Meta.fieldsets |
---|
652 | ) |
---|
653 | |
---|
654 | def create_group_get_emails(group, group_startup, officer_emails, ): |
---|
655 | # Figure out all the accounts mail parameters |
---|
656 | accounts_count = 0 |
---|
657 | create_officer_list = False |
---|
658 | if group_startup.create_officer_list and group.officer_email: |
---|
659 | create_officer_list = True |
---|
660 | accounts_count += 1 |
---|
661 | create_group_list = False |
---|
662 | if group_startup.create_group_list and group.group_email: |
---|
663 | create_group_list = True |
---|
664 | accounts_count += 1 |
---|
665 | create_athena_locker = False |
---|
666 | if group_startup.create_athena_locker and group.athena_locker: |
---|
667 | create_athena_locker = True |
---|
668 | accounts_count += 1 |
---|
669 | officer_list, _, officer_domain = group.officer_email.partition('@') |
---|
670 | group_list, _, group_domain = group.group_email.partition('@') |
---|
671 | |
---|
672 | # Fill out the Context |
---|
673 | mail_context = Context({ |
---|
674 | 'group': group, |
---|
675 | 'group_startup': group_startup, |
---|
676 | 'create_officer_list': create_officer_list, |
---|
677 | 'create_group_list': create_group_list, |
---|
678 | 'create_athena_locker': create_athena_locker, |
---|
679 | 'officer_list': officer_list, |
---|
680 | 'group_list': group_list, |
---|
681 | 'officer_emails': officer_emails, |
---|
682 | }) |
---|
683 | |
---|
684 | # Welcome mail |
---|
685 | welcome_mail = email_from_template( |
---|
686 | tmpl='groups/diffs/new-group-announce.txt', |
---|
687 | context=mail_context, |
---|
688 | subject='ASA Group Recognition: %s' % (group.name, ), |
---|
689 | to=officer_emails, |
---|
690 | cc=['asa-new-group-announce@mit.edu'], |
---|
691 | from_email='asa-exec@mit.edu', |
---|
692 | ) |
---|
693 | |
---|
694 | # Accounts mail |
---|
695 | if accounts_count > 0: |
---|
696 | accounts_mail = email_from_template( |
---|
697 | tmpl='groups/diffs/new-group-accounts.txt', |
---|
698 | context=mail_context, |
---|
699 | subject='New Student Activity: %s' % (group.name, ), |
---|
700 | to=['accounts@mit.edu'], |
---|
701 | cc=officer_emails+['asa-admin@mit.edu'], |
---|
702 | from_email='asa-admin@mit.edu', |
---|
703 | ) |
---|
704 | |
---|
705 | else: |
---|
706 | accounts_mail = None |
---|
707 | return welcome_mail, accounts_mail |
---|
708 | |
---|
709 | def create_group_officers(group, formdata, save=True, ): |
---|
710 | officer_emails = [ ] |
---|
711 | for officer in ('president', 'treasurer', ): |
---|
712 | username = formdata[officer+'_kerberos'] |
---|
713 | if username: |
---|
714 | if save: groups.models.OfficeHolder( |
---|
715 | person=username, |
---|
716 | role=groups.models.OfficerRole.objects.get(slug=officer), |
---|
717 | group=group, |
---|
718 | ).save() |
---|
719 | officer_emails.append('%s@mit.edu' % (formdata[officer+'_kerberos'], )) |
---|
720 | return officer_emails |
---|
721 | |
---|
722 | @permission_required('groups.recognize_nge') |
---|
723 | def recognize_nge(request, ): |
---|
724 | msg = None |
---|
725 | |
---|
726 | initial = { |
---|
727 | 'create_officer_list': False, |
---|
728 | 'create_group_list': False, |
---|
729 | 'create_athena_locker': True, |
---|
730 | } |
---|
731 | group = groups.models.Group() |
---|
732 | group.group_status = groups.models.GroupStatus.objects.get(slug='nge', ) |
---|
733 | group.recognition_date = datetime.datetime.now() |
---|
734 | if request.method == 'POST': # If the form has been submitted... |
---|
735 | # A form bound to the POST data |
---|
736 | form = GroupCreateNgeForm( |
---|
737 | request.POST, request.FILES, |
---|
738 | initial=initial, |
---|
739 | instance=group, |
---|
740 | ) |
---|
741 | |
---|
742 | if form.is_valid(): # All validation rules pass |
---|
743 | group.set_updater(request.user) |
---|
744 | form.save() |
---|
745 | officer_emails = create_group_officers(group, form.cleaned_data, save=True, ) |
---|
746 | |
---|
747 | return redirect(reverse('groups:group-detail', args=[group.pk])) |
---|
748 | else: |
---|
749 | msg = "Validation failed. See below for details." |
---|
750 | |
---|
751 | else: |
---|
752 | form = GroupCreateNgeForm(initial=initial, instance=group, ) # An unbound form |
---|
753 | |
---|
754 | context = { |
---|
755 | 'form': form, |
---|
756 | 'msg': msg, |
---|
757 | 'pagename': 'groups', |
---|
758 | } |
---|
759 | return render_to_response('groups/create/nge.html', context, context_instance=RequestContext(request), ) |
---|
760 | |
---|
761 | @login_required |
---|
762 | def startup_form(request, ): |
---|
763 | msg = None |
---|
764 | |
---|
765 | initial = { |
---|
766 | 'create_officer_list': True, |
---|
767 | 'create_group_list': True, |
---|
768 | 'create_athena_locker': True, |
---|
769 | } |
---|
770 | group = groups.models.Group() |
---|
771 | group.group_status = groups.models.GroupStatus.objects.get(slug='applying', ) |
---|
772 | group.recognition_date = datetime.datetime.now() |
---|
773 | if request.method == 'POST': # If the form has been submitted... |
---|
774 | # A form bound to the POST data |
---|
775 | form = GroupCreateStartupForm( |
---|
776 | request.POST, request.FILES, |
---|
777 | initial=initial, |
---|
778 | instance=group, |
---|
779 | ) |
---|
780 | |
---|
781 | if form.is_valid(): # All validation rules pass |
---|
782 | group.set_updater(request.user) |
---|
783 | form.save() |
---|
784 | |
---|
785 | group_startup = groups.models.GroupStartup() |
---|
786 | group_startup.group = group |
---|
787 | group_startup.stage = groups.models.GROUP_STARTUP_STAGE_SUBMITTED |
---|
788 | group_startup.submitter = request.user.username |
---|
789 | |
---|
790 | group_startup.create_officer_list = form.cleaned_data['create_officer_list'] |
---|
791 | group_startup.create_group_list = form.cleaned_data['create_group_list'] |
---|
792 | group_startup.create_athena_locker = form.cleaned_data['create_athena_locker'] |
---|
793 | |
---|
794 | group_startup.president_name = form.cleaned_data['president_name'] |
---|
795 | group_startup.president_kerberos = form.cleaned_data['president_kerberos'] |
---|
796 | group_startup.treasurer_name = form.cleaned_data['treasurer_name'] |
---|
797 | group_startup.treasurer_kerberos = form.cleaned_data['treasurer_kerberos'] |
---|
798 | |
---|
799 | group_startup.save() |
---|
800 | |
---|
801 | context = { |
---|
802 | 'group': group, |
---|
803 | 'group_startup': group_startup, |
---|
804 | 'pagename': 'groups', |
---|
805 | } |
---|
806 | |
---|
807 | email_from_template( |
---|
808 | tmpl='groups/create/startup-submitted-email.txt', |
---|
809 | context=context, |
---|
810 | subject='ASA Startup Application: %s' % (group.name, ), |
---|
811 | to=[request.user.email] + create_group_officers(group, form.cleaned_data, save=False, ), |
---|
812 | cc=['asa-groups@mit.edu'], |
---|
813 | from_email='asa-groups@mit.edu', |
---|
814 | ).send() |
---|
815 | |
---|
816 | return render_to_response('groups/create/startup_thanks.html', context, context_instance=RequestContext(request), ) |
---|
817 | else: |
---|
818 | msg = "Validation failed. See below for details." |
---|
819 | |
---|
820 | else: |
---|
821 | form = GroupCreateStartupForm(initial=initial, instance=group, ) # An unbound form |
---|
822 | |
---|
823 | context = { |
---|
824 | 'form': form, |
---|
825 | 'msg': msg, |
---|
826 | 'pagename': 'groups', |
---|
827 | } |
---|
828 | return render_to_response('groups/create/startup.html', context, context_instance=RequestContext(request), ) |
---|
829 | |
---|
830 | def review_group_check_warnings(group_startup, group, ): |
---|
831 | warnings = [] |
---|
832 | |
---|
833 | if group.name.startswith("MIT "): |
---|
834 | warnings.append('Group name starts with "MIT". Generally, we prefer "Foo, MIT" instead.') |
---|
835 | if "mit" in group.athena_locker.lower(): |
---|
836 | warnings.append('Athena locker name contains "mit", which may be redundant with paths like "http://web.mit.edu/mitfoo" or "/mit/foo/".') |
---|
837 | |
---|
838 | if group_startup.president_kerberos == group_startup.treasurer_kerberos: |
---|
839 | warnings.append('President matches Treasurer.') |
---|
840 | if "%s@mit.edu" % (group_startup.president_kerberos, ) in (group.officer_email, group.group_email): |
---|
841 | warnings.append('President email matches officer and/or group email.') |
---|
842 | if group.officer_email == group.group_email: |
---|
843 | warnings.append('Officer email matches group email.') |
---|
844 | |
---|
845 | if '@mit.edu' not in group.officer_email or '@mit.edu' not in group.group_email: |
---|
846 | warnings.append('Officer and/or group email are non-MIT. Ensure that they are not requesting the addresses be created, and consider suggesting they use an MIT list instead.') |
---|
847 | |
---|
848 | if '.' in group.athena_locker: |
---|
849 | warnings.append('Athena locker contains a ".". This is not compatible with scripts.mit.edu\'s wildcard certificate, and may cause other problems.') |
---|
850 | if '_' in group.athena_locker: |
---|
851 | warnings.append('Athena locker contains a "_". If this locker name gets used in a URL (for example, locker.scripts.mit.edu), it will technically violate the hostname specification and may not work in some clients.') |
---|
852 | if len(group.athena_locker) > 12: |
---|
853 | warnings.append('Athena locker is more than twelve characters long. In general, twelve characters is the longest Athena locker an ASA-recognized group can get.') |
---|
854 | |
---|
855 | return warnings |
---|
856 | |
---|
857 | @permission_required('groups.recognize_group') |
---|
858 | def recognize_normal_group(request, pk, ): |
---|
859 | group_startup = get_object_or_404(groups.models.GroupStartup, pk=pk, ) |
---|
860 | group = group_startup.group |
---|
861 | |
---|
862 | context = { |
---|
863 | 'startup': group_startup, |
---|
864 | 'group': group, |
---|
865 | 'pagename' : 'groups', |
---|
866 | } |
---|
867 | |
---|
868 | if group.group_status.slug != 'applying': |
---|
869 | return render_to_response('groups/create/err.not-applying.html', context, context_instance=RequestContext(request), ) |
---|
870 | if group_startup.stage != groups.models.GROUP_STARTUP_STAGE_SUBMITTED: |
---|
871 | return render_to_response('groups/create/err.not-applying.html', context, context_instance=RequestContext(request), ) |
---|
872 | |
---|
873 | context['warnings'] = review_group_check_warnings(group_startup, group) |
---|
874 | |
---|
875 | context['msg'] = "" |
---|
876 | if request.method == 'POST': |
---|
877 | if 'approve' in request.POST: |
---|
878 | group_startup.stage = groups.models.GROUP_STARTUP_STAGE_APPROVED |
---|
879 | group_startup.save() |
---|
880 | |
---|
881 | group.group_status = groups.models.GroupStatus.objects.get(slug='suspended') |
---|
882 | group.constitution_url = "" |
---|
883 | group.recognition_date = datetime.datetime.now() |
---|
884 | group.set_updater(request.user) |
---|
885 | |
---|
886 | note = groups.models.GroupNote( |
---|
887 | author=request.user.username, |
---|
888 | body="Approved group for recognition.", |
---|
889 | acl_read_group=True, |
---|
890 | acl_read_offices=True, |
---|
891 | group=group, |
---|
892 | ).save() |
---|
893 | |
---|
894 | group.save() |
---|
895 | officer_emails = create_group_officers(group, group_startup.__dict__, ) |
---|
896 | welcome_mail, accounts_mail = create_group_get_emails(group, group_startup, officer_emails, ) |
---|
897 | welcome_mail.send() |
---|
898 | if accounts_mail: |
---|
899 | accounts_mail.send() |
---|
900 | context['msg'] = 'Group approved.' |
---|
901 | context['msg_type'] = 'info' |
---|
902 | elif 'reject' in request.POST: |
---|
903 | group_startup.stage = groups.models.GROUP_STARTUP_STAGE_REJECTED |
---|
904 | group_startup.save() |
---|
905 | group.group_status = groups.models.GroupStatus.objects.get(slug='derecognized') |
---|
906 | group.save() |
---|
907 | note = groups.models.GroupNote( |
---|
908 | author=request.user.username, |
---|
909 | body="Group rejected during recognition process.", |
---|
910 | acl_read_group=True, |
---|
911 | acl_read_offices=True, |
---|
912 | group=group, |
---|
913 | ).save() |
---|
914 | context['msg'] = 'Group rejected.' |
---|
915 | context['msg_type'] = 'info' |
---|
916 | else: |
---|
917 | context['disp_form'] = True |
---|
918 | else: |
---|
919 | context['disp_form'] = True |
---|
920 | |
---|
921 | return render_to_response('groups/create/startup_review.html', context, context_instance=RequestContext(request), ) |
---|
922 | |
---|
923 | class GroupStartupListView(ListView): |
---|
924 | model = groups.models.GroupStartup |
---|
925 | template_object_name = 'startup' |
---|
926 | |
---|
927 | def get_queryset(self, ): |
---|
928 | qs = super(GroupStartupListView, self).get_queryset() |
---|
929 | qs = qs.filter(stage=groups.models.GROUP_STARTUP_STAGE_SUBMITTED) |
---|
930 | qs = qs.select_related('group') |
---|
931 | return qs |
---|
932 | |
---|
933 | def get_context_data(self, **kwargs): |
---|
934 | context = super(GroupStartupListView, self).get_context_data(**kwargs) |
---|
935 | context['pagename'] = 'groups' |
---|
936 | return context |
---|
937 | |
---|
938 | |
---|
939 | |
---|
940 | ################## |
---|
941 | # Multiple group # |
---|
942 | ################## |
---|
943 | |
---|
944 | class GroupFilter(django_filters.FilterSet): |
---|
945 | name = django_filters.CharFilter(lookup_type='icontains', label="Name contains") |
---|
946 | abbreviation = django_filters.CharFilter(lookup_type='iexact', label="Abbreviation is") |
---|
947 | officer_email = django_filters.CharFilter(lookup_type='icontains', label="Officers' list contains") |
---|
948 | |
---|
949 | account_filter = util.db_filters.MultiNumberFilter( |
---|
950 | lookup_type='exact', label="Account number", |
---|
951 | names=('main_account_id', 'funding_account_id', ), |
---|
952 | ) |
---|
953 | |
---|
954 | class Meta: |
---|
955 | model = groups.models.Group |
---|
956 | fields = [ |
---|
957 | 'name', |
---|
958 | 'abbreviation', |
---|
959 | 'officer_email', |
---|
960 | 'activity_category', |
---|
961 | 'group_class', |
---|
962 | 'group_status', |
---|
963 | 'group_funding', |
---|
964 | 'account_filter', |
---|
965 | ] |
---|
966 | |
---|
967 | def __init__(self, data=None, *args, **kwargs): |
---|
968 | if not data: data = None |
---|
969 | super(GroupFilter, self).__init__(data, *args, **kwargs) |
---|
970 | active_pk = groups.models.GroupStatus.objects.get(slug='active').pk |
---|
971 | self.form.initial['group_status'] = active_pk |
---|
972 | |
---|
973 | |
---|
974 | class GroupListView(ListView): |
---|
975 | model = groups.models.Group |
---|
976 | template_object_name = 'group' |
---|
977 | |
---|
978 | def get(self, *args, **kwargs): |
---|
979 | qs = super(GroupListView, self).get_queryset() |
---|
980 | self.filterset = GroupFilter(self.request.GET, qs) |
---|
981 | return super(GroupListView, self).get(*args, **kwargs) |
---|
982 | |
---|
983 | def get_queryset(self, ): |
---|
984 | qs = self.filterset.qs |
---|
985 | return qs |
---|
986 | |
---|
987 | def get_context_data(self, **kwargs): |
---|
988 | context = super(GroupListView, self).get_context_data(**kwargs) |
---|
989 | # Add in the publisher |
---|
990 | context['pagename'] = 'groups' |
---|
991 | context['filter'] = self.filterset |
---|
992 | return context |
---|
993 | |
---|
994 | |
---|
995 | @permission_required('groups.view_signatories') |
---|
996 | def view_signatories(request, ): |
---|
997 | the_groups = groups.models.Group.objects.all() |
---|
998 | groups_filterset = GroupFilter(request.GET, the_groups) |
---|
999 | the_groups = groups_filterset.qs |
---|
1000 | |
---|
1001 | officers = groups.models.OfficeHolder.objects.filter(start_time__lte=datetime.datetime.now(), end_time__gte=datetime.datetime.now()) |
---|
1002 | officers = officers.filter(group__in=the_groups) |
---|
1003 | officers = officers.select_related(depth=1) |
---|
1004 | |
---|
1005 | role_slugs = ['president', 'treasurer', 'financial', 'reservation'] |
---|
1006 | roles = groups.models.OfficerRole.objects.filter(slug__in=role_slugs) |
---|
1007 | roles = sorted(roles, key=lambda r: role_slugs.index(r.slug)) |
---|
1008 | |
---|
1009 | officers_map = collections.defaultdict(lambda: collections.defaultdict(set)) |
---|
1010 | for officer in officers: |
---|
1011 | officers_map[officer.group][officer.role].add(officer.person) |
---|
1012 | officers_data = [] |
---|
1013 | for group in the_groups: |
---|
1014 | role_list = [] |
---|
1015 | for role in roles: |
---|
1016 | role_list.append(sorted(officers_map[group][role])) |
---|
1017 | officers_data.append((group, role_list)) |
---|
1018 | |
---|
1019 | context = { |
---|
1020 | 'roles': roles, |
---|
1021 | 'officers': officers_data, |
---|
1022 | 'filter': groups_filterset, |
---|
1023 | 'pagename': 'groups', |
---|
1024 | } |
---|
1025 | return render_to_response('groups/groups_signatories.html', context, context_instance=RequestContext(request), ) |
---|
1026 | |
---|
1027 | def search_groups(request, ): |
---|
1028 | the_groups = groups.models.Group.objects.all() |
---|
1029 | groups_filterset = GroupFilter(request.GET, the_groups) |
---|
1030 | |
---|
1031 | dest = None |
---|
1032 | if 'signatories' in request.GET: |
---|
1033 | dest = reverse('groups:signatories') |
---|
1034 | print dest |
---|
1035 | elif 'group-goto' in request.GET: |
---|
1036 | if len(groups_filterset.qs) == 1: |
---|
1037 | group = groups_filterset.qs[0] |
---|
1038 | return redirect(reverse('groups:group-detail', kwargs={'pk':group.pk})) |
---|
1039 | else: |
---|
1040 | dest = reverse('groups:list') |
---|
1041 | elif 'group-list' in request.GET: |
---|
1042 | dest = reverse('groups:list') |
---|
1043 | |
---|
1044 | if dest: |
---|
1045 | return redirect(dest + "?" + request.META['QUERY_STRING']) |
---|
1046 | else: |
---|
1047 | context = { |
---|
1048 | 'filter': groups_filterset, |
---|
1049 | 'pagename': 'groups', |
---|
1050 | } |
---|
1051 | return render_to_response('groups/group_search.html', context, context_instance=RequestContext(request), ) |
---|
1052 | |
---|
1053 | |
---|
1054 | class GroupHistoryView(ListView): |
---|
1055 | context_object_name = "version_list" |
---|
1056 | template_name = "groups/group_version.html" |
---|
1057 | |
---|
1058 | def get_queryset(self): |
---|
1059 | history_entries = None |
---|
1060 | if 'pk' in self.kwargs: |
---|
1061 | group = get_object_or_404(groups.models.Group, pk=self.kwargs['pk']) |
---|
1062 | history_entries = reversion.get_for_object(group) |
---|
1063 | else: |
---|
1064 | history_entries = reversion.models.Version.objects.all() |
---|
1065 | group_content_type = ContentType.objects.get_for_model(groups.models.Group) |
---|
1066 | history_entries = history_entries.filter(content_type=group_content_type) |
---|
1067 | length = len(history_entries) |
---|
1068 | if length > 150: |
---|
1069 | history_entries = history_entries[length-100:] |
---|
1070 | return history_entries |
---|
1071 | |
---|
1072 | def get_context_data(self, **kwargs): |
---|
1073 | context = super(GroupHistoryView, self).get_context_data(**kwargs) |
---|
1074 | if 'pk' in self.kwargs: |
---|
1075 | group = get_object_or_404(groups.models.Group, pk=self.kwargs['pk']) |
---|
1076 | context['title'] = "History for %s" % (group.name, ) |
---|
1077 | context['adminpriv'] = self.request.user.has_perm('groups.admin_group', group) |
---|
1078 | context['group'] = group |
---|
1079 | else: |
---|
1080 | context['title'] = "Recent Changes" |
---|
1081 | context['pagename'] = 'groups' |
---|
1082 | return context |
---|
1083 | |
---|
1084 | |
---|
1085 | @permission_required('groups.view_group_private_info') |
---|
1086 | def downloaded_constitutions(request, ): |
---|
1087 | constitutions = groups.models.GroupConstitution.objects |
---|
1088 | constitutions = constitutions.order_by('failure_reason', 'status_msg', 'failure_date', 'group__name', ).select_related('group', 'group__group_status', ) |
---|
1089 | failures = collections.defaultdict(list) |
---|
1090 | successes = collections.defaultdict(list) |
---|
1091 | for const in constitutions: |
---|
1092 | if const.failure_reason: |
---|
1093 | failures[const.failure_reason].append(const) |
---|
1094 | else: |
---|
1095 | successes[const.status_msg].append(const) |
---|
1096 | context = {} |
---|
1097 | context['failures'] = sorted(failures.items(), key=lambda x: x[0]) |
---|
1098 | context['successes'] = sorted(successes.items(), key=lambda x: x[0]) |
---|
1099 | context['pagename'] = 'groups' |
---|
1100 | return render_to_response('groups/groups_constitutions.html', context, context_instance=RequestContext(request), ) |
---|
1101 | |
---|
1102 | |
---|
1103 | @permission_required('groups.view_group_private_info') |
---|
1104 | def downloaded_constitutions_csv(request, ): |
---|
1105 | active_groups = groups.models.Group.active_groups.all() |
---|
1106 | constitutions = groups.models.GroupConstitution.objects.filter(group__in=active_groups) |
---|
1107 | constitutions = constitutions.order_by('failure_reason', 'status_msg', 'failure_date', 'group__name', ).select_related('group', 'group__group_status') |
---|
1108 | |
---|
1109 | response = HttpResponse(mimetype='text/csv') |
---|
1110 | writer = csv.writer(response) |
---|
1111 | |
---|
1112 | writer.writerow([ |
---|
1113 | 'failure_date', |
---|
1114 | 'status_msg', |
---|
1115 | 'name', 'id', 'group_status', 'email', |
---|
1116 | 'constitution_url', |
---|
1117 | ]) |
---|
1118 | for const in constitutions: |
---|
1119 | writer.writerow([ |
---|
1120 | const.failure_date, |
---|
1121 | const.status_msg.strip(), |
---|
1122 | const.group.name, |
---|
1123 | const.group.pk, |
---|
1124 | const.group.group_status.slug, |
---|
1125 | const.group.officer_email, |
---|
1126 | const.source_url, |
---|
1127 | ]) |
---|
1128 | return response |
---|
1129 | |
---|
1130 | |
---|
1131 | |
---|
1132 | ####################### |
---|
1133 | # REPORTING COMPONENT # |
---|
1134 | ####################### |
---|
1135 | |
---|
1136 | name_formats = collections.OrderedDict() |
---|
1137 | name_formats['username'] = ('username', False, '%(user)s') |
---|
1138 | name_formats['email'] = ('username@mit.edu', False, '%(user)s@mit.edu') |
---|
1139 | name_formats['name'] = ('First Last', True, '%(first)s %(last)s') |
---|
1140 | name_formats['name-email'] = ('First Last <username@mit.edu>', True, '%(first)s %(last)s <%(user)s@mit.edu>') |
---|
1141 | |
---|
1142 | class ReportingForm(form_utils.forms.BetterForm): |
---|
1143 | special_filters = forms.fields.MultipleChoiceField( |
---|
1144 | choices=[], |
---|
1145 | widget=forms.SelectMultiple(attrs={'size': 10}), |
---|
1146 | validators=[groups.models.filter_registry.validate_filter_slug], |
---|
1147 | required=False, |
---|
1148 | ) |
---|
1149 | |
---|
1150 | basic_fields_choices = groups.models.Group.reporting_fields() |
---|
1151 | basic_fields_labels = dict(basic_fields_choices) # name -> verbose_name |
---|
1152 | basic_fields = forms.fields.MultipleChoiceField( |
---|
1153 | choices=basic_fields_choices, |
---|
1154 | widget=forms.CheckboxSelectMultiple, |
---|
1155 | initial = ['id', 'name'], |
---|
1156 | ) |
---|
1157 | |
---|
1158 | people_fields = forms.models.ModelMultipleChoiceField( |
---|
1159 | queryset=groups.models.OfficerRole.objects.all(), |
---|
1160 | widget=forms.CheckboxSelectMultiple, |
---|
1161 | required=False, |
---|
1162 | ) |
---|
1163 | name_format = forms.ChoiceField( |
---|
1164 | help_text='How to format the names of the President, Treasurer, etc., if displayed', |
---|
1165 | choices=[(k, v[0]) for k,v in name_formats.items()], |
---|
1166 | initial='username', |
---|
1167 | required=True, |
---|
1168 | ) |
---|
1169 | |
---|
1170 | special_fields_choices = ( |
---|
1171 | ('option_entry', '<option> entry', ), |
---|
1172 | ) |
---|
1173 | special_fields = forms.fields.MultipleChoiceField( |
---|
1174 | choices=special_fields_choices, |
---|
1175 | widget=forms.CheckboxSelectMultiple, |
---|
1176 | required=False, |
---|
1177 | ) |
---|
1178 | |
---|
1179 | _format_choices = [ |
---|
1180 | ('html/inline', "Web (HTML)", ), |
---|
1181 | ('csv/inline', "Spreadsheet (CSV) --- in browser", ), |
---|
1182 | ('csv/download', "Spreadsheet (CSV) --- download", ), |
---|
1183 | ] |
---|
1184 | output_format = forms.fields.ChoiceField(choices=_format_choices, widget=forms.RadioSelect, initial='html/inline') |
---|
1185 | |
---|
1186 | class Meta: |
---|
1187 | fieldsets = [ |
---|
1188 | ('filter', { |
---|
1189 | 'legend': 'Filter Groups', |
---|
1190 | 'fields': [ |
---|
1191 | 'name', 'abbreviation', |
---|
1192 | 'activity_category', 'group_class', 'group_status', 'group_funding', |
---|
1193 | 'special_filters', |
---|
1194 | ], |
---|
1195 | }), |
---|
1196 | ('fields', { |
---|
1197 | 'legend': 'Data to display', |
---|
1198 | 'fields': ['basic_fields', 'people_fields', 'name_format', 'special_fields', ], |
---|
1199 | }), |
---|
1200 | ('final', { |
---|
1201 | 'legend': 'Final options', |
---|
1202 | 'fields': ['o', 'output_format', ], |
---|
1203 | }), |
---|
1204 | ] |
---|
1205 | |
---|
1206 | def __init__(self, *args, **kwargs): |
---|
1207 | super(ReportingForm, self).__init__(*args, **kwargs) |
---|
1208 | |
---|
1209 | registry = groups.models.filter_registry |
---|
1210 | self.fields['special_filters'].choices = registry.get_choices() |
---|
1211 | |
---|
1212 | class GroupReportingFilter(GroupFilter): |
---|
1213 | class Meta(GroupFilter.Meta): |
---|
1214 | form = ReportingForm |
---|
1215 | order_by = True # we customize the field, so the value needs to be true-like but doesn't matter otherwise |
---|
1216 | |
---|
1217 | def get_ordering_field(self): |
---|
1218 | return forms.ChoiceField(label="Ordering", required=False, choices=ReportingForm.basic_fields_choices) |
---|
1219 | |
---|
1220 | def __init__(self, data=None, *args, **kwargs): |
---|
1221 | super(GroupReportingFilter, self).__init__(data, *args, **kwargs) |
---|
1222 | |
---|
1223 | def format_id(pk): |
---|
1224 | url = reverse('groups:group-detail', kwargs={'pk':pk}) |
---|
1225 | return mark_safe("<a href='%s'>%d</a>" % (url, pk)) |
---|
1226 | |
---|
1227 | def format_url(url): |
---|
1228 | try: |
---|
1229 | urlvalidator(url) |
---|
1230 | except ValidationError: |
---|
1231 | return url |
---|
1232 | else: |
---|
1233 | escaped = html.escape(url) |
---|
1234 | return mark_safe("<a href='%s'>%s</a>" % (escaped, escaped)) |
---|
1235 | |
---|
1236 | def format_email(email): |
---|
1237 | try: |
---|
1238 | emailvalidator(email) |
---|
1239 | except ValidationError: |
---|
1240 | return email |
---|
1241 | else: |
---|
1242 | escaped = html.escape(email) |
---|
1243 | return mark_safe("<a href='mailto:%s'>%s</a>" % (escaped, escaped)) |
---|
1244 | |
---|
1245 | def format_option_entry(group): |
---|
1246 | name = html.escape(group.name) |
---|
1247 | return '<option value="%s">%s</option>' % (name, name, ) |
---|
1248 | |
---|
1249 | reporting_html_formatters = { |
---|
1250 | 'id': format_id, |
---|
1251 | 'website_url': format_url, |
---|
1252 | 'constitution_url': format_url, |
---|
1253 | 'group_email': format_email, |
---|
1254 | 'officer_email': format_email, |
---|
1255 | } |
---|
1256 | |
---|
1257 | @permission_required('groups.view_group_private_info') |
---|
1258 | def reporting(request, ): |
---|
1259 | the_groups = groups.models.Group.objects.all() |
---|
1260 | groups_filterset = GroupReportingFilter(request.GET, the_groups) |
---|
1261 | form = groups_filterset.form |
---|
1262 | |
---|
1263 | col_labels = [] |
---|
1264 | report_groups = [] |
---|
1265 | run_report = 'go' in request.GET and form.is_valid() |
---|
1266 | if run_report: |
---|
1267 | basic_fields = form.cleaned_data['basic_fields'] |
---|
1268 | output_format, output_disposition = form.cleaned_data['output_format'].split('/') |
---|
1269 | col_labels = [form.basic_fields_labels[field] for field in basic_fields] |
---|
1270 | |
---|
1271 | # Set up query |
---|
1272 | qs = groups_filterset.qs |
---|
1273 | for fltr_slug in form.cleaned_data['special_filters']: |
---|
1274 | fltr = groups.models.filter_registry.get(fltr_slug) |
---|
1275 | qs = qs.filter(pk__in=fltr.queryset()) |
---|
1276 | |
---|
1277 | # Prefetch foreign keys |
---|
1278 | prefetch_fields = groups.models.Group.reporting_prefetch() |
---|
1279 | prefetch_fields = prefetch_fields.intersection(basic_fields) |
---|
1280 | if prefetch_fields: |
---|
1281 | qs = qs.select_related(*list(prefetch_fields)) |
---|
1282 | |
---|
1283 | # Set up people |
---|
1284 | people_fields = form.cleaned_data['people_fields'] |
---|
1285 | people_data = groups.models.OfficeHolder.current_holders.filter(group__in=qs, role__in=people_fields) |
---|
1286 | # Group.pk -> (OfficerRole.pk -> set(username)) |
---|
1287 | people_map = collections.defaultdict(lambda: collections.defaultdict(set)) |
---|
1288 | for holder in people_data: |
---|
1289 | people_map[holder.group_id][holder.role_id].add(holder.person) |
---|
1290 | for field in people_fields: |
---|
1291 | col_labels.append(field.display_name) |
---|
1292 | # Figure out the format, and if necessary fetch human names |
---|
1293 | nf_slug = form.cleaned_data['name_format'] |
---|
1294 | nf_label, nf_need_name, nf_fmt = name_formats[nf_slug] |
---|
1295 | name_map = collections.defaultdict(lambda: ('???', '???')) |
---|
1296 | if nf_need_name: |
---|
1297 | people_usernames = [p.person.lower() for p in people_data] |
---|
1298 | name_data = groups.models.AthenaMoiraAccount.objects.filter(username__in=people_usernames) |
---|
1299 | for account in name_data: |
---|
1300 | name_map[account.username.lower()] = (account.first_name, account.last_name) |
---|
1301 | |
---|
1302 | # Set up special fields |
---|
1303 | special_formatters = [] |
---|
1304 | if 'option_entry' in form.cleaned_data['special_fields']: |
---|
1305 | col_labels.append('option_entry') |
---|
1306 | special_formatters.append(format_option_entry) |
---|
1307 | |
---|
1308 | # Assemble data |
---|
1309 | if output_format == 'html': |
---|
1310 | formatters = reporting_html_formatters |
---|
1311 | else: |
---|
1312 | formatters = {} |
---|
1313 | def fetch_item(group, field): |
---|
1314 | val = getattr(group, field) |
---|
1315 | if field in formatters: |
---|
1316 | val = formatters[field](val) |
---|
1317 | return val |
---|
1318 | |
---|
1319 | for group in qs: |
---|
1320 | group_data = [fetch_item(group, field) for field in basic_fields] |
---|
1321 | for field in people_fields: |
---|
1322 | people = people_map[group.pk][field.pk] |
---|
1323 | if nf_need_name: |
---|
1324 | def fmt(p): |
---|
1325 | first, last = name_map[p.lower()] |
---|
1326 | ctx = {'user': p, 'first': first, 'last': last} |
---|
1327 | return nf_fmt % ctx |
---|
1328 | people = [fmt(p) for p in people] |
---|
1329 | else: |
---|
1330 | people = [nf_fmt % {'user': p} for p in people] |
---|
1331 | group_data.append(", ".join(people)) |
---|
1332 | |
---|
1333 | for formatter in special_formatters: |
---|
1334 | group_data.append(formatter(group)) |
---|
1335 | |
---|
1336 | report_groups.append(group_data) |
---|
1337 | |
---|
1338 | # Handle output as CSV |
---|
1339 | if output_format == 'csv': |
---|
1340 | if output_disposition == 'download': |
---|
1341 | mimetype = 'text/csv' |
---|
1342 | else: |
---|
1343 | # Firefox, at least, downloads text/csv regardless |
---|
1344 | mimetype = 'text/plain' |
---|
1345 | response = HttpResponse(mimetype=mimetype) |
---|
1346 | if output_disposition == 'download': |
---|
1347 | response['Content-Disposition'] = 'attachment; filename=asa-db-report.csv' |
---|
1348 | writer = csv.writer(response) |
---|
1349 | writer.writerow(col_labels) |
---|
1350 | for row in report_groups: |
---|
1351 | utf8_row = [unicode(cell).encode("utf-8") for cell in row] |
---|
1352 | writer.writerow(utf8_row) |
---|
1353 | return response |
---|
1354 | |
---|
1355 | # Handle output as HTML |
---|
1356 | context = { |
---|
1357 | 'form': form, |
---|
1358 | 'run_report': run_report, |
---|
1359 | 'column_labels': col_labels, |
---|
1360 | 'report_groups': report_groups, |
---|
1361 | 'pagename': 'groups', |
---|
1362 | } |
---|
1363 | return render_to_response('groups/reporting.html', context, context_instance=RequestContext(request), ) |
---|
1364 | |
---|
1365 | |
---|
1366 | @permission_required('groups.view_group_private_info') |
---|
1367 | def show_nonstudent_officers(request, ): |
---|
1368 | student_roles = groups.models.OfficerRole.objects.filter(require_student=True, ) |
---|
1369 | student_q = groups.models.AthenaMoiraAccount.student_q() |
---|
1370 | students = groups.models.AthenaMoiraAccount.active_accounts.filter(student_q) |
---|
1371 | office_holders = groups.models.OfficeHolder.current_holders.order_by('group__name', 'role', ) |
---|
1372 | office_holders = office_holders.filter(role__in=student_roles) |
---|
1373 | office_holders = office_holders.exclude(person__in=students.values('username')) |
---|
1374 | office_holders = office_holders.select_related('group', 'group__group_status', 'role') |
---|
1375 | |
---|
1376 | msg = None |
---|
1377 | msg_type = "" |
---|
1378 | if 'sort' in request.GET: |
---|
1379 | if request.GET['sort'] == 'group': |
---|
1380 | office_holders = office_holders.order_by('group__name', 'group__group_status', 'role', 'person', ) |
---|
1381 | elif request.GET['sort'] == 'status': |
---|
1382 | office_holders = office_holders.order_by('group__group_status', 'group__name', 'role', 'person', ) |
---|
1383 | elif request.GET['sort'] == 'role': |
---|
1384 | office_holders = office_holders.order_by('role', 'group__group_status', 'group__name', 'person', ) |
---|
1385 | elif request.GET['sort'] == 'person': |
---|
1386 | office_holders = office_holders.order_by('person', 'group__group_status', 'group__name', 'role', ) |
---|
1387 | else: |
---|
1388 | msg = 'Unknown sort key "%s".' % (request.GET['sort'], ) |
---|
1389 | msg_type = 'error' |
---|
1390 | |
---|
1391 | context = { |
---|
1392 | 'pagename': 'groups', |
---|
1393 | 'roles': student_roles, |
---|
1394 | 'holders': office_holders, |
---|
1395 | 'msg': msg, |
---|
1396 | 'msg_type': msg_type, |
---|
1397 | } |
---|
1398 | |
---|
1399 | return render_to_response('groups/reporting/non-students.html', context, context_instance=RequestContext(request), ) |
---|
1400 | |
---|
1401 | |
---|