From a345673c0d4f9c5bade059ea2ba22142a1904a90 Mon Sep 17 00:00:00 2001 From: Kesara Rathnayake Date: Wed, 2 Oct 2024 04:24:21 +1300 Subject: [PATCH 01/41] build: Install fonts from xml2rfc-fonts (#6300) * build: Install fonts from xml2rfc-fonts Fixes #6294 * fixup! build: Install fonts from xml2rfc-fonts --- docker/base.Dockerfile | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docker/base.Dockerfile b/docker/base.Dockerfile index 4a307c6c62..e2465f33c2 100644 --- a/docker/base.Dockerfile +++ b/docker/base.Dockerfile @@ -125,6 +125,15 @@ ENV LC_ALL en_US.UTF-8 ADD https://raw.githubusercontent.com/ietf-tools/idnits-mirror/main/idnits /usr/local/bin/ RUN chmod +rx /usr/local/bin/idnits +# Install required fonts +RUN mkdir -p /tmp/fonts && \ + wget -q -O /tmp/fonts.tar.gz https://github.com/ietf-tools/xml2rfc-fonts/archive/refs/tags/3.22.0.tar.gz && \ + tar zxf /tmp/fonts.tar.gz -C /tmp/fonts && \ + mv /tmp/fonts/*/noto/* /usr/local/share/fonts/ && \ + mv /tmp/fonts/*/roboto_mono/* /usr/local/share/fonts/ && \ + rm -rf /tmp/fonts.tar.gz /tmp/fonts/ && \ + fc-cache -f + # Turn off rsyslog kernel logging (doesn't work in Docker) RUN sed -i '/imklog/s/^/#/' /etc/rsyslog.conf From b34bbe59ad93726872105a1ed18fec6114f89502 Mon Sep 17 00:00:00 2001 From: Sangho Na Date: Thu, 3 Oct 2024 05:44:52 +1300 Subject: [PATCH 02/41] fix: hide rounded pill if session has no purpose specified (#7997) --- ietf/meeting/tests_views.py | 32 ++++++++++++++++++++++++++++ ietf/templates/meeting/requests.html | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index f951eb682d..642edcb9b4 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -7173,6 +7173,20 @@ def test_meeting_requests(self): status_id='schedw', add_to_schedule=False, ) + session_with_none_purpose = SessionFactory( + meeting=meeting, + group__parent=area, + purpose_id="none", + status_id="schedw", + add_to_schedule=False, + ) + tutorial_session = SessionFactory( + meeting=meeting, + group__parent=area, + purpose_id="tutorial", + status_id="schedw", + add_to_schedule=False, + ) def _sreq_edit_link(sess): return urlreverse( 'ietf.secr.sreq.views.edit', @@ -7211,6 +7225,8 @@ def _sreq_edit_link(sess): self.assertContains(r, _sreq_edit_link(proposed_wg_session)) # link to the session request self.assertContains(r, rg_session.group.acronym) self.assertContains(r, _sreq_edit_link(rg_session)) # link to the session request + self.assertContains(r, session_with_none_purpose.group.acronym) + self.assertContains(r, tutorial_session.group.acronym) # check headings - note that the special types (has_meetings, etc) do not have a group parent # so they show up in 'other' q = PyQuery(r.content) @@ -7218,6 +7234,22 @@ def _sreq_edit_link(sess): self.assertEqual(len(q('h2#other-groups')), 1) self.assertEqual(len(q('h2#irtf')), 1) # rg group has irtf group as parent + # check rounded pills + self.assertNotContains( # no rounded pill for sessions with regular purpose + r, + 'Regular', + html=True, + ) + self.assertNotContains( # no rounded pill for session with no purpose specified + r, + 'None', + html=True, + ) + self.assertContains( # rounded pill for session with non-regular purpose + r, + 'Tutorial', + html=True, + ) def test_request_minutes(self): meeting = MeetingFactory(type_id='ietf') diff --git a/ietf/templates/meeting/requests.html b/ietf/templates/meeting/requests.html index 18d8c5cf8c..3008ceb662 100644 --- a/ietf/templates/meeting/requests.html +++ b/ietf/templates/meeting/requests.html @@ -154,7 +154,7 @@

{{ session.group.acronym }} - {% if session.purpose_id != "regular" %} + {% if session.purpose_id != "regular" and session.purpose_id != "none" %}
{{session.purpose}} {% endif %} {% if session.joint_with_groups.count %}joint with {{ session.joint_with_groups_acronyms|join:' ' }}{% endif %} From 364dec3e33cc0df8804d1792584c4ac97336cba6 Mon Sep 17 00:00:00 2001 From: Sangho Na Date: Thu, 3 Oct 2024 11:37:49 +1300 Subject: [PATCH 03/41] fix: fix wg anchor tags (case-sensitive) (#7995) * fix: fix wg anchor tags in areas page * fix: fix wg anchor tags in IESG agenda page * test: ensure wg anchor tags to be all uppercase * test: improve tests --- ietf/group/tests_info.py | 6 ++++++ ietf/iesg/tests.py | 14 ++++++++++++++ ietf/templates/group/active_areas.html | 2 +- ietf/templates/iesg/agenda_charter.html | 2 +- ietf/templates/iesg/agenda_doc.html | 2 +- 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py index 561f542f42..95fe9aa2e5 100644 --- a/ietf/group/tests_info.py +++ b/ietf/group/tests_info.py @@ -87,6 +87,12 @@ def test_active_groups(self): r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertContains(r, g.acronym) + if t == "area": + q = PyQuery(r.content) + wg_url = urlreverse("ietf.group.views.active_groups", kwargs=dict(group_type="wg")) + href = f"{wg_url}#{g.acronym.upper()}" + self.assertEqual(q(f"h2#id-{g.acronym} a").attr("href"), href) + self.assertEqual(q(f'h2#id-{g.acronym} a[href="{href}"]').text(), f"({g.acronym.upper()})") url = urlreverse('ietf.group.views.active_groups', kwargs=dict()) r = self.client.get(url) diff --git a/ietf/iesg/tests.py b/ietf/iesg/tests.py index 8438cb44dd..42e838ebdf 100644 --- a/ietf/iesg/tests.py +++ b/ietf/iesg/tests.py @@ -404,6 +404,8 @@ def test_agenda(self): self.assertContains(r, action_items.text) + q = PyQuery(r.content) + for k, d in self.telechat_docs.items(): if d.type_id == "charter": self.assertContains(r, d.group.name, msg_prefix="%s '%s' not in response" % (k, d.group.name)) @@ -412,6 +414,18 @@ def test_agenda(self): self.assertContains(r, d.name, msg_prefix="%s '%s' not in response" % (k, d.name)) self.assertContains(r, d.title, msg_prefix="%s '%s' title not in response" % (k, d.title)) + if d.type_id in ["charter", "draft"]: + if d.group.parent is None: + continue + wg_url = urlreverse("ietf.group.views.active_groups", kwargs=dict(group_type="wg")) + href = f"{wg_url}#{d.group.parent.acronym.upper()}" + texts = [elem.text.strip() for elem in q(f'a[href="{href}"]')] + self.assertGreater(len(texts), 0) + if d.type_id == "charter": + self.assertTrue(any(t == d.group.parent.acronym.upper() for t in texts)) + elif d.type_id == "draft": + self.assertTrue(any(t == f"({d.group.parent.acronym.upper()})" for t in texts)) + for i, mi in enumerate(self.mgmt_items, start=1): s = "6." + str(i) self.assertContains(r, s, msg_prefix="Section '%s' not in response" % s) diff --git a/ietf/templates/group/active_areas.html b/ietf/templates/group/active_areas.html index 63637889ea..8355d13bbc 100644 --- a/ietf/templates/group/active_areas.html +++ b/ietf/templates/group/active_areas.html @@ -65,7 +65,7 @@

Areas

{% for area in areas %}

{{ area.name }} - ({{ area.acronym|upper }}) + ({{ area.acronym|upper }})

{% if area.description %}

diff --git a/ietf/templates/iesg/agenda_charter.html b/ietf/templates/iesg/agenda_charter.html index 20bd23e06f..b4124914db 100644 --- a/ietf/templates/iesg/agenda_charter.html +++ b/ietf/templates/iesg/agenda_charter.html @@ -24,7 +24,7 @@

Area
- + {{ doc.group.parent.acronym|upper }} ({% person_link doc.ad %})
diff --git a/ietf/templates/iesg/agenda_doc.html b/ietf/templates/iesg/agenda_doc.html index ae78d96d60..87051fda26 100644 --- a/ietf/templates/iesg/agenda_doc.html +++ b/ietf/templates/iesg/agenda_doc.html @@ -37,7 +37,7 @@
Token
From 6aff818b72c9e2e94d3432288ec366ce7f16b86a Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Wed, 9 Oct 2024 12:56:00 -0300 Subject: [PATCH 04/41] feat: remove deprecated /api/submit/ endpoint (#8017) * feat: remove deprecated /api/submit endpoint * chore: remove unused imports * test: test api_submit_tombstone view --- ietf/api/urls.py | 2 +- ietf/submit/forms.py | 273 +-------------------- ietf/submit/tests.py | 143 +---------- ietf/submit/views.py | 107 +------- ietf/templates/api/index.html | 2 +- ietf/templates/submit/api_submit_info.html | 57 +---- 6 files changed, 27 insertions(+), 557 deletions(-) diff --git a/ietf/api/urls.py b/ietf/api/urls.py index 396b3813d6..48525dfda2 100644 --- a/ietf/api/urls.py +++ b/ietf/api/urls.py @@ -59,7 +59,7 @@ # Email alias listing url(r'^person/email/$', api_views.active_email_list), # Draft submission API - url(r'^submit/?$', submit_views.api_submit), + url(r'^submit/?$', submit_views.api_submit_tombstone), # Draft upload API url(r'^submission/?$', submit_views.api_submission), # Draft submission state API diff --git a/ietf/submit/forms.py b/ietf/submit/forms.py index 4e5644b36e..2781d3365a 100644 --- a/ietf/submit/forms.py +++ b/ietf/submit/forms.py @@ -2,19 +2,16 @@ # -*- coding: utf-8 -*- -import io import os import re import datetime import email import sys import tempfile -import xml2rfc from contextlib import ExitStack from email.utils import formataddr from typing import Tuple -from unidecode import unidecode from django import forms from django.conf import settings @@ -37,10 +34,8 @@ from ietf.submit.utils import validate_submission_name, validate_submission_rev, validate_submission_document_date, remote_ip from ietf.submit.parsers.plain_parser import PlainParser from ietf.submit.parsers.xml_parser import XMLParser -from ietf.utils import log from ietf.utils.draft import PlaintextDraft from ietf.utils.fields import ModelMultipleChoiceField -from ietf.utils.text import normalize_text from ietf.utils.timezone import date_today from ietf.utils.xmldraft import InvalidXMLError, XMLDraft, XMLParseError @@ -371,273 +366,6 @@ def deduce_group(name): return None -class DeprecatedSubmissionBaseUploadForm(SubmissionBaseUploadForm): - def clean(self): - def format_messages(where, e, log): - out = log.write_out.getvalue().splitlines() - err = log.write_err.getvalue().splitlines() - m = str(e) - if m: - m = [ m ] - else: - import traceback - typ, val, tb = sys.exc_info() - m = traceback.format_exception(typ, val, tb) - m = [ l.replace('\n ', ':\n ') for l in m ] - msgs = [s for s in (["Error from xml2rfc (%s):" % (where,)] + m + out + err) if s] - return msgs - - if self.shutdown and not has_role(self.request.user, "Secretariat"): - raise forms.ValidationError(self.cutoff_warning) - - for ext in self.formats: - f = self.cleaned_data.get(ext, None) - if not f: - continue - self.file_types.append('.%s' % ext) - if not ('.txt' in self.file_types or '.xml' in self.file_types): - if not self.errors: - raise forms.ValidationError('Unexpected submission file types; found %s, but %s is required' % (', '.join(self.file_types), ' or '.join(self.base_formats))) - - #debug.show('self.cleaned_data["xml"]') - if self.cleaned_data.get('xml'): - #if not self.cleaned_data.get('txt'): - xml_file = self.cleaned_data.get('xml') - file_name = {} - xml2rfc.log.write_out = io.StringIO() # open(os.devnull, "w") - xml2rfc.log.write_err = io.StringIO() # open(os.devnull, "w") - tfn = None - with ExitStack() as stack: - @stack.callback - def cleanup(): # called when context exited, even in case of exception - if tfn is not None: - os.unlink(tfn) - - # We need to write the xml file to disk in order to hand it - # over to the xml parser. XXX FIXME: investigate updating - # xml2rfc to be able to work with file handles to in-memory - # files. - name, ext = os.path.splitext(os.path.basename(xml_file.name)) - with tempfile.NamedTemporaryFile(prefix=name+'-', - suffix='.xml', - mode='wb+', - delete=False) as tf: - tfn = tf.name - for chunk in xml_file.chunks(): - tf.write(chunk) - - parser = xml2rfc.XmlRfcParser(str(tfn), quiet=True) - # --- Parse the xml --- - try: - self.xmltree = parser.parse(remove_comments=False) - # If we have v2, run it through v2v3. Keep track of the submitted version, though. - self.xmlroot = self.xmltree.getroot() - self.xml_version = self.xmlroot.get('version', '2') - if self.xml_version == '2': - v2v3 = xml2rfc.V2v3XmlWriter(self.xmltree) - self.xmltree.tree = v2v3.convert2to3() - self.xmlroot = self.xmltree.getroot() # update to the new root - - draftname = self.xmlroot.attrib.get('docName') - if draftname is None: - self.add_error('xml', "No docName attribute found in the xml root element") - name_error = validate_submission_name(draftname) - if name_error: - self.add_error('xml', name_error) # This is a critical and immediate failure - do not proceed with other validation. - else: - revmatch = re.search("-[0-9][0-9]$", draftname) - if revmatch: - self.revision = draftname[-2:] - self.filename = draftname[:-3] - else: - self.revision = None - self.filename = draftname - self.title = self.xmlroot.findtext('front/title').strip() - if type(self.title) is str: - self.title = unidecode(self.title) - self.title = normalize_text(self.title) - self.abstract = (self.xmlroot.findtext('front/abstract') or '').strip() - if type(self.abstract) is str: - self.abstract = unidecode(self.abstract) - author_info = self.xmlroot.findall('front/author') - for author in author_info: - info = { - "name": author.attrib.get('fullname'), - "email": author.findtext('address/email'), - "affiliation": author.findtext('organization'), - } - elem = author.find('address/postal/country') - if elem != None: - ascii_country = elem.get('ascii', None) - info['country'] = ascii_country if ascii_country else elem.text - - for item in info: - if info[item]: - info[item] = info[item].strip() - self.authors.append(info) - - # --- Prep the xml --- - file_name['xml'] = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s%s' % (self.filename, self.revision, ext)) - try: - prep = xml2rfc.PrepToolWriter(self.xmltree, quiet=True, liberal=True, keep_pis=[xml2rfc.V3_PI_TARGET]) - prep.options.accept_prepped = True - self.xmltree.tree = prep.prep() - if self.xmltree.tree == None: - self.add_error('xml', "Error from xml2rfc (prep): %s" % prep.errors) - except Exception as e: - msgs = format_messages('prep', e, xml2rfc.log) - self.add_error('xml', msgs) - - # --- Convert to txt --- - if not ('txt' in self.cleaned_data and self.cleaned_data['txt']): - file_name['txt'] = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.txt' % (self.filename, self.revision)) - try: - writer = xml2rfc.TextWriter(self.xmltree, quiet=True) - writer.options.accept_prepped = True - writer.write(file_name['txt']) - log.log("In %s: xml2rfc %s generated %s from %s (version %s)" % - ( os.path.dirname(file_name['xml']), - xml2rfc.__version__, - os.path.basename(file_name['txt']), - os.path.basename(file_name['xml']), - self.xml_version)) - except Exception as e: - msgs = format_messages('txt', e, xml2rfc.log) - log.log('\n'.join(msgs)) - self.add_error('xml', msgs) - - # --- Convert to html --- - try: - file_name['html'] = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.html' % (self.filename, self.revision)) - writer = xml2rfc.HtmlWriter(self.xmltree, quiet=True) - writer.write(file_name['html']) - self.file_types.append('.html') - log.log("In %s: xml2rfc %s generated %s from %s (version %s)" % - ( os.path.dirname(file_name['xml']), - xml2rfc.__version__, - os.path.basename(file_name['html']), - os.path.basename(file_name['xml']), - self.xml_version)) - except Exception as e: - msgs = format_messages('html', e, xml2rfc.log) - self.add_error('xml', msgs) - - except Exception as e: - try: - msgs = format_messages('txt', e, xml2rfc.log) - log.log('\n'.join(msgs)) - self.add_error('xml', msgs) - except Exception: - self.add_error('xml', "An exception occurred when trying to process the XML file: %s" % e) - - # The following errors are likely noise if we have previous field - # errors: - if self.errors: - raise forms.ValidationError('') - - if self.cleaned_data.get('txt'): - # try to parse it - txt_file = self.cleaned_data['txt'] - txt_file.seek(0) - bytes = txt_file.read() - txt_file.seek(0) - try: - text = bytes.decode(PlainParser.encoding) - self.parsed_draft = PlaintextDraft(text, txt_file.name) - if self.filename == None: - self.filename = self.parsed_draft.filename - elif self.filename != self.parsed_draft.filename: - self.add_error('txt', "Inconsistent name information: xml:%s, txt:%s" % (self.filename, self.parsed_draft.filename)) - if self.revision == None: - self.revision = self.parsed_draft.revision - elif self.revision != self.parsed_draft.revision: - self.add_error('txt', "Inconsistent revision information: xml:%s, txt:%s" % (self.revision, self.parsed_draft.revision)) - if self.title == None: - self.title = self.parsed_draft.get_title() - elif self.title != self.parsed_draft.get_title(): - self.add_error('txt', "Inconsistent title information: xml:%s, txt:%s" % (self.title, self.parsed_draft.get_title())) - except (UnicodeDecodeError, LookupError) as e: - self.add_error('txt', 'Failed decoding the uploaded file: "%s"' % str(e)) - - rev_error = validate_submission_rev(self.filename, self.revision) - if rev_error: - raise forms.ValidationError(rev_error) - - # The following errors are likely noise if we have previous field - # errors: - if self.errors: - raise forms.ValidationError('') - - if not self.filename: - raise forms.ValidationError("Could not extract a valid Internet-Draft name from the upload. " - "To fix this in a text upload, please make sure that the full Internet-Draft name including " - "revision number appears centered on its own line below the document title on the " - "first page. In an xml upload, please make sure that the top-level " - "element has a docName attribute which provides the full Internet-Draft name including " - "revision number.") - - if not self.revision: - raise forms.ValidationError("Could not extract a valid Internet-Draft revision from the upload. " - "To fix this in a text upload, please make sure that the full Internet-Draft name including " - "revision number appears centered on its own line below the document title on the " - "first page. In an xml upload, please make sure that the top-level " - "element has a docName attribute which provides the full Internet-Draft name including " - "revision number.") - - if not self.title: - raise forms.ValidationError("Could not extract a valid title from the upload") - - if self.cleaned_data.get('txt') or self.cleaned_data.get('xml'): - # check group - self.group = self.deduce_group(self.filename) - - # check existing - existing = Submission.objects.filter(name=self.filename, rev=self.revision).exclude(state__in=("posted", "cancel", "waiting-for-draft")) - if existing: - raise forms.ValidationError(mark_safe('A submission with same name and revision is currently being processed. Check the status here.' % urlreverse("ietf.submit.views.submission_status", kwargs={ 'submission_id': existing[0].pk }))) - - # cut-off - if self.revision == '00' and self.in_first_cut_off: - raise forms.ValidationError(mark_safe(self.cutoff_warning)) - - # check thresholds - today = date_today() - - self.check_submissions_thresholds( - "for the Internet-Draft %s" % self.filename, - dict(name=self.filename, rev=self.revision, submission_date=today), - settings.IDSUBMIT_MAX_DAILY_SAME_DRAFT_NAME, settings.IDSUBMIT_MAX_DAILY_SAME_DRAFT_NAME_SIZE, - ) - self.check_submissions_thresholds( - "for the same submitter", - dict(remote_ip=self.remote_ip, submission_date=today), - settings.IDSUBMIT_MAX_DAILY_SAME_SUBMITTER, settings.IDSUBMIT_MAX_DAILY_SAME_SUBMITTER_SIZE, - ) - if self.group: - self.check_submissions_thresholds( - "for the group \"%s\"" % (self.group.acronym), - dict(group=self.group, submission_date=today), - settings.IDSUBMIT_MAX_DAILY_SAME_GROUP, settings.IDSUBMIT_MAX_DAILY_SAME_GROUP_SIZE, - ) - self.check_submissions_thresholds( - "across all submitters", - dict(submission_date=today), - settings.IDSUBMIT_MAX_DAILY_SUBMISSIONS, settings.IDSUBMIT_MAX_DAILY_SUBMISSIONS_SIZE, - ) - - return super().clean() - - -class DeprecatedSubmissionAutoUploadForm(DeprecatedSubmissionBaseUploadForm): - """Full-service upload form, replaced by the asynchronous version""" - user = forms.EmailField(required=True) - - def __init__(self, request, *args, **kwargs): - super(DeprecatedSubmissionAutoUploadForm, self).__init__(request, *args, **kwargs) - self.formats = ['xml', ] - self.base_formats = ['xml', ] - - class SubmissionManualUploadForm(SubmissionBaseUploadForm): txt = forms.FileField(label='.txt format', required=False) formats = SubmissionBaseUploadForm.formats + ('txt',) @@ -676,6 +404,7 @@ def clean_txt(self): ) return txt_file + class SubmissionAutoUploadForm(SubmissionBaseUploadForm): user = forms.EmailField(required=True) replaces = forms.CharField(required=False, max_length=1000, strip=True) diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py index b48168f8a6..ed28c7ef02 100644 --- a/ietf/submit/tests.py +++ b/ietf/submit/tests.py @@ -44,7 +44,7 @@ from ietf.meeting.factories import MeetingFactory from ietf.name.models import DraftSubmissionStateName, FormalLanguageName from ietf.person.models import Person -from ietf.person.factories import UserFactory, PersonFactory, EmailFactory +from ietf.person.factories import UserFactory, PersonFactory from ietf.submit.factories import SubmissionFactory, SubmissionExtResourceFactory from ietf.submit.forms import SubmissionBaseUploadForm, SubmissionAutoUploadForm from ietf.submit.models import Submission, Preapproval, SubmissionExtResource @@ -2345,6 +2345,12 @@ def setUp(self): super().setUp() MeetingFactory(type_id='ietf', date=date_today()+datetime.timedelta(days=60)) + def test_api_submit_tombstone(self): + """Tombstone for obsolete API endpoint should return 410 Gone""" + url = urlreverse("ietf.submit.views.api_submit_tombstone") + self.assertEqual(self.client.get(url).status_code, 410) + self.assertEqual(self.client.post(url).status_code, 410) + def test_upload_draft(self): """api_submission accepts a submission and queues it for processing""" url = urlreverse('ietf.submit.views.api_submission') @@ -3191,141 +3197,6 @@ def test_cancel_stale_submissions(self): self.assertEqual(subm.state_id, "cancel") self.assertEqual(subm.submissionevent_set.count(), 2) - -class ApiSubmitTests(BaseSubmitTestCase): - def setUp(self): - super().setUp() - # break early in case of missing configuration - self.assertTrue(os.path.exists(settings.IDSUBMIT_IDNITS_BINARY)) - MeetingFactory(type_id='ietf', date=date_today()+datetime.timedelta(days=60)) - - def do_post_submission(self, rev, author=None, name=None, group=None, email=None, title=None, year=None): - url = urlreverse('ietf.submit.views.api_submit') - if author is None: - author = PersonFactory() - if name is None: - slug = re.sub('[^a-z0-9-]+', '', author.ascii_parts()[3].lower()) - name = 'draft-%s-foo' % slug - if email is None: - email = author.user.username - # submit - data = {} - data['xml'], author = submission_file(f'{name}-{rev}', f'{name}-{rev}.xml', group, "test_submission.xml", author=author, email=email, title=title, year=year) - data['user'] = email - r = self.client.post(url, data) - return r, author, name - - def test_api_submit_info(self): - url = urlreverse('ietf.submit.views.api_submit') - r = self.client.get(url) - expected = "A simplified Internet-Draft submission interface, intended for automation" - self.assertContains(r, expected, status_code=200) - - def test_api_submit_bad_method(self): - url = urlreverse('ietf.submit.views.api_submit') - r = self.client.put(url) - self.assertEqual(r.status_code, 405) - - def test_api_submit_ok(self): - r, author, name = self.do_post_submission('00') - expected = "Upload of %s OK, confirmation requests sent to:\n %s" % (name, author.formatted_email().replace('\n','')) - self.assertContains(r, expected, status_code=200) - - def test_api_submit_secondary_email_active(self): - person = PersonFactory() - email = EmailFactory(person=person) - r, author, name = self.do_post_submission('00', author=person, email=email.address) - for expected in [ - "Upload of %s OK, confirmation requests sent to:" % (name, ), - author.formatted_email().replace('\n',''), - ]: - self.assertContains(r, expected, status_code=200) - - def test_api_submit_secondary_email_inactive(self): - person = PersonFactory() - prim = person.email() - prim.primary = True - prim.save() - email = EmailFactory(person=person, active=False) - r, author, name = self.do_post_submission('00', author=person, email=email.address) - expected = "No such user: %s" % email.address - self.assertContains(r, expected, status_code=400) - - def test_api_submit_no_user(self): - email='nonexistant.user@example.org' - r, author, name = self.do_post_submission('00', email=email) - expected = "No such user: %s" % email - self.assertContains(r, expected, status_code=400) - - def test_api_submit_no_person(self): - user = UserFactory() - email = user.username - r, author, name = self.do_post_submission('00', email=email) - expected = "No person with username %s" % email - self.assertContains(r, expected, status_code=400) - - def test_api_submit_wrong_revision(self): - r, author, name = self.do_post_submission('01') - expected = "Invalid revision (revision 00 is expected)" - self.assertContains(r, expected, status_code=400) - - def test_api_submit_update_existing_submissiondocevent_rev(self): - draft, _ = create_draft_submission_with_rev_mismatch(rev='01') - r, _, __ = self.do_post_submission(rev='01', name=draft.name) - expected = "Submission failed" - self.assertContains(r, expected, status_code=409) - - def test_api_submit_update_later_submissiondocevent_rev(self): - draft, _ = create_draft_submission_with_rev_mismatch(rev='02') - r, _, __ = self.do_post_submission(rev='01', name=draft.name) - expected = "Submission failed" - self.assertContains(r, expected, status_code=409) - - def test_api_submit_pending_submission(self): - r, author, name = self.do_post_submission('00') - expected = "Upload of" - self.assertContains(r, expected, status_code=200) - r, author, name = self.do_post_submission('00', author=author, name=name) - expected = "A submission with same name and revision is currently being processed" - self.assertContains(r, expected, status_code=400) - - def test_api_submit_no_title(self): - r, author, name = self.do_post_submission('00', title=" ") - expected = "Could not extract a valid title from the upload" - self.assertContains(r, expected, status_code=400) - - def test_api_submit_failed_idnits(self): - # `year` on the next line must be leap year or this test will fail every Feb 29 - r, author, name = self.do_post_submission('00', year="2012") - expected = "Document date must be within 3 days of submission date" - self.assertContains(r, expected, status_code=400) - - def test_api_submit_keeps_extresources(self): - """API submit should not disturb doc external resources - - Tests that the submission inherits the existing doc's docextresource_set. - Relies on separate testing that Submission external_resources will be - handled appropriately. - """ - draft = WgDraftFactory() - - # add an external resource - self.assertEqual(draft.docextresource_set.count(), 0) - extres = draft.docextresource_set.create( - name_id='faq', - display_name='this is a display name', - value='https://example.com/faq-for-test.html', - ) - - r, _, __ = self.do_post_submission('01', name=draft.name) - self.assertEqual(r.status_code, 200) - # draft = Document.objects.get(pk=draft.pk) # update the draft - sub = Submission.objects.get(name=draft.name) - self.assertEqual( - [str(r) for r in sub.external_resources.all()], - [str(extres)], - ) - class RefsTests(BaseSubmitTestCase): diff --git a/ietf/submit/views.py b/ietf/submit/views.py index 6f23ba49d4..3f745741e4 100644 --- a/ietf/submit/views.py +++ b/ietf/submit/views.py @@ -1,7 +1,5 @@ # Copyright The IETF Trust 2011-2020, All Rights Reserved # -*- coding: utf-8 -*- - - import re import datetime @@ -29,17 +27,15 @@ from ietf.mailtrigger.utils import gather_address_lists from ietf.person.models import Email from ietf.submit.forms import (SubmissionAutoUploadForm, AuthorForm, SubmitterForm, EditSubmissionForm, - PreapprovalForm, ReplacesForm, - DeprecatedSubmissionAutoUploadForm, SubmissionManualUploadForm) + PreapprovalForm, ReplacesForm, SubmissionManualUploadForm) from ietf.submit.mail import send_full_url, send_manual_post_request from ietf.submit.models import (Submission, Preapproval, SubmissionExtResource, DraftSubmissionStateName ) from ietf.submit.tasks import process_uploaded_submission_task, process_and_accept_uploaded_submission_task, poke from ietf.submit.utils import ( approvable_submissions_for_user, preapprovals_for_user, recently_approved_by_user, validate_submission, create_submission_event, docevent_from_submission, - post_submission, cancel_submission, rename_submission_files, remove_submission_files, get_draft_meta, - get_submission, fill_in_submission, apply_checkers, save_files, clear_existing_files, - check_submission_revision_consistency, accept_submission, accept_submission_requires_group_approval, + post_submission, cancel_submission, rename_submission_files, remove_submission_files, + get_submission, save_files, clear_existing_files, accept_submission, accept_submission_requires_group_approval, accept_submission_requires_prev_auth_approval, update_submission_external_resources) from ietf.stats.utils import clean_country_name from ietf.utils.accesstoken import generate_access_token @@ -187,97 +183,14 @@ def api_submission_status(request, submission_id): @csrf_exempt -def api_submit(request): - "Automated submission entrypoint" - submission = None - def err(code, text): - return HttpResponse(text, status=code, content_type='text/plain') - - if request.method == 'GET': - return render(request, 'submit/api_submit_info.html') - elif request.method == 'POST': - exception = None - try: - form = DeprecatedSubmissionAutoUploadForm(request, data=request.POST, files=request.FILES) - if form.is_valid(): - log('got valid submission form for %s' % form.filename) - username = form.cleaned_data['user'] - user = User.objects.filter(username__iexact=username) - if user.count() == 0: - # See if a secondary login was being used - email = Email.objects.filter(address=username, active=True) - # The error messages don't talk about 'email', as the field we're - # looking at is still the 'username' field. - if email.count() == 0: - return err(400, "No such user: %s" % username) - elif email.count() > 1: - return err(500, "Multiple matching accounts for %s" % username) - email = email.first() - if not hasattr(email, 'person'): - return err(400, "No person matches %s" % username) - person = email.person - if not hasattr(person, 'user'): - return err(400, "No user matches: %s" % username) - user = person.user - elif user.count() > 1: - return err(500, "Multiple matching accounts for %s" % username) - else: - user = user.first() - if not hasattr(user, 'person'): - return err(400, "No person with username %s" % username) - - saved_files = save_files(form) - authors, abstract, file_name, file_size = get_draft_meta(form, saved_files) - for a in authors: - if not a['email']: - raise ValidationError("Missing email address for author %s" % a) - - submission = get_submission(form) - fill_in_submission(form, submission, authors, abstract, file_size) - apply_checkers(submission, file_name) - - create_submission_event(request, submission, desc="Uploaded submission via api_submit") - - errors = validate_submission(submission) - if errors: - raise ValidationError(errors) - - # must do this after validate_submission() or data needed for check may be invalid - if check_submission_revision_consistency(submission): - return err( 409, "Submission failed due to a document revision inconsistency error " - "in the database. Please contact the secretariat for assistance.") - - errors = [ c.message for c in submission.checks.all() if c.passed==False ] - if errors: - raise ValidationError(errors) - - if not username.lower() in [ a['email'].lower() for a in authors ]: - raise ValidationError('Submitter %s is not one of the document authors' % user.username) - - submission.submitter = user.person.formatted_email() - sent_to = accept_submission(submission, request) +def api_submit_tombstone(request): + """Tombstone for removed automated submission entrypoint""" + return render( + request, + 'submit/api_submit_info.html', + status=410, # Gone + ) - return HttpResponse( - "Upload of %s OK, confirmation requests sent to:\n %s" % (submission.name, ',\n '.join(sent_to)), - content_type="text/plain") - else: - raise ValidationError(form.errors) - except IOError as e: - exception = e - return err(500, "IO Error: %s" % str(e)) - except ValidationError as e: - exception = e - return err(400, "Validation Error: %s" % str(e)) - except Exception as e: - exception = e - raise - return err(500, "Exception: %s" % str(e)) - finally: - if exception and submission: - remove_submission_files(submission) - submission.delete() - else: - return err(405, "Method not allowed") def tool_instructions(request): return render(request, 'submit/tool_instructions.html', {'selected': 'instructions'}) diff --git a/ietf/templates/api/index.html b/ietf/templates/api/index.html index 8373a387f9..e21a50101a 100644 --- a/ietf/templates/api/index.html +++ b/ietf/templates/api/index.html @@ -9,7 +9,7 @@

Framework API

This section describes the autogenerated read-only API towards the database tables. See also the - Internet-Draft submission API description + Internet-Draft submission API description and the IESG ballot position API description

diff --git a/ietf/templates/submit/api_submit_info.html b/ietf/templates/submit/api_submit_info.html index cd0d52410b..75fc1abfc2 100644 --- a/ietf/templates/submit/api_submit_info.html +++ b/ietf/templates/submit/api_submit_info.html @@ -1,56 +1,13 @@ {% extends "base.html" %} -{# Copyright The IETF Trust 2015-2022, All Rights Reserved #} +{# Copyright The IETF Trust 2015-2024, All Rights Reserved #} {% load origin ietf_filters %} -{% block title %}I-D submission API instructions{% endblock %} +{% block title %}Obsolete I-D submission API notice{% endblock %} {% block content %} {% origin %} -

Internet-Draft submission API instructions

+

Obsolete Internet-Draft submission API notice

- Note: API endpoint described here is known to have a slow response time or to fail - due to timeout for some Internet-Draft submissions, particularly those with large file sizes. - It is recommended to use the new API endpoint - instead for increased reliability. + The API endpoint previously available here is obsolete and is no longer supported. + Please use the new API endpoint + instead.

-

- A simplified Internet-Draft submission interface, intended for automation, - is available at {% absurl 'ietf.submit.views.api_submit' %}. -

-

- The interface accepts only XML uploads that can be processed on the server, and - requires the user to have a datatracker account. A successful submit still requires - the same email confirmation round-trip as submissions done through the regular - submission tool. -

-

- This interface does not provide all the options which the regular submission tool does. - Some limitations: -

-
    -
  • Only XML-only uploads are supported, not text or combined.
  • -
  • Document replacement information cannot be supplied.
  • -
  • - The server expects multipart/form-data, supported by curl but not by wget. -
  • -
-

- It takes two parameters: -

-
    -
  • - user which is the user login -
  • -
  • - xml, which is the submitted file -
  • -
-

- It returns an appropriate http result code, and a brief explanatory text message. -

-

- Here is an example: -

-
-$ curl -S -F "user=user.name@example.com" -F "xml=@~/draft-user-example.xml" {% absurl 'ietf.submit.views.api_submit' %}
-Upload of draft-user-example OK, confirmation requests sent to:
-User Name <user.name@example.com>
-{% endblock %} \ No newline at end of file +{% endblock %} From a6d15c98ba213fc37078957ae343c7f21b58cb97 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Wed, 9 Oct 2024 13:04:54 -0300 Subject: [PATCH 05/41] test: fill in milestone descriptions (#8002) * test: fill in milestone descriptions If "Next" or "Last" appear in the descriptions, the selectors used by the tests get confused. * test: avoid fragile selectors --- ietf/iesg/tests.py | 58 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/ietf/iesg/tests.py b/ietf/iesg/tests.py index 42e838ebdf..86910bc0ce 100644 --- a/ietf/iesg/tests.py +++ b/ietf/iesg/tests.py @@ -87,9 +87,18 @@ def test_milestones_needing_review_ordering(self): group=dated_group, person=Person.objects.get(user__username='ad'), ) - dated_milestones = DatedGroupMilestoneFactory.create_batch( - 2, group=dated_group, state_id="review" - ) + dated_milestones = [ + DatedGroupMilestoneFactory( + group=dated_group, + state_id="review", + desc="This is the description of one dated group milestone", + ), + DatedGroupMilestoneFactory( + group=dated_group, + state_id="review", + desc="This is the description of another dated group milestone", + ), + ] dated_milestones[0].due -= datetime.timedelta(days=1) # make this one earlier dated_milestones[0].save() @@ -99,9 +108,18 @@ def test_milestones_needing_review_ordering(self): group=dateless_group, person=Person.objects.get(user__username='ad'), ) - dateless_milestones = DatelessGroupMilestoneFactory.create_batch( - 2, group=dateless_group, state_id="review" - ) + dateless_milestones = [ + DatelessGroupMilestoneFactory( + group=dateless_group, + state_id="review", + desc="This is the description of one dateless group milestone", + ), + DatelessGroupMilestoneFactory( + group=dateless_group, + state_id="review", + desc="This is the description of another dateless group milestone", + ), + ] url = urlreverse("ietf.iesg.views.milestones_needing_review") self.client.login(username="ad", password="ad+password") @@ -111,17 +129,29 @@ def test_milestones_needing_review_ordering(self): # check order-by-date dated_tbody = pq(f'td:contains("{dated_milestones[0].desc}")').closest("tbody") - next_td = dated_tbody.find('td:contains("Next")') - self.assertEqual(next_td.siblings()[0].text.strip(), dated_milestones[0].desc) - last_td = dated_tbody.find('td:contains("Last")') - self.assertEqual(last_td.siblings()[0].text.strip(), dated_milestones[1].desc) + rows = list(dated_tbody.items("tr")) # keep as pyquery objects + self.assertTrue(rows[0].find('td:first:contains("Last")')) # Last milestone shown first + self.assertFalse(rows[0].find('td:first:contains("Next")')) + self.assertTrue(rows[0].find(f'td:contains("{dated_milestones[1].desc}")')) + self.assertFalse(rows[0].find(f'td:contains("{dated_milestones[0].desc}")')) + + self.assertFalse(rows[1].find('td:first:contains("Last")')) # Last milestone shown first + self.assertTrue(rows[1].find('td:first:contains("Next")')) + self.assertFalse(rows[1].find(f'td:contains("{dated_milestones[1].desc}")')) + self.assertTrue(rows[1].find(f'td:contains("{dated_milestones[0].desc}")')) # check order-by-order dateless_tbody = pq(f'td:contains("{dateless_milestones[0].desc}")').closest("tbody") - next_td = dateless_tbody.find('td:contains("Next")') - self.assertEqual(next_td.siblings()[0].text.strip(), dateless_milestones[0].desc) - last_td = dateless_tbody.find('td:contains("Last")') - self.assertEqual(last_td.siblings()[0].text.strip(), dateless_milestones[1].desc) + rows = list(dateless_tbody.items("tr")) # keep as pyquery objects + self.assertTrue(rows[0].find('td:first:contains("Last")')) # Last milestone shown first + self.assertFalse(rows[0].find('td:first:contains("Next")')) + self.assertTrue(rows[0].find(f'td:contains("{dateless_milestones[1].desc}")')) + self.assertFalse(rows[0].find(f'td:contains("{dateless_milestones[0].desc}")')) + + self.assertFalse(rows[1].find('td:first:contains("Last")')) # Last milestone shown first + self.assertTrue(rows[1].find('td:first:contains("Next")')) + self.assertFalse(rows[1].find(f'td:contains("{dateless_milestones[1].desc}")')) + self.assertTrue(rows[1].find(f'td:contains("{dateless_milestones[0].desc}")')) def test_review_decisions(self): From f7e0a67095df4f89739a38ff5d288be739d8c238 Mon Sep 17 00:00:00 2001 From: Sangho Na Date: Thu, 10 Oct 2024 08:49:05 +1300 Subject: [PATCH 06/41] fix: turn off automatic escaping in session request templates (#8007) * fix: turn off automatic escaping in session request templates * test: ensure no unwanted escaping in session request emails * test: use PersonFactory to create test users * test: minor refactoring --- ietf/secr/sreq/tests.py | 56 ++++++++++++++++++- .../sreq/session_cancel_notification.txt | 5 +- .../sreq/session_request_notification.txt | 4 +- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/ietf/secr/sreq/tests.py b/ietf/secr/sreq/tests.py index 7fb13f1796..847b993e1c 100644 --- a/ietf/secr/sreq/tests.py +++ b/ietf/secr/sreq/tests.py @@ -13,9 +13,10 @@ from ietf.meeting.models import Session, ResourceAssociation, SchedulingEvent, Constraint from ietf.meeting.factories import MeetingFactory, SessionFactory from ietf.name.models import ConstraintName, TimerangeName +from ietf.person.factories import PersonFactory from ietf.person.models import Person from ietf.secr.sreq.forms import SessionForm -from ietf.utils.mail import outbox, empty_outbox, get_payload_text +from ietf.utils.mail import outbox, empty_outbox, get_payload_text, send_mail from ietf.utils.timezone import date_today @@ -78,6 +79,32 @@ def test_cancel(self): self.assertRedirects(r,reverse('ietf.secr.sreq.views.main')) self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'deleted') + def test_cancel_notification_msg(self): + to = "" + subject = "Dummy subject" + template = "sreq/session_cancel_notification.txt" + meeting = MeetingFactory(type_id="ietf", date=date_today()) + requester = PersonFactory(name="James O'Rourke", user__username="jimorourke") + context = {"meeting": meeting, "requester": requester} + cc = "cc.a@example.com, cc.b@example.com" + bcc = "bcc@example.com" + + msg = send_mail( + None, + to, + None, + subject, + template, + context, + cc=cc, + bcc=bcc, + ) + self.assertEqual(requester.name, "James O'Rourke") # note ' (single quote) in the name + self.assertIn( + f"A request to cancel a meeting session has just been submitted by {requester.name}.", + get_payload_text(msg), + ) + def test_edit(self): meeting = MeetingFactory(type_id='ietf', date=date_today()) mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group @@ -701,6 +728,33 @@ def test_request_notification(self): self.assertNotIn('1 Hour, 1 Hour, 1 Hour', notification_payload) self.assertNotIn('The third session requires your approval', notification_payload) + def test_request_notification_msg(self): + to = "" + subject = "Dummy subject" + template = "sreq/session_request_notification.txt" + header = "A new" + meeting = MeetingFactory(type_id="ietf", date=date_today()) + requester = PersonFactory(name="James O'Rourke", user__username="jimorourke") + context = {"header": header, "meeting": meeting, "requester": requester} + cc = "cc.a@example.com, cc.b@example.com" + bcc = "bcc@example.com" + + msg = send_mail( + None, + to, + None, + subject, + template, + context, + cc=cc, + bcc=bcc, + ) + self.assertEqual(requester.name, "James O'Rourke") # note ' (single quote) in the name + self.assertIn( + f"{header} meeting session request has just been submitted by {requester.name}.", + get_payload_text(msg), + ) + def test_request_notification_third_session(self): meeting = MeetingFactory(type_id='ietf', date=date_today()) ad = Person.objects.get(user__username='ad') diff --git a/ietf/secr/templates/sreq/session_cancel_notification.txt b/ietf/secr/templates/sreq/session_cancel_notification.txt index 3e6dd43f69..8aee6c89db 100644 --- a/ietf/secr/templates/sreq/session_cancel_notification.txt +++ b/ietf/secr/templates/sreq/session_cancel_notification.txt @@ -1,4 +1,3 @@ -{% load ams_filters %} - -A request to cancel a meeting session has just been submitted by {{ requester }}. +{% autoescape off %}{% load ams_filters %} +A request to cancel a meeting session has just been submitted by {{ requester }}.{% endautoescape %} diff --git a/ietf/secr/templates/sreq/session_request_notification.txt b/ietf/secr/templates/sreq/session_request_notification.txt index a41f202447..75f2cbbae4 100644 --- a/ietf/secr/templates/sreq/session_request_notification.txt +++ b/ietf/secr/templates/sreq/session_request_notification.txt @@ -1,5 +1,5 @@ -{% load ams_filters %} +{% autoescape off %}{% load ams_filters %} {% filter wordwrap:78 %}{{ header }} meeting session request has just been submitted by {{ requester }}.{% endfilter %} -{% include "includes/session_info.txt" %} +{% include "includes/session_info.txt" %}{% endautoescape %} From 148fa2f9f62d04fd11d2bac74c5ec1727a13a91b Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Thu, 10 Oct 2024 14:23:25 -0300 Subject: [PATCH 07/41] fix: decode header fields for alert msg (#8034) * style: Black * chore: type hints * refactor: % to f-string * refactor: helper to decode header values * fix: decode header fields for pop-up msg * test: add tests * fix: use truthiness check We want to suppress empty strings, too... * refactor: use f-string for time formatting * test: clarify side_effect intention --- ietf/utils/mail.py | 68 ++++++++++++++++----- ietf/utils/tests.py | 143 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 195 insertions(+), 16 deletions(-) diff --git a/ietf/utils/mail.py b/ietf/utils/mail.py index 4585fdb846..5417161451 100644 --- a/ietf/utils/mail.py +++ b/ietf/utils/mail.py @@ -19,11 +19,13 @@ from email.header import Header, decode_header from email import message_from_bytes, message_from_string from email import charset as Charset +from typing import Optional from django.conf import settings from django.contrib import messages from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.validators import validate_email +from django.http import HttpRequest from django.template.loader import render_to_string from django.template import Context,RequestContext from django.utils import timezone @@ -64,6 +66,18 @@ def add_headers(msg): msg['From'] = settings.DEFAULT_FROM_EMAIL return msg + +def decode_header_value(value: str) -> str: + """Decode a header value + + Easier-to-use wrapper around email.message.decode_header() + """ + return "".join( + part.decode(charset if charset else "utf-8") if isinstance(part, bytes) else part + for part, charset in decode_header(value) + ) + + class SMTPSomeRefusedRecipients(smtplib.SMTPException): def __init__(self, message, original_msg, refusals): @@ -251,8 +265,7 @@ def parseaddr(addr): """ - addr = ''.join( [ ( s.decode(m) if m else s.decode()) if isinstance(s, bytes) else s for (s,m) in decode_header(addr) ] ) - name, addr = simple_parseaddr(addr) + name, addr = simple_parseaddr(decode_header_value(addr)) return name, addr def excludeaddrs(addrlist, exlist): @@ -330,18 +343,45 @@ def condition_message(to, frm, subject, msg, cc, extra): msg['Message-ID'] = make_msgid() -def show_that_mail_was_sent(request,leadline,msg,bcc): - if request and request.user: - from ietf.ietfauth.utils import has_role - if has_role(request.user,['Area Director','Secretariat','IANA','RFC Editor','ISE','IAD','IRTF Chair','WG Chair','RG Chair','WG Secretary','RG Secretary']): - info = "%s at %s %s\n" % (leadline,timezone.now().strftime("%Y-%m-%d %H:%M:%S"),settings.TIME_ZONE) - info += "Subject: %s\n" % force_str(msg.get('Subject','[no subject]')) - info += "To: %s\n" % msg.get('To','[no to]') - if msg.get('Cc'): - info += "Cc: %s\n" % msg.get('Cc') - if bcc: - info += "Bcc: %s\n" % bcc - messages.info(request,info,extra_tags='preformatted',fail_silently=True) +def show_that_mail_was_sent(request: HttpRequest, leadline: str, msg: Message, bcc: Optional[str]): + if request and request.user: + from ietf.ietfauth.utils import has_role + + if has_role( + request.user, + [ + "Area Director", + "Secretariat", + "IANA", + "RFC Editor", + "ISE", + "IAD", + "IRTF Chair", + "WG Chair", + "RG Chair", + "WG Secretary", + "RG Secretary", + ], + ): + subject = decode_header_value(msg.get("Subject", "[no subject]")) + _to = decode_header_value(msg.get("To", "[no to]")) + info_lines = [ + f"{leadline} at {timezone.now():%Y-%m-%d %H:%M:%S %Z}", + f"Subject: {subject}", + f"To: {_to}", + ] + cc = msg.get("Cc", None) + if cc: + info_lines.append(f"Cc: {decode_header_value(cc)}") + if bcc: + info_lines.append(f"Bcc: {decode_header_value(bcc)}") + messages.info( + request, + "\n".join(info_lines), + extra_tags="preformatted", + fail_silently=True, + ) + def save_as_message(request, msg, bcc): by = ((request and request.user and not request.user.is_anonymous and request.user.person) diff --git a/ietf/utils/tests.py b/ietf/utils/tests.py index decdd778d9..0a1986a608 100644 --- a/ietf/utils/tests.py +++ b/ietf/utils/tests.py @@ -11,10 +11,11 @@ import shutil import types -from mock import patch +from mock import call, patch from pyquery import PyQuery from typing import Dict, List # pyflakes:ignore +from email.message import Message from email.mime.image import MIMEImage from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText @@ -32,6 +33,7 @@ from django.template.defaulttags import URLNode from django.template.loader import get_template, render_to_string from django.templatetags.static import StaticNode +from django.test import RequestFactory from django.urls import reverse as urlreverse import debug # pyflakes:ignore @@ -42,7 +44,15 @@ from ietf.utils.draft import PlaintextDraft, getmeta from ietf.utils.fields import SearchableField from ietf.utils.log import unreachable, assertion -from ietf.utils.mail import send_mail_preformatted, send_mail_text, send_mail_mime, outbox, get_payload_text +from ietf.utils.mail import ( + send_mail_preformatted, + send_mail_text, + send_mail_mime, + outbox, + get_payload_text, + decode_header_value, + show_that_mail_was_sent, +) from ietf.utils.test_runner import get_template_paths, set_coverage_checking from ietf.utils.test_utils import TestCase, unicontent from ietf.utils.text import parse_unicode @@ -109,6 +119,135 @@ def test_send_mail_preformatted(self): recv = outbox[-1] self.assertEqual(recv['Fuzz'], 'bucket, monger') + +class MailUtilsTests(TestCase): + def test_decode_header_value(self): + self.assertEqual( + decode_header_value("cake"), + "cake", + "decodes simple string value", + ) + self.assertEqual( + decode_header_value("=?utf-8?b?8J+Ogg==?="), + "\U0001f382", + "decodes single utf-8-encoded part", + ) + self.assertEqual( + decode_header_value("=?utf-8?b?8J+Ogg==?= = =?macintosh?b?jYxrjg==?="), + "\U0001f382 = çåké", + "decodes a value with non-utf-8 encodings", + ) + + # Patch in a side_effect so we can distinguish values that came from decode_header_value. + @patch("ietf.utils.mail.decode_header_value", side_effect=lambda s: f"decoded-{s}") + @patch("ietf.utils.mail.messages") + def test_show_that_mail_was_sent(self, mock_messages, mock_decode_header_value): + request = RequestFactory().get("/some/path") + request.user = object() # just needs to exist + msg = Message() + msg["To"] = "to-value" + msg["Subject"] = "subject-value" + msg["Cc"] = "cc-value" + with patch("ietf.ietfauth.utils.has_role", return_value=True): + show_that_mail_was_sent(request, "mail was sent", msg, "bcc-value") + self.assertCountEqual( + mock_decode_header_value.call_args_list, + [call("to-value"), call("subject-value"), call("cc-value"), call("bcc-value")], + ) + self.assertEqual(mock_messages.info.call_args[0][0], request) + self.assertIn("mail was sent", mock_messages.info.call_args[0][1]) + self.assertIn("decoded-subject-value", mock_messages.info.call_args[0][1]) + self.assertIn("decoded-to-value", mock_messages.info.call_args[0][1]) + self.assertIn("decoded-cc-value", mock_messages.info.call_args[0][1]) + self.assertIn("decoded-bcc-value", mock_messages.info.call_args[0][1]) + mock_messages.reset_mock() + mock_decode_header_value.reset_mock() + + # no bcc + with patch("ietf.ietfauth.utils.has_role", return_value=True): + show_that_mail_was_sent(request, "mail was sent", msg, None) + self.assertCountEqual( + mock_decode_header_value.call_args_list, + [call("to-value"), call("subject-value"), call("cc-value")], + ) + self.assertEqual(mock_messages.info.call_args[0][0], request) + self.assertIn("mail was sent", mock_messages.info.call_args[0][1]) + self.assertIn("decoded-subject-value", mock_messages.info.call_args[0][1]) + self.assertIn("decoded-to-value", mock_messages.info.call_args[0][1]) + self.assertIn("decoded-cc-value", mock_messages.info.call_args[0][1]) + # Note: here and below - when using assertNotIn(), leaving off the "decoded-" prefix + # proves that neither the original value nor the decoded value appear. + self.assertNotIn("bcc-value", mock_messages.info.call_args[0][1]) + mock_messages.reset_mock() + mock_decode_header_value.reset_mock() + + # no cc + del msg["Cc"] + with patch("ietf.ietfauth.utils.has_role", return_value=True): + show_that_mail_was_sent(request, "mail was sent", msg, None) + self.assertCountEqual( + mock_decode_header_value.call_args_list, + [call("to-value"), call("subject-value")], + ) + self.assertEqual(mock_messages.info.call_args[0][0], request) + self.assertIn("mail was sent", mock_messages.info.call_args[0][1]) + self.assertIn("decoded-subject-value", mock_messages.info.call_args[0][1]) + self.assertIn("decoded-to-value", mock_messages.info.call_args[0][1]) + self.assertNotIn("cc-value", mock_messages.info.call_args[0][1]) + self.assertNotIn("bcc-value", mock_messages.info.call_args[0][1]) + mock_messages.reset_mock() + mock_decode_header_value.reset_mock() + + # no to + del msg["To"] + with patch("ietf.ietfauth.utils.has_role", return_value=True): + show_that_mail_was_sent(request, "mail was sent", msg, None) + self.assertCountEqual( + mock_decode_header_value.call_args_list, + [call("[no to]"), call("subject-value")], + ) + self.assertEqual(mock_messages.info.call_args[0][0], request) + self.assertIn("mail was sent", mock_messages.info.call_args[0][1]) + self.assertIn("decoded-subject-value", mock_messages.info.call_args[0][1]) + self.assertIn("decoded-[no to]", mock_messages.info.call_args[0][1]) + self.assertNotIn("to-value", mock_messages.info.call_args[0][1]) + self.assertNotIn("cc-value", mock_messages.info.call_args[0][1]) + self.assertNotIn("bcc-value", mock_messages.info.call_args[0][1]) + mock_messages.reset_mock() + mock_decode_header_value.reset_mock() + + # no subject + del msg["Subject"] + with patch("ietf.ietfauth.utils.has_role", return_value=True): + show_that_mail_was_sent(request, "mail was sent", msg, None) + self.assertCountEqual( + mock_decode_header_value.call_args_list, + [call("[no to]"), call("[no subject]")], + ) + self.assertEqual(mock_messages.info.call_args[0][0], request) + self.assertIn("mail was sent", mock_messages.info.call_args[0][1]) + self.assertIn("decoded-[no subject]", mock_messages.info.call_args[0][1]) + self.assertNotIn("subject-value", mock_messages.info.call_args[0][1]) + self.assertIn("decoded-[no to]", mock_messages.info.call_args[0][1]) + self.assertNotIn("to-value", mock_messages.info.call_args[0][1]) + self.assertNotIn("cc-value", mock_messages.info.call_args[0][1]) + self.assertNotIn("bcc-value", mock_messages.info.call_args[0][1]) + mock_messages.reset_mock() + mock_decode_header_value.reset_mock() + + # user does not have role + with patch("ietf.ietfauth.utils.has_role", return_value=False): + show_that_mail_was_sent(request, "mail was sent", msg, None) + self.assertFalse(mock_messages.called) + + # no user + request.user = None + with patch("ietf.ietfauth.utils.has_role", return_value=True) as mock_has_role: + show_that_mail_was_sent(request, "mail was sent", msg, None) + self.assertFalse(mock_messages.called) + self.assertFalse(mock_has_role.called) + + class TestSMTPServer(TestCase): def test_address_rejected(self): From 83f5fc8d1adbad3da99bb95dbbc15110be69e166 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Fri, 11 Oct 2024 12:24:35 -0500 Subject: [PATCH 08/41] fix: add exception to last<->first name swap (#8043) --- ietf/person/name.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/person/name.py b/ietf/person/name.py index dc57f58f4b..0dbeaa9b99 100644 --- a/ietf/person/name.py +++ b/ietf/person/name.py @@ -59,7 +59,7 @@ def name_parts(name): last = parts[0] if len(parts) >= 2: # Handle reverse-order names with uppercase surname correctly - if len(first)>1 and re.search("^[A-Z-]+$", first): + if len(first)>1 and re.search("^[A-Z-]+$", first) and first != "JP": first, last = last, first.capitalize() # Handle exception for RFC Editor if (prefix, first, middle, last, suffix) == ('', 'Editor', '', 'Rfc', ''): From 2d2e879bd15e2cfb71dbaf9c05a29edf01883343 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Mon, 14 Oct 2024 12:22:19 -0300 Subject: [PATCH 09/41] fix: versioned href for slides URL (#8040) --- ietf/meeting/views.py | 2 +- ietf/utils/meetecho.py | 14 ++++++-------- ietf/utils/tests_meetecho.py | 8 ++++---- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 211cdec9a5..6e6c4dfe23 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -1840,7 +1840,7 @@ def agenda_extract_slide(item): "id": item.id, "title": item.title, "rev": item.rev, - "url": item.get_versionless_href(), + "url": item.get_href(), "ext": item.file_extension(), } diff --git a/ietf/utils/meetecho.py b/ietf/utils/meetecho.py index 2f5f146766..0dbf75736a 100644 --- a/ietf/utils/meetecho.py +++ b/ietf/utils/meetecho.py @@ -481,12 +481,10 @@ def delete_conference(self, conf: Conference): class SlidesManager(Manager): """Interface between Datatracker models and Meetecho API - Note: The URL we send comes from get_versionless_href(). This should match what we use as the - URL in api_get_session_materials(). Additionally, it _must_ give the right result for a Document - instance that has not yet been persisted to the database. This is because upload_session_slides() - (as of 2024-03-07) SessionPresentations before saving its updated Documents. This means, for - example, using get_absolute_url() will cause bugs. (We should refactor upload_session_slides() to - avoid this requirement.) + Note: the URL sent for a slide deck comes from DocumentInfo.get_href() and includes the revision + of the slides being sent. Be sure that 1) the URL matches what api_get_session_materials() returns + for the slides; and 2) the URL is valid if it is fetched immediately - possibly even before the call + to SlidesManager.add() or send_update() returns. """ def __init__(self, api_config): @@ -521,7 +519,7 @@ def add(self, session: "Session", slides: "Document", order: int): deck={ "id": slides.pk, "title": slides.title, - "url": slides.get_versionless_href(), # see above note re: get_versionless_href() + "url": slides.get_href(), "rev": slides.rev, "order": order, } @@ -575,7 +573,7 @@ def send_update(self, session: "Session"): { "id": deck.document.pk, "title": deck.document.title, - "url": deck.document.get_versionless_href(), # see note above re: get_versionless_href() + "url": deck.document.get_href(), "rev": deck.document.rev, "order": deck.order, } diff --git a/ietf/utils/tests_meetecho.py b/ietf/utils/tests_meetecho.py index 1aef5894e2..a10ac68c27 100644 --- a/ietf/utils/tests_meetecho.py +++ b/ietf/utils/tests_meetecho.py @@ -558,7 +558,7 @@ def test_add(self, mock_add, mock_wg_token): deck={ "id": slides_doc.pk, "title": slides_doc.title, - "url": slides_doc.get_versionless_href(), + "url": slides_doc.get_href(session.meeting), "rev": slides_doc.rev, "order": 13, }, @@ -597,7 +597,7 @@ def test_delete(self, mock_delete, mock_update, mock_wg_token): { "id": slides_doc.pk, "title": slides_doc.title, - "url": slides_doc.get_versionless_href(), + "url": slides_doc.get_href(session.meeting), "rev": slides_doc.rev, "order": 1, }, @@ -635,7 +635,7 @@ def test_revise(self, mock_add, mock_delete, mock_wg_token): deck={ "id": slides_doc.pk, "title": slides_doc.title, - "url": slides_doc.get_versionless_href(), + "url": slides_doc.get_href(slides.session.meeting), "rev": slides_doc.rev, "order": 23, }, @@ -660,7 +660,7 @@ def test_send_update(self, mock_send_update, mock_wg_token): { "id": slides.document_id, "title": slides.document.title, - "url": slides.document.get_versionless_href(), + "url": slides.document.get_href(slides.session.meeting), "rev": slides.document.rev, "order": 0, } From 166340842ed3e4927cd8dd174aecdce8a4df50aa Mon Sep 17 00:00:00 2001 From: Martin Thomson Date: Tue, 15 Oct 2024 07:24:49 +1100 Subject: [PATCH 10/41] fix: Set overflow-y to clip for pre (#8047) Browsers treat an overflow: auto directive as a request to make an element scrollable, even if the element does not need it. This results in scrolling being confined to that element sometimes, such as when a document is scrolled using a mouse wheel if the document scrolls the pointer into that element. The result is that scrolling can be "trapped" on an element that doesn't really need to scroll. This disables vertical scrolling for those pre elements that might trap scrolling in this way. --- ietf/static/css/document_html_txt.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/static/css/document_html_txt.scss b/ietf/static/css/document_html_txt.scss index b0fec7c4d6..a5991056c9 100644 --- a/ietf/static/css/document_html_txt.scss +++ b/ietf/static/css/document_html_txt.scss @@ -344,7 +344,7 @@ div:is(.artwork, .sourcecode) pre { flex: 0 0 content; margin: 0; max-width: 72ch; - overflow: auto; + overflow: auto clip; } div:is(.artwork, .sourcecode) .pilcrow { flex: 0 0 1ch; From 68ceea9c060fa0f13bef2b9efe225b41bc6ff519 Mon Sep 17 00:00:00 2001 From: Matthew Holloway Date: Wed, 16 Oct 2024 08:38:02 +1300 Subject: [PATCH 11/41] chore: Bypass Firefox for Playwright test due to flakiness (#8050) --- playwright/tests/status/status.spec.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/playwright/tests/status/status.spec.js b/playwright/tests/status/status.spec.js index 7b3b90bfa4..daef7a88f1 100644 --- a/playwright/tests/status/status.spec.js +++ b/playwright/tests/status/status.spec.js @@ -20,6 +20,10 @@ test.describe('site status', () => { by: 'Exile is a cool Amiga game' } + test.beforeEach(({ browserName }) => { + test.skip(browserName === 'firefox', 'bypassing flaky tests on Firefox') + }) + test('Renders server status as Notification', async ({ page }) => { await page.route('/status/latest.json', route => { route.fulfill({ From 34363040cd47d2f7ee407b56f84a44bf256c2af2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:58:14 -0400 Subject: [PATCH 12/41] build(deps): bump appleboy/ssh-action from 1.0.3 to 1.1.0 (#8019) Bumps [appleboy/ssh-action](https://github.com/appleboy/ssh-action) from 1.0.3 to 1.1.0. - [Release notes](https://github.com/appleboy/ssh-action/releases) - [Changelog](https://github.com/appleboy/ssh-action/blob/master/.goreleaser.yaml) - [Commits](https://github.com/appleboy/ssh-action/compare/029f5b4aeeeb58fdfe1410a5d17f967dacf36262...25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1) --- updated-dependencies: - dependency-name: appleboy/ssh-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/tests-az.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-az.yml b/.github/workflows/tests-az.yml index bfd1fdd584..3f828430a9 100644 --- a/.github/workflows/tests-az.yml +++ b/.github/workflows/tests-az.yml @@ -38,7 +38,7 @@ jobs: ssh-keyscan -t rsa $vminfo >> ~/.ssh/known_hosts - name: Remote SSH into VM - uses: appleboy/ssh-action@029f5b4aeeeb58fdfe1410a5d17f967dacf36262 + uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: From c37fec2f1c665cae18d0e96418d6617e0722521b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:58:52 -0400 Subject: [PATCH 13/41] build(deps): bump codecov/codecov-action from 4.5.0 to 4.6.0 (#8020) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.5.0 to 4.6.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4.5.0...v4.6.0) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 616ffdcc63..64db03db09 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -59,7 +59,7 @@ jobs: path: geckodriver.log - name: Upload Coverage Results to Codecov - uses: codecov/codecov-action@v4.5.0 + uses: codecov/codecov-action@v4.6.0 with: files: coverage.xml From 39ba425089a2ec9b24b5c8ac8b07a57ded62f911 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:59:22 -0400 Subject: [PATCH 14/41] build(deps): bump the npm group across 1 directory with 2 updates (#8021) Bumps the npm group with 2 updates in the /dev/coverage-action directory: [@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core) and [luxon](https://github.com/moment/luxon). Updates `@actions/core` from 1.10.1 to 1.11.1 - [Changelog](https://github.com/actions/toolkit/blob/main/packages/core/RELEASES.md) - [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/core) Updates `luxon` from 3.4.4 to 3.5.0 - [Changelog](https://github.com/moment/luxon/blob/master/CHANGELOG.md) - [Commits](https://github.com/moment/luxon/compare/3.4.4...3.5.0) --- updated-dependencies: - dependency-name: "@actions/core" dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm - dependency-name: luxon dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dev/coverage-action/package-lock.json | 75 ++++++++++++++++----------- dev/coverage-action/package.json | 4 +- 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/dev/coverage-action/package-lock.json b/dev/coverage-action/package-lock.json index 8c9b97e026..f23a9b66c7 100644 --- a/dev/coverage-action/package-lock.json +++ b/dev/coverage-action/package-lock.json @@ -9,19 +9,27 @@ "version": "1.0.0", "license": "BSD-3-Clause", "dependencies": { - "@actions/core": "1.10.1", + "@actions/core": "1.11.1", "@actions/github": "6.0.0", "lodash": "4.17.21", - "luxon": "3.4.4" + "luxon": "3.5.0" } }, "node_modules/@actions/core": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz", - "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", "dependencies": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dependencies": { + "@actions/io": "^1.0.1" } }, "node_modules/@actions/github": { @@ -44,6 +52,11 @@ "undici": "^5.25.4" } }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==" + }, "node_modules/@fastify/busboy": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", @@ -196,9 +209,9 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", "engines": { "node": ">=12" } @@ -235,14 +248,6 @@ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -251,12 +256,20 @@ }, "dependencies": { "@actions/core": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz", - "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", "requires": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "requires": { + "@actions/io": "^1.0.1" } }, "@actions/github": { @@ -279,6 +292,11 @@ "undici": "^5.25.4" } }, + "@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==" + }, "@fastify/busboy": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", @@ -395,9 +413,9 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==" + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==" }, "once": { "version": "1.4.0", @@ -425,11 +443,6 @@ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/dev/coverage-action/package.json b/dev/coverage-action/package.json index 0a8794d2a2..1d03fe3dab 100644 --- a/dev/coverage-action/package.json +++ b/dev/coverage-action/package.json @@ -6,9 +6,9 @@ "author": "IETF Trust", "license": "BSD-3-Clause", "dependencies": { - "@actions/core": "1.10.1", + "@actions/core": "1.11.1", "@actions/github": "6.0.0", "lodash": "4.17.21", - "luxon": "3.4.4" + "luxon": "3.5.0" } } From b1b14024e3a3291c334b867f8a5e5cd8842f0fec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:00:38 -0400 Subject: [PATCH 15/41] chore(deps): bump slackapi/slack-github-action from 1.26.0 to 1.27.0 (#7885) Bumps [slackapi/slack-github-action](https://github.com/slackapi/slack-github-action) from 1.26.0 to 1.27.0. - [Release notes](https://github.com/slackapi/slack-github-action/releases) - [Commits](https://github.com/slackapi/slack-github-action/compare/v1.26.0...v1.27.0) --- updated-dependencies: - dependency-name: slackapi/slack-github-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .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 8f88c7c22e..3887661cac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -335,7 +335,7 @@ jobs: steps: - name: Notify on Slack (Success) if: ${{ !contains(join(needs.*.result, ','), 'failure') }} - uses: slackapi/slack-github-action@v1.26.0 + uses: slackapi/slack-github-action@v1.27.0 with: channel-id: ${{ secrets.SLACK_GH_BUILDS_CHANNEL_ID }} payload: | @@ -358,7 +358,7 @@ jobs: SLACK_BOT_TOKEN: ${{ secrets.SLACK_GH_BOT }} - name: Notify on Slack (Failure) if: ${{ contains(join(needs.*.result, ','), 'failure') }} - uses: slackapi/slack-github-action@v1.26.0 + uses: slackapi/slack-github-action@v1.27.0 with: channel-id: ${{ secrets.SLACK_GH_BUILDS_CHANNEL_ID }} payload: | From 51f96eb952160997292af32998467e9f90c40782 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:01:15 -0400 Subject: [PATCH 16/41] chore(deps): bump tar in /dev/deploy-to-container in the npm group (#7793) Bumps the npm group in /dev/deploy-to-container with 1 update: [tar](https://github.com/isaacs/node-tar). Updates `tar` from 7.4.0 to 7.4.3 - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v7.4.0...v7.4.3) --- updated-dependencies: - dependency-name: tar dependency-type: direct:production update-type: version-update:semver-patch dependency-group: npm ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dev/deploy-to-container/package-lock.json | 14 +++++++------- dev/deploy-to-container/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dev/deploy-to-container/package-lock.json b/dev/deploy-to-container/package-lock.json index 7b86986d56..f636a45f05 100644 --- a/dev/deploy-to-container/package-lock.json +++ b/dev/deploy-to-container/package-lock.json @@ -11,7 +11,7 @@ "nanoid": "5.0.7", "nanoid-dictionary": "5.0.0-beta.1", "slugify": "1.6.6", - "tar": "^7.4.0", + "tar": "^7.4.3", "yargs": "^17.7.2" }, "engines": { @@ -788,9 +788,9 @@ } }, "node_modules/tar": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.0.tgz", - "integrity": "sha512-XQs0S8fuAkQWuqhDeCdMlJXDX80D7EOVLDPVFkna9yQfzS+PHKgfxcei0jf6/+QAWcjqrnC8uM3fSAnrQl+XYg==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -1503,9 +1503,9 @@ } }, "tar": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.0.tgz", - "integrity": "sha512-XQs0S8fuAkQWuqhDeCdMlJXDX80D7EOVLDPVFkna9yQfzS+PHKgfxcei0jf6/+QAWcjqrnC8uM3fSAnrQl+XYg==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "requires": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", diff --git a/dev/deploy-to-container/package.json b/dev/deploy-to-container/package.json index 2f582dd2a7..be77fa5cce 100644 --- a/dev/deploy-to-container/package.json +++ b/dev/deploy-to-container/package.json @@ -7,7 +7,7 @@ "nanoid": "5.0.7", "nanoid-dictionary": "5.0.0-beta.1", "slugify": "1.6.6", - "tar": "^7.4.0", + "tar": "^7.4.3", "yargs": "^17.7.2" }, "engines": { From db4ff66fbd3950c280ac9334a8718d6de4f10a5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:02:08 -0400 Subject: [PATCH 17/41] chore(deps): bump the npm group across 1 directory with 2 updates (#7789) Bumps the npm group with 2 updates in the /dev/diff directory: [luxon](https://github.com/moment/luxon) and [tar](https://github.com/isaacs/node-tar). Updates `luxon` from 3.4.4 to 3.5.0 - [Changelog](https://github.com/moment/luxon/blob/master/CHANGELOG.md) - [Commits](https://github.com/moment/luxon/compare/3.4.4...3.5.0) Updates `tar` from 7.4.0 to 7.4.3 - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v7.4.0...v7.4.3) --- updated-dependencies: - dependency-name: luxon dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm - dependency-name: tar dependency-type: direct:production update-type: version-update:semver-patch dependency-group: npm ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dev/diff/package-lock.json | 28 ++++++++++++++-------------- dev/diff/package.json | 4 ++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/dev/diff/package-lock.json b/dev/diff/package-lock.json index 3500ccec48..87aaf3f16f 100644 --- a/dev/diff/package-lock.json +++ b/dev/diff/package-lock.json @@ -15,9 +15,9 @@ "keypress": "^0.2.1", "listr2": "^6.6.1", "lodash-es": "^4.17.21", - "luxon": "^3.4.4", + "luxon": "^3.5.0", "pretty-bytes": "^6.1.1", - "tar": "^7.4.0", + "tar": "^7.4.3", "yargs": "^17.7.2" }, "engines": { @@ -1060,9 +1060,9 @@ } }, "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", "engines": { "node": ">=12" } @@ -1493,9 +1493,9 @@ } }, "node_modules/tar": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.0.tgz", - "integrity": "sha512-XQs0S8fuAkQWuqhDeCdMlJXDX80D7EOVLDPVFkna9yQfzS+PHKgfxcei0jf6/+QAWcjqrnC8uM3fSAnrQl+XYg==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -2410,9 +2410,9 @@ "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==" }, "luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==" + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==" }, "mimic-fn": { "version": "2.1.0", @@ -2691,9 +2691,9 @@ } }, "tar": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.0.tgz", - "integrity": "sha512-XQs0S8fuAkQWuqhDeCdMlJXDX80D7EOVLDPVFkna9yQfzS+PHKgfxcei0jf6/+QAWcjqrnC8uM3fSAnrQl+XYg==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "requires": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", diff --git a/dev/diff/package.json b/dev/diff/package.json index 1b5540e346..4e0e1be8f4 100644 --- a/dev/diff/package.json +++ b/dev/diff/package.json @@ -11,9 +11,9 @@ "keypress": "^0.2.1", "listr2": "^6.6.1", "lodash-es": "^4.17.21", - "luxon": "^3.4.4", + "luxon": "^3.5.0", "pretty-bytes": "^6.1.1", - "tar": "^7.4.0", + "tar": "^7.4.3", "yargs": "^17.7.2" }, "engines": { From a338df16f23322b43fc57c5379d0807e61e12984 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Wed, 16 Oct 2024 13:36:54 -0300 Subject: [PATCH 18/41] fix: 404 instead of 300 for ambiguous email_or_person (#8004) * fix: 404 on CommunityList name collision * fix: 404 on ambiuous person for photo() view * test: update tests --------- Co-authored-by: Robert Sparks --- ietf/community/tests.py | 4 +-- ietf/community/views.py | 54 ++++++++++++----------------------------- ietf/person/tests.py | 4 +-- ietf/person/views.py | 2 +- 4 files changed, 19 insertions(+), 45 deletions(-) diff --git a/ietf/community/tests.py b/ietf/community/tests.py index 181e9e0fa6..5329267d8e 100644 --- a/ietf/community/tests.py +++ b/ietf/community/tests.py @@ -95,9 +95,7 @@ def test_view_list_duplicates(self): url = urlreverse(ietf.community.views.view_list, kwargs={ "email_or_name": person.plain_name()}) r = self.client.get(url) - self.assertEqual(r.status_code, 300) - self.assertIn("bazquux@example.com", r.content.decode()) - self.assertIn("foobar@example.com", r.content.decode()) + self.assertEqual(r.status_code, 404) def complex_person(self, *args, **kwargs): person = PersonFactory(*args, **kwargs) diff --git a/ietf/community/views.py b/ietf/community/views.py index 78b8144d60..923ec556f3 100644 --- a/ietf/community/views.py +++ b/ietf/community/views.py @@ -28,11 +28,14 @@ from ietf.utils.http import is_ajax from ietf.utils.response import permission_denied -class MultiplePersonError(Exception): - """More than one Person record matches the given email or name""" - pass def lookup_community_list(request, email_or_name=None, acronym=None): + """Finds a CommunityList for a person or group + + Instantiates an unsaved CommunityList if one is not found. + + If the person or group cannot be found and uniquely identified, raises an Http404 exception + """ assert email_or_name or acronym if acronym: @@ -44,19 +47,14 @@ def lookup_community_list(request, email_or_name=None, acronym=None): if hasattr(request.user, 'person') and request.user.person in persons: person = request.user.person else: - raise MultiplePersonError("\r\n".join([p.user.username for p in persons])) + raise Http404(f"Unable to identify the CommunityList for {email_or_name}") else: person = persons[0] clist = CommunityList.objects.filter(person=person).first() or CommunityList(person=person) - return clist def view_list(request, email_or_name=None): - try: - clist = lookup_community_list(request, email_or_name) - except MultiplePersonError as err: - return HttpResponse(str(err), status=300) - + clist = lookup_community_list(request, email_or_name) # may raise Http404 docs = docs_tracked_by_community_list(clist) docs, meta = prepare_document_table(request, docs, request.GET) @@ -76,10 +74,7 @@ def view_list(request, email_or_name=None): def manage_list(request, email_or_name=None, acronym=None): # we need to be a bit careful because clist may not exist in the # database so we can't call related stuff on it yet - try: - clist = lookup_community_list(request, email_or_name, acronym) - except MultiplePersonError as err: - return HttpResponse(str(err), status=300) + clist = lookup_community_list(request, email_or_name, acronym) # may raise Http404 if not can_manage_community_list(request.user, clist): permission_denied(request, "You do not have permission to access this view") @@ -166,10 +161,7 @@ def track_document(request, name, email_or_name=None, acronym=None): doc = get_object_or_404(Document, name=name) if request.method == "POST": - try: - clist = lookup_community_list(request, email_or_name, acronym) - except MultiplePersonError as err: - return HttpResponse(str(err), status=300) + clist = lookup_community_list(request, email_or_name, acronym) # may raise Http404 if not can_manage_community_list(request.user, clist): permission_denied(request, "You do not have permission to access this view") @@ -191,10 +183,7 @@ def track_document(request, name, email_or_name=None, acronym=None): @login_required def untrack_document(request, name, email_or_name=None, acronym=None): doc = get_object_or_404(Document, name=name) - try: - clist = lookup_community_list(request, email_or_name, acronym) - except MultiplePersonError as err: - return HttpResponse(str(err), status=300) + clist = lookup_community_list(request, email_or_name, acronym) # may raise Http404 if not can_manage_community_list(request.user, clist): permission_denied(request, "You do not have permission to access this view") @@ -214,11 +203,7 @@ def untrack_document(request, name, email_or_name=None, acronym=None): @ignore_view_kwargs("group_type") def export_to_csv(request, email_or_name=None, acronym=None): - try: - clist = lookup_community_list(request, email_or_name, acronym) - except MultiplePersonError as err: - return HttpResponse(str(err), status=300) - + clist = lookup_community_list(request, email_or_name, acronym) # may raise Http404 response = HttpResponse(content_type='text/csv') if clist.group: @@ -259,11 +244,7 @@ def export_to_csv(request, email_or_name=None, acronym=None): @ignore_view_kwargs("group_type") def feed(request, email_or_name=None, acronym=None): - try: - clist = lookup_community_list(request, email_or_name, acronym) - except MultiplePersonError as err: - return HttpResponse(str(err), status=300) - + clist = lookup_community_list(request, email_or_name, acronym) # may raise Http404 significant = request.GET.get('significant', '') == '1' documents = docs_tracked_by_community_list(clist).values_list('pk', flat=True) @@ -299,12 +280,9 @@ def feed(request, email_or_name=None, acronym=None): @login_required @ignore_view_kwargs("group_type") def subscription(request, email_or_name=None, acronym=None): - try: - clist = lookup_community_list(request, email_or_name, acronym) - if clist.pk is None: - raise Http404 - except MultiplePersonError as err: - return HttpResponse(str(err), status=300) + clist = lookup_community_list(request, email_or_name, acronym) # may raise Http404 + if clist.pk is None: + raise Http404 person = request.user.person diff --git a/ietf/person/tests.py b/ietf/person/tests.py index 9da201b707..61d9b0ed70 100644 --- a/ietf/person/tests.py +++ b/ietf/person/tests.py @@ -173,9 +173,7 @@ def test_person_photo_duplicates(self): url = urlreverse("ietf.person.views.photo", kwargs={ "email_or_name": person.plain_name()}) r = self.client.get(url) - self.assertEqual(r.status_code, 300) - self.assertIn("bazquux@example.com", r.content.decode()) - self.assertIn("foobar@example.com", r.content.decode()) + self.assertEqual(r.status_code, 404) def test_name_methods(self): person = PersonFactory(name="Dr. Jens F. Möller", ) diff --git a/ietf/person/views.py b/ietf/person/views.py index 6d9daf4a81..bb1fa79f84 100644 --- a/ietf/person/views.py +++ b/ietf/person/views.py @@ -76,7 +76,7 @@ def profile(request, email_or_name): def photo(request, email_or_name): persons = lookup_persons(email_or_name) if len(persons) > 1: - return HttpResponse(r"\r\n".join([p.user.username for p in persons]), status=300) + raise Http404("No photo found") person = persons[0] if not person.photo: raise Http404("No photo found") From de2e66ede3396e40baf6e20caf3f85736c9d50aa Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Thu, 17 Oct 2024 18:23:13 -0300 Subject: [PATCH 19/41] feat: report xml2rfc errors for submissions (#8000) * feat: capture xml2rfc output on exception * fix: chain exception properly * feat: log xml2rfc stderr/stdout May want to pass these back to the submitter, but let's watch them to see what sorts of sanitizing we should do first. * Revert "feat: log xml2rfc stderr/stdout" This reverts commit 959c54d30733a4a3df7ed0250fef347ed6f03a08. * feat: log xml2rfc stderr/stdout This time without reformatting all the imports * feat: path sanitization for submission errors * fix: parentheses in regex * test: test SubmissionError sanitization * style: consolidate imports * fix: apply sanitization to all args --------- Co-authored-by: Robert Sparks --- ietf/submit/tests.py | 38 +++++++++++-- ietf/submit/utils.py | 128 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 144 insertions(+), 22 deletions(-) diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py index ed28c7ef02..21ed6672df 100644 --- a/ietf/submit/tests.py +++ b/ietf/submit/tests.py @@ -27,11 +27,6 @@ from django.utils.encoding import force_str import debug # pyflakes:ignore -from ietf.submit.utils import (expirable_submissions, expire_submission, find_submission_filenames, - post_submission, validate_submission_name, validate_submission_rev, - process_and_accept_uploaded_submission, SubmissionError, process_submission_text, - process_submission_xml, process_uploaded_submission, - process_and_validate_submission) from ietf.doc.factories import (DocumentFactory, WgDraftFactory, IndividualDraftFactory, ReviewFactory, WgRfcFactory) from ietf.doc.models import ( Document, DocEvent, State, @@ -49,7 +44,12 @@ from ietf.submit.forms import SubmissionBaseUploadForm, SubmissionAutoUploadForm from ietf.submit.models import Submission, Preapproval, SubmissionExtResource from ietf.submit.tasks import cancel_stale_submissions, process_and_accept_uploaded_submission_task -from ietf.submit.utils import apply_yang_checker_to_draft, run_all_yang_model_checks +from ietf.submit.utils import (expirable_submissions, expire_submission, find_submission_filenames, + post_submission, validate_submission_name, validate_submission_rev, + process_and_accept_uploaded_submission, SubmissionError, process_submission_text, + process_submission_xml, process_uploaded_submission, + process_and_validate_submission, apply_yang_checker_to_draft, + run_all_yang_model_checks) from ietf.utils import tool_version from ietf.utils.accesstoken import generate_access_token from ietf.utils.mail import outbox, get_payload_text @@ -3384,3 +3384,29 @@ def test_apply_yang_checker_to_draft(self): apply_yang_checker_to_draft(checker, draft) self.assertEqual(checker.check_file_txt.call_args, mock.call(draft.get_file_name())) + +@override_settings(IDSUBMIT_REPOSITORY_PATH="/some/path/", IDSUBMIT_STAGING_PATH="/some/other/path") +class SubmissionErrorTests(TestCase): + def test_sanitize_message(self): + sanitized = SubmissionError.sanitize_message( + "This refers to /some/path/with-a-file\n" + "and also /some/other/path/with-a-different-file isn't that neat?\n" + "and has /some/path//////with-slashes" + ) + self.assertEqual( + sanitized, + "This refers to **/with-a-file\n" + "and also **/with-a-different-file isn't that neat?\n" + "and has **/with-slashes" + ) + + @mock.patch.object(SubmissionError, "sanitize_message") + def test_submissionerror(self, mock_sanitize_message): + SubmissionError() + self.assertFalse(mock_sanitize_message.called) + SubmissionError("hi", "there") + self.assertTrue(mock_sanitize_message.called) + self.assertCountEqual( + mock_sanitize_message.call_args_list, + [mock.call("hi"), mock.call("there")], + ) diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index c814b84657..e6cbcb12f7 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -17,6 +17,7 @@ from shutil import move from typing import Optional, Union # pyflakes:ignore from unidecode import unidecode +from xml2rfc import RfcWriterError from xym import xym from django.conf import settings @@ -918,8 +919,51 @@ def accept_submission_requires_group_approval(submission): class SubmissionError(Exception): - """Exception for errors during submission processing""" - pass + """Exception for errors during submission processing + + Sanitizes paths appearing in exception messages. + """ + def __init__(self, *args): + if len(args) > 0: + args = (self.sanitize_message(arg) for arg in args) + super().__init__(*args) + + @staticmethod + def sanitize_message(msg): + # Paths likely to appear in submission-related errors + paths = [ + p for p in ( + getattr(settings, "ALL_ID_DOWNLOAD_DIR", None), + getattr(settings, "BIBXML_BASE_PATH", None), + getattr(settings, "DERIVED_DIR", None), + getattr(settings, "FTP_DIR", None), + getattr(settings, "IDSUBMIT_REPOSITORY_PATH", None), + getattr(settings, "IDSUBMIT_STAGING_PATH", None), + getattr(settings, "INTERNET_ALL_DRAFTS_ARCHIVE_DIR", None), + getattr(settings, "INTERNET_DRAFT_PATH", None), + getattr(settings, "INTERNET_DRAFT_ARCHIVE_DIR", None), + getattr(settings, "INTERNET_DRAFT_PDF_PATH", None), + getattr(settings, "RFC_PATH", None), + getattr(settings, "SUBMIT_YANG_CATALOG_MODEL_DIR", None), + getattr(settings, "SUBMIT_YANG_DRAFT_MODEL_DIR", None), + getattr(settings, "SUBMIT_YANG_IANA_MODEL_DIR", None), + getattr(settings, "SUBMIT_YANG_RFC_MODEL_DIR", None), + "/tmp/", + ) if p is not None + ] + return re.sub(fr"({'|'.join(paths)})/*", "**/", msg) + + +class XmlRfcError(SubmissionError): + """SubmissionError caused by xml2rfc + + Includes the output from xml2rfc, if any, in xml2rfc_stdout / xml2rfc_stderr + """ + def __init__(self, *args, xml2rfc_stdout: str, xml2rfc_stderr: str): + super().__init__(*args) + self.xml2rfc_stderr = xml2rfc_stderr + self.xml2rfc_stdout = xml2rfc_stdout + class InconsistentRevisionError(SubmissionError): """SubmissionError caused by an inconsistent revision""" @@ -937,27 +981,55 @@ def render_missing_formats(submission): If a txt file already exists, leaves it in place. Overwrites an existing html file if there is one. """ - xml2rfc.log.write_out = io.StringIO() # open(os.devnull, "w") - xml2rfc.log.write_err = io.StringIO() # open(os.devnull, "w") + # Capture stdio/stdout from xml2rfc + xml2rfc_stdout = io.StringIO() + xml2rfc_stderr = io.StringIO() + xml2rfc.log.write_out = xml2rfc_stdout + xml2rfc.log.write_err = xml2rfc_stderr xml_path = staging_path(submission.name, submission.rev, '.xml') parser = xml2rfc.XmlRfcParser(str(xml_path), quiet=True) - # --- Parse the xml --- - xmltree = parser.parse(remove_comments=False) + try: + # --- Parse the xml --- + xmltree = parser.parse(remove_comments=False) + except Exception as err: + raise XmlRfcError( + "Error parsing XML", + xml2rfc_stdout=xml2rfc_stdout.getvalue(), + xml2rfc_stderr=xml2rfc_stderr.getvalue(), + ) from err # If we have v2, run it through v2v3. Keep track of the submitted version, though. xmlroot = xmltree.getroot() xml_version = xmlroot.get('version', '2') if xml_version == '2': v2v3 = xml2rfc.V2v3XmlWriter(xmltree) - xmltree.tree = v2v3.convert2to3() + try: + xmltree.tree = v2v3.convert2to3() + except Exception as err: + raise XmlRfcError( + "Error converting v2 XML to v3", + xml2rfc_stdout=xml2rfc_stdout.getvalue(), + xml2rfc_stderr=xml2rfc_stderr.getvalue(), + ) from err # --- Prep the xml --- today = date_today() prep = xml2rfc.PrepToolWriter(xmltree, quiet=True, liberal=True, keep_pis=[xml2rfc.V3_PI_TARGET]) prep.options.accept_prepped = True prep.options.date = today - xmltree.tree = prep.prep() - if xmltree.tree == None: - raise SubmissionError(f'Error from xml2rfc (prep): {prep.errors}') + try: + xmltree.tree = prep.prep() + except RfcWriterError: + raise XmlRfcError( + f"Error during xml2rfc prep: {prep.errors}", + xml2rfc_stdout=xml2rfc_stdout.getvalue(), + xml2rfc_stderr=xml2rfc_stderr.getvalue(), + ) + except Exception as err: + raise XmlRfcError( + "Unexpected error during xml2rfc prep", + xml2rfc_stdout=xml2rfc_stdout.getvalue(), + xml2rfc_stderr=xml2rfc_stderr.getvalue(), + ) from err # --- Convert to txt --- txt_path = staging_path(submission.name, submission.rev, '.txt') @@ -965,7 +1037,14 @@ def render_missing_formats(submission): writer = xml2rfc.TextWriter(xmltree, quiet=True) writer.options.accept_prepped = True writer.options.date = today - writer.write(txt_path) + try: + writer.write(txt_path) + except Exception as err: + raise XmlRfcError( + "Error generating text format from XML", + xml2rfc_stdout=xml2rfc_stdout.getvalue(), + xml2rfc_stderr=xml2rfc_stderr.getvalue(), + ) from err log.log( 'In %s: xml2rfc %s generated %s from %s (version %s)' % ( str(xml_path.parent), @@ -980,7 +1059,14 @@ def render_missing_formats(submission): html_path = staging_path(submission.name, submission.rev, '.html') writer = xml2rfc.HtmlWriter(xmltree, quiet=True) writer.options.date = today - writer.write(str(html_path)) + try: + writer.write(str(html_path)) + except Exception as err: + raise XmlRfcError( + "Error generating HTML format from XML", + xml2rfc_stdout=xml2rfc_stdout.getvalue(), + xml2rfc_stderr=xml2rfc_stderr.getvalue(), + ) from err log.log( 'In %s: xml2rfc %s generated %s from %s (version %s)' % ( str(xml_path.parent), @@ -1263,7 +1349,7 @@ def process_submission_text(filename, revision): def process_and_validate_submission(submission): """Process and validate a submission - Raises SubmissionError if an error is encountered. + Raises SubmissionError or a subclass if an error is encountered. """ if len(set(submission.file_types.split(",")).intersection({".xml", ".txt"})) == 0: raise SubmissionError("Require XML and/or text format to process an Internet-Draft submission.") @@ -1273,7 +1359,16 @@ def process_and_validate_submission(submission): # Parse XML first, if we have it if ".xml" in submission.file_types: xml_metadata = process_submission_xml(submission.name, submission.rev) - render_missing_formats(submission) # makes HTML and text, unless text was uploaded + try: + render_missing_formats(submission) # makes HTML and text, unless text was uploaded + except XmlRfcError as err: + # log stdio/stderr + log.log( + f"xml2rfc failure when rendering missing formats for {submission.name}-{submission.rev}:\n" + f">> stdout:\n{err.xml2rfc_stdout}\n" + f">> stderr:\n{err.xml2rfc_stderr}" + ) + raise # Parse text, whether uploaded or generated from XML text_metadata = process_submission_text(submission.name, submission.rev) @@ -1332,11 +1427,11 @@ def process_and_validate_submission(submission): raise SubmissionError('Checks failed: ' + ' / '.join(errors)) except SubmissionError: raise # pass SubmissionErrors up the stack - except Exception: + except Exception as err: # convert other exceptions into SubmissionErrors log.log(f'Unexpected exception while processing submission {submission.pk}.') log.log(traceback.format_exc()) - raise SubmissionError('A system error occurred while processing the submission.') + raise SubmissionError('A system error occurred while processing the submission.') from err def submitter_is_author(submission): @@ -1428,6 +1523,7 @@ def process_uploaded_submission(submission): create_submission_event(None, submission, desc="Uploaded submission (diverted to manual process)") send_manual_post_request(None, submission, errors=dict(consistency=str(consistency_error))) except SubmissionError as err: + # something generic went wrong submission.refresh_from_db() # guard against incomplete changes in submission validation / processing cancel_submission(submission) # changes Submission.state create_submission_event(None, submission, f"Submission rejected: {err}") From 9c56ba9a0a53b6b92aa1cc0ac97764e976bf1463 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Thu, 17 Oct 2024 18:23:34 -0300 Subject: [PATCH 20/41] fix: drop, not bounce, uninteresting ipr emails (#8057) * fix: drop, not bounce, uninteresting ipr emails * chore: log to address * chore: unused import --- ietf/ipr/mail.py | 35 +++++++---- ietf/ipr/management/commands/process_email.py | 4 +- ietf/ipr/tests.py | 59 +++++++++++++------ ietf/ipr/utils.py | 13 ++-- 4 files changed, 72 insertions(+), 39 deletions(-) diff --git a/ietf/ipr/mail.py b/ietf/ipr/mail.py index 842426d820..167b11956c 100644 --- a/ietf/ipr/mail.py +++ b/ietf/ipr/mail.py @@ -171,31 +171,44 @@ def message_from_message(message,by=None): ) return msg + +class UndeliverableIprResponseError(Exception): + """Response email could not be delivered and should be treated as an error""" + + def process_response_email(msg): - """Saves an incoming message. msg=string. Message "To" field is expected to - be in the format ietf-ipr+[identifier]@ietf.org. Expect to find a message with - a matching value in the reply_to field, associated to an IPR disclosure through - IprEvent. Create a Message object for the incoming message and associate it to - the original message via new IprEvent""" + """Save an incoming IPR response email message + + Message "To" field is expected to be in the format ietf-ipr+[identifier]@ietf.org. If + the address or identifier is missing, the message will be silently dropped. + + Expect to find a message with a matching value in the reply_to field, associated to an + IPR disclosure through IprEvent. If it cannot be matched, raises UndeliverableIprResponseError + + Creates a Message object for the incoming message and associates it to + the original message via new IprEvent + """ message = message_from_bytes(force_bytes(msg)) to = message.get('To', '') # exit if this isn't a response we're interested in (with plus addressing) - local,domain = get_base_ipr_request_address().split('@') + local, domain = get_base_ipr_request_address().split('@') if not re.match(r'^{}\+[a-zA-Z0-9_\-]{}@{}'.format(local,'{16}',domain),to): - return None - + _from = message.get("From", "") + log(f"Ignoring IPR email without a message identifier from {_from} to {to}") + return + try: to_message = Message.objects.get(reply_to=to) except Message.DoesNotExist: log('Error finding matching message ({})'.format(to)) - return None + raise UndeliverableIprResponseError(f"Unable to find message matching {to}") try: disclosure = to_message.msgevents.first().disclosure except: log('Error processing message ({})'.format(to)) - return None + raise UndeliverableIprResponseError("Error processing message for {to}") ietf_message = message_from_message(message) IprEvent.objects.create( @@ -207,4 +220,4 @@ def process_response_email(msg): ) log("Received IPR email from %s" % ietf_message.frm) - return ietf_message + diff --git a/ietf/ipr/management/commands/process_email.py b/ietf/ipr/management/commands/process_email.py index 0b15fb0651..616cade5c4 100644 --- a/ietf/ipr/management/commands/process_email.py +++ b/ietf/ipr/management/commands/process_email.py @@ -9,7 +9,7 @@ from django.core.management import CommandError from ietf.utils.management.base import EmailOnFailureCommand -from ietf.ipr.mail import process_response_email +from ietf.ipr.mail import process_response_email, UndeliverableIprResponseError import debug # pyflakes:ignore @@ -31,7 +31,7 @@ def handle(self, *args, **options): self.msg_bytes = sys.stdin.buffer.read() try: process_response_email(self.msg_bytes) - except ValueError as e: + except (ValueError, UndeliverableIprResponseError) as e: raise CommandError(e) failure_subject = 'Error during ipr email processing' diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index 3c70567fd8..d72018f10b 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -4,6 +4,7 @@ import datetime import mock +import re from pyquery import PyQuery from urllib.parse import quote, urlparse @@ -35,9 +36,9 @@ ) from ietf.ipr.forms import DraftForm, HolderIprDisclosureForm from ietf.ipr.mail import (process_response_email, get_reply_to, get_update_submitter_emails, - get_pseudo_submitter, get_holders, get_update_cc_addrs) -from ietf.ipr.models import (IprDisclosureBase,GenericIprDisclosure,HolderIprDisclosure, - ThirdPartyIprDisclosure) + get_pseudo_submitter, get_holders, get_update_cc_addrs, UndeliverableIprResponseError) +from ietf.ipr.models import (IprDisclosureBase, GenericIprDisclosure, HolderIprDisclosure, + ThirdPartyIprDisclosure, IprEvent) from ietf.ipr.templatetags.ipr_filters import no_revisions_message from ietf.ipr.utils import get_genitive, get_ipr_summary, ingest_response_email from ietf.mailtrigger.utils import gather_address_lists @@ -712,7 +713,7 @@ def test_notify_generic(self): ) self.assertIn(f'{settings.IDTRACKER_BASE_URL}{urlreverse("ietf.ipr.views.showlist")}', get_payload_text(outbox[1]).replace('\n',' ')) - def send_ipr_email_helper(self): + def send_ipr_email_helper(self) -> tuple[str, IprEvent, HolderIprDisclosure]: ipr = HolderIprDisclosureFactory() url = urlreverse('ietf.ipr.views.email',kwargs={ "id": ipr.id }) self.client.login(username="secretary", password="secretary+password") @@ -730,10 +731,11 @@ def send_ipr_email_helper(self): q = Message.objects.filter(reply_to=data['reply_to']) self.assertEqual(q.count(),1) event = q[0].msgevents.first() + assert event is not None self.assertTrue(event.response_past_due()) self.assertEqual(len(outbox), 1) self.assertTrue('joe@test.com' in outbox[0]['To']) - return data['reply_to'], event + return data['reply_to'], event, ipr uninteresting_ipr_message_strings = [ ("To: {to}\nCc: {cc}\nFrom: joe@test.com\nDate: {date}\nSubject: test\n"), @@ -747,34 +749,46 @@ def send_ipr_email_helper(self): def test_process_response_email(self): # first send a mail - reply_to, event = self.send_ipr_email_helper() + reply_to, event, _ = self.send_ipr_email_helper() # test process response uninteresting messages addrs = gather_address_lists('ipr_disclosure_submitted').as_strings() for message_string in self.uninteresting_ipr_message_strings: - result = process_response_email( + process_response_email( message_string.format( to=addrs.to, cc=addrs.cc, date=timezone.now().ctime() ) ) - self.assertIsNone(result) - + # test process response message_string = """To: {} From: joe@test.com Date: {} Subject: test """.format(reply_to, timezone.now().ctime()) - result = process_response_email(message_string) - - self.assertIsInstance(result, Message) + process_response_email(message_string) self.assertFalse(event.response_past_due()) + # test with an unmatchable message identifier + bad_reply_to = re.sub( + r"\+.{16}@", + '+0123456789abcdef@', + reply_to, + ) + self.assertNotEqual(reply_to, bad_reply_to) + message_string = f"""To: {bad_reply_to} + From: joe@test.com + Date: {timezone.now().ctime()} + Subject: test + """ + with self.assertRaises(UndeliverableIprResponseError): + process_response_email(message_string) + def test_process_response_email_with_invalid_encoding(self): """Interesting emails with invalid encoding should be handled""" - reply_to, _ = self.send_ipr_email_helper() + reply_to, _, disclosure = self.send_ipr_email_helper() # test process response message_string = """To: {} From: joe@test.com @@ -782,8 +796,8 @@ def test_process_response_email_with_invalid_encoding(self): Subject: test """.format(reply_to, timezone.now().ctime()) message_bytes = message_string.encode('utf8') + b'\nInvalid stuff: \xfe\xff\n' - result = process_response_email(message_bytes) - self.assertIsInstance(result, Message) + process_response_email(message_bytes) + result = IprEvent.objects.filter(disclosure=disclosure).first().message # newest # \ufffd is a rhombus character with an inverse ?, used to replace invalid characters self.assertEqual(result.body, 'Invalid stuff: \ufffd\ufffd\n\n', # not sure where the extra \n is from 'Invalid characters should be replaced with \ufffd characters') @@ -798,8 +812,7 @@ def test_process_response_email_uninteresting_with_invalid_encoding(self): cc=addrs.cc, date=timezone.now().ctime(), ).encode('utf8') + b'\nInvalid stuff: \xfe\xff\n' - result = process_response_email(message_bytes) - self.assertIsNone(result) + process_response_email(message_bytes) @override_settings(ADMINS=(("Some Admin", "admin@example.com"),)) @mock.patch("ietf.ipr.utils.process_response_email") @@ -816,8 +829,8 @@ def test_ingest_response_email(self, mock_process_response_email): self.assertEqual(mock_process_response_email.call_args, mock.call(message)) mock_process_response_email.reset_mock() - mock_process_response_email.side_effect = None - mock_process_response_email.return_value = None # rejected message + mock_process_response_email.side_effect = UndeliverableIprResponseError + mock_process_response_email.return_value = None with self.assertRaises(EmailIngestionError) as context: ingest_response_email(message) self.assertIsNone(context.exception.as_emailmessage()) # should not send an email on a clean rejection @@ -825,6 +838,14 @@ def test_ingest_response_email(self, mock_process_response_email): self.assertEqual(mock_process_response_email.call_args, mock.call(message)) mock_process_response_email.reset_mock() + mock_process_response_email.side_effect = None + mock_process_response_email.return_value = None # ignored message + ingest_response_email(message) # should not raise an exception + self.assertIsNone(context.exception.as_emailmessage()) # should not send an email on ignored message + self.assertTrue(mock_process_response_email.called) + self.assertEqual(mock_process_response_email.call_args, mock.call(message)) + mock_process_response_email.reset_mock() + # successful operation mock_process_response_email.return_value = MessageFactory() ingest_response_email(message) diff --git a/ietf/ipr/utils.py b/ietf/ipr/utils.py index 42d485ccad..8f0b9cf3f2 100644 --- a/ietf/ipr/utils.py +++ b/ietf/ipr/utils.py @@ -3,7 +3,7 @@ from textwrap import dedent -from ietf.ipr.mail import process_response_email +from ietf.ipr.mail import process_response_email, UndeliverableIprResponseError from ietf.ipr.models import IprDocRel import debug # pyflakes:ignore @@ -92,7 +92,11 @@ def generate_draft_recursive_txt(): def ingest_response_email(message: bytes): from ietf.api.views import EmailIngestionError # avoid circular import try: - result = process_response_email(message) + process_response_email(message) + except UndeliverableIprResponseError: + # Message was rejected due to some problem the sender can fix, so bounce but don't send + # an email to the admins + raise EmailIngestionError("IPR response rejected", email_body=None) except Exception as err: # Message was rejected due to an unhandled exception. This is likely something # the admins need to address, so send them a copy of the email. @@ -106,8 +110,3 @@ def ingest_response_email(message: bytes): email_original_message=message, email_attach_traceback=True, ) from err - - if result is None: - # Message was rejected due to some problem the sender can fix, so bounce but don't send - # an email to the admins - raise EmailIngestionError("IPR response rejected", email_body=None) From 9303aaf5f7d1722f727ded1372f04ae11043dcce Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Thu, 17 Oct 2024 21:07:51 -0400 Subject: [PATCH 21/41] ci: Update build.yml --- .github/workflows/build.yml | 125 ++++++++++++++++++++++++++++++------ 1 file changed, 105 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3887661cac..1ab42ae80a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -234,31 +234,116 @@ jobs: docker-compose.yml EOL - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - name: Launch build VM + id: azlaunch + run: | + echo "Authenticating to Azure..." + az login --service-principal -u ${{ secrets.AZ_BUILD_APP_ID }} -p ${{ secrets.AZ_BUILD_PWD }} --tenant ${{ secrets.AZ_BUILD_TENANT_ID }} + echo "Creating VM..." + vminfo=$(az vm create \ + --resource-group ghaDatatracker \ + --name tmpGhaBuildVM \ + --image Ubuntu2404 \ + --admin-username azureuser \ + --generate-ssh-keys \ + --priority Spot \ + --size Standard_D8as_v6 \ + --max-price -1 \ + --os-disk-size-gb 30 \ + --eviction-policy Delete \ + --nic-delete-option Delete \ + --output tsv \ + --query "publicIpAddress") + echo "ipaddr=$vminfo" >> "$GITHUB_OUTPUT" + echo "VM Public IP: $vminfo" + cat ~/.ssh/id_rsa > ${{ github.workspace }}/prvkey.key + ssh-keyscan -t rsa $vminfo >> ~/.ssh/known_hosts - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + - name: Copy build files + uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + host: ${{ steps.azlaunch.outputs.ipaddr }} + port: 22 + username: azureuser + key_path: ${{ github.workspace }}/prvkey.key + source: "${{ github.workspace }},!${{ github.workspace }}/.git,!${{ github.workspace }}/prvkey.key" + target: /workspace - - name: Build Release Docker Image - uses: docker/build-push-action@v6 + - name: Remote SSH into Build VM + uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 env: - DOCKER_BUILD_SUMMARY: false + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_ACTOR: ${{ github.actor }} + PKG_VERSION: ${{ env.PKG_VERSION }} with: - context: . - file: dev/build/Dockerfile - platforms: ${{ github.event.inputs.skiparm == 'true' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} - push: true - tags: ghcr.io/ietf-tools/datatracker:${{ env.PKG_VERSION }} - cache-from: type=gha - cache-to: type=gha,mode=max + host: ${{ steps.azlaunch.outputs.ipaddr }} + port: 22 + username: azureuser + command_timeout: 60m + key_path: ${{ github.workspace }}/prvkey.key + envs: GITHUB_TOKEN + script_stop: true + script: | + export DEBIAN_FRONTEND=noninteractive + lsb_release -a + sudo apt-get update + sudo apt-get upgrade -y + + echo "Installing Docker..." + curl -fsSL https://get.docker.com -o get-docker.sh + sudo sh get-docker.sh + + echo "Login to ghcr.io..." + echo $GITHUB_TOKEN | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin + + echo "Building Images..." + cd /workspace + ls + + - name: Destroy Build VM + resources + if: always() + shell: pwsh + run: | + echo "Destroying VM..." + az vm delete -g ghaDatatracker -n tmpGhaBuildVM --yes --force-deletion true + + $resourceOrderRemovalOrder = [ordered]@{ + "Microsoft.Compute/virtualMachines" = 0 + "Microsoft.Compute/disks" = 1 + "Microsoft.Network/networkInterfaces" = 2 + "Microsoft.Network/publicIpAddresses" = 3 + "Microsoft.Network/networkSecurityGroups" = 4 + "Microsoft.Network/virtualNetworks" = 5 + } + echo "Fetching remaining resources..." + $resources = az resource list --resource-group ghaDatatracker | ConvertFrom-Json + + $orderedResources = $resources + | Sort-Object @{ + Expression = {$resourceOrderRemovalOrder[$_.type]} + Descending = $False + } + + echo "Deleting remaining resources..." + $orderedResources | ForEach-Object { + az resource delete --resource-group ghaDatatracker --ids $_.id --verbose + } + + echo "Logout from Azure..." + az logout + + # - name: Build Release Docker Image + # uses: docker/build-push-action@v6 + # env: + # DOCKER_BUILD_SUMMARY: false + # with: + # context: . + # file: dev/build/Dockerfile + # platforms: ${{ github.event.inputs.skiparm == 'true' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} + # push: true + # tags: ghcr.io/ietf-tools/datatracker:${{ env.PKG_VERSION }} + # cache-from: type=gha + # cache-to: type=gha,mode=max - name: Update CHANGELOG id: changelog From 5c2369362d7474d9e62de10b2a9bedbf44e691ed Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Thu, 17 Oct 2024 21:12:46 -0400 Subject: [PATCH 22/41] ci: Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1ab42ae80a..07aca3794c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -243,7 +243,7 @@ jobs: vminfo=$(az vm create \ --resource-group ghaDatatracker \ --name tmpGhaBuildVM \ - --image Ubuntu2404 \ + --image Ubuntu2204 \ --admin-username azureuser \ --generate-ssh-keys \ --priority Spot \ From bbfbb25844d412a227d2b801714eb86c61a77ba7 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Thu, 17 Oct 2024 21:46:58 -0400 Subject: [PATCH 23/41] ci: Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 07aca3794c..158ce19107 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -247,7 +247,7 @@ jobs: --admin-username azureuser \ --generate-ssh-keys \ --priority Spot \ - --size Standard_D8as_v6 \ + --size Standard_D8as_v5 \ --max-price -1 \ --os-disk-size-gb 30 \ --eviction-policy Delete \ From 453ddc87ecfb3f9f668adb085d1f9558780c9b50 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Thu, 17 Oct 2024 23:15:27 -0400 Subject: [PATCH 24/41] ci: Update build.yml --- .github/workflows/build.yml | 188 +++++++++++++++++++----------------- 1 file changed, 101 insertions(+), 87 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 158ce19107..55c9b0a1b2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -156,83 +156,6 @@ jobs: with: fetch-depth: 1 fetch-tags: false - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '16' - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - - name: Download a Coverage Results - if: ${{ github.event.inputs.skiptests == 'false' || github.ref_name == 'release' }} - uses: actions/download-artifact@v4.1.8 - with: - name: coverage - - - name: Make Release Build - env: - DEBIAN_FRONTEND: noninteractive - BROWSERSLIST_IGNORE_OLD_DATA: 1 - run: | - echo "PKG_VERSION: $PKG_VERSION" - echo "GITHUB_SHA: $GITHUB_SHA" - echo "GITHUB_REF_NAME: $GITHUB_REF_NAME" - echo "Running frontend build script..." - echo "Compiling native node packages..." - yarn rebuild - echo "Packaging static assets..." - yarn build --base=https://static.ietf.org/dt/$PKG_VERSION/ - yarn legacy:build - echo "Setting version $PKG_VERSION..." - sed -i -r -e "s|^__version__ += '.*'$|__version__ = '$PKG_VERSION'|" ietf/__init__.py - sed -i -r -e "s|^__release_hash__ += '.*'$|__release_hash__ = '$GITHUB_SHA'|" ietf/__init__.py - sed -i -r -e "s|^__release_branch__ += '.*'$|__release_branch__ = '$GITHUB_REF_NAME'|" ietf/__init__.py - - - name: Set Production Flags - if: ${{ env.SHOULD_DEPLOY == 'true' }} - run: | - echo "Setting production flags in settings.py..." - sed -i -r -e 's/^DEBUG *= *.*$/DEBUG = False/' -e "s/^SERVER_MODE *= *.*\$/SERVER_MODE = 'production'/" ietf/settings.py - - - name: Make Release Tarball - env: - DEBIAN_FRONTEND: noninteractive - run: | - echo "Build release tarball..." - mkdir -p /home/runner/work/release - tar -czf /home/runner/work/release/release.tar.gz -X dev/build/exclude-patterns.txt . - - - name: Collect + Push Statics - env: - DEBIAN_FRONTEND: noninteractive - AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_STATIC_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_STATIC_KEY_SECRET }} - AWS_DEFAULT_REGION: auto - AWS_ENDPOINT_URL: ${{ secrets.CF_R2_ENDPOINT }} - run: | - echo "Collecting statics..." - docker run --rm --name collectstatics -v $(pwd):/workspace ghcr.io/ietf-tools/datatracker-app-base:latest sh dev/build/collectstatics.sh - echo "Pushing statics..." - cd static - aws s3 sync . s3://static/dt/$PKG_VERSION --only-show-errors - - - name: Augment dockerignore for docker image build - env: - DEBIAN_FRONTEND: noninteractive - run: | - cat >> .dockerignore < ${{ github.workspace }}/prvkey.key ssh-keyscan -t rsa $vminfo >> ~/.ssh/known_hosts - - name: Copy build files - uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 - with: - host: ${{ steps.azlaunch.outputs.ipaddr }} - port: 22 - username: azureuser - key_path: ${{ github.workspace }}/prvkey.key - source: "${{ github.workspace }},!${{ github.workspace }}/.git,!${{ github.workspace }}/prvkey.key" - target: /workspace + # - name: Copy build files + # uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 + # with: + # host: ${{ steps.azlaunch.outputs.ipaddr }} + # port: 22 + # username: azureuser + # key_path: ${{ github.workspace }}/prvkey.key + # source: "${{ github.workspace }},!${{ github.workspace }}/.git,!${{ github.workspace }}/prvkey.key" + # target: /workspace - name: Remote SSH into Build VM uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_ACTOR: ${{ github.actor }} + GITHUB_SHA: ${{ github.sha }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_RUN_ID: ${{ github.run_id }} + AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_STATIC_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_STATIC_KEY_SECRET }} + AWS_DEFAULT_REGION: auto + AWS_ENDPOINT_URL: ${{ secrets.CF_R2_ENDPOINT }} PKG_VERSION: ${{ env.PKG_VERSION }} + SHOULD_DEPLOY: ${{ env.SHOULD_DEPLOY }} + SKIP_TESTS: ${{ github.event.inputs.skiptests }} + DEBIAN_FRONTEND: noninteractive + BROWSERSLIST_IGNORE_OLD_DATA: 1 with: host: ${{ steps.azlaunch.outputs.ipaddr }} port: 22 @@ -295,10 +229,90 @@ jobs: echo "Login to ghcr.io..." echo $GITHUB_TOKEN | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin + + echo "Installing GH CLI..." + (type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \ + && sudo mkdir -p -m 755 /etc/apt/keyrings \ + && wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ + && sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ + && sudo apt update \ + && sudo apt install gh -y + + echo "Installing AWS CLI..." + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip awscliv2.zip + sudo ./aws/install + + echo "Install Node.js..." + curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh + sudo bash nodesource_setup.sh + sudo apt-get install -y nodejs + sudo corepack enable + + echo "Install Python 3.x..." + sudo apt-get install python3 python3-dev -y + python3 --version + + echo "Clone project..." + sudo mkdir -p /workspace + sudo chown azureuser /workspace + cd /workspace + gh repo clone ietf-tools/datatracker -- --depth=1 --no-tags + cd datatracker + + if [ "$SHOULD_DEPLOY" = false ] || [ "$GITHUB_REF_NAME" = release ] ; then + echo "Downloading coverage..." + gh run download $GITHUB_RUN_ID -n coverage + fi + + echo "Building project..." + echo "PKG_VERSION: $PKG_VERSION" + echo "GITHUB_SHA: $GITHUB_SHA" + echo "GITHUB_REF_NAME: $GITHUB_REF_NAME" + echo "Running frontend build script..." + echo "Compiling native node packages..." + yarn rebuild + echo "Packaging static assets..." + yarn build --base=https://static.ietf.org/dt/$PKG_VERSION/ + yarn legacy:build + echo "Setting version $PKG_VERSION..." + sed -i -r -e "s|^__version__ += '.*'$|__version__ = '$PKG_VERSION'|" ietf/__init__.py + sed -i -r -e "s|^__release_hash__ += '.*'$|__release_hash__ = '$GITHUB_SHA'|" ietf/__init__.py + sed -i -r -e "s|^__release_branch__ += '.*'$|__release_branch__ = '$GITHUB_REF_NAME'|" ietf/__init__.py + + if [ "$SHOULD_DEPLOY" = true ] ; then + echo "Setting production flags in settings.py..." + sed -i -r -e 's/^DEBUG *= *.*$/DEBUG = False/' -e "s/^SERVER_MODE *= *.*\$/SERVER_MODE = 'production'/" ietf/settings.py + fi + + echo "Build release tarball..." + mkdir -p /workspace/release + tar -czf /workspace/release.tar.gz -X dev/build/exclude-patterns.txt . + + echo "Collecting statics..." + docker run --rm --name collectstatics -v $(pwd):/workspace ghcr.io/ietf-tools/datatracker-app-base:latest sh dev/build/collectstatics.sh + + echo "Pushing statics..." + cd static + aws s3 sync . s3://static/dt/$PKG_VERSION --only-show-errors + cd .. + + echo "Augment dockerignore for docker image build..." + cat >> .dockerignore <&2 + exit 64 - name: Destroy Build VM + resources if: always() From a074cd4b85f6584e63fa7491cfd0d464bb83279e Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Thu, 17 Oct 2024 23:25:14 -0400 Subject: [PATCH 25/41] ci: Update build.yml --- .github/workflows/build.yml | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 55c9b0a1b2..cc7b622036 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -222,15 +222,21 @@ jobs: lsb_release -a sudo apt-get update sudo apt-get upgrade -y - + + echo "==========================================================================" echo "Installing Docker..." + echo "==========================================================================" curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh + echo "==========================================================================" echo "Login to ghcr.io..." - echo $GITHUB_TOKEN | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin + echo "==========================================================================" + echo $GITHUB_TOKEN | sudo docker login ghcr.io -u $GITHUB_ACTOR --password-stdin + echo "==========================================================================" echo "Installing GH CLI..." + echo "==========================================================================" (type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \ && sudo mkdir -p -m 755 /etc/apt/keyrings \ && wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ @@ -239,22 +245,30 @@ jobs: && sudo apt update \ && sudo apt install gh -y + echo "==========================================================================" echo "Installing AWS CLI..." + echo "==========================================================================" curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" unzip awscliv2.zip sudo ./aws/install + echo "==========================================================================" echo "Install Node.js..." + echo "==========================================================================" curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh sudo bash nodesource_setup.sh sudo apt-get install -y nodejs sudo corepack enable + echo "==========================================================================" echo "Install Python 3.x..." + echo "==========================================================================" sudo apt-get install python3 python3-dev -y python3 --version + echo "==========================================================================" echo "Clone project..." + echo "==========================================================================" sudo mkdir -p /workspace sudo chown azureuser /workspace cd /workspace @@ -262,11 +276,15 @@ jobs: cd datatracker if [ "$SHOULD_DEPLOY" = false ] || [ "$GITHUB_REF_NAME" = release ] ; then + echo "==========================================================================" echo "Downloading coverage..." + echo "==========================================================================" gh run download $GITHUB_RUN_ID -n coverage fi + echo "==========================================================================" echo "Building project..." + echo "==========================================================================" echo "PKG_VERSION: $PKG_VERSION" echo "GITHUB_SHA: $GITHUB_SHA" echo "GITHUB_REF_NAME: $GITHUB_REF_NAME" @@ -282,23 +300,30 @@ jobs: sed -i -r -e "s|^__release_branch__ += '.*'$|__release_branch__ = '$GITHUB_REF_NAME'|" ietf/__init__.py if [ "$SHOULD_DEPLOY" = true ] ; then + echo "==========================================================================" echo "Setting production flags in settings.py..." + echo "==========================================================================" sed -i -r -e 's/^DEBUG *= *.*$/DEBUG = False/' -e "s/^SERVER_MODE *= *.*\$/SERVER_MODE = 'production'/" ietf/settings.py fi + echo "==========================================================================" echo "Build release tarball..." + echo "==========================================================================" mkdir -p /workspace/release tar -czf /workspace/release.tar.gz -X dev/build/exclude-patterns.txt . + echo "==========================================================================" echo "Collecting statics..." + echo "==========================================================================" docker run --rm --name collectstatics -v $(pwd):/workspace ghcr.io/ietf-tools/datatracker-app-base:latest sh dev/build/collectstatics.sh - echo "Pushing statics..." cd static aws s3 sync . s3://static/dt/$PKG_VERSION --only-show-errors cd .. + echo "==========================================================================" echo "Augment dockerignore for docker image build..." + echo "==========================================================================" cat >> .dockerignore <&2 exit 64 From a104360ace7ce04944cb603ddf4eff5adf47663d Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Thu, 17 Oct 2024 23:31:50 -0400 Subject: [PATCH 26/41] ci: Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cc7b622036..3c7cf523c1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -215,7 +215,7 @@ jobs: username: azureuser command_timeout: 60m key_path: ${{ github.workspace }}/prvkey.key - envs: GITHUB_TOKEN + envs: GITHUB_TOKEN,GITHUB_ACTOR,GITHUB_SHA,GITHUB_REF_NAME,GITHUB_RUN_ID,AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_DEFAULT_REGION,AWS_ENDPOINT_URL,PKG_VERSION,SHOULD_DEPLOY,SKIP_TESTS,DEBIAN_FRONTEND,BROWSERSLIST_IGNORE_OLD_DATA script_stop: true script: | export DEBIAN_FRONTEND=noninteractive From 60553adee4303ffb95f1a4a238d7d72e4805fe68 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Thu, 17 Oct 2024 23:37:43 -0400 Subject: [PATCH 27/41] ci: Update build.yml --- .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 3c7cf523c1..877e7dc95a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -222,6 +222,7 @@ jobs: lsb_release -a sudo apt-get update sudo apt-get upgrade -y + sudo apt-get install wget unzip curl -y echo "==========================================================================" echo "Installing Docker..." @@ -237,8 +238,7 @@ jobs: echo "==========================================================================" echo "Installing GH CLI..." echo "==========================================================================" - (type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \ - && sudo mkdir -p -m 755 /etc/apt/keyrings \ + sudo mkdir -p -m 755 /etc/apt/keyrings \ && wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ && sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ From b324d75583efab775d68141567ca2ddf7bcb75c8 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Thu, 17 Oct 2024 23:46:56 -0400 Subject: [PATCH 28/41] ci: Update build.yml --- .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 877e7dc95a..4eb03f4604 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -275,7 +275,7 @@ jobs: gh repo clone ietf-tools/datatracker -- --depth=1 --no-tags cd datatracker - if [ "$SHOULD_DEPLOY" = false ] || [ "$GITHUB_REF_NAME" = release ] ; then + if [ "$SHOULD_DEPLOY" = "false" ] || [ "$GITHUB_REF_NAME" = "release" ] ; then echo "==========================================================================" echo "Downloading coverage..." echo "==========================================================================" @@ -299,7 +299,7 @@ jobs: sed -i -r -e "s|^__release_hash__ += '.*'$|__release_hash__ = '$GITHUB_SHA'|" ietf/__init__.py sed -i -r -e "s|^__release_branch__ += '.*'$|__release_branch__ = '$GITHUB_REF_NAME'|" ietf/__init__.py - if [ "$SHOULD_DEPLOY" = true ] ; then + if [ "$SHOULD_DEPLOY" = "true" ] ; then echo "==========================================================================" echo "Setting production flags in settings.py..." echo "==========================================================================" From 400bbca958442926917d1df49dd5fb8b0b2f27f9 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Thu, 17 Oct 2024 23:56:15 -0400 Subject: [PATCH 29/41] ci: Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4eb03f4604..0d4e7932d4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -275,7 +275,7 @@ jobs: gh repo clone ietf-tools/datatracker -- --depth=1 --no-tags cd datatracker - if [ "$SHOULD_DEPLOY" = "false" ] || [ "$GITHUB_REF_NAME" = "release" ] ; then + if [ "$SKIP_TESTS" = "false" ] || [ "$GITHUB_REF_NAME" = "release" ] ; then echo "==========================================================================" echo "Downloading coverage..." echo "==========================================================================" From ebc7cde351afcfb1869af92749c046354523c39a Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Fri, 18 Oct 2024 00:04:37 -0400 Subject: [PATCH 30/41] ci: Update build.yml --- .github/workflows/build.yml | 64 ++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0d4e7932d4..e69b4e31c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -258,7 +258,7 @@ jobs: curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh sudo bash nodesource_setup.sh sudo apt-get install -y nodejs - sudo corepack enable + corepack enable echo "==========================================================================" echo "Install Python 3.x..." @@ -341,37 +341,37 @@ jobs: echo "Error!" 1>&2 exit 64 - - name: Destroy Build VM + resources - if: always() - shell: pwsh - run: | - echo "Destroying VM..." - az vm delete -g ghaDatatracker -n tmpGhaBuildVM --yes --force-deletion true - - $resourceOrderRemovalOrder = [ordered]@{ - "Microsoft.Compute/virtualMachines" = 0 - "Microsoft.Compute/disks" = 1 - "Microsoft.Network/networkInterfaces" = 2 - "Microsoft.Network/publicIpAddresses" = 3 - "Microsoft.Network/networkSecurityGroups" = 4 - "Microsoft.Network/virtualNetworks" = 5 - } - echo "Fetching remaining resources..." - $resources = az resource list --resource-group ghaDatatracker | ConvertFrom-Json - - $orderedResources = $resources - | Sort-Object @{ - Expression = {$resourceOrderRemovalOrder[$_.type]} - Descending = $False - } - - echo "Deleting remaining resources..." - $orderedResources | ForEach-Object { - az resource delete --resource-group ghaDatatracker --ids $_.id --verbose - } - - echo "Logout from Azure..." - az logout + # - name: Destroy Build VM + resources + # if: always() + # shell: pwsh + # run: | + # echo "Destroying VM..." + # az vm delete -g ghaDatatracker -n tmpGhaBuildVM --yes --force-deletion true + + # $resourceOrderRemovalOrder = [ordered]@{ + # "Microsoft.Compute/virtualMachines" = 0 + # "Microsoft.Compute/disks" = 1 + # "Microsoft.Network/networkInterfaces" = 2 + # "Microsoft.Network/publicIpAddresses" = 3 + # "Microsoft.Network/networkSecurityGroups" = 4 + # "Microsoft.Network/virtualNetworks" = 5 + # } + # echo "Fetching remaining resources..." + # $resources = az resource list --resource-group ghaDatatracker | ConvertFrom-Json + + # $orderedResources = $resources + # | Sort-Object @{ + # Expression = {$resourceOrderRemovalOrder[$_.type]} + # Descending = $False + # } + + # echo "Deleting remaining resources..." + # $orderedResources | ForEach-Object { + # az resource delete --resource-group ghaDatatracker --ids $_.id --verbose + # } + + # echo "Logout from Azure..." + # az logout # - name: Build Release Docker Image # uses: docker/build-push-action@v6 From 324e09af5ae375d0d80908818ad2686854168b84 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Fri, 18 Oct 2024 00:12:49 -0400 Subject: [PATCH 31/41] ci: Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e69b4e31c2..92f4e93b5b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -258,7 +258,7 @@ jobs: curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh sudo bash nodesource_setup.sh sudo apt-get install -y nodejs - corepack enable + sudo corepack enable echo "==========================================================================" echo "Install Python 3.x..." From aa36fca2805f144f45a4ebd7e4d2813a239ca2e1 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Fri, 18 Oct 2024 00:27:00 -0400 Subject: [PATCH 32/41] ci: Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 92f4e93b5b..c3112c460e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -255,7 +255,7 @@ jobs: echo "==========================================================================" echo "Install Node.js..." echo "==========================================================================" - curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh + curl -fsSL https://deb.nodesource.com/setup_18.x -o nodesource_setup.sh sudo bash nodesource_setup.sh sudo apt-get install -y nodejs sudo corepack enable From dce23818b6d11e8ddddee32c83c84cb38b45d762 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Fri, 18 Oct 2024 00:33:56 -0400 Subject: [PATCH 33/41] ci: Update build.yml --- .github/workflows/build.yml | 64 ++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c3112c460e..f54ab43aa2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -315,7 +315,7 @@ jobs: echo "==========================================================================" echo "Collecting statics..." echo "==========================================================================" - docker run --rm --name collectstatics -v $(pwd):/workspace ghcr.io/ietf-tools/datatracker-app-base:latest sh dev/build/collectstatics.sh + sudo docker run --rm --name collectstatics -v $(pwd):/workspace ghcr.io/ietf-tools/datatracker-app-base:latest sh dev/build/collectstatics.sh echo "Pushing statics..." cd static aws s3 sync . s3://static/dt/$PKG_VERSION --only-show-errors @@ -341,37 +341,37 @@ jobs: echo "Error!" 1>&2 exit 64 - # - name: Destroy Build VM + resources - # if: always() - # shell: pwsh - # run: | - # echo "Destroying VM..." - # az vm delete -g ghaDatatracker -n tmpGhaBuildVM --yes --force-deletion true - - # $resourceOrderRemovalOrder = [ordered]@{ - # "Microsoft.Compute/virtualMachines" = 0 - # "Microsoft.Compute/disks" = 1 - # "Microsoft.Network/networkInterfaces" = 2 - # "Microsoft.Network/publicIpAddresses" = 3 - # "Microsoft.Network/networkSecurityGroups" = 4 - # "Microsoft.Network/virtualNetworks" = 5 - # } - # echo "Fetching remaining resources..." - # $resources = az resource list --resource-group ghaDatatracker | ConvertFrom-Json - - # $orderedResources = $resources - # | Sort-Object @{ - # Expression = {$resourceOrderRemovalOrder[$_.type]} - # Descending = $False - # } - - # echo "Deleting remaining resources..." - # $orderedResources | ForEach-Object { - # az resource delete --resource-group ghaDatatracker --ids $_.id --verbose - # } - - # echo "Logout from Azure..." - # az logout + - name: Destroy Build VM + resources + if: always() + shell: pwsh + run: | + echo "Destroying VM..." + az vm delete -g ghaDatatracker -n tmpGhaBuildVM --yes --force-deletion true + + $resourceOrderRemovalOrder = [ordered]@{ + "Microsoft.Compute/virtualMachines" = 0 + "Microsoft.Compute/disks" = 1 + "Microsoft.Network/networkInterfaces" = 2 + "Microsoft.Network/publicIpAddresses" = 3 + "Microsoft.Network/networkSecurityGroups" = 4 + "Microsoft.Network/virtualNetworks" = 5 + } + echo "Fetching remaining resources..." + $resources = az resource list --resource-group ghaDatatracker | ConvertFrom-Json + + $orderedResources = $resources + | Sort-Object @{ + Expression = {$resourceOrderRemovalOrder[$_.type]} + Descending = $False + } + + echo "Deleting remaining resources..." + $orderedResources | ForEach-Object { + az resource delete --resource-group ghaDatatracker --ids $_.id --verbose + } + + echo "Logout from Azure..." + az logout # - name: Build Release Docker Image # uses: docker/build-push-action@v6 From d9d191fba56a77297cb20c5cbd634406fff79b12 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Fri, 18 Oct 2024 00:57:31 -0400 Subject: [PATCH 34/41] ci: Update build.yml --- .github/workflows/build.yml | 38 ++++++++++++------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f54ab43aa2..48113ffb30 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -182,16 +182,6 @@ jobs: cat ~/.ssh/id_rsa > ${{ github.workspace }}/prvkey.key ssh-keyscan -t rsa $vminfo >> ~/.ssh/known_hosts - # - name: Copy build files - # uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 - # with: - # host: ${{ steps.azlaunch.outputs.ipaddr }} - # port: 22 - # username: azureuser - # key_path: ${{ github.workspace }}/prvkey.key - # source: "${{ github.workspace }},!${{ github.workspace }}/.git,!${{ github.workspace }}/prvkey.key" - # target: /workspace - - name: Remote SSH into Build VM uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 env: @@ -337,9 +327,12 @@ jobs: echo "==========================================================================" echo "Building Images..." echo "==========================================================================" - ls - echo "Error!" 1>&2 - exit 64 + sudo docker buildx build --file dev/build/Dockerfile --platform linux/amd64,linux/arm64 --tag ghcr.io/ietf-tools/datatracker:$PKG_VERSION --push . + + - name: Fetch release tarball + run: | + mkdir -p /home/runner/work/release + scp -i ${{ github.workspace }}/prvkey.key azureuser@${{ steps.azlaunch.outputs.ipaddr }}:/workspace/release.tar.gz /home/runner/work/release/release.tar.gz - name: Destroy Build VM + resources if: always() @@ -372,19 +365,6 @@ jobs: echo "Logout from Azure..." az logout - - # - name: Build Release Docker Image - # uses: docker/build-push-action@v6 - # env: - # DOCKER_BUILD_SUMMARY: false - # with: - # context: . - # file: dev/build/Dockerfile - # platforms: ${{ github.event.inputs.skiparm == 'true' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} - # push: true - # tags: ghcr.io/ietf-tools/datatracker:${{ env.PKG_VERSION }} - # cache-from: type=gha - # cache-to: type=gha,mode=max - name: Update CHANGELOG id: changelog @@ -396,6 +376,12 @@ jobs: toTag: ${{ env.TO_TAG }} writeToFile: false + - name: Download Coverage Results + if: ${{ github.event.inputs.skiptests == 'false' || github.ref_name == 'release' }} + uses: actions/download-artifact@v4.1.8 + with: + name: coverage + - name: Prepare Coverage Action if: ${{ github.event.inputs.skiptests == 'false' || github.ref_name == 'release' }} working-directory: ./dev/coverage-action From 8b417470fd0697f95bb4e809b87b6f84a355c020 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Fri, 18 Oct 2024 01:07:21 -0400 Subject: [PATCH 35/41] ci: Update build.yml --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 48113ffb30..3a7779494a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -219,6 +219,10 @@ jobs: echo "==========================================================================" curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh + sudo docker buildx create \ + --name container-builder \ + --driver docker-container \ + --bootstrap --use echo "==========================================================================" echo "Login to ghcr.io..." From d26bd5f4204c48743ca363adfd0d9036cc910d6a Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Fri, 18 Oct 2024 01:37:58 -0400 Subject: [PATCH 36/41] ci: Update build.yml --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3a7779494a..104b29c3fe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -336,6 +336,7 @@ jobs: - name: Fetch release tarball run: | mkdir -p /home/runner/work/release + chmod 0600 ${{ github.workspace }}/prvkey.key scp -i ${{ github.workspace }}/prvkey.key azureuser@${{ steps.azlaunch.outputs.ipaddr }}:/workspace/release.tar.gz /home/runner/work/release/release.tar.gz - name: Destroy Build VM + resources From 9873439cdcc12e50a283e3db763b575bedd53adb Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Fri, 18 Oct 2024 04:06:34 -0400 Subject: [PATCH 37/41] ci: Update build.yml --- .github/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 104b29c3fe..756600cc1e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -170,9 +170,10 @@ jobs: --admin-username azureuser \ --generate-ssh-keys \ --priority Spot \ - --size Standard_D8as_v5 \ + --size Standard_D8ads_v5 \ --max-price -1 \ - --os-disk-size-gb 30 \ + --ephemeral-os-disk \ + --os-disk-size-gb 100 \ --eviction-policy Delete \ --nic-delete-option Delete \ --output tsv \ From 888101005148703bc06df73265cd310ed7b1e178 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Mon, 21 Oct 2024 16:45:36 -0500 Subject: [PATCH 38/41] feat: explicit names for meetecho recordings (#8062) * feat: explicit names for meetecho recordings * fix: better regex and kebab-styled-endpoint-name * fix: spaces around comparison operator --- ietf/api/tests.py | 66 ++++++++++++++++++- ietf/api/urls.py | 4 +- .../0009_session_meetecho_recording_name.py | 20 ++++++ ietf/meeting/models.py | 15 +++-- ietf/meeting/tests_models.py | 6 +- ietf/meeting/views.py | 39 +++++++++++ 6 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 ietf/meeting/migrations/0009_session_meetecho_recording_name.py diff --git a/ietf/api/tests.py b/ietf/api/tests.py index 4f2a7f7d3c..97e928dc51 100644 --- a/ietf/api/tests.py +++ b/ietf/api/tests.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2015-2020, All Rights Reserved +# Copyright The IETF Trust 2015-2024, All Rights Reserved # -*- coding: utf-8 -*- import base64 import datetime @@ -222,6 +222,70 @@ def test_api_set_session_video_url(self): event = doc.latest_event() self.assertEqual(event.by, recman) + def test_api_set_meetecho_recording_name(self): + url = urlreverse("ietf.meeting.views.api_set_meetecho_recording_name") + recmanrole = RoleFactory(group__type_id="ietf", name_id="recman") + recman = recmanrole.person + meeting = MeetingFactory(type_id="ietf") + session = SessionFactory(group__type_id="wg", meeting=meeting) + apikey = PersonalApiKey.objects.create(endpoint=url, person=recman) + name = "testname" + + # error cases + r = self.client.post(url, {}) + self.assertContains(r, "Missing apikey parameter", status_code=400) + + badrole = RoleFactory(group__type_id="ietf", name_id="ad") + badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person) + badrole.person.user.last_login = timezone.now() + badrole.person.user.save() + r = self.client.post(url, {"apikey": badapikey.hash()}) + self.assertContains(r, "Restricted to role: Recording Manager", status_code=403) + + r = self.client.post(url, {"apikey": apikey.hash()}) + self.assertContains(r, "Too long since last regular login", status_code=400) + recman.user.last_login = timezone.now() + recman.user.save() + + r = self.client.get(url, {"apikey": apikey.hash()}) + self.assertContains(r, "Method not allowed", status_code=405) + + r = self.client.post(url, {"apikey": apikey.hash()}) + self.assertContains(r, "Missing session_id parameter", status_code=400) + + r = self.client.post(url, {"apikey": apikey.hash(), "session_id": session.pk}) + self.assertContains(r, "Missing name parameter", status_code=400) + + bad_pk = int(Session.objects.order_by("-pk").first().pk) + 1 + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "session_id": bad_pk, + "name": name, + }, + ) + self.assertContains(r, "Session not found", status_code=400) + + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "session_id": "foo", + "name": name, + }, + ) + self.assertContains(r, "Invalid session_id", status_code=400) + + r = self.client.post( + url, {"apikey": apikey.hash(), "session_id": session.pk, "name": name} + ) + self.assertContains(r, "Done", status_code=200) + + session.refresh_from_db() + self.assertEqual(session.meetecho_recording_name, name) + + def test_api_add_session_attendees_deprecated(self): # Deprecated test - should be removed when we stop accepting a simple list of user PKs in # the add_session_attendees() view diff --git a/ietf/api/urls.py b/ietf/api/urls.py index 48525dfda2..431ad5c5d4 100644 --- a/ietf/api/urls.py +++ b/ietf/api/urls.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2017, All Rights Reserved +# Copyright The IETF Trust 2017-2024, All Rights Reserved from django.conf import settings from django.urls import include @@ -39,6 +39,8 @@ url(r'^iesg/position', views_ballot.api_set_position), # Let Meetecho set session video URLs url(r'^meeting/session/video/url$', meeting_views.api_set_session_video_url), + # Let Meetecho tell us the name of its recordings + url(r'^meeting/session/recording-name$', meeting_views.api_set_meetecho_recording_name), # Meeting agenda + floorplan data url(r'^meeting/(?P[A-Za-z0-9._+-]+)/agenda-data$', meeting_views.api_get_agenda_data), # Meeting session materials diff --git a/ietf/meeting/migrations/0009_session_meetecho_recording_name.py b/ietf/meeting/migrations/0009_session_meetecho_recording_name.py new file mode 100644 index 0000000000..79ca4919a3 --- /dev/null +++ b/ietf/meeting/migrations/0009_session_meetecho_recording_name.py @@ -0,0 +1,20 @@ +# Copyright The IETF Trust 2024, All Rights Reserved + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("meeting", "0008_remove_schedtimesessassignment_notes"), + ] + + operations = [ + migrations.AddField( + model_name="session", + name="meetecho_recording_name", + field=models.CharField( + blank=True, help_text="Name of the meetecho recording", max_length=64 + ), + ), + ] diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index e169077800..8c6fb97413 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -1042,6 +1042,7 @@ class Session(models.Model): on_agenda = models.BooleanField(default=True, help_text='Is this session visible on the meeting agenda?') has_onsite_tool = models.BooleanField(default=False, help_text="Does this session use the officially supported onsite and remote tooling?") chat_room = models.CharField(blank=True, max_length=32, help_text='Name of Zulip stream, if different from group acronym') + meetecho_recording_name = models.CharField(blank=True, max_length=64, help_text="Name of the meetecho recording") tombstone_for = models.ForeignKey('Session', blank=True, null=True, help_text="This session is the tombstone for a session that was rescheduled", on_delete=models.CASCADE) @@ -1332,17 +1333,23 @@ def onsite_tool_url(self): return None def _session_recording_url_label(self): + otsa = self.official_timeslotassignment() + if otsa is None: + return None if self.meeting.type.slug == "ietf" and self.has_onsite_tool: - session_label = f"IETF{self.meeting.number}-{self.group.acronym.upper()}-{self.official_timeslotassignment().timeslot.time.strftime('%Y%m%d-%H%M')}" + session_label = f"IETF{self.meeting.number}-{self.group.acronym.upper()}-{otsa.timeslot.time.strftime('%Y%m%d-%H%M')}" else: - session_label = f"IETF-{self.group.acronym.upper()}-{self.official_timeslotassignment().timeslot.time.strftime('%Y%m%d-%H%M')}" + session_label = f"IETF-{self.group.acronym.upper()}-{otsa.timeslot.time.strftime('%Y%m%d-%H%M')}" return session_label def session_recording_url(self): url_formatter = getattr(settings, "MEETECHO_SESSION_RECORDING_URL", "") url = None - if url_formatter and self.video_stream_url: - url = url_formatter.format(session_label=self._session_recording_url_label()) + name = self.meetecho_recording_name + if name is None or name.strip() == "": + name = self._session_recording_url_label() + if url_formatter.strip() != "" and name is not None: + url = url_formatter.format(session_label=name) return url diff --git a/ietf/meeting/tests_models.py b/ietf/meeting/tests_models.py index 8457423c51..03b706e1d7 100644 --- a/ietf/meeting/tests_models.py +++ b/ietf/meeting/tests_models.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2021, All Rights Reserved +# Copyright The IETF Trust 2021-2024, All Rights Reserved # -*- coding: utf-8 -*- """Tests of models in the Meeting application""" import datetime @@ -172,6 +172,10 @@ def test_session_recording_url(self, mock): settings.MEETECHO_SESSION_RECORDING_URL = "http://player.example.com?{session_label}" self.assertEqual(session.session_recording_url(), "http://player.example.com?LABEL") + session.meetecho_recording_name="actualname" + session.save() + self.assertEqual(session.session_recording_url(), "http://player.example.com?actualname") + def test_session_recording_url_label_ietf(self): session = SessionFactory( meeting__type_id='ietf', diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 6e6c4dfe23..bce6ccd1bf 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -4270,6 +4270,45 @@ class OldUploadRedirect(RedirectView): def get_redirect_url(self, **kwargs): return reverse_lazy('ietf.meeting.views.session_details',kwargs=self.kwargs) + +@require_api_key +@role_required("Recording Manager") +@csrf_exempt +def api_set_meetecho_recording_name(request): + """Set name for meetecho recording + + parameters: + apikey: the poster's personal API key + session_id: id of the session to update + name: the name to use for the recording at meetecho player + """ + def err(code, text): + return HttpResponse(text, status=code, content_type='text/plain') + + if request.method != "POST": + return HttpResponseNotAllowed( + content="Method not allowed", content_type="text/plain", permitted_methods=('POST',) + ) + + session_id = request.POST.get('session_id', None) + if session_id is None: + return err(400, 'Missing session_id parameter') + name = request.POST.get('name', None) + if name is None: + return err(400, 'Missing name parameter') + + try: + session = Session.objects.get(pk=session_id) + except Session.DoesNotExist: + return err(400, f"Session not found with session_id '{session_id}'") + except ValueError: + return err(400, "Invalid session_id: {session_id}") + + session.meetecho_recording_name = name + session.save() + + return HttpResponse("Done", status=200, content_type='text/plain') + @require_api_key @role_required('Recording Manager') @csrf_exempt From 1e193377df7cca32656c6bb062cea0a5ac3680f3 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Mon, 21 Oct 2024 18:48:51 -0300 Subject: [PATCH 39/41] ci: replace nginx default.conf (#8063) * ci: squash nginx default.conf * ci: replace nginx default.conf for auth --- k8s/auth.yaml | 2 +- k8s/datatracker.yaml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/k8s/auth.yaml b/k8s/auth.yaml index c92ed05163..2bdb064447 100644 --- a/k8s/auth.yaml +++ b/k8s/auth.yaml @@ -84,7 +84,7 @@ spec: mountPath: /etc/nginx/conf.d/00logging.conf subPath: nginx-logging.conf - name: dt-cfg - mountPath: /etc/nginx/conf.d/auth.conf + mountPath: /etc/nginx/conf.d/default.conf subPath: nginx-auth.conf # ----------------------------------------------------- # ScoutAPM Container diff --git a/k8s/datatracker.yaml b/k8s/datatracker.yaml index a8a9675687..3d9e86a29d 100644 --- a/k8s/datatracker.yaml +++ b/k8s/datatracker.yaml @@ -84,7 +84,8 @@ spec: mountPath: /etc/nginx/conf.d/00logging.conf subPath: nginx-logging.conf - name: dt-cfg - mountPath: /etc/nginx/conf.d/datatracker.conf + # Replaces the original default.conf + mountPath: /etc/nginx/conf.d/default.conf subPath: nginx-datatracker.conf # ----------------------------------------------------- # ScoutAPM Container From c26ba974baa50a8f95e0b112f7cab90d007f3302 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Tue, 22 Oct 2024 12:51:16 -0300 Subject: [PATCH 40/41] refactor: use form for search_submission (#8065) * refactor: use form for search_submission * chore: remove lint * style: Black --- ietf/submit/forms.py | 10 +++ ietf/submit/views.py | 93 ++++++++++++++------ ietf/templates/submit/search_submission.html | 8 +- 3 files changed, 77 insertions(+), 34 deletions(-) diff --git a/ietf/submit/forms.py b/ietf/submit/forms.py index 2781d3365a..bed87b77c2 100644 --- a/ietf/submit/forms.py +++ b/ietf/submit/forms.py @@ -659,3 +659,13 @@ def __init__(self, *args, **kwargs): self.fields['frm'].label='From' self.fields['frm'].widget.attrs['readonly'] = True self.fields['reply_to'].widget.attrs['readonly'] = True + + +class SubmissionSearchForm(forms.Form): + """Form used for search_submission""" + + name = forms.CharField(max_length=255, required=True, label="I-D name") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["name"].widget.attrs["placeholder"] = "draft-..." diff --git a/ietf/submit/views.py b/ietf/submit/views.py index 3f745741e4..f710bdeb0c 100644 --- a/ietf/submit/views.py +++ b/ietf/submit/views.py @@ -26,17 +26,47 @@ from ietf.ietfauth.utils import has_role, role_required from ietf.mailtrigger.utils import gather_address_lists from ietf.person.models import Email -from ietf.submit.forms import (SubmissionAutoUploadForm, AuthorForm, SubmitterForm, EditSubmissionForm, - PreapprovalForm, ReplacesForm, SubmissionManualUploadForm) +from ietf.submit.forms import ( + SubmissionAutoUploadForm, + AuthorForm, + SubmitterForm, + EditSubmissionForm, + PreapprovalForm, + ReplacesForm, + SubmissionManualUploadForm, + SubmissionSearchForm, +) from ietf.submit.mail import send_full_url, send_manual_post_request -from ietf.submit.models import (Submission, Preapproval, SubmissionExtResource, - DraftSubmissionStateName ) -from ietf.submit.tasks import process_uploaded_submission_task, process_and_accept_uploaded_submission_task, poke -from ietf.submit.utils import ( approvable_submissions_for_user, preapprovals_for_user, - recently_approved_by_user, validate_submission, create_submission_event, docevent_from_submission, - post_submission, cancel_submission, rename_submission_files, remove_submission_files, - get_submission, save_files, clear_existing_files, accept_submission, accept_submission_requires_group_approval, - accept_submission_requires_prev_auth_approval, update_submission_external_resources) +from ietf.submit.models import ( + Submission, + Preapproval, + SubmissionExtResource, + DraftSubmissionStateName, +) +from ietf.submit.tasks import ( + process_uploaded_submission_task, + process_and_accept_uploaded_submission_task, + poke, +) +from ietf.submit.utils import ( + approvable_submissions_for_user, + preapprovals_for_user, + recently_approved_by_user, + validate_submission, + create_submission_event, + docevent_from_submission, + post_submission, + cancel_submission, + rename_submission_files, + remove_submission_files, + get_submission, + save_files, + clear_existing_files, + accept_submission, + accept_submission_requires_group_approval, + accept_submission_requires_prev_auth_approval, + update_submission_external_resources, +) from ietf.stats.utils import clean_country_name from ietf.utils.accesstoken import generate_access_token from ietf.utils.log import log @@ -195,24 +225,33 @@ def api_submit_tombstone(request): def tool_instructions(request): return render(request, 'submit/tool_instructions.html', {'selected': 'instructions'}) + def search_submission(request): - error = None - name = None - if request.method == 'POST': - name = request.POST.get('name', '') - submission = Submission.objects.filter(name=name).order_by('-pk').first() - if submission: - return redirect(submission_status, submission_id=submission.pk) - else: - if re.search(r'-\d\d$', name): - submission = Submission.objects.filter(name=name[:-3]).order_by('-pk').first() - if submission: - return redirect(submission_status, submission_id=submission.pk) - error = 'No valid submission found for %s' % name - return render(request, 'submit/search_submission.html', - {'selected': 'status', - 'error': error, - 'name': name}) + if request.method == "POST": + form = SubmissionSearchForm(request.POST) + if form.is_valid(): + name = form.cleaned_data["name"] + submission = Submission.objects.filter(name=name).order_by("-pk").first() + if submission: + return redirect(submission_status, submission_id=submission.pk) + else: + if re.search(r"-\d\d$", name): + submission = ( + Submission.objects.filter(name=name[:-3]) + .order_by("-pk") + .first() + ) + if submission: + return redirect(submission_status, submission_id=submission.pk) + form.add_error(None, f"No valid submission found for {name}") + else: + form = SubmissionSearchForm() + return render( + request, + "submit/search_submission.html", + {"selected": "status", "form": form}, + ) + def can_edit_submission(user, submission, access_token): key_matched = access_token and submission.access_token() == access_token diff --git a/ietf/templates/submit/search_submission.html b/ietf/templates/submit/search_submission.html index 3f827abbea..807d6d7d86 100644 --- a/ietf/templates/submit/search_submission.html +++ b/ietf/templates/submit/search_submission.html @@ -12,14 +12,8 @@
{% csrf_token %}
- - + {% bootstrap_form form %}
- {% if error %} -

- {{ error }} -

- {% endif %}
{% endblock %} \ No newline at end of file From 2947e286f66880c17f26293cf1f02e83ab7e7e65 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Wed, 23 Oct 2024 04:51:11 -0400 Subject: [PATCH 41/41] docs: Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index c180fa3f9f..dc6e0c5663 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2008-2023, The IETF Trust +Copyright (c) 2008-2024, The IETF Trust All rights reserved. Redistribution and use in source and binary forms, with or without