From 5a1f3eaf36178f9fdb52b3a64d125b9acb7fc965 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz Date: Wed, 10 Jan 2018 14:22:10 +0000 Subject: [PATCH 1/4] Changed the patent information text fields to individual fields for patent number, inventor, title, date and notes, with validation. Fixes issue #2411. - Legacy-Id: 14499 --- ietf/ipr/forms.py | 84 +++++++++++++++++++++++++--- ietf/ipr/tests.py | 28 +++++++--- ietf/templates/ipr/details_edit.html | 16 +++++- 3 files changed, 111 insertions(+), 17 deletions(-) diff --git a/ietf/ipr/forms.py b/ietf/ipr/forms.py index cff81c7e5..8f2f69c1c 100644 --- a/ietf/ipr/forms.py +++ b/ietf/ipr/forms.py @@ -1,8 +1,12 @@ import datetime import email -from django.utils.safestring import mark_safe + from django import forms +from django.core.validators import RegexValidator +from django.utils.safestring import mark_safe + +import debug # pyflakes:ignore from ietf.group.models import Group from ietf.doc.fields import SearchableDocAliasField @@ -101,6 +105,31 @@ class DraftForm(forms.ModelForm): } help_texts = { 'sections': 'Sections' } +validate_patent_number = RegexValidator( + regex="^(([A-Z][A-Z]\d{6,12}|[A-Z][A-Z]\d{4}(\w{1,2}\d{5,7})?)[, ]*)+$", + message="Please enter one or more patent publication or application numbers as country code and serial number, e.g.: WO2017123456." ) + +def validate_string(s, letter_min, digit_min, space_min, message): + letter_count = 0 + space_count = 0 + digit_count = 0 + s = s.strip() + for c in s: + if c.isalpha(): + letter_count += 1 + if c.isspace(): + space_count += 1 + if not (letter_count >= letter_min and digit_count >= digit_min and space_count >= space_min): + raise forms.ValidationError(message) + +def validate_name(name): + return validate_string(name, letter_min=3, space_min=1, digit_min=0, + message="This doesn't look like a name. Please enter the actual inventor name.") + +def validate_title(title): + return validate_string(title, letter_min=15, space_min=2, digit_min=0, + message="This doesn't look like a patent title. Please enter the actual patent title.") + class GenericDisclosureForm(forms.Form): """Custom ModelForm-like form to use for new Generic or NonDocSpecific Iprs. If patent_info is submitted create a NonDocSpecificIprDisclosure object @@ -114,7 +143,14 @@ class GenericDisclosureForm(forms.Form): holder_contact_info = forms.CharField(label="Other Info (address, phone, etc.)", max_length=255,widget=forms.Textarea,required=False, strip=False) submitter_name = forms.CharField(max_length=255,required=False) submitter_email = forms.EmailField(required=False) - 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) + #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) + patent_number = forms.CharField(max_length=127, required=False, validators=[ validate_patent_number ], + help_text = "Patent publication or application number (2-letter country code followed by serial number)") + patent_inventor = forms.CharField(max_length=63, required=False, validators=[ validate_name ], help_text="Inventor name") + patent_title = forms.CharField(max_length=63, required=False, validators=[ validate_title ], help_text="Title of invention") + patent_date = forms.DateField(required=False, help_text="Date granted or applied for") + patent_notes = forms.CharField(max_length=127, required=False, widget=forms.Textarea) + has_patent_pending = forms.BooleanField(required=False) statement = forms.CharField(max_length=255,widget=forms.Textarea,required=False, strip=False) updates = SearchableIprDisclosuresField(required=False, help_text="If this disclosure updates 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.") @@ -132,7 +168,20 @@ class GenericDisclosureForm(forms.Form): if not self.cleaned_data.get('same_as_ii_above'): if not ( self.cleaned_data.get('submitter_name') and self.cleaned_data.get('submitter_email') ): raise forms.ValidationError('Submitter information must be provided in section VII') - + + patent_fields = [ 'patent_'+k for k in ['number', 'inventor', 'title', 'date', ] ] + patent_values = [ cleaned_data.get(k) for k in patent_fields ] + if any(patent_values) and not all(patent_values): + for k in patent_fields: + if not cleaned_data.get(k): + self.add_error(k, "This field is required if you are filing a patent-specific disclosure.") + raise forms.ValidationError("A generic IPR disclosure cannot have any patent-specific information, " + "but a patent-specific disclosure must provide full patent information.") + + patent_values = [str(v) for v in patent_values if v ] + [ cleaned_data['patent_notes'] ] + cleaned_data['patent_info'] = ('\n'.join(patent_values)).strip() + cleaned_data['patent_fields'] = patent_fields + return cleaned_data def save(self, *args, **kwargs): @@ -140,6 +189,9 @@ class GenericDisclosureForm(forms.Form): same_as_ii_above = nargs.get('same_as_ii_above') del nargs['same_as_ii_above'] + for k in self.cleaned_data['patent_fields'] + ['patent_fields', 'patent_notes']: + del nargs[k] + if self.cleaned_data.get('patent_info'): obj = NonDocSpecificIprDisclosure(**nargs) else: @@ -160,6 +212,12 @@ class IprDisclosureFormBase(forms.ModelForm): """Base form for Holder and ThirdParty disclosures""" updates = SearchableIprDisclosuresField(required=False, help_text=mark_safe("If this disclosure updates 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.")) same_as_ii_above = forms.BooleanField(required=False) + patent_number = forms.CharField(max_length=127, required=True, validators=[ validate_patent_number ], + help_text = "Patent publication or application number (2-letter country code followed by serial number)") + patent_inventor = forms.CharField(max_length=63, required=True, validators=[ validate_name ], help_text="Inventor name") + patent_title = forms.CharField(max_length=63, required=True, validators=[ validate_title ], help_text="Title of invention") + patent_date = forms.DateField(required=True, help_text="Date granted or applied for") + patent_notes = forms.CharField(max_length=127, required=False, widget=forms.Textarea) def __init__(self,*args,**kwargs): super(IprDisclosureFormBase, self).__init__(*args,**kwargs) @@ -167,6 +225,7 @@ class IprDisclosureFormBase(forms.ModelForm): self.fields['submitter_email'].required = False self.fields['compliant'].initial = True self.fields['compliant'].label = "This disclosure complies with RFC 3979" + patent_fields = [ 'patent_'+k for k in ['number', 'inventor', 'title', 'date', ] ] if "ietfer_name" in self.fields: self.fields["ietfer_name"].label = "Name" if "ietfer_contact_email" in self.fields: @@ -175,7 +234,10 @@ class IprDisclosureFormBase(forms.ModelForm): self.fields["ietfer_contact_info"].label = "Other info" self.fields["ietfer_contact_info"].help_text = "Address, phone, etc." if "patent_info" in self.fields: - 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" + self.fields['patent_info'].required = False + else: + for f in patent_fields: + del self.fields[f] if "licensing" in self.fields: self.fields["licensing_comments"].label = "Licensing information, comments, notes, or URL for further information" if "submitter_claims_all_terms_disclosed" in self.fields: @@ -187,7 +249,7 @@ class IprDisclosureFormBase(forms.ModelForm): """This will be overridden""" model = IprDisclosureBase fields = '__all__' - + def clean(self): super(IprDisclosureFormBase, self).clean() cleaned_data = self.cleaned_data @@ -198,6 +260,12 @@ class IprDisclosureFormBase(forms.ModelForm): if not ( self.cleaned_data.get('submitter_name') and self.cleaned_data.get('submitter_email') ): raise forms.ValidationError('Submitter information must be provided in section VII') + patent_fields = [ 'patent_'+k for k in ['number', 'inventor', 'title', 'date', 'notes'] ] + patent_values = [ cleaned_data.get(k) for k in patent_fields ] + patent_values = [ str(v) for v in patent_values if v ] + cleaned_data['patent_info'] = ('\n'.join(patent_values)).strip() + cleaned_data['patent_fields'] = patent_fields + return cleaned_data class HolderIprDisclosureForm(IprDisclosureFormBase): @@ -220,8 +288,7 @@ class HolderIprDisclosureForm(IprDisclosureFormBase): self.fields['licensing'].queryset = IprLicenseTypeName.objects.exclude(slug='none-selected') def clean(self): - super(HolderIprDisclosureForm, self).clean() - cleaned_data = self.cleaned_data + cleaned_data = super(HolderIprDisclosureForm, self).clean() if not self.data.get('iprdocrel_set-0-document') and not cleaned_data.get('other_designations'): raise forms.ValidationError('You need to specify a contribution in Section IV') return cleaned_data @@ -270,8 +337,7 @@ class ThirdPartyIprDisclosureForm(IprDisclosureFormBase): exclude = [ 'by','docs','state','rel' ] def clean(self): - super(ThirdPartyIprDisclosureForm, self).clean() - cleaned_data = self.cleaned_data + cleaned_data = super(ThirdPartyIprDisclosureForm, self).clean() if not self.data.get('iprdocrel_set-0-document') and not cleaned_data.get('other_designations'): raise forms.ValidationError('You need to specify a contribution in Section III') return cleaned_data diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index 03596953a..aaea72449 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -292,7 +292,7 @@ class IprTests(TestCase): ipr = iprs[0] self.assertEqual(ipr.holder_legal_name, "Test Legal") self.assertEqual(ipr.state.slug, 'pending') - self.assertTrue(isinstance(ipr.get_child(),GenericIprDisclosure)) + self.assertTrue(isinstance(ipr.get_child(), GenericIprDisclosure)) def test_new_specific(self): """Add a new specific disclosure. Note: submitter does not need to be logged in. @@ -314,14 +314,16 @@ class IprTests(TestCase): "iprdocrel_set-0-document": "%s" % draft.docalias_set.first().pk, "iprdocrel_set-0-revisions": '00', "iprdocrel_set-1-document": DocAlias.objects.filter(name__startswith="rfc").first().pk, - "patent_info": "none", + "patent_number": "SE12345678901", + "patent_inventor": "A. Nonymous", + "patent_title": "A method of transfering bits", + "patent_date": "2000-01-01", "has_patent_pending": False, "licensing": "royalty-free", "submitter_name": "Test Holder", "submitter_email": "test@holder.com", }) self.assertEqual(r.status_code, 200) - # print r.content self.assertTrue("Your IPR disclosure has been submitted" in unicontent(r)) iprs = IprDisclosureBase.objects.filter(title__icontains=draft.name) @@ -329,6 +331,8 @@ class IprTests(TestCase): ipr = iprs[0] self.assertEqual(ipr.holder_legal_name, "Test Legal") self.assertEqual(ipr.state.slug, 'pending') + for item in [u'SE12345678901','A method of transfering bits','2000-01-01']: + self.assertIn(item, ipr.get_child().patent_info) self.assertTrue(isinstance(ipr.get_child(),HolderIprDisclosure)) self.assertEqual(len(outbox),1) self.assertTrue('New IPR Submission' in outbox[0]['Subject']) @@ -352,7 +356,10 @@ class IprTests(TestCase): "iprdocrel_set-0-document": "%s" % draft.docalias_set.first().pk, "iprdocrel_set-0-revisions": '00', "iprdocrel_set-1-document": DocAlias.objects.filter(name__startswith="rfc").first().pk, - "patent_info": "none", + "patent_number": "SE12345678901", + "patent_inventor": "A. Nonymous", + "patent_title": "A method of transfering bits", + "patent_date": "2000-01-01", "has_patent_pending": False, "licensing": "royalty-free", "submitter_name": "Test Holder", @@ -366,6 +373,8 @@ class IprTests(TestCase): ipr = iprs[0] self.assertEqual(ipr.holder_legal_name, "Test Legal") self.assertEqual(ipr.state.slug, "pending") + for item in [u'SE12345678901','A method of transfering bits','2000-01-01' ]: + self.assertIn(item, ipr.get_child().patent_info) self.assertTrue(isinstance(ipr.get_child(),ThirdPartyIprDisclosure)) self.assertEqual(len(outbox),1) self.assertTrue('New IPR Submission' in outbox[0]['Subject']) @@ -391,7 +400,10 @@ class IprTests(TestCase): "iprdocrel_set-0-document": "%s" % draft.docalias_set.first().pk, "iprdocrel_set-0-revisions": '00', "iprdocrel_set-1-document": DocAlias.objects.filter(name__startswith="rfc").first().pk, - "patent_info": "none", + "patent_number": "SE12345678901", + "patent_inventor": "A. Nonymous", + "patent_title": "A method of transfering bits", + "patent_date": "2000-01-01", "has_patent_pending": False, "licensing": "royalty-free", "submitter_name": "Test Holder", @@ -415,7 +427,6 @@ class IprTests(TestCase): draft = make_test_data() url = urlreverse("ietf.ipr.views.new", kwargs={ "type": "specific" }) - # successful post empty_outbox() r = self.client.post(url, { "updates": "this is supposed to be an integer", @@ -426,7 +437,10 @@ class IprTests(TestCase): "iprdocrel_set-INITIAL_FORMS": 0, "iprdocrel_set-0-document": "%s" % draft.docalias_set.first().pk, "iprdocrel_set-0-revisions": '00', - "patent_info": "none", + "patent_number": "SE12345678901", + "patent_inventor": "A. Nonymous", + "patent_title": "A method of transfering bits", + "patent_date": "2000-01-01", "has_patent_pending": False, "licensing": "royalty-free", "submitter_name": "Test Holder", diff --git a/ietf/templates/ipr/details_edit.html b/ietf/templates/ipr/details_edit.html index 2511ef764..f2b3f65f9 100644 --- a/ietf/templates/ipr/details_edit.html +++ b/ietf/templates/ipr/details_edit.html @@ -162,7 +162,21 @@

