Skip to content

Commit 0996842

Browse files
committed
Merged in [19658] from jennifer@painless-security.com:
Return rev used to find doc when heuristics modify the input. Share heuristics between rfcdiff and html views. Fixes ietf-tools#3437. - Legacy-Id: 19659 Note: SVN reference [19658] has been migrated to Git commit 3846996
2 parents 4dce42d + 3846996 commit 0996842

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
@@ -2494,113 +2494,141 @@ class RfcdiffSupportTests(TestCase):
24942494
def setUp(self):
24952495
super().setUp()
24962496
self.target_view = 'ietf.doc.views_doc.rfcdiff_latest_json'
2497+
self._last_rfc_num = 8000
24972498

24982499
def getJson(self, view_args):
24992500
url = urlreverse(self.target_view, kwargs=view_args)
25002501
r = self.client.get(url)
25012502
self.assertEqual(r.status_code, 200)
25022503
return r.json()
25032504

2504-
def test_draft(self):
2505-
draft = IndividualDraftFactory(name='draft-somebody-did-something',rev='00')
2506-
for r in range(0,13):
2507-
e = NewRevisionDocEventFactory(doc=draft,rev=f'{r:02d}')
2508-
draft.rev = f'{r:02d}'
2509-
draft.save_with_history([e])
2505+
def next_rfc_number(self):
2506+
self._last_rfc_num += 1
2507+
return self._last_rfc_num
2508+
2509+
def do_draft_test(self, name):
2510+
draft = IndividualDraftFactory(name=name, rev='00', create_revisions=range(0,13))
25102511
draft = reload_db_objects(draft)
25112512

25122513
received = self.getJson(dict(name=draft.name))
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-
))
2514+
self.assertEqual(
2515+
received,
2516+
dict(
2517+
name=draft.name,
2518+
rev=draft.rev,
2519+
content_url=draft.get_href(),
2520+
previous=f'{draft.name}-{(int(draft.rev)-1):02d}'
2521+
),
2522+
'Incorrect JSON when draft revision not specified',
2523+
)
25192524

25202525
received = self.getJson(dict(name=draft.name, rev=draft.rev))
2521-
self.assertEqual(received, dict(
2522-
name=draft.name,
2523-
rev=draft.rev,
2524-
content_url=draft.get_href(),
2525-
previous=f'{draft.name}-{(int(draft.rev)-1):02d}'
2526-
))
2526+
self.assertEqual(
2527+
received,
2528+
dict(
2529+
name=draft.name,
2530+
rev=draft.rev,
2531+
content_url=draft.get_href(),
2532+
previous=f'{draft.name}-{(int(draft.rev)-1):02d}'
2533+
),
2534+
'Incorrect JSON when latest revision specified',
2535+
)
25272536

25282537
received = self.getJson(dict(name=draft.name, rev='10'))
2529-
self.assertEqual(received, dict(
2530-
name=draft.name,
2531-
rev='10',
2532-
content_url=draft.history_set.get(rev='10').get_href(),
2533-
previous=f'{draft.name}-09'
2534-
))
2538+
self.assertEqual(
2539+
received,
2540+
dict(
2541+
name=draft.name,
2542+
rev='10',
2543+
content_url=draft.history_set.get(rev='10').get_href(),
2544+
previous=f'{draft.name}-09'
2545+
),
2546+
'Incorrect JSON when historical revision specified',
2547+
)
25352548

25362549
received = self.getJson(dict(name=draft.name, rev='00'))
2537-
self.assertNotIn('previous', received)
2550+
self.assertNotIn('previous', received, 'Rev 00 has no previous name when not replacing a draft')
25382551

25392552
replaced = IndividualDraftFactory()
25402553
RelatedDocument.objects.create(relationship_id='replaces',source=draft,target=replaced.docalias.first())
25412554
received = self.getJson(dict(name=draft.name, rev='00'))
2542-
self.assertEqual(received['previous'], f'{replaced.name}-{replaced.rev}')
2543-
2555+
self.assertEqual(received['previous'], f'{replaced.name}-{replaced.rev}',
2556+
'Rev 00 has a previous name when replacing a draft')
25442557

