forked from ietf-tools/datatracker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathviews.py
More file actions
574 lines (487 loc) · 23 KB
/
views.py
File metadata and controls
574 lines (487 loc) · 23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
# Copyright The IETF Trust 2007-2020, All Rights Reserved
# -*- coding: utf-8 -*-
import json
from email.utils import parseaddr
from django.contrib import messages
from django.urls import reverse as urlreverse
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.db.models import Q, Prefetch
from django.http import HttpResponse
from django.shortcuts import render, get_object_or_404, redirect
import debug # pyflakes:ignore
from ietf.doc.models import Document
from ietf.ietfauth.utils import role_required, has_role
from ietf.group.models import Group, Role
from ietf.liaisons.models import (LiaisonStatement,LiaisonStatementEvent,
LiaisonStatementAttachment)
from ietf.liaisons.utils import (get_person_for_user, can_add_outgoing_liaison,
can_add_incoming_liaison, can_edit_liaison,can_submit_liaison_required,
can_add_liaison)
from ietf.liaisons.forms import liaison_form_factory, SearchLiaisonForm, EditAttachmentForm, AddCommentForm
from ietf.liaisons.mails import notify_pending_by_email, send_liaison_by_email
from ietf.liaisons.fields import select2_id_liaison_json
from ietf.name.models import LiaisonStatementTagName
from ietf.utils.response import permission_denied
EMAIL_ALIASES = {
'IETFCHAIR':'The IETF Chair <chair@ietf.org>',
'IESG':'The IESG <iesg@ietf.org>',
'IAB':'The IAB <iab@iab.org>',
'IABCHAIR':'The IAB Chair <iab-chair@iab.org>',
'IABEXECUTIVEDIRECTOR':'The IAB Executive Director <execd@iab.org>'}
# -------------------------------------------------
# Helper Functions
# -------------------------------------------------
def _can_reply(liaison, user):
'''Returns true if the user can send a reply to this liaison'''
if user.is_authenticated:
person = get_person_for_user(user)
if has_role(user, "Secretariat"):
return True
if liaison.is_outgoing() and Role.objects.filter(group__in=liaison.to_groups.all(),person=person,name='auth'):
return True
if not liaison.is_outgoing() and Role.objects.filter(group__in=liaison.from_groups.all(),person=person,name='liaiman'):
return True
return False
def _can_take_care(liaison, user):
'''Returns True if user can take care of awaiting actions associated with this liaison'''
if not liaison.deadline or liaison.action_taken:
return False
if user.is_authenticated:
if has_role(user, "Secretariat"):
return True
else:
return _find_person_in_emails(liaison, get_person_for_user(user))
return False
def _find_person_in_emails(liaison, person):
'''Returns true if person corresponds with any of the email addresses associated
with this liaison statement'''
if not person:
return False
emails = ','.join(e for e in [liaison.response_contacts, liaison.cc_contacts,
liaison.to_contacts,liaison.technical_contacts] if e)
for email in emails.split(','):
name, addr = parseaddr(email)
try:
validate_email(addr)
except ValidationError:
continue
if person.email_set.filter(address=addr):
return True
elif addr in ('chair@ietf.org', 'iesg@ietf.org') and has_role(person.user, "IETF Chair"):
return True
elif addr in ('iab@iab.org', 'iab-chair@iab.org') and has_role(person.user, "IAB Chair"):
return True
elif addr in ('execd@iab.org', ) and has_role(person.user, "IAB Executive Director"):
return True
return False
def contact_email_from_role(role):
return '{} <{}>'.format(role.person.plain_name(), role.email.address)
def contacts_from_roles(roles):
'''Returns contact string for given roles'''
emails = [ contact_email_from_role(r) for r in roles ]
return ','.join(emails)
def get_cc(group):
'''Returns list of emails to use as CC for group. Simplified refactor of IETFHierarchy
get_cc() and get_from_cc()
'''
emails = []
# role based CCs
if group.acronym in ('ietf','iesg'):
emails.append(EMAIL_ALIASES['IESG'])
emails.append(EMAIL_ALIASES['IETFCHAIR'])
elif group.acronym in ('iab'):
emails.append(EMAIL_ALIASES['IAB'])
emails.append(EMAIL_ALIASES['IABCHAIR'])
emails.append(EMAIL_ALIASES['IABEXECUTIVEDIRECTOR'])
elif group.type_id == 'area':
emails.append(EMAIL_ALIASES['IETFCHAIR'])
ad_roles = group.role_set.filter(name='ad')
emails.extend([ contact_email_from_role(r) for r in ad_roles ])
elif group.type_id == 'wg':
ad_roles = group.parent.role_set.filter(name='ad')
emails.extend([ contact_email_from_role(r) for r in ad_roles ])
chair_roles = group.role_set.filter(name='chair')
emails.extend([ contact_email_from_role(r) for r in chair_roles ])
if group.list_email:
emails.append('{} Discussion List <{}>'.format(group.name,group.list_email))
elif group.type_id == 'sdo':
liaiman_roles = group.role_set.filter(name='liaiman')
emails.extend([ contact_email_from_role(r) for r in liaiman_roles ])
# explicit CCs
liaison_cc_roles = group.role_set.filter(name='liaison_cc_contact')
emails.extend([ contact_email_from_role(r) for r in liaison_cc_roles ])
return emails
def get_contacts_for_group(group):
'''Returns default contacts for groups as a comma separated string'''
# use explicit default contacts if defined
explicit_contacts = contacts_from_roles(group.role_set.filter(name='liaison_contact'))
if explicit_contacts:
return explicit_contacts
# otherwise construct based on group type
contacts = []
if group.type_id == 'area':
roles = group.role_set.filter(name='ad')
contacts.append(contacts_from_roles(roles))
elif group.type_id == 'wg':
roles = group.role_set.filter(name='chair')
contacts.append(contacts_from_roles(roles))
elif group.acronym == 'ietf':
contacts.append(EMAIL_ALIASES['IETFCHAIR'])
elif group.acronym == 'iab':
contacts.append(EMAIL_ALIASES['IABCHAIR'])
contacts.append(EMAIL_ALIASES['IABEXECUTIVEDIRECTOR'])
elif group.acronym == 'iesg':
contacts.append(EMAIL_ALIASES['IESG'])
return ','.join(contacts)
def get_details_tabs(stmt, selected):
return [
t + (t[0].lower() == selected.lower(),)
for t in [
('Statement', urlreverse('ietf.liaisons.views.liaison_detail', kwargs={ 'object_id': stmt.pk })),
('History', urlreverse('ietf.liaisons.views.liaison_history', kwargs={ 'object_id': stmt.pk }))
]]
def needs_approval(group,person):
'''Returns True if the person does not have authority to send a Liaison Statement
from group. For outgoing Liaison Statements only'''
user = person.user
if group.acronym in ('ietf','iesg') and has_role(user, 'IETF Chair'):
return False
if group.acronym == 'iab' and (has_role(user,'IAB Chair') or has_role(user,'IAB Executive Director')):
return False
if group.type_id == 'area' and group.role_set.filter(name='ad',person=person):
return False
if group.type_id == 'wg' and group.parent and group.parent.role_set.filter(name='ad',person=person):
return False
return True
def normalize_sort(request):
sort = request.GET.get('sort', "")
if sort not in ('date', 'deadline', 'title', 'to_groups', 'from_groups'):
sort = "date"
# reverse dates
order_by = "-" + sort if sort in ("date", "deadline") else sort
return sort, order_by
def post_only(group,person):
'''Returns true if the user is restricted to post_only (vs. post_and_send) for this
group. This is for incoming liaison statements.
- Secretariat have full access.
- Authorized Individuals have full access for the group they are associated with
- Liaison Managers can post only
'''
if group.type_id == 'sdo' and ( not(has_role(person.user,"Secretariat") or group.role_set.filter(name='auth',person=person)) ):
return True
else:
return False
# -------------------------------------------------
# Ajax Functions
# -------------------------------------------------
@can_submit_liaison_required
def ajax_get_liaison_info(request):
'''Returns dictionary of info to update entry form given the groups
that have been selected
'''
person = get_person_for_user(request.user)
from_groups = request.GET.getlist('from_groups', None)
if not any(from_groups):
from_groups = []
to_groups = request.GET.getlist('to_groups', None)
if not any(to_groups):
to_groups = []
from_groups = [ Group.objects.get(id=id) for id in from_groups ]
to_groups = [ Group.objects.get(id=id) for id in to_groups ]
cc = []
does_need_approval = []
can_post_only = []
to_contacts = []
response_contacts = []
result = {'response_contacts':[],'to_contacts': [], 'cc': [], 'needs_approval': False, 'post_only': False, 'full_list': []}
for group in from_groups:
cc.extend(get_cc(group))
does_need_approval.append(needs_approval(group,person))
can_post_only.append(post_only(group,person))
response_contacts.append(get_contacts_for_group(group))
for group in to_groups:
cc.extend(get_cc(group))
to_contacts.append(get_contacts_for_group(group))
# if there are from_groups and any need approval
if does_need_approval:
if any(does_need_approval):
does_need_approval = True
else:
does_need_approval = False
else:
does_need_approval = True
result.update({'error': False,
'cc': list(set(cc)),
'response_contacts':list(set(response_contacts)),
'to_contacts': list(set(to_contacts)),
'needs_approval': does_need_approval,
'post_only': any(can_post_only)})
json_result = json.dumps(result)
return HttpResponse(json_result, content_type='application/json')
def ajax_select2_search_liaison_statements(request):
query = [w.strip() for w in request.GET.get('q', '').split() if w.strip()]
if not query:
objs = LiaisonStatement.objects.none()
else:
qs = LiaisonStatement.objects.filter(state='posted')
for term in query:
if term.isdigit():
q = Q(title__icontains=term)|Q(pk=term)
else:
q = Q(title__icontains=term)
qs = qs.filter(q)
objs = qs.distinct().order_by("-id")[:20]
return HttpResponse(select2_id_liaison_json(objs), content_type='application/json')
# -------------------------------------------------
# Redirects for backwards compatibility
# -------------------------------------------------
def redirect_add(request):
"""Redirects old add urls"""
if 'incoming' in list(request.GET.keys()):
return redirect('ietf.liaisons.views.liaison_add', type='incoming')
else:
return redirect('ietf.liaisons.views.liaison_add', type='outgoing')
def redirect_for_approval(request, object_id=None):
"""Redirects old approval urls"""
if object_id:
return redirect('ietf.liaisons.views.liaison_detail', object_id=object_id)
else:
return redirect('ietf.liaisons.views.liaison_list', state='pending')
# -------------------------------------------------
# View Functions
# -------------------------------------------------
@role_required('Secretariat',)
def add_comment(request, object_id):
"""Add comment to history"""
statement = get_object_or_404(LiaisonStatement, id=object_id)
login = request.user.person
if request.method == 'POST':
form = AddCommentForm(request.POST)
if form.is_valid():
if form.cleaned_data.get('private'):
type_id = 'private_comment'
else:
type_id = 'comment'
LiaisonStatementEvent.objects.create(
by=login,
type_id=type_id,
statement=statement,
desc=form.cleaned_data['comment']
)
messages.success(request, 'Comment added.')
return redirect("ietf.liaisons.views.liaison_history", object_id=statement.id)
else:
form = AddCommentForm()
return render(request, 'liaisons/add_comment.html', dict(liaison=statement,form=form) )
@can_submit_liaison_required
def liaison_add(request, type=None, **kwargs):
if type == 'incoming' and not can_add_incoming_liaison(request.user):
permission_denied(request, "Restricted to users who are authorized to submit incoming liaison statements.")
if type == 'outgoing' and not can_add_outgoing_liaison(request.user):
permission_denied(request, "Restricted to users who are authorized to submit outgoing liaison statements.")
if request.method == 'POST':
form = liaison_form_factory(request, data=request.POST.copy(),
files=request.FILES, type=type, **kwargs)
if form.is_valid():
liaison = form.save()
# notifications
if 'save' in request.POST:
# the result of an edit, no notifications necessary
messages.success(request, 'The statement has been updated')
elif 'send' in request.POST and liaison.state.slug == 'posted':
send_liaison_by_email(request, liaison)
messages.success(request, 'The statement has been sent and posted')
elif liaison.state.slug == 'pending':
notify_pending_by_email(request, liaison)
messages.success(request, 'The statement has been submitted and is awaiting approval')
return redirect('ietf.liaisons.views.liaison_detail', object_id=liaison.pk)
else:
form = liaison_form_factory(request,type=type,**kwargs)
return render(request, 'liaisons/edit.html', {
'form': form,
'liaison': kwargs.get('instance')
})
def liaison_history(request, object_id):
"""Show the history for a specific liaison statement"""
liaison = get_object_or_404(LiaisonStatement, id=object_id)
events = liaison.liaisonstatementevent_set.all().order_by("-time", "-id").select_related("by")
if not has_role(request.user, "Secretariat"):
events = events.exclude(type='private_comment')
return render(request, "liaisons/detail_history.html", {
'events':events,
'liaison': liaison,
'tabs': get_details_tabs(liaison, 'History'),
'selected_tab_entry':'history'
})
def liaison_delete_attachment(request, object_id, attach_id):
liaison = get_object_or_404(LiaisonStatement, pk=object_id)
attach = get_object_or_404(LiaisonStatementAttachment, pk=attach_id)
if not can_edit_liaison(request.user, liaison):
permission_denied(request, "You are not authorized for this action.")
# FIXME: this view should use POST instead of GET when deleting
attach.removed = True
attach.save()
# create event
LiaisonStatementEvent.objects.create(
type_id='modified',
by=get_person_for_user(request.user),
statement=liaison,
desc='Attachment Removed: {}'.format(attach.document.title)
)
messages.success(request, 'Attachment Deleted')
return redirect('ietf.liaisons.views.liaison_detail', object_id=liaison.pk)
def liaison_detail(request, object_id):
liaison = get_object_or_404(LiaisonStatement, pk=object_id)
can_edit = can_edit_liaison(request.user, liaison)
can_take_care = _can_take_care(liaison, request.user)
can_reply = _can_reply(liaison, request.user)
person = get_person_for_user(request.user)
if request.method == 'POST':
if request.POST.get('approved'):
liaison.change_state(state_id='approved',person=person)
liaison.change_state(state_id='posted',person=person)
send_liaison_by_email(request, liaison)
messages.success(request,'Liaison Statement Approved and Posted')
elif request.POST.get('dead'):
liaison.change_state(state_id='dead',person=person)
messages.success(request,'Liaison Statement Killed')
elif request.POST.get('resurrect'):
liaison.change_state(state_id='pending',person=person)
messages.success(request,'Liaison Statement Resurrected')
elif request.POST.get('do_action_taken') and can_take_care:
liaison.tags.remove('required')
liaison.tags.add('taken')
can_take_care = False
messages.success(request,'Action handled')
relations_by = [i.target for i in liaison.source_of_set.filter(target__state__slug='posted')]
relations_to = [i.source for i in liaison.target_of_set.filter(source__state__slug='posted')]
return render(request, "liaisons/detail.html", {
"liaison": liaison,
'tabs': get_details_tabs(liaison, 'Statement'),
"can_edit": can_edit,
"can_take_care": can_take_care,
"can_reply": can_reply,
"relations_to": relations_to,
"relations_by": relations_by,
})
def liaison_edit(request, object_id):
liaison = get_object_or_404(LiaisonStatement, pk=object_id)
if not can_edit_liaison(request.user, liaison):
permission_denied(request, 'You do not have permission to edit this liaison statement.')
return liaison_add(request, instance=liaison)
def liaison_edit_attachment(request, object_id, doc_id):
'''Edit the Liaison Statement attachment title'''
liaison = get_object_or_404(LiaisonStatement, pk=object_id)
doc = get_object_or_404(Document, pk=doc_id)
if not can_edit_liaison(request.user, liaison):
permission_denied(request, "You are not authorized for this action.")
if request.method == 'POST':
form = EditAttachmentForm(request.POST)
if form.is_valid():
title = form.cleaned_data.get('title')
# create event
e = LiaisonStatementEvent.objects.create(
type_id='modified',
by=get_person_for_user(request.user),
statement=liaison,
desc='Attachment Title changed to {}'.format(title)
)
doc.title = title
doc.save_with_history([e])
messages.success(request,'Attachment title changed')
return redirect('ietf.liaisons.views.liaison_detail', object_id=liaison.pk)
else:
form = EditAttachmentForm(initial={'title':doc.title})
return render(request, 'liaisons/edit_attachment.html', {
'form': form,
'liaison': liaison
})
def liaison_list(request, state='posted'):
"""A generic list view with tabs for different states: posted, pending, dead"""
# use prefetch to speed up main liaison page load
selected_menu_entry = state
liaisons = LiaisonStatement.objects.filter(state=state).prefetch_related(
Prefetch('from_groups',queryset=Group.objects.order_by('acronym').select_related('type'),to_attr='prefetched_from_groups'),
Prefetch('to_groups',queryset=Group.objects.order_by('acronym').select_related('type'),to_attr='prefetched_to_groups'),
Prefetch('tags',queryset=LiaisonStatementTagName.objects.filter(slug='taken'),to_attr='prefetched_tags'),
Prefetch('liaisonstatementevent_set',queryset=LiaisonStatementEvent.objects.filter(type='posted'),to_attr='prefetched_posted_events')
)
# check authorization for pending and dead tabs
if state in ('pending','dead') and not can_add_liaison(request.user):
msg = "Restricted to participants who are authorized to submit liaison statements on behalf of the various IETF entities."
permission_denied(request, msg)
if 'tags' in request.GET:
value = request.GET.get('tags')
liaisons = liaisons.filter(tags__slug=value)
selected_menu_entry = 'action needed'
# perform search / filter
if 'text' in request.GET:
form = SearchLiaisonForm(data=request.GET,queryset=liaisons)
search_conducted = True
if form.is_valid():
results = form.get_results()
liaisons = results
else:
form = SearchLiaisonForm(queryset=liaisons)
search_conducted = False
# perform sort
sort, order_by = normalize_sort(request)
if sort == 'date':
liaisons = sorted(liaisons, key=lambda a: a.sort_date, reverse=True)
if sort == 'from_groups':
liaisons = sorted(liaisons, key=lambda a: a.sort_date, reverse=True)
liaisons = sorted(liaisons, key=lambda a: a.from_groups_display.lower())
if sort == 'to_groups':
liaisons = sorted(liaisons, key=lambda a: a.sort_date, reverse=True)
liaisons = sorted(liaisons, key=lambda a: a.to_groups_display.lower())
if sort == 'deadline':
liaisons = liaisons.order_by('-deadline')
if sort == 'title':
liaisons = liaisons.order_by('title')
# add menu entries
entries = []
entries.append(("Posted", urlreverse("ietf.liaisons.views.liaison_list", kwargs={'state':'posted'})))
if can_add_liaison(request.user):
entries.append(("Action Needed", urlreverse("ietf.liaisons.views.liaison_list", kwargs={'state':'posted'}) + '?tags=required'))
entries.append(("Pending", urlreverse("ietf.liaisons.views.liaison_list", kwargs={'state':'pending'})))
entries.append(("Dead", urlreverse("ietf.liaisons.views.liaison_list", kwargs={'state':'dead'})))
# add menu actions
actions = []
if can_add_incoming_liaison(request.user):
actions.append(("New incoming liaison", urlreverse("ietf.liaisons.views.liaison_add", kwargs={'type':'incoming'})))
if can_add_outgoing_liaison(request.user):
actions.append(("New outgoing liaison", urlreverse("ietf.liaisons.views.liaison_add", kwargs={'type':'outgoing'})))
return render(request, 'liaisons/liaison_base.html', {
'liaisons':liaisons,
'selected_menu_entry':selected_menu_entry,
'menu_entries':entries,
'menu_actions':actions,
'sort':sort,
'form':form,
'with_search':True,
'search_conducted':search_conducted,
'state':state,
})
def liaison_reply(request,object_id):
'''Returns a new liaison form with initial data to reply to the given liaison'''
liaison = get_object_or_404(LiaisonStatement, pk=object_id)
if liaison.is_outgoing():
reply_type = 'incoming'
else:
reply_type = 'outgoing'
initial = dict(
to_groups=[ x.pk for x in liaison.from_groups.all() ],
from_groups=[ x.pk for x in liaison.to_groups.all() ],
to_contacts=liaison.response_contacts,
related_to=str(liaison.pk))
return liaison_add(request,type=reply_type,initial=initial)
@role_required('Secretariat',)
def liaison_resend(request, object_id):
'''Resend the liaison statement notification email'''
liaison = get_object_or_404(LiaisonStatement, pk=object_id)
person = get_person_for_user(request.user)
send_liaison_by_email(request,liaison)
LiaisonStatementEvent.objects.create(type_id='resent',by=person,statement=liaison,desc='Statement Resent')
messages.success(request,'Liaison Statement resent')
return redirect('ietf.liaisons.views.liaison_list')