{% cycle section %}. Disclosure of Patent Information{% if form.instance|to_class_name == "ThirdPartyIprDicslosure" %}, if known{% endif %} i.e., patents or patent applications required to be disclosed by Section 5 of RFC8179

- {% if form.patent_info %} + {% if form.patent_number %} +

+ A. For granted patents or published pending patent applications, + please provide the following information: +

+ {% bootstrap_field form.patent_number layout='horizontal' %} + {% bootstrap_field form.patent_inventor layout='horizontal' %} + {% bootstrap_field form.patent_title layout='horizontal' %} + {% bootstrap_field form.patent_date layout='horizontal' %} + {% bootstrap_field form.patent_notes layout='horizontal' %} + +

B. Does your disclosure relate to an unpublished pending patent application?

+ {% bootstrap_field form.has_patent_pending layout='horizontal' %} + + {% elif form.patent_info %}

A. For granted patents or published pending patent applications, please provide the following information: From d7e1d258e543c3e94b30c2ce4bb5f1b9bafe76e2 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz Date: Thu, 11 Jan 2018 18:03:29 +0000 Subject: [PATCH 2/4] Added a new section for IPR disclosures on related documents to the IPR document search result page. Fixes issue #2412. - Legacy-Id: 14514 --- ietf/ipr/utils.py | 8 +++-- ietf/ipr/views.py | 18 ++++++----- ietf/templates/ipr/search_doc_result.html | 37 +++++++++++++++++++++-- 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/ietf/ipr/utils.py b/ietf/ipr/utils.py index 016445733..531fcaa65 100644 --- a/ietf/ipr/utils.py +++ b/ietf/ipr/utils.py @@ -29,11 +29,11 @@ def iprs_from_docs(aliases,**kwargs): iprdocrels += alias.document.ipr(**kwargs) return list(set([i.disclosure for i in iprdocrels])) -def related_docs(alias): +def related_docs(alias, relationship=['replaces', 'obs']): """Returns list of related documents""" results = list(alias.document.docalias_set.all()) - rels = alias.document.all_relations_that_doc(['replaces','obs']) + rels = alias.document.all_relations_that_doc(relationship) for rel in rels: rel_aliases = list(rel.target.document.docalias_set.all()) @@ -42,4 +42,6 @@ def related_docs(alias): x.related = rel x.relation = rel.relationship.revname results += rel_aliases - return list(set(results)) \ No newline at end of file + return list(set(results)) + + diff --git a/ietf/ipr/views.py b/ietf/ipr/views.py index 53d1d8540..8ae158045 100644 --- a/ietf/ipr/views.py +++ b/ietf/ipr/views.py @@ -619,7 +619,8 @@ def search(request): docid = request.GET.get("id") or request.GET.get("id_document_tag") or "" docs = doc = None iprs = [] - + related_iprs = [] + # set states states = request.GET.getlist('state',('posted','removed')) if states == ['all']: @@ -647,10 +648,12 @@ def search(request): # one match if len(start) == 1: first = start[0] - doc = str(first) + doc = first.document docs = related_docs(first) iprs = iprs_from_docs(docs,states=states) template = "ipr/search_doc_result.html" + updated_docs = related_docs(first, ['updates',]) + related_iprs = list(set(iprs_from_docs(updated_docs, states=states)) - set(iprs)) # multiple matches, select just one elif start: docs = start @@ -723,11 +726,12 @@ def search(request): return render(request, template, { "q": q, - "iprs": iprs, - "docs": docs, - "doc": doc, - "form":form, - "states":states + "iprs": iprs, + "docs": docs, + "doc": doc, + "form": form, + "states": states, + "related_iprs": related_iprs, }) return HttpResponseRedirect(request.path) diff --git a/ietf/templates/ipr/search_doc_result.html b/ietf/templates/ipr/search_doc_result.html index 09d35b91a..7cb69afcd 100644 --- a/ietf/templates/ipr/search_doc_result.html +++ b/ietf/templates/ipr/search_doc_result.html @@ -8,7 +8,7 @@ {% endblock %} -{% block search_header %}Draft search results{% endblock %} +{% block search_header %}Document IPR search results for {{doc}}{% endblock %} {% block search_result %}

