diff --git a/ietf/meeting/migrations/0031_auto_20200803_1153.py b/ietf/meeting/migrations/0031_auto_20200803_1153.py new file mode 100644 index 000000000..8a5ac54aa --- /dev/null +++ b/ietf/meeting/migrations/0031_auto_20200803_1153.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.14 on 2020-08-03 11:53 + +from django.db import migrations +import django.db.models.deletion +import ietf.utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('name', '0018_slidesubmissionstatusname'), + ('doc', '0035_populate_docextresources'), + ('meeting', '0030_allow_empty_joint_with_sessions'), + ] + + operations = [ + migrations.AddField( + model_name='slidesubmission', + name='doc', + field=ietf.utils.models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='doc.Document'), + ), + ] diff --git a/ietf/meeting/migrations/0032_auto_20200824_1642.py b/ietf/meeting/migrations/0032_auto_20200824_1642.py new file mode 100644 index 000000000..31155da6a --- /dev/null +++ b/ietf/meeting/migrations/0032_auto_20200824_1642.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.14 on 2020-08-03 11:53 + +from django.db import migrations +import django.db.models.deletion +import ietf.utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('meeting', '0031_auto_20200803_1153'), + ] + + operations = [ + migrations.AddField( + model_name='slidesubmission', + name='status', + field=ietf.utils.models.ForeignKey(null=True, default='pending', on_delete=django.db.models.deletion.SET_NULL, to='name.SlideSubmissionStatusName'), + ), + ] diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index 5df942106..c73374d34 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -29,7 +29,7 @@ from ietf.dbtemplate.models import DBTemplate from ietf.doc.models import Document from ietf.group.models import Group from ietf.group.utils import can_manage_materials -from ietf.name.models import MeetingTypeName, TimeSlotTypeName, SessionStatusName, ConstraintName, RoomResourceName, ImportantDateName, TimerangeName +from ietf.name.models import MeetingTypeName, TimeSlotTypeName, SessionStatusName, ConstraintName, RoomResourceName, ImportantDateName, TimerangeName, SlideSubmissionStatusName from ietf.person.models import Person from ietf.utils.decorators import memoize from ietf.utils.storage import NoLocationMigrationFileSystemStorage @@ -1277,6 +1277,8 @@ class SlideSubmission(models.Model): filename = models.CharField(max_length=255) apply_to_all = models.BooleanField(default=False) submitter = ForeignKey(Person) + status = ForeignKey(SlideSubmissionStatusName, null=True, default='pending', on_delete=models.SET_NULL) + doc = ForeignKey(Document, null=True, on_delete=models.SET_NULL) def staged_filepath(self): return os.path.join(settings.SLIDE_STAGING_PATH , self.filename) diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index b3dcf9094..30889de0b 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -2788,6 +2788,7 @@ class MaterialsTests(TestCase): def test_disapprove_proposed_slides(self): submission = SlideSubmissionFactory() submission.session.meeting.importantdate_set.create(name_id='revsub',date=datetime.date.today()+datetime.timedelta(days=20)) + self.assertEqual(SlideSubmission.objects.filter(status__slug = 'pending').count(), 1) chair = RoleFactory(group=submission.session.group,name_id='chair').person url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number}) login_testing_unauthorized(self, chair.user.username, url) @@ -2795,7 +2796,11 @@ class MaterialsTests(TestCase): self.assertEqual(r.status_code,200) r = self.client.post(url,dict(title='some title',disapprove="disapprove")) self.assertEqual(r.status_code,302) - self.assertEqual(SlideSubmission.objects.count(), 0) + self.assertEqual(SlideSubmission.objects.filter(status__slug = 'rejected').count(), 1) + self.assertEqual(SlideSubmission.objects.filter(status__slug = 'pending').count(), 0) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + self.assertContains(r, "These slides have already been rejected") def test_approve_proposed_slides(self): submission = SlideSubmissionFactory() @@ -2804,13 +2809,22 @@ class MaterialsTests(TestCase): chair = RoleFactory(group=submission.session.group,name_id='chair').person url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number}) login_testing_unauthorized(self, chair.user.username, url) + self.assertEqual(submission.status_id, 'pending') + self.assertIsNone(submission.doc) r = self.client.get(url) self.assertEqual(r.status_code,200) r = self.client.post(url,dict(title='different title',approve='approve')) self.assertEqual(r.status_code,302) - self.assertEqual(SlideSubmission.objects.count(), 0) + self.assertEqual(SlideSubmission.objects.filter(status__slug = 'pending').count(), 0) + self.assertEqual(SlideSubmission.objects.filter(status__slug = 'approved').count(), 1) + submission = SlideSubmission.objects.get(id = submission.id) + self.assertEqual(submission.status_id, 'approved') + self.assertIsNotNone(submission.doc) self.assertEqual(session.sessionpresentation_set.count(),1) self.assertEqual(session.sessionpresentation_set.first().document.title,'different title') + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + self.assertContains(r, "These slides have already been approved") def test_approve_proposed_slides_multisession_apply_one(self): submission = SlideSubmissionFactory(session__meeting__type_id='ietf') @@ -2881,7 +2895,7 @@ class MaterialsTests(TestCase): self.assertEqual(r.status_code, 302) self.client.logout() - (first_submission, second_submission) = SlideSubmission.objects.filter(session=session).order_by('id') + (first_submission, second_submission) = SlideSubmission.objects.filter(session=session, status__slug = 'pending').order_by('id') approve_url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':second_submission.pk,'num':second_submission.session.meeting.number}) login_testing_unauthorized(self, chair.user.username, approve_url) @@ -2893,7 +2907,8 @@ class MaterialsTests(TestCase): self.assertEqual(r.status_code,302) self.client.logout() - self.assertEqual(SlideSubmission.objects.count(),0) + self.assertEqual(SlideSubmission.objects.filter(status__slug = 'pending').count(),0) + self.assertEqual(SlideSubmission.objects.filter(status__slug = 'rejected').count(),1) self.assertEqual(session.sessionpresentation_set.first().document.rev,'01') path = os.path.join(submission.session.meeting.get_materials_path(),'slides') filename = os.path.join(path,session.sessionpresentation_set.first().document.name+'-01.txt') diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 4008d9cc9..b8a847207 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -25,7 +25,7 @@ from wsgiref.handlers import format_date_time from django import forms from django.shortcuts import render, redirect, get_object_or_404 -from django.http import HttpResponse, HttpResponseRedirect, Http404 +from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound, Http404 from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required @@ -79,6 +79,7 @@ from ietf.meeting.utils import current_session_status from ietf.meeting.utils import data_for_meetings_overview from ietf.meeting.utils import preprocess_constraints_for_meeting_schedule_editor from ietf.message.utils import infer_message +from ietf.name.models import SlideSubmissionStatusName from ietf.secr.proceedings.utils import handle_upload_file from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files, create_recording) @@ -1541,9 +1542,9 @@ def session_details(request, num, acronym): pending_suggestions = None if request.user.is_authenticated: if can_manage: - pending_suggestions = session.slidesubmission_set.all() + pending_suggestions = session.slidesubmission_set.filter(status__slug='pending') else: - pending_suggestions = session.slidesubmission_set.filter(submitter=request.user.person) + pending_suggestions = session.slidesubmission_set.filter(status__slug='pending', submitter=request.user.person) return render(request, "meeting/session_details.html", { 'scheduled_sessions':scheduled_sessions , @@ -3146,13 +3147,16 @@ def approve_proposed_slides(request, slidesubmission_id, num): name, _ = os.path.splitext(submission.filename) name = name[:name.rfind('-ss')] existing_doc = Document.objects.filter(name=name).first() - if request.method == 'POST': + if request.method == 'POST' and submission.status.slug == 'pending': form = ApproveSlidesForm(show_apply_to_all_checkbox, request.POST) if form.is_valid(): apply_to_all = submission.session.type_id == 'regular' if show_apply_to_all_checkbox: apply_to_all = form.cleaned_data['apply_to_all'] if request.POST.get('approve'): + # Ensure that we have a file to approve. The system gets cranky otherwise. + if submission.filename is None or submission.filename == '' or not os.path.isfile(submission.staged_filepath()): + return HttpResponseNotFound("The slides you attempted to approve could not be found. Please disapprove and delete them instead.") title = form.cleaned_data['title'] if existing_doc: doc = Document.objects.get(name=name) @@ -3192,15 +3196,29 @@ def approve_proposed_slides(request, slidesubmission_id, num): os.rename(submission.staged_filepath(), os.path.join(path, target_filename)) post_process(doc) acronym = submission.session.group.acronym - submission.delete() + submission.status = SlideSubmissionStatusName.objects.get(slug='approved') + submission.doc = doc + submission.save() return redirect('ietf.meeting.views.session_details',num=num,acronym=acronym) elif request.POST.get('disapprove'): - os.unlink(submission.staged_filepath()) + # Errors in processing a submit request sometimes result + # in a SlideSubmission object without a file. Handle + # this case and keep processing the 'disapprove' even if + # the filename doesn't exist. + try: + if submission.filename != None and submission.filename != '': + os.unlink(submission.staged_filepath()) + except (FileNotFoundError, IsADirectoryError): + pass acronym = submission.session.group.acronym - submission.delete() + submission.status = SlideSubmissionStatusName.objects.get(slug='rejected') + submission.save() return redirect('ietf.meeting.views.session_details',num=num,acronym=acronym) else: pass + elif not submission.status.slug == 'pending': + return render(request, "meeting/previously_approved_slides.html", + {'submission': submission }) else: initial = { 'title': submission.title, diff --git a/ietf/name/admin.py b/ietf/name/admin.py index 8462bddeb..81182f79a 100644 --- a/ietf/name/admin.py +++ b/ietf/name/admin.py @@ -11,7 +11,7 @@ from ietf.name.models import ( ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, TopicAudienceName, DocUrlTagName, ReviewAssignmentStateName, ReviewerQueuePolicyName, TimerangeName, - ExtResourceName, ExtResourceTypeName, ) + ExtResourceName, ExtResourceTypeName, SlideSubmissionStatusName) from ietf.stats.models import CountryAlias @@ -89,3 +89,4 @@ admin.site.register(TimerangeName, NameAdmin) admin.site.register(TopicAudienceName, NameAdmin) admin.site.register(DocUrlTagName, NameAdmin) admin.site.register(ExtResourceTypeName, NameAdmin) +admin.site.register(SlideSubmissionStatusName, NameAdmin) diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 21253571c..2fe5a4c5d 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -12077,6 +12077,36 @@ "model": "name.sessionstatusname", "pk": "schedw" }, + { + "fields": { + "desc": "Approved", + "name": "approved", + "order": 1, + "used": true + }, + "model": "name.slidesubmissionstatusname", + "pk": "approved" + }, + { + "fields": { + "desc": "Pending", + "name": "pending", + "order": 0, + "used": true + }, + "model": "name.slidesubmissionstatusname", + "pk": "pending" + }, + { + "fields": { + "desc": "Rejected", + "name": "rejected", + "order": 2, + "used": true + }, + "model": "name.slidesubmissionstatusname", + "pk": "rejected" + }, { "fields": { "desc": "", @@ -14905,7 +14935,7 @@ "fields": { "command": "xym", "switch": "--version", - "time": "2020-08-12T00:12:54.984", + "time": "2020-07-23T00:12:27.508", "used": true, "version": "xym 0.4.8" }, @@ -14916,7 +14946,7 @@ "fields": { "command": "pyang", "switch": "--version", - "time": "2020-08-12T00:12:56.359", + "time": "2020-07-23T00:12:28.886", "used": true, "version": "pyang 2.3.2" }, @@ -14927,7 +14957,7 @@ "fields": { "command": "yanglint", "switch": "--version", - "time": "2020-08-12T00:12:56.632", + "time": "2020-07-23T00:12:29.140", "used": true, "version": "yanglint SO 1.6.7" }, @@ -14938,7 +14968,7 @@ "fields": { "command": "xml2rfc", "switch": "--version", - "time": "2020-08-12T00:12:58.366", + "time": "2020-07-23T00:12:30.892", "used": true, "version": "xml2rfc 2.47.0" }, diff --git a/ietf/name/migrations/0018_slidesubmissionstatusname.py b/ietf/name/migrations/0018_slidesubmissionstatusname.py new file mode 100644 index 000000000..b0f1e18e4 --- /dev/null +++ b/ietf/name/migrations/0018_slidesubmissionstatusname.py @@ -0,0 +1,42 @@ +# Generated by Django 2.2.14 on 2020-08-03 11:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('name', '0017_update_constraintname_order_and_label'), + ] + + def forward(apps, schema_editor): + SlideSubmissionStatusName = apps.get_model('name', 'SlideSubmissionStatusName') + slide_submission_status_names = [ + ('pending', 'Pending'), + ('approved', 'Approved'), + ('rejected', 'Rejected'), + ] + for order, (slug, desc) in enumerate(slide_submission_status_names): + SlideSubmissionStatusName.objects.create(slug=slug, name=slug, desc=desc, used=True, order=order) + + + def reverse(apps, schema_editor): + pass + + operations = [ + migrations.CreateModel( + name='SlideSubmissionStatusName', + fields=[ + ('slug', models.CharField(max_length=32, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=255)), + ('desc', models.TextField(blank=True)), + ('used', models.BooleanField(default=True)), + ('order', models.IntegerField(default=0)), + ], + options={ + 'ordering': ['order', 'name'], + 'abstract': False, + }, + ), + migrations.RunPython(forward, reverse), + ] diff --git a/ietf/name/models.py b/ietf/name/models.py index 648f07abd..a1d2deebf 100644 --- a/ietf/name/models.py +++ b/ietf/name/models.py @@ -130,3 +130,5 @@ class ExtResourceTypeName(NameModel): class ExtResourceName(NameModel): """GitHub Repository URL, GitHub Username, ...""" type = ForeignKey(ExtResourceTypeName) +class SlideSubmissionStatusName(NameModel): + "Pending, Accepted, Rejected" diff --git a/ietf/name/resources.py b/ietf/name/resources.py index 80c78842a..40d2cc076 100644 --- a/ietf/name/resources.py +++ b/ietf/name/resources.py @@ -17,7 +17,8 @@ from ietf.name.models import ( AgendaTypeName, BallotPositionName, ConstraintNam LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName, ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, - TopicAudienceName, ReviewerQueuePolicyName, TimerangeName, ExtResourceTypeName, ExtResourceName) + TopicAudienceName, ReviewerQueuePolicyName, TimerangeName, ExtResourceTypeName, ExtResourceName, + SlideSubmissionStatusName) class TimeSlotTypeNameResource(ModelResource): class Meta: @@ -650,3 +651,20 @@ class ExtResourceNameResource(ModelResource): "type": ALL_WITH_RELATIONS, } api.name.register(ExtResourceNameResource()) + +class SlideSubmissionStatusNameResource(ModelResource): + class Meta: + queryset = SlideSubmissionStatusName.objects.all() + serializer = api.Serializer() + cache = SimpleCache() + resource_name = 'slidesubmissionstatusname' + ordering = ['slug', ] + filtering = { + "slug": ALL, + "name": ALL, + "desc": ALL, + "used": ALL, + "order": ALL, + } +api.name.register(SlideSubmissionStatusNameResource()) + diff --git a/ietf/templates/meeting/previously_approved_slides.html b/ietf/templates/meeting/previously_approved_slides.html new file mode 100644 index 000000000..a27890288 --- /dev/null +++ b/ietf/templates/meeting/previously_approved_slides.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2020, All Rights Reserved #} +{% load origin staticfiles bootstrap3 %} + +{% block title %}Approved Slides for {{ submission.session.meeting }} : {{ submission.session.group.acronym }}{% endblock %} + +{% block content %} + {% origin %} + + <h1>These slides have already been {% if submission.status.slug == 'approved' %} approved {% else %} rejected {% endif %}</h1> + + <p>The slides from {{ submission.submitter }} have already been {% if submission.status.slug == 'approved' %} approved {% else %} rejected {% endif %}. No further action is needed.</p> + + <p>You may wish to + {% if submission.status.slug == 'approved' and submission.doc %} + <a href="{% url 'ietf.doc.views_doc.document_main' name=submission.doc.name %}">view the slides</a> or + {% endif %} + <a href="{% url "ietf.meeting.views.session_details" num=submission.session.meeting.number acronym=submission.session.group.acronym %}">return to this meeting session</a>.</p> + +{% endblock %}