Skip to content

Commit a4cc4bb

Browse files
committed
Merged in ^/personal/henrik/6.68.4-ipr@14609 from henrik@levkowetz.com:
Changes to the IPR disclosure pages, requested during WG Chairs Lunch where the changes to IPR handling introduced by RFC8179 were presented. - Legacy-Id: 14610
2 parents 945ba1f + 4647240 commit a4cc4bb

11 files changed

Lines changed: 477 additions & 132 deletions

File tree

changelog

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,30 @@
1+
ietfdb (6.72.0) ietf; urgency=medium
2+
3+
**Updated IPR disclosure pages**
4+
5+
This release introduces changes to the IPR disclosure pages, requested
6+
during WG Chairs Lunch where the changes to IPR handling introduced by
7+
RFC8179 were presented. From the commit log:
8+
9+
* Changed the IPR disclosure page for IPR disclosure updates to show both
10+
the previous and current disclosure details side-by-side. Fixes issue
11+
#2414.
12+
13+
* Rewrote text_to_dict() and dict_to_text() to support unicode without
14+
RFC2822 encoding issues. Added initial values in IPR update forms, from
15+
the original disclosure, in order to make updates easier. Addresses issue
16+
#2413.
17+
18+
* Added a new section for IPR disclosures on related documents to the IPR
19+
document search result page. Fixes issue #2412.
20+
21+
* Changed the patent information text fields to individual fields for
22+
patent number, inventor, title, date and notes, with validation. Fixes
23+
issue #2411.
24+
25+
-- Henrik Levkowetz <henrik@levkowetz.com> 01 Feb 2018 07:10:21 -0800
26+
27+
128
ietfdb (6.71.1) ietf; urgency=low
229

330
This is a small bugfix release related to the new non-wg mailing list index

ietf/ipr/forms.py

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import datetime
22
import email
33

4-
from django.utils.safestring import mark_safe
4+
55
from django import forms
6+
from django.core.validators import RegexValidator
7+
from django.utils.safestring import mark_safe
8+
9+
import debug # pyflakes:ignore
610

711
from ietf.group.models import Group
812
from ietf.doc.fields import SearchableDocAliasField
@@ -13,6 +17,7 @@
1317
IprLicenseTypeName, IprDisclosureStateName)
1418
from ietf.message.models import Message
1519
from ietf.utils.fields import DatepickerDateField
20+
from ietf.utils.text import dict_to_text
1621

1722
# ----------------------------------------------------------------
1823
# Globals
@@ -101,6 +106,31 @@ class Meta:
101106
}
102107
help_texts = { 'sections': 'Sections' }
103108

