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
527 lines (444 loc) · 21.2 KB
/
views.py
File metadata and controls
527 lines (444 loc) · 21.2 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
# 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, ObjectDoesNotExist, PermissionDenied
from django.core.validators import validate_email
from django.db.models import Q, Prefetch
from django.http import Http404, HttpResponse
from django.shortcuts import render, get_object_or_404, redirect
import debug # pyflakes:ignore
from ietf.ietfauth.utils import role_required, has_role
from ietf.group.models import Group, Role
from ietf.liaisons.models import LiaisonStatement,LiaisonStatementEvent
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
# -------------------------------------------------
# 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") or has_role(user, "Liaison Coordinator"):
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
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_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'):
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 has_role(person.user, "Liaison Coordinator")
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):
from ietf.mailtrigger.utils import get_contacts_for_liaison_messages_for_group_primary,get_contacts_for_liaison_messages_for_group_secondary
'''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_contacts_for_liaison_messages_for_group_primary(group))
does_need_approval.append(needs_approval(group,person))
can_post_only.append(post_only(group,person))
response_contacts.append(get_contacts_for_liaison_messages_for_group_secondary(group))
for group in to_groups:
cc.extend(get_contacts_for_liaison_messages_for_group_primary(group))
to_contacts.append(get_contacts_for_liaison_messages_for_group_secondary(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)
if not can_edit_liaison(request.user, liaison):
permission_denied(request, "You are not authorized for this action.")
else:
permission_denied(request, "This operation is temporarily unavailable. Ask the secretariat to mark the attachment as removed using the admin.")
# The following will be replaced with a different approach in the next generation of the liaison tool
# attach = get_object_or_404(LiaisonStatementAttachment, pk=attach_id)
# # FIXME: this view should use POST instead of GET when deleting
# attach.removed = True
# debug.say("Got here")
# 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('do_action_taken') and can_take_care:
liaison.tags.remove('required')
liaison.tags.add('taken')
can_take_care = False
messages.success(request,'Action handled')
else:
if can_edit:
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')
else:
pass
else:
raise PermissionDenied()
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)
try:
doc = liaison.attachments.get(pk=doc_id)
except ObjectDoesNotExist:
raise Http404
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')