From 89a3afe025926b49c5f6c336dc0956ba735807d4 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Fri, 26 Aug 2016 21:06:01 +0000 Subject: [PATCH 1/2] Added an order field to sessionpresentation to allow ordering slides. Reworked the session details page to break slides and drafts into their own sections. Commit ready for merge. - Legacy-Id: 11888 --- .../migrations/0009_auto_20150930_0758.py | 1 + .../0036_add_order_to_sessionpresentation.py | 31 +++++++ ietf/meeting/models.py | 1 + ietf/meeting/views.py | 11 ++- ietf/templates/meeting/session_details.html | 92 ++++++++++++++----- 5 files changed, 110 insertions(+), 26 deletions(-) create mode 100644 ietf/meeting/migrations/0036_add_order_to_sessionpresentation.py diff --git a/ietf/group/migrations/0009_auto_20150930_0758.py b/ietf/group/migrations/0009_auto_20150930_0758.py index e84626a50..2071cc898 100644 --- a/ietf/group/migrations/0009_auto_20150930_0758.py +++ b/ietf/group/migrations/0009_auto_20150930_0758.py @@ -109,6 +109,7 @@ class Migration(migrations.Migration): dependencies = [ ('doc', '0010_auto_20150930_0251'), ('group', '0008_auto_20160505_0523'), + ('community', '0004_cleanup_data'), ] operations = [ diff --git a/ietf/meeting/migrations/0036_add_order_to_sessionpresentation.py b/ietf/meeting/migrations/0036_add_order_to_sessionpresentation.py new file mode 100644 index 000000000..0388eef09 --- /dev/null +++ b/ietf/meeting/migrations/0036_add_order_to_sessionpresentation.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import debug # pyflakes:ignore + +from django.db import models, migrations + +def forward(apps, schema_editor): + SessionPresentation = apps.get_model("meeting","SessionPresentation") + for sp in SessionPresentation.objects.filter(document__type__slug='slides',session__meeting__number__in=['95','96']): + sp.order = int(sp.document.name.split('-')[-1]) + sp.save() + +def reverse(apps, schema_editor): + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('meeting', '0035_auto_20160818_1610'), + ] + + operations = [ + migrations.AddField( + model_name='sessionpresentation', + name='order', + field=models.PositiveSmallIntegerField(default=0), + preserve_default=True, + ), + migrations.RunPython(forward,reverse) + ] diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index efcddd629..e37d435de 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -959,6 +959,7 @@ class SessionPresentation(models.Model): session = models.ForeignKey('Session') document = models.ForeignKey(Document) rev = models.CharField(verbose_name="revision", max_length=16, null=True, blank=True) + order = models.PositiveSmallIntegerField(default=0) class Meta: db_table = 'meeting_session_materials' diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 94fe57d3e..42c7f0884 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -1022,9 +1022,16 @@ def session_details(request, num, acronym ): else: session.time = session.status.name + session.filtered_artifacts = session.sessionpresentation_set.filter(document__type__slug__in=['agenda','minutes','bluesheets']) + session.filtered_slides = session.sessionpresentation_set.filter(document__type__slug='slides').order_by('order') + session.filtered_drafts = session.sessionpresentation_set.filter(document__type__slug='draft') # TODO FIXME Deleted materials shouldn't be in the sessionpresentation_set - session.filtered_sessionpresentation_set = [p for p in session.sessionpresentation_set.all() if p.document.get_state_slug(p.document.type_id)!='deleted'] - type_counter.update([p.document.type.slug for p in session.filtered_sessionpresentation_set]) + for qs in [session.filtered_artifacts,session.filtered_slides,session.filtered_drafts]: + qs = [p for p in qs if p.document.get_state_slug(p.document.type_id)!='deleted'] + type_counter.update([p.document.type.slug for p in qs]) + + #session.filtered_sessionpresentation_set = [p for p in session.sessionpresentation_set.all() if p.document.get_state_slug(p.document.type_id)!='deleted'] + #type_counter.update([p.document.type.slug for p in session.filtered_sessionpresentation_set]) can_manage = can_manage_materials(request.user, Group.objects.get(acronym=acronym)) diff --git a/ietf/templates/meeting/session_details.html b/ietf/templates/meeting/session_details.html index 0a3cc674f..a6f4f3abf 100644 --- a/ietf/templates/meeting/session_details.html +++ b/ietf/templates/meeting/session_details.html @@ -10,6 +10,7 @@ {% for session in sessions %}

{% if sessions|length > 1 %}Session {{ forloop.counter }} : {% endif %}{{ session.time }}{% if session.name %} : {{ session.name }}{% endif %}

+ {% if session.agenda_note %}

{{session.agenda_note}}

{% endif %} {% if can_manage_materials %} {% if session.status.slug == 'sched' or session.status.slug == 'schedw' %} @@ -33,30 +34,73 @@ {% endif %} {% endif %} - {% if session.filtered_sessionpresentation_set %} -
-
Materials:
-
- - {% for pres in session.filtered_sessionpresentation_set %} - {% if pres.document.type_id != 'bluesheets' and pres.document.type_id != 'recording' %} - - - - {% endif %} - {% endfor %} -
- {% if pres.rev %} - {% url 'doc_view' name=pres.document.name rev=pres.rev as url %} - {% else %} - {% url 'doc_view' name=pres.document.name as url %} - {% endif %} - {{pres.document.title}} ({{ pres.document.name }}{% if pres.rev %}-{{ pres.rev }}{% endif %}) - -
-
-
- {% endif %} + {% if session.filtered_artifacts %} +
+
Artifacts
+
+ + {% for pres in session.filtered_artifacts %} + + + + {% endfor %} +
+ {% if pres.rev %} + {% url 'doc_view' name=pres.document.name rev=pres.rev as url %} + {% else %} + {% url 'doc_view' name=pres.document.name as url %} + {% endif %} + {{pres.document.title}} ({{ pres.document.name }}{% if pres.rev %}-{{ pres.rev }}{% endif %}) + +
+
+
+ {% endif %} + {% if session.filtered_slides %} +
+
Slides
+
+ + {% for pres in session.filtered_slides %} + + + + {% endfor %} +
+ {% if pres.rev %} + {% url 'doc_view' name=pres.document.name rev=pres.rev as url %} + {% else %} + {% url 'doc_view' name=pres.document.name as url %} + {% endif %} + {{pres.document.title}} ({{ pres.document.name }}{% if pres.rev %}-{{ pres.rev }}{% endif %}) + +
+
+
+ {% endif %} + {% if session.filtered_drafts %} +
+
Drafts
+
+ + {% for pres in session.filtered_drafts %} + + + + {% endfor %} +
+ {% if pres.rev %} + {% url 'doc_view' name=pres.document.name rev=pres.rev as url %} + {% else %} + {% url 'doc_view' name=pres.document.name as url %} + {% endif %} + {{pres.document.title}} ({{ pres.document.name }}{% if pres.rev %}-{{ pres.rev }}{% endif %}) + +
+
+
+ {% endif %} + {% endfor %} {% endblock %} From 53f41a6327e0a8f921aba7adf944f89d92a5d54d Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 14 Sep 2016 19:11:31 +0000 Subject: [PATCH 2/2] Added an order field to SessionPresentation. Set up removing the order field from Document. Modified the meeting materials, proceedings, and session details views to use the order field on SessionPresentation. Added bootstrapped views to upload session materials (agendas, slides, minutes, and bluesheets). Integrated upload into the session details view. Commit ready for merge. - Legacy-Id: 11989 --- ietf/doc/models.py | 2 +- ...nge_meta_options_on_sessionpresentation.py | 18 + ietf/meeting/models.py | 3 +- ietf/meeting/tests_views.py | 136 +++++++- ietf/meeting/urls.py | 3 + ietf/meeting/views.py | 311 +++++++++++++++++- ietf/templates/meeting/session_details.html | 190 ++++++----- .../meeting/upload_session_agenda.html | 22 ++ .../meeting/upload_session_bluesheets.html | 12 +- .../meeting/upload_session_minutes.html | 22 ++ .../meeting/upload_session_slides.html | 23 ++ 11 files changed, 631 insertions(+), 111 deletions(-) create mode 100644 ietf/meeting/migrations/0037_change_meta_options_on_sessionpresentation.py create mode 100644 ietf/templates/meeting/upload_session_agenda.html create mode 100644 ietf/templates/meeting/upload_session_minutes.html create mode 100644 ietf/templates/meeting/upload_session_slides.html diff --git a/ietf/doc/models.py b/ietf/doc/models.py index 4937280de..96f40dc05 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -59,7 +59,7 @@ class DocumentInfo(models.Model): abstract = models.TextField(blank=True) rev = models.CharField(verbose_name="revision", max_length=16, blank=True) pages = models.IntegerField(blank=True, null=True) - order = models.IntegerField(default=1, blank=True) + order = models.IntegerField(default=1, blank=True) # This is probably obviated by SessionPresentaion.order intended_std_level = models.ForeignKey(IntendedStdLevelName, verbose_name="Intended standardization level", blank=True, null=True) std_level = models.ForeignKey(StdLevelName, verbose_name="Standardization level", blank=True, null=True) ad = models.ForeignKey(Person, verbose_name="area director", related_name='ad_%(class)s_set', blank=True, null=True) diff --git a/ietf/meeting/migrations/0037_change_meta_options_on_sessionpresentation.py b/ietf/meeting/migrations/0037_change_meta_options_on_sessionpresentation.py new file mode 100644 index 000000000..c823c62d5 --- /dev/null +++ b/ietf/meeting/migrations/0037_change_meta_options_on_sessionpresentation.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('meeting', '0036_add_order_to_sessionpresentation'), + ] + + operations = [ + migrations.AlterModelOptions( + name='sessionpresentation', + options={'ordering': ('order',)}, + ), + ] diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index e37d435de..f88279b94 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -963,6 +963,7 @@ class SessionPresentation(models.Model): class Meta: db_table = 'meeting_session_materials' + ordering = ('order',) def __unicode__(self): return u"%s -> %s-%s" % (self.session, self.document.name, self.rev) @@ -1005,7 +1006,7 @@ class Session(models.Model): for d in l: d.meeting_related = lambda: True else: - l = self.materials.filter(type=material_type).exclude(states__type=material_type, states__slug='deleted').order_by("order") + l = self.materials.filter(type=material_type).exclude(states__type=material_type, states__slug='deleted').order_by('sessionpresentation__order') if only_one: if l: diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 3811f2dd4..66badd226 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -1271,7 +1271,7 @@ class FinalizeProceedingsTests(TestCase): self.assertEqual(meeting.proceedings_final,True) self.assertEqual(meeting.session_set.filter(group__acronym="mars").first().sessionpresentation_set.filter(document__type="draft").first().rev,'00') -class BluesheetsTests(TestCase): +class MaterialsTests(TestCase): def setUp(self): self.materials_dir = os.path.abspath(settings.TEST_MATERIALS_DIR) @@ -1284,14 +1284,14 @@ class BluesheetsTests(TestCase): settings.AGENDA_PATH = self.saved_agenda_path shutil.rmtree(self.materials_dir) - def test_upload_blusheets(self): + def test_upload_bluesheets(self): session = SessionFactory(meeting__type_id='ietf') url = urlreverse('ietf.meeting.views.upload_session_bluesheets',kwargs={'num':session.meeting.number,'session_id':session.id}) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertFalse(q("div.alert")) + self.assertTrue('Upload' in unicode(q("title"))) self.assertFalse(session.sessionpresentation_set.exists()) test_file = StringIO('this is some text for a test') test_file.name = "not_really.pdf" @@ -1302,7 +1302,7 @@ class BluesheetsTests(TestCase): r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertTrue(q("div.alert")) + self.assertTrue('Revise' in unicode(q("title"))) test_file = StringIO('this is some different text for a test') test_file.name = "also_not_really.pdf" r = self.client.post(url,dict(file=test_file)) @@ -1317,7 +1317,7 @@ class BluesheetsTests(TestCase): r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertFalse(q("div.alert")) + self.assertTrue('Upload' in unicode(q("title"))) self.assertFalse(session.sessionpresentation_set.exists()) test_file = StringIO('this is some text for a test') test_file.name = "not_really.pdf" @@ -1325,3 +1325,129 @@ class BluesheetsTests(TestCase): self.assertEqual(r.status_code, 302) bs_doc = session.sessionpresentation_set.filter(document__type_id='bluesheets').first().document self.assertEqual(bs_doc.rev,'00') + + def test_upload_minutes_agenda(self): + for doctype in ('minutes','agenda'): + session = SessionFactory(meeting__type_id='ietf') + if doctype == 'minutes': + url = urlreverse('ietf.meeting.views.upload_session_minutes',kwargs={'num':session.meeting.number,'session_id':session.id}) + else: + url = urlreverse('ietf.meeting.views.upload_session_agenda',kwargs={'num':session.meeting.number,'session_id':session.id}) + self.client.logout() + login_testing_unauthorized(self,"secretary",url) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue('Upload' in unicode(q("Title"))) + self.assertFalse(session.sessionpresentation_set.exists()) + self.assertFalse(q('form input[type="checkbox"]')) + + session2 = SessionFactory(meeting=session.meeting,group=session.group) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(q('form input[type="checkbox"]')) + + test_file = StringIO('this is some text for a test') + test_file.name = "not_really.json" + r = self.client.post(url,dict(file=test_file)) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(q('form .has-error')) + + test_file = StringIO('this is some text for a test'*1510000) + test_file.name = "not_really.pdf" + r = self.client.post(url,dict(file=test_file)) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(q('form .has-error')) + + test_file = StringIO('this is some text for a test') + test_file.name = "not_really.txt" + r = self.client.post(url,dict(file=test_file,apply_to_all=False)) + self.assertEqual(r.status_code, 302) + doc = session.sessionpresentation_set.filter(document__type_id=doctype).first().document + self.assertEqual(doc.rev,'00') + self.assertFalse(session2.sessionpresentation_set.filter(document__type_id=doctype)) + + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue('Revise' in unicode(q("Title"))) + test_file = StringIO('this is some different text for a test') + test_file.name = "also_not_really.txt" + r = self.client.post(url,dict(file=test_file,apply_to_all=True)) + self.assertEqual(r.status_code, 302) + doc = Document.objects.get(pk=doc.pk) + self.assertEqual(doc.rev,'01') + self.assertTrue(session2.sessionpresentation_set.filter(document__type_id=doctype)) + + def test_upload_minutes_agenda_interim(self): + session=SessionFactory(meeting__type_id='interim') + for doctype in ('minutes','agenda'): + if doctype=='minutes': + url = urlreverse('ietf.meeting.views.upload_session_minutes',kwargs={'num':session.meeting.number,'session_id':session.id}) + else: + url = urlreverse('ietf.meeting.views.upload_session_agenda',kwargs={'num':session.meeting.number,'session_id':session.id}) + self.client.logout() + login_testing_unauthorized(self,"secretary",url) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue('Upload' in unicode(q("title"))) + self.assertFalse(session.sessionpresentation_set.filter(document__type_id=doctype)) + test_file = StringIO('this is some text for a test') + test_file.name = "not_really.txt" + r = self.client.post(url,dict(file=test_file)) + self.assertEqual(r.status_code, 302) + doc = session.sessionpresentation_set.filter(document__type_id=doctype).first().document + self.assertEqual(doc.rev,'00') + + def test_upload_slides(self): + + session1 = SessionFactory(meeting__type_id='ietf') + session2 = SessionFactory(meeting=session1.meeting,group=session1.group) + url = urlreverse('ietf.meeting.views.upload_session_slides',kwargs={'num':session1.meeting.number,'session_id':session1.id}) + login_testing_unauthorized(self,"secretary",url) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue('Upload' in unicode(q("title"))) + self.assertFalse(session1.sessionpresentation_set.filter(document__type_id='slides')) + test_file = StringIO('this is not really a slide') + test_file.name = 'not_really.txt' + r = self.client.post(url,dict(file=test_file,title='a test slide file',apply_to_all=True)) + self.assertEqual(r.status_code, 302) + self.assertEqual(session1.sessionpresentation_set.count(),1) + self.assertEqual(session2.sessionpresentation_set.count(),1) + sp = session2.sessionpresentation_set.first() + self.assertEqual(sp.document.name, 'slides-%s-%s-a-test-slide-file' % (session1.meeting.number,session1.group.acronym ) ) + self.assertEqual(sp.order,1) + + url = urlreverse('ietf.meeting.views.upload_session_slides',kwargs={'num':session2.meeting.number,'session_id':session2.id}) + test_file = StringIO('some other thing still not slidelike') + test_file.name = 'also_not_really.txt' + r = self.client.post(url,dict(file=test_file,title='a different slide file',apply_to_all=False)) + self.assertEqual(r.status_code, 302) + self.assertEqual(session1.sessionpresentation_set.count(),1) + self.assertEqual(session2.sessionpresentation_set.count(),2) + sp = session2.sessionpresentation_set.get(document__name__endswith='-a-different-slide-file') + self.assertEqual(sp.order,2) + self.assertEqual(sp.rev,u'00') + self.assertEqual(sp.document.rev,u'00') + + url = urlreverse('ietf.meeting.views.upload_session_slides',kwargs={'num':session2.meeting.number,'session_id':session2.id,'name':session2.sessionpresentation_set.get(order=2).document.name}) + r = self.client.get(url) + self.assertTrue(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue('Revise' in unicode(q("title"))) + test_file = StringIO('new content for the second slide deck') + test_file.name = 'doesnotmatter.txt' + r = self.client.post(url,dict(file=test_file,title='rename the presentation',apply_to_all=False)) + self.assertEqual(r.status_code, 302) + self.assertEqual(session1.sessionpresentation_set.count(),1) + self.assertEqual(session2.sessionpresentation_set.count(),2) + sp = session2.sessionpresentation_set.get(order=2) + self.assertEqual(sp.rev,u'01') + self.assertEqual(sp.document.rev,u'01') + diff --git a/ietf/meeting/urls.py b/ietf/meeting/urls.py index 045821e50..659f124dd 100644 --- a/ietf/meeting/urls.py +++ b/ietf/meeting/urls.py @@ -11,6 +11,9 @@ safe_for_all_meeting_types = [ url(r'^session/(?P[-a-z0-9]+)/?$', views.session_details), url(r'^session/(?P\d+)/drafts$', views.add_session_drafts), url(r'^session/(?P\d+)/bluesheets$', views.upload_session_bluesheets), + url(r'^session/(?P\d+)/minutes$', views.upload_session_minutes), + url(r'^session/(?P\d+)/agenda$', views.upload_session_agenda), + url(r'^session/(?P\d+)/slides(?:/%(name)s)?$' % settings.URL_REGEXPS, views.upload_session_slides), ] diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 42c7f0884..6ef9386e8 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -27,7 +27,9 @@ from django.forms.models import modelform_factory, inlineformset_factory from django.forms import ModelForm from django.template.loader import render_to_string from django.utils.functional import curry +from django.utils.text import slugify from django.views.decorators.csrf import ensure_csrf_cookie +from django.template.defaultfilters import filesizeformat from ietf.doc.fields import SearchableDocumentsField from ietf.doc.models import Document, State, DocEvent, NewRevisionDocEvent @@ -51,7 +53,6 @@ from ietf.meeting.helpers import send_interim_cancellation_notice from ietf.meeting.helpers import send_interim_approval_request from ietf.meeting.helpers import send_interim_announcement_request from ietf.meeting.utils import finalize -from ietf.person.models import Person from ietf.secr.proceedings.utils import handle_upload_file from ietf.utils.mail import send_mail_message from ietf.utils.pipe import pipe @@ -1006,10 +1007,9 @@ def session_details(request, num, acronym ): if not sessions: raise Http404 - type_counter = Counter() - for session in sessions: + session.type_counter = Counter() ss = session.timeslotassignments.filter(schedule=meeting.agenda).order_by('timeslot__time') if ss: session.time = ', '.join(x.timeslot.time.strftime("%A %b-%d-%Y %H%M") for x in ss) @@ -1028,10 +1028,7 @@ def session_details(request, num, acronym ): # TODO FIXME Deleted materials shouldn't be in the sessionpresentation_set for qs in [session.filtered_artifacts,session.filtered_slides,session.filtered_drafts]: qs = [p for p in qs if p.document.get_state_slug(p.document.type_id)!='deleted'] - type_counter.update([p.document.type.slug for p in qs]) - - #session.filtered_sessionpresentation_set = [p for p in session.sessionpresentation_set.all() if p.document.get_state_slug(p.document.type_id)!='deleted'] - #type_counter.update([p.document.type.slug for p in session.filtered_sessionpresentation_set]) + session.type_counter.update([p.document.type.slug for p in qs]) can_manage = can_manage_materials(request.user, Group.objects.get(acronym=acronym)) @@ -1040,7 +1037,6 @@ def session_details(request, num, acronym ): 'meeting' :meeting , 'acronym' :acronym, 'can_manage_materials' : can_manage, - 'type_counter': type_counter, }) class SessionDraftsForm(forms.Form): @@ -1114,6 +1110,8 @@ def upload_session_bluesheets(request, session_id, num): if bluesheet_sp: doc = bluesheet_sp.document doc.rev = '%02d' % (int(doc.rev)+1) + bluesheet_sp.rev = doc.rev + bluesheet_sp.save() else: sess_time = session.official_timeslotassignment().timeslot.time if session.meeting.type_id=='ietf': @@ -1138,7 +1136,7 @@ def upload_session_bluesheets(request, session_id, num): session.sessionpresentation_set.create(document=doc,rev='00') filename = '%s-%s%s'% ( doc.name, doc.rev, ext) doc.external_url = filename - e = NewRevisionDocEvent.objects.create(doc=doc,time=doc.time,by=Person.objects.get(name='(System)'),type='new_revision',desc='New revision available: %s'%doc.rev,rev=doc.rev) + e = NewRevisionDocEvent.objects.create(doc=doc,time=doc.time,by=request.user.person,type='new_revision',desc='New revision available: %s'%doc.rev,rev=doc.rev) doc.save_with_history([e]) handle_upload_file(file, filename, session.meeting, 'bluesheets') return redirect('ietf.meeting.views.session_details',num=num,acronym=session.group.acronym) @@ -1152,6 +1150,301 @@ def upload_session_bluesheets(request, session_id, num): 'form': form, }) +VALID_MINUTES_EXTENSIONS = ('.txt','.html','.htm','.pdf') +# FIXME: This form validation code (based on the secretariat upload code) only looks at filename extensions +# It should look at the contents of the files instead. +class UploadMinutesForm(forms.Form): + file = forms.FileField(label='Minutes file to upload. Note that you can only upload minutes in txt, html, or pdf formats.') + apply_to_all = forms.BooleanField(label='Apply to all group sessions at this meeting',initial=True,required=False) + + def __init__(self, num_sessions, *args, **kwargs): + super(UploadMinutesForm, self).__init__(*args, **kwargs) + if num_sessions<2: + self.fields.pop('apply_to_all') + + def clean_file(self): + file = self.cleaned_data['file'] + if file._size > settings.SECR_MAX_UPLOAD_SIZE: + raise forms.ValidationError('Please keep filesize under %s. Requested upload size is %s' % (filesizeformat(settings.SECR_MAX_UPLOAD_SIZE),filesizeformat(file._size))) + if os.path.splitext(file.name)[1].lower() not in VALID_MINUTES_EXTENSIONS: + raise forms.ValidationError('Only these file types supported for minutes: %s' % ','.join(VALID_MINUTES_EXTENSIONS)) + return file + +def upload_session_minutes(request, session_id, num): + # num is redundant, but we're dragging it along an artifact of where we are in the current URL structure + session = get_object_or_404(Session,pk=session_id) + + if not session.can_manage_materials(request.user): + return HttpResponseForbidden("You don't have permission to upload minutes for this session.") + if session.is_material_submission_cutoff() and not has_role(request.user, "Secretariat"): + return HttpResponseForbidden("The materials cutoff for this session has passed. Contact the secretariat for further action.") + + session_number = None + sessions = get_sessions(session.meeting.number,session.group.acronym) + num_sessions = len(sessions) + if len(sessions) > 1: + session_number = 1 + sessions.index(session) + + minutes_sp = session.sessionpresentation_set.filter(document__type='minutes').first() + + if request.method == 'POST': + form = UploadMinutesForm(num_sessions,request.POST,request.FILES) + if form.is_valid(): + file = request.FILES['file'] + _, ext = os.path.splitext(file.name) + apply_to_all = True + if num_sessions > 1: + apply_to_all = form.cleaned_data['apply_to_all'] + if minutes_sp: + doc = minutes_sp.document + doc.rev = '%02d' % (int(doc.rev)+1) + minutes_sp.rev = doc.rev + minutes_sp.save() + else: + sess_time = session.official_timeslotassignment().timeslot.time + if session.meeting.type_id=='ietf': + name = 'minutes-%s-%s' % (session.meeting.number, + session.group.acronym) + title = 'Minutes IETF%s: %s' % (session.meeting.number, + session.group.acronym) + if not apply_to_all: + name += '-%s' % (sess_time.strftime("%Y%m%d%H%M"),) + title += ': %s' % (sess_time.strftime("%a %H:%M"),) + else: + name = 'minutes-%s-%s' % (session.meeting.number, sess_time.strftime("%Y%m%d%H%M")) + title = 'Minutes %s: %s' % (session.meeting.number, sess_time.strftime("%a %H:%M")) + doc = Document.objects.create( + name = name, + type_id = 'minutes', + title = title, + group = session.group, + rev = '00', + ) + doc.states.add(State.objects.get(type_id='minutes',slug='active')) + doc.docalias_set.create(name=doc.name) + session.sessionpresentation_set.create(document=doc,rev='00') + if apply_to_all: + for other_session in sessions: + if other_session != session: + other_session.sessionpresentation_set.filter(document__type='minutes').delete() + other_session.sessionpresentation_set.create(document=doc,rev=doc.rev) + filename = '%s-%s%s'% ( doc.name, doc.rev, ext) + doc.external_url = filename + e = NewRevisionDocEvent.objects.create(doc=doc,time=doc.time,by=request.user.person,type='new_revision',desc='New revision available: %s'%doc.rev,rev=doc.rev) + doc.save_with_history([e]) + # The way this function builds the filename it will never trigger the file delete in handle_file_upload. + handle_upload_file(file, filename, session.meeting, 'minutes') + return redirect('ietf.meeting.views.session_details',num=num,acronym=session.group.acronym) + else: + form = UploadMinutesForm(num_sessions) + + return render(request, "meeting/upload_session_minutes.html", + {'session': session, + 'session_number': session_number, + 'minutes_sp' : minutes_sp, + 'form': form, + }) + +VALID_AGENDA_EXTENSIONS = ('.txt','.html','.htm',) +# FIXME: This form validation code (based on the secretariat upload code) only looks at filename extensions +# It should look at the contents of the files instead. +class UploadAgendaForm(forms.Form): + file = forms.FileField(label='Agenda file to upload. Note that you can only upload agendas in txt or html formats.') + apply_to_all = forms.BooleanField(label='Apply to all group sessions at this meeting',initial=True,required=False) + + def __init__(self, num_sessions, *args, **kwargs): + super(UploadAgendaForm, self).__init__(*args, **kwargs) + if num_sessions<2: + self.fields.pop('apply_to_all') + + def clean_file(self): + file = self.cleaned_data['file'] + if file._size > settings.SECR_MAX_UPLOAD_SIZE: + raise forms.ValidationError('Please keep filesize under %s. Requested upload size is %s' % (filesizeformat(settings.SECR_MAX_UPLOAD_SIZE),filesizeformat(file._size))) + if os.path.splitext(file.name)[1].lower() not in VALID_AGENDA_EXTENSIONS: + raise forms.ValidationError('Only these file types supported for agendas: %s' % ','.join(VALID_AGENDA_EXTENSIONS)) + return file + +def upload_session_agenda(request, session_id, num): + # num is redundant, but we're dragging it along an artifact of where we are in the current URL structure + session = get_object_or_404(Session,pk=session_id) + + if not session.can_manage_materials(request.user): + return HttpResponseForbidden("You don't have permission to upload an agenda for this session.") + if session.is_material_submission_cutoff() and not has_role(request.user, "Secretariat"): + return HttpResponseForbidden("The materials cutoff for this session has passed. Contact the secretariat for further action.") + + session_number = None + sessions = get_sessions(session.meeting.number,session.group.acronym) + num_sessions = len(sessions) + if len(sessions) > 1: + session_number = 1 + sessions.index(session) + + agenda_sp = session.sessionpresentation_set.filter(document__type='agenda').first() + + if request.method == 'POST': + form = UploadAgendaForm(num_sessions,request.POST,request.FILES) + if form.is_valid(): + file = request.FILES['file'] + _, ext = os.path.splitext(file.name) + apply_to_all = True + if num_sessions > 1: + apply_to_all = form.cleaned_data['apply_to_all'] + if agenda_sp: + doc = agenda_sp.document + doc.rev = '%02d' % (int(doc.rev)+1) + agenda_sp.rev = doc.rev + agenda_sp.save() + else: + sess_time = session.official_timeslotassignment().timeslot.time + if session.meeting.type_id=='ietf': + name = 'agenda-%s-%s' % (session.meeting.number, + session.group.acronym) + title = 'Agenda IETF%s: %s' % (session.meeting.number, + session.group.acronym) + if not apply_to_all: + name += '-%s' % (sess_time.strftime("%Y%m%d%H%M"),) + title += ': %s' % (sess_time.strftime("%a %H:%M"),) + else: + name = 'agenda-%s-%s' % (session.meeting.number, sess_time.strftime("%Y%m%d%H%M")) + title = 'Agenda %s: %s' % (session.meeting.number, sess_time.strftime("%a %H:%M")) + doc = Document.objects.create( + name = name, + type_id = 'agenda', + title = title, + group = session.group, + rev = '00', + ) + doc.states.add(State.objects.get(type_id='agenda',slug='active')) + doc.docalias_set.create(name=doc.name) + session.sessionpresentation_set.create(document=doc,rev='00') + if apply_to_all: + for other_session in sessions: + if other_session != session: + other_session.sessionpresentation_set.filter(document__type='agenda').delete() + other_session.sessionpresentation_set.create(document=doc,rev=doc.rev) + filename = '%s-%s%s'% ( doc.name, doc.rev, ext) + doc.external_url = filename + e = NewRevisionDocEvent.objects.create(doc=doc,time=doc.time,by=request.user.person,type='new_revision',desc='New revision available: %s'%doc.rev,rev=doc.rev) + doc.save_with_history([e]) + # The way this function builds the filename it will never trigger the file delete in handle_file_upload. + handle_upload_file(file, filename, session.meeting, 'agenda') + return redirect('ietf.meeting.views.session_details',num=num,acronym=session.group.acronym) + else: + form = UploadAgendaForm(num_sessions) + + return render(request, "meeting/upload_session_agenda.html", + {'session': session, + 'session_number': session_number, + 'agenda_sp' : agenda_sp, + 'form': form, + }) + +VALID_SLIDE_EXTENSIONS = ('.doc','.docx','.pdf','.ppt','.pptx','.txt') # Note the removal of .zip +# FIXME: This form validation code (based on the secretariat upload code) only looks at filename extensions +# It should look at the contents of the files instead. +class UploadSlidesForm(forms.Form): + title = forms.CharField(max_length=255) + file = forms.FileField(label='Slides file to upload.') + apply_to_all = forms.BooleanField(label='Apply to all group sessions at this meeting',initial=True,required=False) + + def __init__(self, num_sessions, *args, **kwargs): + super(UploadSlidesForm, self).__init__(*args, **kwargs) + if num_sessions<2: + self.fields.pop('apply_to_all') + + def clean_file(self): + file = self.cleaned_data['file'] + if file._size > settings.SECR_MAX_UPLOAD_SIZE: + raise forms.ValidationError('Please keep filesize under %s. Requested upload size is %s' % (filesizeformat(settings.SECR_MAX_UPLOAD_SIZE),filesizeformat(file._size))) + if os.path.splitext(file.name)[1].lower() not in VALID_SLIDE_EXTENSIONS: + raise forms.ValidationError('Only these file types supported for slides: %s' % ','.join(VALID_SLIDE_EXTENSIONS)) + return file + +def upload_session_slides(request, session_id, num, name): + # num is redundant, but we're dragging it along an artifact of where we are in the current URL structure + session = get_object_or_404(Session,pk=session_id) + if not session.can_manage_materials(request.user): + return HttpResponseForbidden("You don't have permission to upload slides for this session.") + if session.is_material_submission_cutoff() and not has_role(request.user, "Secretariat"): + return HttpResponseForbidden("The materials cutoff for this session has passed. Contact the secretariat for further action.") + + session_number = None + sessions = get_sessions(session.meeting.number,session.group.acronym) + num_sessions = len(sessions) + if len(sessions) > 1: + session_number = 1 + sessions.index(session) + + slides = None + slides_sp = None + if name: + slides = Document.objects.filter(name=name).first() + if not (slides and slides.type_id=='slides'): + raise Http404 + slides_sp = session.sessionpresentation_set.filter(document=slides).first() + + if request.method == 'POST': + form = UploadSlidesForm(num_sessions,request.POST,request.FILES) + if form.is_valid(): + file = request.FILES['file'] + _, ext = os.path.splitext(file.name) + apply_to_all = True + if num_sessions > 1: + apply_to_all = form.cleaned_data['apply_to_all'] + if slides_sp: + doc = slides_sp.document + doc.rev = '%02d' % (int(doc.rev)+1) + doc.title = form.cleaned_data['title'] + slides_sp.rev = doc.rev + slides_sp.save() + else: + title = form.cleaned_data['title'] + sess_time = session.official_timeslotassignment().timeslot.time + if session.meeting.type_id=='ietf': + name = 'slides-%s-%s' % (session.meeting.number, + session.group.acronym) + if not apply_to_all: + name += '-%s' % (sess_time.strftime("%Y%m%d%H%M"),) + else: + name = 'slides-%s-%s' % (session.meeting.number, sess_time.strftime("%Y%m%d%H%M")) + name = name + '-' + slugify(title) + doc = Document.objects.create( + name = name, + type_id = 'slides', + title = title, + group = session.group, + rev = '00', + ) + doc.states.add(State.objects.get(type_id='slides',slug='active')) + doc.states.add(State.objects.get(type_id='reuse_policy',slug='single')) + doc.docalias_set.create(name=doc.name) + max_order = session.sessionpresentation_set.filter(document__type='slides').aggregate(Max('order'))['order__max'] or 0 + session.sessionpresentation_set.create(document=doc,rev=doc.rev,order=max_order+1) + if apply_to_all: + for other_session in sessions: + if other_session != session: + max_order = other_session.sessionpresentation_set.filter(document__type='slides').aggregate(Max('order'))['order__max'] or 0 + other_session.sessionpresentation_set.create(document=doc,rev=doc.rev,order=max_order+1) + filename = '%s-%s%s'% ( doc.name, doc.rev, ext) + doc.external_url = filename + e = NewRevisionDocEvent.objects.create(doc=doc,time=doc.time,by=request.user.person,type='new_revision',desc='New revision available: %s'%doc.rev,rev=doc.rev) + doc.save_with_history([e]) + # The way this function builds the filename it will never trigger the file delete in handle_file_upload. + handle_upload_file(file, filename, session.meeting, 'slides') + return redirect('ietf.meeting.views.session_details',num=num,acronym=session.group.acronym) + else: + initial = {} + if slides: + initial = {'title':slides.title} + form = UploadSlidesForm(num_sessions, initial=initial) + + return render(request, "meeting/upload_session_slides.html", + {'session': session, + 'session_number': session_number, + 'slides_sp' : slides_sp, + 'form': form, + }) + @role_required('Secretariat') def make_schedule_official(request, num, owner, name): diff --git a/ietf/templates/meeting/session_details.html b/ietf/templates/meeting/session_details.html index a6f4f3abf..41319f287 100644 --- a/ietf/templates/meeting/session_details.html +++ b/ietf/templates/meeting/session_details.html @@ -12,95 +12,115 @@

{% if sessions|length > 1 %}Session {{ forloop.counter }} : {% endif %}{{ session.time }}{% if session.name %} : {{ session.name }}{% endif %}

{% if session.agenda_note %}

{{session.agenda_note}}

{% endif %} - {% if can_manage_materials %} - {% if session.status.slug == 'sched' or session.status.slug == 'schedw' %} -
- {% if meeting.type.slug == 'interim' and user|has_role:"Secretariat" %} - Meeting Details - {% endif %} - - Upload/Edit materials - - - Link additional drafts to session - - {% if user|has_role:"Secretariat" %} - Upload Bluesheets - {% endif %} - {% if not type_counter.agenda %} - This session does not yet have an agenda - {% endif %} -
- {% endif %} - {% endif %} + {% if can_manage_materials %} + {% if session.status.slug == 'sched' or session.status.slug == 'schedw' %} +
+ {% if meeting.type.slug == 'interim' and user|has_role:"Secretariat" %} + Meeting Details + {% endif %} +
+ {% if not session.type_counter.agenda %} + This session does not yet have an agenda + {% endif %} + {% endif %} + {% endif %} - {% if session.filtered_artifacts %} -
-
Artifacts
-
- - {% for pres in session.filtered_artifacts %} - - - - {% endfor %} -
- {% if pres.rev %} - {% url 'doc_view' name=pres.document.name rev=pres.rev as url %} - {% else %} - {% url 'doc_view' name=pres.document.name as url %} - {% endif %} - {{pres.document.title}} ({{ pres.document.name }}{% if pres.rev %}-{{ pres.rev }}{% endif %}) - -
-
-
+
+
Artifacts
+
+ + {% for pres in session.filtered_artifacts %} + + {% if pres.rev %} + {% url 'doc_view' name=pres.document.name rev=pres.rev as url %} + {% else %} + {% url 'doc_view' name=pres.document.name as url %} + {% endif %} + + {% if user|has_role:"Secretariat" or can_manage_materials %} + + {% endif %} + + {% endfor %} +
+ {{pres.document.title}} ({{ pres.document.name }}{% if pres.rev %}-{{ pres.rev }}{% endif %}) + + {% if pres.document.type.slug == 'minutes' %} + {% url 'ietf.meeting.views.upload_session_minutes' session_id=session.pk num=session.meeting.number as upload_url %} + {% elif pres.document.type.slug == 'agenda' %} + {% url 'ietf.meeting.views.upload_session_agenda' session_id=session.pk num=session.meeting.number as upload_url %} + {% else %} + {% url 'ietf.meeting.views.upload_session_bluesheets' session_id=session.pk num=session.meeting.number as upload_url %} + {% endif %} + {% if pres.document.type.slug != 'bluesheets' or user|has_role:"Secretariat" %} + Upload Revision + {% endif %} +
+ {% if can_manage_materials %} + {% if not session.type_counter.agenda %} + Upload Agenda + {% endif %} + {% if not session.type_counter.minutes %} + Upload Minutes + {% endif %} {% endif %} - {% if session.filtered_slides %} -
-
Slides
-
- - {% for pres in session.filtered_slides %} - - - - {% endfor %} -
- {% if pres.rev %} - {% url 'doc_view' name=pres.document.name rev=pres.rev as url %} - {% else %} - {% url 'doc_view' name=pres.document.name as url %} - {% endif %} - {{pres.document.title}} ({{ pres.document.name }}{% if pres.rev %}-{{ pres.rev }}{% endif %}) - -
-
-
+ {% if user|has_role:"Secretariat" and not session.type_counter.bluesheets %} + Upload Bluesheets {% endif %} - {% if session.filtered_drafts %} -
-
Drafts
-
- - {% for pres in session.filtered_drafts %} - - - - {% endfor %} -
- {% if pres.rev %} - {% url 'doc_view' name=pres.document.name rev=pres.rev as url %} - {% else %} - {% url 'doc_view' name=pres.document.name as url %} - {% endif %} - {{pres.document.title}} ({{ pres.document.name }}{% if pres.rev %}-{{ pres.rev }}{% endif %}) - -
-
-
+
+
+
+
Slides
+
+ + {% for pres in session.filtered_slides %} + + {% if pres.rev %} + {% url 'doc_view' name=pres.document.name rev=pres.rev as url %} + {% else %} + {% url 'doc_view' name=pres.document.name as url %} + {% endif %} + + {% if can_manage_materials %} + + {% endif %} + + {% endfor %} +
+ {{pres.document.title}} ({{ pres.document.name }}{% if pres.rev %}-{{ pres.rev }}{% endif %}) + + Upload Revision +
+ {% if can_manage_materials %} + Upload New Slides {% endif %} - +
+
+
+
Drafts +
+
+ + {% for pres in session.filtered_drafts %} + + + + {% endfor %} +
+ {% if pres.rev %} + {% url 'doc_view' name=pres.document.name rev=pres.rev as url %} + {% else %} + {% url 'doc_view' name=pres.document.name as url %} + {% endif %} + {{pres.document.title}} ({{ pres.document.name }}{% if pres.rev %}-{{ pres.rev }}{% endif %}) +
+ {% if can_manage_materials %} + + Link additional drafts to session + + {% endif %} +
+
{% endfor %} {% endblock %} diff --git a/ietf/templates/meeting/upload_session_agenda.html b/ietf/templates/meeting/upload_session_agenda.html new file mode 100644 index 000000000..1ddec4ca7 --- /dev/null +++ b/ietf/templates/meeting/upload_session_agenda.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2015, All Rights Reserved #} +{% load origin staticfiles bootstrap3 %} + +{% block title %}{% if agenda_sp %}Revise{% else %}Upload{% endif %} Agenda for {{ session.meeting }} : {{ session.group.acronym }}{% endblock %} + +{% block content %} + {% origin %} + +

{% if agenda_sp %}Revise{% else %}Upload{% endif %} Agenda for {{ session.meeting }} : {{ session.group.acronym }}{% if session.name %} : {{session.name}}{% endif %}

+{% if session_number %}

Session {{session_number}} : {{session.official_timeslotassignment.timeslot.time|date:"D M-d-Y Hi"}}

{% endif %} + +
+ {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + {% endbuttons %} +
+ + +{% endblock %} diff --git a/ietf/templates/meeting/upload_session_bluesheets.html b/ietf/templates/meeting/upload_session_bluesheets.html index 53a59f618..8698b267f 100644 --- a/ietf/templates/meeting/upload_session_bluesheets.html +++ b/ietf/templates/meeting/upload_session_bluesheets.html @@ -2,22 +2,14 @@ {# Copyright The IETF Trust 2015, All Rights Reserved #} {% load origin staticfiles bootstrap3 %} -{% block title %}Upload Bluesheets for {{ session.meeting }} : {{ session.group.acronym }}{% endblock %} +{% block title %}{% if bluesheet_sp %}Revise{% else %}Upload{% endif %} Bluesheets for {{ session.meeting }} : {{ session.group.acronym }}{% endblock %} {% block content %} {% origin %} -

Upload Bluesheets for {{ session.meeting }} : {{ session.group.acronym }}{% if session.name %} : {{session.name}}{% endif %}

+

{% if bluesheet_sp %}Revise{% else %}Upload{% endif %} Bluesheets for {{ session.meeting }} : {{ session.group.acronym }}{% if session.name %} : {{session.name}}{% endif %}

{% if session_number %}

Session {{session_number}} : {{session.official_timeslotassignment.timeslot.time|date:"D M-d-Y Hi"}}

{% endif %} - {% if bluesheet_sp %} -
- Bluesheets have alrady been uploaded for this session. - See {{bluesheet_sp.document.name}}-{{bluesheet_sp.document.rev}}. - Continue with this upload to provide an updated version of that document. -
- {% endif %} -
{% csrf_token %} {% bootstrap_form form %} diff --git a/ietf/templates/meeting/upload_session_minutes.html b/ietf/templates/meeting/upload_session_minutes.html new file mode 100644 index 000000000..ba96cf761 --- /dev/null +++ b/ietf/templates/meeting/upload_session_minutes.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2015, All Rights Reserved #} +{% load origin staticfiles bootstrap3 %} + +{% block title %}{% if minutes_sp %}Revise{% else %}Upload{% endif %} Minutes for {{ session.meeting }} : {{ session.group.acronym }}{% endblock %} + +{% block content %} + {% origin %} + +

{% if minutes_sp %}Revise{% else %}Upload{% endif %} Minutes for {{ session.meeting }} : {{ session.group.acronym }}{% if session.name %} : {{session.name}}{% endif %}

+{% if session_number %}

Session {{session_number}} : {{session.official_timeslotassignment.timeslot.time|date:"D M-d-Y Hi"}}

{% endif %} + + + {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + {% endbuttons %} +
+ + +{% endblock %} diff --git a/ietf/templates/meeting/upload_session_slides.html b/ietf/templates/meeting/upload_session_slides.html new file mode 100644 index 000000000..92dd9af51 --- /dev/null +++ b/ietf/templates/meeting/upload_session_slides.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2015, All Rights Reserved #} +{% load origin staticfiles bootstrap3 %} + +{% block title %}{% if slides_sp %}Revise{% else %}Upload New{% endif %} Slides for {{ session.meeting }} : {{ session.group.acronym }}{% endblock %} + +{% block content %} + {% origin %} + +

{% if slides_sp %}Revise{% else %}Upload New{% endif %} Slides for {{ session.meeting }} : {{ session.group.acronym }}{% if session.name %} : {{session.name}}{% endif %}

+{% if session_number %}

Session {{session_number}} : {{session.official_timeslotassignment.timeslot.time|date:"D M-d-Y Hi"}}

{% endif %} +{% if slides_sp %}

{{slides_sp.document.name}}

{% endif %} + +
+ {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + {% endbuttons %} +
+ + +{% endblock %}