diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 14a0d5ea90..ac7854f265 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -33,23 +33,16 @@ "oderwat.indent-rainbow", "redhat.vscode-yaml", "spmeesseman.vscode-taskexplorer", - "visualstudioexptteam.vscodeintellicode" + "visualstudioexptteam.vscodeintellicode", + "ms-python.pylint" ], "settings": { "terminal.integrated.defaultProfile.linux": "zsh", "python.pythonPath": "/usr/local/bin/python", "python.languageServer": "Default", - "python.linting.enabled": true, - "python.linting.pylintEnabled": true, "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", "python.formatting.blackPath": "/usr/local/py-utils/bin/black", "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", - "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", - "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", - "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", - "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", - "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", - "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint", "python.testing.pytestArgs": [ "ietf" ], diff --git a/dev/deploy-to-container/cli.js b/dev/deploy-to-container/cli.js index 1c3d466286..a22c746ae2 100644 --- a/dev/deploy-to-container/cli.js +++ b/dev/deploy-to-container/cli.js @@ -245,7 +245,7 @@ async function main () { name: `dt-app-${branch}`, Hostname: `dt-app-${branch}`, Env: [ - `LETSENCRYPT_HOST=${hostname}`, + // `LETSENCRYPT_HOST=${hostname}`, `VIRTUAL_HOST=${hostname}`, `VIRTUAL_PORT=8000`, `PGHOST=dt-db-${branch}` diff --git a/dev/deploy-to-container/start.sh b/dev/deploy-to-container/start.sh index 271c54a43e..2c83d6970c 100644 --- a/dev/deploy-to-container/start.sh +++ b/dev/deploy-to-container/start.sh @@ -38,8 +38,5 @@ echo "Running Datatracker checks..." echo "Running Datatracker migrations..." /usr/local/bin/python ./ietf/manage.py migrate --settings=settings_local -echo "Syncing with the rfc-index" -./ietf/bin/rfc-editor-index-updates -d 1969-01-01 - echo "Starting Datatracker..." ./ietf/manage.py runserver 0.0.0.0:8000 --settings=settings_local diff --git a/ietf/doc/models.py b/ietf/doc/models.py index 9275b54101..e53717cb7d 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -634,6 +634,9 @@ def pdfized(self): ) except AssertionError: pdf = None + except Exception as e: + log.log('weasyprint failed:'+str(e)) + raise if pdf: cache.set(cache_key, pdf, settings.PDFIZER_CACHE_TIME) return pdf @@ -649,7 +652,7 @@ def referenced_by(self): source__states__slug="active", ) | models.Q(source__type__slug="rfc") - ) + ).distinct() def referenced_by_rfcs(self): """Get refs to this doc from RFCs""" diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index 8ae588ad1d..0b5a651060 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2012-2020, All Rights Reserved +# Copyright The IETF Trust 2012-2023, All Rights Reserved # -*- coding: utf-8 -*- @@ -31,6 +31,8 @@ from tastypie.test import ResourceTestCaseMixin +from weasyprint.urls import URLFetchingError + import debug # pyflakes:ignore from ietf.doc.models import ( Document, DocRelationshipName, RelatedDocument, State, @@ -40,7 +42,7 @@ ConflictReviewFactory, WgDraftFactory, IndividualDraftFactory, WgRfcFactory, IndividualRfcFactory, StateDocEventFactory, BallotPositionDocEventFactory, BallotDocEventFactory, DocumentAuthorFactory, NewRevisionDocEventFactory, - StatusChangeFactory, DocExtResourceFactory, RgDraftFactory) + StatusChangeFactory, DocExtResourceFactory, RgDraftFactory, BcpFactory) from ietf.doc.forms import NotifyForm from ietf.doc.fields import SearchableDocumentsField from ietf.doc.utils import create_ballot_if_not_open, uppercase_std_abbreviated_name @@ -156,6 +158,23 @@ def test_search(self): self.assertEqual(r.status_code, 200) self.assertContains(r, draft.title) + def test_search_became_rfc(self): + draft = WgDraftFactory() + rfc = WgRfcFactory() + draft.set_state(State.objects.get(type="draft", slug="rfc")) + draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) + base_url = urlreverse('ietf.doc.views_search.search') + + # find by RFC + r = self.client.get(base_url + f"?rfcs=on&name={rfc.name}") + self.assertEqual(r.status_code, 200) + self.assertContains(r, rfc.title) + + # find by draft + r = self.client.get(base_url + f"?activedrafts=on&rfcs=on&name={draft.name}") + self.assertEqual(r.status_code, 200) + self.assertContains(r, rfc.title) + def test_search_for_name(self): draft = WgDraftFactory(name='draft-ietf-mars-test',group=GroupFactory(acronym='mars',parent=Group.objects.get(acronym='farfut')),authors=[PersonFactory()],ad=PersonFactory()) draft.set_state(State.objects.get(used=True, type="draft-iesg", slug="pub-req")) @@ -1948,6 +1967,12 @@ def _parse_bibtex_response(self, response) -> dict: @override_settings(RFC_EDITOR_INFO_BASE_URL='https://www.rfc-editor.ietf.org/info/') def test_document_bibtex(self): + + for factory in [CharterFactory, BcpFactory, StatusChangeFactory, ConflictReviewFactory]: # Should be extended to all other doc types + doc = factory() + url = urlreverse("ietf.doc.views_doc.document_bibtex", kwargs=dict(name=doc.name)) + r = self.client.get(url) + self.assertEqual(r.status_code, 404) rfc = WgRfcFactory.create( time=datetime.datetime(2010, 10, 10, tzinfo=ZoneInfo(settings.TIME_ZONE)) ) @@ -2844,6 +2869,12 @@ def test_pdfized(self): self.should_succeed(dict(name=draft.name,rev=f'{r:02d}',ext=ext)) self.should_404(dict(name=draft.name,rev='02')) + with mock.patch('ietf.doc.models.DocumentInfo.pdfized', side_effect=URLFetchingError): + url = urlreverse(self.view, kwargs=dict(name=rfc.name)) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + self.assertContains(r, "Error while rendering PDF") + class NotifyValidationTests(TestCase): def test_notify_validation(self): valid_values = [ diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py index e168f685f9..045fdc7f46 100644 --- a/ietf/doc/tests_draft.py +++ b/ietf/doc/tests_draft.py @@ -1483,6 +1483,42 @@ def test_confirm_submission(self): self.assertTrue("aread@" in outbox[-1]['To']) self.assertTrue("iesg-secretary@" in outbox[-1]['Cc']) + def test_confirm_submission_no_doc_ad(self): + url = urlreverse('ietf.doc.views_draft.to_iesg', kwargs=dict(name=self.docname)) + self.client.login(username="marschairman", password="marschairman+password") + + doc = Document.objects.get(name=self.docname) + RoleFactory(name_id='ad', group=doc.group, person=doc.ad) + e = DocEvent(type="changed_document", by=doc.ad, doc=doc, rev=doc.rev, desc="Remove doc AD") + e.save() + doc.ad = None + doc.save_with_history([e]) + + docevents_pre = set(doc.docevent_set.all()) + mailbox_before = len(outbox) + + r = self.client.post(url, dict(confirm="1")) + self.assertEqual(r.status_code, 302) + + doc = Document.objects.get(name=self.docname) + self.assertTrue(doc.get_state('draft-iesg').slug=='pub-req') + self.assertTrue(doc.get_state('draft-stream-ietf').slug=='sub-pub') + + self.assertCountEqual(doc.action_holders.all(), [doc.ad]) + + new_docevents = set(doc.docevent_set.all()) - docevents_pre + self.assertEqual(len(new_docevents), 5) + new_docevent_type_count = Counter([e.type for e in new_docevents]) + self.assertEqual(new_docevent_type_count['changed_state'],2) + self.assertEqual(new_docevent_type_count['started_iesg_process'],1) + self.assertEqual(new_docevent_type_count['changed_action_holders'], 1) + self.assertEqual(new_docevent_type_count['changed_document'], 1) + + self.assertEqual(len(outbox), mailbox_before + 1) + self.assertTrue("Publication has been requested" in outbox[-1]['Subject']) + self.assertTrue("aread@" in outbox[-1]['To']) + self.assertTrue("iesg-secretary@" in outbox[-1]['Cc']) + class RequestPublicationTests(TestCase): diff --git a/ietf/doc/tests_review.py b/ietf/doc/tests_review.py index 1c2e6c7ace..d9aca94e86 100644 --- a/ietf/doc/tests_review.py +++ b/ietf/doc/tests_review.py @@ -380,6 +380,25 @@ def test_assign_reviewer_after_reject(self): reviewer_label = q("option[value=\"{}\"]".format(reviewer_email.address)).text().lower() self.assertIn("rejected review of document before", reviewer_label) + def test_assign_reviewer_after_withdraw(self): + doc = WgDraftFactory() + review_team = ReviewTeamFactory() + rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer') + RoleFactory(group=review_team,person__user__username='reviewsecretary',name_id='secr') + review_req = ReviewRequestFactory(team=review_team,doc=doc) + reviewer = rev_role.person.email_set.first() + ReviewAssignmentFactory(review_request=review_req, state_id='withdrawn', reviewer=reviewer) + req_url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk }) + assign_url = urlreverse('ietf.doc.views_review.assign_reviewer', kwargs={ "name": doc.name, "request_id": review_req.pk }) + + login_testing_unauthorized(self, "reviewsecretary", assign_url) + r = self.client.post(assign_url, { "action": "assign", "reviewer": reviewer.pk }) + self.assertRedirects(r, req_url) + review_req = reload_db_objects(review_req) + assignment = review_req.reviewassignment_set.last() + self.assertEqual(assignment.state, ReviewAssignmentStateName.objects.get(slug='assigned')) + self.assertEqual(review_req.state, ReviewRequestStateName.objects.get(slug='assigned')) + def test_previously_reviewed_replaced_doc(self): review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut")) rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',person__name='Some Reviewer',name_id='reviewer') diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 665393c24e..293d32daff 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -51,7 +51,6 @@ from django import forms from django.contrib.staticfiles import finders - import debug # pyflakes:ignore from ietf.doc.models import ( Document, DocHistory, DocEvent, BallotDocEvent, BallotType, @@ -1064,7 +1063,10 @@ def document_pdfized(request, name, rev=None, ext=None): if not os.path.exists(doc.get_file_name()): raise Http404("File not found: %s" % doc.get_file_name()) - pdf = doc.pdfized() + try: + pdf = doc.pdfized() + except Exception: + return render(request, "doc/weasyprint_failed.html") if pdf: return HttpResponse(pdf,content_type='application/pdf') else: @@ -1262,6 +1264,9 @@ def document_bibtex(request, name, rev=None): doc = get_object_or_404(Document, name=name) + if doc.type_id not in ["rfc", "draft"]: + raise Http404() + doi = None draft_became_rfc = None replaced_by = None @@ -2185,13 +2190,31 @@ def idnits2_state(request, name, rev=None): if doc.type_id == "rfc": draft = doc.came_from_draft() if draft: - zero_revision = NewRevisionDocEvent.objects.filter(doc=draft,rev='00').first() + zero_revision = NewRevisionDocEvent.objects.filter( + doc=draft, rev="00" + ).first() else: - zero_revision = NewRevisionDocEvent.objects.filter(doc=doc,rev='00').first() + zero_revision = NewRevisionDocEvent.objects.filter(doc=doc, rev="00").first() if zero_revision: doc.created = zero_revision.time else: - doc.created = doc.docevent_set.order_by('-time').first().time + if doc.type_id == "draft": + if doc.became_rfc(): + interesting_event = ( + doc.became_rfc() + .docevent_set.filter(type="published_rfc") + .order_by("-time") + .first() + ) + else: + interesting_event = doc.docevent_set.order_by( + "-time" + ).first() # Is taking the most _recent_ instead of the oldest event correct? + else: # doc.type_id == "rfc" + interesting_event = ( + doc.docevent_set.filter(type="published_rfc").order_by("-time").first() + ) + doc.created = interesting_event.time if doc.std_level: doc.deststatus = doc.std_level.name elif doc.intended_std_level: @@ -2199,8 +2222,16 @@ def idnits2_state(request, name, rev=None): else: text = doc.text() if text: - parsed_draft = PlaintextDraft(text=doc.text(), source=name, name_from_source=False) + parsed_draft = PlaintextDraft( + text=doc.text(), source=name, name_from_source=False + ) doc.deststatus = parsed_draft.get_status() else: - doc.deststatus="Unknown" - return render(request, 'doc/idnits2-state.txt', context={'doc':doc}, content_type='text/plain;charset=utf-8') + doc.deststatus = "Unknown" + return render( + request, + "doc/idnits2-state.txt", + context={"doc": doc}, + content_type="text/plain;charset=utf-8", + ) + diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index 4f6659af9f..ea30e7bd2d 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -560,22 +560,19 @@ def to_iesg(request,name): if request.method == 'POST': if request.POST.get("confirm", ""): - by = request.user.person events = [] - - changes = [] + def doc_event(type, by, doc, desc): + return DocEvent.objects.create(type=type, by=by, doc=doc, rev=doc.rev, desc=desc) if doc.get_state_slug("draft-iesg") == "idexists": - e = DocEvent() - e.type = "started_iesg_process" - e.by = by - e.doc = doc - e.rev = doc.rev - e.desc = "Document is now in IESG state %s" % target_state['iesg'].name - e.save() - events.append(e) + events.append(doc_event("started_iesg_process", by, doc, f"Document is now in IESG state {target_state['iesg'].name}")) + + # do this first, so AD becomes action holder + if not doc.ad == ad : + doc.ad = ad + events.append(doc_event("changed_document", by, doc, f"Responsible AD changed to {doc.ad}")) for state_type in ['draft-iesg','draft-stream-ietf']: prev_state=doc.get_state(state_type) @@ -587,25 +584,14 @@ def to_iesg(request,name): events.append(e) events.append(add_state_change_event(doc=doc,by=by,prev_state=prev_state,new_state=new_state)) - if not doc.ad == ad : - doc.ad = ad - changes.append("Responsible AD changed to %s" % doc.ad) - if not doc.notify == notify : doc.notify = notify - changes.append("State Change Notice email list changed to %s" % doc.notify) + events.append(doc_event("changed_document", by, doc, f"State Change Notice email list changed to {doc.notify}")) # Get the last available writeup previous_writeup = doc.latest_event(WriteupDocEvent,type="changed_protocol_writeup") if previous_writeup != None: - changes.append(previous_writeup.text) - - for c in changes: - e = DocEvent(doc=doc, rev=doc.rev, by=by) - e.desc = c - e.type = "changed_document" - e.save() - events.append(e) + events.append(doc_event("changed_document", by, doc, previous_writeup.text)) doc.save_with_history(events) diff --git a/ietf/doc/views_help.py b/ietf/doc/views_help.py index 73cdcdd20f..e8e63ed8b7 100644 --- a/ietf/doc/views_help.py +++ b/ietf/doc/views_help.py @@ -1,5 +1,7 @@ # Copyright The IETF Trust 2013-2023, All Rights Reserved +import debug # pyflakes: ignore + from django.shortcuts import render, get_object_or_404 from django.http import Http404 @@ -18,6 +20,7 @@ def state_help(request, type=None): "draft-stream-irtf": ("draft-stream-irtf", "IRTF Stream States for Internet-Drafts"), "draft-stream-ise": ("draft-stream-ise", "ISE Stream States for Internet-Drafts"), "draft-stream-iab": ("draft-stream-iab", "IAB Stream States for Internet-Drafts"), + "draft-stream-editorial": ("draft-stream-editorial", "Editorial Stream States for Internet-Drafts"), "charter": ("charter", "Charter States"), "conflict-review": ("conflrev", "Conflict Review States"), "status-change": ("statchg", "RFC Status Change States"), diff --git a/ietf/doc/views_search.py b/ietf/doc/views_search.py index 2e4231c5ac..964894bff9 100644 --- a/ietf/doc/views_search.py +++ b/ietf/doc/views_search.py @@ -211,6 +211,9 @@ def retrieve_search_results(form, all_types=False): Q(targets_related__source__title__icontains=singlespace, targets_related__relationship_id="contains"), ]) + if query["rfcs"]: + queries.extend([Q(targets_related__source__name__icontains=look_for, targets_related__relationship_id="became_rfc")]) + combined_query = reduce(operator.or_, queries) docs = docs.filter(combined_query).distinct() @@ -468,11 +471,11 @@ def ad_workload(request): state = doc_state(doc) state_events = doc.docevent_set.filter( - Q(type="started_iesg_process") - | Q(type="changed_state") - | Q(type="published_rfc") - | Q(type="closed_ballot"), - ).order_by("-time") + type__in=["started_iesg_process", "changed_state", "closed_ballot"] + ) + if doc.became_rfc(): + state_events = state_events | doc.became_rfc().docevent_set.filter(type="published_rfc") + state_events = state_events.order_by("-time") # compute state history for drafts last = now diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 47e2334f47..a57fcf63c1 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2009-2020, All Rights Reserved +# Copyright The IETF Trust 2009-2023, All Rights Reserved # -*- coding: utf-8 -*- import datetime import io @@ -6106,21 +6106,21 @@ def test_upload_minutes_agenda(self): test_file = BytesIO(b'this is some text for a test') test_file.name = "not_really.json" - r = self.client.post(url,dict(file=test_file)) + r = self.client.post(url,dict(submission_method="upload",file=test_file)) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q('form .is-invalid')) test_file = BytesIO(b'this is some text for a test'*1510000) test_file.name = "not_really.pdf" - r = self.client.post(url,dict(file=test_file)) + r = self.client.post(url,dict(submission_method="upload",file=test_file)) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q('form .is-invalid')) test_file = BytesIO(b'') test_file.name = "not_really.html" - r = self.client.post(url,dict(file=test_file)) + r = self.client.post(url,dict(submission_method="upload",file=test_file)) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q('form .is-invalid')) @@ -6128,7 +6128,7 @@ def test_upload_minutes_agenda(self): # Test html sanitization test_file = BytesIO(b'Title

Title

Some text
') test_file.name = "some.html" - r = self.client.post(url,dict(file=test_file)) + r = self.client.post(url,dict(submission_method="upload",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') @@ -6140,7 +6140,7 @@ def test_upload_minutes_agenda(self): # txt upload test_file = BytesIO(b'This is some text for a test, with the word\nvirtual at the beginning of a line.') test_file.name = "some.txt" - r = self.client.post(url,dict(file=test_file,apply_to_all=False)) + r = self.client.post(url,dict(submission_method="upload",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,'01') @@ -6152,7 +6152,7 @@ def test_upload_minutes_agenda(self): self.assertIn('Revise', str(q("Title"))) test_file = BytesIO(b'this is some different text for a test') test_file.name = "also_some.txt" - r = self.client.post(url,dict(file=test_file,apply_to_all=True)) + r = self.client.post(url,dict(submission_method="upload",file=test_file,apply_to_all=True)) self.assertEqual(r.status_code, 302) doc = Document.objects.get(pk=doc.pk) self.assertEqual(doc.rev,'02') @@ -6161,7 +6161,7 @@ def test_upload_minutes_agenda(self): # Test bad encoding test_file = BytesIO('

Title

Some\x93text
'.encode('latin1')) test_file.name = "some.html" - r = self.client.post(url,dict(file=test_file)) + r = self.client.post(url,dict(submission_method="upload",file=test_file)) self.assertContains(r, 'Could not identify the file encoding') doc = Document.objects.get(pk=doc.pk) self.assertEqual(doc.rev,'02') @@ -6191,7 +6191,7 @@ def test_upload_minutes_agenda_unscheduled(self): test_file = BytesIO(b'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)) + r = self.client.post(url,dict(submission_method="upload",file=test_file,apply_to_all=False)) self.assertEqual(r.status_code, 410) @override_settings(MEETING_MATERIALS_SERVE_LOCALLY=True) @@ -6211,7 +6211,7 @@ def test_upload_minutes_agenda_interim(self): self.assertFalse(session.sessionpresentation_set.filter(document__type_id=doctype)) test_file = BytesIO(b'this is some text for a test') test_file.name = "not_really.txt" - r = self.client.post(url,dict(file=test_file)) + r = self.client.post(url,dict(submission_method="upload",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') @@ -6223,6 +6223,46 @@ def test_upload_minutes_agenda_interim(self): self.requests_mock.get(f'{session.notes_url()}/info', text=json.dumps({'title': 'title', 'updatetime': '2021-12-01T17:11:00z'})) self.crawl_materials(url=url, top=top) + def test_enter_agenda(self): + session = SessionFactory(meeting__type_id='ietf') + url = urlreverse('ietf.meeting.views.upload_session_agenda',kwargs={'num':session.meeting.number,'session_id':session.id}) + redirect_url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number,'acronym':session.group.acronym}) + login_testing_unauthorized(self,"secretary",url) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertIn('Upload', str(q("Title"))) + self.assertFalse(session.sessionpresentation_set.exists()) + + test_text = 'Enter agenda from scratch' + r = self.client.post(url,dict(submission_method="enter",content=test_text)) + self.assertRedirects(r, redirect_url) + doc = session.sessionpresentation_set.filter(document__type_id='agenda').first().document + self.assertEqual(doc.rev,'00') + + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertIn('Revise', str(q("Title"))) + + test_file = BytesIO(b'Upload after enter') + test_file.name = "some.txt" + r = self.client.post(url,dict(submission_method="upload",file=test_file)) + self.assertRedirects(r, redirect_url) + doc = Document.objects.get(pk=doc.pk) + self.assertEqual(doc.rev,'01') + + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertIn('Revise', str(q("Title"))) + + test_text = 'Enter after upload' + r = self.client.post(url,dict(submission_method="enter",content=test_text)) + self.assertRedirects(r, redirect_url) + doc = Document.objects.get(pk=doc.pk) + self.assertEqual(doc.rev,'02') + def test_upload_slides(self): session1 = SessionFactory(meeting__type_id='ietf') diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index ab39266396..9d07df103e 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2007-2022, All Rights Reserved +# Copyright The IETF Trust 2007-2023, All Rights Reserved # -*- coding: utf-8 -*- @@ -2662,6 +2662,40 @@ def upload_session_minutes(request, session_id, num): }) +class UploadOrEnterAgendaForm(UploadAgendaForm): + ACTIONS = [ + ("upload", "Upload agenda"), + ("enter", "Enter agenda"), + ] + submission_method = forms.ChoiceField(choices=ACTIONS, widget=forms.RadioSelect) + + content = forms.CharField(widget=forms.Textarea, required=False, strip=False, label="Agenda text") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["file"].required=False + self.order_fields(["submission_method", "file", "content"]) + + def clean_content(self): + return self.cleaned_data["content"].replace("\r", "") + + def clean_file(self): + submission_method = self.cleaned_data.get("submission_method") + if submission_method == "upload": + return super().clean_file() + return None + + def clean(self): + def require_field(f): + if not self.cleaned_data.get(f): + self.add_error(f, ValidationError("You must fill in this field.")) + + submission_method = self.cleaned_data.get("submission_method") + if submission_method == "upload": + require_field("file") + elif submission_method == "enter": + require_field("content") + 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) @@ -2680,10 +2714,23 @@ def upload_session_agenda(request, session_id, num): agenda_sp = session.sessionpresentation_set.filter(document__type='agenda').first() if request.method == 'POST': - form = UploadAgendaForm(show_apply_to_all_checkbox,request.POST,request.FILES) + form = UploadOrEnterAgendaForm(show_apply_to_all_checkbox,request.POST,request.FILES) if form.is_valid(): - file = request.FILES['file'] - _, ext = os.path.splitext(file.name) + submission_method = form.cleaned_data['submission_method'] + if submission_method == "upload": + file = request.FILES['file'] + _, ext = os.path.splitext(file.name) + else: + if agenda_sp: + doc = agenda_sp.document + _, ext = os.path.splitext(doc.uploaded_filename) + else: + ext = ".md" + fd, name = tempfile.mkstemp(suffix=ext, text=True) + os.close(fd) + with open(name, "w") as file: + file.write(form.cleaned_data['content']) + file = open(name, "rb") apply_to_all = session.type.slug == 'regular' if show_apply_to_all_checkbox: apply_to_all = form.cleaned_data['apply_to_all'] @@ -2738,7 +2785,11 @@ def upload_session_agenda(request, session_id, num): doc.uploaded_filename = filename e = NewRevisionDocEvent.objects.create(doc=doc,by=request.user.person,type='new_revision',desc='New revision available: %s'%doc.rev,rev=doc.rev) # The way this function builds the filename it will never trigger the file delete in handle_file_upload. - save_error = handle_upload_file(file, filename, session.meeting, 'agenda', request=request, encoding=form.file_encoding[file.name]) + try: + encoding=form.file_encoding[file.name] + except AttributeError: + encoding=None + save_error = handle_upload_file(file, filename, session.meeting, 'agenda', request=request, encoding=encoding) if save_error: form.add_error(None, save_error) else: @@ -2746,7 +2797,11 @@ def upload_session_agenda(request, session_id, num): messages.success(request, f'Successfully uploaded agenda as revision {doc.rev}.') return redirect('ietf.meeting.views.session_details',num=num,acronym=session.group.acronym) else: - form = UploadAgendaForm(show_apply_to_all_checkbox, initial={'apply_to_all':session.type_id=='regular'}) + initial={'apply_to_all':session.type_id=='regular', 'submission_method':'upload'} + if agenda_sp: + doc = agenda_sp.document + initial['content'] = doc.text() + form = UploadOrEnterAgendaForm(show_apply_to_all_checkbox, initial=initial) return render(request, "meeting/upload_session_agenda.html", {'session': session, diff --git a/ietf/review/utils.py b/ietf/review/utils.py index 8869efaee0..61494738d3 100644 --- a/ietf/review/utils.py +++ b/ietf/review/utils.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2016-2020, All Rights Reserved +# Copyright The IETF Trust 2016-2023, All Rights Reserved # -*- coding: utf-8 -*- @@ -392,7 +392,9 @@ def assign_review_request_to_reviewer(request, review_req, reviewer, add_skip=Fa # cannot reference reviewassignment_set relation until pk exists if review_req.pk is not None: reviewassignment_set = review_req.reviewassignment_set.filter(reviewer=reviewer) - if reviewassignment_set.exists() and not reviewassignment_set.filter(state_id='rejected').exists(): + if (reviewassignment_set.exists() and not + (reviewassignment_set.filter(state_id='rejected').exists() or + reviewassignment_set.filter(state_id='withdrawn').exists())): return # Note that assigning a review no longer unassigns other reviews diff --git a/ietf/static/js/upload-session-agenda.js b/ietf/static/js/upload-session-agenda.js new file mode 100644 index 0000000000..b63d460ed8 --- /dev/null +++ b/ietf/static/js/upload-session-agenda.js @@ -0,0 +1,28 @@ +$(document) + .ready(function () { + var form = $("form.my-3"); + + // review submission selection + form.find("[name=submission_method]") + .on("click change", function () { + var val = form.find("[name=submission_method]:checked") + .val(); + + var shouldBeVisible = { + upload: ['[name="file"]'], + enter: ['[name="content"]'] + }; + + for (var v in shouldBeVisible) { + for (var i in shouldBeVisible[v]) { + var selector = shouldBeVisible[v][i]; + var row = form.find(selector).parent(); + if ($.inArray(selector, shouldBeVisible[val]) != -1) + row.show(); + else + row.hide(); + } + } + }) + .trigger("change"); + }); \ No newline at end of file diff --git a/ietf/templates/doc/weasyprint_failed.html b/ietf/templates/doc/weasyprint_failed.html new file mode 100644 index 0000000000..985fe12fe1 --- /dev/null +++ b/ietf/templates/doc/weasyprint_failed.html @@ -0,0 +1,17 @@ +{# Copyright The IETF Trust 2023, All Rights Reserved #} +{% extends "base.html" %} +{% load origin %} +{% block title %}Error while rendering PDF{% endblock %} +{% block content %} + {% origin %} +

Error while rendering PDF

+

+ An error was encountered while trying to render your document as PDF. + In case this was a temporary error, you may want to try again in a + little while. +

+

+ A failure report with details about what happened has been sent to the + server administrators. +

+{% endblock %} \ No newline at end of file diff --git a/ietf/templates/group/meetings-row.html b/ietf/templates/group/meetings-row.html index fbaf7cd560..65ba435baa 100644 --- a/ietf/templates/group/meetings-row.html +++ b/ietf/templates/group/meetings-row.html @@ -16,6 +16,11 @@ {% endwith %} {% else %}
{{ s.current_status_name }}
+ {% if s.current_status == "canceled" %} + {% with timeslot=s.official_timeslotassignment.timeslot %} + + {% endwith %} + {% endif %} {% endif %} {% if show_request and s.meeting.type_id == 'ietf' %} {% if can_edit %} diff --git a/ietf/templates/meeting/upload_session_agenda.html b/ietf/templates/meeting/upload_session_agenda.html index 1856a75bdb..57cba6b53c 100644 --- a/ietf/templates/meeting/upload_session_agenda.html +++ b/ietf/templates/meeting/upload_session_agenda.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{# Copyright The IETF Trust 2015, All Rights Reserved #} +{# Copyright The IETF Trust 2015-2023, All Rights Reserved #} {% load origin static django_bootstrap5 tz %} {% block title %} {% if agenda_sp %} @@ -29,6 +29,9 @@

Session {{ session_number }} : {{ session.official_timeslotassignment.timesl
{% csrf_token %} {% bootstrap_form form %} - +
+{% endblock %} +{% block js %} + {% endblock %} \ No newline at end of file diff --git a/package.json b/package.json index b6df202e7d..a87d5ac131 100644 --- a/package.json +++ b/package.json @@ -152,6 +152,7 @@ "ietf/static/js/timezone.js", "ietf/static/js/upcoming.js", "ietf/static/js/upload-material.js", + "ietf/static/js/upload-session-agenda.js", "ietf/static/js/upload_bofreq.js", "ietf/static/js/upload_statement.js", "ietf/static/js/zxcvbn.js"