Skip to content

Commit a7b3d9c

Browse files
committed
Merged in [18157] from jennifer@painless-security.com:
Store Auth48 URL as DocumentURL and display in RFC-Editor state. Migrates old data when possible. Alternative to 17563. Fixes ietf-tools#2722. - Legacy-Id: 18166 Note: SVN reference [18157] has been migrated to Git commit fff927b
2 parents d7de7f6 + fff927b commit a7b3d9c

9 files changed

Lines changed: 307 additions & 52 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright The IETF Trust 2020, All Rights Reserved
2+
# -*- coding: utf-8 -*-
3+
from __future__ import unicode_literals
4+
5+
from django.db import migrations
6+
from django.db.models import OuterRef, Subquery
7+
8+
from re import match
9+
10+
11+
def forward(apps, schema_editor):
12+
"""Add DocumentURLs for docs in the Auth48 state
13+
14+
Checks the latest StateDocEvent; if it is in the auth48 state and the
15+
event desc has an AUTH48 link, creates an auth48 DocumentURL for that doc.
16+
"""
17+
Document = apps.get_model('doc', 'Document')
18+
StateDocEvent = apps.get_model('doc', 'StateDocEvent')
19+
DocumentURL = apps.get_model('doc', 'DocumentURL')
20+
21+
# Regex - extracts auth48 URL as first match group
22+
pattern = r'RFC Editor state changed to <a href="(.*)"><b>AUTH48.*</b></a>.*'
23+
24+
# To avoid 100k queries, set up a subquery to find the latest StateDocEvent for each doc...
25+
latest_events = StateDocEvent.objects.filter(doc=OuterRef('pk')).order_by('-time', '-id')
26+
# ... then annotate the doc list with that and select only those in the auth48 state...
27+
auth48_docs = Document.objects.annotate(
28+
current_state_slug=Subquery(latest_events.values('state__slug')[:1])
29+
).filter(current_state_slug='auth48')
30+
# ... and add an auth48 DocumentURL if one is found.
31+
for doc in auth48_docs:
32+
# Retrieve the full StateDocEvent. Results in a query per doc, but
33+
# only for the few few in the auth48 state.
34+
sde = StateDocEvent.objects.filter(doc=doc).order_by('-time', '-id').first()
35+
urlmatch = match(pattern, sde.desc) # Auth48 URL is usually in the event desc
36+
if urlmatch is not None:
37+
DocumentURL.objects.create(doc=doc, tag_id='auth48', url=urlmatch[1])
38+
39+
# Validate the migration using a different approach to find auth48 docs.
40+
# This is slower than above, but still avoids querying for every Document.
41+
auth48_events = StateDocEvent.objects.filter(state__slug='auth48')
42+
for a48_event in auth48_events:
43+
doc = a48_event.doc
44+
latest_sde = StateDocEvent.objects.filter(doc=doc).order_by('-time', '-id').first()
45+
if latest_sde.state and latest_sde.state.slug == 'auth48' and match(pattern, latest_sde.desc) is not None:
46+
# Currently in the auth48 state with a URL
47+
assert doc.documenturl_set.filter(tag_id='auth48').count() == 1
48+
else:
49+
# Either no longer in auth48 state or had no URL
50+
assert doc.documenturl_set.filter(tag_id='auth48').count() == 0
51+
52+
53+
def reverse(apps, schema_editor):
54+
"""Remove any auth48 DocumentURLs - these did not exist before"""
55+
DocumentURL = apps.get_model('doc', 'DocumentURL')
56+
DocumentURL.objects.filter(tag_id='auth48').delete()
57+
58+
59+
class Migration(migrations.Migration):
60+
dependencies = [
61+
('doc', '0031_set_state_for_charters_of_replaced_groups'),
62+
('name', '0012_add_auth48_docurltagname'),
63+
]
64+
65+
operations = [
66+
migrations.RunPython(forward, reverse),
67+
]

ietf/doc/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,7 @@ def is_dochistory(self):
916916
def related_ipr(self):
917917
return self.doc.related_ipr()
918918

919+
@property
919920
def documenturl_set(self):
920921
return self.doc.documenturl_set
921922

ietf/doc/tests.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,44 @@ def test_document_primary_and_history_views(self):
780780
self.assertEqual(r.status_code, 200)
781781
self.assertContains(r, "%s-00"%docname)
782782