2545-
def test_draft_with_broken_history(self):
2546-
draft = IndividualDraftFactory(rev='10')
2558+
def test_draft(self):
2559+
# test with typical, straightforward names
2560+
self.do_draft_test(name='draft-somebody-did-a-thing')
2561+
# try with different potentially problematic names
2562+
self.do_draft_test(name='draft-someone-did-something-01-02')
2563+
self.do_draft_test(name='draft-someone-did-something-else-02')
2564+
self.do_draft_test(name='draft-someone-did-something-02-weird-01')
2565+
2566+
def do_draft_with_broken_history_test(self, name):
2567+
draft = IndividualDraftFactory(name=name, rev='10')
25472568
received = self.getJson(dict(name=draft.name,rev='09'))
25482569
self.assertEqual(received['rev'],'09')
25492570
self.assertEqual(received['previous'], f'{draft.name}-08')
25502571
self.assertTrue('warning' in received)
25512572

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

25822589
number = rfc.rfc_number()
25832590
received = self.getJson(dict(name=number))
2584-
self.assertEqual(received, dict(
2585-
content_url=rfc.get_href(),
2586-
name=rfc.canonical_name(),
2587-
previous=f'{draft.name}-{draft.rev}',
2588-
))
2591+
self.assertEqual(
2592+
received,
2593+
dict(
2594+
content_url=rfc.get_href(),
2595+
name=rfc.canonical_name(),
2596+
previous=f'{draft.name}-{draft.rev}',
2597+
),
2598+
'Can look up an RFC by number',
2599+
)
25892600

25902601
num_received = received
25912602
received = self.getJson(dict(name=rfc.canonical_name()))
2592-
self.assertEqual(num_received, received)
2603+
self.assertEqual(num_received, received, 'RFC by canonical name gives same result as by number')
25932604

25942605
received = self.getJson(dict(name=f'RfC {number}'))
2595-
self.assertEqual(num_received, received)
2606+
self.assertEqual(num_received, received, 'RFC with unusual spacing/caps gives same result as by number')
25962607

2597-
def test_rfc_with_tombstone(self):
2598-
draft = WgDraftFactory()
2599-
for r in range(0,2):
2600-
e = NewRevisionDocEventFactory(doc=draft,rev=f'{r:02d}')
2601-
draft.rev = f'{r:02d}'
2602-
draft.save_with_history([e])
2608+
received = self.getJson(dict(name=draft.name))
2609+
self.assertEqual(num_received, received, 'RFC by draft name and no rev gives same result as by number')
2610+
2611+
received = self.getJson(dict(name=draft.name, rev='01'))
2612+
self.assertEqual(
2613+
received,
2614+
dict(
2615+
content_url=draft.history_set.get(rev='01').get_href(),
2616+
name=draft.name,
2617+
rev='01',
2618+
previous=f'{draft.name}-00',
2619+
),
2620+
'RFC by draft name with rev should give draft name, not canonical name'
2621+
)
26032622

2623+
def test_rfc(self):
2624+
# simple draft name
2625+
self.do_rfc_test(draft_name='draft-test-ar-ef-see')
2626+
# tricky draft names
2627+
self.do_rfc_test(draft_name='draft-whatever-02')
2628+
self.do_rfc_test(draft_name='draft-test-me-03-04')
2629+
2630+
def test_rfc_with_tombstone(self):
2631+
draft = WgDraftFactory(create_revisions=range(0,2))
26042632
draft.docalias.create(name='rfc3261') # See views_doc.HAS_TOMBSTONE
26052633
draft.set_state(State.objects.get(type_id='draft',slug='rfc'))
26062634
draft.set_state(State.objects.get(type_id='draft-iesg', slug='pub'))
@@ -2611,4 +2639,46 @@ def test_rfc_with_tombstone(self):
26112639
received = self.getJson(dict(name=rfc.canonical_name()))
26122640
self.assertTrue(received['previous'].endswith('00'))
26132641

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

2679+
def test_rfc_with_broken_history(self):
2680+
# simple draft name
2681+
self.do_rfc_with_broken_history_test(draft_name='draft-some-draft')
2682+
# tricky draft names
2683+
self.do_rfc_with_broken_history_test(draft_name='draft-gizmo-01')
2684+
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)