Total number of IPR disclosures found: {{ iprs|length }}.

@@ -35,6 +35,12 @@ {% endfor %} + {% if related_iprs %} +

+ IPR declarations exist for related documents, see below. + These cannot be assumed to apply to the current document without closer inspection. +

+ {% endif %} {% endif %}

Total number of documents searched: {{ docs|length}}.

@@ -85,8 +91,35 @@ {% endfor %} + {% if related_iprs %} + + + + + + + + + + + + + {% for ipr in related_iprs %} + + + + + + {% endfor %} + +
DateIDStatement
{{ ipr.time|date:"Y-m-d" }}{{ ipr.id }}{{ ipr.title }} + {% if ipr.updates %}
(Updates ID#: {% for upd in ipr.updates %}{{upd.target_id}}{% if not forloop.last %}, {% endif %}{% endfor %}){% endif %} +
+ {% endif %} + + {% endblock %} {% block js %} -{% endblock %} \ No newline at end of file +{% endblock %} From 717868cae2fc70fa559b071b7cce7cc9387b3c35 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz Date: Wed, 17 Jan 2018 00:21:34 +0000 Subject: [PATCH 3/4] Rewrote text_to_dict() and dict_to_text() to support unicode without RFC2822 encoding issues. Added initial values in IPR update forms, from the original disclosure, in order to make updates easier. Addresses issue #2413. - Legacy-Id: 14531 --- ietf/ipr/forms.py | 14 ++++++++------ ietf/ipr/tests.py | 9 +++++++-- ietf/ipr/views.py | 20 +++++++++++++++----- ietf/utils/test_data.py | 2 +- ietf/utils/text.py | 35 ++++++++++++++++++++++++++++++++++- 5 files changed, 65 insertions(+), 15 deletions(-) diff --git a/ietf/ipr/forms.py b/ietf/ipr/forms.py index 8f2f69c1c..cfd73ec0a 100644 --- a/ietf/ipr/forms.py +++ b/ietf/ipr/forms.py @@ -17,6 +17,7 @@ from ietf.ipr.models import (IprDocRel, IprDisclosureBase, HolderIprDisclosure, IprLicenseTypeName, IprDisclosureStateName) from ietf.message.models import Message from ietf.utils.fields import DatepickerDateField +from ietf.utils.text import dict_to_text # ---------------------------------------------------------------- # Globals @@ -178,8 +179,9 @@ class GenericDisclosureForm(forms.Form): raise forms.ValidationError("A generic IPR disclosure cannot have any patent-specific information, " "but a patent-specific disclosure must provide full patent information.") - patent_values = [str(v) for v in patent_values if v ] + [ cleaned_data['patent_notes'] ] - cleaned_data['patent_info'] = ('\n'.join(patent_values)).strip() + patent_fields += ['patent_notes'] + patent_info = dict([ (k.replace('patent_','').capitalize(), cleaned_data.get(k)) for k in patent_fields if cleaned_data.get(k) ] ) + cleaned_data['patent_info'] = dict_to_text(patent_info).strip() cleaned_data['patent_fields'] = patent_fields return cleaned_data @@ -189,7 +191,7 @@ class GenericDisclosureForm(forms.Form): same_as_ii_above = nargs.get('same_as_ii_above') del nargs['same_as_ii_above'] - for k in self.cleaned_data['patent_fields'] + ['patent_fields', 'patent_notes']: + for k in self.cleaned_data['patent_fields'] + ['patent_fields',]: del nargs[k] if self.cleaned_data.get('patent_info'): @@ -261,9 +263,9 @@ class IprDisclosureFormBase(forms.ModelForm): raise forms.ValidationError('Submitter information must be provided in section VII') patent_fields = [ 'patent_'+k for k in ['number', 'inventor', 'title', 'date', 'notes'] ] - patent_values = [ cleaned_data.get(k) for k in patent_fields ] - patent_values = [ str(v) for v in patent_values if v ] - cleaned_data['patent_info'] = ('\n'.join(patent_values)).strip() + + patent_info = dict([ (k.replace('patent_','').capitalize(), cleaned_data.get(k)) for k in patent_fields if cleaned_data.get(k) ] ) + cleaned_data['patent_info'] = dict_to_text(patent_info).strip() cleaned_data['patent_fields'] = patent_fields return cleaned_data diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index aaea72449..0f8004cfc 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -39,7 +39,7 @@ class IprTests(TestCase): title="Statement regarding rights Update", holder_legal_name="Native Martians United", state_id='pending', - patent_info='US12345', + patent_info='Number: US12345\nTitle: A method of transfering bits\nInventor: A. Nonymous\nDate: 2000-01-01', holder_contact_name='Update Holder', holder_contact_email='update_holder@acme.com', licensing_id='royalty-free', @@ -383,8 +383,13 @@ class IprTests(TestCase): def test_update(self): draft = make_test_data() original_ipr = IprDisclosureBase.objects.get(title='Statement regarding rights') - url = urlreverse("ietf.ipr.views.new", kwargs={ "type": "specific" }) + # get + url = urlreverse("ietf.ipr.views.update", kwargs={ "id": original_ipr.id }) + r = self.client.get(url) + self.assertContains(r, "Statement regarding rights") + + #url = urlreverse("ietf.ipr.views.new", kwargs={ "type": "specific" }) # successful post empty_outbox() r = self.client.post(url, { diff --git a/ietf/ipr/views.py b/ietf/ipr/views.py index 8ae158045..526536d80 100644 --- a/ietf/ipr/views.py +++ b/ietf/ipr/views.py @@ -7,7 +7,7 @@ from django.conf import settings from django.contrib import messages from django.urls import reverse as urlreverse from django.db.models import Q -from django.forms.models import inlineformset_factory +from django.forms.models import inlineformset_factory, model_to_dict from django.forms.formsets import formset_factory from django.http import HttpResponse, Http404, HttpResponseRedirect from django.shortcuts import render, get_object_or_404, redirect @@ -30,6 +30,7 @@ from ietf.ipr.models import (IprDisclosureStateName, IprDisclosureBase, RelatedIpr,IprEvent) from ietf.ipr.utils import (get_genitive, get_ipr_summary, iprs_from_docs, related_docs) +from ietf.mailtrigger.utils import gather_address_lists from ietf.message.models import Message from ietf.message.utils import infer_message from ietf.name.models import IprLicenseTypeName @@ -37,7 +38,7 @@ from ietf.person.models import Person from ietf.secr.utils.document import get_rfc_num, is_draft from ietf.utils.draft_search import normalize_draftname from ietf.utils.mail import send_mail, send_mail_message -from ietf.mailtrigger.utils import gather_address_lists +from ietf.utils.text import text_to_dict # ---------------------------------------------------------------- # Globals @@ -531,7 +532,16 @@ def new(request, type, updates=None): else: if updates: - form = ipr_form_mapping[type](initial={'updates':str(updates)}) + original = IprDisclosureBase(id=updates).get_child() + initial = model_to_dict(original) + initial.update({'updates':str(updates), }) + patent_info = text_to_dict(initial['patent_info']) + if patent_info.keys(): + patent_dict = dict([ ('patent_'+k.lower(), v) for k,v in patent_info.items() ]) + else: + patent_dict = {'patent_notes': initial['patent_info']} + initial.update(patent_dict) + form = ipr_form_mapping[type](initial=initial) else: form = ipr_form_mapping[type]() disclosure = IprDisclosureBase() # dummy disclosure for inlineformset @@ -764,8 +774,8 @@ def show(request, id): 'in_force_ipr_rfc': ipr_rfc_number(ipr.time, ipr.is_thirdparty), 'tabs': get_details_tabs(ipr, 'Disclosure'), 'choices_abc': [ i.desc for i in IprLicenseTypeName.objects.filter(slug__in=['no-license', 'royalty-free', 'reasonable', ]) ], - 'updates_iprs': ipr.relatedipr_source_set.all(), - 'updated_by_iprs': ipr.relatedipr_target_set.filter(source__state="posted") + 'updates_iprs': ipr.relatedipr_source_set.all().order_by('source__time'), + 'updated_by_iprs': ipr.relatedipr_target_set.filter(source__state="posted").order_by('target__time') }) def showlist(request): diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index fd36cb044..2d912602d 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -331,7 +331,7 @@ def make_test_data(): title="Statement regarding rights", holder_legal_name="Native Martians United", state=IprDisclosureStateName.objects.get(slug='posted'), - patent_info='US12345', + patent_info='Number: US12345\nTitle: A method of transfering bits\nInventor: A. Nonymous\nDate: 2000-01-01', holder_contact_name='George', holder_contact_email='george@acme.com', holder_contact_info='14 Main Street\nEarth', diff --git a/ietf/utils/text.py b/ietf/utils/text.py index edbdc37c5..c93bb55de 100644 --- a/ietf/utils/text.py +++ b/ietf/utils/text.py @@ -140,4 +140,37 @@ def decode(raw): text = raw.decode('latin-1') # return text - + +def text_to_dict(t): + "Converts text with RFC2822-formatted header fields into a dictionary-like object." + # ensure we're handed a unicode parameter + assert isinstance(t, six.text_type) + d = {} + # Return {} for malformed input + if not len(t.lstrip()) == len(t): + return {} + lines = t.splitlines() + items = [] + # unfold folded lines + for l in lines: + if l[0].isspace(): + if items: + items[-1] += l + else: + return {} + else: + items.append(l) + for i in items: + if re.match('^[A-Za-z0-9-]+: ', i): + k, v = i.split(': ', 1) + d[k] = v + else: + return {} + return d + +def dict_to_text(d): + "Convert a dictionary to RFC2822-formatted text" + t = "" + for k, v in d.items(): + t += "%s: %s\n" % (k, v) + return t From 46472401ad490daf6f35f4cb24203c15aaaa77f9 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz Date: Sat, 27 Jan 2018 21:34:52 +0000 Subject: [PATCH 4/4] Changed the IPR disclosure page for IPR disclosure updates to show both the previous and current disclosure details side-by-side. Fixes issue #2414. - Legacy-Id: 14581 --- ietf/ipr/views.py | 7 +- ietf/static/ietf/css/ietf.css | 17 ++ ietf/templates/ipr/details_view.html | 283 +++++++++++++++++++-------- 3 files changed, 221 insertions(+), 86 deletions(-) diff --git a/ietf/ipr/views.py b/ietf/ipr/views.py index 526536d80..5fb61770a 100644 --- a/ietf/ipr/views.py +++ b/ietf/ipr/views.py @@ -769,12 +769,17 @@ def show(request, id): elif ipr.state.slug != 'posted': raise Http404 + updates_iprs = ipr.relatedipr_source_set.all().order_by('source__time') + prev_rel = updates_iprs.last() + prev = prev_rel.target.get_child() if prev_rel else None + return render(request, "ipr/details_view.html", { 'ipr': ipr, + 'prev': prev, 'in_force_ipr_rfc': ipr_rfc_number(ipr.time, ipr.is_thirdparty), 'tabs': get_details_tabs(ipr, 'Disclosure'), 'choices_abc': [ i.desc for i in IprLicenseTypeName.objects.filter(slug__in=['no-license', 'royalty-free', 'reasonable', ]) ], - 'updates_iprs': ipr.relatedipr_source_set.all().order_by('source__time'), + 'updates_iprs': updates_iprs, 'updated_by_iprs': ipr.relatedipr_target_set.filter(source__state="posted").order_by('target__time') }) diff --git a/ietf/static/ietf/css/ietf.css b/ietf/static/ietf/css/ietf.css index 1d9903eb9..b0e9ba386 100644 --- a/ietf/static/ietf/css/ietf.css +++ b/ietf/static/ietf/css/ietf.css @@ -753,6 +753,23 @@ blockquote { } +.table-condensed th.ipr-label { + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + padding-right: 1em; + vertical-align: top; +} +.table-condensed .ipr-prev { + color: #777; + background-color: #f0f0f0; + vertical-align: top; +} +.table-condensed .ipr-this { + vertical-align: top; +} + .rfcmarkup div { margin-top: 1em; } diff --git a/ietf/templates/ipr/details_view.html b/ietf/templates/ipr/details_view.html index a41ec7526..89d8d1cfa 100644 --- a/ietf/templates/ipr/details_view.html +++ b/ietf/templates/ipr/details_view.html @@ -73,20 +73,27 @@ of the original IPR disclosure.

{% endif %} - {% if updated_by_iprs %} -

Updated by

- - {% for item in updated_by_iprs %} -

- This IPR disclosure has been updated by IPR disclosure ID #{{ item.source.id }}, - "{{ item.source.title }}". -

- {% endfor %} - {% endif %} - - {% if updates_iprs %} + {% if updates_iprs or updated_by_iprs%}

Updates

+ {% if updated_by_iprs %} +
+
Updated by
+
+ {% for item in updated_by_iprs %} +
+ IPR Disclosure ID #{{ item.source.id }}, + {% if item.source.state.slug == "removed" %} + "{{ item.source.title }}" (which was removed at the request of the submitter) + {% else %} + "{{ item.source.title }}" + {% endif %} +
+ {% endfor %} +
+ {% endif %} + + {% if updates_iprs %}
Updates
@@ -101,6 +108,7 @@ {% endfor %}
+ {% endif %} {% endif %} {% if user|has_role:"Secretariat" and ipr.update_notified_date %} @@ -113,65 +121,128 @@ {% if ipr|to_class_name == "ThirdPartyIprDisclosure" %}Possible{% endif %} Patent Holder/Applicant ("Patent Holder") -
-
Holder legal name
-
{{ ipr.holder_legal_name }}
-
+ + {% if prev %} + + {% endif %} + + {% if prev %}{% endif %} + + + +
Previous (#{{prev.id}})This (#{{ipr.id}})
{{ prev.holder_legal_name }}Holder legal name{{ ipr.holder_legal_name }}
{% if ipr.holder_contact_name or ipr.holder_contact_info %} -

{% cycle section %}. Patent Holder's Contact for License Application

+

{% cycle section %}. Patent Holder's Contact for Licence Application

-
-
Holder contact name
-
{{ ipr.holder_contact_name }}
+ + {% if prev %} + + {% endif %} -
Holder contact email
-
{{ ipr.holder_contact_email }}
+ + {% if prev %}{% endif %} + + + -
Holder contact info
-
{{ ipr.holder_contact_info|linebreaks }}
- - {% endif %} + + {% if prev %}{% endif %} + + + + + + {% if prev %}{% endif %} + + + + +
Previous (#{{prev.id}})This (#{{ipr.id}})
{{ prev.holder_contact_name }}Holder contact name{{ ipr.holder_contact_name }}
{{ prev.holder_contact_email }}Holder contact email{{ ipr.holder_contact_email }}
{{ prev.holder_contact_info|linebreaks }}Holder contact info{{ ipr.holder_contact_info|linebreaks }}
+ {% endif %} {% if ipr.ietfer_name or ipr.ietfer_contact_email or ipr.ietfer_contact_info %}

{% cycle section %}. Contact Information for the IETF Participant Whose Personal Belief Triggered this Disclosure

-
-
Name
-
{{ ipr.ietfer_name }}
+ + {% if prev %} + + {% endif %} + + {% if prev %}{% endif %} + + + -
Email
-
{{ ipr.ietfer_contact_email }}
+ + {% if prev %}{% endif %} + + + -
Other info
-
{{ ipr.ietfer_contact_info|linebreaks }}
- + + {% if prev %}{% endif %} + + + + +
Previous (#{{prev.id}})This (#{{ipr.id}})
{{ prev.ietfer_name }}Name{{ ipr.ietfer_name }}
{{ prev.ietfer_contact_email }}Email{{ ipr.ietfer_contact_email }}
{{ prev.ietfer_contact_info|linebreaks }}Other info{{ ipr.ietfer_contact_info|linebreaks }}
{% endif %} {% if ipr.iprdocrel_set.all or ipr.other_designations %}

{% cycle section %}. IETF Document or Other Contribution to Which this IPR Disclosure Relates

-
- {% for iprdocrel in ipr.iprdocrel_set.all %} -
{{ iprdocrel.doc_type }}
-
-
{{ iprdocrel.formatted_name }} ("{{ iprdocrel.document.document.title }}")
+ + {% if prev %} + + {% endif %} + - {% if iprdocrel.revisions %} -
Revisions: {{ iprdocrel.revisions }}
- {% endif %} + {% if prev %} + + {% endif %} + + + + + +
Previous (#{{prev.id}})This (#{{ipr.id}})
+ {% for iprdocrel in prev.iprdocrel_set.all %} + {{ iprdocrel.doc_type }}: + {{ iprdocrel.formatted_name }} ("{{ iprdocrel.document.document.title }}") - {% if iprdocrel.sections %} -
Sections: {{ iprdocrel.sections }}
- {% endif %} - - {% endfor %} + {% if iprdocrel.revisions %} +
Revisions: {{ iprdocrel.revisions }}
+ {% endif %} - {% if ipr.other_designations %} -
Designations for Other Contributions
-
{{ ipr.other_designations }}
- {% endif %} - + {% if iprdocrel.sections %} +
Sections: {{ iprdocrel.sections }}
+ {% endif %} + {% endfor %} + + {% if prev.other_designations %} + Designations for Other Contributions + {{ prev.other_designations }} + {% endif %} +
+ {% for iprdocrel in ipr.iprdocrel_set.all %} + {{ iprdocrel.doc_type }}: + {{ iprdocrel.formatted_name }} ("{{ iprdocrel.document.document.title }}") + + {% if iprdocrel.revisions %} +
Revisions: {{ iprdocrel.revisions }}
+ {% endif %} + + {% if iprdocrel.sections %} +
Sections: {{ iprdocrel.sections }}
+ {% endif %} + {% endfor %} + + {% if ipr.other_designations %} +
Designations for Other Contributions
+
{{ ipr.other_designations }}
+ {% endif %} +
{% endif %} {% if ipr.patent_info or ipr.has_patent_pending %} @@ -179,17 +250,29 @@

A. For granted patents or published pending patent applications, please provide the following information:

-
-
Patent, Serial, Publication, Registration, or Application/File number(s)
-
{{ ipr.patent_info|linebreaks }}
-
- + + {% if prev %} + + {% endif %} + + {% if prev %}{% endif %} + + + +
Previous (#{{prev.id}})This (#{{ipr.id}})
{{ prev.patent_info|linebreaks }}Patent, Serial, Publication, Registration, or Application/File number(s){{ ipr.patent_info|linebreaks }}
+

B. Does this disclosure relate to an unpublished pending patent application?:

-
-
Has patent pending
-
{{ ipr.has_patent_pending|yesno:"Yes,No" }}
-
+ + {% if prev %} + + {% endif %} + + {% if prev %}{% endif %} + + + +
Previous (#{{prev.id}})This (#{{ipr.id}})
{{ prev.has_patent_pending|yesno:"Yes,No" }}Has patent pending{{ ipr.has_patent_pending|yesno:"Yes,No" }}
{% endif %} {% if ipr.licensing %} @@ -204,23 +287,32 @@ specification, is as follows(select one licensing declaration option only):

- {% if ipr.licensing.slug == "provided-later" %} + {% if prev.licensing.slug == "provided-later" or ipr.licensing.slug == "provided-later" %}
- Possible licencing choices a), b), and c) when Licencing Declaration to be Provided Later: + Possible licensing choices a), b), and c) when Licensing Declaration to be Provided Later:
    {% for desc in choices_abc %}
  • {{ desc}}
  • {% endfor %}
-

+
{% endif %} -
-
Licensing
-
{% if ipr.licensing.slug == "provided-later" %}{{ ipr.licensing.desc|slice:"2:"|slice:":117" }}){% else %}{{ ipr.licensing.desc|slice:"2:" }}{% endif %}
-
Licensing information, comments, notes, or URL for further information
-
{{ ipr.licensing_comments|default:"(No information submitted)"|linebreaks }}
-
+ + {% if prev %} + + {% endif %} + + {% if prev %}{% endif %} + + + + + {% if prev %}{% endif %} + + + +
Previous (#{{prev.id}})This (#{{ipr.id}})
{% if prev.licensing.slug == "provided-later" %}{{ prev.licensing.desc|slice:"2:"|slice:":117" }}){% else %}{{ prev.licensing.desc|slice:"2:" }}{% endif %}Licensing{% if ipr.licensing.slug == "provided-later" %}{{ ipr.licensing.desc|slice:"2:"|slice:":117" }}){% else %}{{ ipr.licensing.desc|slice:"2:" }}{% endif %}
{{ prev.licensing_comments|default:"(No information submitted)"|linebreaks }}Licensing information, comments, notes, or URL for further information{{ ipr.licensing_comments|default:"(No information submitted)"|linebreaks }}