109+
validate_patent_number = RegexValidator(
110+
regex="^(([A-Z][A-Z]\d{6,12}|[A-Z][A-Z]\d{4}(\w{1,2}\d{5,7})?)[, ]*)+$",
111+
message="Please enter one or more patent publication or application numbers as country code and serial number, e.g.: WO2017123456." )
112+
113+
def validate_string(s, letter_min, digit_min, space_min, message):
114+
letter_count = 0
115+
space_count = 0
116+
digit_count = 0
117+
s = s.strip()
118+
for c in s:
119+
if c.isalpha():
120+
letter_count += 1
121+
if c.isspace():
122+
space_count += 1
123+
if not (letter_count >= letter_min and digit_count >= digit_min and space_count >= space_min):
124+
raise forms.ValidationError(message)
125+
126+
def validate_name(name):
127+
return validate_string(name, letter_min=3, space_min=1, digit_min=0,
128+
message="This doesn't look like a name. Please enter the actual inventor name.")
129+
130+
def validate_title(title):
131+
return validate_string(title, letter_min=15, space_min=2, digit_min=0,
132+
message="This doesn't look like a patent title. Please enter the actual patent title.")
133+
104134
class GenericDisclosureForm(forms.Form):
105135
"""Custom ModelForm-like form to use for new Generic or NonDocSpecific Iprs.
106136
If patent_info is submitted create a NonDocSpecificIprDisclosure object
@@ -114,7 +144,14 @@ class GenericDisclosureForm(forms.Form):
114144
holder_contact_info = forms.CharField(label="Other Info (address, phone, etc.)", max_length=255,widget=forms.Textarea,required=False, strip=False)
115145
submitter_name = forms.CharField(max_length=255,required=False)
116146
submitter_email = forms.EmailField(required=False)
117-
patent_info = forms.CharField(max_length=255,widget=forms.Textarea, required=False, help_text="Patent, Serial, Publication, Registration, or Application/File number(s), Date(s) granted or applied for, Country, and any additional notes.", strip=False)
147+
#patent_info = forms.CharField(max_length=255,widget=forms.Textarea, required=False, help_text="Patent, Serial, Publication, Registration, or Application/File number(s), Date(s) granted or applied for, Country, and any additional notes.", strip=False)
148+
patent_number = forms.CharField(max_length=127, required=False, validators=[ validate_patent_number ],
149+
help_text = "Patent publication or application number (2-letter country code followed by serial number)")
150+
patent_inventor = forms.CharField(max_length=63, required=False, validators=[ validate_name ], help_text="Inventor name")
151+
patent_title = forms.CharField(max_length=63, required=False, validators=[ validate_title ], help_text="Title of invention")
152+
patent_date = forms.DateField(required=False, help_text="Date granted or applied for")
153+
patent_notes = forms.CharField(max_length=127, required=False, widget=forms.Textarea)
154+
118155
has_patent_pending = forms.BooleanField(required=False)
119156
statement = forms.CharField(max_length=255,widget=forms.Textarea,required=False, strip=False)
120157
updates = SearchableIprDisclosuresField(required=False, help_text="If this disclosure <strong>updates</strong> other disclosures identify here which ones. Leave this field blank if this disclosure does not update any prior disclosures. <strong>Note</strong>: Updates to IPR disclosures must only be made by authorized representatives of the original submitters. Updates will automatically be forwarded to the current Patent Holder's Contact and to the Submitter of the original IPR disclosure.")
@@ -132,14 +169,31 @@ def clean(self):
132169
if not self.cleaned_data.get('same_as_ii_above'):
133170
if not ( self.cleaned_data.get('submitter_name') and self.cleaned_data.get('submitter_email') ):
134171
raise forms.ValidationError('Submitter information must be provided in section VII')
135-
172+
173+
patent_fields = [ 'patent_'+k for k in ['number', 'inventor', 'title', 'date', ] ]
174+
patent_values = [ cleaned_data.get(k) for k in patent_fields ]
175+
if any(patent_values) and not all(patent_values):
176+
for k in patent_fields:
177+
if not cleaned_data.get(k):
178+
self.add_error(k, "This field is required if you are filing a patent-specific disclosure.")
179+
raise forms.ValidationError("A generic IPR disclosure cannot have any patent-specific information, "
180+
"but a patent-specific disclosure must provide full patent information.")
181+
182+
patent_fields += ['patent_notes']
183+
patent_info = dict([ (k.replace('patent_','').capitalize(), cleaned_data.get(k)) for k in patent_fields if cleaned_data.get(k) ] )
184+
cleaned_data['patent_info'] = dict_to_text(patent_info).strip()
185+
cleaned_data['patent_fields'] = patent_fields
186+
136187
return cleaned_data
137188

138189
def save(self, *args, **kwargs):
139190
nargs = self.cleaned_data.copy()
140191
same_as_ii_above = nargs.get('same_as_ii_above')
141192
del nargs['same_as_ii_above']
142193

194+
for k in self.cleaned_data['patent_fields'] + ['patent_fields',]:
195+
del nargs[k]
196+
143197
if self.cleaned_data.get('patent_info'):
144198
obj = NonDocSpecificIprDisclosure(**nargs)
145199
else:
@@ -160,13 +214,20 @@ class IprDisclosureFormBase(forms.ModelForm):
160214
"""Base form for Holder and ThirdParty disclosures"""
161215
updates = SearchableIprDisclosuresField(required=False, help_text=mark_safe("If this disclosure <strong>updates</strong> other disclosures identify here which ones. Leave this field blank if this disclosure does not update any prior disclosures. Note: Updates to IPR disclosures must only be made by authorized representatives of the original submitters. Updates will automatically be forwarded to the current Patent Holder's Contact and to the Submitter of the original IPR disclosure."))
162216
same_as_ii_above = forms.BooleanField(required=False)
217+
patent_number = forms.CharField(max_length=127, required=True, validators=[ validate_patent_number ],
218+
help_text = "Patent publication or application number (2-letter country code followed by serial number)")
219+
patent_inventor = forms.CharField(max_length=63, required=True, validators=[ validate_name ], help_text="Inventor name")
220+
patent_title = forms.CharField(max_length=63, required=True, validators=[ validate_title ], help_text="Title of invention")
221+
patent_date = forms.DateField(required=True, help_text="Date granted or applied for")
222+
patent_notes = forms.CharField(max_length=127, required=False, widget=forms.Textarea)
163223

164224
def __init__(self,*args,**kwargs):
165225
super(IprDisclosureFormBase, self).__init__(*args,**kwargs)
166226
self.fields['submitter_name'].required = False
167227
self.fields['submitter_email'].required = False
168228
self.fields['compliant'].initial = True
169229
self.fields['compliant'].label = "This disclosure complies with RFC 3979"
230+
patent_fields = [ 'patent_'+k for k in ['number', 'inventor', 'title', 'date', ] ]
170231
if "ietfer_name" in self.fields:
171232
self.fields["ietfer_name"].label = "Name"
172233
if "ietfer_contact_email" in self.fields:
@@ -175,7 +236,10 @@ def __init__(self,*args,**kwargs):
175236
self.fields["ietfer_contact_info"].label = "Other info"
176237
self.fields["ietfer_contact_info"].help_text = "Address, phone, etc."
177238
if "patent_info" in self.fields:
178-
self.fields["patent_info"].help_text = "Patent, Serial, Publication, Registration, or Application/File number(s), Date(s) granted or applied for, Country, and any additional notes"
239+
self.fields['patent_info'].required = False
240+
else:
241+
for f in patent_fields:
242+
del self.fields[f]
179243
if "licensing" in self.fields:
180244
self.fields["licensing_comments"].label = "Licensing information, comments, notes, or URL for further information"
181245
if "submitter_claims_all_terms_disclosed" in self.fields:
@@ -187,7 +251,7 @@ class Meta:
187251
"""This will be overridden"""
188252
model = IprDisclosureBase
189253
fields = '__all__'
190-
254+
191255
def clean(self):
192256
super(IprDisclosureFormBase, self).clean()
193257
cleaned_data = self.cleaned_data
@@ -198,6 +262,12 @@ def clean(self):
198262
if not ( self.cleaned_data.get('submitter_name') and self.cleaned_data.get('submitter_email') ):
199263
raise forms.ValidationError('Submitter information must be provided in section VII')
200264

265+
patent_fields = [ 'patent_'+k for k in ['number', 'inventor', 'title', 'date', 'notes'] ]
266+
267+
patent_info = dict([ (k.replace('patent_','').capitalize(), cleaned_data.get(k)) for k in patent_fields if cleaned_data.get(k) ] )
268+
cleaned_data['patent_info'] = dict_to_text(patent_info).strip()
269+
cleaned_data['patent_fields'] = patent_fields
270+
201271
return cleaned_data
202272

203273
class HolderIprDisclosureForm(IprDisclosureFormBase):
@@ -220,8 +290,7 @@ def __init__(self, *args, **kwargs):
220290
self.fields['licensing'].queryset = IprLicenseTypeName.objects.exclude(slug='none-selected')
221291

222292
def clean(self):
223-
super(HolderIprDisclosureForm, self).clean()
224-
cleaned_data = self.cleaned_data
293+
cleaned_data = super(HolderIprDisclosureForm, self).clean()
225294
if not self.data.get('iprdocrel_set-0-document') and not cleaned_data.get('other_designations'):
226295
raise forms.ValidationError('You need to specify a contribution in Section IV')
227296
return cleaned_data
@@ -270,8 +339,7 @@ class Meta:
270339
exclude = [ 'by','docs','state','rel' ]
271340

272341
def clean(self):
273-
super(ThirdPartyIprDisclosureForm, self).clean()
274-
cleaned_data = self.cleaned_data
342+
cleaned_data = super(ThirdPartyIprDisclosureForm, self).clean()
275343
if not self.data.get('iprdocrel_set-0-document') and not cleaned_data.get('other_designations'):
276344
raise forms.ValidationError('You need to specify a contribution in Section III')
277345
return cleaned_data

ietf/ipr/tests.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def test_get_holders(self):
3939
title="Statement regarding rights Update",
4040
holder_legal_name="Native Martians United",
4141
state_id='pending',
42-
patent_info='US12345',
42+
patent_info='Number: US12345\nTitle: A method of transfering bits\nInventor: A. Nonymous\nDate: 2000-01-01',
4343
holder_contact_name='Update Holder',
4444
holder_contact_email='update_holder@acme.com',
4545
licensing_id='royalty-free',
@@ -292,7 +292,7 @@ def test_new_generic(self):
292292
ipr = iprs[0]
293293
self.assertEqual(ipr.holder_legal_name, "Test Legal")
294294
self.assertEqual(ipr.state.slug, 'pending')
295-
self.assertTrue(isinstance(ipr.get_child(),GenericIprDisclosure))
295+
self.assertTrue(isinstance(ipr.get_child(), GenericIprDisclosure))
296296

297297
def test_new_specific(self):
298298
"""Add a new specific disclosure. Note: submitter does not need to be logged in.
@@ -314,21 +314,25 @@ def test_new_specific(self):
314314
"iprdocrel_set-0-document": "%s" % draft.docalias_set.first().pk,
315315
"iprdocrel_set-0-revisions": '00',
316316
"iprdocrel_set-1-document": DocAlias.objects.filter(name__startswith="rfc").first().pk,
317-
"patent_info": "none",
317+
"patent_number": "SE12345678901",
318+
"patent_inventor": "A. Nonymous",
319+
"patent_title": "A method of transfering bits",
320+
"patent_date": "2000-01-01",
318321
"has_patent_pending": False,
319322
"licensing": "royalty-free",
320323
"submitter_name": "Test Holder",
321324
"submitter_email": "test@holder.com",
322325
})
323326
self.assertEqual(r.status_code, 200)
324-
# print r.content
325327
self.assertTrue("Your IPR disclosure has been submitted" in unicontent(r))
326328

