feat: Capture that IPR disclosures are removed under the Objectively False IPR Disclosure Policy (#6231)

* feat: Capture that IPR disclosures are removed under the Objectively False IPR Disclosure Policy (#6088)

* chore: Move PUBLISH_IPR_STATES from settings_local to settings

* fix: Add migration for removed_objfalse in IprEventTypeName

* fix: De-conflict migration

* fix: De-conflict migration

* style: Move PUBLISH_IPR_STATES ahead of not-production block
This commit is contained in:
Paul Selkirk 2023-09-11 14:14:46 -04:00 committed by GitHub
parent 6b6c881353
commit febdeff85f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 186 additions and 20 deletions

View file

@ -0,0 +1,90 @@
# Generated by Django 4.2.4 on 2023-08-23 21:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("doc", "0006_statements"),
]
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_action_holders", "Changed action holders for document"),
("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"),
(
"removed_objfalse_related_ipr",
"Removed Objectively False related IPR",
),
("changed_editors", "Changed BOF Request editors"),
("published_statement", "Published statement"),
],
max_length=50,
),
),
]

View file

@ -963,7 +963,7 @@ class Document(DocumentInfo):
def displayname_with_link(self):
return mark_safe('<a href="%s">%s-%s</a>' % (self.get_absolute_url(), self.name , self.rev))
def ipr(self,states=('posted','removed')):
def ipr(self,states=settings.PUBLISH_IPR_STATES):
"""Returns the IPR disclosures against this document (as a queryset over IprDocRel)."""
from ietf.ipr.models import IprDocRel
return IprDocRel.objects.filter(document__docs=self, disclosure__state__in=states)
@ -973,7 +973,7 @@ class Document(DocumentInfo):
document directly or indirectly obsoletes or replaces
"""
from ietf.ipr.models import IprDocRel
iprs = IprDocRel.objects.filter(document__in=list(self.docalias.all())+self.all_related_that_doc(('obs','replaces'))).filter(disclosure__state__in=('posted','removed')).values_list('disclosure', flat=True).distinct()
iprs = IprDocRel.objects.filter(document__in=list(self.docalias.all())+self.all_related_that_doc(('obs','replaces'))).filter(disclosure__state__in=settings.PUBLISH_IPR_STATES).values_list('disclosure', flat=True).distinct()
return iprs
def future_presentations(self):
@ -1288,6 +1288,7 @@ EVENT_TYPES = [
# IPR events
("posted_related_ipr", "Posted related IPR"),
("removed_related_ipr", "Removed related IPR"),
("removed_objfalse_related_ipr", "Removed Objectively False related IPR"),
# Bofreq Editor events
("changed_editors", "Changed BOF Request editors"),

View file

@ -1,7 +1,8 @@
# Copyright The IETF Trust 2007-2020, All Rights Reserved
# Copyright The IETF Trust 2007-2023, All Rights Reserved
# -*- coding: utf-8 -*-
from django.conf import settings
from django.contrib.syndication.views import Feed
from django.utils.feedgenerator import Atom1Feed
from django.urls import reverse_lazy
@ -19,7 +20,7 @@ class LatestIprDisclosuresFeed(Feed):
feed_url = "/feed/ipr/"
def items(self):
return IprDisclosureBase.objects.filter(state__in=('posted','removed')).order_by('-time')[:30]
return IprDisclosureBase.objects.filter(state__in=settings.PUBLISH_IPR_STATES).order_by('-time')[:30]
def item_title(self, item):
return mark_safe(item.title)

View file

@ -1,4 +1,4 @@
# Copyright The IETF Trust 2007-2020, All Rights Reserved
# Copyright The IETF Trust 2007-2023, All Rights Reserved
# -*- coding: utf-8 -*-
@ -231,6 +231,7 @@ class IprEvent(models.Model):
event_type_map = {
'posted': 'posted_related_ipr',
'removed': 'removed_related_ipr',
'removed_objfalse': 'removed_objfalse_related_ipr',
}
if self.type_id in event_type_map:
related_docs = set() # related docs, no duplicates

View file

@ -1,11 +1,12 @@
# Copyright The IETF Trust 2007-2019, All Rights Reserved
# Copyright The IETF Trust 2007-2023, All Rights Reserved
#
from django.conf import settings
from django.contrib.sitemaps import GenericSitemap
from ietf.ipr.models import IprDisclosureBase
# changefreq is "never except when it gets updated or withdrawn"
# so skip giving one
queryset = IprDisclosureBase.objects.filter(state__in=('posted','removed'))
queryset = IprDisclosureBase.objects.filter(state__in=settings.PUBLISH_IPR_STATES)
archive = {'queryset':queryset, 'date_field': 'time', 'allow_empty':True }
IPRMap = GenericSitemap(archive) # type: ignore

View file

@ -126,6 +126,11 @@ class IprTests(TestCase):
r = self.client.get(urlreverse("ietf.ipr.views.show", kwargs=dict(id=ipr.pk)))
self.assertContains(r, 'This IPR disclosure was removed')
def test_show_removed_objfalse(self):
ipr = HolderIprDisclosureFactory(state_id='removed_objfalse')
r = self.client.get(urlreverse("ietf.ipr.views.show", kwargs=dict(id=ipr.pk)))
self.assertContains(r, 'This IPR disclosure was removed as objectively false')
def test_ipr_history(self):
ipr = HolderIprDisclosureFactory()
r = self.client.get(urlreverse("ietf.ipr.views.history", kwargs=dict(id=ipr.pk)))
@ -576,7 +581,7 @@ I would like to revoke this declaration.
self.client.login(username="secretary", password="secretary+password")
# test for presence of pending ipr
num = IprDisclosureBase.objects.filter(state__in=('removed','rejected')).count()
num = IprDisclosureBase.objects.filter(state__in=('removed','removed_objfalse','rejected')).count()
r = self.client.get(url)
self.assertEqual(r.status_code,200)
@ -785,18 +790,28 @@ Subject: test
'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')
self.assertEqual(0, doc.docevent_set.filter(type='removed_objfalse_related_ipr').count(),
'New Document already has a "removed_objfalse_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')
self.assertEqual(0, doc.docevent_set.filter(type='removed_objfalse_related_ipr').count(),
'Creating "posted" IprEvent created a "removed_objfalse_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')
# A 'removed_objfalse' IprEvent must create a corresponding DocEvent
IprEventFactory(type_id='removed_objfalse', disclosure=ipr)
self.assertEqual(1, doc.docevent_set.filter(type='posted_related_ipr').count(),
'Creating "removed_objfalse" IprEvent created a "posted_related_ipr" DocEvent')
self.assertEqual(1, doc.docevent_set.filter(type='removed_objfalse_related_ipr').count(),
'Creating "removed_objfalse" IprEvent did not create a "removed_objfalse_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,
@ -804,6 +819,9 @@ Subject: test
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')
removed_objfalse_docevent = doc.docevent_set.filter(type='removed_objfalse_related_ipr').first()
self.assertIn(ipr.title, removed_objfalse_docevent.desc,
'IprDisclosure title does not appear in DocEvent desc when removed as objectively false')
def test_no_revisions_message(self):
draft = WgDraftFactory(rev="02")

View file

@ -1,4 +1,4 @@
# Copyright The IETF Trust 2007-2022, All Rights Reserved
# Copyright The IETF Trust 2007-2023, All Rights Reserved
# -*- coding: utf-8 -*-
@ -269,7 +269,7 @@ def add_email(request, id):
@role_required('Secretariat',)
def admin(request, state):
"""Administrative disclosure listing. For non-posted disclosures"""
states = IprDisclosureStateName.objects.filter(slug__in=[state, "rejected"] if state == "removed" else [state])
states = IprDisclosureStateName.objects.filter(slug__in=[state, "rejected", "removed_objfalse"] if state == "removed" else [state])
if not states:
raise Http404
@ -648,7 +648,7 @@ def search(request):
related_iprs = []
# set states
states = request.GET.getlist('state',('posted','removed'))
states = request.GET.getlist('state',settings.PUBLISH_IPR_STATES)
if states == ['all']:
states = IprDisclosureStateName.objects.values_list('slug',flat=True)
@ -778,7 +778,7 @@ def show(request, id):
"""View of individual declaration"""
ipr = get_object_or_404(IprDisclosureBase, id=id).get_child()
if not has_role(request.user, 'Secretariat'):
if ipr.state.slug == 'removed':
if ipr.state.slug in ['removed', 'removed_objfalse']:
return render(request, "ipr/removed.html", {
'ipr': ipr
})
@ -801,10 +801,10 @@ def show(request, id):
def showlist(request):
"""List all disclosures by type, posted only"""
generic = GenericIprDisclosure.objects.filter(state__in=('posted','removed')).prefetch_related('relatedipr_source_set__target','relatedipr_target_set__source').order_by('-time')
specific = HolderIprDisclosure.objects.filter(state__in=('posted','removed')).prefetch_related('relatedipr_source_set__target','relatedipr_target_set__source').order_by('-time')
thirdpty = ThirdPartyIprDisclosure.objects.filter(state__in=('posted','removed')).prefetch_related('relatedipr_source_set__target','relatedipr_target_set__source').order_by('-time')
nondocspecific = NonDocSpecificIprDisclosure.objects.filter(state__in=('posted','removed')).prefetch_related('relatedipr_source_set__target','relatedipr_target_set__source').order_by('-time')
generic = GenericIprDisclosure.objects.filter(state__in=settings.PUBLISH_IPR_STATES).prefetch_related('relatedipr_source_set__target','relatedipr_target_set__source').order_by('-time')
specific = HolderIprDisclosure.objects.filter(state__in=settings.PUBLISH_IPR_STATES).prefetch_related('relatedipr_source_set__target','relatedipr_target_set__source').order_by('-time')
thirdpty = ThirdPartyIprDisclosure.objects.filter(state__in=settings.PUBLISH_IPR_STATES).prefetch_related('relatedipr_source_set__target','relatedipr_target_set__source').order_by('-time')
nondocspecific = NonDocSpecificIprDisclosure.objects.filter(state__in=settings.PUBLISH_IPR_STATES).prefetch_related('relatedipr_source_set__target','relatedipr_target_set__source').order_by('-time')
# combine nondocspecific with generic and re-sort
generic = itertools.chain(generic,nondocspecific)

View file

@ -12097,6 +12097,16 @@
"model": "name.iprdisclosurestatename",
"pk": "removed"
},
{
"fields": {
"desc": "",
"name": "Removed Objectively False",
"order": 5,
"used": true
},
"model": "name.iprdisclosurestatename",
"pk": "removed_objfalse"
},
{
"fields": {
"desc": "",
@ -12207,6 +12217,16 @@
"model": "name.ipreventtypename",
"pk": "removed"
},
{
"fields": {
"desc": "",
"name": "Removed Objectively False",
"order": 0,
"used": true
},
"model": "name.ipreventtypename",
"pk": "removed_objfalse"
},
{
"fields": {
"desc": "",

View file

@ -0,0 +1,24 @@
# Copyright The IETF Trust 2023, All Rights Reserved
from django.db import migrations
def forward(apps, schema_editor):
IprDisclosureStateName = apps.get_model("name", "IprDisclosureStateName")
IprDisclosureStateName.objects.create(slug="removed_objfalse", name="Removed Objectively False", order=5)
IprEventTypeName = apps.get_model("name", "IprEventTypeName")
IprEventTypeName.objects.create(slug="removed_objfalse", name="Removed Objectively False")
def reverse(apps, schema_editor):
IprDisclosureStateName = apps.get_model("name", "IprDisclosureStateName")
IprDisclosureStateName.objects.filter(slug="removed_objfalse").delete()
IprEventTypeName = apps.get_model("name", "IprEventTypeName")
IprEventTypeName.objects.filter(slug="removed_objfalse").delete()
class Migration(migrations.Migration):
dependencies = [
("name", "0007_appeal_artifact_typename"),
]
operations = [
migrations.RunPython(forward, reverse),
]

View file

@ -1270,6 +1270,8 @@ if 'CACHES' not in locals():
},
}
PUBLISH_IPR_STATES = ['posted', 'removed', 'removed_objfalse']
# We provide a secret key only for test and development modes. It's
# absolutely vital that django fails to start in production mode unless a
# secret key has been provided elsewhere, not in this file which is

View file

@ -103,6 +103,8 @@
IPR Disclosure ID #{{ item.source.id }},
{% if item.source.state.slug == "removed" %}
"{{ item.source.title }}" (which was removed at the request of the submitter)
{% elif item.source.state.slug == "removed_objfalse" %}
"{{ item.source.title }}" (which was removed as objectively false)
{% else %}
"<a href="{% url "ietf.ipr.views.show" id=item.source.id %}">{{ item.source.title }}</a>"
{% endif %}
@ -122,6 +124,8 @@
IPR Disclosure ID #{{ item.target.id }},
{% if item.target.state.slug == "removed" %}
"{{ item.target.title }}" (which was removed at the request of the submitter)
{% elif item.source.state.slug == "removed_objfalse" %}
"{{ item.source.title }}" (which was removed as objectively false)
{% elif item.target.state.slug == "rejected" %}
"{{ item.target.title }}" (which was rejected)
{% elif item.target.state.slug == "parked" %}

View file

@ -1,11 +1,15 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{# Copyright The IETF Trust 2015-2023, All Rights Reserved #}
{% load origin %}
{% block title %}{{ ipr.title }}{% endblock %}
{% block content %}
{% origin %}
<h1>{{ ipr.title }}</h1>
<p class="alert alert-info my-3">
This IPR disclosure was removed at the submitter's request.
{% if ipr.state.slug == "removed" %}
This IPR disclosure was removed at the submitter's request.
{% elif ipr.state.slug == "removed_objfalse" %}
This IPR disclosure was removed as objectively false.
{% endif %}
</p>
{% endblock %}

View file

@ -28,7 +28,7 @@
<tr>
<td>{{ ipr.time|date:"Y-m-d" }}</td>
<td>{{ ipr.id }}</td>
<td><a href="{% url "ietf.ipr.views.show" id=ipr.id %}">{{ ipr.title }}</a>{% if ipr.state_id == 'removed' %}<span class="badge rounded-pill text-bg-info">Removed</span>{% endif %}
<td><a href="{% url "ietf.ipr.views.show" id=ipr.id %}">{{ ipr.title }}</a>{% if ipr.state_id == 'removed' or ipr.state_id == 'removed_objfalse' %}<span class="badge rounded-pill text-bg-info">Removed</span>{% endif %}
{% if ipr.updates %} <br>(Updates ID#: {% for upd in ipr.updates %}{{upd.target_id}}{% if not forloop.last %}, {% endif %}{% endfor %}){% endif %}
</td>
</tr>
@ -70,7 +70,7 @@
<tr>
<td>{{ ipr.disclosure.time|date:"Y-m-d" }}</td>
<td>{{ ipr.disclosure.id }}</td>
<td><a href="{% url "ietf.ipr.views.show" id=ipr.disclosure.id %}">{{ ipr.disclosure.title }}</a>{% if ipr.disclosure.state_id == 'removed' %} (Removed) {% endif %}
<td><a href="{% url "ietf.ipr.views.show" id=ipr.disclosure.id %}">{{ ipr.disclosure.title }}</a>{% if ipr.disclosure.state_id == 'removed' or ipr.disclosure.state_id == 'removed_objfalse' %} (Removed) {% endif %}
{% if ipr.disclosure.updates %} <br>(Updates ID#: {% for upd in ipr.disclosure.updates %}{{upd.target_id}}{% if not forloop.last %}, {% endif %}{% endfor %}){% endif %}
</td>
</tr>