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:
parent
6b6c881353
commit
febdeff85f
90
ietf/doc/migrations/0007_alter_docevent_type.py
Normal file
90
ietf/doc/migrations/0007_alter_docevent_type.py
Normal 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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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"),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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": "",
|
||||
|
|
24
ietf/name/migrations/0008_removed_objfalse.py
Normal file
24
ietf/name/migrations/0008_removed_objfalse.py
Normal 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),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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" %}
|
||||
|
|
|
@ -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 %}
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue