Skip to content

Commit 5133e59

Browse files
committed
Merged in [8711] from rjsparks@nostrum.com:\n Added functionality for management of group materials,
focusing on groups of type 'team'. * Simplifies managing the Abstract for uploaded group materials. Displays the abstract on the groups materials page * Provides SessionPresentation: a binding between Document and Session objects, that tracks doc version. * Adds helpers for finding upcoming sessions, and the most recent SessionPresentations whose sessions have passed * Generalizes the snippet concept (as a templatetage) used in the history tab for use on other pages * Adds a variant of the snippet that can be styled as an edit link * Provides several ways to address sessions, using sequence, dates, group acronyms, and day of week * Adds a 'Materials Manager' Role - Legacy-Id: 8765 Note: SVN reference [8711] has been migrated to Git commit b4448a8
2 parents da9419a + b4448a8 commit 5133e59

29 files changed

Lines changed: 1892 additions & 39 deletions

ietf/doc/migrations/0024_archive_slides.py

Lines changed: 398 additions & 0 deletions
Large diffs are not rendered by default.

ietf/doc/models.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,32 @@ def active_ballot(self):
186186
return None
187187

188188
def meeting_related(self):
189-
return(self.type_id in ("agenda", "minutes", "slides") and (
190-
self.name.split("-")[1] == "interim"
191-
or (self.session_set.exists() if isinstance(self, Document) else self.doc.session_set.exists())))
189+
if self.type_id in ("agenda","minutes",):
190+
return (self.name.split("-")[1] == "interim"
191+
or (self.session_set.exists() if isinstance(self, Document) else self.doc.session_set.exists()))
192+
elif self.type_id in ("slides",):
193+
return (self.name.split("-")[1] == "interim"
194+
or (self.get_state('slides') in ("sessonly","archived") ))
195+
else:
196+
return False
197+
198+
def future_presentations(self):
199+
""" returns related SessionPresentation objects for meetings that
200+
have not yet ended. This implementation allows for 2 week meetings """
201+
candidate_presentations = self.sessionpresentation_set.filter(session__meeting__date__gte=datetime.date.today()-datetime.timedelta(days=15))
202+
return sorted([pres for pres in candidate_presentations if pres.session.meeting.end_date()>=datetime.date.today()], key=lambda x:x.session.meeting.date)
203+
204+
def last_presented(self):
205+
""" returns related SessionPresentation objects for the most recent meeting in the past"""
206+
# Assumes no two meetings have the same start date - if the assumption is violated, one will be chosen arbitrariy
207+
candidate_presentations = self.sessionpresentation_set.filter(session__meeting__date__lte=datetime.date.today())
208+
candidate_meetings = set([p.session.meeting for p in candidate_presentations if p.session.meeting.end_date()<datetime.date.today()])
209+
if candidate_meetings:
210+
mtg = sorted(list(candidate_meetings),key=lambda x:x.date,reverse=True)[0]
211+
return self.sessionpresentation_set.filter(session__meeting=mtg)
212+
else:
213+
return None
214+
192215

193216
class Meta:
194217
abstract = True

ietf/doc/templatetags/ietf_filters.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,12 +448,29 @@ def format_history_text(text):
448448
if text.startswith("This was part of a ballot set with:"):
449449
full = urlize_ietf_docs(full)
450450

451-
full = mark_safe(keep_spacing(linebreaksbr(urlize(sanitize_html(full)))))
451+
return format_snippet(full)
452+
453+
@register.filter
454+
def format_snippet(text):
455+
full = mark_safe(keep_spacing(linebreaksbr(urlize(sanitize_html(text)))))
452456
snippet = truncatewords_html(full, 25)
453457
if snippet != full:
454458
return mark_safe(u'<div class="snippet">%s<span class="show-all">[show all]</span></div><div style="display:none" class="full">%s</div>' % (snippet, full))
455459
return full
456460

