Skip to content

Commit 53be2c3

Browse files
authored
feat: require draft revisions in ipr disclosures. Explain missing ones. (ietf-tools#6160)
* feat: require draft revisions in ipr disclosures. Explain missing ones. * chore: update copyrights * chore: address review comments * fix: draft should be Internet-Draft * test: cover single revision case
1 parent 5522de9 commit 53be2c3

5 files changed

Lines changed: 179 additions & 7 deletions

File tree

ietf/ipr/factories.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright The IETF Trust 2018-2020, All Rights Reserved
1+
# Copyright The IETF Trust 2018-2023, All Rights Reserved
22
# -*- coding: utf-8 -*-
33

44

@@ -95,3 +95,11 @@ class Meta:
9595
disclosure = factory.SubFactory(IprDisclosureBaseFactory)
9696
desc = factory.Faker('sentence')
9797

98+
class IprDocRelFactory(factory.django.DjangoModelFactory):
99+
class Meta:
100+
model = IprDocRel
101+
102+
disclosure = factory.SubFactory(HolderIprDisclosureFactory)
103+
document = factory.SubFactory("ietf.doc.factories.IndividualDraftFactory")
104+
revisions = "00"
105+
sections = ""

ietf/ipr/forms.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright The IETF Trust 2014-2020, All Rights Reserved
1+
# Copyright The IETF Trust 2014-2023, All Rights Reserved
22
# -*- coding: utf-8 -*-
33

44

@@ -105,6 +105,15 @@ class Meta:
105105
}
106106
help_texts = { 'sections': 'Sections' }
107107

