diff --git a/ietf/doc/migrations/0029_add_ipr_event_types.py b/ietf/doc/migrations/0029_add_ipr_event_types.py
new file mode 100644
index 000000000..3c29628e2
--- /dev/null
+++ b/ietf/doc/migrations/0029_add_ipr_event_types.py
@@ -0,0 +1,21 @@
+# Copyright The IETF Trust 2020, All Rights Reserved
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.27 on 2020-01-17 11:54
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('doc', '0028_irsgballotdocevent'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='docevent',
+ name='type',
+ field=models.CharField(choices=[('new_revision', 'Added new revision'), ('new_submission', 'Uploaded new revision'), ('changed_document', 'Changed document metadata'), ('added_comment', 'Added comment'), ('added_message', 'Added message'), ('edited_authors', 'Edited the documents author list'), ('deleted', 'Deleted document'), ('changed_state', 'Changed state'), ('changed_stream', 'Changed document stream'), ('expired_document', 'Expired document'), ('extended_expiry', 'Extended expiry of document'), ('requested_resurrect', 'Requested resurrect'), ('completed_resurrect', 'Completed resurrect'), ('changed_consensus', 'Changed consensus'), ('published_rfc', 'Published RFC'), ('added_suggested_replaces', 'Added suggested replacement relationships'), ('reviewed_suggested_replaces', 'Reviewed suggested replacement relationships'), ('changed_group', 'Changed group'), ('changed_protocol_writeup', 'Changed protocol writeup'), ('changed_charter_milestone', 'Changed charter milestone'), ('initial_review', 'Set initial review time'), ('changed_review_announcement', 'Changed WG Review text'), ('changed_action_announcement', 'Changed WG Action text'), ('started_iesg_process', 'Started IESG process on document'), ('created_ballot', 'Created ballot'), ('closed_ballot', 'Closed ballot'), ('sent_ballot_announcement', 'Sent ballot announcement'), ('changed_ballot_position', 'Changed ballot position'), ('changed_ballot_approval_text', 'Changed ballot approval text'), ('changed_ballot_writeup_text', 'Changed ballot writeup text'), ('changed_rfc_editor_note_text', 'Changed RFC Editor Note text'), ('changed_last_call_text', 'Changed last call text'), ('requested_last_call', 'Requested last call'), ('sent_last_call', 'Sent last call'), ('scheduled_for_telechat', 'Scheduled for telechat'), ('iesg_approved', 'IESG approved document (no problem)'), ('iesg_disapproved', 'IESG disapproved document (do not publish)'), ('approved_in_minute', 'Approved in minute'), ('iana_review', 'IANA review comment'), ('rfc_in_iana_registry', 'RFC is in IANA registry'), ('rfc_editor_received_announcement', 'Announcement was received by RFC Editor'), ('requested_publication', 'Publication at RFC Editor requested'), ('sync_from_rfc_editor', 'Received updated information from RFC Editor'), ('requested_review', 'Requested review'), ('assigned_review_request', 'Assigned review request'), ('closed_review_request', 'Closed review request'), ('closed_review_assignment', 'Closed review assignment'), ('downref_approved', 'Downref approved'), ('posted_related_ipr', 'Posted related IPR'), ('removed_related_ipr', 'Removed related IPR')], max_length=50),
+ ),
+ ]
diff --git a/ietf/doc/models.py b/ietf/doc/models.py
index dd7ed55fb..d3dfe8bb5 100644
--- a/ietf/doc/models.py
+++ b/ietf/doc/models.py
@@ -1031,6 +1031,10 @@ EVENT_TYPES = [
# downref
("downref_approved", "Downref approved"),
+
+ # IPR events
+ ("posted_related_ipr", "Posted related IPR"),
+ ("removed_related_ipr", "Removed related IPR"),
]
@python_2_unicode_compatible
diff --git a/ietf/ipr/migrations/0007_create_ipr_doc_events.py b/ietf/ipr/migrations/0007_create_ipr_doc_events.py
new file mode 100644
index 000000000..d5f17ab43
--- /dev/null
+++ b/ietf/ipr/migrations/0007_create_ipr_doc_events.py
@@ -0,0 +1,68 @@
+# Copyright The IETF Trust 2020, All Rights Reserved
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.27 on 2020-01-17 12:32
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+def create_or_delete_ipr_doc_events(apps, delete=False):
+ """Create or delete DocEvents for IprEvents
+
+ Mostly duplicates IprEvent.create_doc_events(). This is necessary
+ because model methods, including custom save() methods, are not
+ available to migrations.
+ """
+ IprEvent = apps.get_model('ipr', 'IprEvent')
+ DocEvent = apps.get_model('doc', 'DocEvent')
+
+ # Map from self.type_id to DocEvent.EVENT_TYPES for types that
+ # should be logged as DocEvents
+ event_type_map = {
+ 'posted': 'posted_related_ipr',
+ 'removed': 'removed_related_ipr',
+ }
+
+ for ipr_event in IprEvent.objects.filter(type_id__in=event_type_map):
+ related_docs = set() # related docs, no duplicates
+ for alias in ipr_event.disclosure.docs.all():
+ related_docs.update(alias.docs.all())
+ for doc in related_docs:
+ kwargs = dict(
+ type=event_type_map[ipr_event.type_id],
+ time=ipr_event.time,
+ by=ipr_event.by,
+ doc=doc,
+ rev='',
+ desc='%s related IPR disclosure: %s' % (ipr_event.type.name,
+ ipr_event.disclosure.title),
+ )
+ events = DocEvent.objects.filter(**kwargs) # get existing events
+ if delete:
+ events.delete()
+ elif len(events) == 0:
+ DocEvent.objects.create(**kwargs) # create if did not exist
+
+def forward(apps, schema_editor):
+ """Create a DocEvent for each 'posted' or 'removed' IprEvent"""
+ create_or_delete_ipr_doc_events(apps, delete=False)
+
+def reverse(apps, schema_editor):
+ """Delete DocEvents that would be created by the forward migration
+
+ This removes data, but only data that can be regenerated by running
+ the forward migration.
+ """
+ create_or_delete_ipr_doc_events(apps, delete=True)
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ipr', '0006_document_primary_key_cleanup'),
+ # Ensure the DocEvent types we need exist
+ ('doc', '0029_add_ipr_event_types'),
+ ]
+
+ operations = [
+ migrations.RunPython(forward, reverse),
+ ]
diff --git a/ietf/ipr/models.py b/ietf/ipr/models.py
index 6f14412fc..9e6c3bc26 100644
--- a/ietf/ipr/models.py
+++ b/ietf/ipr/models.py
@@ -1,4 +1,4 @@
-# Copyright The IETF Trust 2007-2019, All Rights Reserved
+# Copyright The IETF Trust 2007-2020, All Rights Reserved
# -*- coding: utf-8 -*-
@@ -11,7 +11,7 @@ from django.db import models
from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible
-from ietf.doc.models import DocAlias
+from ietf.doc.models import DocAlias, DocEvent
from ietf.name.models import DocRelationshipName,IprDisclosureStateName,IprLicenseTypeName,IprEventTypeName
from ietf.person.models import Person
from ietf.message.models import Message
@@ -214,6 +214,12 @@ class IprEvent(models.Model):
def __str__(self):
return "%s %s by %s at %s" % (self.disclosure.title, self.type.name.lower(), self.by.plain_name(), self.time)
+ def save(self, *args, **kwargs):
+ created = not self.pk
+ super(IprEvent, self).save(*args, **kwargs)
+ if created:
+ self.create_doc_events()
+
def response_past_due(self):
"""Returns true if it's beyond the response_due date and no response has been
received"""
@@ -223,6 +229,29 @@ class IprEvent(models.Model):
else:
return False
+ def create_doc_events(self):
+ """Create DocEvents for documents affected by an IprEvent"""
+ # Map from self.type_id to DocEvent.EVENT_TYPES for types that
+ # should be logged as DocEvents
+ event_type_map = {
+ 'posted': 'posted_related_ipr',
+ 'removed': 'removed_related_ipr',
+ }
+ if self.type_id in event_type_map:
+ related_docs = set() # related docs, no duplicates
+ for alias in self.disclosure.docs.all():
+ related_docs.update(alias.docs.all())
+ for doc in related_docs:
+ DocEvent.objects.create(
+ type=event_type_map[self.type_id],
+ time=self.time,
+ by=self.by,
+ doc=doc,
+ rev='',
+ desc='%s related IPR disclosure %s' % (self.type.name,
+ self.disclosure.title),
+ )
+
class Meta:
ordering = ['-time', '-id']
diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py
index e782ae5ef..ff9e98cbc 100644
--- a/ietf/ipr/tests.py
+++ b/ietf/ipr/tests.py
@@ -1,4 +1,4 @@
-# Copyright The IETF Trust 2009-2019, All Rights Reserved
+# Copyright The IETF Trust 2009-2020, All Rights Reserved
# -*- coding: utf-8 -*-
@@ -681,3 +681,33 @@ Subject: test
self.assertEqual(response.status_code,302)
disclosure = HolderIprDisclosure.objects.get(pk=disclosure.pk)
self.assertEqual(disclosure.compliant,False)
+
+ def test_docevent_creation(self):
+ """Test that IprEvent creation triggers DocEvent creation"""
+ doc = DocumentFactory()
+ ipr = HolderIprDisclosureFactory(docs=[doc])
+ # Document starts with no ipr-related events
+ self.assertEqual(0, doc.docevent_set.filter(type='posted_related_ipr').count(),
+ 'New Document already has a "posted_related_ipr" DocEvent')
+ self.assertEqual(0, doc.docevent_set.filter(type='removed_related_ipr').count(),
+ 'New Document already has a "removed_related_ipr" DocEvent')
+ # A 'posted' IprEvent must create a corresponding DocEvent
+ IprEventFactory(type_id='posted', disclosure=ipr)
+ self.assertEqual(1, doc.docevent_set.filter(type='posted_related_ipr').count(),
+ 'Creating "posted" IprEvent did not create a "posted_related_ipr" DocEvent')
+ self.assertEqual(0, doc.docevent_set.filter(type='removed_related_ipr').count(),
+ 'Creating "posted" IprEvent created a "removed_related_ipr" DocEvent')
+ # A 'removed' IprEvent must create a corresponding DocEvent
+ IprEventFactory(type_id='removed', disclosure=ipr)
+ self.assertEqual(1, doc.docevent_set.filter(type='posted_related_ipr').count(),
+ 'Creating "removed" IprEvent created a "posted_related_ipr" DocEvent')
+ self.assertEqual(1, doc.docevent_set.filter(type='removed_related_ipr').count(),
+ 'Creating "removed" IprEvent did not create a "removed_related_ipr" DocEvent')
+ # The DocEvent descriptions must refer to the IprEvents
+ posted_docevent = doc.docevent_set.filter(type='posted_related_ipr').first()
+ self.assertIn(ipr.title, posted_docevent.desc,
+ 'IprDisclosure title does not appear in DocEvent desc when posted')
+ 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')
+
\ No newline at end of file