783+
def test_rfcqueue_auth48_views(self):
784+
"""Test view handling of RFC editor queue auth48 state"""
785+
def _change_state(doc, state):
786+
event = StateDocEventFactory(doc=doc, state=state)
787+
doc.set_state(event.state)
788+
doc.save_with_history([event])
789+
790+
draft = IndividualDraftFactory()
791+
792+
# Put in an rfceditor state other than auth48
793+
for state in [('draft-iesg', 'rfcqueue'), ('draft-rfceditor', 'rfc-edit')]:
794+
_change_state(draft, state)
795+
r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)))
796+
self.assertEqual(r.status_code, 200)
797+
self.assertNotContains(r, 'Auth48 status')
798+
799+
# Put in auth48 state without a URL
800+
_change_state(draft, ('draft-rfceditor', 'auth48'))
801+
r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)))
802+
self.assertEqual(r.status_code, 200)
803+
self.assertNotContains(r, 'Auth48 status')
804+
805+
# Now add a URL
806+
documenturl = draft.documenturl_set.create(tag_id='auth48',
807+
url='http://rfceditor.example.com/auth48-url')
808+
r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)))
809+
self.assertEqual(r.status_code, 200)
810+
self.assertContains(r, 'Auth48 status')
811+
self.assertContains(r, documenturl.url)
812+
813+
# Put in auth48-done state and delete auth48 DocumentURL
814+
draft.documenturl_set.filter(tag_id='auth48').delete()
815+
_change_state(draft, ('draft-rfceditor', 'auth48-done'))
816+
r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)))
817+
self.assertEqual(r.status_code, 200)
818+
self.assertNotContains(r, 'Auth48 status')
819+
820+
783821
class DocTestCase(TestCase):
784822
def test_document_charter(self):
785823
CharterFactory(name='charter-ietf-mars')

ietf/doc/views_doc.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,15 @@ def document_main(request, name, rev=None):
420420
exp_comment = doc.latest_event(IanaExpertDocEvent,type="comment")
421421
iana_experts_comment = exp_comment and exp_comment.desc
422422