461+
@register.filter
462+
def format_editable_snippet(text,link):
463+
full = mark_safe(keep_spacing(linebreaksbr(urlize(sanitize_html(text)))))
464+
snippet = truncatewords_html(full, 25)
465+
if snippet != full:
466+
return mark_safe(u'<div class="snippet">%s<span class="show-all">[show all]</span></div><div style="display:none" class="full">%s' % (format_editable(snippet,link),format_editable(full,link)) )
467+
else:
468+
return format_editable(full,link)
469+
470+
@register.filter
471+
def format_editable(text,link):
472+
return mark_safe(u'<a class="editlink" href="%s">%s</a>' % (link,text))
473+
457474
@register.filter
458475
def textify(text):
459476
text = re.sub("</?b>", "*", text)

ietf/doc/tests_material.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def test_upload_slides(self):
6666

6767
# post
6868
r = self.client.post(url, dict(title="Test File - with fancy title",
69+
abstract = "Test Abstract",
6970
name="slides-%s-test-file" % group.acronym,
7071
state=State.objects.get(type="slides", slug="active").pk,
7172
material=test_file))
@@ -125,6 +126,7 @@ def test_revise(self):
125126

126127
# post
127128
r = self.client.post(url, dict(title="New title",
129+
abstract="New abstract",
128130
state=State.objects.get(type="slides", slug="active").pk,
129131
material=test_file))
130132
self.assertEqual(r.status_code, 302)

ietf/doc/urls_material.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
from django.conf.urls import patterns, url
22

33
urlpatterns = patterns('ietf.doc.views_material',
4-
url(r'^(?P<action>state|title|revise)/$', "edit_material", name="material_edit"),
4+
url(r'^(?P<action>state|title|abstract|revise)/$', "edit_material", name="material_edit"),
5+
url(r'^sessions/$', "material_presentations", name="material_presentations"),
6+
(r'^sessions/(?P<seq>\d+)/edit/$', "edit_material_presentations"),
7+
(r'^sessions/(?P<acronym>[A-Za-z0-9_\-\+]+)/edit/$', "edit_material_presentations"),
8+
(r'^sessions/(?P<acronym>[A-Za-z0-9_\-\+]+)/(?P<seq>\d+)/edit/$', "edit_material_presentations"),
9+
(r'^sessions/(?P<acronym>[A-Za-z0-9_\-\+]+)/(?P<week_day>[a-zA-Z]+)/edit/$', "edit_material_presentations"),
10+
(r'^sessions/(?P<acronym>[A-Za-z0-9_\-\+]+)/(?P<week_day>[a-zA-Z]+)/(?P<seq>\d+)/edit/$', "edit_material_presentations"),
11+
(r'^sessions/(?P<acronym>[A-Za-z0-9_\-\+]+)/(?P<date>\d{4}-\d{2}-\d{2}(-\d{4})?)/edit/$', "edit_material_presentations"),
12+
(r'^sessions/(?P<acronym>[A-Za-z0-9_\-\+]+)/(?P<date>\d{4}-\d{2}-\d{2}(-\d{4})?)/(?P<seq>\d+)/edit/$', "edit_material_presentations"),
13+
(r'^sessions/(?P<seq>\d+)/$', "material_presentations"),
14+
(r'^sessions/(?P<acronym>[A-Za-z0-9_\-\+]+)/$', "material_presentations"),
15+
(r'^sessions/(?P<acronym>[A-Za-z0-9_\-\+]+)/(?P<seq>\d+)/$', "material_presentations"),
16+
(r'^sessions/(?P<acronym>[A-Za-z0-9_\-\+]+)/(?P<week_day>[a-zA-Z]+)/$', "material_presentations"),
17+
(r'^sessions/(?P<acronym>[A-Za-z0-9_\-\+]+)/(?P<week_day>[a-zA-Z]+)/(?P<seq>\d+)/$', "material_presentations"),
18+
(r'^sessions/(?P<acronym>[A-Za-z0-9_\-\+]+)/(?P<date>\d{4}-\d{2}-\d{2}(-\d{4})?)/$', "material_presentations"),
19+
(r'^sessions/(?P<acronym>[A-Za-z0-9_\-\+]+)/(?P<date>\d{4}-\d{2}-\d{2}(-\d{4})?)/(?P<seq>\d+)/$', "material_presentations"),
520
)
621

