Skip to content

Commit 3846996

Browse files
Return rev used to find doc when heuristics modify the input. Share heuristics between rfcdiff and html views. Fixes ietf-tools#3437. Commit ready for merge.
- Legacy-Id: 19658
1 parent d6a2627 commit 3846996

5 files changed

Lines changed: 279 additions & 136 deletions

File tree

ietf/doc/factories.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,19 @@ def relations(obj, create, extracted, **kwargs): # pylint: disable=no-self-argum
8787
continue
8888
obj.relateddocument_set.create(relationship_id=rel_id, target=docalias)
8989

90+
@factory.post_generation
91+
def create_revisions(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument
92+
"""Create additional revisions of the document
93+
94+
Argument should be an iterable of revisions. Remember that range() is exclusive on the end
95+
index, so range(1, 10) stops at 9.
96+
"""
97+
if create and extracted:
98+
for rev in extracted:
99+
e = NewRevisionDocEventFactory(doc=obj, rev=f'{rev:02d}')
100+
obj.rev = f'{rev:02d}'
101+
obj.save_with_history([e])
102+
90103
@classmethod
91104
def _after_postgeneration(cls, obj, create, results=None):
92105
"""Save again the instance if creating and at least one hook ran."""

ietf/doc/tests.py

Lines changed: 137 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2486,113 +2486,141 @@ class RfcdiffSupportTests(TestCase):
24862486
def setUp(self):
24872487
super().setUp()
24882488
self.target_view = 'ietf.doc.views_doc.rfcdiff_latest_json'
2489+
self._last_rfc_num = 8000
24892490

24902491
def getJson(self, view_args):
24912492
url = urlreverse(self.target_view, kwargs=view_args)
24922493
r = self.client.get(url)
24932494
self.assertEqual(r.status_code, 200)
24942495
return r.json()
24952496

2496-
def test_draft(self):
2497-
draft = IndividualDraftFactory(name='draft-somebody-did-something',rev='00')
2498-
for r in range(0,13):
2499-
e = NewRevisionDocEventFactory(doc=draft,rev=f'{r:02d}')
2500-
draft.rev = f'{r:02d}'
2501-
draft.save_with_history([e])
2497+
def next_rfc_number(self):
2498+
self._last_rfc_num += 1
2499+
return self._last_rfc_num
2500+
2501+
def do_draft_test(self, name):
2502+
draft = IndividualDraftFactory(name=name, rev='00', create_revisions=range(0,13))
25022503
draft = reload_db_objects(draft)
25032504

25042505
received = self.getJson(dict(name=draft.name))
2505-
self.assertEqual(received, dict(
2506-
name=draft.name,
2507-
rev=draft.rev,
2508-
content_url=draft.get_href(),
2509-
previous=f'{draft.name}-{(int(draft.rev)-1):02d}'
2510-
))
2506+
self.assertEqual(
2507+
received,
2508+
dict(
2509+
name=draft.name,
2510+
rev=draft.rev,
2511+
content_url=draft.get_href(),
2512+
previous=f'{draft.name}-{(int(draft.rev)-1):02d}'
2513+
),
2514+
'Incorrect JSON when draft revision not specified',
2515+
)
25112516

25122517
received = self.getJson(dict(name=draft.name, rev=draft.rev))
2513-
self.assertEqual(received, dict(
2514-
name=draft.name,
2515-
rev=draft.rev,
2516-
content_url=draft.get_href(),
2517-
previous=f'{draft.name}-{(int(draft.rev)-1):02d}'
2518-
))
2518+
self.assertEqual(
2519+
received,
2520+
dict(
2521+
name=draft.name,
2522+
rev=draft.rev,
2523+
content_url=draft.get_href(),
2524+
previous=f'{draft.name}-{(int(draft.rev)-1):02d}'
2525+
),
2526+
'Incorrect JSON when latest revision specified',
2527+
)
25192528

25202529
received = self.getJson(dict(name=draft.name, rev='10'))
2521-
self.assertEqual(received, dict(
2522-
name=draft.name,
2523-
rev='10',
2524-
content_url=draft.history_set.get(rev='10').get_href(),
2525-
previous=f'{draft.name}-09'
2526-
))
2530+
self.assertEqual(
2531+
received,
2532+
dict(
2533+
name=draft.name,
2534+
rev='10',
2535+
content_url=draft.history_set.get(rev='10').get_href(),
2536+
previous=f'{draft.name}-09'
2537+
),
2538+
'Incorrect JSON when historical revision specified',
2539+
)
25272540

25282541
received = self.getJson(dict(name=draft.name, rev='00'))
2529-
self.assertNotIn('previous', received)
2542+
self.assertNotIn('previous', received, 'Rev 00 has no previous name when not replacing a draft')
25302543

25312544
replaced = IndividualDraftFactory()
25322545
RelatedDocument.objects.create(relationship_id='replaces',source=draft,target=replaced.docalias.first())
25332546
received = self.getJson(dict(name=draft.name, rev='00'))
2534-
self.assertEqual(received['previous'], f'{replaced.name}-{replaced.rev}')
2535-
2547+
self.assertEqual(received['previous'], f'{replaced.name}-{replaced.rev}',
2548+
'Rev 00 has a previous name when replacing a draft')
25362549

2537-
def test_draft_with_broken_history(self):
2538-
draft = IndividualDraftFactory(rev='10')
2550+
def test_draft(self):
2551+
# test with typical, straightforward names
2552+
self.do_draft_test(name='draft-somebody-did-a-thing')
2553+
# try with different potentially problematic names
2554+
self.do_draft_test(name='draft-someone-did-something-01-02')
2555+
self.do_draft_test(name='draft-someone-did-something-else-02')
2556+
self.do_draft_test(name='draft-someone-did-something-02-weird-01')
2557+
2558+
def do_draft_with_broken_history_test(self, name):
2559+
draft = IndividualDraftFactory(name=name, rev='10')
25392560
received = self.getJson(dict(name=draft.name,rev='09'))
25402561
self.assertEqual(received['rev'],'09')
25412562
self.assertEqual(received['previous'], f'{draft.name}-08')
25422563
self.assertTrue('warning' in received)
25432564

2544-
2545-
def test_draftname_with_numeric_suffix(self):
2546-
draft = IndividualDraftFactory(name='draft-someone-did-something-01-02',rev='00')
2547-
for r in range(0,4):
2548-
e = NewRevisionDocEventFactory(doc=draft,rev=f'{r:02d}')
2549-
draft.rev = f'{r:02d}'
2550-
draft.save_with_history([e])
2551-
2552-
received = self.getJson(dict(name=draft.name))
2553-
self.assertEqual(received['rev'],'03')
2554-
self.assertIn('01-02-03',received['content_url'])
2555-
self.assertIn('01-02-02',received['previous'])
2556-
2557-
received = self.getJson(dict(name=draft.name,rev='02'))
2558-
self.assertEqual(received['rev'],'02')
2559-
self.assertIn('01-02-02',received['content_url'])
2560-
2561-
def test_rfc(self):
2562-
draft = WgDraftFactory()
2563-
for r in range(0,2):
2564-
e = NewRevisionDocEventFactory(doc=draft,rev=f'{r:02d}')
2565-
draft.rev = f'{r:02d}'
2566-
draft.save_with_history([e])
2567-
2568-
draft.docalias.create(name='rfc8000')
2565+
def test_draft_with_broken_history(self):
2566+
# test with typical, straightforward names
2567+
self.do_draft_with_broken_history_test(name='draft-somebody-did-something')
2568+
# try with different potentially problematic names
2569+
self.do_draft_with_broken_history_test(name='draft-someone-did-something-01-02')
2570+
self.do_draft_with_broken_history_test(name='draft-someone-did-something-else-02')
2571+
self.do_draft_with_broken_history_test(name='draft-someone-did-something-02-weird-03')
2572+
2573+
def do_rfc_test(self, draft_name):
2574+
draft = WgDraftFactory(name=draft_name, create_revisions=range(0,2))
2575+
draft.docalias.create(name=f'rfc{self.next_rfc_number():04}')
25692576
draft.set_state(State.objects.get(type_id='draft',slug='rfc'))
25702577
draft.set_state(State.objects.get(type_id='draft-iesg', slug='pub'))
25712578
draft = reload_db_objects(draft)
25722579
rfc = draft
25732580

25742581
number = rfc.rfc_number()
25752582
received = self.getJson(dict(name=number))
2576-
self.assertEqual(received, dict(
2577-
content_url=rfc.get_href(),
2578-
name=rfc.canonical_name(),
2579-
previous=f'{draft.name}-{draft.rev}',
2580-
))
2583+
self.assertEqual(
2584+
received,
2585+
dict(
2586+
content_url=rfc.get_href(),
2587+
name=rfc.canonical_name(),
2588+
previous=f'{draft.name}-{draft.rev}',
2589+
),
2590+
'Can look up an RFC by number',
2591+
)
25812592

25822593
num_received = received
25832594
received = self.getJson(dict(name=rfc.canonical_name()))
2584-
self.assertEqual(num_received, received)
2595+
self.assertEqual(num_received, received, 'RFC by canonical name gives same result as by number')
25852596

25862597
received = self.getJson(dict(name=f'RfC {number}'))
2587-
self.assertEqual(num_received, received)
2598+
self.assertEqual(num_received, received, 'RFC with unusual spacing/caps gives same result as by number')
25882599

2589-
def test_rfc_with_tombstone(self):
2590-
draft = WgDraftFactory()
2591-
for r in range(0,2):
2592-
e = NewRevisionDocEventFactory(doc=draft,rev=f'{r:02d}')
2593-
draft.rev = f'{r:02d}'
2594-
draft.save_with_history([e])
2600+
received = self.getJson(dict(name=draft.name))
2601+
self.assertEqual(num_received, received, 'RFC by draft name and no rev gives same result as by number')
2602+
2603+
received = self.getJson(dict(name=draft.name, rev='01'))
2604+
self.assertEqual(
2605+
received,
2606+
dict(
2607+
content_url=draft.history_set.get(rev='01').get_href(),
2608+
name=draft.name,
2609+
rev='01',
2610+
previous=f'{draft.name}-00',
2611+
),
2612+
'RFC by draft name with rev should give draft name, not canonical name'
2613+
)
25952614

2615+
def test_rfc(self):
2616+
# simple draft name
2617+
self.do_rfc_test(draft_name='draft-test-ar-ef-see')
2618+
# tricky draft names
2619+
self.do_rfc_test(draft_name='draft-whatever-02')
2620+
self.do_rfc_test(draft_name='draft-test-me-03-04')
2621+
2622+
def test_rfc_with_tombstone(self):
2623+
draft = WgDraftFactory(create_revisions=range(0,2))
25962624
draft.docalias.create(name='rfc3261') # See views_doc.HAS_TOMBSTONE
25972625
draft.set_state(State.objects.get(type_id='draft',slug='rfc'))
25982626
draft.set_state(State.objects.get(type_id='draft-iesg', slug='pub'))
@@ -2603,4 +2631,46 @@ def test_rfc_with_tombstone(self):
26032631
received = self.getJson(dict(name=rfc.canonical_name()))
26042632
self.assertTrue(received['previous'].endswith('00'))
26052633

2634+
def do_rfc_with_broken_history_test(self, draft_name):
2635+
draft = WgDraftFactory(rev='10', name=draft_name)
2636+
draft.docalias.create(name=f'rfc{self.next_rfc_number():04}')
2637+
draft.set_state(State.objects.get(type_id='draft',slug='rfc'))
2638+
draft.set_state(State.objects.get(type_id='draft-iesg', slug='pub'))
2639+
draft = reload_db_objects(draft)
2640+
rfc = draft
2641+
2642+
received = self.getJson(dict(name=draft.name))
2643+
self.assertEqual(
2644+
received,
2645+
dict(
2646+
content_url=rfc.get_href(),
2647+
name=rfc.canonical_name(),
2648+
previous=f'{draft.name}-10',
2649+
),
2650+
'RFC by draft name without rev should return canonical RFC name and no rev',
2651+
)
2652+
2653+
received = self.getJson(dict(name=draft.name, rev='10'))
2654+
self.assertEqual(received['name'], draft.name, 'RFC by draft name with rev should return draft name')
2655+
self.assertEqual(received['rev'], '10', 'Requested rev should be returned')
2656+
self.assertEqual(received['previous'], f'{draft.name}-09', 'Previous rev is one less than requested')
2657+
self.assertIn(f'{draft.name}-10', received['content_url'], 'Returned URL should include requested rev')
2658+
self.assertNotIn('warning', received, 'No warning when we have the rev requested')
2659+
2660+
received = self.getJson(dict(name=f'{draft.name}-09'))
2661+
self.assertEqual(received['name'], draft.name, 'RFC by draft name with rev should return draft name')
2662+
self.assertEqual(received['rev'], '09', 'Requested rev should be returned')
2663+
self.assertEqual(received['previous'], f'{draft.name}-08', 'Previous rev is one less than requested')
2664+
self.assertIn(f'{draft.name}-09', received['content_url'], 'Returned URL should include requested rev')
2665+
self.assertEqual(
2666+
received['warning'],
2667+
'History for this version not found - these results are speculation',
2668+
'Warning should be issued when requested rev is not found'
2669+
)
26062670

2671+
def test_rfc_with_broken_history(self):
2672+
# simple draft name
2673+
self.do_rfc_with_broken_history_test(draft_name='draft-some-draft')
2674+
# tricky draft names
2675+
self.do_rfc_with_broken_history_test(draft_name='draft-gizmo-01')
2676+
self.do_rfc_with_broken_history_test(draft_name='draft-oh-boy-what-a-draft-02-03')

ietf/doc/tests_utils.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
from ietf.person.factories import PersonFactory
99
from ietf.utils.test_utils import TestCase
1010
from ietf.person.models import Person
11-
from ietf.doc.factories import DocumentFactory
12-
from ietf.doc.models import State, DocumentActionHolder, DocumentAuthor
13-
from ietf.doc.utils import update_action_holders, add_state_change_event, update_documentauthors
11+
from ietf.doc.factories import DocumentFactory, WgRfcFactory
12+
from ietf.doc.models import State, DocumentActionHolder, DocumentAuthor, Document
13+
from ietf.doc.utils import update_action_holders, add_state_change_event, update_documentauthors, fuzzy_find_documents
1414

1515

1616
class ActionHoldersTests(TestCase):
@@ -239,4 +239,49 @@ def test_update_documentauthors_with_nulls(self):
239239
self.assertIn('cleared country (was "USA")', events[0].desc)
240240
docauth = doc.documentauthor_set.first()
241241
self.assertEqual(docauth.affiliation, '')
242-
self.assertEqual(docauth.country, '')
242+
self.assertEqual(docauth.country, '')
243+
244+
def do_fuzzy_find_documents_rfc_test(self, name):
245+
rfc = WgRfcFactory(name=name, create_revisions=(0, 1, 2))
246+
rfc = Document.objects.get(pk=rfc.pk) # clear out any cached values
247+
248+
# by canonical name
249+
found = fuzzy_find_documents(rfc.canonical_name(), None)
250+
self.assertCountEqual(found.documents, [rfc])
251+
self.assertEqual(found.matched_rev, None)
252+
self.assertEqual(found.matched_name, rfc.canonical_name())
253+
254+
# by draft name, no rev
255+
found = fuzzy_find_documents(rfc.name, None)
256+
self.assertCountEqual(found.documents, [rfc])
257+
self.assertEqual(found.matched_rev, None)
258+
self.assertEqual(found.matched_name, rfc.name)
259+
260+
# by draft name, latest rev
261+
found = fuzzy_find_documents(rfc.name, '02')
262+
self.assertCountEqual(found.documents, [rfc])
263+
self.assertEqual(found.matched_rev, '02')
264+
self.assertEqual(found.matched_name, rfc.name)
265+
266+
# by draft name, earlier rev
267+
found = fuzzy_find_documents(rfc.name, '01')
268+
self.assertCountEqual(found.documents, [rfc])
269+
self.assertEqual(found.matched_rev, '01')
270+
self.assertEqual(found.matched_name, rfc.name)
271+
272+
# wrong name or revision
273+
found = fuzzy_find_documents(rfc.name + '-incorrect')
274+
self.assertCountEqual(found.documents, [], 'Should not find document that does not match')
275+
found = fuzzy_find_documents(rfc.name + '-incorrect', '02')
276+
self.assertCountEqual(found.documents, [], 'Still should not find document, even with a version')
277+
found = fuzzy_find_documents(rfc.name, '22')
278+
self.assertCountEqual(found.documents, [rfc],
279+
'Should find document even if rev does not exist')
280+
281+
282+
def test_fuzzy_find_documents(self):
283+
# Should add additional tests/test cases for other document types/name formats
284+
self.do_fuzzy_find_documents_rfc_test('draft-normal-name')
285+
self.do_fuzzy_find_documents_rfc_test('draft-name-with-number-01')
286+
self.do_fuzzy_find_documents_rfc_test('draft-name-that-has-two-02-04')
287+
self.do_fuzzy_find_documents_rfc_test('draft-wild-01-numbers-0312')

ietf/doc/utils.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import re
1212
import textwrap
1313

14-
from collections import defaultdict
14+
from collections import defaultdict, namedtuple
1515
from urllib.parse import quote
1616

1717
from django.conf import settings
@@ -1286,3 +1286,39 @@ def generate_idnits2_rfcs_obsoleted():
12861286
obsdict[k] = sorted(obsdict[k])
12871287
return render_to_string('doc/idnits2-rfcs-obsoleted.txt', context={'obsitems':sorted(obsdict.items())})
12881288

1289+
1290+
def fuzzy_find_documents(name, rev=None):
1291+
"""Find a document based on name/rev
1292+
1293+
Applies heuristics, assuming the inputs were joined by a '-' that may have been misplaced.
1294+
If returned documents queryset is empty, matched_rev and and matched_name are meaningless.
1295+
The rev input is not validated - it is used to find possible names if the name input does
1296+
not match anything, but matched_rev may not correspond to an actual version of the found
1297+
document.
1298+
"""
1299+
# Handle special case name formats
1300+
if name.startswith('rfc0'):
1301+
name = "rfc" + name[3:].lstrip('0')
1302+
if name.startswith('review-') and re.search(r'-\d\d\d\d-\d\d$', name):
1303+
name = "%s-%s" % (name, rev)
1304+
rev = None
1305+
if rev and not name.startswith('charter-') and re.search('[0-9]{1,2}-[0-9]{2}', rev):
1306+
name = "%s-%s" % (name, rev[:-3])
1307+
rev = rev[-2:]
1308+
if re.match("^[0-9]+$", name):
1309+
name = f'rfc{name}'
1310+
if re.match("^[Rr][Ff][Cc] [0-9]+$",name):
1311+
name = f'rfc{name[4:]}'
1312+
1313+
# see if we can find a document using this name
1314+
docs = Document.objects.filter(docalias__name=name, type_id='draft')
1315+
if rev and not docs.exists():
1316+
# No document found, see if the name/rev split has been misidentified.
1317+
# Handles some special cases, like draft-ietf-tsvwg-ieee-802-11.
1318+
name = '%s-%s' % (name, rev)
1319+
docs = Document.objects.filter(docalias__name=name, type_id='draft')
1320+
if docs.exists():
1321+
rev = None # found a doc by name with rev = None, so update that
1322+
1323+
FoundDocuments = namedtuple('FoundDocuments', 'documents matched_name matched_rev')
1324+
return FoundDocuments(docs, name, rev)

0 commit comments

Comments
 (0)