Note: The individual submitting this template represents and warrants that he or she is authorized by the Patent Holder to agree to the @@ -229,33 +321,54 @@ {% elif ipr.statement %}

{% cycle section %}. Statement

-
-
Statement
-
{{ ipr.statement|linebreaks }}
-
+ + {% if prev %} + + {% endif %} + + {% if prev %}{% endif %} + + + +
Previous (#{{prev.id}})This (#{{ipr.id}})
{{ prev.statement|linebreaks }}Statement{{ ipr.statement|linebreaks }}
{% endif %}

{% cycle section %}. Contact Information of Submitter of this Form

-
-
Submitter name
-
{{ ipr.submitter_name }}
+ + {% if prev %} + + {% endif %} + + {% if prev %}{% endif %} + + + -
Submitter email
-
{{ ipr.submitter_email }}
- + + {% if prev %}{% endif %} + + + +
Previous (#{{prev.id}})This (#{{ipr.id}})
{{ prev.submitter_name }}Submitter name{{ ipr.submitter_name }}
{{ prev.submitter_email }}Submitter email{{ ipr.submitter_email }}
{% if ipr.notes %}

{% cycle section %}. Other Notes

-
-
Additional notes
-
{{ ipr.notes|linebreaks }}
-
+ + {% if prev %} + + {% endif %} + + {% if prev %}{% endif %} + + + +
Previous (#{{prev.id}})This (#{{ipr.id}})
{{ prev.notes|linebreaks }}Additional notes{{ ipr.notes|linebreaks }}
+ {% endif %}

Only those sections of the relevant entry form where the submitter provided information are displayed.

- {% endblock content %}