ietf/doc/views_doc.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,9 @@ def document_main(request, name, rev=None):
499499

500500
if doc.type_id in ("slides", "agenda", "minutes"):
501501
can_manage_material = can_manage_materials(request.user, doc.group)
502+
presentations = None
503+
if doc.type_id=='slides' and doc.get_state_slug('slides') in ['sessonly','active']:
504+
presentations = doc.future_presentations()
502505
if doc.meeting_related():
503506
# disallow editing meeting-related stuff through this
504507
# interface for the time being
@@ -536,6 +539,7 @@ def document_main(request, name, rev=None):
536539
snapshot=snapshot,
537540
can_manage_material=can_manage_material,
538541
other_types=other_types,
542+
presentations=presentations,
539543
),
540544
context_instance=RequestContext(request))
541545

ietf/doc/views_material.py

Lines changed: 159 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from ietf.doc.utils import add_state_change_event, check_common_doc_name_rules
1818
from ietf.group.models import Group
1919
from ietf.group.utils import can_manage_materials
20+
from ietf.meeting.models import Session
2021

2122
@login_required
2223
def choose_material_type(request, acronym):
@@ -32,8 +33,9 @@ def choose_material_type(request, acronym):
3233
class UploadMaterialForm(forms.Form):
3334
title = forms.CharField(max_length=Document._meta.get_field("title").max_length)
3435
name = forms.CharField(max_length=Document._meta.get_field("name").max_length)
36+
abstract = forms.CharField(max_length=Document._meta.get_field("abstract").max_length,widget=forms.Textarea)
3537
state = forms.ModelChoiceField(State.objects.all(), empty_label=None)
36-
material = forms.FileField(label='File', help_text="PDF or text file (ASCII/UTF-8)")
38+
material = forms.FileField(label='File')
3739

3840
def __init__(self, doc_type, action, group, doc, *args, **kwargs):
3941
super(UploadMaterialForm, self).__init__(*args, **kwargs)
@@ -53,16 +55,15 @@ def __init__(self, doc_type, action, group, doc, *args, **kwargs):
5355
del self.fields["name"]
5456

5557
self.fields["title"].initial = doc.title
58+
self.fields["abstract"].initial = doc.abstract
5659
self.fields["state"].initial = doc.get_state().pk if doc.get_state() else None
5760
if doc.get_state_slug() == "deleted":
5861
self.fields["state"].help_text = "Note: If you wish to revise this document, you may wish to change the state so it's not deleted."
5962

60-
if action == "title":
61-
del self.fields["state"]
62-
del self.fields["material"]
63-
elif action == "state":
64-
del self.fields["title"]
65-
del self.fields["material"]
63+
if action in ["title","state","abstract"]:
64+
for fieldname in ["title","state","material","abstract"]:
65+
if fieldname != action:
66+
del self.fields[fieldname]
6667

6768
def clean_name(self):
6869
name = self.cleaned_data["name"].strip().rstrip("-")
@@ -120,6 +121,9 @@ def edit_material(request, name=None, acronym=None, action=None, doc_type=None):
120121
if "title" in form.cleaned_data:
121122
doc.title = form.cleaned_data["title"]
122123

124+
if "abstract" in form.cleaned_data:
125+
doc.abstract = form.cleaned_data["abstract"]
126+
123127
doc.time = datetime.datetime.now()
124128