423+
# See if we should show an Auth48 URL
424+
auth48_url = None # stays None unless we are in the auth48 state
425+
if doc.get_state_slug('draft-rfceditor') == 'auth48':
426+
document_url = doc.documenturl_set.filter(tag_id='auth48').first()
427+
auth48_url = document_url.url if document_url else ''
428+
429+
# Do not show the Auth48 URL in the "Additional URLs" section
430+
additional_urls = doc.documenturl_set.exclude(tag_id='auth48')
431+
423432
return render(request, "doc/document_draft.html",
424433
dict(doc=doc,
425434
group=group,
@@ -469,6 +478,7 @@ def document_main(request, name, rev=None):
469478
has_errata=doc.tags.filter(slug="errata"),
470479
published=published,
471480
file_urls=file_urls,
481+
additional_urls=additional_urls,
472482
stream_state_type_slug=stream_state_type_slug,
473483
stream_state=stream_state,
474484
stream_tags=stream_tags,
@@ -477,6 +487,7 @@ def document_main(request, name, rev=None):
477487
iesg_state=iesg_state,
478488
iesg_state_summary=iesg_state_summary,
479489
rfc_editor_state=doc.get_state("draft-rfceditor"),
490+
rfc_editor_auth48_url=auth48_url,
480491
iana_review_state=doc.get_state("draft-iana-review"),
481492
iana_action_state=doc.get_state("draft-iana-action"),
482493
iana_experts_state=doc.get_state("draft-iana-experts"),

ietf/name/fixtures/names.json

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9510,6 +9510,16 @@
95109510
"model": "name.doctypename",
95119511
"pk": "statchg"
95129512
},
9513+
{
9514+
"fields": {
9515+
"desc": "",
9516+
"name": "RFC Editor Auth48 status",
9517+
"order": 0,
9518+
"used": true
9519+
},
9520+
"model": "name.docurltagname",
9521+
"pk": "auth48"
9522+
},
95139523
{
95149524
"fields": {
95159525
"desc": "",
@@ -14584,7 +14594,7 @@
1458414594
"fields": {
1458514595
"command": "xym",
1458614596
"switch": "--version",
14587-
"time": "2020-07-11T00:12:44.590",
14597+
"time": "2020-05-29T00:13:35.959",
1458814598
"used": true,
1458914599
"version": "xym 0.4.8"
1459014600
},
@@ -14595,7 +14605,7 @@
1459514605
"fields": {
1459614606
"command": "pyang",
1459714607
"switch": "--version",
14598-
"time": "2020-07-11T00:12:46.120",
14608+
"time": "2020-05-29T00:13:38.724",
1459914609
"used": true,
1460014610
"version": "pyang 2.2.1"
1460114611
},
@@ -14606,7 +14616,7 @@
1460614616
"fields": {
1460714617
"command": "yanglint",
1460814618
"switch": "--version",
14609-
"time": "2020-07-11T00:12:46.381",
14619+
"time": "2020-05-29T00:13:39.026",
1461014620
"used": true,
1461114621
"version": "yanglint SO 1.6.7"
1461214622
},
@@ -14617,7 +14627,7 @@
1461714627
"fields": {
1461814628
"command": "xml2rfc",
1461914629
"switch": "--version",
14620-
"time": "2020-07-11T00:12:48.052",
14630+
"time": "2020-05-29T00:13:40.790",
1462114631
"used": true,
1462214632
"version": "xml2rfc 2.46.0"
1462314633
},
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright The IETF Trust 2020, All Rights Reserved
2+
# -*- coding: utf-8 -*-
3+
# Generated by Django 2.0.13 on 2020-06-02 10:13
4+
5+
from django.db import migrations
6+
7+
def forward(apps, schema_editor):
8+
DocUrlTagName = apps.get_model('name', 'DocUrlTagName')
9+
DocUrlTagName.objects.create(
10+
slug='auth48',
11+
name='RFC Editor Auth48 status',
12+
used=True,
13+
)
14+
15+
def reverse(apps, schema_editor):
16+
DocUrlTagName = apps.get_model('name', 'DocUrlTagName')
17+
auth48_tag = DocUrlTagName.objects.get(slug='auth48')
18+
auth48_tag.delete()
19+
20+
class Migration(migrations.Migration):
21+
"""Add DocUrlTagName entry for RFC Ed Auth48 URL"""
22+
dependencies = [
23+
('name', '0011_constraintname_editor_label'),
24+
]
25+
26+
operations = [
27+
migrations.RunPython(forward, reverse),
28+
]

ietf/sync/rfceditor.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,14 @@ def update_drafts_from_queue(drafts):
199199
if auth48:
200200
e.desc = re.sub(r"(<b>.*</b>)", "<a href=\"%s\">\\1</a>" % auth48, e.desc)
201201
e.save()
202-
202+
# Create or update the auth48 URL whether or not this is a state expected to have one.
203+
d.documenturl_set.update_or_create(
204+
tag_id='auth48', # look up existing based on this field
205+
defaults=dict(url=auth48) # create or update with this field
206+
)
207+
else:
208+
# Remove any existing auth48 URL when an update does not have one.
209+
d.documenturl_set.filter(tag_id='auth48').delete()
203210
if e:
204211
events.append(e)
205212

ietf/sync/tests.py

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -381,17 +381,15 @@ def test_rfc_index(self):
381381
changed = list(rfceditor.update_docs_from_rfc_index(data, errata, today - datetime.timedelta(days=30)))
382382
self.assertEqual(len(changed), 0)
383383

384-
385-
def test_rfc_queue(self):
386-
draft = WgDraftFactory(states=[('draft-iesg','ann')])
387-
384+
def _generate_rfc_queue_xml(self, draft, state, auth48_url=None):
385+
"""Generate an RFC queue xml string for a draft"""
388386
t = '''<rfc-editor-queue xmlns="http://www.rfc-editor.org/rfc-editor-queue">
389387
<section name="IETF STREAM: WORKING GROUP STANDARDS TRACK">
390388
<entry xml:id="%(name)s">
391389
<draft>%(name)s-%(rev)s.txt</draft>
392390
<date-received>2010-09-08</date-received>
393-
<state>EDIT*R*A(1G)</state>
394-
<auth48-url>http://www.rfc-editor.org/auth48/rfc1234</auth48-url>
391+
<state>%(state)s</state>
392+
<auth48-url>%(auth48_url)s</auth48-url>
395393
<normRef>
396394
<ref-name>%(ref)s</ref-name>
397395
<ref-state>IN-QUEUE</ref-state>
@@ -408,26 +406,24 @@ def test_rfc_queue(self):
408406
rev=draft.rev,
409407
title=draft.title,
410408
group=draft.group.name,
411-
ref="draft-ietf-test")
409+
ref="draft-ietf-test",
410+
state=state,
411+
auth48_url=(auth48_url or ''))
412+
t = t.replace('<auth48-url></auth48-url>\n', '') # strip empty auth48-url tags
413+
return t
414+
415+
def test_rfc_queue(self):
416+
draft = WgDraftFactory(states=[('draft-iesg','ann')])
417+
expected_auth48_url = "http://www.rfc-editor.org/auth48/rfc1234"
418+
t = self._generate_rfc_queue_xml(draft,
419+
state='EDIT*R*A(1G)',
420+
auth48_url=expected_auth48_url)
412421

413422
drafts, warnings = rfceditor.parse_queue(io.StringIO(t))
423+
# rfceditor.parse_queue() is tested independently; just sanity check here
414424
self.assertEqual(len(drafts), 1)
415425
self.assertEqual(len(warnings), 0)
416426

417-
# Test with TI state introduced 11 Sep 2019
418-
t = t.replace("<state>EDIT*R*A(1G)</state>", "<state>TI</state>")
419-
__, warnings = rfceditor.parse_queue(io.StringIO(t))
420-
self.assertEqual(len(warnings), 0)
421-
422-
draft_name, date_received, state, tags, missref_generation, stream, auth48, cluster, refs = drafts[0]
423-
424-
# currently, we only check what we actually use
425-
self.assertEqual(draft_name, draft.name)
426-
self.assertEqual(state, "EDIT")
427-
self.assertEqual(set(tags), set(["iana", "ref"]))
428-
self.assertEqual(auth48, "http://www.rfc-editor.org/auth48/rfc1234")
429-
430-
431427
mailbox_before = len(outbox)
432428

433429
changed, warnings = rfceditor.update_drafts_from_queue(drafts)
@@ -450,6 +446,83 @@ def test_rfc_queue(self):
450446
self.assertEqual(len(changed), 0)
451447
self.assertEqual(len(warnings), 0)
452448

449+
def test_rfceditor_parse_queue(self):
450+
"""Test that rfceditor.parse_queue() behaves as expected.
451+
452+
Currently does a limited test - old comment was
453+
"currently, we only check what we actually use".
454+
"""
455+
draft = WgDraftFactory(states=[('draft-iesg','ann')])
456+
t = self._generate_rfc_queue_xml(draft,
457+
state='EDIT*R*A(1G)',
458+
auth48_url="http://www.rfc-editor.org/auth48/rfc1234")
459+
460+
drafts, warnings = rfceditor.parse_queue(io.StringIO(t))
461+
self.assertEqual(len(drafts), 1)
462+
self.assertEqual(len(warnings), 0)
463+
464+
draft_name, date_received, state, tags, missref_generation, stream, auth48, cluster, refs = drafts[0]
465+
self.assertEqual(draft_name, draft.name)
466+
self.assertEqual(state, "EDIT")
467+
self.assertEqual(set(tags), set(["iana", "ref"]))
468+
self.assertEqual(auth48, "http://www.rfc-editor.org/auth48/rfc1234")
469+
470+
def test_rfceditor_parse_queue_TI_state(self):
471+
# Test with TI state introduced 11 Sep 2019
472+
draft = WgDraftFactory(states=[('draft-iesg','ann')])
473+
t = self._generate_rfc_queue_xml(draft,
474+
state='TI',
475+
auth48_url="http://www.rfc-editor.org/auth48/rfc1234")
476+
__, warnings = rfceditor.parse_queue(io.StringIO(t))
477+
self.assertEqual(len(warnings), 0)
478+
479+
def _generate_rfceditor_update(self, draft, state, tags=None, auth48_url=None):
480+
"""Helper to generate fake output from rfceditor.parse_queue()"""
481+
return [[
482+
draft.name, # draft_name
483+
'2020-06-03', # date_received
484+
state,
485+
tags or [],
486+
'1', # missref_generation
487+
'ietf', # stream
488+
auth48_url or '',
489+
'', # cluster
490+
['draft-ietf-test'], # refs
491+
]]
492+
493+
def test_update_draft_auth48_url(self):
494+
"""Test that auth48 URLs are handled correctly."""
495+
draft = WgDraftFactory(states=[('draft-iesg','ann')])
496+
497+
# Step 1 setup: update to a state with no auth48 URL
498+
changed, warnings = rfceditor.update_drafts_from_queue(
499+
self._generate_rfceditor_update(draft, state='EDIT')
500+
)
501+
self.assertEqual(len(changed), 1)
502+
self.assertEqual(len(warnings), 0)
503+
auth48_docurl = draft.documenturl_set.filter(tag_id='auth48').first()
504+
self.assertIsNone(auth48_docurl)
505+
506+
# Step 2: update to auth48 state with auth48 URL
507+
changed, warnings = rfceditor.update_drafts_from_queue(
508+
self._generate_rfceditor_update(draft, state='AUTH48', auth48_url='http://www.rfc-editor.org/rfc1234')
509+
)
510+
self.assertEqual(len(changed), 1)
511+
self.assertEqual(len(warnings), 0)
512+
auth48_docurl = draft.documenturl_set.filter(tag_id='auth48').first()
513+
self.assertIsNotNone(auth48_docurl)
514+
self.assertEqual(auth48_docurl.url, 'http://www.rfc-editor.org/rfc1234')
515+
516+
# Step 3: update to auth48-done state without auth48 URL
517+
changed, warnings = rfceditor.update_drafts_from_queue(
518+
self._generate_rfceditor_update(draft, state='AUTH48-DONE')
519+
)
520+
self.assertEqual(len(changed), 1)
521+
self.assertEqual(len(warnings), 0)
522+
auth48_docurl = draft.documenturl_set.filter(tag_id='auth48').first()
523+
self.assertIsNone(auth48_docurl)
524+
525+
453526
class DiscrepanciesTests(TestCase):
454527
def test_discrepancies(self):
455528

0 commit comments

Comments
 (0)