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..e2470fc97 --- /dev/null +++ b/ietf/meeting/migrations/0031_auto_20200803_1153.py @@ -0,0 +1,28 @@ +# 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', '0016_slidesubmissionstatusname'), + ('doc', '0035_populate_docextresources'), + ('meeting', '0030_allow_empty_joint_with_sessions'), + ('name', '0016_slidesubmissionstatusname'), + ] + + 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'), + ), + migrations.AddField( + model_name='slidesubmission', + name='status', + field=ietf.utils.models.ForeignKey(default='pending', on_delete=django.db.models.deletion.PROTECT, to='name.SlideSubmissionStatusName'), + ), + ] diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index 251c89359..5802082e8 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -28,7 +28,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 @@ -1269,6 +1269,9 @@ class SlideSubmission(models.Model): apply_to_all = models.BooleanField(default=False) submitter = ForeignKey(Person) + status = ForeignKey(SlideSubmissionStatusName, default='pending', on_delete=models.PROTECT) + 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 f39814bea..b20bd4bf8 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -2760,7 +2760,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() @@ -2769,13 +2773,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') @@ -2846,7 +2859,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) @@ -2858,7 +2871,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 e2b86ad29..5f65f82dc 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -27,7 +27,7 @@ import debug # pyflakes:ignore from django import forms from django.shortcuts import render, redirect, get_object_or_404 -from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, Http404 +from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, 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 session_requested_by from ietf.meeting.utils import current_session_status from ietf.meeting.utils import data_for_meetings_overview 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) @@ -1615,9 +1616,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 , @@ -3213,13 +3214,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) @@ -3259,15 +3263,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 3822cecc8..ce5da2d88 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -12015,6 +12015,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": "", @@ -14843,7 +14873,7 @@ "fields": { "command": "xym", "switch": "--version", - "time": "2020-07-09T00:12:56.528", + "time": "2020-07-23T00:12:27.508", "used": true, "version": "xym 0.4.8" }, @@ -14854,9 +14884,9 @@ "fields": { "command": "pyang", "switch": "--version", - "time": "2020-07-09T00:12:58.135", + "time": "2020-07-23T00:12:28.886", "used": true, - "version": "pyang 2.2.1" + "version": "pyang 2.3.2" }, "model": "utils.versioninfo", "pk": 2 @@ -14865,7 +14895,7 @@ "fields": { "command": "yanglint", "switch": "--version", - "time": "2020-07-09T00:12:58.398", + "time": "2020-07-23T00:12:29.140", "used": true, "version": "yanglint SO 1.6.7" }, @@ -14876,9 +14906,9 @@ "fields": { "command": "xml2rfc", "switch": "--version", - "time": "2020-07-09T00:13:00.193", + "time": "2020-07-23T00:12:30.892", "used": true, - "version": "xml2rfc 2.46.0" + "version": "xml2rfc 2.47.0" }, "model": "utils.versioninfo", "pk": 4 diff --git a/ietf/name/migrations/0016_slidesubmissionstatusname.py b/ietf/name/migrations/0016_slidesubmissionstatusname.py new file mode 100644 index 000000000..fb511656d --- /dev/null +++ b/ietf/name/migrations/0016_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', '0015_populate_extres'), + ] + + 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 69c96c29a..64c81ac9e 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()) +