125129
if "material" in form.fields:
@@ -168,3 +172,151 @@ def edit_material(request, name=None, acronym=None, action=None, doc_type=None):
168172
'document_type': document_type,
169173
'doc_name': doc.name if doc else "",
170174
})
175+
176+
class MaterialVersionForm(forms.Form):
177+
178+
version = forms.ChoiceField(required=False,
179+
label='Which version of this document will be presented at this session')
180+
181+
def __init__(self, *args, **kwargs):
182+
choices = kwargs.pop('choices')
183+
super(MaterialVersionForm,self).__init__(*args,**kwargs)
184+
self.fields['version'].choices = choices
185+
186+
def get_upcoming_manageable_sessions(user, doc, acronym=None, date=None, seq=None, week_day = None):
187+
188+
# Find all the sessions for meetings that haven't ended that the user could affect
189+
# This motif is also in Document.future_presentations - it would be nice to consolodate it somehow
190+
191+
candidate_sessions = Session.objects.exclude(status__in=['canceled','disappr','notmeet','deleted']).filter(meeting__date__gte=datetime.date.today()-datetime.timedelta(days=15))
192+
refined_candidates = [ sess for sess in candidate_sessions if sess.meeting.end_date()>=datetime.date.today()]
193+
194+
if acronym:
195+
refined_candidates = [ sess for sess in refined_candidates if sess.group.acronym==acronym]
196+
197+
if date:
198+
if len(date)==15:
199+
start = datetime.datetime.strptime(date,"%Y-%m-%d-%H%M")
200+
refined_candidates = [ sess for sess in refined_candidates if sess.scheduledsession_set.filter(schedule=sess.meeting.agenda,timeslot__time=start) ]
201+
else:
202+
start = datetime.datetime.strptime(date,"%Y-%m-%d").date()
203+
end = start+datetime.timedelta(days=1)
204+
refined_candidates = [ sess for sess in refined_candidates if sess.scheduledsession_set.filter(schedule=sess.meeting.agenda,timeslot__time__range=(start,end)) ]
205+
206+
if week_day:
207+
try:
208+
dow = ['sun','mon','tue','wed','thu','fri','sat'].index(week_day.lower()[:3]) + 1
209+
except ValueError:
210+
raise Http404
211+
refined_candidates = [ sess for sess in refined_candidates if sess.scheduledsession_set.filter(schedule=sess.meeting.agenda,timeslot__time__week_day=dow) ]
212+
213+
changeable_sessions = [ sess for sess in refined_candidates if can_manage_materials(user, sess.group) ]
214+
215+
if not changeable_sessions:
216+
raise Http404
217+
218+
for sess in changeable_sessions:
219+
sess.has_presentation = bool(sess.sessionpresentation_set.filter(document=doc))
220+
if sess.has_presentation:
221+
sess.version = sess.sessionpresentation_set.get(document=doc).rev
222+
223+
# Since Python 2.2 sorts are stable, so this series results in a list sorted first by whether
224+
# the session has any presentations, then by the meeting 'number', then by session's group
225+
# acronym, then by scheduled time (or the time of the session request if the session isn't
226+
# scheduled).
227+
228+
def time_sort_key(session):
229+
official_sessions = session.scheduledsession_set.filter(schedule=session.meeting.agenda)
230+
if official_sessions:
231+
return official_sessions.first().timeslot.time
232+
else:
233+
return session.requested
234+
235+
time_sorted = sorted(changeable_sessions,key=time_sort_key)
236+
acronym_sorted = sorted(time_sorted,key=lambda x: x.group.acronym)
237+
meeting_sorted = sorted(acronym_sorted,key=lambda x: x.meeting.number)
238+
sorted_sessions = sorted(meeting_sorted,key=lambda x: '0' if x.has_presentation else '1')
239+
240+
if seq:
241+
iseq = int(seq) - 1
242+
if not iseq in range(0,len(sorted_sessions)):
243+
raise Http404
244+
else:
245+
sorted_sessions = [sorted_sessions[iseq]]
246+
247+
return sorted_sessions
248+
249+
@login_required
250+
def edit_material_presentations(request, name, acronym=None, date=None, seq=None, week_day=None):
251+
252+
doc = get_object_or_404(Document, name=name)
253+
if not (doc.type_id=='slides' and doc.get_state('slides').slug=='active'):
254+
raise Http404
255+
256+
group = doc.group
257+
if not (group.features.has_materials and can_manage_materials(request.user,group)):
258+
raise Http404
259+
260+
sorted_sessions = get_upcoming_manageable_sessions(request.user, doc, acronym, date, seq, week_day)
261+
262+
if len(sorted_sessions)!=1:
263+
raise Http404
264+
265+
session = sorted_sessions[0]
266+
choices = [('notpresented','Not Presented')]
267+
choices.extend([(x,x) for x in doc.docevent_set.filter(type='new_revision').values_list('newrevisiondocevent__rev',flat=True)])
268+
initial = {'version' : session.version if hasattr(session,'version') else 'notpresented'}
269+
270+
if request.method == 'POST':
271+
form = MaterialVersionForm(request.POST,choices=choices)
272+
if form.is_valid():
273+
if request.POST.get("action", "") == "Save":
274+
new_selection = form.cleaned_data['version']
275+
if initial['version'] != new_selection:
276+
if initial['version'] == 'notpresented':
277+
doc.sessionpresentation_set.create(session=session,rev=new_selection)
278+
c = DocEvent(type="added_comment", doc=doc, by=request.user.person)
279+
c.desc = "Added version %s to session: %s" % (new_selection,session)
280+
c.save()
281+
elif new_selection == 'notpresented':
282+
doc.sessionpresentation_set.filter(session=session).delete()
283+
c = DocEvent(type="added_comment", doc=doc, by=request.user.person)
284+
c.desc = "Removed from session: %s" % (session)
285+
c.save()
286+
else:
287+
doc.sessionpresentation_set.filter(session=session).update(rev=new_selection)
288+
c = DocEvent(type="added_comment", doc=doc, by=request.user.person)
289+
c.desc = "Revision for session %s changed to %s" % (session,new_selection)
290+
c.save()
291+
return redirect('doc_view',name=doc.name)
292+
else:
293+
form = MaterialVersionForm(choices=choices,initial=initial)
294+
295+
return render(request, 'doc/material/edit_material_presentations.html', {
296+
'session': session,
297+
'doc': doc,
298+
'form': form,
299+
})
300+
301+
@login_required
302+
def material_presentations(request, name, acronym=None, date=None, seq=None, week_day=None):
303+
304+
doc = get_object_or_404(Document, name=name)
305+
if not (doc.type_id=='slides' and doc.get_state('slides').slug=='active'):
306+
raise Http404
307+
308+
group = doc.group
309+
if not (group.features.has_materials and can_manage_materials(request.user,group)):
310+
raise Http404
311+
312+
sorted_sessions = get_upcoming_manageable_sessions(request.user, doc, acronym, date, seq, week_day)
313+
314+
#for index,session in enumerate(sorted_sessions):
315+
# session.sequence = index+1
316+
317+
return render(request, 'doc/material/material_presentations.html', {
318+
'sessions' : sorted_sessions,
319+
'doc': doc,
320+
'date': date,
321+
'week_day': week_day,
322+
})

ietf/group/info.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,8 @@ def concluded_groups(request):
267267
dict(group_types=group_types))
268268

269269
def get_group_materials(group):
270-
return Document.objects.filter(group=group, type__in=group.features.material_types, session=None).exclude(states__slug="deleted")
270+
# return Document.objects.filter(group=group, type__in=group.features.material_types, session=None).exclude(states__slug="deleted")
271+
return Document.objects.filter(group=group, type__in=group.features.material_types).exclude(states__slug__in=['deleted','archived'])
271272

272273
def construct_group_menu_context(request, group, selected, group_type, others):
273274
"""Return context with info for the group menu filled in."""

0 commit comments

Comments
 (0)