431 lines
20 KiB
Python
431 lines
20 KiB
Python
# Copyright The IETF Trust 2020, All Rights Reserved
|
|
import datetime
|
|
import debug # pyflakes:ignore
|
|
|
|
from unittest.mock import patch
|
|
|
|
from django.db import IntegrityError
|
|
|
|
from ietf.group.factories import GroupFactory, RoleFactory
|
|
from ietf.name.models import DocTagName
|
|
from ietf.person.factories import PersonFactory
|
|
from ietf.utils.test_utils import TestCase, name_of_file_containing
|
|
from ietf.person.models import Person
|
|
from ietf.doc.factories import DocumentFactory, WgRfcFactory, WgDraftFactory
|
|
from ietf.doc.models import State, DocumentActionHolder, DocumentAuthor, Document
|
|
from ietf.doc.utils import (update_action_holders, add_state_change_event, update_documentauthors,
|
|
fuzzy_find_documents, rebuild_reference_relations)
|
|
from ietf.utils.draft import Draft, PlaintextDraft
|
|
from ietf.utils.xmldraft import XMLDraft
|
|
|
|
|
|
class ActionHoldersTests(TestCase):
|
|
|
|
def setUp(self):
|
|
"""Set up helper for the update_action_holders tests"""
|
|
super().setUp()
|
|
self.authors = PersonFactory.create_batch(3)
|
|
self.ad = Person.objects.get(user__username='ad')
|
|
self.group = GroupFactory()
|
|
RoleFactory(name_id='ad', group=self.group, person=self.ad)
|
|
|
|
def doc_in_iesg_state(self, slug):
|
|
return DocumentFactory(authors=self.authors, group=self.group, ad=self.ad, states=[('draft-iesg', slug)])
|
|
|
|
def update_doc_state(self, doc, new_state, add_tags=None, remove_tags=None):
|
|
"""Update document state/tags, create change event, and save"""
|
|
prev_tags = list(doc.tags.all()) # list to make sure we retrieve now
|
|
# prev_action_holders = list(doc.action_holders.all())
|
|
|
|
prev_state = doc.get_state(new_state.type_id)
|
|
if new_state != prev_state:
|
|
doc.set_state(new_state)
|
|
|
|
if add_tags:
|
|
doc.tags.add(*DocTagName.objects.filter(slug__in=add_tags))
|
|
if remove_tags:
|
|
doc.tags.remove(*DocTagName.objects.filter(slug__in=remove_tags))
|
|
new_tags = list(doc.tags.all())
|
|
|
|
events = []
|
|
e = add_state_change_event(
|
|
doc,
|
|
Person.objects.get(name='(System)'),
|
|
prev_state, new_state,
|
|
prev_tags, new_tags)
|
|
self.assertIsNotNone(e, 'Test logic error')
|
|
events.append(e)
|
|
e = update_action_holders(doc, prev_state, new_state, prev_tags, new_tags)
|
|
if e:
|
|
events.append(e)
|
|
doc.save_with_history(events)
|
|
|
|
|
|
def test_update_action_holders_by_state(self):
|
|
"""Doc action holders should auto-update correctly on state change"""
|
|
# Test the transition from every state to each of its 'next_states'
|
|
|
|
for initial_state in State.objects.filter(type__slug='draft-iesg'):
|
|
for next_state in initial_state.next_states.all():
|
|
# Test with no action holders initially
|
|
doc = DocumentFactory(
|
|
authors=self.authors,
|
|
group=self.group,
|
|
ad=self.ad,
|
|
states=[('draft-iesg', initial_state.slug)],
|
|
)
|
|
docevents_before = set(doc.docevent_set.all())
|
|
|
|
self.update_doc_state(doc, next_state)
|
|
|
|
new_docevents = set(doc.docevent_set.all()).difference(docevents_before)
|
|
self.assertIn(doc.latest_event(type='changed_state'), new_docevents)
|
|
|
|
if next_state.slug in DocumentActionHolder.CLEAR_ACTION_HOLDERS_STATES:
|
|
self.assertCountEqual(doc.action_holders.all(), [])
|
|
self.assertEqual(len(new_docevents), 1)
|
|
else:
|
|
self.assertCountEqual(
|
|
doc.action_holders.all(), [doc.ad],
|
|
'AD should be only action holder after transition to %s' % next_state.slug)
|
|
|
|
self.assertEqual(len(new_docevents), 2)
|
|
change_event = doc.latest_event(type='changed_action_holders')
|
|
self.assertIn(change_event, new_docevents)
|
|
self.assertIn('Changed action holders', change_event.desc)
|
|
self.assertIn(doc.ad.name, change_event.desc)
|
|
doc.delete() # clean up for next iteration
|
|
|
|
# Test with action holders initially
|
|
doc = DocumentFactory(
|
|
authors=self.authors,
|
|
group=self.group,
|
|
ad=self.ad,
|
|
states=[('draft-iesg', initial_state.slug)],
|
|
)
|
|
doc.action_holders.add(*self.authors) # adds all authors
|
|
docevents_before = set(doc.docevent_set.all())
|
|
|
|
self.update_doc_state(doc, next_state)
|
|
|
|
new_docevents = set(doc.docevent_set.all()).difference(docevents_before)
|
|
self.assertEqual(len(new_docevents), 2)
|
|
self.assertIn(doc.latest_event(type='changed_state'), new_docevents)
|
|
change_event = doc.latest_event(type='changed_action_holders')
|
|
self.assertIn(change_event, new_docevents)
|
|
|
|
if next_state.slug in DocumentActionHolder.CLEAR_ACTION_HOLDERS_STATES:
|
|
self.assertCountEqual(doc.action_holders.all(), [])
|
|
self.assertIn('Removed all action holders', change_event.desc)
|
|
else:
|
|
self.assertCountEqual(
|
|
doc.action_holders.all(), [doc.ad],
|
|
'AD should be only action holder after transition to %s' % next_state.slug)
|
|
self.assertIn('Changed action holders', change_event.desc)
|
|
self.assertIn(doc.ad.name, change_event.desc)
|
|
doc.delete() # clean up for next iteration
|
|
|
|
def test_update_action_holders_with_no_ad(self):
|
|
"""A document with no AD should be handled gracefully"""
|
|
doc = self.doc_in_iesg_state('idexists')
|
|
doc.ad = None
|
|
doc.save()
|
|
|
|
docevents_before = set(doc.docevent_set.all())
|
|
self.update_doc_state(doc, State.objects.get(slug='pub-req'))
|
|
new_docevents = set(doc.docevent_set.all()).difference(docevents_before)
|
|
self.assertEqual(len(new_docevents), 1)
|
|
self.assertIn(doc.latest_event(type='changed_state'), new_docevents)
|
|
self.assertCountEqual(doc.action_holders.all(), [])
|
|
|
|
def test_update_action_holders_resets_age(self):
|
|
"""Action holder age should reset when document state changes"""
|
|
doc = self.doc_in_iesg_state('pub-req')
|
|
doc.action_holders.set([self.ad])
|
|
dah = doc.documentactionholder_set.get(person=self.ad)
|
|
dah.time_added = datetime.datetime(2020, 1, 1) # arbitrary date in the past
|
|
dah.save()
|
|
|
|
self.assertNotEqual(doc.documentactionholder_set.get(person=self.ad).time_added.date(), datetime.date.today())
|
|
self.update_doc_state(doc, State.objects.get(slug='ad-eval'))
|
|
self.assertEqual(doc.documentactionholder_set.get(person=self.ad).time_added.date(), datetime.date.today())
|
|
|
|
def test_update_action_holders_add_tag_need_rev(self):
|
|
"""Adding need-rev tag adds authors as action holders"""
|
|
doc = self.doc_in_iesg_state('pub-req')
|
|
first_author = self.authors[0]
|
|
doc.action_holders.add(first_author)
|
|
self.assertCountEqual(doc.action_holders.all(), [first_author])
|
|
self.update_doc_state(doc,
|
|
doc.get_state('draft-iesg'),
|
|
add_tags=['need-rev'],
|
|
remove_tags=None)
|
|
self.assertCountEqual(doc.action_holders.all(), self.authors)
|
|
|
|
def test_update_action_holders_add_tag_need_rev_no_dups(self):
|
|
"""Adding need-rev tag does not duplicate existing action holders"""
|
|
doc = self.doc_in_iesg_state('pub-req')
|
|
self.assertCountEqual(doc.action_holders.all(), [])
|
|
self.update_doc_state(doc,
|
|
doc.get_state('draft-iesg'),
|
|
add_tags=['need-rev'],
|
|
remove_tags=None)
|
|
self.assertCountEqual(doc.action_holders.all(), self.authors)
|
|
|
|
def test_update_action_holders_remove_tag_need_rev(self):
|
|
"""Removing need-rev tag drops authors as action holders"""
|
|
doc = self.doc_in_iesg_state('pub-req')
|
|
doc.tags.add(DocTagName.objects.get(slug='need-rev'))
|
|
self.assertEqual(doc.action_holders.count(), 0)
|
|
self.update_doc_state(doc,
|
|
doc.get_state('draft-iesg'),
|
|
add_tags=None,
|
|
remove_tags=['need-rev'])
|
|
self.assertEqual(doc.action_holders.count(), 0)
|
|
|
|
def test_update_action_holders_add_tag_need_rev_ignores_non_authors(self):
|
|
"""Adding need-rev tag does not affect existing action holders"""
|
|
doc = self.doc_in_iesg_state('pub-req')
|
|
doc.action_holders.add(self.ad)
|
|
self.assertCountEqual(doc.action_holders.all(),[self.ad])
|
|
self.update_doc_state(doc,
|
|
doc.get_state('draft-iesg'),
|
|
add_tags=['need-rev'],
|
|
remove_tags=None)
|
|
self.assertCountEqual(doc.action_holders.all(), [self.ad] + self.authors)
|
|
|
|
def test_update_action_holders_remove_tag_need_rev_ignores_non_authors(self):
|
|
"""Removing need-rev tag does not affect non-author action holders"""
|
|
doc = self.doc_in_iesg_state('pub-req')
|
|
doc.tags.add(DocTagName.objects.get(slug='need-rev'))
|
|
doc.action_holders.add(self.ad)
|
|
self.assertCountEqual(doc.action_holders.all(), [self.ad])
|
|
self.update_doc_state(doc,
|
|
doc.get_state('draft-iesg'),
|
|
add_tags=None,
|
|
remove_tags=['need-rev'])
|
|
self.assertCountEqual(doc.action_holders.all(), [self.ad])
|
|
|
|
def test_doc_action_holders_enabled(self):
|
|
"""Action holders should only be enabled in certain states"""
|
|
doc = self.doc_in_iesg_state('idexists')
|
|
self.assertFalse(doc.action_holders_enabled())
|
|
|
|
for state in State.objects.filter(type='draft-iesg').exclude(slug='idexists'):
|
|
doc.set_state(state)
|
|
self.assertTrue(doc.action_holders_enabled())
|
|
|
|
|
|
class MiscTests(TestCase):
|
|
def test_update_documentauthors_with_nulls(self):
|
|
"""A 'None' value in the affiliation/country should be handled correctly"""
|
|
author_person = PersonFactory()
|
|
doc = DocumentFactory(authors=[author_person])
|
|
doc.documentauthor_set.update(
|
|
affiliation='Some Affiliation', country='USA'
|
|
)
|
|
try:
|
|
events = update_documentauthors(
|
|
doc,
|
|
[
|
|
DocumentAuthor(
|
|
person=author_person,
|
|
email=author_person.email(),
|
|
affiliation=None,
|
|
country=None,
|
|
)
|
|
],
|
|
)
|
|
except IntegrityError as err:
|
|
self.fail('IntegrityError was raised: {}'.format(err))
|
|
|
|
self.assertEqual(len(events), 1)
|
|
self.assertEqual(events[0].type, 'edited_authors')
|
|
self.assertIn('cleared affiliation (was "Some Affiliation")', events[0].desc)
|
|
self.assertIn('cleared country (was "USA")', events[0].desc)
|
|
docauth = doc.documentauthor_set.first()
|
|
self.assertEqual(docauth.affiliation, '')
|
|
self.assertEqual(docauth.country, '')
|
|
|
|
def do_fuzzy_find_documents_rfc_test(self, name):
|
|
rfc = WgRfcFactory(name=name, create_revisions=(0, 1, 2))
|
|
rfc = Document.objects.get(pk=rfc.pk) # clear out any cached values
|
|
|
|
# by canonical name
|
|
found = fuzzy_find_documents(rfc.canonical_name(), None)
|
|
self.assertCountEqual(found.documents, [rfc])
|
|
self.assertEqual(found.matched_rev, None)
|
|
self.assertEqual(found.matched_name, rfc.canonical_name())
|
|
|
|
# by draft name, no rev
|
|
found = fuzzy_find_documents(rfc.name, None)
|
|
self.assertCountEqual(found.documents, [rfc])
|
|
self.assertEqual(found.matched_rev, None)
|
|
self.assertEqual(found.matched_name, rfc.name)
|
|
|
|
# by draft name, latest rev
|
|
found = fuzzy_find_documents(rfc.name, '02')
|
|
self.assertCountEqual(found.documents, [rfc])
|
|
self.assertEqual(found.matched_rev, '02')
|
|
self.assertEqual(found.matched_name, rfc.name)
|
|
|
|
# by draft name, earlier rev
|
|
found = fuzzy_find_documents(rfc.name, '01')
|
|
self.assertCountEqual(found.documents, [rfc])
|
|
self.assertEqual(found.matched_rev, '01')
|
|
self.assertEqual(found.matched_name, rfc.name)
|
|
|
|
# wrong name or revision
|
|
found = fuzzy_find_documents(rfc.name + '-incorrect')
|
|
self.assertCountEqual(found.documents, [], 'Should not find document that does not match')
|
|
found = fuzzy_find_documents(rfc.name + '-incorrect', '02')
|
|
self.assertCountEqual(found.documents, [], 'Still should not find document, even with a version')
|
|
found = fuzzy_find_documents(rfc.name, '22')
|
|
self.assertCountEqual(found.documents, [rfc],
|
|
'Should find document even if rev does not exist')
|
|
|
|
|
|
def test_fuzzy_find_documents(self):
|
|
# Should add additional tests/test cases for other document types/name formats
|
|
self.do_fuzzy_find_documents_rfc_test('draft-normal-name')
|
|
self.do_fuzzy_find_documents_rfc_test('draft-name-with-number-01')
|
|
self.do_fuzzy_find_documents_rfc_test('draft-name-that-has-two-02-04')
|
|
self.do_fuzzy_find_documents_rfc_test('draft-wild-01-numbers-0312')
|
|
|
|
|
|
class RebuildReferenceRelationsTests(TestCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.doc = WgDraftFactory() # document under test
|
|
# Other documents that should be found by rebuild_reference_relations
|
|
self.normative, self.informative, self.unknown = WgRfcFactory.create_batch(3)
|
|
for relationship in ['refnorm', 'refinfo', 'refunk', 'refold']:
|
|
self.doc.relateddocument_set.create(
|
|
target=WgRfcFactory().docalias.first(),
|
|
relationship_id=relationship,
|
|
)
|
|
self.updated = WgRfcFactory() # related document that should be left alone
|
|
self.doc.relateddocument_set.create(target=self.updated.docalias.first(), relationship_id='updates')
|
|
self.assertCountEqual(self.doc.relateddocument_set.values_list('relationship__slug', flat=True),
|
|
['refnorm', 'refinfo', 'refold', 'refunk', 'updates'],
|
|
'Test conditions set up incorrectly: wrong prior document relationships')
|
|
for other_doc in [self.normative, self.informative, self.unknown]:
|
|
self.assertEqual(
|
|
self.doc.relateddocument_set.filter(target__name=other_doc.canonical_name()).count(),
|
|
0,
|
|
'Test conditions set up incorrectly: new documents already related',
|
|
)
|
|
|
|
def _get_refs_return_value(self):
|
|
return {
|
|
self.normative.canonical_name(): Draft.REF_TYPE_NORMATIVE,
|
|
self.informative.canonical_name(): Draft.REF_TYPE_INFORMATIVE,
|
|
self.unknown.canonical_name(): Draft.REF_TYPE_UNKNOWN,
|
|
'draft-not-found': Draft.REF_TYPE_NORMATIVE,
|
|
}
|
|
|
|
def test_requires_txt_or_xml(self):
|
|
result = rebuild_reference_relations(self.doc, {})
|
|
self.assertCountEqual(result.keys(), ['errors'])
|
|
self.assertEqual(len(result['errors']), 1)
|
|
self.assertIn('No draft text available', result['errors'][0],
|
|
'Error should be reported if no draft file is given')
|
|
|
|
result = rebuild_reference_relations(self.doc, {'md': 'cant-do-this.md'})
|
|
self.assertCountEqual(result.keys(), ['errors'])
|
|
self.assertEqual(len(result['errors']), 1)
|
|
self.assertIn('No draft text available', result['errors'][0],
|
|
'Error should be reported if no XML or plaintext file is given')
|
|
|
|
@patch.object(XMLDraft, 'get_refs')
|
|
@patch.object(XMLDraft, '__init__', return_value=None)
|
|
def test_xml(self, mock_init, mock_get_refs):
|
|
"""Should build reference relations with only XML"""
|
|
mock_get_refs.return_value = self._get_refs_return_value()
|
|
|
|
result = rebuild_reference_relations(self.doc, {'xml': 'file.xml'})
|
|
|
|
# if the method of calling the XMLDraft() constructor changes, this will need to be updated
|
|
xmldraft_init_args, _ = mock_init.call_args
|
|
self.assertEqual(xmldraft_init_args, ('file.xml',), 'XMLDraft initialized with unexpected arguments')
|
|
self.assertEqual(
|
|
result,
|
|
{
|
|
'warnings': ['There were 1 references with no matching DocAlias'],
|
|
'unfound': ['draft-not-found'],
|
|
}
|
|
)
|
|
|
|
self.assertCountEqual(
|
|
self.doc.relateddocument_set.values_list('target__name', 'relationship__slug'),
|
|
[
|
|
(self.normative.canonical_name(), 'refnorm'),
|
|
(self.informative.canonical_name(), 'refinfo'),
|
|
(self.unknown.canonical_name(), 'refunk'),
|
|
(self.updated.docalias.first().name, 'updates'),
|
|
]
|
|
)
|
|
|
|
@patch.object(PlaintextDraft, 'get_refs')
|
|
@patch.object(PlaintextDraft, '__init__', return_value=None)
|
|
def test_plaintext(self, mock_init, mock_get_refs):
|
|
"""Should build reference relations with only plaintext"""
|
|
mock_get_refs.return_value = self._get_refs_return_value()
|
|
|
|
with name_of_file_containing('contents') as temp_file_name:
|
|
result = rebuild_reference_relations(self.doc, {'txt': temp_file_name})
|
|
|
|
# if the method of calling the PlaintextDraft() constructor changes, this test will need to be updated
|
|
_, mock_init_kwargs = mock_init.call_args
|
|
self.assertEqual(mock_init_kwargs, {'text': 'contents', 'source': temp_file_name},
|
|
'PlaintextDraft initialized with unexpected arguments')
|
|
self.assertEqual(
|
|
result,
|
|
{
|
|
'warnings': ['There were 1 references with no matching DocAlias'],
|
|
'unfound': ['draft-not-found'],
|
|
}
|
|
)
|
|
|
|
self.assertCountEqual(
|
|
self.doc.relateddocument_set.values_list('target__name', 'relationship__slug'),
|
|
[
|
|
(self.normative.canonical_name(), 'refnorm'),
|
|
(self.informative.canonical_name(), 'refinfo'),
|
|
(self.unknown.canonical_name(), 'refunk'),
|
|
(self.updated.docalias.first().name, 'updates'),
|
|
]
|
|
)
|
|
|
|
@patch.object(PlaintextDraft, '__init__')
|
|
@patch.object(XMLDraft, 'get_refs')
|
|
@patch.object(XMLDraft, '__init__', return_value=None)
|
|
def test_xml_and_plaintext(self, mock_init, mock_get_refs, mock_plaintext_init):
|
|
"""Should build reference relations with XML when plaintext also available"""
|
|
mock_get_refs.return_value = self._get_refs_return_value()
|
|
|
|
result = rebuild_reference_relations(self.doc, {'txt': 'file.txt', 'xml': 'file.xml'})
|
|
|
|
self.assertFalse(mock_plaintext_init.called, 'PlaintextDraft should not be used when XML is available')
|
|
|
|
# if the method of calling the XMLDraft() constructor changes, this will need to be updated
|
|
xmldraft_init_args, _ = mock_init.call_args
|
|
self.assertEqual(xmldraft_init_args, ('file.xml',), 'XMLDraft initialized with unexpected arguments')
|
|
self.assertEqual(
|
|
result,
|
|
{
|
|
'warnings': ['There were 1 references with no matching DocAlias'],
|
|
'unfound': ['draft-not-found'],
|
|
}
|
|
)
|
|
|
|
self.assertCountEqual(
|
|
self.doc.relateddocument_set.values_list('target__name', 'relationship__slug'),
|
|
[
|
|
(self.normative.canonical_name(), 'refnorm'),
|
|
(self.informative.canonical_name(), 'refinfo'),
|
|
(self.unknown.canonical_name(), 'refunk'),
|
|
(self.updated.docalias.first().name, 'updates'),
|
|
]
|
|
)
|