# Copyright The IETF Trust 2020, All Rights Reserved import datetime 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 from ietf.person.models import Person from ietf.doc.factories import DocumentFactory, WgRfcFactory 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 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')