From 106df1ad7c05476903d7eda4fa38ed6a0f501ab0 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 16 Aug 2023 16:39:07 -0500 Subject: [PATCH 1/5] feat: require draft revisions in ipr disclosures. Explain missing ones. --- ietf/ipr/factories.py | 8 +++ ietf/ipr/forms.py | 9 +++ ietf/ipr/templatetags/ipr_filters.py | 40 +++++++++++- ietf/ipr/tests.py | 98 +++++++++++++++++++++++++++- ietf/templates/ipr/details_view.html | 16 ++++- 5 files changed, 167 insertions(+), 4 deletions(-) diff --git a/ietf/ipr/factories.py b/ietf/ipr/factories.py index 5e948e010b8..229bcb03d47 100644 --- a/ietf/ipr/factories.py +++ b/ietf/ipr/factories.py @@ -95,3 +95,11 @@ class Meta: disclosure = factory.SubFactory(IprDisclosureBaseFactory) desc = factory.Faker('sentence') +class IprDocRelFactory(factory.django.DjangoModelFactory): + class Meta: + model = IprDocRel + + disclosure = factory.SubFactory(HolderIprDisclosureFactory) + document = factory.SubFactory("ietf.doc.factories.IndividualDraftFactory") + revisions = "00" + sections = "" diff --git a/ietf/ipr/forms.py b/ietf/ipr/forms.py index 5f6c76c3778..7bf995aee9f 100644 --- a/ietf/ipr/forms.py +++ b/ietf/ipr/forms.py @@ -105,6 +105,15 @@ class Meta: } help_texts = { 'sections': 'Sections' } + def clean(self): + cleaned_data = super().clean() + revisions = cleaned_data.get("revisions") + document = cleaned_data.get("document") + if not document.name.startswith("rfc"): + if revisions.strip()=="": + self.add_error("revisions", "Revisions of this Internet-Draft for which this disclosure is relevant must be specified.") + return cleaned_data + patent_number_help_text = "Enter one or more comma-separated patent publication or application numbers as two-letter country code and serial number, e.g.: US62/123456 or WO2017123456. Do not include thousands-separator commas in serial numbers. It is preferable to use individual disclosures for each patent, even if this field permits multiple patents to be listed, in order to get inventor, title, and date information below correct." validate_patent_number = RegexValidator( regex=(r"^(" diff --git a/ietf/ipr/templatetags/ipr_filters.py b/ietf/ipr/templatetags/ipr_filters.py index 21d5579bf94..1f980158243 100644 --- a/ietf/ipr/templatetags/ipr_filters.py +++ b/ietf/ipr/templatetags/ipr_filters.py @@ -1,10 +1,14 @@ -# Copyright The IETF Trust 2014-2020, All Rights Reserved +# Copyright The IETF Trust 2014-2023, All Rights Reserved # -*- coding: utf-8 -*- +import debug # pyflakes: ignore + from django import template from django.utils.html import format_html +from ietf.doc.models import NewRevisionDocEvent + register = template.Library() @@ -26,3 +30,37 @@ def render_message_for_history(msg): @register.filter def to_class_name(value): return value.__class__.__name__ + +def draft_rev_at_time(iprdocrel): + draft = iprdocrel.document.document + event = iprdocrel.disclosure.get_latest_event_posted() + if event is None: + return ("","The draft's revision at the time this disclosure was posted could not be determined.") + time = event.time + if not NewRevisionDocEvent.objects.filter(doc=draft).exists(): + return ("","The draft's revision at the time this disclosure was posted could not be determined.") + revs_before = NewRevisionDocEvent.objects.filter(doc=draft, time__lte=time).order_by('-time') + if not revs_before.exists(): + return ("","The draft's initial submission was after this disclosure was posted.") + result = revs_before[0].rev + return (result, "") + +@register.filter +def no_revisions_message(iprdocrel): + draft = iprdocrel.document.document + if draft.type_id != "draft" or iprdocrel.revisions.strip()!="": + return "" + rev_at_time, exception = draft_rev_at_time(iprdocrel) + current_rev = draft.rev + + first_line = "No revisions for this draft were specified in this disclosure." + contact_line = "Contact the discloser or patent holder if there are questions about which revisions this disclosure pertains to." + + if current_rev == "00": + return f"{first_line} However, there is only one revision of this draft." + + if rev_at_time: + return f"{first_line} The draft's revision was {rev_at_time} at the time this disclosure was posted. {contact_line}" + else: + return f"{first_line} {exception} {contact_line}" + diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index fd07821af3c..296c3a8aecf 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -16,13 +16,24 @@ import debug # pyflakes:ignore from ietf.doc.models import DocAlias -from ietf.doc.factories import DocumentFactory, WgDraftFactory, WgRfcFactory +from ietf.doc.factories import ( + DocumentFactory, + WgDraftFactory, + WgRfcFactory, + NewRevisionDocEventFactory +) from ietf.group.factories import RoleFactory -from ietf.ipr.factories import HolderIprDisclosureFactory, GenericIprDisclosureFactory, IprEventFactory +from ietf.ipr.factories import ( + HolderIprDisclosureFactory, + GenericIprDisclosureFactory, + IprDocRelFactory, + IprEventFactory +) 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) +from ietf.ipr.templatetags.ipr_filters import no_revisions_message from ietf.ipr.utils import get_genitive, get_ipr_summary from ietf.mailtrigger.utils import gather_address_lists from ietf.message.models import Message @@ -305,6 +316,38 @@ def test_new_specific(self): r = self.client.post(url, data) self.assertContains(r, "Your IPR disclosure has been submitted", msg_prefix="Checked patent number: %s" % patent_number) + def test_new_specific_no_revision(self): + draft = WgDraftFactory() + WgRfcFactory() + url = urlreverse("ietf.ipr.views.new", kwargs={ "type": "specific" }) + + # successful post + empty_outbox() + data = { + "holder_legal_name": "Test Legal", + "holder_contact_name": "Test Holder", + "holder_contact_email": "test@holder.com", + "holder_contact_info": "555-555-0100", + "ietfer_name": "Test Participant", + "ietfer_contact_info": "555-555-0101", + "iprdocrel_set-TOTAL_FORMS": 2, + "iprdocrel_set-INITIAL_FORMS": 0, + "iprdocrel_set-0-document": draft.docalias.first().pk, + "iprdocrel_set-1-document": DocAlias.objects.filter(name__startswith="rfc").first().pk, + "patent_number": "SE12345678901", + "patent_inventor": "A. Nonymous", + "patent_title": "A method of transferring bits", + "patent_date": "2000-01-01", + "has_patent_pending": False, + "licensing": "royalty-free", + "submitter_name": "Test Holder", + "submitter_email": "test@holder.com", + } + r = self.client.post(url, data) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(q("#id_iprdocrel_set-0-revisions").hasClass("is-invalid")) + def test_new_thirdparty(self): """Add a new third-party disclosure. Note: submitter does not need to be logged in. """ @@ -761,4 +804,55 @@ def test_docevent_creation(self): removed_docevent = doc.docevent_set.filter(type='removed_related_ipr').first() self.assertIn(ipr.title, removed_docevent.desc, 'IprDisclosure title does not appear in DocEvent desc when removed') + + def test_no_revisions_message(self): + draft = WgDraftFactory(rev="02") + now = timezone.now() + for rev in range(0,3): + NewRevisionDocEventFactory(doc=draft, rev=f"{rev:02d}", time=now-datetime.timedelta(days=30*(2-rev))) + # Disclosure has non-empty revisions field on its related draft + iprdocrel = IprDocRelFactory(document=draft.docalias.first()) + IprEventFactory(type_id="posted",time=now,disclosure=iprdocrel.disclosure) + self.assertEqual( + no_revisions_message(iprdocrel), + "" + ) + + # Disclosure has more than one revision, none called out, disclosure after submissions + iprdocrel = IprDocRelFactory(document=draft.docalias.first(), revisions="") + IprEventFactory(type_id="posted",time=now,disclosure=iprdocrel.disclosure) + self.assertEqual( + no_revisions_message(iprdocrel), + "No revisions for this draft were specified in this disclosure. The draft's revision was 02 at the time this disclosure was posted. Contact the discloser or patent holder if there are questions about which revisions this disclosure pertains to." + ) + + # Disclosure has more than one revision, none called out, disclosure after 01 + iprdocrel = IprDocRelFactory(document=draft.docalias.first(), revisions="") + e = IprEventFactory(type_id="posted",disclosure=iprdocrel.disclosure) + e.time = now-datetime.timedelta(days=15) + e.save() + self.assertEqual( + no_revisions_message(iprdocrel), + "No revisions for this draft were specified in this disclosure. The draft's revision was 01 at the time this disclosure was posted. Contact the discloser or patent holder if there are questions about which revisions this disclosure pertains to." + ) + + # Disclosure has more than one revision, none called out, disclosure was before the 00 + iprdocrel = IprDocRelFactory(document=draft.docalias.first(), revisions="") + e = IprEventFactory(type_id="posted",disclosure=iprdocrel.disclosure) + e.time = now-datetime.timedelta(days=180) + e.save() + self.assertEqual( + no_revisions_message(iprdocrel), + "No revisions for this draft were specified in this disclosure. The draft's initial submission was after this disclosure was posted. Contact the discloser or patent holder if there are questions about which revisions this disclosure pertains to." + ) + + # disclosed draft has no NewRevisionDocEvents + draft = WgDraftFactory(rev="20") + draft.docevent_set.all().delete() + iprdocrel = IprDocRelFactory(document=draft.docalias.first(), revisions="") + IprEventFactory(type_id="posted",disclosure=iprdocrel.disclosure) + self.assertEqual( + no_revisions_message(iprdocrel), + "No revisions for this draft were specified in this disclosure. The draft's revision at the time this disclosure was posted could not be determined. Contact the discloser or patent holder if there are questions about which revisions this disclosure pertains to." + ) diff --git a/ietf/templates/ipr/details_view.html b/ietf/templates/ipr/details_view.html index aa695aeee79..97a2af656aa 100644 --- a/ietf/templates/ipr/details_view.html +++ b/ietf/templates/ipr/details_view.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{# Copyright The IETF Trust 2015, 2017. All Rights Reserved. #} +{# Copyright The IETF Trust 2015-2023. All Rights Reserved. #} {% load origin %} {% load ietf_filters ipr_filters textfilters %} {% block title %}IPR Details - {{ ipr.title }}{% endblock %} @@ -388,6 +388,13 @@

{{ iprdocrel.revisions }}
+ {% elif iprdocrel.doc_type == "Internet-Draft" %} +
+ Notice: +
+
+ {{ iprdocrel|no_revisions_message }} +
{% endif %} {% if iprdocrel.sections %}
@@ -427,6 +434,13 @@

{{ iprdocrel.revisions }}
+ {% elif iprdocrel.doc_type == "Internet-Draft" %} +
+ Notice: +
+
+ {{ iprdocrel|no_revisions_message }} +
{% endif %} {% if iprdocrel.sections %}
From ee0a4e7dcb0e5f310f79c92177908c820372fc7a Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 16 Aug 2023 16:41:39 -0500 Subject: [PATCH 2/5] chore: update copyrights --- ietf/ipr/factories.py | 2 +- ietf/ipr/forms.py | 2 +- ietf/ipr/tests.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ietf/ipr/factories.py b/ietf/ipr/factories.py index 229bcb03d47..d4a1f2eb4eb 100644 --- a/ietf/ipr/factories.py +++ b/ietf/ipr/factories.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2018-2020, All Rights Reserved +# Copyright The IETF Trust 2018-2023, All Rights Reserved # -*- coding: utf-8 -*- diff --git a/ietf/ipr/forms.py b/ietf/ipr/forms.py index 7bf995aee9f..92c829b1f4e 100644 --- a/ietf/ipr/forms.py +++ b/ietf/ipr/forms.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2014-2020, All Rights Reserved +# Copyright The IETF Trust 2014-2023, All Rights Reserved # -*- coding: utf-8 -*- diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index 296c3a8aecf..c8317507694 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2009-2020, All Rights Reserved +# Copyright The IETF Trust 2009-2023, All Rights Reserved # -*- coding: utf-8 -*- From 16406d51038918a9e9aa23aa403a66031247fe37 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 17 Aug 2023 10:43:25 -0500 Subject: [PATCH 3/5] chore: address review comments --- ietf/ipr/forms.py | 2 +- ietf/ipr/templatetags/ipr_filters.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ietf/ipr/forms.py b/ietf/ipr/forms.py index 92c829b1f4e..cd4b406f957 100644 --- a/ietf/ipr/forms.py +++ b/ietf/ipr/forms.py @@ -110,7 +110,7 @@ def clean(self): revisions = cleaned_data.get("revisions") document = cleaned_data.get("document") if not document.name.startswith("rfc"): - if revisions.strip()=="": + if revisions.strip() == "": self.add_error("revisions", "Revisions of this Internet-Draft for which this disclosure is relevant must be specified.") return cleaned_data diff --git a/ietf/ipr/templatetags/ipr_filters.py b/ietf/ipr/templatetags/ipr_filters.py index 1f980158243..d02312383e1 100644 --- a/ietf/ipr/templatetags/ipr_filters.py +++ b/ietf/ipr/templatetags/ipr_filters.py @@ -39,16 +39,16 @@ def draft_rev_at_time(iprdocrel): time = event.time if not NewRevisionDocEvent.objects.filter(doc=draft).exists(): return ("","The draft's revision at the time this disclosure was posted could not be determined.") - revs_before = NewRevisionDocEvent.objects.filter(doc=draft, time__lte=time).order_by('-time') - if not revs_before.exists(): + rev_event_before = NewRevisionDocEvent.objects.filter(doc=draft, time__lte=time).order_by('-time').first() + if rev_event_before is None: return ("","The draft's initial submission was after this disclosure was posted.") - result = revs_before[0].rev - return (result, "") + else: + return(rev_event_before.rev, "") @register.filter def no_revisions_message(iprdocrel): draft = iprdocrel.document.document - if draft.type_id != "draft" or iprdocrel.revisions.strip()!="": + if draft.type_id != "draft" or iprdocrel.revisions.strip() != "": return "" rev_at_time, exception = draft_rev_at_time(iprdocrel) current_rev = draft.rev From b758f740cd03689caeb50d15bb36d36f6bc378c8 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 17 Aug 2023 11:11:17 -0500 Subject: [PATCH 4/5] fix: draft should be Internet-Draft --- ietf/ipr/templatetags/ipr_filters.py | 12 ++++++------ ietf/ipr/tests.py | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ietf/ipr/templatetags/ipr_filters.py b/ietf/ipr/templatetags/ipr_filters.py index d02312383e1..01303875006 100644 --- a/ietf/ipr/templatetags/ipr_filters.py +++ b/ietf/ipr/templatetags/ipr_filters.py @@ -35,13 +35,13 @@ def draft_rev_at_time(iprdocrel): draft = iprdocrel.document.document event = iprdocrel.disclosure.get_latest_event_posted() if event is None: - return ("","The draft's revision at the time this disclosure was posted could not be determined.") + return ("","The Internet-Draft's revision at the time this disclosure was posted could not be determined.") time = event.time if not NewRevisionDocEvent.objects.filter(doc=draft).exists(): - return ("","The draft's revision at the time this disclosure was posted could not be determined.") + return ("","The Internet-Draft's revision at the time this disclosure was posted could not be determined.") rev_event_before = NewRevisionDocEvent.objects.filter(doc=draft, time__lte=time).order_by('-time').first() if rev_event_before is None: - return ("","The draft's initial submission was after this disclosure was posted.") + return ("","The Internet-Draft's initial submission was after this disclosure was posted.") else: return(rev_event_before.rev, "") @@ -53,14 +53,14 @@ def no_revisions_message(iprdocrel): rev_at_time, exception = draft_rev_at_time(iprdocrel) current_rev = draft.rev - first_line = "No revisions for this draft were specified in this disclosure." + first_line = "No revisions for this Internet-Draft were specified in this disclosure." contact_line = "Contact the discloser or patent holder if there are questions about which revisions this disclosure pertains to." if current_rev == "00": - return f"{first_line} However, there is only one revision of this draft." + return f"{first_line} However, there is only one revision of this Internet-Draft." if rev_at_time: - return f"{first_line} The draft's revision was {rev_at_time} at the time this disclosure was posted. {contact_line}" + return f"{first_line} The Internet-Draft's revision was {rev_at_time} at the time this disclosure was posted. {contact_line}" else: return f"{first_line} {exception} {contact_line}" diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index c8317507694..76eafa58021 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -824,7 +824,7 @@ def test_no_revisions_message(self): IprEventFactory(type_id="posted",time=now,disclosure=iprdocrel.disclosure) self.assertEqual( no_revisions_message(iprdocrel), - "No revisions for this draft were specified in this disclosure. The draft's revision was 02 at the time this disclosure was posted. Contact the discloser or patent holder if there are questions about which revisions this disclosure pertains to." + "No revisions for this Internet-Draft were specified in this disclosure. The Internet-Draft's revision was 02 at the time this disclosure was posted. Contact the discloser or patent holder if there are questions about which revisions this disclosure pertains to." ) # Disclosure has more than one revision, none called out, disclosure after 01 @@ -834,7 +834,7 @@ def test_no_revisions_message(self): e.save() self.assertEqual( no_revisions_message(iprdocrel), - "No revisions for this draft were specified in this disclosure. The draft's revision was 01 at the time this disclosure was posted. Contact the discloser or patent holder if there are questions about which revisions this disclosure pertains to." + "No revisions for this Internet-Draft were specified in this disclosure. The Internet-Draft's revision was 01 at the time this disclosure was posted. Contact the discloser or patent holder if there are questions about which revisions this disclosure pertains to." ) # Disclosure has more than one revision, none called out, disclosure was before the 00 @@ -844,7 +844,7 @@ def test_no_revisions_message(self): e.save() self.assertEqual( no_revisions_message(iprdocrel), - "No revisions for this draft were specified in this disclosure. The draft's initial submission was after this disclosure was posted. Contact the discloser or patent holder if there are questions about which revisions this disclosure pertains to." + "No revisions for this Internet-Draft were specified in this disclosure. The Internet-Draft's initial submission was after this disclosure was posted. Contact the discloser or patent holder if there are questions about which revisions this disclosure pertains to." ) # disclosed draft has no NewRevisionDocEvents @@ -854,5 +854,5 @@ def test_no_revisions_message(self): IprEventFactory(type_id="posted",disclosure=iprdocrel.disclosure) self.assertEqual( no_revisions_message(iprdocrel), - "No revisions for this draft were specified in this disclosure. The draft's revision at the time this disclosure was posted could not be determined. Contact the discloser or patent holder if there are questions about which revisions this disclosure pertains to." + "No revisions for this Internet-Draft were specified in this disclosure. The Internet-Draft's revision at the time this disclosure was posted could not be determined. Contact the discloser or patent holder if there are questions about which revisions this disclosure pertains to." ) From 97a2e473d9a2fbf442157fe74235259af17b7c73 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 17 Aug 2023 11:13:59 -0500 Subject: [PATCH 5/5] test: cover single revision case --- ietf/ipr/tests.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index 76eafa58021..65d8323eb13 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -855,4 +855,13 @@ def test_no_revisions_message(self): self.assertEqual( no_revisions_message(iprdocrel), "No revisions for this Internet-Draft were specified in this disclosure. The Internet-Draft's revision at the time this disclosure was posted could not be determined. Contact the discloser or patent holder if there are questions about which revisions this disclosure pertains to." - ) + ) + + # disclosed draft has only one revision + draft = WgDraftFactory(rev="00") + iprdocrel = IprDocRelFactory(document=draft.docalias.first(), revisions="") + IprEventFactory(type_id="posted",disclosure=iprdocrel.disclosure) + self.assertEqual( + no_revisions_message(iprdocrel), + "No revisions for this Internet-Draft were specified in this disclosure. However, there is only one revision of this Internet-Draft." + )