diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 99c6c9ab3..5d5233656 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -247,6 +247,8 @@ jobs: platforms: 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 diff --git a/ietf/doc/templatetags/ietf_filters.py b/ietf/doc/templatetags/ietf_filters.py index 35c79ac28..35f9f91b4 100644 --- a/ietf/doc/templatetags/ietf_filters.py +++ b/ietf/doc/templatetags/ietf_filters.py @@ -933,3 +933,28 @@ def url_for_path(path): return f"{settings.IETF_ID_ARCHIVE_URL}{path.name}" else: return "#" + + +@register.filter +def is_in_stream(doc): + """ + Check if the doc is in one of the states in it stream that + indicate that is actually adopted, i.e., part of the stream. + (There are various "candidate" states that necessitate this + filter.) + """ + if not doc.stream: + return False + stream = doc.stream.slug + state = doc.get_state_slug(f"draft-stream-{doc.stream.slug}") + if not state: + return True + if stream == "ietf": + return state not in ["wg-cand", "c-adopt"] + elif stream == "irtf": + return state != "candidat" + elif stream == "iab": + return state not in ["candidat", "diff-org"] + elif stream == "editorial": + return True + return False diff --git a/ietf/doc/templatetags/tests_ietf_filters.py b/ietf/doc/templatetags/tests_ietf_filters.py index 72796abeb..f018b7d9b 100644 --- a/ietf/doc/templatetags/tests_ietf_filters.py +++ b/ietf/doc/templatetags/tests_ietf_filters.py @@ -7,9 +7,20 @@ from ietf.doc.factories import ( IndividualDraftFactory, CharterFactory, NewRevisionDocEventFactory, + StatusChangeFactory, + RgDraftFactory, + EditorialDraftFactory, + WgDraftFactory, + ConflictReviewFactory, + BofreqFactory, + StatementFactory, ) from ietf.doc.models import DocEvent -from ietf.doc.templatetags.ietf_filters import urlize_ietf_docs, is_valid_url +from ietf.doc.templatetags.ietf_filters import ( + urlize_ietf_docs, + is_valid_url, + is_in_stream, +) from ietf.person.models import Person from ietf.utils.test_utils import TestCase @@ -19,13 +30,28 @@ import debug # pyflakes: ignore class IetfFiltersTests(TestCase): + def test_is_in_stream(self): + for draft in [ + IndividualDraftFactory(), + CharterFactory(), + StatusChangeFactory(), + ConflictReviewFactory(), + StatementFactory(), + BofreqFactory(), + ]: + self.assertFalse(is_in_stream(draft)) + for draft in [RgDraftFactory(), WgDraftFactory(), EditorialDraftFactory()]: + self.assertTrue(is_in_stream(draft)) + for stream in ["iab", "ietf", "irtf", "ise", "editorial"]: + self.assertTrue(is_in_stream(IndividualDraftFactory(stream_id=stream))) + def test_is_valid_url(self): cases = [(settings.IDTRACKER_BASE_URL, True), ("not valid", False)] for url, result in cases: self.assertEqual(is_valid_url(url), result) def test_urlize_ietf_docs(self): - rfc = WgRfcFactory(rfc_number=123456,std_level_id="bcp") + rfc = WgRfcFactory(rfc_number=123456, std_level_id="bcp") rfc.save_with_history( [ DocEvent.objects.create( @@ -57,7 +83,6 @@ class IetfFiltersTests(TestCase): cases = [ ("no change", "no change"), - # TODO: rework subseries when we add them # ("bCp123456", 'bCp123456'), # ("Std 00123456", 'Std 00123456'), diff --git a/ietf/person/migrations/0002_alter_historicalperson_ascii_and_more.py b/ietf/person/migrations/0002_alter_historicalperson_ascii_and_more.py new file mode 100644 index 000000000..98d5da75d --- /dev/null +++ b/ietf/person/migrations/0002_alter_historicalperson_ascii_and_more.py @@ -0,0 +1,82 @@ +# Generated by Django 4.2.13 on 2024-05-22 18:50 + +from django.db import migrations, models +import ietf.person.models + + +class Migration(migrations.Migration): + + dependencies = [ + ("person", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="historicalperson", + name="ascii", + field=models.CharField( + help_text="Name as rendered in ASCII (Latin, unaccented) characters.", + max_length=255, + validators=[ietf.person.models.name_character_validator], + verbose_name="Full Name (ASCII)", + ), + ), + migrations.AlterField( + model_name="historicalperson", + name="ascii_short", + field=models.CharField( + blank=True, + help_text="Example: A. Nonymous. Fill in this with initials and surname only if taking the initials and surname of the ASCII name above produces an incorrect initials-only form. (Blank is OK).", + max_length=32, + null=True, + validators=[ietf.person.models.name_character_validator], + verbose_name="Abbreviated Name (ASCII)", + ), + ), + migrations.AlterField( + model_name="historicalperson", + name="plain", + field=models.CharField( + blank=True, + default="", + help_text="Use this if you have a Spanish double surname. Don't use this for nicknames, and don't use it unless you've actually observed that the datatracker shows your name incorrectly.", + max_length=64, + validators=[ietf.person.models.name_character_validator], + verbose_name="Plain Name correction (Unicode)", + ), + ), + migrations.AlterField( + model_name="person", + name="ascii", + field=models.CharField( + help_text="Name as rendered in ASCII (Latin, unaccented) characters.", + max_length=255, + validators=[ietf.person.models.name_character_validator], + verbose_name="Full Name (ASCII)", + ), + ), + migrations.AlterField( + model_name="person", + name="ascii_short", + field=models.CharField( + blank=True, + help_text="Example: A. Nonymous. Fill in this with initials and surname only if taking the initials and surname of the ASCII name above produces an incorrect initials-only form. (Blank is OK).", + max_length=32, + null=True, + validators=[ietf.person.models.name_character_validator], + verbose_name="Abbreviated Name (ASCII)", + ), + ), + migrations.AlterField( + model_name="person", + name="plain", + field=models.CharField( + blank=True, + default="", + help_text="Use this if you have a Spanish double surname. Don't use this for nicknames, and don't use it unless you've actually observed that the datatracker shows your name incorrectly.", + max_length=64, + validators=[ietf.person.models.name_character_validator], + verbose_name="Plain Name correction (Unicode)", + ), + ), + ] diff --git a/ietf/person/models.py b/ietf/person/models.py index 0bb2b149e..0c2515236 100644 --- a/ietf/person/models.py +++ b/ietf/person/models.py @@ -37,8 +37,12 @@ from ietf.utils.models import ForeignKey, OneToOneField def name_character_validator(value): - if '/' in value: - raise ValidationError('Name cannot contain "/" character.') + disallowed = "@:/" + found = set(disallowed).intersection(value) + if len(found) > 0: + raise ValidationError( + f"This name cannot contain the characters {', '.join(disallowed)}" + ) class Person(models.Model): @@ -48,11 +52,11 @@ class Person(models.Model): # The normal unicode form of the name. This must be # set to the same value as the ascii-form if equal. name = models.CharField("Full Name (Unicode)", max_length=255, db_index=True, help_text="Preferred long form of name.", validators=[name_character_validator]) - plain = models.CharField("Plain Name correction (Unicode)", max_length=64, default='', blank=True, help_text="Use this if you have a Spanish double surname. Don't use this for nicknames, and don't use it unless you've actually observed that the datatracker shows your name incorrectly.") + plain = models.CharField("Plain Name correction (Unicode)", max_length=64, default='', blank=True, help_text="Use this if you have a Spanish double surname. Don't use this for nicknames, and don't use it unless you've actually observed that the datatracker shows your name incorrectly.", validators=[name_character_validator]) # The normal ascii-form of the name. - ascii = models.CharField("Full Name (ASCII)", max_length=255, help_text="Name as rendered in ASCII (Latin, unaccented) characters.") + ascii = models.CharField("Full Name (ASCII)", max_length=255, help_text="Name as rendered in ASCII (Latin, unaccented) characters.", validators=[name_character_validator]) # The short ascii-form of the name. Also in alias table if non-null - ascii_short = models.CharField("Abbreviated Name (ASCII)", max_length=32, null=True, blank=True, help_text="Example: A. Nonymous. Fill in this with initials and surname only if taking the initials and surname of the ASCII name above produces an incorrect initials-only form. (Blank is OK).") + ascii_short = models.CharField("Abbreviated Name (ASCII)", max_length=32, null=True, blank=True, help_text="Example: A. Nonymous. Fill in this with initials and surname only if taking the initials and surname of the ASCII name above produces an incorrect initials-only form. (Blank is OK).", validators=[name_character_validator]) pronouns_selectable = jsonfield.JSONCharField("Pronouns", max_length=120, blank=True, null=True, default=list ) pronouns_freetext = models.CharField(" ", max_length=30, null=True, blank=True, help_text="Optionally provide your personal pronouns. These will be displayed on your public profile page and alongside your name in Meetecho and, in future, other systems. Select any number of the checkboxes OR provide a custom string up to 30 characters.") biography = models.TextField(blank=True, help_text="Short biography for use on leadership pages. Use plain text or reStructuredText markup.") diff --git a/ietf/person/tests.py b/ietf/person/tests.py index be3cfc056..e5bc855a2 100644 --- a/ietf/person/tests.py +++ b/ietf/person/tests.py @@ -246,9 +246,11 @@ class PersonTests(TestCase): self.assertNotIn('cdn-cgi/photo',p.cdn_photo_url()) def test_invalid_name_characters_rejected(self): - slash_person = PersonFactory.build(name='I have a /', user=None) # build() does not save the new object - with self.assertRaises(ValidationError): - slash_person.full_clean() # calls validators (save() does *not*) + for disallowed in "/:@": + # build() does not save the new object + person_with_bad_name = PersonFactory.build(name=f"I have a {disallowed}", user=None) + with self.assertRaises(ValidationError, msg=f"Name with a {disallowed} char should be rejected"): + person_with_bad_name.full_clean() # calls validators (save() does *not*) class PersonUtilsTests(TestCase): diff --git a/ietf/static/js/ietf.js b/ietf/static/js/ietf.js index 215d80553..74fd39a85 100644 --- a/ietf/static/js/ietf.js +++ b/ietf/static/js/ietf.js @@ -57,7 +57,7 @@ $(document) var text = $(this) .text(); // insert some at strategic places - var newtext = text.replace(/([@._+])/g, "$1"); + var newtext = text.replace(/(\S)([@._+])(\S)/g, "$1$2$3"); if (newtext === text) { return; } diff --git a/ietf/templates/doc/disclaimer.html b/ietf/templates/doc/disclaimer.html new file mode 100644 index 000000000..31ce6c397 --- /dev/null +++ b/ietf/templates/doc/disclaimer.html @@ -0,0 +1,28 @@ +{# Copyright The IETF Trust 2016-2023, All Rights Reserved #} +{% load origin %} +{% load ietf_filters %} +{% origin %} +{% if doc.type_id == "rfc" %} + {% if doc.stream.slug != "ietf" and doc.std_level.slug|default:"unk" not in "bcp,ds,ps,std"|split:"," %} + + {% endif %} +{% elif doc|is_in_stream %} + {% if doc.stream.slug != "ietf" and doc.std_level.slug|default:"unk" not in "bcp,ds,ps,std"|split:"," %} + + {% endif %} +{% else %} + +{% endif %} \ No newline at end of file diff --git a/ietf/templates/doc/document_draft.html b/ietf/templates/doc/document_draft.html index 0dc64e9f3..befbc759f 100644 --- a/ietf/templates/doc/document_draft.html +++ b/ietf/templates/doc/document_draft.html @@ -27,6 +27,7 @@ {% origin %} {{ top|safe }} {% include "doc/revisions_list.html" with document_html=document_html %} + {% include "doc/disclaimer.html" with document_html=document_html %}
{% if doc.rev != latest_rev %}
The information below is for an old version of the document.
diff --git a/ietf/templates/doc/document_info.html b/ietf/templates/doc/document_info.html index e30656e16..97cd49cf7 100644 --- a/ietf/templates/doc/document_info.html +++ b/ietf/templates/doc/document_info.html @@ -63,7 +63,7 @@ {% if doc.became_rfc %} This is an older version of an Internet-Draft that was ultimately published as {{doc.became_rfc.name|prettystdname}}. {% elif snapshot and doc.rev != latest_rev %} - This is an older version of an Internet-Draft whose latest revision state is "{{ doc.doc.get_state }}". + This is an older version of an Internet-Draft whose latest revision state is "{{ doc.doc.get_state }}". {% else %} {% if snapshot and doc.rev == latest_rev %}{{ doc.doc.get_state }}{% else %}{{ doc.get_state }}{% endif %} Internet-Draft {% if submission %}({{ submission|safe }}){% endif %} @@ -75,6 +75,9 @@ Expired & archived {% endif %} + {% if document_html %} + {% include "doc/disclaimer.html" with document_html=document_html %} + {% endif %} {% if document_html %} diff --git a/ietf/templates/doc/document_rfc.html b/ietf/templates/doc/document_rfc.html index b25d434de..7612ef891 100644 --- a/ietf/templates/doc/document_rfc.html +++ b/ietf/templates/doc/document_rfc.html @@ -22,6 +22,7 @@ {% block content %} {% origin %} {{ top|safe }} + {% include "doc/disclaimer.html" with document_html=document_html %}
{% include "doc/document_info.html" %}