From 8e439df7c55751bb5d603aab029858d931470bdd Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Sat, 22 Jul 2023 01:52:19 +0300 Subject: [PATCH 01/42] fix: HTML-escape person name in tests (#5986) * fix: Add `mark_safe` to `person_link` to prevent HTML escaping Fixes part of #5834, namely https://github.com/ietf-tools/datatracker/issues/5834#issuecomment-1627454562 * fix: Fix tests instead of marking name safe --- ietf/group/tests_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ietf/group/tests_review.py b/ietf/group/tests_review.py index 6ca77a0e18..6b673ad959 100644 --- a/ietf/group/tests_review.py +++ b/ietf/group/tests_review.py @@ -41,7 +41,7 @@ def test_review_requests(self): r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertContains(r, review_req.doc.name) - self.assertContains(r, assignment.reviewer.person.name) + self.assertContains(r, escape(assignment.reviewer.person.name)) url = urlreverse(ietf.group.views.review_requests, kwargs={ 'acronym': group.acronym }) @@ -183,7 +183,7 @@ def test_reviewer_overview(self): urlreverse(ietf.group.views.reviewer_overview, kwargs={ 'acronym': group.acronym, 'group_type': group.type_id })]: r = self.client.get(url) self.assertEqual(r.status_code, 200) - self.assertContains(r, reviewer.name) + self.assertContains(r, escape(reviewer.name)) self.assertContains(r, review_req1.doc.name) # without a login, reason for being unavailable should not be seen self.assertNotContains(r, "Availability") From a38b6c1bde377cb375e9f98982a394b82ce29f50 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Fri, 21 Jul 2023 18:59:29 -0400 Subject: [PATCH 02/42] docs: update docker/README.md --- docker/README.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/docker/README.md b/docker/README.md index 08017a7ab7..61fdcfa856 100644 --- a/docker/README.md +++ b/docker/README.md @@ -45,7 +45,7 @@ You can also open the datatracker project folder and click the **Reopen in conta ### Usage -- Under the **Run and Debug** tab, you can run the server with the debugger attached using **Run Server** (F5). Once the server is ready to accept connections, you'll be prompted to open in a browser. You can also open [http://localhost:8000](http://localhost:8000) in a browser. +- Under the **Run and Debug** tab, you can run the server with the debugger attached using **Run Server** (F5). Once the server is ready to accept connections, you'll be prompted to open in a browser. Navigate to [http://localhost:8000](http://localhost:8000) in your preferred browser. > An alternate profile **Run Server with Debug Toolbar** is also available from the dropdown menu, which displays various tools on top of the webpage. However, note that this configuration has a significant performance impact. @@ -64,11 +64,7 @@ You can also open the datatracker project folder and click the **Reopen in conta ![](assets/vscode-terminal-new.png) -- Under the **SQL Tools** tab, a connection **Local Dev** is preconfigured to connect to the DB container. Using this tool, you can list tables, view records and execute SQL queries directly from VS Code. - - > The port `3306` is also exposed to the host automatically, should you prefer to use your own SQL tool. - - ![](assets/vscode-sqltools.png) +- The pgAdmin web interface, a PostgreSQL DB browser / management UI, is available at [http://localhost:8000/pgadmin/](http://localhost:8000/pgadmin/). - Under the **Task Explorer** tab, a list of available preconfigured tasks is displayed. *(You may need to expand the tree to `src > vscode` to see it.)* These are common scritps you can run *(e.g. run tests, fetch assets, etc.)*. @@ -103,7 +99,7 @@ You can also open the datatracker project folder and click the **Reopen in conta 2. Wait for the containers to initialize. Upon completion, you will be dropped into a shell from which you can start the datatracker and execute related commands as usual, for example ``` - ietf/manage.py runserver 0.0.0.0:8000 + ietf/manage.py runserver 0.0.0.0:8001 ``` to start the datatracker. @@ -161,11 +157,11 @@ docker compose down -v --rmi all docker image prune ``` -### Accessing MariaDB Port +### Accessing PostgreSQL Port -The port is exposed but not mapped to `3306` to avoid potential conflicts with the host. To get the mapped port, run the command *(from the project `/docker` directory)*: +The port is exposed but not automatically mapped to `5432` to avoid potential conflicts with the host. To get the mapped port, run the command *(from the project `/docker` directory)*: ```sh -docker compose port db 3306 +docker compose port db 5432 ``` ## Notes / Troubleshooting From 98634ea27b715cf50737a8452d29beb35936ac08 Mon Sep 17 00:00:00 2001 From: Darrel Date: Sat, 22 Jul 2023 10:24:41 -0700 Subject: [PATCH 03/42] chore(dev): fails to build a container in a GitHub Codespace (#6006) --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2889bce9b0..3b13fc46e6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -74,7 +74,7 @@ services: CELERY_APP: ietf CELERY_ROLE: worker UPDATE_REQUIREMENTS_FROM: requirements.txt - DEV_MODE: yes + DEV_MODE: "yes" command: - '--loglevel=INFO' depends_on: From 5f8fca68f46c733a9d46fb4b11fc1d6cf27d4606 Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Sat, 22 Jul 2023 22:36:07 +0300 Subject: [PATCH 04/42] chore: Remove unused "rendertest" stuff (#6015) --- ietf/group/tests_info.py | 6 --- ietf/group/urls.py | 1 - ietf/group/views.py | 11 ---- .../group/group_about_rendertest.html | 54 ------------------- 4 files changed, 72 deletions(-) delete mode 100644 ietf/templates/group/group_about_rendertest.html diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py index 672d18c8ff..01d1cc3f1d 100644 --- a/ietf/group/tests_info.py +++ b/ietf/group/tests_info.py @@ -283,12 +283,6 @@ def test_group_charter(self): self.assertContains(r, milestone.desc) self.assertContains(r, milestone.docs.all()[0].name) - def test_about_rendertest(self): - group = CharterFactory().group - url = urlreverse('ietf.group.views.group_about_rendertest', kwargs=dict(acronym=group.acronym)) - r = self.client.get(url) - self.assertEqual(r.status_code,200) - def test_group_about(self): diff --git a/ietf/group/urls.py b/ietf/group/urls.py index 0e4f7ef2fb..18a2ecd67a 100644 --- a/ietf/group/urls.py +++ b/ietf/group/urls.py @@ -20,7 +20,6 @@ url(r'^documents/subscription/$', community_views.subscription), url(r'^charter/$', views.group_about), url(r'^about/$', views.group_about), - url(r'^about/rendertest/$', views.group_about_rendertest), url(r'^about/status/$', views.group_about_status), url(r'^about/status/edit/$', views.group_about_status_edit), url(r'^about/status/meeting/(?P\d+)/$', views.group_about_status_meeting), diff --git a/ietf/group/views.py b/ietf/group/views.py index 88b25a1091..d67a49150c 100644 --- a/ietf/group/views.py +++ b/ietf/group/views.py @@ -604,17 +604,6 @@ def all_status(request): } ) -def group_about_rendertest(request, acronym, group_type=None): - group = get_group_or_404(acronym, group_type) - charter = None - if group.charter: - charter = get_charter_text(group) - try: - rendered = markdown.markdown(charter) - except Exception as e: - rendered = f'Markdown rendering failed: {e}' - return render(request, 'group/group_about_rendertest.html', {'group':group, 'charter':charter, 'rendered':rendered}) - def group_about_status(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) status_update = group.latest_event(type='status_update') diff --git a/ietf/templates/group/group_about_rendertest.html b/ietf/templates/group/group_about_rendertest.html deleted file mode 100644 index b3abf71915..0000000000 --- a/ietf/templates/group/group_about_rendertest.html +++ /dev/null @@ -1,54 +0,0 @@ -{% extends "group/group_base.html" %} -{# Copyright The IETF Trust 2021, All Rights Reserved #} -{% load origin %} -{% load ietf_filters %} -{% block group_content %} - {% if charter %} - {% comment %} -
-
Current about page rendering
-
Markdown rendering
-
-
-
 
-
Constrain width
-
-
-
{{charter|linebreaks}}
-
{{rendered|sanitize|safe}}
-
- {% endcomment %} -
-
-
Current about page rendering
-
 
-
{{ charter|linebreaks }}
-
-
-
Markdown rendering
-
- - -
-
{{ rendered|sanitize|safe }}
-
-
-{% else %} -
Group has no charter document
-{% endif %} -{% endblock %} -{% block js %} - -{% endblock %} \ No newline at end of file From f82988d8b756436f5181b977a1a6926b15006e54 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Sat, 22 Jul 2023 14:40:38 -0500 Subject: [PATCH 05/42] fix: restore ability to create status change documents (#5963) * fix: restore ability to create status change documents Fixes #5962 * chore: address review comment --- ietf/doc/tests_status_change.py | 26 +++++++++++++++++-- ietf/doc/views_status_change.py | 4 +-- .../doc/status_change/last_call.html | 25 +++++++++++------- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/ietf/doc/tests_status_change.py b/ietf/doc/tests_status_change.py index 571d9ed1db..4064b52523 100644 --- a/ietf/doc/tests_status_change.py +++ b/ietf/doc/tests_status_change.py @@ -14,7 +14,7 @@ from django.conf import settings from django.urls import reverse as urlreverse -from ietf.doc.factories import DocumentFactory, IndividualRfcFactory, WgRfcFactory +from ietf.doc.factories import DocumentFactory, IndividualRfcFactory, WgRfcFactory, DocEventFactory from ietf.doc.models import ( Document, DocAlias, State, DocEvent, BallotPositionDocEvent, NewRevisionDocEvent, TelechatDocEvent, WriteupDocEvent ) from ietf.doc.utils import create_ballot_if_not_open @@ -86,6 +86,16 @@ def test_start_review(self): status_change = Document.objects.get(name='status-change-imaginary-new2') self.assertIsNone(status_change.ad) + # Verify that the right thing happens if a control along the way uppercases RFC + r = self.client.post(url,dict( + document_name="imaginary-new3",title="A new imaginary status change", + create_in_state=state_strpk,notify='ipu@ietf.org',new_relation_row_blah="RFC9999", + statchg_relation_row_blah="tois") + ) + self.assertEqual(r.status_code, 302) + status_change = Document.objects.get(name='status-change-imaginary-new3') + self.assertTrue(status_change.relateddocument_set.filter(relationship__slug='tois',target__name='rfc9999')) + def test_change_state(self): @@ -289,7 +299,19 @@ def test_edit_lc(self): self.assertEqual(r.status_code,200) self.assertContains(r, 'RFC9999 from Proposed Standard to Internet Standard') self.assertContains(r, 'RFC9998 from Informational to Historic') - + q = PyQuery(r.content) + self.assertEqual(len(q("button[name='send_last_call_request']")), 1) + + # Make sure request LC isn't offered with no responsible AD. + doc.ad = None + doc.save_with_history([DocEventFactory(doc=doc)]) + r = self.client.get(url) + self.assertEqual(r.status_code,200) + q = PyQuery(r.content) + self.assertEqual(len(q("button[name='send_last_call_request']")), 0) + doc.ad = Person.objects.get(name='Ad No2') + doc.save_with_history([DocEventFactory(doc=doc)]) + # request last call messages_before = len(outbox) r = self.client.post(url,dict(last_call_text='stuff',send_last_call_request='Save+and+Request+Last+Call')) diff --git a/ietf/doc/views_status_change.py b/ietf/doc/views_status_change.py index ec914eebeb..5f9b6090fc 100644 --- a/ietf/doc/views_status_change.py +++ b/ietf/doc/views_status_change.py @@ -418,7 +418,7 @@ def clean_helper(form, formtype): rfc_fields = {} status_fields={} for k in sorted(form.data.keys()): - v = form.data[k] + v = form.data[k].lower() if k.startswith('new_relation_row'): if re.match(r'\d{1,4}',v): v = 'rfc'+v @@ -685,7 +685,7 @@ def last_call(request, name): form = LastCallTextForm(initial=dict(last_call_text=escape(last_call_event.text))) if request.method == 'POST': - if "save_last_call_text" in request.POST or "send_last_call_request" in request.POST: + if "save_last_call_text" in request.POST or ("send_last_call_request" in request.POST and status_change.ad is not None): form = LastCallTextForm(request.POST) if form.is_valid(): events = [] diff --git a/ietf/templates/doc/status_change/last_call.html b/ietf/templates/doc/status_change/last_call.html index c81792e82a..2f1f9666a9 100644 --- a/ietf/templates/doc/status_change/last_call.html +++ b/ietf/templates/doc/status_change/last_call.html @@ -11,6 +11,11 @@


{{ doc }}

+ {% if doc.ad is None %} +
+ A responsible AD must be set before last call can be requested. +
+ {% endif %}
{% csrf_token %} {% bootstrap_form last_call_form %} @@ -18,15 +23,17 @@

class="btn btn-primary" name="save_last_call_text" value="Save Last Call Text">Save text - - {% if user|has_role:"Secretariat" %} - Issue last call + {% if doc.ad is not None %} + + {% if user|has_role:"Secretariat" %} + Issue last call + {% endif %} {% endif %} + +{% endblock %} +{% block js %} + +{% endblock %} \ No newline at end of file diff --git a/ietf/templates/doc/statement/statement_template.md b/ietf/templates/doc/statement/statement_template.md new file mode 100644 index 0000000000..cc311530ec --- /dev/null +++ b/ietf/templates/doc/statement/statement_template.md @@ -0,0 +1 @@ +Replace this with the content of the statement in markdown source diff --git a/ietf/templates/doc/statement/upload_content.html b/ietf/templates/doc/statement/upload_content.html new file mode 100644 index 0000000000..712c44043d --- /dev/null +++ b/ietf/templates/doc/statement/upload_content.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2023, All Rights Reserved #} +{% load origin django_bootstrap5 static %} +{% block title %}Upload new revision: {{ doc.name }}{% endblock %} +{% block content %} + {% origin %} +

+ Upload New Revision +
+ {{ doc.name }} +

+
+ {% csrf_token %} + {% bootstrap_form form layout="horizontal" %} + + Back +
+{% endblock %} +{% block js %} + +{% endblock %} \ No newline at end of file diff --git a/ietf/templates/group/statements.html b/ietf/templates/group/statements.html new file mode 100644 index 0000000000..4e0fc61532 --- /dev/null +++ b/ietf/templates/group/statements.html @@ -0,0 +1,40 @@ +{% extends "group/group_base.html" %} +{# Copyright The IETF Trust 2023, All Rights Reserved #} +{% load origin %} +{% load ietf_filters person_filters textfilters %} +{% load static %} +{% block pagehead %} + +{% endblock %} +{% block group_content %} + {% origin %} +

{{group.acronym|upper}} Statements

+ {% if request.user|has_role:"Secretariat" %} + + {% endif %} + + + + + + + + + {% for statement in statements %} + + + + + {% endfor %} + +
DateStatement
{{ statement.published|date:"Y-m-d" }}{{statement.title}}
+{% endblock %} +{% block js %} + +{% endblock %} \ No newline at end of file diff --git a/package.json b/package.json index 30323c6be6..1d8365ce08 100644 --- a/package.json +++ b/package.json @@ -148,6 +148,7 @@ "ietf/static/js/upcoming.js", "ietf/static/js/upload-material.js", "ietf/static/js/upload_bofreq.js", + "ietf/static/js/upload_statement.js", "ietf/static/js/zxcvbn.js" ] }, From ab0b8e12aa5b80212b22d4f99eff08227e468983 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Sun, 23 Jul 2023 13:56:49 -0700 Subject: [PATCH 20/42] feat: include submitter in email about submitted slides (#6033) * feat: include submitter in email about submitted slides fixes #6031 * chore: remove unintended whitespace change --- .../migrations/0002_slidesubmitter.py | 31 +++++++++++++++++++ ietf/meeting/views.py | 2 +- ietf/name/fixtures/names.json | 26 +++++++++++----- 3 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 ietf/mailtrigger/migrations/0002_slidesubmitter.py diff --git a/ietf/mailtrigger/migrations/0002_slidesubmitter.py b/ietf/mailtrigger/migrations/0002_slidesubmitter.py new file mode 100644 index 0000000000..394c7d92ce --- /dev/null +++ b/ietf/mailtrigger/migrations/0002_slidesubmitter.py @@ -0,0 +1,31 @@ +# Copyright The IETF Trust 2023, All Rights Reserved + +from django.db import migrations + +def forward(apps, schema_editor): + MailTrigger = apps.get_model("mailtrigger", "MailTrigger") + Recipient = apps.get_model("mailtrigger", "Recipient") + r = Recipient.objects.create( + slug="slides_proposer", + desc="Person who proposed slides", + template="{{ proposer.email }}" + ) + mt = MailTrigger.objects.get(slug="slides_proposed") + mt.cc.add(r) + +def reverse(apps, schema_editor): + MailTrigger = apps.get_model("mailtrigger", "MailTrigger") + Recipient = apps.get_model("mailtrigger", "Recipient") + mt = MailTrigger.objects.get(slug="slides_proposed") + r = Recipient.objects.get(slug="slides_proposer") + mt.cc.remove(r) + r.delete() + +class Migration(migrations.Migration): + dependencies = [ + ("mailtrigger", "0001_initial"), + ] + + operations = [ + migrations.RunPython(forward, reverse) + ] diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index ddd3cd2d1d..18f0f73cbb 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -2887,7 +2887,7 @@ def propose_session_slides(request, session_id, num): submission.filename = filename submission.save() - (to, cc) = gather_address_lists('slides_proposed', group=session.group).as_strings() + (to, cc) = gather_address_lists('slides_proposed', group=session.group, proposer=request.user.person).as_strings() msg_txt = render_to_string("meeting/slides_proposed.txt", { "to": to, "cc": cc, diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index d073991af4..f44a446e2b 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -5590,7 +5590,9 @@ }, { "fields": { - "cc": [], + "cc": [ + "slides_proposer" + ], "desc": "Recipients when slides are proposed for a given session", "to": [ "group_chairs", @@ -6381,6 +6383,14 @@ "model": "mailtrigger.recipient", "pk": "session_requests" }, + { + "fields": { + "desc": "Person who proposed slides", + "template": "{{ proposer.email }}" + }, + "model": "mailtrigger.recipient", + "pk": "slides_proposer" + }, { "fields": { "desc": "The managers of any related streams", @@ -13224,7 +13234,7 @@ "desc": "Flipchars", "name": "Flipcharts", "order": 0, - "used": true + "used": false }, "model": "name.roomresourcename", "pk": "flipcharts" @@ -13274,7 +13284,7 @@ "desc": "Experimental Room Setup (U-Shape and classroom, subject to availability)", "name": "Experimental Room Setup (U-Shape and classroom)", "order": 0, - "used": true + "used": false }, "model": "name.roomresourcename", "pk": "u-shape" @@ -16429,7 +16439,7 @@ "fields": { "command": "xym", "switch": "--version", - "time": "2023-06-21T07:09:38.578Z", + "time": "2023-07-17T07:09:47.664Z", "used": true, "version": "xym 0.7.0" }, @@ -16440,7 +16450,7 @@ "fields": { "command": "pyang", "switch": "--version", - "time": "2023-06-21T07:09:38.928Z", + "time": "2023-07-17T07:09:48.075Z", "used": true, "version": "pyang 2.5.3" }, @@ -16451,7 +16461,7 @@ "fields": { "command": "yanglint", "switch": "--version", - "time": "2023-06-21T07:09:38.948Z", + "time": "2023-07-17T07:09:48.104Z", "used": true, "version": "yanglint SO 1.9.2" }, @@ -16462,9 +16472,9 @@ "fields": { "command": "xml2rfc", "switch": "--version", - "time": "2023-06-21T07:09:39.903Z", + "time": "2023-07-17T07:09:49.075Z", "used": true, - "version": "xml2rfc 3.17.3" + "version": "xml2rfc 3.17.4" }, "model": "utils.versioninfo", "pk": 4 From f124af8c2d68b716448fdf28df56252de9ed301e Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Sun, 23 Jul 2023 18:45:19 -0400 Subject: [PATCH 21/42] chore(dev): update .vscode/settings.json with new taskExplorer settings --- .vscode/settings.json | 115 +++++++++++++++++++++--------------------- 1 file changed, 58 insertions(+), 57 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 3324e6cb76..b0ceba5c9d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,59 +1,60 @@ { - "taskExplorer.exclude": [ - "**/.vscode-test/**", - "**/bin/**", - "**/build/**", - "**/CompiledOutput/**", - "**/dist/**", - "**/doc/**", - "**/ext/**", - "**/out/**", - "**/output/**", - "**/packages/**", - "**/release/**", - "**/releases/**", - "**/samples/**", - "**/sdks/**", - "**/static/**", - "**/target/**", - "**/test/**", - "**/third_party/**", - "**/vendor/**", - "**/work/**", - "/workspace/bootstrap/nuget/MyGet.ps1" - ], - "taskExplorer.enableAnt": false, - "taskExplorer.enableAppPublisher": false, - "taskExplorer.enablePipenv": false, - "taskExplorer.enableBash": false, - "taskExplorer.enableBatch": false, - "taskExplorer.enableGradle": false, - "taskExplorer.enableGrunt": false, - "taskExplorer.enableGulp": false, - "taskExplorer.enablePerl": false, - "taskExplorer.enableMake": false, - "taskExplorer.enableMaven": false, - "taskExplorer.enableNsis": false, - "taskExplorer.enableNpm": false, - "taskExplorer.enablePowershell": false, - "taskExplorer.enablePython": false, - "taskExplorer.enableRuby": false, - "taskExplorer.enableTsc": false, - "taskExplorer.enableWorkspace": true, - "taskExplorer.enableExplorerView": false, - "taskExplorer.enableSideBar": true, - "search.exclude": { - "**/.yarn": true, - "**/.pnp.*": true - }, - "eslint.nodePath": ".yarn/sdks", - "eslint.validate": [ - "javascript", - "javascriptreact", - "vue" - ], - "python.linting.pylintArgs": ["--load-plugins", "pylint_django"], - "python.testing.pytestEnabled": false, - "python.testing.unittestEnabled": false, - "python.linting.enabled": true + "taskExplorer.exclude": [ + "**/.vscode-test/**", + "**/bin/**", + "**/build/**", + "**/CompiledOutput/**", + "**/dist/**", + "**/doc/**", + "**/ext/**", + "**/out/**", + "**/output/**", + "**/packages/**", + "**/release/**", + "**/releases/**", + "**/samples/**", + "**/sdks/**", + "**/static/**", + "**/target/**", + "**/test/**", + "**/third_party/**", + "**/vendor/**", + "**/work/**", + "/workspace/bootstrap/nuget/MyGet.ps1" + ], + "taskExplorer.enabledTasks": { + "ant": false, + "bash": false, + "batch": false, + "composer": false, + "gradle": false, + "grunt": false, + "gulp": false, + "make": false, + "maven": false, + "npm": false, + "perl": false, + "pipenv": false, + "powershell": false, + "python": false, + "ruby": false, + "tsc": false + }, + "taskExplorer.enableExplorerView": false, + "taskExplorer.enableSideBar": true, + "taskExplorer.showLastTasks": false, + "search.exclude": { + "**/.yarn": true, + "**/.pnp.*": true + }, + "eslint.nodePath": ".yarn/sdks", + "eslint.validate": [ + "javascript", + "javascriptreact", + "vue" + ], + "python.linting.pylintArgs": ["--load-plugins", "pylint_django"], + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": false, + "python.linting.enabled": true } From 101963d3bd999ed52a3cd648357efaafe190b4df Mon Sep 17 00:00:00 2001 From: Kesara Rathnayake Date: Tue, 25 Jul 2023 05:06:28 +1200 Subject: [PATCH 22/42] fix: Add editorial stream to proceedings (#6027) * fix: Add editorial stream to proceedings Fixes #5717 * fix: Move editorial stream after the irtf in proceedings --- ietf/meeting/tests_views.py | 22 +++++++++++++++++ ietf/meeting/views.py | 5 ++++ ietf/templates/meeting/proceedings.html | 32 ++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 50323a5433..5f68ba1fd4 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -7632,6 +7632,13 @@ def _assertProceedingsMaterialsDisplayed(self, response, meeting): 'Correct title and link for each ProceedingsMaterial should appear in the correct order' ) + def _assertGroupSessions(self, response, meeting): + """Checks that group/sessions are present""" + pq = PyQuery(response.content) + sections = ["plenaries", "gen", "iab", "editorial", "irtf", "training"] + for section in sections: + self.assertEqual(len(pq(f"#{section}")), 1, f"{section} section should exists in proceedings") + def test_proceedings(self): """Proceedings should be displayed correctly @@ -7645,6 +7652,20 @@ def test_proceedings(self): SessionPresentationFactory(document__type_id='recording',session=session) SessionPresentationFactory(document__type_id='recording',session=session,document__title="Audio recording for tests") + # Add various group sessions + groups = [] + parent_groups = [ + GroupFactory.create(type_id="area", acronym="gen"), + GroupFactory.create(acronym="iab"), + GroupFactory.create(acronym="irtf"), + ] + for parent in parent_groups: + groups.append(GroupFactory.create(parent=parent)) + for acronym in ["rsab", "edu"]: + groups.append(GroupFactory.create(acronym=acronym)) + for group in groups: + SessionFactory(meeting=meeting, group=group) + self.write_materials_files(meeting, session) self._create_proceedings_materials(meeting) @@ -7691,6 +7712,7 @@ def test_proceedings(self): # configurable contents self._assertMeetingHostsDisplayed(r, meeting) self._assertProceedingsMaterialsDisplayed(r, meeting) + self._assertGroupSessions(r, meeting) def test_named_session(self): """Session with a name should appear separately in the proceedings""" diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 18f0f73cbb..d7b072bfc4 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -3779,6 +3779,10 @@ def proceedings(request, num=None): sessions.filter(group__parent__acronym = 'iab') .exclude(current_status='notmeet') ) + editorial, _ = organize_proceedings_sessions( + sessions.filter(group__acronym__in=['rsab','rswg']) + .exclude(current_status='notmeet') + ) ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym='edu').order_by('group__parent__acronym', 'group__acronym') ietf_areas = [] @@ -3798,6 +3802,7 @@ def proceedings(request, num=None): 'training': training, 'irtf': irtf, 'iab': iab, + 'editorial': editorial, 'ietf_areas': ietf_areas, 'cut_off_date': cut_off_date, 'cor_cut_off_date': cor_cut_off_date, diff --git a/ietf/templates/meeting/proceedings.html b/ietf/templates/meeting/proceedings.html index 00ee18df62..b5d4a6198a 100644 --- a/ietf/templates/meeting/proceedings.html +++ b/ietf/templates/meeting/proceedings.html @@ -206,10 +206,40 @@

{% endif %} + + {% if editorial %} +

Editorial Stream

+ + + + + + + + + + + + {% for entry in editorial %} + {% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %} + {% endfor %} + +
+ Group + + Artifacts + + Recordings + + Slides + + Internet-Drafts +
+ {% endif %} {% endif %} {% endcache %} {% endblock %} {% block js %} -{% endblock %} \ No newline at end of file +{% endblock %} From e1e15da398ff49327b82aa3d8632c0d788f1441a Mon Sep 17 00:00:00 2001 From: Kesara Rathnayake Date: Wed, 26 Jul 2023 02:50:49 +1200 Subject: [PATCH 23/42] fix: Add editorial stream to meeting materials (#6047) Fixes #6042 --- ietf/meeting/tests_views.py | 18 ++++++++++++++ ietf/meeting/views.py | 9 ++++--- ietf/templates/meeting/materials.html | 36 +++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 5f68ba1fd4..9609fad3df 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -648,6 +648,20 @@ def do_test_materials(self, meeting, session): self.assertFalse(row.find("a:contains(\"Bad Slideshow\")")) # test with no meeting number in url + # Add various group sessions + groups = [] + parent_groups = [ + GroupFactory.create(type_id="area", acronym="gen"), + GroupFactory.create(acronym="iab"), + GroupFactory.create(acronym="irtf"), + ] + for parent in parent_groups: + groups.append(GroupFactory.create(parent=parent)) + for acronym in ["rsab", "edu"]: + groups.append(GroupFactory.create(acronym=acronym)) + for group in groups: + SessionFactory(meeting=meeting, group=group) + self.write_materials_files(meeting, session) url = urlreverse("ietf.meeting.views.materials", kwargs=dict()) r = self.client.get(url) self.assertEqual(r.status_code, 200) @@ -657,6 +671,10 @@ def do_test_materials(self, meeting, session): self.assertTrue(row.find('a:contains("Minutes")')) self.assertTrue(row.find('a:contains("Slideshow")')) self.assertFalse(row.find("a:contains(\"Bad Slideshow\")")) + # test for different sections + sections = ["plenaries", "gen", "iab", "editorial", "irtf", "training"] + for section in sections: + self.assertEqual(len(q(f"#{section}")), 1, f"{section} section should exists in proceedings") # test with a loggged-in wg chair self.client.login(username="marschairman", password="marschairman+password") diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index d7b072bfc4..e0364e63b5 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -159,18 +159,19 @@ def materials(request, num=None): irtf = sessions.filter(group__parent__acronym = 'irtf') training = sessions.filter(group__acronym__in=['edu','iaoc'], type_id__in=['regular', 'other', ]) iab = sessions.filter(group__parent__acronym = 'iab') + editorial = sessions.filter(group__acronym__in=['rsab','rswg']) - session_pks = [s.pk for ss in [plenaries, ietf, irtf, training, iab] for s in ss] + session_pks = [s.pk for ss in [plenaries, ietf, irtf, training, iab, editorial] for s in ss] other = sessions.filter(type__in=['regular'], group__type__features__has_meetings=True).exclude(pk__in=session_pks) - for topic in [plenaries, ietf, training, irtf, iab]: + for topic in [plenaries, ietf, training, irtf, iab, editorial]: for event in topic: date_list = [] for slide_event in event.all_meeting_slides(): date_list.append(slide_event.time) for agenda_event in event.all_meeting_agendas(): date_list.append(agenda_event.time) if date_list: setattr(event, 'last_update', sorted(date_list, reverse=True)[0]) - for session_list in [plenaries, ietf, training, irtf, iab, other]: + for session_list in [plenaries, ietf, training, irtf, iab, editorial, other]: for session in session_list: session.past_cutoff_date = past_cutoff_date @@ -183,6 +184,7 @@ def materials(request, num=None): irtf, _ = organize_proceedings_sessions(irtf) training, _ = organize_proceedings_sessions(training) iab, _ = organize_proceedings_sessions(iab) + editorial, _ = organize_proceedings_sessions(editorial) other, _ = organize_proceedings_sessions(other) ietf_areas = [] @@ -202,6 +204,7 @@ def materials(request, num=None): 'training': training, 'irtf': irtf, 'iab': iab, + 'editorial': editorial, 'other': other, 'cut_off_date': cut_off_date, 'cor_cut_off_date': cor_cut_off_date, diff --git a/ietf/templates/meeting/materials.html b/ietf/templates/meeting/materials.html index 667ce72353..ff4964a973 100644 --- a/ietf/templates/meeting/materials.html +++ b/ietf/templates/meeting/materials.html @@ -190,6 +190,42 @@

{% endif %} + + {% if editorial %} +

Editorial Stream

+ + + + + + + + + + {% if user|has_role:"Secretariat" or user_groups %} + + {% endif %} + + + + {% for entry in editorial %} + {% include "meeting/group_materials.html" %} + {% endfor %} + +
+ Group + + Agenda + + Minutes + + Slides + + Internet-Drafts + + Updated +
+ {% endif %} {% if other %}

Other Miscellaneous other sessions From b24dd4427bcd4f27cac90d6a20c24972fd12d497 Mon Sep 17 00:00:00 2001 From: Tero Kivinen Date: Tue, 25 Jul 2023 10:59:45 -0400 Subject: [PATCH 24/42] fix: Shows requested reviews for doc fixes (#6022) * Fix: Shows requested reviews for doc * Changed template includes to only give required variables to them. --- ietf/doc/views_doc.py | 4 +++- ietf/review/utils.py | 5 +++++ ietf/templates/doc/document_info.html | 5 ++++- ietf/templates/doc/review_assignment_summary.html | 4 ++-- ietf/templates/doc/review_request_summary.html | 9 +++++++++ 5 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 ietf/templates/doc/review_request_summary.html diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 6adbe15326..5a20bdc120 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -79,7 +79,7 @@ from ietf.meeting.models import Session from ietf.meeting.utils import group_sessions, get_upcoming_manageable_sessions, sort_sessions, add_event_info_to_session_qs from ietf.review.models import ReviewAssignment -from ietf.review.utils import can_request_review_of_doc, review_assignments_to_list_for_docs +from ietf.review.utils import can_request_review_of_doc, review_assignments_to_list_for_docs, review_requests_to_list_for_docs from ietf.review.utils import no_review_from_teams_on_doc from ietf.utils import markup_txt, log, markdown from ietf.utils.draft import PlaintextDraft @@ -501,6 +501,7 @@ def document_main(request, name, rev=None, document_html=False): started_iesg_process = doc.latest_event(type="started_iesg_process") review_assignments = review_assignments_to_list_for_docs([doc]).get(doc.name, []) + review_requests = review_requests_to_list_for_docs([doc]).get(doc.name, []) no_review_from_teams = no_review_from_teams_on_doc(doc, rev or doc.rev) exp_comment = doc.latest_event(IanaExpertDocEvent,type="comment") @@ -616,6 +617,7 @@ def document_main(request, name, rev=None, document_html=False): actions=actions, presentations=presentations, review_assignments=review_assignments, + review_requests=review_requests, no_review_from_teams=no_review_from_teams, due_date=due_date, diff_revisions=diff_revisions diff --git a/ietf/review/utils.py b/ietf/review/utils.py index 4563a82fd1..058e8cee93 100644 --- a/ietf/review/utils.py +++ b/ietf/review/utils.py @@ -79,6 +79,11 @@ def review_assignments_to_list_for_docs(docs): return extract_revision_ordered_review_assignments_for_documents_and_replaced(assignment_qs, doc_names) +def review_requests_to_list_for_docs(docs): + review_requests_qs = ReviewRequest.objects.filter(Q(state_id='requested')) + doc_names = [d.name for d in docs] + return extract_revision_ordered_review_requests_for_documents_and_replaced(review_requests_qs, doc_names) + def augment_review_requests_with_events(review_reqs): req_dict = { r.pk: r for r in review_reqs } for e in ReviewRequestDocEvent.objects.filter(review_request__in=review_reqs, type__in=["assigned_review_request", "closed_review_request"]).order_by("time"): diff --git a/ietf/templates/doc/document_info.html b/ietf/templates/doc/document_info.html index e61d85a2bd..2807d275df 100644 --- a/ietf/templates/doc/document_info.html +++ b/ietf/templates/doc/document_info.html @@ -349,7 +349,10 @@ {% for review_assignment in review_assignments %} - {% include "doc/review_assignment_summary.html" with current_doc_name=doc.name current_rev=doc.rev %} + {% include "doc/review_assignment_summary.html" with current_doc_name=doc.name current_rev=doc.rev review_assignment=review_assignment only %} + {% endfor %} + {% for review_request in review_requests %} + {% include "doc/review_request_summary.html" with review_request=review_request only %} {% endfor %} {% if no_review_from_teams %} {% for team in no_review_from_teams %} diff --git a/ietf/templates/doc/review_assignment_summary.html b/ietf/templates/doc/review_assignment_summary.html index b45c27da6a..ed5c4bfdd4 100644 --- a/ietf/templates/doc/review_assignment_summary.html +++ b/ietf/templates/doc/review_assignment_summary.html @@ -20,9 +20,9 @@ {% else %} - {{ review_assignment.review_request.team.acronym|upper }} {{ review_assignment.review_request.type.name }} Review + {{ review_assignment.review_request.team.acronym|upper }} {{ review_assignment.review_request.type.name }} Review due {{ review_assignment.review_request.deadline|date:"Y-m-d" }} - Incomplete, due {{ review_assignment.review_request.deadline|date:"Y-m-d" }} + Incomplete {% endif %} \ No newline at end of file diff --git a/ietf/templates/doc/review_request_summary.html b/ietf/templates/doc/review_request_summary.html new file mode 100644 index 0000000000..efeb4a030e --- /dev/null +++ b/ietf/templates/doc/review_request_summary.html @@ -0,0 +1,9 @@ + From 593bdb465d78e145e4855cfc7e5112f52b28b181 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Tue, 25 Jul 2023 12:15:39 -0700 Subject: [PATCH 25/42] feat: allow openId to choose an unactive email if there are none active (#6041) * feat: allow openId to choose an unactive email if there are no active ones * chore: correct typo * chore: rename unactive to inactive --- ietf/ietfauth/utils.py | 2 +- ietf/person/models.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/ietf/ietfauth/utils.py b/ietf/ietfauth/utils.py index 52f582ca86..6fa9cddbcb 100644 --- a/ietf/ietfauth/utils.py +++ b/ietf/ietfauth/utils.py @@ -223,7 +223,7 @@ def is_bofreq_editor(user, doc): def openid_userinfo(claims, user): # Populate claims dict. person = get_object_or_404(Person, user=user) - email = person.email() + email = person.email_allowing_inactive() if person.photo: photo_url = person.cdn_photo_url() else: diff --git a/ietf/person/models.py b/ietf/person/models.py index a09656b810..22c63d4a0f 100644 --- a/ietf/person/models.py +++ b/ietf/person/models.py @@ -145,6 +145,14 @@ def email(self): e = self.email_set.filter(active=True).order_by("-time").first() self._cached_email = e return self._cached_email + def email_allowing_inactive(self): + if not hasattr(self, "_cached_email_allowing_inactive"): + e = self.email() + if not e: + e = self.email_set.order_by("-time").first() + log.assertion(statement="e is not None", note=f"Person {self.pk} has no Email objects") + self._cached_email_allowing_inactive = e + return self._cached_email_allowing_inactive def email_address(self): e = self.email() if e: From 04df7973d8bd673e23fd3cfa97708f21e87b4751 Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Thu, 27 Jul 2023 17:48:51 +0300 Subject: [PATCH 26/42] fix: Make review table more responsive (#6053) * fix: Improve layout of review table * Progress * Progress * Final changes * Fix tests * Remove fluff * Undo commits --- ietf/group/tests_review.py | 4 +- ietf/templates/group/reviewer_overview.html | 143 ++++++++++--------- ietf/templates/ietfauth/review_overview.html | 8 +- ietf/templates/review/unavailable_table.html | 51 +++++-- 4 files changed, 124 insertions(+), 82 deletions(-) diff --git a/ietf/group/tests_review.py b/ietf/group/tests_review.py index 0e56c9f46f..af374f6765 100644 --- a/ietf/group/tests_review.py +++ b/ietf/group/tests_review.py @@ -212,13 +212,13 @@ def test_reviewer_overview(self): r = self.client.get(url) self.assertEqual(r.status_code, 200) # review team members can see reason for being unavailable - self.assertContains(r, "Availability") + self.assertContains(r, "Available") self.client.login(username="secretary", password="secretary+password") r = self.client.get(url) self.assertEqual(r.status_code, 200) # secretariat can see reason for being unavailable - self.assertContains(r, "Availability") + self.assertContains(r, "Available") # add one closed review with no response and see it is visible review_req2 = ReviewRequestFactory(state_id='completed',team=team) diff --git a/ietf/templates/group/reviewer_overview.html b/ietf/templates/group/reviewer_overview.html index 39ba7606a9..b4b1b6e3d7 100644 --- a/ietf/templates/group/reviewer_overview.html +++ b/ietf/templates/group/reviewer_overview.html @@ -24,7 +24,7 @@

Reviewers

rotation with the next reviewer in the rotation at the top. Rows with darker backgrounds have the following meaning:

-

+

Has already been assigned a document within the given interval.

@@ -44,89 +44,102 @@

Reviewers

{% endif %} {% if reviewers %} - +
- + - - - + + + {% for person in reviewers %} - + + + - - + {% endfor %} diff --git a/ietf/templates/ietfauth/review_overview.html b/ietf/templates/ietfauth/review_overview.html index 65a451c7a2..b33e75421b 100644 --- a/ietf/templates/ietfauth/review_overview.html +++ b/ietf/templates/ietfauth/review_overview.html @@ -101,7 +101,7 @@

Latest closed review assignments

{% if r.due %}{{ r.due }} day{{ r.due|pluralize }}{% endif %} {% endif %} - + From 4e4603215dfdf7376f4152a7709e4f60ec752e2e Mon Sep 17 00:00:00 2001 From: Paul Selkirk Date: Tue, 1 Aug 2023 16:46:04 -0400 Subject: [PATCH 30/42] fix: Clean up view_feedback_pending (#6070) - Remove "Unclassified" column header, which caused misalignment in the table body. - Show the message author - previously displayed as `(None)`. --- ietf/templates/nomcom/view_feedback_pending.html | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/ietf/templates/nomcom/view_feedback_pending.html b/ietf/templates/nomcom/view_feedback_pending.html index 890bf0d759..b086eae297 100644 --- a/ietf/templates/nomcom/view_feedback_pending.html +++ b/ietf/templates/nomcom/view_feedback_pending.html @@ -1,5 +1,5 @@ {% extends "nomcom/nomcom_private_base.html" %} -{# Copyright The IETF Trust 2015, All Rights Reserved #} +{# Copyright The IETF Trust 2015-2023, All Rights Reserved #} {% load origin %} {% load django_bootstrap5 %} {% load static %} @@ -73,10 +73,6 @@

Feedback pending from email list

- - - - {% for legend, t in type_dict.items %} @@ -89,7 +85,6 @@

Feedback pending from email list

- {% for legend, t in type_dict.items %}{% endfor %} @@ -118,7 +113,7 @@

Feedback pending from email list

title="{{ choice.1 }}"> {% endfor %} - +
NextNext ReviewerRecent historyDays since completedSettings +
+
Assigned
+
Deadline
+
State
+
Days for review
+
Document
+
+
Days since reviewSettings
{{ forloop.counter }} - {% person_link person %} - {% if person.settings_url %} - Edit - - {% endif %} + {% person_link person with_email=False %} + +
+ {% if person.settings_url %} + + + + {% endif %} + +
{% if person.latest_reqs %} - - - - - - - - - - - - {% for assn_pk, req_pk, doc_name, reviewed_rev, assigned_time, deadline, state, assignment_to_closure_days in person.latest_reqs %} - - - - - - - - {% endfor %} - -
AssignedDeadlineStateReview timeDocument
- {{ assigned_time|date }} - - {{ deadline|date }} - - {{ state.name }} - - {% if assignment_to_closure_days != None %} - {{ assignment_to_closure_days }} day{{ assignment_to_closure_days|pluralize }} - {% endif %} - - {{ doc_name }} - {% if reviewed_rev %}-{{ reviewed_rev }}{% endif %} -
+ {% for assn_pk, req_pk, doc_name, reviewed_rev, assigned_time, deadline, state, assignment_to_closure_days in person.latest_reqs %} +
+
+ {{ assigned_time|date|split:"-"|join:"-" }} +
+ +
+ {{ state.name }} +
+
+ {% if assignment_to_closure_days != None %} + {{ assignment_to_closure_days }} + {% endif %} +
+
+ {{ doc_name }}{% if reviewed_rev %}-{{ reviewed_rev }}{% endif %} +
+
+ {% endfor %} {% endif %}
+ + {% if person.days_since_completed_review != 9999 %} {{ person.days_since_completed_review }} {% endif %} - {% if person.settings.min_interval %} - {{ person.settings.get_min_interval_display }} -
- {% endif %} - {% if person.settings.skip_next %} - Skip: {{ person.settings.skip_next }} -
- {% endif %} - {% if person.settings.filter_re %} - Filter: {{ person.settings.filter_re|truncatechars:15 }} -
- {% endif %} - {% if person.unavailable_periods %} - {% include "review/unavailable_table.html" with unavailable_periods=person.unavailable_periods %} - {% endif %} + +
+ {% include "review/unavailable_table.html" with person=person unavailable_periods=person.unavailable_periods %} +
- {{ r.state.name }} + {{ r.state.name }} {% if r.result %}{{ r.result.name }}{% endif %} @@ -186,10 +186,10 @@

- - @@ -208,7 +208,7 @@

Skip next assignments

diff --git a/ietf/templates/review/unavailable_table.html b/ietf/templates/review/unavailable_table.html index ea34bf2cbe..573f1c3ff4 100644 --- a/ietf/templates/review/unavailable_table.html +++ b/ietf/templates/review/unavailable_table.html @@ -1,25 +1,54 @@ -
+ Setting + Value
- {{ t.reviewer_settings.skip_next }} + {{ t.reviewer_settings.skip_next|yesno }}
+{% load origin %} +{% origin %} +
+ + + + + + + {% if person %} + {% if person.settings.min_interval %} + + + + + {% endif %} + {% if person.settings.skip_next %} + + + + + {% endif %} + {% if person.settings.filter_re %} + + + + + {% endif %} + {% endif %} {% for p in unavailable_periods %} - - + + - - + - - + + + {% if p.reason %} - - + + + {% endif %} {% endfor %}
Can review:{{ person.settings.get_min_interval_display }}
Skip next:{{ person.settings.skip_next|yesno }}
Filter:{{ person.settings.filter_re }}
Period:{{ p.state }}Period:{{ p.state }}
Dates: - {% if p.start_date or p.end_date %}{{ p.start_date|default:"∞" }} -{% endif %} - {{ p.end_date|default:"∞" }} + Dates: + {% if p.start_date or p.end_date %}{{ p.start_date|default:"∞" }}-{% endif %}{{ p.end_date|default:"∞" }}
Availability:{{ p.get_availability_display }}Available:{{ p.get_availability_display }}
Reason:{{ p.reason }}Reason:{{ p.reason }}
\ No newline at end of file From 416ffb0d6f7815b9b8b927efdd32195bd5121e95 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Thu, 27 Jul 2023 19:15:48 -0400 Subject: [PATCH 27/42] ci: add --validate-html-harder to tests --- .github/workflows/ci-run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-run-tests.yml b/.github/workflows/ci-run-tests.yml index e8e9fe324b..a2f2e3d952 100644 --- a/.github/workflows/ci-run-tests.yml +++ b/.github/workflows/ci-run-tests.yml @@ -45,7 +45,7 @@ jobs: exit 1 fi echo "Running tests..." - ./ietf/manage.py test --settings=settings_postgrestest + ./ietf/manage.py test --validate-html-harder --settings=settings_postgrestest coverage xml - name: Upload Coverage Results to Codecov From ff07286404311f97481fb22a1b89a10b72d6d295 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Fri, 28 Jul 2023 14:19:51 -0400 Subject: [PATCH 28/42] ci: add --validate-html-harder to build.yml workflow --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4e1bac3c5..8e0cc2acff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -137,9 +137,9 @@ jobs: echo "Running tests..." if [[ "x${{ github.event.inputs.ignoreLowerCoverage }}" == "xtrue" ]]; then echo "Lower coverage failures will be ignored." - ./ietf/manage.py test --settings=settings_postgrestest --ignore-lower-coverage + ./ietf/manage.py test --validate-html-harder --settings=settings_postgrestest --ignore-lower-coverage else - ./ietf/manage.py test --settings=settings_postgrestest + ./ietf/manage.py test --validate-html-harder --settings=settings_postgrestest fi coverage xml From 22624a3f38337ff1e0c6ee12fb428887f895f3b4 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Tue, 1 Aug 2023 16:53:45 -0300 Subject: [PATCH 29/42] fix: Set colspan to actual number of columns (#6069) --- ietf/templates/meeting/requests.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/templates/meeting/requests.html b/ietf/templates/meeting/requests.html index 7c70ee0b02..8478f39924 100644 --- a/ietf/templates/meeting/requests.html +++ b/ietf/templates/meeting/requests.html @@ -144,7 +144,7 @@

{% if not forloop.first %}

{{ session.current_status_name|capfirst }}{{ session.current_status_name|capfirst }}
UUnclassified
{{ legend }}
DateU{{ legend }}Author Subject{% person_link form.instance.author %}{{ form.instance.author }} {{ form.instance.subject }} - {% if iesg_state.slug != 'idexists' and can_edit %} + {% if iesg_state.slug != 'idexists' and iesg_state.slug != 'dead' and can_edit %} Edit From 399b7b99942680caac95112866838bffb0cfa588 Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Tue, 8 Aug 2023 00:53:19 +0300 Subject: [PATCH 33/42] fix: Correctly order "last call requested" column in the IESG dashboard (#6079) --- ietf/doc/views_search.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ietf/doc/views_search.py b/ietf/doc/views_search.py index 8b66c7c8d4..d9ff119291 100644 --- a/ietf/doc/views_search.py +++ b/ietf/doc/views_search.py @@ -497,6 +497,7 @@ def ad_workload(request): [ ("Publication Requested Internet-Draft", False), ("AD Evaluation Internet-Draft", False), + ("Last Call Requested Internet-Draft", True), ("In Last Call Internet-Draft", True), ("Waiting for Writeup Internet-Draft", False), ("IESG Evaluation - Defer Internet-Draft", False), @@ -532,6 +533,7 @@ def ad_workload(request): [ ("Publication Requested Status Change", False), ("AD Evaluation Status Change", False), + ("Last Call Requested Status Change", True), ("In Last Call Status Change", True), ("Waiting for Writeup Status Change", False), ("IESG Evaluation Status Change", True), From b327a27736472b94686ec4a07f7e44f5982c4dbc Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Tue, 8 Aug 2023 11:44:05 -0400 Subject: [PATCH 34/42] ci: update dev sandbox init script to start memcached --- dev/deploy-to-container/start.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dev/deploy-to-container/start.sh b/dev/deploy-to-container/start.sh index 2257aaf11c..8c9c624618 100644 --- a/dev/deploy-to-container/start.sh +++ b/dev/deploy-to-container/start.sh @@ -27,6 +27,9 @@ if [ -n "$PGHOST" ]; then psql -U django -h $PGHOST -d datatracker -v ON_ERROR_STOP=1 -c '\x' -c 'ALTER USER django set search_path=datatracker,public;' fi +echo "Starting memcached..." +/usr/bin/memcached -d + echo "Running Datatracker checks..." ./ietf/manage.py check From 06c9f06d5504f0fd75dca1609fa1960e7b286444 Mon Sep 17 00:00:00 2001 From: Paul Selkirk Date: Tue, 8 Aug 2023 13:33:17 -0400 Subject: [PATCH 35/42] feat: Reclassify nomcom feedback (#6002) * fix: Clean up view_feedback_pending - Remove "Unclassified" column header, which caused misalignment in the table body. - Show the message author - previously displayed as `(None)`. * feat: Reclassify nomcom feedback (#4669) - There's a new `Chair/Advisor Tasks` menu item `Reclassify feedback`. - I overloaded `view_feedback*` URLs with a `?reclassify` parameter. - This adds a checkbox to each feedback message, and a `Reclassify` button at the bottom of each feedback page. - "Reclassifying" basically de-classifies the feedback, and punts it back to the "Pending emails" view for reclassification. - If a feedback has been applied to multiple nominees, declassifying it from one nominee removes it from all. * fix: Remove unused local variables * fix: Fix some missing and mis-nested html * test: Add tests for reclassifying feedback * refactor: Substantial redesign of feedback reclassification - Break out reclassify_feedback* as their own URLs and views, and revert changes to view_feedback*.html. - Replace checkboxes with a Reclassify button on each message. * fix: Remember to clear the feedback associations when reclassifying * feat: Add an 'Overcome by events' feedback type * refactor: When invoking reclassification from a view-feedback page, load the corresponding reclassify-feedback page * fix: De-conflict migration with 0004_statements Also change the coding style to match, and add a reverse migration. * fix: Fix a test case to account for new feedback type * fix: 842e730 broke the Back button * refactor: Reclassify feedback directly instead of putting it back in the work queue * fix: Adjust tests to new workflow * refactor: Further refine reclassification to avoid redirects * refactor: Impose a FeedbackTypeName ordering Also add FeedbackTypeName.legend field, rather than synthesizing it every time we classify or reclassify feedback. In the reclassification forms, only show the relevant feedback types. * refactor: Merge reclassify_feedback_* back into view_feedback_* This means the "Reclassify" button is always present, but eliminates some complexity. * refactor: Add filter(used=True) on FeedbackTypeName querysets * refactor: Add the new FeedbackTypeName to the reclassification success message * fix: Secure reclassification against rogue nomcom members --- ietf/name/fixtures/names.json | 26 ++++- .../0005_feedbacktypename_schema.py | 20 ++++ .../migrations/0006_feedbacktypename_data.py | 36 ++++++ ietf/name/models.py | 1 + ietf/nomcom/forms.py | 2 +- ietf/nomcom/tests.py | 91 ++++++++++++++- ietf/nomcom/views.py | 105 +++++++++++++----- ietf/settings.py | 4 +- .../nomcom/reclassify_feedback_item.html | 103 +++++++++++++++++ .../nomcom/view_feedback_nominee.html | 21 +++- .../nomcom/view_feedback_pending.html | 6 +- .../templates/nomcom/view_feedback_topic.html | 21 +++- .../nomcom/view_feedback_unrelated.html | 21 +++- 13 files changed, 412 insertions(+), 45 deletions(-) create mode 100644 ietf/name/migrations/0005_feedbacktypename_schema.py create mode 100644 ietf/name/migrations/0006_feedbacktypename_data.py create mode 100644 ietf/templates/nomcom/reclassify_feedback_item.html diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index f44a446e2b..f746231a4a 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -11113,8 +11113,9 @@ { "fields": { "desc": "", + "legend": "C", "name": "Comment", - "order": 0, + "order": 1, "used": true }, "model": "name.feedbacktypename", @@ -11123,8 +11124,9 @@ { "fields": { "desc": "", + "legend": "J", "name": "Junk", - "order": 0, + "order": 5, "used": true }, "model": "name.feedbacktypename", @@ -11133,8 +11135,9 @@ { "fields": { "desc": "", + "legend": "N", "name": "Nomination", - "order": 0, + "order": 2, "used": true }, "model": "name.feedbacktypename", @@ -11143,8 +11146,20 @@ { "fields": { "desc": "", + "legend": "O", + "name": "Overcome by events", + "order": 4, + "used": true + }, + "model": "name.feedbacktypename", + "pk": "obe" + }, + { + "fields": { + "desc": "", + "legend": "Q", "name": "Questionnaire response", - "order": 0, + "order": 3, "used": true }, "model": "name.feedbacktypename", @@ -11153,8 +11168,9 @@ { "fields": { "desc": "", + "legend": "R", "name": "Read", - "order": 0, + "order": 6, "used": true }, "model": "name.feedbacktypename", diff --git a/ietf/name/migrations/0005_feedbacktypename_schema.py b/ietf/name/migrations/0005_feedbacktypename_schema.py new file mode 100644 index 0000000000..cedb129be3 --- /dev/null +++ b/ietf/name/migrations/0005_feedbacktypename_schema.py @@ -0,0 +1,20 @@ +# Copyright The IETF Trust 2023, All Rights Reserved + +from django.db import migrations, models + +class Migration(migrations.Migration): + dependencies = [ + ("name", "0004_statements"), + ] + + operations = [ + migrations.AddField( + model_name="FeedbackTypeName", + name="legend", + field=models.CharField( + default="", + help_text="One-character legend for feedback classification form", + max_length=1, + ), + ), + ] diff --git a/ietf/name/migrations/0006_feedbacktypename_data.py b/ietf/name/migrations/0006_feedbacktypename_data.py new file mode 100644 index 0000000000..f11fca889b --- /dev/null +++ b/ietf/name/migrations/0006_feedbacktypename_data.py @@ -0,0 +1,36 @@ +# Copyright The IETF Trust 2023, All Rights Reserved + +from django.db import migrations + +def forward(apps, schema_editor): + FeedbackTypeName = apps.get_model("name", "FeedbackTypeName") + FeedbackTypeName.objects.create(slug="obe", name="Overcome by events") + for slug, legend, order in ( + ('comment', 'C', 1), + ('nomina', 'N', 2), + ('questio', 'Q', 3), + ('obe', 'O', 4), + ('junk', 'J', 5), + ('read', 'R', 6), + ): + ft = FeedbackTypeName.objects.get(slug=slug) + ft.legend = legend + ft.order = order + ft.save() + +def reverse(apps, schema_editor): + FeedbackTypeName = apps.get_model("name", "FeedbackTypeName") + FeedbackTypeName.objects.filter(slug="obe").delete() + for ft in FeedbackTypeName.objects.all(): + ft.legend = "" + ft.order = 0 + ft.save() + +class Migration(migrations.Migration): + dependencies = [ + ("name", "0005_feedbacktypename_schema"), + ] + + operations = [ + migrations.RunPython(forward, reverse), + ] diff --git a/ietf/name/models.py b/ietf/name/models.py index b9e75e8f99..4eda88afda 100644 --- a/ietf/name/models.py +++ b/ietf/name/models.py @@ -94,6 +94,7 @@ class NomineePositionStateName(NameModel): """Status of a candidate for a position: None, Accepted, Declined""" class FeedbackTypeName(NameModel): """Type of feedback: questionnaires, nominations, comments""" + legend = models.CharField(max_length=1, default="", help_text="One-character legend for feedback classification form") class DBTemplateTypeName(NameModel): """reStructuredText, Plain, Django""" class DraftSubmissionStateName(NameModel): diff --git a/ietf/nomcom/forms.py b/ietf/nomcom/forms.py index ad7bc67c23..919ed6e187 100644 --- a/ietf/nomcom/forms.py +++ b/ietf/nomcom/forms.py @@ -653,7 +653,7 @@ def clean_key(self): class PendingFeedbackForm(forms.ModelForm): - type = forms.ModelChoiceField(queryset=FeedbackTypeName.objects.all().order_by('pk'), widget=forms.RadioSelect, empty_label='Unclassified', required=False) + type = forms.ModelChoiceField(queryset=FeedbackTypeName.objects.all(), widget=forms.RadioSelect, empty_label='Unclassified', required=False) class Meta: model = Feedback diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index bce7e5a214..b68b0beb8c 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -1450,7 +1450,7 @@ def test_feedback_index_totals(self): self.assertEqual(response.status_code,200) q = PyQuery(response.content) r = q('tfoot').eq(0).find('td').contents() - self.assertEqual([a.strip() for a in r], ['1', '1', '1']) + self.assertEqual([a.strip() for a in r], ['1', '1', '1', '0']) class FeedbackLastSeenTests(TestCase): @@ -2863,3 +2863,92 @@ def test_decorate_volunteers_with_qualifications(self): self.assertEqual(v.qualifications,'path_2') if v.person == author_person: self.assertEqual(v.qualifications,'path_3') + +class ReclassifyFeedbackTests(TestCase): + """Tests for feedback reclassification""" + + def setUp(self): + super().setUp() + setup_test_public_keys_dir(self) + nomcom_test_data() + self.nc = NomComFactory.create(**nomcom_kwargs_for_year()) + self.chair = self.nc.group.role_set.filter(name='chair').first().person + self.member = self.nc.group.role_set.filter(name='member').first().person + self.nominee = self.nc.nominee_set.order_by('pk').first() + self.position = self.nc.position_set.first() + self.topic = self.nc.topic_set.first() + + def tearDown(self): + teardown_test_public_keys_dir(self) + super().tearDown() + + def test_reclassify_feedback_nominee(self): + fb = FeedbackFactory.create(nomcom=self.nc,type_id='comment') + fb.positions.add(self.position) + fb.nominees.add(self.nominee) + fb.save() + self.assertEqual(Feedback.objects.comments().count(), 1) + + url = reverse('ietf.nomcom.views.view_feedback_nominee', kwargs={'year':self.nc.year(), 'nominee_id':self.nominee.id}) + login_testing_unauthorized(self,self.member.user.username,url) + provide_private_key_to_test_client(self) + response = self.client.post(url, {'feedback_id': fb.id, 'type': 'obe'}) + self.assertEqual(response.status_code, 403) + + self.client.logout() + self.client.login(username=self.chair.user.username, password=self.chair.user.username + "+password") + provide_private_key_to_test_client(self) + + response = self.client.post(url, {'feedback_id': fb.id, 'type': 'obe'}) + self.assertEqual(response.status_code, 200) + + fb = Feedback.objects.get(id=fb.id) + self.assertEqual(fb.type_id,'obe') + self.assertEqual(Feedback.objects.comments().count(), 0) + self.assertEqual(Feedback.objects.filter(type='obe').count(), 1) + + def test_reclassify_feedback_topic(self): + fb = FeedbackFactory.create(nomcom=self.nc,type_id='comment') + fb.topics.add(self.topic) + fb.save() + self.assertEqual(Feedback.objects.comments().count(), 1) + + url = reverse('ietf.nomcom.views.view_feedback_topic', kwargs={'year':self.nc.year(), 'topic_id':self.topic.id}) + login_testing_unauthorized(self,self.member.user.username,url) + provide_private_key_to_test_client(self) + response = self.client.post(url, {'feedback_id': fb.id, 'type': 'unclassified'}) + self.assertEqual(response.status_code, 403) + + self.client.logout() + self.client.login(username=self.chair.user.username, password=self.chair.user.username + "+password") + provide_private_key_to_test_client(self) + + response = self.client.post(url, {'feedback_id': fb.id, 'type': 'unclassified'}) + self.assertEqual(response.status_code, 200) + + fb = Feedback.objects.get(id=fb.id) + self.assertEqual(fb.type_id,None) + self.assertEqual(Feedback.objects.comments().count(), 0) + self.assertEqual(Feedback.objects.filter(type=None).count(), 1) + + def test_reclassify_feedback_unrelated(self): + fb = FeedbackFactory(nomcom=self.nc, type_id='read') + self.assertEqual(Feedback.objects.filter(type='read').count(), 1) + + url = reverse('ietf.nomcom.views.view_feedback_unrelated', kwargs={'year':self.nc.year()}) + login_testing_unauthorized(self,self.member.user.username,url) + provide_private_key_to_test_client(self) + response = self.client.post(url, {'feedback_id': fb.id, 'type': 'junk'}) + self.assertEqual(response.status_code, 403) + + self.client.logout() + self.client.login(username=self.chair.user.username, password=self.chair.user.username + "+password") + provide_private_key_to_test_client(self) + + response = self.client.post(url, {'feedback_id': fb.id, 'type': 'junk'}) + self.assertEqual(response.status_code, 200) + + fb = Feedback.objects.get(id=fb.id) + self.assertEqual(fb.type_id, 'junk') + self.assertEqual(Feedback.objects.filter(type='read').count(), 0) + self.assertEqual(Feedback.objects.filter(type='junk').count(), 1) diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index 883bc3ca17..b36f664500 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -1,10 +1,10 @@ -# Copyright The IETF Trust 2012-2020, All Rights Reserved +# Copyright The IETF Trust 2012-2023, All Rights Reserved # -*- coding: utf-8 -*- import datetime import re -from collections import OrderedDict, Counter +from collections import Counter import csv import hmac @@ -14,7 +14,7 @@ from django.contrib.auth.models import AnonymousUser from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.forms.models import modelformset_factory, inlineformset_factory -from django.http import Http404, HttpResponseRedirect, HttpResponse +from django.http import Http404, HttpResponseRedirect, HttpResponse, HttpResponseForbidden from django.shortcuts import render, get_object_or_404, redirect from django.template.loader import render_to_string from django.urls import reverse @@ -767,7 +767,6 @@ def process_nomination_status(request, year, nominee_position_id, state, date, h 'selected': 'feedback', 'form': form }) - @role_required("Nomcom") @nomcom_private_key_required def view_feedback(request, year): @@ -775,7 +774,7 @@ def view_feedback(request, year): nominees = Nominee.objects.get_by_nomcom(nomcom).not_duplicated().distinct() independent_feedback_types = [] nominee_feedback_types = [] - for ft in FeedbackTypeName.objects.all(): + for ft in FeedbackTypeName.objects.filter(used=True): if ft.slug in settings.NOMINEE_FEEDBACK_TYPES: nominee_feedback_types.append(ft) else: @@ -838,7 +837,8 @@ def nominee_staterank(nominee): 'topics_feedback': topics_feedback, 'independent_feedback': independent_feedback, 'nominees_feedback': nominees_feedback, - 'nomcom': nomcom}) + 'nomcom': nomcom, + }) @role_required("Nomcom Chair", "Nomcom Advisor") @@ -924,23 +924,13 @@ def view_feedback_pending(request, year): formset = FeedbackFormSet(queryset=feedback_page.object_list) for form in formset.forms: form.set_nomcom(nomcom, request.user) - type_dict = OrderedDict() - for t in FeedbackTypeName.objects.all().order_by('pk'): - rest = t.name - slug = rest[0] - rest = rest[1:] - while slug in type_dict and rest: - slug = rest[0] - rest = rest[1] - type_dict[slug] = t return render(request, 'nomcom/view_feedback_pending.html', {'year': year, 'selected': 'feedback_pending', 'formset': formset, 'extra_step': extra_step, - 'type_dict': type_dict, 'extra_ids': extra_ids, - 'types': FeedbackTypeName.objects.all().order_by('pk'), + 'types': FeedbackTypeName.objects.filter(used=True), 'nomcom': nomcom, 'is_chair_task' : True, 'page': feedback_page, @@ -951,22 +941,59 @@ def view_feedback_pending(request, year): @nomcom_private_key_required def view_feedback_unrelated(request, year): nomcom = get_nomcom_by_year(year) + + if request.method == 'POST': + if not nomcom.group.has_role(request.user, ['chair','advisor']): + return HttpResponseForbidden('Restricted to roles: Nomcom Chair, Nomcom Advisor') + feedback_id = request.POST.get('feedback_id', None) + feedback = get_object_or_404(Feedback, id=feedback_id) + type = request.POST.get('type', None) + if type: + if type == 'unclassified': + feedback.type = None + messages.success(request, 'The selected feedback has been de-classified. Please reclassify it in the Pending emails tab.') + else: + feedback.type = FeedbackTypeName.objects.get(slug=type) + messages.success(request, f'The selected feedback has been reclassified as {feedback.type.name}.') + feedback.save() + else: + return render(request, 'nomcom/view_feedback_unrelated.html', + {'year': year, + 'nomcom': nomcom, + 'feedback_types': FeedbackTypeName.objects.filter(used=True).exclude(slug__in=settings.NOMINEE_FEEDBACK_TYPES), + 'reclassify_feedback': feedback, + 'is_chair_task' : True, + }) + feedback_types = [] - for ft in FeedbackTypeName.objects.exclude(slug__in=settings.NOMINEE_FEEDBACK_TYPES): + for ft in FeedbackTypeName.objects.filter(used=True).exclude(slug__in=settings.NOMINEE_FEEDBACK_TYPES): feedback_types.append({'ft': ft, 'feedback': ft.feedback_set.get_by_nomcom(nomcom)}) - return render(request, 'nomcom/view_feedback_unrelated.html', {'year': year, - 'selected': 'view_feedback', 'feedback_types': feedback_types, - 'nomcom': nomcom}) + 'nomcom': nomcom, + }) @role_required("Nomcom") @nomcom_private_key_required def view_feedback_topic(request, year, topic_id): - nomcom = get_nomcom_by_year(year) + # At present, the only feedback type for topics is 'comment'. + # Reclassifying from 'comment' to 'comment' is a no-op, + # so the only meaningful action is to de-classify it. + if request.method == 'POST': + nomcom = get_nomcom_by_year(year) + if not nomcom.group.has_role(request.user, ['chair','advisor']): + return HttpResponseForbidden('Restricted to roles: Nomcom Chair, Nomcom Advisor') + feedback_id = request.POST.get('feedback_id', None) + feedback = get_object_or_404(Feedback, id=feedback_id) + feedback.type = None + feedback.topics.clear() + feedback.save() + messages.success(request, 'The selected feedback has been de-classified. Please reclassify it in the Pending emails tab.') + topic = get_object_or_404(Topic, id=topic_id) + nomcom = get_nomcom_by_year(year) feedback_types = FeedbackTypeName.objects.filter(slug__in=['comment',]) last_seen = TopicFeedbackLastSeen.objects.filter(reviewer=request.user.person,topic=topic).first() @@ -978,18 +1005,42 @@ def view_feedback_topic(request, year, topic_id): return render(request, 'nomcom/view_feedback_topic.html', {'year': year, - 'selected': 'view_feedback', 'topic': topic, 'feedback_types': feedback_types, 'last_seen_time' : last_seen_time, - 'nomcom': nomcom}) + 'nomcom': nomcom, + }) @role_required("Nomcom") @nomcom_private_key_required def view_feedback_nominee(request, year, nominee_id): nomcom = get_nomcom_by_year(year) nominee = get_object_or_404(Nominee, id=nominee_id) - feedback_types = FeedbackTypeName.objects.filter(slug__in=settings.NOMINEE_FEEDBACK_TYPES) + feedback_types = FeedbackTypeName.objects.filter(used=True, slug__in=settings.NOMINEE_FEEDBACK_TYPES) + + if request.method == 'POST': + if not nomcom.group.has_role(request.user, ['chair','advisor']): + return HttpResponseForbidden('Restricted to roles: Nomcom Chair, Nomcom Advisor') + feedback_id = request.POST.get('feedback_id', None) + feedback = get_object_or_404(Feedback, id=feedback_id) + type = request.POST.get('type', None) + if type: + if type == 'unclassified': + feedback.type = None + feedback.nominees.clear() + messages.success(request, 'The selected feedback has been de-classified. Please reclassify it in the Pending emails tab.') + else: + feedback.type = FeedbackTypeName.objects.get(slug=type) + messages.success(request, f'The selected feedback has been reclassified as {feedback.type.name}.') + feedback.save() + else: + return render(request, 'nomcom/view_feedback_nominee.html', + {'year': year, + 'nomcom': nomcom, + 'feedback_types': feedback_types, + 'reclassify_feedback': feedback, + 'is_chair_task': True, + }) last_seen = FeedbackLastSeen.objects.filter(reviewer=request.user.person,nominee=nominee).first() last_seen_time = (last_seen and last_seen.time) or datetime.datetime(year=1, month=1, day=1, tzinfo=datetime.timezone.utc) @@ -1000,11 +1051,11 @@ def view_feedback_nominee(request, year, nominee_id): return render(request, 'nomcom/view_feedback_nominee.html', {'year': year, - 'selected': 'view_feedback', 'nominee': nominee, 'feedback_types': feedback_types, 'last_seen_time' : last_seen_time, - 'nomcom': nomcom}) + 'nomcom': nomcom, + }) @role_required("Nomcom Chair", "Nomcom Advisor") diff --git a/ietf/settings.py b/ietf/settings.py index be2d62c7b9..678ba40576 100644 --- a/ietf/settings.py +++ b/ietf/settings.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 -*- @@ -802,7 +802,7 @@ def skip_unreadable_post(record): NOMCOM_FROM_EMAIL = 'nomcom-chair-{year}@ietf.org' OPENSSL_COMMAND = '/usr/bin/openssl' DAYS_TO_EXPIRE_NOMINATION_LINK = '' -NOMINEE_FEEDBACK_TYPES = ['comment', 'questio', 'nomina'] +NOMINEE_FEEDBACK_TYPES = ['comment', 'questio', 'nomina', 'obe'] # SlideSubmission settings SLIDE_STAGING_PATH = '/a/www/www6s/staging/' diff --git a/ietf/templates/nomcom/reclassify_feedback_item.html b/ietf/templates/nomcom/reclassify_feedback_item.html new file mode 100644 index 0000000000..efa71dc104 --- /dev/null +++ b/ietf/templates/nomcom/reclassify_feedback_item.html @@ -0,0 +1,103 @@ +{# Copyright The IETF Trust 2023, All Rights Reserved #} +{% load nomcom_tags textfilters %} +

Reclassify feedback item

+
+ {% csrf_token %} + + + + + + + + + + + + + {% for ft in feedback_types %} + + + + + {% endfor %} + +
CodeExplanation
UUnclassified
{{ ft.legend }}{{ ft.name }}
+ + + + + + {% for ft in feedback_types %} + + {% endfor %} + + + + + + + + + + + {% for ft in feedback_types %} + + {% endfor %} + + + + + +
DateU{{ ft.legend }}AuthorSubject
{{ reclassify_feedback.time|date:"r" }} + + + + {{ reclassify_feedback.author }}{{ reclassify_feedback.subject }} + + +
+ + +
diff --git a/ietf/templates/nomcom/view_feedback_nominee.html b/ietf/templates/nomcom/view_feedback_nominee.html index 7ead9f477a..5408880da4 100644 --- a/ietf/templates/nomcom/view_feedback_nominee.html +++ b/ietf/templates/nomcom/view_feedback_nominee.html @@ -1,10 +1,13 @@ {% extends "nomcom/nomcom_private_base.html" %} -{# Copyright The IETF Trust 2015, All Rights Reserved #} +{# Copyright The IETF Trust 2015-2023, All Rights Reserved #} {% load origin %} {% load nomcom_tags textfilters %} {% block subtitle %}- View feedback about {{ nominee.email.person.name }}{% endblock %} {% block nomcom_content %} - {% origin %} +{% origin %} +{% if reclassify_feedback %} + {% include "nomcom/reclassify_feedback_item.html" %} +{% else %}

Feedback about {{ nominee }}