327329
iprs = IprDisclosureBase.objects.filter(title__icontains=draft.name)
328330
self.assertEqual(len(iprs), 1)
329331
ipr = iprs[0]
330332
self.assertEqual(ipr.holder_legal_name, "Test Legal")
331333
self.assertEqual(ipr.state.slug, 'pending')
334+
for item in [u'SE12345678901','A method of transfering bits','2000-01-01']:
335+
self.assertIn(item, ipr.get_child().patent_info)
332336
self.assertTrue(isinstance(ipr.get_child(),HolderIprDisclosure))
333337
self.assertEqual(len(outbox),1)
334338
self.assertTrue('New IPR Submission' in outbox[0]['Subject'])
@@ -352,7 +356,10 @@ def test_new_thirdparty(self):
352356
"iprdocrel_set-0-document": "%s" % draft.docalias_set.first().pk,
353357
"iprdocrel_set-0-revisions": '00',
354358
"iprdocrel_set-1-document": DocAlias.objects.filter(name__startswith="rfc").first().pk,
355-
"patent_info": "none",
359+
"patent_number": "SE12345678901",
360+
"patent_inventor": "A. Nonymous",
361+
"patent_title": "A method of transfering bits",
362+
"patent_date": "2000-01-01",
356363
"has_patent_pending": False,
357364
"licensing": "royalty-free",
358365
"submitter_name": "Test Holder",
@@ -366,6 +373,8 @@ def test_new_thirdparty(self):
366373
ipr = iprs[0]
367374
self.assertEqual(ipr.holder_legal_name, "Test Legal")
368375
self.assertEqual(ipr.state.slug, "pending")
376+
for item in [u'SE12345678901','A method of transfering bits','2000-01-01' ]:
377+
self.assertIn(item, ipr.get_child().patent_info)
369378
self.assertTrue(isinstance(ipr.get_child(),ThirdPartyIprDisclosure))
370379
self.assertEqual(len(outbox),1)
371380
self.assertTrue('New IPR Submission' in outbox[0]['Subject'])
@@ -374,8 +383,13 @@ def test_new_thirdparty(self):
374383
def test_update(self):
375384
draft = make_test_data()
376385
original_ipr = IprDisclosureBase.objects.get(title='Statement regarding rights')
377-
url = urlreverse("ietf.ipr.views.new", kwargs={ "type": "specific" })
378386