108+
def clean(self):
109+
cleaned_data = super().clean()
110+
revisions = cleaned_data.get("revisions")
111+
document = cleaned_data.get("document")
112+
if not document.name.startswith("rfc"):
113+
if revisions.strip() == "":
114+
self.add_error("revisions", "Revisions of this Internet-Draft for which this disclosure is relevant must be specified.")
115+
return cleaned_data
116+
108117
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."
109118
validate_patent_number = RegexValidator(
110119
regex=(r"^("

ietf/ipr/templatetags/ipr_filters.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
# Copyright The IETF Trust 2014-2020, All Rights Reserved
1+
# Copyright The IETF Trust 2014-2023, All Rights Reserved
22
# -*- coding: utf-8 -*-
33

44

5+
import debug # pyflakes: ignore
6+
57
from django import template
68
from django.utils.html import format_html
79

10+
from ietf.doc.models import NewRevisionDocEvent
11+
812
register = template.Library()
913

1014

@@ -26,3 +30,37 @@ def render_message_for_history(msg):
2630
@register.filter
2731
def to_class_name(value):
2832
return value.__class__.__name__
33+
34+
def draft_rev_at_time(iprdocrel):
35+
draft = iprdocrel.document.document
36+
event = iprdocrel.disclosure.get_latest_event_posted()
37+
if event is None:
38+
return ("","The Internet-Draft's revision at the time this disclosure was posted could not be determined.")
39+
time = event.time
40+
if not NewRevisionDocEvent.objects.filter(doc=draft).exists():
41+
return ("","The Internet-Draft's revision at the time this disclosure was posted could not be determined.")
42+
rev_event_before = NewRevisionDocEvent.objects.filter(doc=draft, time__lte=time).order_by('-time').first()
43+
if rev_event_before is None:
44+
return ("","The Internet-Draft's initial submission was after this disclosure was posted.")
45+
else:
46+
return(rev_event_before.rev, "")
47+
48+
@register.filter
49+
def no_revisions_message(iprdocrel):
50+
draft = iprdocrel.document.document
51+
if draft.type_id != "draft" or iprdocrel.revisions.strip() != "":
52+
return ""
53+
rev_at_time, exception = draft_rev_at_time(iprdocrel)
54+
current_rev = draft.rev
55+
56+
first_line = "No revisions for this Internet-Draft were specified in this disclosure."
57+
contact_line = "Contact the discloser or patent holder if there are questions about which revisions this disclosure pertains to."
58+
59+
if current_rev == "00":
60+
return f"{first_line} However, there is only one revision of this Internet-Draft."
61+
62+
if rev_at_time:
63+
return f"{first_line} The Internet-Draft's revision was {rev_at_time} at the time this disclosure was posted. {contact_line}"
64+
else:
65+
return f"{first_line} {exception} {contact_line}"
66+

ietf/ipr/tests.py

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright The IETF Trust 2009-2020, All Rights Reserved
1+
# Copyright The IETF Trust 2009-2023, All Rights Reserved
22
# -*- coding: utf-8 -*-
33

44

@@ -16,13 +16,24 @@
1616
import debug # pyflakes:ignore
1717

1818
from ietf.doc.models import DocAlias
19-
from ietf.doc.factories import DocumentFactory, WgDraftFactory, WgRfcFactory
19+
from ietf.doc.factories import (
20+
DocumentFactory,
21+
WgDraftFactory,
22+
WgRfcFactory,
23+
NewRevisionDocEventFactory
24+
)
2025
from ietf.group.factories import RoleFactory
21-
from ietf.ipr.factories import HolderIprDisclosureFactory, GenericIprDisclosureFactory, IprEventFactory
26+
from ietf.ipr.factories import (
27+
HolderIprDisclosureFactory,
28+
GenericIprDisclosureFactory,
29+
IprDocRelFactory,
30+
IprEventFactory
31+
)
2232
from ietf.ipr.mail import (process_response_email, get_reply_to, get_update_submitter_emails,
2333
get_pseudo_submitter, get_holders, get_update_cc_addrs)
2434
from ietf.ipr.models import (IprDisclosureBase,GenericIprDisclosure,HolderIprDisclosure,
2535
ThirdPartyIprDisclosure)
36+
from ietf.ipr.templatetags.ipr_filters import no_revisions_message
2637
from ietf.ipr.utils import get_genitive, get_ipr_summary
2738
from ietf.mailtrigger.utils import gather_address_lists
2839
from ietf.message.models import Message
@@ -305,6 +316,38 @@ def test_new_specific(self):
305316
r = self.client.post(url, data)
306317
self.assertContains(r, "Your IPR disclosure has been submitted", msg_prefix="Checked patent number: %s" % patent_number)
307318

319+
def test_new_specific_no_revision(self):
320+
draft = WgDraftFactory()
321+
WgRfcFactory()
322+
url = urlreverse("ietf.ipr.views.new", kwargs={ "type": "specific" })
323+
324+
# successful post
325+
empty_outbox()
326+
data = {
327+
"holder_legal_name": "Test Legal",
328+
"holder_contact_name": "Test Holder",
329+
"holder_contact_email": "test@holder.com",
330+
"holder_contact_info": "555-555-0100",
331+
"ietfer_name": "Test Participant",
332+
"ietfer_contact_info": "555-555-0101",
333+
"iprdocrel_set-TOTAL_FORMS": 2,
334+
"iprdocrel_set-INITIAL_FORMS": 0,
335+
"iprdocrel_set-0-document": draft.docalias.first().pk,
336+
"iprdocrel_set-1-document": DocAlias.objects.filter(name__startswith="rfc").first().pk,
337+
"patent_number": "SE12345678901",
338+
"patent_inventor": "A. Nonymous",
339+
"patent_title": "A method of transferring bits",
340+
"patent_date": "2000-01-01",
341+
"has_patent_pending": False,
342+
"licensing": "royalty-free",
343+
"submitter_name": "Test Holder",
344+
"submitter_email": "test@holder.com",
345+
}
346+
r = self.client.post(url, data)
347+
self.assertEqual(r.status_code, 200)
348+
q = PyQuery(r.content)
349+
self.assertTrue(q("#id_iprdocrel_set-0-revisions").hasClass("is-invalid"))
350+
308351
def test_new_thirdparty(self):
309352
"""Add a new third-party disclosure. Note: submitter does not need to be logged in.
310353
"""
@@ -761,4 +804,64 @@ def test_docevent_creation(self):
761804
removed_docevent = doc.docevent_set.filter(type='removed_related_ipr').first()
762805
self.assertIn(ipr.title, removed_docevent.desc,
763806
'IprDisclosure title does not appear in DocEvent desc when removed')
807+
808+
def test_no_revisions_message(self):
809+
draft = WgDraftFactory(rev="02")
810+
now = timezone.now()
811+
for rev in range(0,3):
812+
NewRevisionDocEventFactory(doc=draft, rev=f"{rev:02d}", time=now-datetime.timedelta(days=30*(2-rev)))
764813

814+
# Disclosure has non-empty revisions field on its related draft
815+
iprdocrel = IprDocRelFactory(document=draft.docalias.first())
816+
IprEventFactory(type_id="posted",time=now,disclosure=iprdocrel.disclosure)
817+
self.assertEqual(
818+
no_revisions_message(iprdocrel),
819+
""
820+
)
821+
822+
# Disclosure has more than one revision, none called out, disclosure after submissions
823+
iprdocrel = IprDocRelFactory(document=draft.docalias.first(), revisions="")
824+
IprEventFactory(type_id="posted",time=now,disclosure=iprdocrel.disclosure)
825+
self.assertEqual(
826+
no_revisions_message(iprdocrel),
827+
"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."
828+
)
829+
830+
# Disclosure has more than one revision, none called out, disclosure after 01
831+
iprdocrel = IprDocRelFactory(document=draft.docalias.first(), revisions="")
832+
e = IprEventFactory(type_id="posted",disclosure=iprdocrel.disclosure)
833+
e.time = now-datetime.timedelta(days=15)
834+
e.save()
835+
self.assertEqual(
836+
no_revisions_message(iprdocrel),
837+
"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."
838+
)
839+
840+
# Disclosure has more than one revision, none called out, disclosure was before the 00
841+
iprdocrel = IprDocRelFactory(document=draft.docalias.first(), revisions="")
842+
e = IprEventFactory(type_id="posted",disclosure=iprdocrel.disclosure)
843+
e.time = now-datetime.timedelta(days=180)
844+
e.save()
845+
self.assertEqual(
846+
no_revisions_message(iprdocrel),
847+
"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."
848+
)
849+
850+
# disclosed draft has no NewRevisionDocEvents
851+
draft = WgDraftFactory(rev="20")
852+
draft.docevent_set.all().delete()
853+
iprdocrel = IprDocRelFactory(document=draft.docalias.first(), revisions="")
854+
IprEventFactory(type_id="posted",disclosure=iprdocrel.disclosure)
855+
self.assertEqual(
856+
no_revisions_message(iprdocrel),
857+
"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."
858+
)
859+
860+
# disclosed draft has only one revision
861+
draft = WgDraftFactory(rev="00")
862+
iprdocrel = IprDocRelFactory(document=draft.docalias.first(), revisions="")
863+
IprEventFactory(type_id="posted",disclosure=iprdocrel.disclosure)
864+
self.assertEqual(
865+
no_revisions_message(iprdocrel),
866+
"No revisions for this Internet-Draft were specified in this disclosure. However, there is only one revision of this Internet-Draft."
867+
)

ietf/templates/ipr/details_view.html

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% extends "base.html" %}
2-
{# Copyright The IETF Trust 2015, 2017. All Rights Reserved. #}
2+
{# Copyright The IETF Trust 2015-2023. All Rights Reserved. #}
33
{% load origin %}
44
{% load ietf_filters ipr_filters textfilters %}
55
{% block title %}IPR Details - {{ ipr.title }}{% endblock %}
@@ -388,6 +388,13 @@ <h2 class="mt-5">
388388
<dd class="col-sm-8 my-0">
389389
{{ iprdocrel.revisions }}
390390
</dd>
391+
{% elif iprdocrel.doc_type == "Internet-Draft" %}
392+
<dt class="{% if prev %}col-sm-4{% else %}col-sm-3{% endif %} my-0 fw-bolder fst-italic">
393+
Notice:
394+
</dt>
395+
<dd class="{% if prev %}col-sm-8{% else %}col-sm-9{% endif %} my-0 fst-italic">
396+
{{ iprdocrel|no_revisions_message }}
397+
</dd>
391398
{% endif %}
392399
{% if iprdocrel.sections %}
393400
<dt class="col-sm-4 my-0">
@@ -427,6 +434,13 @@ <h2 class="mt-5">
427434
<dd class="{% if prev %}col-sm-8{% else %}col-sm-9{% endif %} my-0">
428435
{{ iprdocrel.revisions }}
429436
</dd>
437+
{% elif iprdocrel.doc_type == "Internet-Draft" %}
438+
<dt class="{% if prev %}col-sm-4{% else %}col-sm-3{% endif %} my-0 fw-bolder fst-italic">
439+
Notice:
440+
</dt>
441+
<dd class="{% if prev %}col-sm-8{% else %}col-sm-9{% endif %} my-0 fst-italic">
442+
{{ iprdocrel|no_revisions_message }}
443+
</dd>
430444
{% endif %}
431445
{% if iprdocrel.sections %}
432446
<dt class="{% if prev %}col-sm-4{% else %}col-sm-3{% endif %} my-0">

0 commit comments

Comments
 (0)