387+
# get
388+
url = urlreverse("ietf.ipr.views.update", kwargs={ "id": original_ipr.id })
389+
r = self.client.get(url)
390+
self.assertContains(r, "Statement regarding rights")
391+
392+
#url = urlreverse("ietf.ipr.views.new", kwargs={ "type": "specific" })
379393
# successful post
380394
empty_outbox()
381395
r = self.client.post(url, {
@@ -391,7 +405,10 @@ def test_update(self):
391405
"iprdocrel_set-0-document": "%s" % draft.docalias_set.first().pk,
392406
"iprdocrel_set-0-revisions": '00',
393407
"iprdocrel_set-1-document": DocAlias.objects.filter(name__startswith="rfc").first().pk,
394-
"patent_info": "none",
408+
"patent_number": "SE12345678901",
409+
"patent_inventor": "A. Nonymous",
410+
"patent_title": "A method of transfering bits",
411+
"patent_date": "2000-01-01",
395412
"has_patent_pending": False,
396413
"licensing": "royalty-free",
397414
"submitter_name": "Test Holder",
@@ -415,7 +432,6 @@ def test_update_bad_post(self):
415432
draft = make_test_data()
416433
url = urlreverse("ietf.ipr.views.new", kwargs={ "type": "specific" })
417434

418-
# successful post
419435
empty_outbox()
420436
r = self.client.post(url, {
421437
"updates": "this is supposed to be an integer",
@@ -426,7 +442,10 @@ def test_update_bad_post(self):
426442
"iprdocrel_set-INITIAL_FORMS": 0,
427443
"iprdocrel_set-0-document": "%s" % draft.docalias_set.first().pk,
428444
"iprdocrel_set-0-revisions": '00',
429-
"patent_info": "none",
445+
"patent_number": "SE12345678901",
446+
"patent_inventor": "A. Nonymous",
447+
"patent_title": "A method of transfering bits",
448+
"patent_date": "2000-01-01",
430449
"has_patent_pending": False,
431450
"licensing": "royalty-free",
432451
"submitter_name": "Test Holder",

ietf/ipr/utils.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ def iprs_from_docs(aliases,**kwargs):
2929
iprdocrels += alias.document.ipr(**kwargs)
3030
return list(set([i.disclosure for i in iprdocrels]))
3131

32-
def related_docs(alias):
32+
def related_docs(alias, relationship=['replaces', 'obs']):
3333
"""Returns list of related documents"""
3434
results = list(alias.document.docalias_set.all())
3535

36-
rels = alias.document.all_relations_that_doc(['replaces','obs'])
36+
rels = alias.document.all_relations_that_doc(relationship)
3737

3838
for rel in rels:
3939
rel_aliases = list(rel.target.document.docalias_set.all())
@@ -42,4 +42,6 @@ def related_docs(alias):
4242
x.related = rel
4343
x.relation = rel.relationship.revname
4444
results += rel_aliases
45-
return list(set(results))
45+
return list(set(results))
46+
47+

0 commit comments

Comments
 (0)