diff --git a/.github/workflows/ci-run-tests.yml b/.github/workflows/ci-run-tests.yml index a2f2e3d95..3d1647c8d 100644 --- a/.github/workflows/ci-run-tests.yml +++ b/.github/workflows/ci-run-tests.yml @@ -4,6 +4,7 @@ on: pull_request: branches: - 'main' + - 'feat/rfc' paths: - 'client/**' - 'ietf/**' diff --git a/ietf/api/tests.py b/ietf/api/tests.py index 5dd1823e7..ef2d4f710 100644 --- a/ietf/api/tests.py +++ b/ietf/api/tests.py @@ -24,7 +24,7 @@ import debug # pyflakes:ignore import ietf from ietf.doc.utils import get_unicode_document_content from ietf.doc.models import RelatedDocument, State -from ietf.doc.factories import IndividualDraftFactory, WgDraftFactory +from ietf.doc.factories import IndividualDraftFactory, WgDraftFactory, WgRfcFactory from ietf.group.factories import RoleFactory from ietf.meeting.factories import MeetingFactory, SessionFactory from ietf.meeting.models import Session @@ -943,7 +943,7 @@ class RfcdiffSupportTests(TestCase): self.assertNotIn('previous', received, 'Rev 00 has no previous name when not replacing a draft') replaced = IndividualDraftFactory() - RelatedDocument.objects.create(relationship_id='replaces',source=draft,target=replaced.docalias.first()) + RelatedDocument.objects.create(relationship_id='replaces',source=draft,target=replaced) received = self.getJson(dict(name=draft.name, rev='00')) self.assertEqual(received['previous'], f'{replaced.name}-{replaced.rev}', 'Rev 00 has a previous name when replacing a draft') @@ -973,19 +973,19 @@ class RfcdiffSupportTests(TestCase): def do_rfc_test(self, draft_name): draft = WgDraftFactory(name=draft_name, create_revisions=range(0,2)) - draft.docalias.create(name=f'rfc{self.next_rfc_number():04}') + rfc = WgRfcFactory(group=draft.group, rfc_number=self.next_rfc_number()) + draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) draft.set_state(State.objects.get(type_id='draft',slug='rfc')) draft.set_state(State.objects.get(type_id='draft-iesg', slug='pub')) - draft = reload_db_objects(draft) - rfc = draft + draft, rfc = reload_db_objects(draft, rfc) - number = rfc.rfc_number() + number = rfc.rfc_number received = self.getJson(dict(name=number)) self.assertEqual( received, dict( content_url=rfc.get_href(), - name=rfc.canonical_name(), + name=rfc.name, previous=f'{draft.name}-{draft.rev}', previous_url= draft.history_set.get(rev=draft.rev).get_href(), ), @@ -1025,11 +1025,11 @@ class RfcdiffSupportTests(TestCase): def test_rfc_with_tombstone(self): draft = WgDraftFactory(create_revisions=range(0,2)) - draft.docalias.create(name='rfc3261') # See views_doc.HAS_TOMBSTONE + rfc = WgRfcFactory(rfc_number=3261,group=draft.group)# See views_doc.HAS_TOMBSTONE + draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) draft.set_state(State.objects.get(type_id='draft',slug='rfc')) draft.set_state(State.objects.get(type_id='draft-iesg', slug='pub')) draft = reload_db_objects(draft) - rfc = draft # Some old rfcs had tombstones that shouldn't be used for comparisons received = self.getJson(dict(name=rfc.canonical_name())) @@ -1037,11 +1037,11 @@ class RfcdiffSupportTests(TestCase): def do_rfc_with_broken_history_test(self, draft_name): draft = WgDraftFactory(rev='10', name=draft_name) - draft.docalias.create(name=f'rfc{self.next_rfc_number():04}') + rfc = WgRfcFactory(group=draft.group, rfc_number=self.next_rfc_number()) + draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) draft.set_state(State.objects.get(type_id='draft',slug='rfc')) draft.set_state(State.objects.get(type_id='draft-iesg', slug='pub')) draft = reload_db_objects(draft) - rfc = draft received = self.getJson(dict(name=draft.name)) self.assertEqual( diff --git a/ietf/api/views.py b/ietf/api/views.py index b56544c43..ee85141b4 100644 --- a/ietf/api/views.py +++ b/ietf/api/views.py @@ -317,12 +317,9 @@ def get_previous_url(name, rev=None): previous_url = '' if condition in ('historic version', 'current version'): doc = history if history else document - if found_rev: - doc.is_rfc = lambda: False previous_url = doc.get_href() elif condition == 'version dochistory not found': document.rev = found_rev - document.is_rfc = lambda: False previous_url = document.get_href() return previous_url @@ -330,32 +327,39 @@ def get_previous_url(name, rev=None): def rfcdiff_latest_json(request, name, rev=None): response = dict() condition, document, history, found_rev = find_doc_for_rfcdiff(name, rev) - + if document.type_id == "rfc": + draft_alias = next(iter(document.related_that('became_rfc')), None) if condition == 'no such document': raise Http404 elif condition in ('historic version', 'current version'): doc = history if history else document - if not found_rev and doc.is_rfc(): - response['content_url'] = doc.get_href() - response['name']=doc.canonical_name() - if doc.name != doc.canonical_name(): + if doc.type_id == "rfc": + response['content_url'] = doc.get_href() + response['name']=doc.name + if draft_alias: + draft = draft_alias.document + prev_rev = draft.rev + if doc.rfc_number in HAS_TOMBSTONE and prev_rev != '00': + prev_rev = f'{(int(draft.rev)-1):02d}' + response['previous'] = f'{draft.name}-{prev_rev}' + response['previous_url'] = get_previous_url(draft.name, prev_rev) + elif doc.type_id == "draft" and not found_rev and doc.relateddocument_set.filter(relationship_id="became_rfc").exists(): + rfc = doc.related_that_doc("became_rfc")[0] + response['content_url'] = rfc.get_href() + response['name']=rfc.name prev_rev = doc.rev - # not sure what to do if non-numeric values come back, so at least log it - log.assertion('doc.rfc_number().isdigit()') # .rfc_number() is expensive... - log.assertion('doc.rev.isdigit()') - if int(doc.rfc_number()) in HAS_TOMBSTONE and prev_rev != '00': + if rfc.rfc_number in HAS_TOMBSTONE and prev_rev != '00': prev_rev = f'{(int(doc.rev)-1):02d}' response['previous'] = f'{doc.name}-{prev_rev}' response['previous_url'] = get_previous_url(doc.name, prev_rev) else: - doc.is_rfc = lambda: False response['content_url'] = doc.get_href() response['rev'] = doc.rev response['name'] = doc.name if doc.rev == '00': replaces_docs = (history.doc if condition=='historic version' else doc).related_that_doc('replaces') if replaces_docs: - replaces = replaces_docs[0].document + replaces = replaces_docs[0] response['previous'] = f'{replaces.name}-{replaces.rev}' response['previous_url'] = get_previous_url(replaces.name, replaces.rev) else: @@ -374,7 +378,6 @@ def rfcdiff_latest_json(request, name, rev=None): response['name'] = document.name response['rev'] = found_rev document.rev = found_rev - document.is_rfc = lambda: False response['content_url'] = document.get_href() # not sure what to do if non-numeric values come back, so at least log it log.assertion('found_rev.isdigit()') diff --git a/ietf/bin/rfc-editor-index-updates b/ietf/bin/rfc-editor-index-updates index dc7abe26b..356c78d22 100755 --- a/ietf/bin/rfc-editor-index-updates +++ b/ietf/bin/rfc-editor-index-updates @@ -84,7 +84,7 @@ for changes, doc, rfc_published in ietf.sync.rfceditor.update_docs_from_rfc_inde new_rfcs.append(doc) for c in changes: - log("RFC%s, %s: %s" % (doc.rfcnum, doc.name, c)) + log("RFC%s, %s: %s" % (doc.rfc_number, doc.name, c)) sys.exit(0) diff --git a/ietf/community/forms.py b/ietf/community/forms.py index 8d72ce0d7..ad8570896 100644 --- a/ietf/community/forms.py +++ b/ietf/community/forms.py @@ -30,6 +30,8 @@ class SearchRuleForm(forms.ModelForm): super(SearchRuleForm, self).__init__(*args, **kwargs) def restrict_state(state_type, slug=None): + if "state" not in self.fields: + raise RuntimeError(f"Rule type {rule_type} cannot include state filtering") f = self.fields['state'] f.queryset = f.queryset.filter(used=True).filter(type=state_type) if slug: @@ -38,11 +40,15 @@ class SearchRuleForm(forms.ModelForm): f.initial = f.queryset[0].pk f.widget = forms.HiddenInput() + if rule_type.endswith("_rfc"): + del self.fields["state"] # rfc rules must not look at document states + if rule_type in ["group", "group_rfc", "area", "area_rfc", "group_exp"]: if rule_type == "group_exp": restrict_state("draft", "expired") else: - restrict_state("draft", "rfc" if rule_type.endswith("rfc") else "active") + if not rule_type.endswith("_rfc"): + restrict_state("draft", "active") if rule_type.startswith("area"): self.fields["group"].label = "Area" @@ -70,7 +76,8 @@ class SearchRuleForm(forms.ModelForm): del self.fields["text"] elif rule_type in ["author", "author_rfc", "shepherd", "ad"]: - restrict_state("draft", "rfc" if rule_type.endswith("rfc") else "active") + if not rule_type.endswith("_rfc"): + restrict_state("draft", "active") if rule_type.startswith("author"): self.fields["person"].label = "Author" @@ -84,7 +91,8 @@ class SearchRuleForm(forms.ModelForm): del self.fields["text"] elif rule_type == "name_contains": - restrict_state("draft", "rfc" if rule_type.endswith("rfc") else "active") + if not rule_type.endswith("_rfc"): + restrict_state("draft", "active") del self.fields["person"] del self.fields["group"] diff --git a/ietf/community/migrations/0003_track_rfcs.py b/ietf/community/migrations/0003_track_rfcs.py new file mode 100644 index 000000000..2bf90fdb0 --- /dev/null +++ b/ietf/community/migrations/0003_track_rfcs.py @@ -0,0 +1,50 @@ +# Generated by Django 4.2.3 on 2023-07-07 18:33 + +from django.db import migrations + + +def forward(apps, schema_editor): + """Track any RFCs that were created from tracked drafts""" + CommunityList = apps.get_model("community", "CommunityList") + RelatedDocument = apps.get_model("doc", "RelatedDocument") + + # Handle individually tracked documents + for cl in CommunityList.objects.all(): + for rfc in set( + RelatedDocument.objects.filter( + source__in=cl.added_docs.all(), + relationship__slug="became_rfc", + ).values_list("target__docs", flat=True) + ): + cl.added_docs.add(rfc) + + # Handle rules - rules ending with _rfc should no longer filter by state. + # There are 9 CommunityLists with invalid author_rfc rules that are filtering + # by (draft, active) instead of (draft, rfc) state before migration. All but one + # also includes an author rule for (draft, active), so these will start following + # RFCs as well. The one exception will start tracking RFCs instead of I-Ds, which + # is probably what was intended, but will be a change in their user experience. + SearchRule = apps.get_model("community", "SearchRule") + rfc_rules = SearchRule.objects.filter(rule_type__endswith="_rfc") + rfc_rules.update(state=None) + +def reverse(apps, schema_editor): + Document = apps.get_model("doc", "Document") + for rfc in Document.objects.filter(type__slug="rfc"): + rfc.communitylist_set.clear() + + # See the comment above regarding author_rfc + SearchRule = apps.get_model("community", "SearchRule") + State = apps.get_model("doc", "State") + SearchRule.objects.filter(rule_type__endswith="_rfc").update( + state=State.objects.get(type_id="draft", slug="rfc") + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("community", "0002_auto_20230320_1222"), + ("doc", "0010_move_rfc_docaliases"), + ] + + operations = [migrations.RunPython(forward, reverse)] diff --git a/ietf/community/tests.py b/ietf/community/tests.py index 3dd86f70e..19530efcd 100644 --- a/ietf/community/tests.py +++ b/ietf/community/tests.py @@ -151,7 +151,7 @@ class CommunityListTests(WebTest): "action": "add_rule", "rule_type": "author_rfc", "author_rfc-person": Person.objects.filter(documentauthor__document=draft).first().pk, - "author_rfc-state": State.objects.get(type="draft", slug="rfc").pk, + "author_rfc-state": State.objects.get(type="rfc", slug="published").pk, }) self.assertEqual(r.status_code, 302) clist = CommunityList.objects.get(user__username="plain") @@ -408,4 +408,4 @@ class CommunityListTests(WebTest): self.assertEqual(len(outbox), mailbox_before + 1) self.assertTrue(draft.name in outbox[-1]["Subject"]) - \ No newline at end of file + diff --git a/ietf/community/utils.py b/ietf/community/utils.py index 8130954b9..4a6a2fadd 100644 --- a/ietf/community/utils.py +++ b/ietf/community/utils.py @@ -71,70 +71,103 @@ def update_name_contains_indexes_with_new_doc(doc): if re.search(r.text, doc.name) and not doc in r.name_contains_index.all(): r.name_contains_index.add(doc) + def docs_matching_community_list_rule(rule): docs = Document.objects.all() + + if rule.rule_type.endswith("_rfc"): + docs = docs.filter(type_id="rfc") # rule.state is ignored for RFCs + else: + docs = docs.filter(type_id="draft", states=rule.state) + if rule.rule_type in ['group', 'area', 'group_rfc', 'area_rfc']: - return docs.filter(Q(group=rule.group_id) | Q(group__parent=rule.group_id), states=rule.state) + return docs.filter(Q(group=rule.group_id) | Q(group__parent=rule.group_id)) elif rule.rule_type in ['group_exp']: - return docs.filter(group=rule.group_id, states=rule.state) + return docs.filter(group=rule.group_id) elif rule.rule_type.startswith("state_"): - return docs.filter(states=rule.state) + return docs elif rule.rule_type in ["author", "author_rfc"]: - return docs.filter(states=rule.state, documentauthor__person=rule.person) + return docs.filter(documentauthor__person=rule.person) elif rule.rule_type == "ad": - return docs.filter(states=rule.state, ad=rule.person) + return docs.filter(ad=rule.person) elif rule.rule_type == "shepherd": - return docs.filter(states=rule.state, shepherd__person=rule.person) + return docs.filter(shepherd__person=rule.person) elif rule.rule_type == "name_contains": - return docs.filter(states=rule.state, searchrule=rule) + return docs.filter(searchrule=rule) raise NotImplementedError + def community_list_rules_matching_doc(doc): + rules = SearchRule.objects.none() + if doc.type_id not in ["draft", "rfc"]: + return rules # none states = list(doc.states.values_list("pk", flat=True)) - rules = SearchRule.objects.none() - + # group and area rules if doc.group_id: groups = [doc.group_id] if doc.group.parent_id: groups.append(doc.group.parent_id) + rules_to_add = SearchRule.objects.filter(group__in=groups) + if doc.type_id == "rfc": + rules_to_add = rules_to_add.filter(rule_type__in=["group_rfc", "area_rfc"]) + else: + rules_to_add = rules_to_add.filter( + rule_type__in=["group", "area", "group_exp"], + state__in=states, + ) + rules |= rules_to_add + + # state rules (only relevant for I-Ds) + if doc.type_id == "draft": rules |= SearchRule.objects.filter( - rule_type__in=['group', 'area', 'group_rfc', 'area_rfc', 'group_exp'], + rule_type__in=[ + "state_iab", + "state_iana", + "state_iesg", + "state_irtf", + "state_ise", + "state_rfceditor", + "state_ietf", + ], state__in=states, - group__in=groups ) - rules |= SearchRule.objects.filter( - rule_type__in=['state_iab', 'state_iana', 'state_iesg', 'state_irtf', 'state_ise', 'state_rfceditor', 'state_ietf'], - state__in=states, - ) - - rules |= SearchRule.objects.filter( - rule_type__in=["author", "author_rfc"], - state__in=states, - person__in=list(Person.objects.filter(documentauthor__document=doc)), - ) - - if doc.ad_id: + # author rules + if doc.type_id == "rfc": rules |= SearchRule.objects.filter( - rule_type="ad", + rule_type="author_rfc", + person__in=list(Person.objects.filter(documentauthor__document=doc)), + ) + else: + rules |= SearchRule.objects.filter( + rule_type="author", state__in=states, - person=doc.ad_id, + person__in=list(Person.objects.filter(documentauthor__document=doc)), ) - if doc.shepherd_id: - rules |= SearchRule.objects.filter( - rule_type="shepherd", - state__in=states, - person__email=doc.shepherd_id, - ) + # Other draft-only rules rules + if doc.type_id == "draft": + if doc.ad_id: + rules |= SearchRule.objects.filter( + rule_type="ad", + state__in=states, + person=doc.ad_id, + ) - rules |= SearchRule.objects.filter( - rule_type="name_contains", - state__in=states, - name_contains_index=doc, # search our materialized index to avoid full scan - ) + if doc.shepherd_id: + rules |= SearchRule.objects.filter( + rule_type="shepherd", + state__in=states, + person__email=doc.shepherd_id, + ) + + rules |= SearchRule.objects.filter( + rule_type="name_contains", + state__in=states, + name_contains_index=doc, # search our materialized index to avoid full scan + ) return rules @@ -146,7 +179,11 @@ def docs_tracked_by_community_list(clist): # in theory, we could use an OR query, but databases seem to have # trouble with OR queries and complicated joins so do the OR'ing # manually - doc_ids = set(clist.added_docs.values_list("pk", flat=True)) + doc_ids = set() + for doc in clist.added_docs.all(): + doc_ids.add(doc.pk) + doc_ids.update(alias.docs.first().pk for alias in doc.related_that_doc("became_rfc")) + for rule in clist.searchrule_set.all(): doc_ids = doc_ids | set(docs_matching_community_list_rule(rule).values_list("pk", flat=True)) diff --git a/ietf/community/views.py b/ietf/community/views.py index 1dbbfcaf0..884ba1203 100644 --- a/ietf/community/views.py +++ b/ietf/community/views.py @@ -79,19 +79,18 @@ def manage_list(request, username=None, acronym=None, group_type=None): rule_type_form = SearchRuleTypeForm(request.POST) if rule_type_form.is_valid(): rule_type = rule_type_form.cleaned_data['rule_type'] - - if rule_type: - rule_form = SearchRuleForm(clist, rule_type, request.POST) - if rule_form.is_valid(): - if clist.pk is None: - clist.save() - - rule = rule_form.save(commit=False) - rule.community_list = clist - rule.rule_type = rule_type - rule.save() - if rule.rule_type == "name_contains": - reset_name_contains_index_for_rule(rule) + if rule_type: + rule_form = SearchRuleForm(clist, rule_type, request.POST) + if rule_form.is_valid(): + if clist.pk is None: + clist.save() + + rule = rule_form.save(commit=False) + rule.community_list = clist + rule.rule_type = rule_type + rule.save() + if rule.rule_type == "name_contains": + reset_name_contains_index_for_rule(rule) return HttpResponseRedirect("") else: diff --git a/ietf/doc/admin.py b/ietf/doc/admin.py index 64b9d9eff..3ebce516c 100644 --- a/ietf/doc/admin.py +++ b/ietf/doc/admin.py @@ -43,6 +43,7 @@ class DocActionHolderInline(admin.TabularInline): class RelatedDocumentInline(admin.TabularInline): model = RelatedDocument + fk_name= 'source' def this(self, instance): return instance.source.canonical_name() readonly_fields = ['this', ] @@ -125,7 +126,7 @@ admin.site.register(DocReminder, DocReminderAdmin) class RelatedDocumentAdmin(admin.ModelAdmin): list_display = ['source', 'target', 'relationship', ] list_filter = ['relationship', ] - search_fields = ['source__name', 'target__name', 'target__docs__name', ] + search_fields = ['source__name', 'target__name', ] raw_id_fields = ['source', 'target', ] admin.site.register(RelatedDocument, RelatedDocumentAdmin) diff --git a/ietf/doc/factories.py b/ietf/doc/factories.py index a8c20b6bb..ce3adcb2a 100644 --- a/ietf/doc/factories.py +++ b/ietf/doc/factories.py @@ -82,13 +82,7 @@ class BaseDocumentFactory(factory.django.DjangoModelFactory): def relations(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument if create and extracted: for (rel_id, doc) in extracted: - if isinstance(doc, Document): - docalias = doc.docalias.first() - elif isinstance(doc, DocAlias): - docalias = doc - else: - continue - obj.relateddocument_set.create(relationship_id=rel_id, target=docalias) + obj.relateddocument_set.create(relationship_id=rel_id, target=doc) @factory.post_generation def create_revisions(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument @@ -118,6 +112,29 @@ class DocumentFactory(BaseDocumentFactory): group = factory.SubFactory('ietf.group.factories.GroupFactory',acronym='none') +class RfcFactory(BaseDocumentFactory): + type_id = "rfc" + rfc_number = factory.Sequence(lambda n: n + 1000) + name = factory.LazyAttribute(lambda o: f"rfc{o.rfc_number:d}") + expires = None + + @factory.post_generation + def states(obj, create, extracted, **kwargs): + if not create: + return + if extracted: + for (state_type_id,state_slug) in extracted: + obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug)) + else: + obj.set_state(State.objects.get(type_id='rfc',slug='published')) + + @factory.post_generation + def reset_canonical_name(obj, create, extracted, **kwargs): + if hasattr(obj, '_canonical_name'): + del obj._canonical_name + return None + + class IndividualDraftFactory(BaseDocumentFactory): type_id = 'draft' @@ -136,28 +153,11 @@ class IndividualDraftFactory(BaseDocumentFactory): obj.set_state(State.objects.get(type_id='draft',slug='active')) obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists')) -class IndividualRfcFactory(IndividualDraftFactory): +class IndividualRfcFactory(RfcFactory): + group = factory.SubFactory('ietf.group.factories.GroupFactory',acronym='none') - alias2 = factory.RelatedFactory('ietf.doc.factories.DocAliasFactory','document',name=factory.Sequence(lambda n: 'rfc%04d'%(n+1000))) - - @factory.post_generation - def states(obj, create, extracted, **kwargs): - if not create: - return - if extracted: - for (state_type_id,state_slug) in extracted: - obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug)) - else: - obj.set_state(State.objects.get(type_id='draft',slug='rfc')) - - @factory.post_generation - def reset_canonical_name(obj, create, extracted, **kwargs): - if hasattr(obj, '_canonical_name'): - del obj._canonical_name - return None class WgDraftFactory(BaseDocumentFactory): - type_id = 'draft' group = factory.SubFactory('ietf.group.factories.GroupFactory',type_id='wg') stream_id = 'ietf' @@ -176,30 +176,12 @@ class WgDraftFactory(BaseDocumentFactory): obj.set_state(State.objects.get(type_id='draft-stream-ietf',slug='wg-doc')) obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists')) -class WgRfcFactory(WgDraftFactory): - - alias2 = factory.RelatedFactory('ietf.doc.factories.DocAliasFactory','document',name=factory.Sequence(lambda n: 'rfc%04d'%(n+1000))) +class WgRfcFactory(RfcFactory): + group = factory.SubFactory('ietf.group.factories.GroupFactory',type_id='wg') + stream_id = 'ietf' std_level_id = 'ps' - @factory.post_generation - def states(obj, create, extracted, **kwargs): - if not create: - return - if extracted: - for (state_type_id,state_slug) in extracted: - obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug)) - if not obj.get_state('draft-iesg'): - obj.set_state(State.objects.get(type_id='draft-iesg', slug='pub')) - else: - obj.set_state(State.objects.get(type_id='draft',slug='rfc')) - obj.set_state(State.objects.get(type_id='draft-iesg', slug='pub')) - - @factory.post_generation - def reset_canonical_name(obj, create, extracted, **kwargs): - if hasattr(obj, '_canonical_name'): - del obj._canonical_name - return None class RgDraftFactory(BaseDocumentFactory): @@ -222,34 +204,11 @@ class RgDraftFactory(BaseDocumentFactory): obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists')) -class RgRfcFactory(RgDraftFactory): - - alias2 = factory.RelatedFactory('ietf.doc.factories.DocAliasFactory','document',name=factory.Sequence(lambda n: 'rfc%04d'%(n+1000))) - +class RgRfcFactory(RfcFactory): + group = factory.SubFactory('ietf.group.factories.GroupFactory',type_id='rg') + stream_id = 'irtf' std_level_id = 'inf' - @factory.post_generation - def states(obj, create, extracted, **kwargs): - if not create: - return - if extracted: - for (state_type_id,state_slug) in extracted: - obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug)) - if not obj.get_state('draft-stream-irtf'): - obj.set_state(State.objects.get(type_id='draft-stream-irtf', slug='pub')) - if not obj.get_state('draft-iesg'): - obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists')) - else: - obj.set_state(State.objects.get(type_id='draft',slug='rfc')) - obj.set_state(State.objects.get(type_id='draft-stream-irtf', slug='pub')) - obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists')) - - @factory.post_generation - def reset_canonical_name(obj, create, extracted, **kwargs): - if hasattr(obj, '_canonical_name'): - del obj._canonical_name - return None - class CharterFactory(BaseDocumentFactory): @@ -278,7 +237,7 @@ class StatusChangeFactory(BaseDocumentFactory): for (rel, target) in extracted: obj.relateddocument_set.create(relationship_id=rel,target=target) else: - obj.relateddocument_set.create(relationship_id='tobcp', target=WgRfcFactory().docalias.first()) + obj.relateddocument_set.create(relationship_id='tobcp', target=WgRfcFactory()) @factory.post_generation def states(obj, create, extracted, **kwargs): @@ -305,9 +264,9 @@ class ConflictReviewFactory(BaseDocumentFactory): if not create: return if extracted: - obj.relateddocument_set.create(relationship_id='conflrev',target=extracted.docalias.first()) + obj.relateddocument_set.create(relationship_id='conflrev',target=extracted) else: - obj.relateddocument_set.create(relationship_id='conflrev',target=DocumentFactory(name=obj.name.replace('conflict-review-','draft-'),type_id='draft',group=Group.objects.get(type_id='individ')).docalias.first()) + obj.relateddocument_set.create(relationship_id='conflrev',target=DocumentFactory(name=obj.name.replace('conflict-review-','draft-'),type_id='draft',group=Group.objects.get(type_id='individ'))) @factory.post_generation @@ -550,33 +509,8 @@ class EditorialDraftFactory(BaseDocumentFactory): obj.set_state(State.objects.get(type_id='draft-stream-editorial',slug='active')) obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists')) -class EditorialRfcFactory(RgDraftFactory): - - alias2 = factory.RelatedFactory('ietf.doc.factories.DocAliasFactory','document',name=factory.Sequence(lambda n: 'rfc%04d'%(n+1000))) - - std_level_id = 'inf' - - @factory.post_generation - def states(obj, create, extracted, **kwargs): - if not create: - return - if extracted: - for (state_type_id,state_slug) in extracted: - obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug)) - if not obj.get_state('draft-stream-editorial'): - obj.set_state(State.objects.get(type_id='draft-stream-editorial', slug='pub')) - if not obj.get_state('draft-iesg'): - obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists')) - else: - obj.set_state(State.objects.get(type_id='draft',slug='rfc')) - obj.set_state(State.objects.get(type_id='draft-stream-editorial', slug='pub')) - obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists')) - - @factory.post_generation - def reset_canonical_name(obj, create, extracted, **kwargs): - if hasattr(obj, '_canonical_name'): - del obj._canonical_name - return None +class EditorialRfcFactory(RgRfcFactory): + pass class StatementFactory(BaseDocumentFactory): type_id = "statement" diff --git a/ietf/doc/feeds.py b/ietf/doc/feeds.py index c5bb467e9..4f49aec66 100644 --- a/ietf/doc/feeds.py +++ b/ietf/doc/feeds.py @@ -224,8 +224,8 @@ class RfcFeed(Feed): extra.update({"dcterms_accessRights": "gratis"}) extra.update({"dcterms_format": "text/html"}) media_contents = [] - if int(item.rfc_number()) < 8650: - if int(item.rfc_number()) not in [8, 9, 51, 418, 500, 530, 589]: + if item.rfc_number < 8650: + if item.rfc_number not in [8, 9, 51, 418, 500, 530, 589]: for fmt, media_type in [("txt", "text/plain"), ("html", "text/html")]: media_contents.append( { @@ -234,7 +234,7 @@ class RfcFeed(Feed): "is_format_of": self.item_link(item), } ) - if int(item.rfc_number()) not in [571, 587]: + if item.rfc_number not in [571, 587]: media_contents.append( { "url": f"https://www.rfc-editor.org/rfc/pdfrfc/{item.canonical_name()}.txt.pdf", diff --git a/ietf/doc/forms.py b/ietf/doc/forms.py index c0c52571c..daf02e8ac 100644 --- a/ietf/doc/forms.py +++ b/ietf/doc/forms.py @@ -8,7 +8,7 @@ from django import forms from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.validators import validate_email -from ietf.doc.fields import SearchableDocAliasesField, SearchableDocAliasField +from ietf.doc.fields import SearchableDocumentField, SearchableDocumentsField from ietf.doc.models import RelatedDocument, DocExtResource from ietf.iesg.models import TelechatDate from ietf.iesg.utils import telechat_page_count @@ -134,11 +134,11 @@ class ActionHoldersForm(forms.Form): IESG_APPROVED_STATE_LIST = ("ann", "rfcqueue", "pub") class AddDownrefForm(forms.Form): - rfc = SearchableDocAliasField( + rfc = SearchableDocumentField( label="Referenced RFC", help_text="The RFC that is approved for downref", required=True) - drafts = SearchableDocAliasesField( + drafts = SearchableDocumentsField( label="Internet-Drafts that makes the reference", help_text="The Internet-Drafts that approve the downref in their Last Call", required=True) @@ -148,7 +148,7 @@ class AddDownrefForm(forms.Form): raise forms.ValidationError("Please provide a referenced RFC and a referencing Internet-Draft") rfc = self.cleaned_data['rfc'] - if not rfc.document.is_rfc(): + if rfc.type_id != "rfc": raise forms.ValidationError("Cannot find the RFC: " + rfc.name) return rfc @@ -158,10 +158,10 @@ class AddDownrefForm(forms.Form): v_err_names = [] drafts = self.cleaned_data['drafts'] - for da in drafts: - state = da.document.get_state("draft-iesg") + for d in drafts: + state = d.get_state("draft-iesg") if not state or state.slug not in IESG_APPROVED_STATE_LIST: - v_err_names.append(da.name) + v_err_names.append(d.name) if v_err_names: raise forms.ValidationError("Internet-Draft is not yet approved: " + ", ".join(v_err_names)) return drafts @@ -173,23 +173,23 @@ class AddDownrefForm(forms.Form): v_err_pairs = [] rfc = self.cleaned_data['rfc'] drafts = self.cleaned_data['drafts'] - for da in drafts: - if RelatedDocument.objects.filter(source=da.document, target=rfc, relationship_id='downref-approval'): - v_err_pairs.append(da.name + " --> RFC " + rfc.document.rfc_number()) + for d in drafts: + if RelatedDocument.objects.filter(source=d, target=rfc, relationship_id='downref-approval'): + v_err_pairs.append(f"{d.name} --> RFC {rfc.rfc_number}") if v_err_pairs: raise forms.ValidationError("Downref is already in the registry: " + ", ".join(v_err_pairs)) if 'save_downref_anyway' not in self.data: # this check is skipped if the save_downref_anyway button is used v_err_refnorm = "" - for da in drafts: - if not RelatedDocument.objects.filter(source=da.document, target=rfc, relationship_id='refnorm'): + for d in drafts: + if not RelatedDocument.objects.filter(source=d, target=rfc, relationship_id='refnorm'): if v_err_refnorm: - v_err_refnorm = v_err_refnorm + " or " + da.name + v_err_refnorm = v_err_refnorm + " or " + d.name else: - v_err_refnorm = da.name + v_err_refnorm = d.name if v_err_refnorm: - v_err_refnorm_prefix = "There does not seem to be a normative reference to RFC " + rfc.document.rfc_number() + " by " + v_err_refnorm_prefix = f"There does not seem to be a normative reference to RFC {rfc.rfc_number} by " raise forms.ValidationError(v_err_refnorm_prefix + v_err_refnorm) diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 8f5d0eb67..9a9447c7f 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -54,7 +54,7 @@ def email_ad_approved_doc(request, doc, text): def email_ad_approved_conflict_review(request, review, ok_to_publish): """Email notification when AD approves a conflict review""" - conflictdoc = review.relateddocument_set.get(relationship__slug='conflrev').target.document + conflictdoc = review.relateddocument_set.get(relationship__slug='conflrev').target (to, cc) = gather_address_lists("ad_approved_conflict_review") frm = request.user.person.formatted_email() send_mail(request, diff --git a/ietf/doc/management/commands/generate_draft_aliases.py b/ietf/doc/management/commands/generate_draft_aliases.py index 88f4aa98c..b377fdda1 100755 --- a/ietf/doc/management/commands/generate_draft_aliases.py +++ b/ietf/doc/management/commands/generate_draft_aliases.py @@ -24,6 +24,7 @@ from ietf.doc.models import Document from ietf.group.utils import get_group_role_emails, get_group_ad_emails from ietf.utils.aliases import dump_sublist from utils.mail import parseaddr +from ietf.utils import log DEFAULT_YEARS = 2 @@ -120,16 +121,18 @@ class Command(BaseCommand): vfile.write("%s anything\n" % settings.DRAFT_VIRTUAL_DOMAIN) # Internet-Drafts with active status or expired within DEFAULT_YEARS - drafts = Document.objects.filter(name__startswith='draft-') + drafts = Document.objects.filter(type_id="draft") active_drafts = drafts.filter(states__slug='active') inactive_recent_drafts = drafts.exclude(states__slug='active').filter(expires__gte=show_since) interesting_drafts = active_drafts | inactive_recent_drafts alias_domains = ['ietf.org', ] for draft in interesting_drafts.distinct().iterator(): - # Omit RFCs, unless they were published in the last DEFAULT_YEARS - if draft.docalias.filter(name__startswith='rfc'): - if draft.latest_event(type='published_rfc').time < show_since: + # Omit drafts that became RFCs, unless they were published in the last DEFAULT_YEARS + if draft.get_state_slug()=="rfc": + rfc = next(iter(draft.related_that_doc("became_rfc")), None) + log.assertion("rfc is not None") + if rfc.latest_event(type='published_rfc').time < show_since: continue alias = draft.name diff --git a/ietf/doc/migrations/0005_add_rfc_states.py b/ietf/doc/migrations/0005_add_rfc_states.py new file mode 100644 index 000000000..01d672581 --- /dev/null +++ b/ietf/doc/migrations/0005_add_rfc_states.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.2 on 2023-06-14 20:57 + +from django.db import migrations + + +def forward(apps, schema_editor): + StateType = apps.get_model("doc", "StateType") + rfc_statetype, _ = StateType.objects.get_or_create(slug="rfc", label="State") + + State = apps.get_model("doc", "State") + State.objects.get_or_create( + type=rfc_statetype, slug="published", name="Published", used=True, order=1 + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("doc", "0004_alter_dochistory_ad_alter_dochistory_shepherd_and_more"), + ] + + operations = [ + migrations.RunPython(forward), + ] diff --git a/ietf/doc/migrations/0006_dochistory_rfc_number_document_rfc_number.py b/ietf/doc/migrations/0006_dochistory_rfc_number_document_rfc_number.py new file mode 100644 index 000000000..efb77ce70 --- /dev/null +++ b/ietf/doc/migrations/0006_dochistory_rfc_number_document_rfc_number.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.2 on 2023-06-14 22:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("doc", "0005_add_rfc_states"), + ] + + operations = [ + migrations.AddField( + model_name="dochistory", + name="rfc_number", + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="document", + name="rfc_number", + field=models.PositiveIntegerField(blank=True, null=True), + ), + ] diff --git a/ietf/doc/migrations/0007_create_rfc_documents.py b/ietf/doc/migrations/0007_create_rfc_documents.py new file mode 100644 index 000000000..c11dcc291 --- /dev/null +++ b/ietf/doc/migrations/0007_create_rfc_documents.py @@ -0,0 +1,79 @@ +# Generated by Django 4.2.2 on 2023-06-15 15:27 + +from django.db import migrations + + +def forward(apps, schema_editor): + Document = apps.get_model("doc", "Document") + DocAlias = apps.get_model("doc", "DocAlias") + DocumentAuthor = apps.get_model("doc", "DocumentAuthor") + + State = apps.get_model("doc", "State") + draft_rfc_state = State.objects.get(type_id="draft", slug="rfc") + rfc_published_state = State.objects.get(type_id="rfc", slug="published") + + DocTypeName = apps.get_model("name", "DocTypeName") + rfc_doctype = DocTypeName(slug="rfc") + + # Find draft Documents in the "rfc" state + found_by_state = Document.objects.filter(states=draft_rfc_state).distinct() + + # Find Documents with an "rfc..." alias and confirm they're the same set + rfc_docaliases = DocAlias.objects.filter(name__startswith="rfc") + found_by_name = Document.objects.filter(docalias__in=rfc_docaliases).distinct() + assert set(found_by_name) == set(found_by_state), "mismatch between rfcs identified by state and docalias" + + # As of 2023-06-15, there is one Document with two rfc aliases: rfc6312 and rfc6342 are the same Document. This + # was due to a publication error. Because we go alias-by-alias, no special handling is needed in this migration. + + for rfc_alias in rfc_docaliases.order_by("name"): + assert rfc_alias.docs.count() == 1, f"DocAlias {rfc_alias} is linked to more than 1 Document" + draft = rfc_alias.docs.first() + if draft.name.startswith("rfc"): + rfc = draft + rfc.type = rfc_doctype + rfc.rfc_number = int(draft.name[3:]) + rfc.save() + rfc.states.set([rfc_published_state]) + else: + rfc = Document.objects.create( + type=rfc_doctype, + name=rfc_alias.name, + rfc_number=int(rfc_alias.name[3:]), + time=draft.time, + title=draft.title, + stream=draft.stream, + group=draft.group, + abstract=draft.abstract, + pages=draft.pages, + words=draft.words, + std_level=draft.std_level, + ad=draft.ad, + external_url=draft.external_url, + uploaded_filename=draft.uploaded_filename, + note=draft.note, + ) + rfc.states.set([rfc_published_state]) + rfc.formal_languages.set(draft.formal_languages.all()) + + # Copy Authors + for da in draft.documentauthor_set.all(): + DocumentAuthor.objects.create( + document=rfc, + person=da.person, + email=da.email, + affiliation=da.affiliation, + country=da.country, + order=da.order, + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("doc", "0006_dochistory_rfc_number_document_rfc_number"), + ("name", "0004_rfc_doctype_names"), + ] + + operations = [ + migrations.RunPython(forward), + ] diff --git a/ietf/doc/migrations/0008_move_rfc_docevents.py b/ietf/doc/migrations/0008_move_rfc_docevents.py new file mode 100644 index 000000000..6b6730795 --- /dev/null +++ b/ietf/doc/migrations/0008_move_rfc_docevents.py @@ -0,0 +1,98 @@ +# Generated by Django 4.2.2 on 2023-06-20 18:36 + +from django.db import migrations +from django.db.models import Q + + +def forward(apps, schema_editor): + """Move RFC events from the draft to the rfc Document""" + DocAlias = apps.get_model("doc", "DocAlias") + DocEvent = apps.get_model("doc", "DocEvent") + Document = apps.get_model("doc", "Document") + + # queryset with events migrated regardless of whether before or after the "published_rfc" event + events_always_migrated = DocEvent.objects.filter( + Q( + type__in=[ + "published_rfc", # do not remove this one! + "sync_from_rfc_editor", + "rfc_editor_received_announcement", # problematic for new RFCs until RPC tools enhancements come in? + ] + ) + | Q( + type="changed_state", + desc__startswith="RFC Editor state", + ) + | Q( + type="changed_state", + desc__startswith="IANA Action state", + ) + ) + + # queryset with events migrated only after the "published_rfc" event + events_migrated_after_pub = DocEvent.objects.exclude( + type__in=[ + "created_ballot", + "closed_ballot", + "sent_ballot_announcement", + "changed_ballot_position", + "changed_ballot_approval_text", + "changed_ballot_writeup_text", + ] + ).exclude( + type="added_comment", + desc__contains="ballot set", # excludes 311 comments that all apply to drafts + ) + + # special case for rfc 6312/6342 draft, which has two published_rfc events + ignore = ["rfc6312", "rfc6342"] # do not reprocess these later + rfc6312 = Document.objects.get(name="rfc6312") + rfc6342 = Document.objects.get(name="rfc6342") + draft = DocAlias.objects.get(name="rfc6312").docs.first() + assert draft == DocAlias.objects.get(name="rfc6342").docs.first() + published_events = list( + DocEvent.objects.filter(doc=draft, type="published_rfc").order_by("time") + ) + assert len(published_events) == 2 + ( + pub_event_6312, + pub_event_6342, + ) = published_events # order matches pub dates at rfc-editor.org + + pub_event_6312.doc = rfc6312 + pub_event_6312.save() + events_migrated_after_pub.filter( + doc=draft, + time__gte=pub_event_6312.time, + time__lt=pub_event_6342.time, + ).update(doc=rfc6312) + + pub_event_6342.doc = rfc6342 + pub_event_6342.save() + events_migrated_after_pub.filter( + doc=draft, + time__gte=pub_event_6342.time, + ).update(doc=rfc6342) + + # Now handle all the rest + for rfc in Document.objects.filter(type_id="rfc").exclude(name__in=ignore): + draft = DocAlias.objects.get(name=rfc.name).docs.first() + assert draft is not None + published_event = DocEvent.objects.get(doc=draft, type="published_rfc") + events_always_migrated.filter( + doc=draft, + ).update(doc=rfc) + events_migrated_after_pub.filter( + doc=draft, + time__gte=published_event.time, + ).update(doc=rfc) + + +class Migration(migrations.Migration): + dependencies = [ + ("doc", "0007_create_rfc_documents"), + ] + + operations = [ + migrations.RunPython(forward), + ] diff --git a/ietf/doc/migrations/0009_rfc_relateddocuments.py b/ietf/doc/migrations/0009_rfc_relateddocuments.py new file mode 100644 index 000000000..a00de56d6 --- /dev/null +++ b/ietf/doc/migrations/0009_rfc_relateddocuments.py @@ -0,0 +1,45 @@ +# Generated by Django 4.2.3 on 2023-07-05 22:40 + +from django.db import migrations + + +def forward(apps, schema_editor): + DocAlias = apps.get_model("doc", "DocAlias") + Document = apps.get_model("doc", "Document") + RelatedDocument = apps.get_model("doc", "RelatedDocument") + for rfc_alias in DocAlias.objects.filter(name__startswith="rfc").exclude( + docs__type__slug="rfc" + ): + # Move these over to the RFC + RelatedDocument.objects.filter( + relationship__slug__in=( + "tobcp", + "toexp", + "tohist", + "toinf", + "tois", + "tops", + "obs", + "updates", + ), + source__docalias=rfc_alias, + ).update(source=Document.objects.get(name=rfc_alias.name)) + # Duplicate references on the RFC but keep the ones on the draft as well + originals = list( + RelatedDocument.objects.filter( + relationship__slug__in=("refinfo", "refnorm", "refold", "refunk"), + source__docalias=rfc_alias, + ) + ) + for o in originals: + o.pk = None + o.source = Document.objects.get(name=rfc_alias.name) + RelatedDocument.objects.bulk_create(originals) + + +class Migration(migrations.Migration): + dependencies = [ + ("doc", "0008_move_rfc_docevents"), + ] + + operations = [migrations.RunPython(forward)] diff --git a/ietf/doc/migrations/0010_move_rfc_docaliases.py b/ietf/doc/migrations/0010_move_rfc_docaliases.py new file mode 100644 index 000000000..af12c26a1 --- /dev/null +++ b/ietf/doc/migrations/0010_move_rfc_docaliases.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.2 on 2023-06-20 18:36 + +from django.db import migrations + + +def forward(apps, schema_editor): + """Point "rfc..." DocAliases at the rfc-type Document + + Creates a became_rfc RelatedDocument to preserve the connection between the draft and the rfc. + """ + DocAlias = apps.get_model("doc", "DocAlias") + Document = apps.get_model("doc", "Document") + RelatedDocument = apps.get_model("doc", "RelatedDocument") + + for rfc_alias in DocAlias.objects.filter(name__startswith="rfc"): + rfc = Document.objects.get(name=rfc_alias.name) + aliased_doc = rfc_alias.docs.get() # implicitly confirms only one value in rfc_alias.docs + if aliased_doc != rfc: + # If the DocAlias was not already pointing at the rfc, it was pointing at the draft + # it came from. Create the relationship between draft and rfc Documents. + assert aliased_doc.type_id == "draft", f"Alias for {rfc.name} should be pointing at a draft" + RelatedDocument.objects.create( + source=aliased_doc, + target=rfc_alias, + relationship_id="became_rfc", + ) + # Now move the alias from the draft to the rfc + rfc_alias.docs.set([rfc]) + + +class Migration(migrations.Migration): + dependencies = [ + ("doc", "0009_rfc_relateddocuments"), + ] + + operations = [ + migrations.RunPython(forward), + ] diff --git a/ietf/doc/migrations/0011_relate_no_aliases.py b/ietf/doc/migrations/0011_relate_no_aliases.py new file mode 100644 index 000000000..2eedcdf7c --- /dev/null +++ b/ietf/doc/migrations/0011_relate_no_aliases.py @@ -0,0 +1,77 @@ +# Generated by Django 4.2.2 on 2023-06-16 13:40 + +from django.db import migrations +import django.db.models.deletion +from django.db.models import F, Subquery, OuterRef +import ietf.utils.models + +def forward(apps, schema_editor): + RelatedDocument = apps.get_model("doc", "RelatedDocument") + DocAlias = apps.get_model("doc", "DocAlias") + subquery = Subquery(DocAlias.objects.filter(pk=OuterRef("deprecated_target")).values("docs")[:1]) + RelatedDocument.objects.annotate(firstdoc=subquery).update(target=F("firstdoc")) + +def reverse(apps, schema_editor): + pass + +class Migration(migrations.Migration): + dependencies = [ + ("doc", "0010_move_rfc_docaliases"), + ] + + operations = [ + migrations.AlterField( + model_name='relateddocument', + name='target', + field=ietf.utils.models.ForeignKey( + db_index=False, + on_delete=django.db.models.deletion.CASCADE, + to='doc.docalias', + ), + ), + migrations.RenameField( + model_name="relateddocument", + old_name="target", + new_name="deprecated_target" + ), + migrations.AlterField( + model_name='relateddocument', + name='deprecated_target', + field=ietf.utils.models.ForeignKey( + db_index=True, + on_delete=django.db.models.deletion.CASCADE, + to='doc.docalias', + ), + ), + migrations.AddField( + model_name="relateddocument", + name="target", + field=ietf.utils.models.ForeignKey( + default=1, # A lie, but a convenient one - no relations point here. + on_delete=django.db.models.deletion.CASCADE, + related_name="targets_related", + to="doc.document", + db_index=False, + ), + preserve_default=False, + ), + migrations.RunPython(forward, reverse), + migrations.AlterField( + model_name="relateddocument", + name="target", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="targets_related", + to="doc.document", + db_index=True, + ), + ), + migrations.RemoveField( + model_name="relateddocument", + name="deprecated_target", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='doc.DocAlias', + ), + ), + ] diff --git a/ietf/doc/migrations/0012_relate_hist_no_aliases.py b/ietf/doc/migrations/0012_relate_hist_no_aliases.py new file mode 100644 index 000000000..e4317795e --- /dev/null +++ b/ietf/doc/migrations/0012_relate_hist_no_aliases.py @@ -0,0 +1,80 @@ +# Generated by Django 4.2.2 on 2023-06-16 13:40 + +from django.db import migrations +import django.db.models.deletion +from django.db.models import F, Subquery, OuterRef +import ietf.utils.models + +def forward(apps, schema_editor): + RelatedDocHistory = apps.get_model("doc", "RelatedDocHistory") + DocAlias = apps.get_model("doc", "DocAlias") + subquery = Subquery(DocAlias.objects.filter(pk=OuterRef("deprecated_target")).values("docs")[:1]) + RelatedDocHistory.objects.annotate(firstdoc=subquery).update(target=F("firstdoc")) + +def reverse(apps, schema_editor): + pass + +class Migration(migrations.Migration): + dependencies = [ + ("doc", "0011_relate_no_aliases"), + ] + + operations = [ + migrations.AlterField( + model_name='relateddochistory', + name='target', + field=ietf.utils.models.ForeignKey( + db_index=False, + on_delete=django.db.models.deletion.CASCADE, + to='doc.docalias', + related_name='reversely_related_document_history_set', + ), + ), + migrations.RenameField( + model_name="relateddochistory", + old_name="target", + new_name="deprecated_target" + ), + migrations.AlterField( + model_name='relateddochistory', + name='deprecated_target', + field=ietf.utils.models.ForeignKey( + db_index=True, + on_delete=django.db.models.deletion.CASCADE, + to='doc.docalias', + related_name='deprecated_reversely_related_document_history_set', + ), + ), + migrations.AddField( + model_name="relateddochistory", + name="target", + field=ietf.utils.models.ForeignKey( + default=1, # A lie, but a convenient one - no relations point here. + on_delete=django.db.models.deletion.CASCADE, + to="doc.document", + db_index=False, + related_name='reversely_related_document_history_set', + ), + preserve_default=False, + ), + migrations.RunPython(forward, reverse), + migrations.AlterField( + model_name="relateddochistory", + name="target", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="doc.document", + db_index=True, + related_name='reversely_related_document_history_set', + ), + ), + migrations.RemoveField( + model_name="relateddochistory", + name="deprecated_target", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='doc.DocAlias', + related_name='deprecated_reversely_related_document_history_set', + ), + ), + ] diff --git a/ietf/doc/models.py b/ietf/doc/models.py index d48032676..324767ee3 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -124,6 +124,7 @@ class DocumentInfo(models.Model): uploaded_filename = models.TextField(blank=True) note = models.TextField(blank=True) internal_comments = models.TextField(blank=True) + rfc_number = models.PositiveIntegerField(blank=True, null=True) # only valid for type="rfc" def file_extension(self): if not hasattr(self, '_cached_extension'): @@ -136,18 +137,17 @@ class DocumentInfo(models.Model): def get_file_path(self): if not hasattr(self, '_cached_file_path'): - if self.type_id == "draft": + if self.type_id == "rfc": + self._cached_file_path = settings.RFC_PATH + elif self.type_id == "draft": if self.is_dochistory(): self._cached_file_path = settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR else: - if self.get_state_slug() == "rfc": - self._cached_file_path = settings.RFC_PATH + draft_state = self.get_state('draft') + if draft_state and draft_state.slug == 'active': + self._cached_file_path = settings.INTERNET_DRAFT_PATH else: - draft_state = self.get_state('draft') - if draft_state and draft_state.slug == 'active': - self._cached_file_path = settings.INTERNET_DRAFT_PATH - else: - self._cached_file_path = settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR + self._cached_file_path = settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR elif self.meeting_related() and self.type_id in ( "agenda", "minutes", "slides", "bluesheets", "procmaterials", "chatlog", "polls" ): @@ -172,14 +172,13 @@ class DocumentInfo(models.Model): if not hasattr(self, '_cached_base_name'): if self.uploaded_filename: self._cached_base_name = self.uploaded_filename + elif self.type_id == 'rfc': + self._cached_base_name = "%s.txt" % self.canonical_name() elif self.type_id == 'draft': if self.is_dochistory(): self._cached_base_name = "%s-%s.txt" % (self.doc.name, self.rev) else: - if self.get_state_slug() == 'rfc': - self._cached_base_name = "%s.txt" % self.canonical_name() - else: - self._cached_base_name = "%s-%s.txt" % (self.name, self.rev) + self._cached_base_name = "%s-%s.txt" % (self.name, self.rev) elif self.type_id in ["slides", "agenda", "minutes", "bluesheets", "procmaterials", ] and self.meeting_related(): ext = 'pdf' if self.type_id == 'procmaterials' else 'txt' self._cached_base_name = f'{self.canonical_name()}-{self.rev}.{ext}' @@ -244,7 +243,7 @@ class DocumentInfo(models.Model): format = settings.DOC_HREFS[self.type_id] elif self.type_id in settings.DOC_HREFS: self.is_meeting_related = False - if self.is_rfc(): + if self.type_id == "rfc": format = settings.DOC_HREFS['rfc'] else: format = settings.DOC_HREFS[self.type_id] @@ -334,7 +333,9 @@ class DocumentInfo(models.Model): if not state: return "Unknown state" - if self.type_id == 'draft': + if self.type_id == "rfc": + return f"RFC {self.rfc_number} ({self.std_level})" + elif self.type_id == 'draft': iesg_state = self.get_state("draft-iesg") iesg_state_summary = None if iesg_state: @@ -345,7 +346,12 @@ class DocumentInfo(models.Model): iesg_state_summary = iesg_state_summary + "::"+"::".join(tag.name for tag in iesg_substate) if state.slug == "rfc": - return "RFC %s (%s)" % (self.rfc_number(), self.std_level) + rfcs = self.related_that_doc("became_rfc") # should be only one + if len(rfcs) > 0: + rfc = rfcs[0].document + return f"Became RFC {rfc.rfc_number} ({rfc.std_level})" + else: + return "Became RFC" elif state.slug == "repl": rs = self.related_that("replaces") if rs: @@ -375,27 +381,6 @@ class DocumentInfo(models.Model): else: return state.name - def is_rfc(self): - if not hasattr(self, '_cached_is_rfc'): - self._cached_is_rfc = self.pk and self.type_id == 'draft' and self.states.filter(type='draft',slug='rfc').exists() - return self._cached_is_rfc - - def rfc_number(self): - if not hasattr(self, '_cached_rfc_number'): - self._cached_rfc_number = None - if self.is_rfc(): - n = self.canonical_name() - if n.startswith("rfc"): - self._cached_rfc_number = n[3:] - else: - if isinstance(self,Document): - logger.error("Document self.is_rfc() is True but self.canonical_name() is %s" % n) - return self._cached_rfc_number - - @property - def rfcnum(self): - return self.rfc_number() - def author_list(self): best_addresses = [] for author in self.documentauthor_set.all(): @@ -468,9 +453,9 @@ class DocumentInfo(models.Model): if not isinstance(relationship, tuple): raise TypeError("Expected a string or tuple, received %s" % type(relationship)) if isinstance(self, Document): - return RelatedDocument.objects.filter(target__docs=self, relationship__in=relationship).select_related('source') + return RelatedDocument.objects.filter(target=self, relationship__in=relationship).select_related('source') elif isinstance(self, DocHistory): - return RelatedDocHistory.objects.filter(target__docs=self.doc, relationship__in=relationship).select_related('source') + return RelatedDocHistory.objects.filter(target=self.doc, relationship__in=relationship).select_related('source') else: raise TypeError("Expected method called on Document or DocHistory") @@ -504,8 +489,7 @@ class DocumentInfo(models.Model): for r in rels: if not r in related: related += ( r, ) - for doc in r.target.docs.all(): - related = doc.all_relations_that_doc(relationship, related) + related = r.target.all_relations_that_doc(relationship, related) return related def related_that(self, relationship): @@ -656,10 +640,20 @@ class DocumentInfo(models.Model): return self.relations_that_doc(('refnorm','refinfo','refunk','refold')) def referenced_by(self): - return self.relations_that(('refnorm','refinfo','refunk','refold')).filter(source__states__type__slug='draft',source__states__slug__in=['rfc','active']) - + return self.relations_that(("refnorm", "refinfo", "refunk", "refold")).filter( + models.Q( + source__type__slug="draft", + source__states__type__slug="draft", + source__states__slug="active", + ) + | models.Q(source__type__slug="rfc") + ) + + def referenced_by_rfcs(self): - return self.relations_that(('refnorm','refinfo','refunk','refold')).filter(source__states__type__slug='draft',source__states__slug='rfc') + return self.relations_that(("refnorm", "refinfo", "refunk", "refold")).filter( + source__type__slug="rfc" + ) class Meta: abstract = True @@ -668,7 +662,7 @@ STATUSCHANGE_RELATIONS = ('tops','tois','tohist','toinf','tobcp','toexp') class RelatedDocument(models.Model): source = ForeignKey('Document') - target = ForeignKey('DocAlias') + target = ForeignKey('Document', related_name='targets_related') relationship = ForeignKey(DocRelationshipName) def action(self): return self.relationship.name @@ -691,16 +685,16 @@ class RelatedDocument(models.Model): if source_lvl not in ['bcp','ps','ds','std']: return None - if self.target.document.get_state().slug == 'rfc': - if not self.target.document.std_level: + if self.target.type_id == 'rfc': + if not self.target.std_level: target_lvl = 'unkn' else: - target_lvl = self.target.document.std_level.slug + target_lvl = self.target.std_level.slug else: - if not self.target.document.intended_std_level: + if not self.target.intended_std_level: target_lvl = 'unkn' else: - target_lvl = self.target.document.intended_std_level.slug + target_lvl = self.target.intended_std_level.slug rank = { 'ps':1, 'ds':2, 'std':3, 'bcp':3 } @@ -714,8 +708,8 @@ class RelatedDocument(models.Model): def is_approved_downref(self): - if self.target.document.get_state().slug == 'rfc': - if RelatedDocument.objects.filter(relationship_id='downref-approval', target=self.target): + if self.target.type_id == 'rfc': + if RelatedDocument.objects.filter(relationship_id='downref-approval', target=self.target).exists(): return "Approved Downref" return False @@ -856,12 +850,6 @@ class Document(DocumentInfo): a = self.docalias.filter(name__startswith="rfc").order_by('-name').first() if a: name = a.name - elif self.type_id == "charter": - from ietf.doc.utils_charter import charter_name_for_group # Imported locally to avoid circular imports - try: - name = charter_name_for_group(self.chartered_group) - except Group.DoesNotExist: - pass self._canonical_name = name return self._canonical_name @@ -973,7 +961,15 @@ class Document(DocumentInfo): document directly or indirectly obsoletes or replaces """ from ietf.ipr.models import IprDocRel - iprs = IprDocRel.objects.filter(document__in=list(self.docalias.all())+self.all_related_that_doc(('obs','replaces'))).filter(disclosure__state__in=('posted','removed')).values_list('disclosure', flat=True).distinct() + iprs = ( + IprDocRel.objects.filter( + document__in=list(self.docalias.all()) + + [x.docalias.first() for x in self.all_related_that_doc(("obs", "replaces"))] # this really is docalias until IprDocRel changes + ) + .filter(disclosure__state__in=("posted", "removed")) + .values_list("disclosure", flat=True) + .distinct() + ) return iprs def future_presentations(self): @@ -1010,7 +1006,7 @@ class Document(DocumentInfo): This is the rfc publication date for RFCs, and the new-revision date for other documents. """ - if self.get_state_slug() == "rfc": + if self.type_id == "rfc": # As of Sept 2022, in ietf.sync.rfceditor.update_docs_from_rfc_index() `published_rfc` events are # created with a timestamp whose date *in the PST8PDT timezone* is the official publication date # assigned by the RFC editor. @@ -1112,7 +1108,7 @@ class DocExtResource(ExtResource): class RelatedDocHistory(models.Model): source = ForeignKey('DocHistory') - target = ForeignKey('DocAlias', related_name="reversely_related_document_history_set") + target = ForeignKey('Document', related_name="reversely_related_document_history_set") relationship = ForeignKey(DocRelationshipName) def __str__(self): return u"%s %s %s" % (self.source.doc.name, self.relationship.name.lower(), self.target.name) diff --git a/ietf/doc/templatetags/ietf_filters.py b/ietf/doc/templatetags/ietf_filters.py index 1c5836328..e22912f95 100644 --- a/ietf/doc/templatetags/ietf_filters.py +++ b/ietf/doc/templatetags/ietf_filters.py @@ -288,8 +288,8 @@ def urlize_related_target_list(related, document_html=False): """Convert a list of RelatedDocuments into list of links using the target document's canonical name""" links = [] for rel in related: - name=rel.target.document.canonical_name() - title = rel.target.document.title + name=rel.target.canonical_name() + title = rel.target.title url = urlreverse('ietf.doc.views_doc.document_main' if document_html is False else 'ietf.doc.views_doc.document_html', kwargs=dict(name=name)) name = escape(name) title = escape(title) @@ -556,7 +556,7 @@ def consensus(doc): @register.filter def std_level_to_label_format(doc): """Returns valid Bootstrap classes to label a status level badge.""" - if doc.is_rfc(): + if doc.type_id == "rfc": if doc.related_that("obs"): return "obs" else: diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index 65fb80a15..9ebc2e6c0 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -630,23 +630,22 @@ Man Expires September 22, 2015 [Page 3] def test_document_draft(self): draft = WgDraftFactory(name='draft-ietf-mars-test',rev='01', create_revisions=range(0,2)) - HolderIprDisclosureFactory(docs=[draft]) # Docs for testing relationships. Does not test 'possibly-replaces'. The 'replaced_by' direction # is tested separately below. replaced = IndividualDraftFactory() - draft.relateddocument_set.create(relationship_id='replaces',source=draft,target=replaced.docalias.first()) + draft.relateddocument_set.create(relationship_id='replaces',source=draft,target=replaced) obsoleted = IndividualDraftFactory() - draft.relateddocument_set.create(relationship_id='obs',source=draft,target=obsoleted.docalias.first()) + draft.relateddocument_set.create(relationship_id='obs',source=draft,target=obsoleted) obsoleted_by = IndividualDraftFactory() - obsoleted_by.relateddocument_set.create(relationship_id='obs',source=obsoleted_by,target=draft.docalias.first()) + obsoleted_by.relateddocument_set.create(relationship_id='obs',source=obsoleted_by,target=draft) updated = IndividualDraftFactory() - draft.relateddocument_set.create(relationship_id='updates',source=draft,target=updated.docalias.first()) + draft.relateddocument_set.create(relationship_id='updates',source=draft,target=updated) updated_by = IndividualDraftFactory() - updated_by.relateddocument_set.create(relationship_id='updates',source=obsoleted_by,target=draft.docalias.first()) + updated_by.relateddocument_set.create(relationship_id='updates',source=obsoleted_by,target=draft) - external_resource = DocExtResourceFactory(doc=draft) + DocExtResourceFactory(doc=draft) # these tests aren't testing all attributes yet, feel free to # expand them @@ -659,16 +658,6 @@ Man Expires September 22, 2015 [Page 3] self.assertNotContains(r, "Deimos street") self.assertContains(r, replaced.canonical_name()) self.assertContains(r, replaced.title) - # obs/updates not included until draft is RFC - self.assertNotContains(r, obsoleted.canonical_name()) - self.assertNotContains(r, obsoleted.title) - self.assertNotContains(r, obsoleted_by.canonical_name()) - self.assertNotContains(r, obsoleted_by.title) - self.assertNotContains(r, updated.canonical_name()) - self.assertNotContains(r, updated.title) - self.assertNotContains(r, updated_by.canonical_name()) - self.assertNotContains(r, updated_by.title) - self.assertContains(r, external_resource.value) r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + "?include_text=0") self.assertEqual(r.status_code, 200) @@ -677,15 +666,6 @@ Man Expires September 22, 2015 [Page 3] self.assertNotContains(r, "Deimos street") self.assertContains(r, replaced.canonical_name()) self.assertContains(r, replaced.title) - # obs/updates not included until draft is RFC - self.assertNotContains(r, obsoleted.canonical_name()) - self.assertNotContains(r, obsoleted.title) - self.assertNotContains(r, obsoleted_by.canonical_name()) - self.assertNotContains(r, obsoleted_by.title) - self.assertNotContains(r, updated.canonical_name()) - self.assertNotContains(r, updated.title) - self.assertNotContains(r, updated_by.canonical_name()) - self.assertNotContains(r, updated_by.title) r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + "?include_text=foo") self.assertEqual(r.status_code, 200) @@ -694,15 +674,6 @@ Man Expires September 22, 2015 [Page 3] self.assertContains(r, "Deimos street") self.assertContains(r, replaced.canonical_name()) self.assertContains(r, replaced.title) - # obs/updates not included until draft is RFC - self.assertNotContains(r, obsoleted.canonical_name()) - self.assertNotContains(r, obsoleted.title) - self.assertNotContains(r, obsoleted_by.canonical_name()) - self.assertNotContains(r, obsoleted_by.title) - self.assertNotContains(r, updated.canonical_name()) - self.assertNotContains(r, updated.title) - self.assertNotContains(r, updated_by.canonical_name()) - self.assertNotContains(r, updated_by.title) r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + "?include_text=1") self.assertEqual(r.status_code, 200) @@ -711,15 +682,6 @@ Man Expires September 22, 2015 [Page 3] self.assertContains(r, "Deimos street") self.assertContains(r, replaced.canonical_name()) self.assertContains(r, replaced.title) - # obs/updates not included until draft is RFC - self.assertNotContains(r, obsoleted.canonical_name()) - self.assertNotContains(r, obsoleted.title) - self.assertNotContains(r, obsoleted_by.canonical_name()) - self.assertNotContains(r, obsoleted_by.title) - self.assertNotContains(r, updated.canonical_name()) - self.assertNotContains(r, updated.title) - self.assertNotContains(r, updated_by.canonical_name()) - self.assertNotContains(r, updated_by.title) self.client.cookies = SimpleCookie({str('full_draft'): str('on')}) r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) @@ -729,15 +691,6 @@ Man Expires September 22, 2015 [Page 3] self.assertContains(r, "Deimos street") self.assertContains(r, replaced.canonical_name()) self.assertContains(r, replaced.title) - # obs/updates not included until draft is RFC - self.assertNotContains(r, obsoleted.canonical_name()) - self.assertNotContains(r, obsoleted.title) - self.assertNotContains(r, obsoleted_by.canonical_name()) - self.assertNotContains(r, obsoleted_by.title) - self.assertNotContains(r, updated.canonical_name()) - self.assertNotContains(r, updated.title) - self.assertNotContains(r, updated_by.canonical_name()) - self.assertNotContains(r, updated_by.title) self.client.cookies = SimpleCookie({str('full_draft'): str('off')}) r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) @@ -747,15 +700,6 @@ Man Expires September 22, 2015 [Page 3] self.assertNotContains(r, "Deimos street") self.assertContains(r, replaced.canonical_name()) self.assertContains(r, replaced.title) - # obs/updates not included until draft is RFC - self.assertNotContains(r, obsoleted.canonical_name()) - self.assertNotContains(r, obsoleted.title) - self.assertNotContains(r, obsoleted_by.canonical_name()) - self.assertNotContains(r, obsoleted_by.title) - self.assertNotContains(r, updated.canonical_name()) - self.assertNotContains(r, updated.title) - self.assertNotContains(r, updated_by.canonical_name()) - self.assertNotContains(r, updated_by.title) self.client.cookies = SimpleCookie({str('full_draft'): str('foo')}) r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) @@ -766,15 +710,6 @@ Man Expires September 22, 2015 [Page 3] self.assertNotContains(r, "Deimos street") self.assertContains(r, replaced.canonical_name()) self.assertContains(r, replaced.title) - # obs/updates not included until draft is RFC - self.assertNotContains(r, obsoleted.canonical_name()) - self.assertNotContains(r, obsoleted.title) - self.assertNotContains(r, obsoleted_by.canonical_name()) - self.assertNotContains(r, obsoleted_by.title) - self.assertNotContains(r, updated.canonical_name()) - self.assertNotContains(r, updated.title) - self.assertNotContains(r, updated_by.canonical_name()) - self.assertNotContains(r, updated_by.title) r = self.client.get(urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=draft.name))) self.assertEqual(r.status_code, 200) @@ -803,12 +738,12 @@ Man Expires September 22, 2015 [Page 3] r = self.client.get(urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=rfc.canonical_name()))) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertEqual(q('title').text(), f'RFC {rfc.rfc_number()} - {rfc.title}') + self.assertEqual(q('title').text(), f'RFC {rfc.rfc_number} - {rfc.title}') # synonyms for the rfc should be redirected to its canonical view - r = self.client.get(urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=rfc.rfc_number()))) + r = self.client.get(urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=rfc.rfc_number))) self.assertRedirects(r, urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=rfc.canonical_name()))) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=f'RFC {rfc.rfc_number()}'))) + r = self.client.get(urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=f'RFC {rfc.rfc_number}'))) self.assertRedirects(r, urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=rfc.canonical_name()))) # expired draft @@ -830,7 +765,7 @@ Man Expires September 22, 2015 [Page 3] shepherd_id=draft.shepherd_id, ad_id=draft.ad_id, expires=draft.expires, notify=draft.notify, note=draft.note) rel = RelatedDocument.objects.create(source=replacement, - target=draft.docalias.get(name__startswith="draft"), + target=draft, relationship_id="replaces") r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) @@ -842,26 +777,29 @@ Man Expires September 22, 2015 [Page 3] # draft published as RFC draft.set_state(State.objects.get(type="draft", slug="rfc")) - draft.std_level_id = "bcp" - draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="published_rfc", by=Person.objects.get(name="(System)"))]) + draft.std_level_id = "ps" + rfc = WgRfcFactory(group=draft.group, name="rfc123456") + rfc.save_with_history([DocEvent.objects.create(doc=rfc, rev=None, type="published_rfc", by=Person.objects.get(name="(System)"))]) - rfc_alias = DocAlias.objects.create(name="rfc123456") - rfc_alias.docs.add(draft) - bcp_alias = DocAlias.objects.create(name="bcp123456") - bcp_alias.docs.add(draft) + draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) + + obsoleted = IndividualRfcFactory() + rfc.relateddocument_set.create(relationship_id='obs',target=obsoleted) + obsoleted_by = IndividualRfcFactory() + obsoleted_by.relateddocument_set.create(relationship_id='obs',target=rfc) + updated = IndividualRfcFactory() + rfc.relateddocument_set.create(relationship_id='updates',target=updated) + updated_by = IndividualRfcFactory() + updated_by.relateddocument_set.create(relationship_id='updates',target=rfc) r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) self.assertEqual(r.status_code, 302) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=bcp_alias.name))) - self.assertEqual(r.status_code, 302) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=rfc_alias.name))) + r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=rfc.name))) self.assertEqual(r.status_code, 200) self.assertContains(r, "RFC 123456") self.assertContains(r, draft.name) - self.assertContains(r, replaced.canonical_name()) - self.assertContains(r, replaced.title) # obs/updates included with RFC self.assertContains(r, obsoleted.canonical_name()) self.assertContains(r, obsoleted.title) @@ -902,7 +840,7 @@ Man Expires September 22, 2015 [Page 3] draft = WgRfcFactory() status_change_doc = StatusChangeFactory( group=draft.group, - changes_status_of=[('tops', draft.docalias.first())], + changes_status_of=[('tops', draft)], ) status_change_url = urlreverse( 'ietf.doc.views_doc.document_main', @@ -910,7 +848,7 @@ Man Expires September 22, 2015 [Page 3] ) proposed_status_change_doc = StatusChangeFactory( group=draft.group, - changes_status_of=[('tobcp', draft.docalias.first())], + changes_status_of=[('tobcp', draft)], states=[State.objects.get(slug='needshep', type='statchg')], ) proposed_status_change_url = urlreverse( @@ -1501,11 +1439,11 @@ Man Expires September 22, 2015 [Page 3] self.assertEqual(r.status_code, 200) self.assert_correct_wg_group_link(r, group) - rfc = WgRfcFactory(name='draft-rfc-document-%s' % group_type_id, group=group) + rfc = WgRfcFactory(group=group) + draft = WgDraftFactory(group=group) + draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) DocEventFactory.create(doc=rfc, type='published_rfc', time=event_datetime) - # get the rfc name to avoid a redirect - rfc_name = rfc.docalias.filter(name__startswith='rfc').first().name - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=rfc_name))) + r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=rfc.name))) self.assertEqual(r.status_code, 200) self.assert_correct_wg_group_link(r, group) @@ -1516,11 +1454,11 @@ Man Expires September 22, 2015 [Page 3] self.assertEqual(r.status_code, 200) self.assert_correct_non_wg_group_link(r, group) - rfc = WgRfcFactory(name='draft-rfc-document-%s' % group_type_id, group=group) + rfc = WgRfcFactory(group=group) + draft = WgDraftFactory(name='draft-rfc-document-%s'% group_type_id, group=group) + draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) DocEventFactory.create(doc=rfc, type='published_rfc', time=event_datetime) - # get the rfc name to avoid a redirect - rfc_name = rfc.docalias.filter(name__startswith='rfc').first().name - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=rfc_name))) + r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=rfc.name))) self.assertEqual(r.status_code, 200) self.assert_correct_non_wg_group_link(r, group) @@ -1621,8 +1559,8 @@ class DocTestCase(TestCase): statchg = StatusChangeFactory() r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=statchg.name))) self.assertEqual(r.status_code, 200) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=statchg.relateddocument_set.first().target.document))) - self.assertEqual(r.status_code, 302) + r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=statchg.relateddocument_set.first().target))) + self.assertEqual(r.status_code, 200) def test_document_charter(self): CharterFactory(name='charter-ietf-mars') @@ -1786,8 +1724,8 @@ class DocTestCase(TestCase): self.assertNotContains(r, 'more YES or NO') # status change - DocAlias.objects.create(name='rfc9998').docs.add(IndividualDraftFactory()) - DocAlias.objects.create(name='rfc9999').docs.add(IndividualDraftFactory()) + Document.objects.create(name='rfc9998') + Document.objects.create(name='rfc9999') doc = DocumentFactory(type_id='statchg',name='status-change-imaginary-mid-review') iesgeval_pk = str(State.objects.get(slug='iesgeval',type__slug='statchg').pk) empty_outbox() @@ -1800,12 +1738,12 @@ class DocTestCase(TestCase): self.assertIn('iesg-secretary',outbox[0]['To']) self.assertIn('drafts-eval',outbox[1]['To']) - doc.relateddocument_set.create(target=DocAlias.objects.get(name='rfc9998'),relationship_id='tohist') + doc.relateddocument_set.create(target=Document.objects.get(name='rfc9998'),relationship_id='tohist') r = self.client.get(urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name))) self.assertNotContains(r, 'Needs a YES') self.assertNotContains(r, 'more YES or NO') - doc.relateddocument_set.create(target=DocAlias.objects.get(name='rfc9999'),relationship_id='tois') + doc.relateddocument_set.create(target=Document.objects.get(name='rfc9999'),relationship_id='tois') r = self.client.get(urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name))) self.assertContains(r, 'more YES or NO') @@ -1868,15 +1806,14 @@ class DocTestCase(TestCase): self.assertContains(r, e.desc) def test_history_bis_00(self): - rfcname='rfc9090' - rfc = WgRfcFactory(alias2=rfcname) - bis_draft = WgDraftFactory(name='draft-ietf-{}-{}bis'.format(rfc.group.acronym,rfcname)) + rfc = WgRfcFactory(rfc_number=9090) + bis_draft = WgDraftFactory(name='draft-ietf-{}-{}bis'.format(rfc.group.acronym,rfc.name)) url = urlreverse('ietf.doc.views_doc.document_history', kwargs=dict(name=bis_draft.name)) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(unicontent(r)) - attr1='value="{}"'.format(rfcname) + attr1='value="{}"'.format(rfc.name) self.assertEqual(len(q('option['+attr1+'][selected="selected"]')), 1) @@ -1926,7 +1863,7 @@ class DocTestCase(TestCase): self.assertContains(r, doc.name) def test_rfc_feed(self): - rfc = WgRfcFactory(alias2__name="rfc9000") + rfc = WgRfcFactory(rfc_number=9000) DocEventFactory(doc=rfc, type="published_rfc") r = self.client.get("/feed/rfc/") self.assertTrue(r.status_code, 200) @@ -1989,7 +1926,7 @@ class DocTestCase(TestCase): std_level_id = 'ps', time = datetime.datetime(2010, 10, 10, tzinfo=ZoneInfo(settings.TIME_ZONE)), ) - num = rfc.rfc_number() + num = rfc.rfc_number DocEventFactory.create( doc=rfc, type='published_rfc', @@ -2000,7 +1937,7 @@ class DocTestCase(TestCase): r = self.client.get(url) entry = self._parse_bibtex_response(r)["rfc%s"%num] self.assertEqual(entry['series'], 'Request for Comments') - self.assertEqual(entry['number'], num) + self.assertEqual(int(entry['number']), num) self.assertEqual(entry['doi'], '10.17487/RFC%s'%num) self.assertEqual(entry['year'], '2010') self.assertEqual(entry['month'].lower()[0:3], 'oct') @@ -2020,7 +1957,7 @@ class DocTestCase(TestCase): std_level_id = 'inf', time = datetime.datetime(1990, 4, 1, tzinfo=ZoneInfo(settings.TIME_ZONE)), ) - num = april1.rfc_number() + num = april1.rfc_number DocEventFactory.create( doc=april1, type='published_rfc', @@ -2032,7 +1969,7 @@ class DocTestCase(TestCase): self.assertEqual(r.get('Content-Type'), 'text/plain; charset=utf-8') entry = self._parse_bibtex_response(r)["rfc%s"%num] self.assertEqual(entry['series'], 'Request for Comments') - self.assertEqual(entry['number'], num) + self.assertEqual(int(entry['number']), num) self.assertEqual(entry['doi'], '10.17487/RFC%s'%num) self.assertEqual(entry['year'], '1990') self.assertEqual(entry['month'].lower()[0:3], 'apr') @@ -2129,7 +2066,7 @@ class ReferencesTest(TestCase): def test_references(self): doc1 = WgDraftFactory(name='draft-ietf-mars-test') - doc2 = IndividualDraftFactory(name='draft-imaginary-independent-submission').docalias.first() + doc2 = IndividualDraftFactory(name='draft-imaginary-independent-submission') RelatedDocument.objects.get_or_create(source=doc1,target=doc2,relationship=DocRelationshipName.objects.get(slug='refnorm')) url = urlreverse('ietf.doc.views_doc.document_references', kwargs=dict(name=doc1.name)) r = self.client.get(url) @@ -2141,124 +2078,168 @@ class ReferencesTest(TestCase): self.assertContains(r, doc1.name) class GenerateDraftAliasesTests(TestCase): - def setUp(self): - super().setUp() - self.doc_aliases_file = NamedTemporaryFile(delete=False, mode='w+') - self.doc_aliases_file.close() - self.doc_virtual_file = NamedTemporaryFile(delete=False, mode='w+') - self.doc_virtual_file.close() - self.saved_draft_aliases_path = settings.DRAFT_ALIASES_PATH - self.saved_draft_virtual_path = settings.DRAFT_VIRTUAL_PATH - settings.DRAFT_ALIASES_PATH = self.doc_aliases_file.name - settings.DRAFT_VIRTUAL_PATH = self.doc_virtual_file.name + def setUp(self): + super().setUp() + self.doc_aliases_file = NamedTemporaryFile(delete=False, mode="w+") + self.doc_aliases_file.close() + self.doc_virtual_file = NamedTemporaryFile(delete=False, mode="w+") + self.doc_virtual_file.close() + self.saved_draft_aliases_path = settings.DRAFT_ALIASES_PATH + self.saved_draft_virtual_path = settings.DRAFT_VIRTUAL_PATH + settings.DRAFT_ALIASES_PATH = self.doc_aliases_file.name + settings.DRAFT_VIRTUAL_PATH = self.doc_virtual_file.name - def tearDown(self): - settings.DRAFT_ALIASES_PATH = self.saved_draft_aliases_path - settings.DRAFT_VIRTUAL_PATH = self.saved_draft_virtual_path - os.unlink(self.doc_aliases_file.name) - os.unlink(self.doc_virtual_file.name) - super().tearDown() + def tearDown(self): + settings.DRAFT_ALIASES_PATH = self.saved_draft_aliases_path + settings.DRAFT_VIRTUAL_PATH = self.saved_draft_virtual_path + os.unlink(self.doc_aliases_file.name) + os.unlink(self.doc_virtual_file.name) + super().tearDown() - def testManagementCommand(self): - a_month_ago = (timezone.now() - datetime.timedelta(30)).astimezone(RPC_TZINFO) - a_month_ago = a_month_ago.replace(hour=0, minute=0, second=0, microsecond=0) - ad = RoleFactory(name_id='ad', group__type_id='area', group__state_id='active').person - shepherd = PersonFactory() - author1 = PersonFactory() - author2 = PersonFactory() - author3 = PersonFactory() - author4 = PersonFactory() - author5 = PersonFactory() - author6 = PersonFactory() - mars = GroupFactory(type_id='wg', acronym='mars') - marschairman = PersonFactory(user__username='marschairman') - mars.role_set.create(name_id='chair', person=marschairman, email=marschairman.email()) - doc1 = IndividualDraftFactory(authors=[author1], shepherd=shepherd.email(), ad=ad) - doc2 = WgDraftFactory(name='draft-ietf-mars-test', group__acronym='mars', authors=[author2], ad=ad) - doc3 = WgRfcFactory.create(name='draft-ietf-mars-finished', group__acronym='mars', authors=[author3], ad=ad, std_level_id='ps', states=[('draft','rfc'),('draft-iesg','pub')], time=a_month_ago) - DocEventFactory.create(doc=doc3, type='published_rfc', time=a_month_ago) - doc4 = WgRfcFactory.create(authors=[author4,author5], ad=ad, std_level_id='ps', states=[('draft','rfc'),('draft-iesg','pub')], time=datetime.datetime(2010,10,10, tzinfo=ZoneInfo(settings.TIME_ZONE))) - DocEventFactory.create(doc=doc4, type='published_rfc', time=datetime.datetime(2010, 10, 10, tzinfo=RPC_TZINFO)) - doc5 = IndividualDraftFactory(authors=[author6]) + def testManagementCommand(self): + a_month_ago = (timezone.now() - datetime.timedelta(30)).astimezone(RPC_TZINFO) + a_month_ago = a_month_ago.replace(hour=0, minute=0, second=0, microsecond=0) + ad = RoleFactory( + name_id="ad", group__type_id="area", group__state_id="active" + ).person + shepherd = PersonFactory() + author1 = PersonFactory() + author2 = PersonFactory() + author3 = PersonFactory() + author4 = PersonFactory() + author5 = PersonFactory() + author6 = PersonFactory() + mars = GroupFactory(type_id="wg", acronym="mars") + marschairman = PersonFactory(user__username="marschairman") + mars.role_set.create( + name_id="chair", person=marschairman, email=marschairman.email() + ) + doc1 = IndividualDraftFactory( + authors=[author1], shepherd=shepherd.email(), ad=ad + ) + doc2 = WgDraftFactory( + name="draft-ietf-mars-test", group__acronym="mars", authors=[author2], ad=ad + ) + doc3 = WgDraftFactory.create( + name="draft-ietf-mars-finished", + group__acronym="mars", + authors=[author3], + ad=ad, + std_level_id="ps", + states=[("draft", "rfc"), ("draft-iesg", "pub")], + time=a_month_ago, + ) + rfc3 = WgRfcFactory() + DocEventFactory.create(doc=rfc3, type="published_rfc", time=a_month_ago) + doc3.relateddocument_set.create( + relationship_id="became_rfc", target=rfc3 + ) + doc4 = WgDraftFactory.create( + authors=[author4, author5], + ad=ad, + std_level_id="ps", + states=[("draft", "rfc"), ("draft-iesg", "pub")], + time=datetime.datetime(2010, 10, 10, tzinfo=ZoneInfo(settings.TIME_ZONE)), + ) + rfc4 = WgRfcFactory() + DocEventFactory.create( + doc=rfc4, + type="published_rfc", + time=datetime.datetime(2010, 10, 10, tzinfo=RPC_TZINFO), + ) + doc4.relateddocument_set.create( + relationship_id="became_rfc", target=rfc4 + ) + doc5 = IndividualDraftFactory(authors=[author6]) - args = [ ] - kwargs = { } - out = io.StringIO() - call_command("generate_draft_aliases", *args, **kwargs, stdout=out, stderr=out) - self.assertFalse(out.getvalue()) + args = [] + kwargs = {} + out = io.StringIO() + call_command("generate_draft_aliases", *args, **kwargs, stdout=out, stderr=out) + self.assertFalse(out.getvalue()) - with open(settings.DRAFT_ALIASES_PATH) as afile: - acontent = afile.read() - self.assertTrue(all([x in acontent for x in [ - 'xfilter-' + doc1.name, - 'xfilter-' + doc1.name + '.ad', - 'xfilter-' + doc1.name + '.authors', - 'xfilter-' + doc1.name + '.shepherd', - 'xfilter-' + doc1.name + '.all', - 'xfilter-' + doc2.name, - 'xfilter-' + doc2.name + '.ad', - 'xfilter-' + doc2.name + '.authors', - 'xfilter-' + doc2.name + '.chairs', - 'xfilter-' + doc2.name + '.all', - 'xfilter-' + doc3.name, - 'xfilter-' + doc3.name + '.ad', - 'xfilter-' + doc3.name + '.authors', - 'xfilter-' + doc3.name + '.chairs', - 'xfilter-' + doc5.name, - 'xfilter-' + doc5.name + '.authors', - 'xfilter-' + doc5.name + '.all', - ]])) - self.assertFalse(all([x in acontent for x in [ - 'xfilter-' + doc1.name + '.chairs', - 'xfilter-' + doc2.name + '.shepherd', - 'xfilter-' + doc3.name + '.shepherd', - 'xfilter-' + doc4.name, - 'xfilter-' + doc5.name + '.shepherd', - 'xfilter-' + doc5.name + '.ad', - ]])) + with open(settings.DRAFT_ALIASES_PATH) as afile: + acontent = afile.read() + for x in [ + "xfilter-" + doc1.name, + "xfilter-" + doc1.name + ".ad", + "xfilter-" + doc1.name + ".authors", + "xfilter-" + doc1.name + ".shepherd", + "xfilter-" + doc1.name + ".all", + "xfilter-" + doc2.name, + "xfilter-" + doc2.name + ".ad", + "xfilter-" + doc2.name + ".authors", + "xfilter-" + doc2.name + ".chairs", + "xfilter-" + doc2.name + ".all", + "xfilter-" + doc3.name, + "xfilter-" + doc3.name + ".ad", + "xfilter-" + doc3.name + ".authors", + "xfilter-" + doc3.name + ".chairs", + "xfilter-" + doc5.name, + "xfilter-" + doc5.name + ".authors", + "xfilter-" + doc5.name + ".all", + ]: + self.assertIn(x, acontent) - with open(settings.DRAFT_VIRTUAL_PATH) as vfile: - vcontent = vfile.read() - self.assertTrue(all([x in vcontent for x in [ - ad.email_address(), - shepherd.email_address(), - marschairman.email_address(), - author1.email_address(), - author2.email_address(), - author3.email_address(), - author6.email_address(), - ]])) - self.assertFalse(all([x in vcontent for x in [ - author4.email_address(), - author5.email_address(), - ]])) - self.assertTrue(all([x in vcontent for x in [ - 'xfilter-' + doc1.name, - 'xfilter-' + doc1.name + '.ad', - 'xfilter-' + doc1.name + '.authors', - 'xfilter-' + doc1.name + '.shepherd', - 'xfilter-' + doc1.name + '.all', - 'xfilter-' + doc2.name, - 'xfilter-' + doc2.name + '.ad', - 'xfilter-' + doc2.name + '.authors', - 'xfilter-' + doc2.name + '.chairs', - 'xfilter-' + doc2.name + '.all', - 'xfilter-' + doc3.name, - 'xfilter-' + doc3.name + '.ad', - 'xfilter-' + doc3.name + '.authors', - 'xfilter-' + doc3.name + '.chairs', - 'xfilter-' + doc5.name, - 'xfilter-' + doc5.name + '.authors', - 'xfilter-' + doc5.name + '.all', - ]])) - self.assertFalse(all([x in vcontent for x in [ - 'xfilter-' + doc1.name + '.chairs', - 'xfilter-' + doc2.name + '.shepherd', - 'xfilter-' + doc3.name + '.shepherd', - 'xfilter-' + doc4.name, - 'xfilter-' + doc5.name + '.shepherd', - 'xfilter-' + doc5.name + '.ad', - ]])) + for x in [ + "xfilter-" + doc1.name + ".chairs", + "xfilter-" + doc2.name + ".shepherd", + "xfilter-" + doc3.name + ".shepherd", + "xfilter-" + doc4.name, + "xfilter-" + doc5.name + ".shepherd", + "xfilter-" + doc5.name + ".ad", + ]: + self.assertNotIn(x, acontent) + + with open(settings.DRAFT_VIRTUAL_PATH) as vfile: + vcontent = vfile.read() + for x in [ + ad.email_address(), + shepherd.email_address(), + marschairman.email_address(), + author1.email_address(), + author2.email_address(), + author3.email_address(), + author6.email_address(), + ]: + self.assertIn(x, vcontent) + + for x in [ + author4.email_address(), + author5.email_address(), + ]: + self.assertNotIn(x, vcontent) + + for x in [ + "xfilter-" + doc1.name, + "xfilter-" + doc1.name + ".ad", + "xfilter-" + doc1.name + ".authors", + "xfilter-" + doc1.name + ".shepherd", + "xfilter-" + doc1.name + ".all", + "xfilter-" + doc2.name, + "xfilter-" + doc2.name + ".ad", + "xfilter-" + doc2.name + ".authors", + "xfilter-" + doc2.name + ".chairs", + "xfilter-" + doc2.name + ".all", + "xfilter-" + doc3.name, + "xfilter-" + doc3.name + ".ad", + "xfilter-" + doc3.name + ".authors", + "xfilter-" + doc3.name + ".chairs", + "xfilter-" + doc5.name, + "xfilter-" + doc5.name + ".authors", + "xfilter-" + doc5.name + ".all", + ]: + self.assertIn(x, vcontent) + + for x in [ + "xfilter-" + doc1.name + ".chairs", + "xfilter-" + doc2.name + ".shepherd", + "xfilter-" + doc3.name + ".shepherd", + "xfilter-" + doc4.name, + "xfilter-" + doc5.name + ".shepherd", + "xfilter-" + doc5.name + ".ad", + ]: + self.assertNotIn(x, vcontent) class EmailAliasesTests(TestCase): @@ -2692,10 +2673,10 @@ class Idnits2SupportTests(TestCase): settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['DERIVED_DIR'] def test_obsoleted(self): - rfc = WgRfcFactory(alias2__name='rfc1001') - WgRfcFactory(alias2__name='rfc1003',relations=[('obs',rfc)]) - rfc = WgRfcFactory(alias2__name='rfc1005') - WgRfcFactory(alias2__name='rfc1007',relations=[('obs',rfc)]) + rfc = WgRfcFactory(rfc_number=1001) + WgRfcFactory(rfc_number=1003,relations=[('obs',rfc)]) + rfc = WgRfcFactory(rfc_number=1005) + WgRfcFactory(rfc_number=1007,relations=[('obs',rfc)]) url = urlreverse('ietf.doc.views_doc.idnits2_rfcs_obsoleted') r = self.client.get(url) @@ -2720,6 +2701,8 @@ class Idnits2SupportTests(TestCase): def test_idnits2_state(self): rfc = WgRfcFactory() + draft = WgDraftFactory() + draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) url = urlreverse('ietf.doc.views_doc.idnits2_state', kwargs=dict(name=rfc.canonical_name())) r = self.client.get(url) self.assertEqual(r.status_code, 200) @@ -2778,16 +2761,12 @@ class RawIdTests(TestCase): self.should_succeed(dict(name=draft.name, rev='00',ext='txt')) self.should_404(dict(name=draft.name, rev='00',ext='html')) - def test_raw_id_rfc(self): - rfc = WgRfcFactory() - dir = settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR - (Path(dir) / f'{rfc.name}-{rfc.rev}.txt').touch() - self.should_succeed(dict(name=rfc.name)) - self.should_404(dict(name=rfc.canonical_name())) + # test_raw_id_rfc intentionally removed + # an rfc is no longer a pseudo-version of a draft. def test_non_draft(self): - charter = CharterFactory() - self.should_404(dict(name=charter.name)) + for doc in [CharterFactory(), WgRfcFactory()]: + self.should_404(dict(name=doc.name)) class PdfizedTests(TestCase): @@ -2806,24 +2785,27 @@ class PdfizedTests(TestCase): r = self.client.get(url) self.assertEqual(r.status_code, 404) + # This takes a _long_ time (32s on a 2022 m1 macbook pro) - is it worth what it covers? def test_pdfized(self): - rfc = WgRfcFactory(create_revisions=range(0,2)) + rfc = WgRfcFactory() + draft = WgDraftFactory(create_revisions=range(0,2)) + draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) dir = settings.RFC_PATH - with (Path(dir) / f'{rfc.canonical_name()}.txt').open('w') as f: + with (Path(dir) / f'{rfc.name}.txt').open('w') as f: f.write('text content') dir = settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR for r in range(0,2): - with (Path(dir) / f'{rfc.name}-{r:02d}.txt').open('w') as f: + with (Path(dir) / f'{draft.name}-{r:02d}.txt').open('w') as f: f.write('text content') - self.should_succeed(dict(name=rfc.canonical_name())) self.should_succeed(dict(name=rfc.name)) + self.should_succeed(dict(name=draft.name)) for r in range(0,2): - self.should_succeed(dict(name=rfc.name,rev=f'{r:02d}')) + self.should_succeed(dict(name=draft.name,rev=f'{r:02d}')) for ext in ('pdf','txt','html','anythingatall'): - self.should_succeed(dict(name=rfc.name,rev=f'{r:02d}',ext=ext)) - self.should_404(dict(name=rfc.name,rev='02')) + self.should_succeed(dict(name=draft.name,rev=f'{r:02d}',ext=ext)) + self.should_404(dict(name=draft.name,rev='02')) class NotifyValidationTests(TestCase): def test_notify_validation(self): diff --git a/ietf/doc/tests_ballot.py b/ietf/doc/tests_ballot.py index 8a4717c74..9c9287dab 100644 --- a/ietf/doc/tests_ballot.py +++ b/ietf/doc/tests_ballot.py @@ -803,8 +803,8 @@ class ApproveBallotTests(TestCase): desc='Last call announcement was changed', text='this is simple last call text.' ) rfc = IndividualRfcFactory.create( + name = "rfc6666", stream_id='ise', - other_aliases=['rfc6666',], states=[('draft','rfc'),('draft-iesg','pub')], std_level_id='inf', ) @@ -821,7 +821,7 @@ class ApproveBallotTests(TestCase): self.assertContains(r, "No downward references for") # Add a downref, the page should ask if it should be added to the registry - rel = draft.relateddocument_set.create(target=rfc.docalias.get(name='rfc6666'),relationship_id='refnorm') + rel = draft.relateddocument_set.create(target=rfc, relationship_id='refnorm') d = [rdoc for rdoc in draft.relateddocument_set.all() if rel.is_approved_downref()] original_len = len(d) r = self.client.get(url) @@ -1121,13 +1121,13 @@ class RegenerateLastCallTestCase(TestCase): self.assertFalse("contains these normative down" in lc_text) rfc = IndividualRfcFactory.create( + rfc_number=6666, stream_id='ise', - other_aliases=['rfc6666',], states=[('draft','rfc'),('draft-iesg','pub')], std_level_id='inf', ) - draft.relateddocument_set.create(target=rfc.docalias.get(name='rfc6666'),relationship_id='refnorm') + draft.relateddocument_set.create(target=rfc,relationship_id='refnorm') r = self.client.post(url, dict(regenerate_last_call_text="1")) self.assertEqual(r.status_code, 200) @@ -1137,7 +1137,7 @@ class RegenerateLastCallTestCase(TestCase): self.assertTrue("rfc6666" in lc_text) self.assertTrue("Independent Submission" in lc_text) - draft.relateddocument_set.create(target=rfc.docalias.get(name='rfc6666'), relationship_id='downref-approval') + draft.relateddocument_set.create(target=rfc, relationship_id='downref-approval') r = self.client.post(url, dict(regenerate_last_call_text="1")) self.assertEqual(r.status_code, 200) diff --git a/ietf/doc/tests_charter.py b/ietf/doc/tests_charter.py index f65cf14e0..c5c5ce5f9 100644 --- a/ietf/doc/tests_charter.py +++ b/ietf/doc/tests_charter.py @@ -88,10 +88,7 @@ class EditCharterTests(TestCase): settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['CHARTER_PATH'] def write_charter_file(self, charter): - with (Path(settings.CHARTER_PATH) / - ("%s-%s.txt" % (charter.canonical_name(), charter.rev)) - ).open("w") as f: - f.write("This is a charter.") + (Path(settings.CHARTER_PATH) / f"{charter.name}-{charter.rev}.txt").write_text("This is a charter.") def test_startstop_process(self): CharterFactory(group__acronym='mars') @@ -509,8 +506,13 @@ class EditCharterTests(TestCase): self.assertEqual(charter.rev, next_revision(prev_rev)) self.assertTrue("new_revision" in charter.latest_event().type) - with (Path(settings.CHARTER_PATH) / (charter.canonical_name() + "-" + charter.rev + ".txt")).open(encoding='utf-8') as f: - self.assertEqual(f.read(), "Windows line\nMac line\nUnix line\n" + utf_8_snippet.decode('utf-8')) + file_contents = ( + Path(settings.CHARTER_PATH) / (charter.name + "-" + charter.rev + ".txt") + ).read_text("utf-8") + self.assertEqual( + file_contents, + "Windows line\nMac line\nUnix line\n" + utf_8_snippet.decode("utf-8"), + ) def test_submit_initial_charter(self): group = GroupFactory(type_id='wg',acronym='mars',list_email='mars-wg@ietf.org') @@ -538,6 +540,24 @@ class EditCharterTests(TestCase): group = Group.objects.get(pk=group.pk) self.assertEqual(group.charter, charter) + def test_submit_charter_with_invalid_name(self): + self.client.login(username="secretary", password="secretary+password") + ietf_group = GroupFactory(type_id="wg") + for bad_name in ("charter-irtf-{}", "charter-randomjunk-{}", "charter-ietf-thisisnotagroup"): + url = urlreverse("ietf.doc.views_charter.submit", kwargs={"name": bad_name.format(ietf_group.acronym)}) + r = self.client.get(url) + self.assertEqual(r.status_code, 404, f"GET of charter named {bad_name} should 404") + r = self.client.post(url, {}) + self.assertEqual(r.status_code, 404, f"POST of charter named {bad_name} should 404") + + irtf_group = GroupFactory(type_id="rg") + for bad_name in ("charter-ietf-{}", "charter-whatisthis-{}", "charter-irtf-thisisnotagroup"): + url = urlreverse("ietf.doc.views_charter.submit", kwargs={"name": bad_name.format(irtf_group.acronym)}) + r = self.client.get(url) + self.assertEqual(r.status_code, 404, f"GET of charter named {bad_name} should 404") + r = self.client.post(url, {}) + self.assertEqual(r.status_code, 404, f"POST of charter named {bad_name} should 404") + def test_edit_review_announcement_text(self): area = GroupFactory(type_id='area') RoleFactory(name_id='ad',group=area,person=Person.objects.get(user__username='ad')) diff --git a/ietf/doc/tests_conflict_review.py b/ietf/doc/tests_conflict_review.py index 6fa1a4d9b..2aa7a161c 100644 --- a/ietf/doc/tests_conflict_review.py +++ b/ietf/doc/tests_conflict_review.py @@ -70,12 +70,12 @@ class ConflictReviewTests(TestCase): self.assertEqual(review_doc.ad.name,'Areað Irector') self.assertEqual(review_doc.notify,'ipu@ietf.org') doc = Document.objects.get(name='draft-imaginary-independent-submission') - self.assertTrue(doc in [x.target.document for x in review_doc.relateddocument_set.filter(relationship__slug='conflrev')]) + self.assertTrue(doc in [x.target for x in review_doc.relateddocument_set.filter(relationship__slug='conflrev')]) self.assertTrue(review_doc.latest_event(DocEvent,type="added_comment").desc.startswith("IETF conflict review requested")) self.assertTrue(doc.latest_event(DocEvent,type="added_comment").desc.startswith("IETF conflict review initiated")) self.assertTrue('Conflict Review requested' in outbox[-1]['Subject']) - + # verify you can't start a review when a review is already in progress r = self.client.post(url,dict(ad="Areað Irector",create_in_state="Needs Shepherd",notify='ipu@ietf.org')) self.assertEqual(r.status_code, 404) @@ -119,7 +119,7 @@ class ConflictReviewTests(TestCase): self.assertEqual(review_doc.ad.name,'Ietf Chair') self.assertEqual(review_doc.notify,'ipu@ietf.org') doc = Document.objects.get(name='draft-imaginary-independent-submission') - self.assertTrue(doc in [x.target.document for x in review_doc.relateddocument_set.filter(relationship__slug='conflrev')]) + self.assertTrue(doc in [x.target for x in review_doc.relateddocument_set.filter(relationship__slug='conflrev')]) self.assertEqual(len(outbox), messages_before + 2) diff --git a/ietf/doc/tests_downref.py b/ietf/doc/tests_downref.py index 258494e36..04bd908a9 100644 --- a/ietf/doc/tests_downref.py +++ b/ietf/doc/tests_downref.py @@ -22,9 +22,9 @@ class Downref(TestCase): self.draftalias = self.draft.docalias.get(name='draft-ietf-mars-test') self.doc = WgDraftFactory(name='draft-ietf-mars-approved-document',states=[('draft-iesg','rfcqueue')]) self.docalias = self.doc.docalias.get(name='draft-ietf-mars-approved-document') - self.rfc = WgRfcFactory(alias2__name='rfc9998') + self.rfc = WgRfcFactory(rfc_number=9998) self.rfcalias = self.rfc.docalias.get(name='rfc9998') - RelatedDocument.objects.create(source=self.doc, target=self.rfcalias, relationship_id='downref-approval') + RelatedDocument.objects.create(source=self.doc, target=self.rfc, relationship_id='downref-approval') def test_downref_registry(self): url = urlreverse('ietf.doc.views_downref.downref_registry') @@ -64,44 +64,44 @@ class Downref(TestCase): self.assertContains(r, 'Save downref') # error - already in the downref registry - r = self.client.post(url, dict(rfc=self.rfcalias.pk, drafts=(self.docalias.pk, ))) + r = self.client.post(url, dict(rfc=self.rfc.pk, drafts=(self.doc.pk, ))) self.assertContains(r, 'Downref is already in the registry') # error - source is not in an approved state r = self.client.get(url) self.assertEqual(r.status_code, 200) - r = self.client.post(url, dict(rfc=self.rfcalias.pk, drafts=(self.draftalias.pk, ))) + r = self.client.post(url, dict(rfc=self.rfc.pk, drafts=(self.draft.pk, ))) self.assertContains(r, 'Draft is not yet approved') # error - the target is not a normative reference of the source self.draft.set_state(State.objects.get(used=True, type="draft-iesg", slug="pub")) r = self.client.get(url) self.assertEqual(r.status_code, 200) - r = self.client.post(url, dict(rfc=self.rfcalias.pk, drafts=(self.draftalias.pk, ))) + r = self.client.post(url, dict(rfc=self.rfc.pk, drafts=(self.draft.pk, ))) self.assertContains(r, 'There does not seem to be a normative reference to RFC') self.assertContains(r, 'Save downref anyway') # normal - approve the document so the downref is now okay - RelatedDocument.objects.create(source=self.draft, target=self.rfcalias, relationship_id='refnorm') + RelatedDocument.objects.create(source=self.draft, target=self.rfc, relationship_id='refnorm') draft_de_count_before = self.draft.docevent_set.count() rfc_de_count_before = self.rfc.docevent_set.count() r = self.client.get(url) self.assertEqual(r.status_code, 200) - r = self.client.post(url, dict(rfc=self.rfcalias.pk, drafts=(self.draftalias.pk, ))) + r = self.client.post(url, dict(rfc=self.rfc.pk, drafts=(self.draft.pk, ))) self.assertEqual(r.status_code, 302) newurl = urlreverse('ietf.doc.views_downref.downref_registry') r = self.client.get(newurl) self.assertContains(r, ' 1 should not occur if count == 0: unfound.add( "%s" % ref ) continue elif count > 1: - errors.append("Too many DocAlias objects found for %s"%ref) + errors.append("Too many Document objects found for %s"%ref) else: # Don't add references to ourself - if doc != refdoc[0].document: + if doc != refdoc[0]: RelatedDocument.objects.get_or_create( source=doc, target=refdoc[ 0 ], relationship=DocRelationshipName.objects.get( slug='ref%s' % refType ) ) if unfound: - warnings.append('There were %d references with no matching DocAlias'%len(unfound)) + warnings.append('There were %d references with no matching Document'%len(unfound)) ret = {} if errors: @@ -821,26 +821,26 @@ def set_replaces_for_document(request, doc, new_replaces, by, email_subject, com for d in old_replaces: if d not in new_replaces: - other_addrs = gather_address_lists('doc_replacement_changed',doc=d.document) + other_addrs = gather_address_lists('doc_replacement_changed',doc=d) to.update(other_addrs.to) cc.update(other_addrs.cc) RelatedDocument.objects.filter(source=doc, target=d, relationship=relationship).delete() if not RelatedDocument.objects.filter(target=d, relationship=relationship): - s = 'active' if d.document.expires > timezone.now() else 'expired' - d.document.set_state(State.objects.get(type='draft', slug=s)) + s = 'active' if d.expires > timezone.now() else 'expired' + d.set_state(State.objects.get(type='draft', slug=s)) for d in new_replaces: if d not in old_replaces: - other_addrs = gather_address_lists('doc_replacement_changed',doc=d.document) + other_addrs = gather_address_lists('doc_replacement_changed',doc=d) to.update(other_addrs.to) cc.update(other_addrs.cc) RelatedDocument.objects.create(source=doc, target=d, relationship=relationship) - d.document.set_state(State.objects.get(type='draft', slug='repl')) + d.set_state(State.objects.get(type='draft', slug='repl')) - if d.document.stream_id in ('irtf','ise','iab'): - repl_state = State.objects.get(type_id='draft-stream-%s'%d.document.stream_id, slug='repl') - d.document.set_state(repl_state) - events.append(StateDocEvent.objects.create(doc=d.document, rev=d.document.rev, by=by, type='changed_state', desc="Set stream state to Replaced",state_type=repl_state.type, state=repl_state)) + if d.stream_id in ('irtf','ise','iab'): + repl_state = State.objects.get(type_id='draft-stream-%s'%d.stream_id, slug='repl') + d.set_state(repl_state) + events.append(StateDocEvent.objects.create(doc=d, rev=d.rev, by=by, type='changed_state', desc="Set stream state to Replaced",state_type=repl_state.type, state=repl_state)) # make sure there are no lingering suggestions duplicating new replacements RelatedDocument.objects.filter(source=doc, target__in=new_replaces, relationship="possibly-replaces").delete() @@ -910,7 +910,7 @@ def extract_complete_replaces_ancestor_mapping_for_docs(names): break relations = ( RelatedDocument.objects.filter(source__name__in=front, relationship="replaces") - .select_related("target").values_list("source__name", "target__docs__name") ) + .select_related("target").values_list("source__name", "target__name") ) if not relations: break @@ -998,14 +998,11 @@ def get_search_cache_key(params): kwargs = dict([ (k,v) for (k,v) in list(params.items()) if k in fields ]) key = "doc:document:search:" + hashlib.sha512(json.dumps(kwargs, sort_keys=True).encode('utf-8')).hexdigest() return key - -def build_file_urls(doc: Union[Document, DocHistory]): - if doc.type_id != 'draft': - return [], [] - if doc.get_state_slug() == "rfc": - name = doc.canonical_name() - base_path = os.path.join(settings.RFC_PATH, name + ".") + +def build_file_urls(doc: Union[Document, DocHistory]): + if doc.type_id == "rfc": + base_path = os.path.join(settings.RFC_PATH, doc.name + ".") possible_types = settings.RFC_FILE_TYPES found_types = [t for t in possible_types if os.path.exists(base_path + t)] @@ -1014,17 +1011,17 @@ def build_file_urls(doc: Union[Document, DocHistory]): file_urls = [] for t in found_types: label = "plain text" if t == "txt" else t - file_urls.append((label, base + name + "." + t)) + file_urls.append((label, base + doc.name + "." + t)) if "pdf" not in found_types and "txt" in found_types: - file_urls.append(("pdf", base + "pdfrfc/" + name + ".txt.pdf")) + file_urls.append(("pdf", base + "pdfrfc/" + doc.name + ".txt.pdf")) if "txt" in found_types: - file_urls.append(("htmlized", urlreverse('ietf.doc.views_doc.document_html', kwargs=dict(name=name)))) + file_urls.append(("htmlized", urlreverse('ietf.doc.views_doc.document_html', kwargs=dict(name=doc.name)))) if doc.tags.filter(slug="verified-errata").exists(): - file_urls.append(("with errata", settings.RFC_EDITOR_INLINE_ERRATA_URL.format(rfc_number=doc.rfc_number()))) - file_urls.append(("bibtex", urlreverse('ietf.doc.views_doc.document_bibtex',kwargs=dict(name=name)))) - elif doc.rev: + file_urls.append(("with errata", settings.RFC_EDITOR_INLINE_ERRATA_URL.format(rfc_number=doc.rfc_number))) + file_urls.append(("bibtex", urlreverse('ietf.doc.views_doc.document_bibtex',kwargs=dict(name=doc.name)))) + elif doc.type_id == "draft" and doc.rev != "": base_path = os.path.join(settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR, doc.name + "-" + doc.rev + ".") possible_types = settings.IDSUBMIT_FILE_TYPES found_types = [t for t in possible_types if os.path.exists(base_path + t)] @@ -1040,12 +1037,14 @@ def build_file_urls(doc: Union[Document, DocHistory]): file_urls.append(("bibtex", urlreverse('ietf.doc.views_doc.document_bibtex',kwargs=dict(name=doc.name,rev=doc.rev)))) file_urls.append(("bibxml", urlreverse('ietf.doc.views_doc.document_bibxml',kwargs=dict(name=doc.name,rev=doc.rev)))) else: - # As of 2022-12-14, there are 1463 Document and 3136 DocHistory records with type='draft' and rev=''. - # All of these are in the rfc state and are covered by the above cases. - log.unreachable('2022-12-14') + if doc.type_id == "draft": + # TODO: look at the state of the database post migration and update this comment, or remove the block + # As of 2022-12-14, there are 1463 Document and 3136 DocHistory records with type='draft' and rev=''. + # All of these are in the rfc state and are covered by the above cases. + log.unreachable('2022-12-14') file_urls = [] found_types = [] - + return file_urls, found_types def augment_docs_and_user_with_user_info(docs, user): @@ -1112,16 +1111,16 @@ def generate_idnits2_rfc_status(): 'unkn': 'U', } - rfcs = Document.objects.filter(type_id='draft',states__slug='rfc',states__type='draft') + rfcs = Document.objects.filter(type_id='rfc') for rfc in rfcs: - offset = int(rfc.rfcnum)-1 + offset = int(rfc.rfc_number)-1 blob[offset] = symbols[rfc.std_level_id] if rfc.related_that('obs'): blob[offset] = 'O' # Workarounds for unusual states in the datatracker - # Document.get(docalias='rfc6312').rfcnum == 6342 + # Document.get(docalias='rfc6312').rfc_number == 6342 # 6312 was published with the wrong rfc number in it # weird workaround in the datatracker - there are two # DocAliases starting with rfc - the canonical name code @@ -1142,7 +1141,7 @@ def generate_idnits2_rfc_status(): def generate_idnits2_rfcs_obsoleted(): obsdict = defaultdict(list) for r in RelatedDocument.objects.filter(relationship_id='obs'): - obsdict[int(r.target.document.rfc_number())].append(int(r.source.rfc_number())) + obsdict[int(r.target.rfc_number)].append(int(r.source.rfc_number)) # Aren't these already guaranteed to be ints? for k in obsdict: obsdict[k] = sorted(obsdict[k]) return render_to_string('doc/idnits2-rfcs-obsoleted.txt', context={'obsitems':sorted(obsdict.items())}) @@ -1171,8 +1170,14 @@ def fuzzy_find_documents(name, rev=None): if re.match("^[0-9]+$", name): name = f'rfc{name}' + if name.startswith("rfc"): + sought_type = "rfc" + log.assertion("rev is None") + else: + sought_type = "draft" + # see if we can find a document using this name - docs = Document.objects.filter(docalias__name=name, type_id='draft') + docs = Document.objects.filter(docalias__name=name, type_id=sought_type) if rev and not docs.exists(): # No document found, see if the name/rev split has been misidentified. # Handles some special cases, like draft-ietf-tsvwg-ieee-802-11. diff --git a/ietf/doc/utils_charter.py b/ietf/doc/utils_charter.py index 2e85b3cc1..7d2001e4d 100644 --- a/ietf/doc/utils_charter.py +++ b/ietf/doc/utils_charter.py @@ -3,11 +3,12 @@ import datetime -import io import os import re import shutil +from pathlib import Path + from django.conf import settings from django.urls import reverse as urlreverse from django.template.loader import render_to_string @@ -62,10 +63,9 @@ def next_approved_revision(rev): return "%#02d" % (int(m.group('major')) + 1) def read_charter_text(doc): - filename = os.path.join(settings.CHARTER_PATH, '%s-%s.txt' % (doc.canonical_name(), doc.rev)) + filename = Path(settings.CHARTER_PATH) / f"{doc.name}-{doc.rev}.txt" try: - with io.open(filename, 'r') as f: - return f.read() + return filename.read_text() except IOError: return "Error: couldn't read charter text" @@ -92,8 +92,8 @@ def change_group_state_after_charter_approval(group, by): def fix_charter_revision_after_approval(charter, by): # according to spec, 00-02 becomes 01, so copy file and record new revision try: - old = os.path.join(charter.get_file_path(), '%s-%s.txt' % (charter.canonical_name(), charter.rev)) - new = os.path.join(charter.get_file_path(), '%s-%s.txt' % (charter.canonical_name(), next_approved_revision(charter.rev))) + old = os.path.join(charter.get_file_path(), '%s-%s.txt' % (charter.name, charter.rev)) + new = os.path.join(charter.get_file_path(), '%s-%s.txt' % (charter.name, next_approved_revision(charter.rev))) shutil.copy(old, new) except IOError: log("There was an error copying %s to %s" % (old, new)) @@ -101,7 +101,7 @@ def fix_charter_revision_after_approval(charter, by): events = [] e = NewRevisionDocEvent(doc=charter, by=by, type="new_revision") e.rev = next_approved_revision(charter.rev) - e.desc = "New version available: %s-%s.txt" % (charter.canonical_name(), e.rev) + e.desc = "New version available: %s-%s.txt" % (charter.name, e.rev) e.save() events.append(e) diff --git a/ietf/doc/utils_search.py b/ietf/doc/utils_search.py index 31aedda0d..3c5bfeab4 100644 --- a/ietf/doc/utils_search.py +++ b/ietf/doc/utils_search.py @@ -93,7 +93,7 @@ def fill_in_document_table_attributes(docs, have_telechat_date=False): # emulate canonical name which is used by a lot of the utils # d.canonical_name = wrap_value(rfc_aliases[d.pk] if d.pk in rfc_aliases else d.name) - if d.rfc_number() != None and d.latest_event_cache["published_rfc"]: + if d.type_id == "rfc" and d.latest_event_cache["published_rfc"]: d.latest_revision_date = d.latest_event_cache["published_rfc"].time elif d.latest_event_cache["new_revision"]: d.latest_revision_date = d.latest_event_cache["new_revision"].time @@ -164,12 +164,11 @@ def fill_in_document_table_attributes(docs, have_telechat_date=False): ) ) for rel in xed_by: - d = doc_dict[rel.target.document.id] - s = rel_rfc_aliases[rel.source_id] + d = doc_dict[rel.target.id] if rel.relationship_id == "obs": - d.obsoleted_by_list.append(s) + d.obsoleted_by_list.append(rel.source) elif rel.relationship_id == "updates": - d.updated_by_list.append(s) + d.updated_by_list.append(rel.source) def augment_docs_with_related_docs_info(docs): """Augment all documents with related documents information. @@ -217,7 +216,7 @@ def prepare_document_table(request, docs, query=None, max_results=200): res = [] - rfc_num = d.rfc_number() + rfc_num = num(d.rfc_number) if d.rfc_number else None if d.type_id == "draft": res.append(num(["Active", "Expired", "Replaced", "Withdrawn", "RFC"].index(d.search_heading.split()[0]))) @@ -232,23 +231,23 @@ def prepare_document_table(request, docs, query=None, max_results=200): elif sort_key == "date": res.append(str(d.latest_revision_date.astimezone(ZoneInfo(settings.TIME_ZONE)))) elif sort_key == "status": - if rfc_num != None: - res.append(num(rfc_num)) + if rfc_num is not None: + res.append(rfc_num) else: res.append(num(d.get_state().order) if d.get_state() else None) elif sort_key == "ipr": res.append(len(d.ipr())) elif sort_key == "ad": - if rfc_num != None: - res.append(num(rfc_num)) + if rfc_num is not None: + res.append(rfc_num) elif d.get_state_slug() == "active": if d.get_state("draft-iesg"): res.append(d.get_state("draft-iesg").order) else: res.append(0) else: - if rfc_num != None: - res.append(num(rfc_num)) + if rfc_num is not None: + res.append(rfc_num) else: res.append(d.canonical_name()) diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py index 98fb12610..879dd0c3c 100644 --- a/ietf/doc/views_ballot.py +++ b/ietf/doc/views_ballot.py @@ -953,7 +953,13 @@ def approve_downrefs(request, name): login = request.user.person - downrefs_to_rfc = [rel for rel in doc.relateddocument_set.all() if rel.is_downref() and not rel.is_approved_downref() and rel.target.document.is_rfc()] + downrefs_to_rfc = [ + rel + for rel in doc.relateddocument_set.all() + if rel.is_downref() + and not rel.is_approved_downref() + and rel.target.type_id == "rfc" + ] downrefs_to_rfc_qs = RelatedDocument.objects.filter(pk__in=[r.pk for r in downrefs_to_rfc]) @@ -968,12 +974,12 @@ def approve_downrefs(request, name): c = DocEvent(type="downref_approved", doc=rel.source, rev=rel.source.rev, by=login) c.desc = "Downref to RFC %s approved by Last Call for %s-%s" % ( - rel.target.document.rfc_number(), rel.source, rel.source.rev) + rel.target.rfc_number, rel.source, rel.source.rev) c.save() - c = DocEvent(type="downref_approved", doc=rel.target.document, - rev=rel.target.document.rev, by=login) + c = DocEvent(type="downref_approved", doc=rel.target, + rev=rel.target.rev, by=login) c.desc = "Downref to RFC %s approved by Last Call for %s-%s" % ( - rel.target.document.rfc_number(), rel.source, rel.source.rev) + rel.target.rfc_number, rel.source, rel.source.rev) c.save() return HttpResponseRedirect(doc.get_absolute_url()) diff --git a/ietf/doc/views_charter.py b/ietf/doc/views_charter.py index d3173291d..b3d9d06c3 100644 --- a/ietf/doc/views_charter.py +++ b/ietf/doc/views_charter.py @@ -3,11 +3,11 @@ import datetime -import io import json -import os import textwrap +from pathlib import Path + from django.http import HttpResponseRedirect, HttpResponseNotFound, Http404 from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse as urlreverse @@ -32,7 +32,7 @@ from ietf.doc.utils_charter import ( historic_milestones_for_charter, generate_ballot_writeup, generate_issue_ballot_mail, next_revision, derive_new_work_text, change_group_state_after_charter_approval, fix_charter_revision_after_approval, - split_charter_name) + split_charter_name, charter_name_for_group) from ietf.doc.mails import email_state_changed, email_charter_internal_review from ietf.group.mails import email_admin_re_charter from ietf.group.models import Group, ChangeStateGroupEvent, MilestoneGroupEvent @@ -42,6 +42,7 @@ from ietf.ietfauth.utils import has_role, role_required from ietf.name.models import GroupStateName from ietf.person.models import Person from ietf.utils.history import find_history_active_at +from ietf.utils.log import assertion from ietf.utils.mail import send_mail_preformatted from ietf.utils.textupload import get_cleaned_text_file_content from ietf.utils.response import permission_denied @@ -362,38 +363,41 @@ class UploadForm(forms.Form): @login_required def submit(request, name, option=None): - if not name.startswith('charter-'): - raise Http404 - + # Charters are named "charter--" charter = Document.objects.filter(type="charter", name=name).first() if charter: group = charter.group - charter_canonical_name = charter.canonical_name() + assertion("charter.name == charter_name_for_group(group)") charter_rev = charter.rev else: top_org, group_acronym = split_charter_name(name) group = get_object_or_404(Group, acronym=group_acronym) - charter_canonical_name = name + if name != charter_name_for_group(group): + raise Http404 # do not allow creation of misnamed charters charter_rev = "00-00" - if not can_manage_all_groups_of_type(request.user, group.type_id) or not group.features.has_chartering_process: + if ( + not can_manage_all_groups_of_type(request.user, group.type_id) + or not group.features.has_chartering_process + ): permission_denied(request, "You don't have permission to access this view.") - - path = os.path.join(settings.CHARTER_PATH, '%s-%s.txt' % (charter_canonical_name, charter_rev)) - not_uploaded_yet = charter_rev.endswith("-00") and not os.path.exists(path) + charter_filename = Path(settings.CHARTER_PATH) / f"{name}-{charter_rev}.txt" + not_uploaded_yet = charter_rev.endswith("-00") and not charter_filename.exists() if not_uploaded_yet or not charter: # this case is special - we recently chartered or rechartered and have no file yet next_rev = charter_rev else: # search history for possible collisions with abandoned efforts - prev_revs = list(charter.history_set.order_by('-time').values_list('rev', flat=True)) + prev_revs = list( + charter.history_set.order_by("-time").values_list("rev", flat=True) + ) next_rev = next_revision(charter.rev) while next_rev in prev_revs: next_rev = next_revision(next_rev) - if request.method == 'POST': + if request.method == "POST": form = UploadForm(request.POST, request.FILES) if form.is_valid(): # Also save group history so we can search for it @@ -408,9 +412,11 @@ def submit(request, name, option=None): abstract=group.name, rev=next_rev, ) - DocAlias.objects.create(name=charter.name).docs.add(charter) + DocAlias.objects.create(name=name).docs.add(charter) - charter.set_state(State.objects.get(used=True, type="charter", slug="notrev")) + charter.set_state( + State.objects.get(used=True, type="charter", slug="notrev") + ) group.charter = charter group.save() @@ -418,56 +424,74 @@ def submit(request, name, option=None): charter.rev = next_rev events = [] - e = NewRevisionDocEvent(doc=charter, by=request.user.person, type="new_revision") - e.desc = "New version available: %s-%s.txt" % (charter.canonical_name(), charter.rev) + e = NewRevisionDocEvent( + doc=charter, by=request.user.person, type="new_revision" + ) + e.desc = "New version available: %s-%s.txt" % ( + charter.name, + charter.rev, + ) e.rev = charter.rev e.save() events.append(e) # Save file on disk - filename = os.path.join(settings.CHARTER_PATH, '%s-%s.txt' % (charter.canonical_name(), charter.rev)) - with io.open(filename, 'w', encoding='utf-8') as destination: - if form.cleaned_data['txt']: - destination.write(form.cleaned_data['txt']) + charter_filename = charter_filename.with_name( + f"{name}-{charter.rev}.txt" + ) # update rev + with charter_filename.open("w", encoding="utf-8") as destination: + if form.cleaned_data["txt"]: + destination.write(form.cleaned_data["txt"]) else: - destination.write(form.cleaned_data['content']) + destination.write(form.cleaned_data["content"]) - if option in ['initcharter','recharter'] and charter.ad == None: - charter.ad = getattr(group.ad_role(),'person',None) + if option in ["initcharter", "recharter"] and charter.ad == None: + charter.ad = getattr(group.ad_role(), "person", None) charter.save_with_history(events) if option: - return redirect('ietf.doc.views_charter.change_state', name=charter.name, option=option) + return redirect( + "ietf.doc.views_charter.change_state", + name=charter.name, + option=option, + ) else: return redirect("ietf.doc.views_doc.document_main", name=charter.name) else: - init = { "content": "" } + init = {"content": ""} if not_uploaded_yet and charter: # use text from last approved revision last_approved = charter.rev.split("-")[0] - h = charter.history_set.filter(rev=last_approved).order_by("-time", "-id").first() + h = ( + charter.history_set.filter(rev=last_approved) + .order_by("-time", "-id") + .first() + ) if h: - charter_canonical_name = h.canonical_name() - charter_rev = h.rev - - filename = os.path.join(settings.CHARTER_PATH, '%s-%s.txt' % (charter_canonical_name, charter_rev)) + assertion("h.name == charter_name_for_group(group)") + charter_filename = charter_filename.with_name( + f"{name}-{h.rev}.txt" + ) # update rev try: - with io.open(filename, 'r') as f: - init["content"] = f.read() + init["content"] = charter_filename.read_text() except IOError: pass form = UploadForm(initial=init) fill_in_charter_info(group) - return render(request, 'doc/charter/submit.html', { - 'form': form, - 'next_rev': next_rev, - 'group': group, - 'name': name, - }) + return render( + request, + "doc/charter/submit.html", + { + "form": form, + "next_rev": next_rev, + "group": group, + "name": name, + }, + ) class ActionAnnouncementTextForm(forms.Form): announcement_text = forms.CharField(widget=forms.Textarea, required=True, strip=False) @@ -484,7 +508,7 @@ class ReviewAnnouncementTextForm(forms.Form): return self.cleaned_data["announcement_text"].replace("\r", "") -@role_required('Area Director','Secretariat') +@role_required("Area Director", "Secretariat") def review_announcement_text(request, name): """Editing of review announcement text""" charter = get_object_or_404(Document, type="charter", name=name) @@ -493,7 +517,9 @@ def review_announcement_text(request, name): by = request.user.person existing = charter.latest_event(WriteupDocEvent, type="changed_review_announcement") - existing_new_work = charter.latest_event(WriteupDocEvent, type="changed_new_work_text") + existing_new_work = charter.latest_event( + WriteupDocEvent, type="changed_new_work_text" + ) if not existing: (existing, existing_new_work) = default_review_text(group, charter, by) @@ -506,19 +532,23 @@ def review_announcement_text(request, name): existing_new_work.by = by existing_new_work.type = "changed_new_work_text" existing_new_work.desc = "%s review text was changed" % group.type.name - existing_new_work.text = derive_new_work_text(existing.text,group) + existing_new_work.text = derive_new_work_text(existing.text, group) existing_new_work.time = timezone.now() - form = ReviewAnnouncementTextForm(initial=dict(announcement_text=escape(existing.text),new_work_text=escape(existing_new_work.text))) + form = ReviewAnnouncementTextForm( + initial=dict( + announcement_text=escape(existing.text), + new_work_text=escape(existing_new_work.text), + ) + ) - if request.method == 'POST': + if request.method == "POST": form = ReviewAnnouncementTextForm(request.POST) if "save_text" in request.POST and form.is_valid(): - now = timezone.now() events = [] - t = form.cleaned_data['announcement_text'] + t = form.cleaned_data["announcement_text"] if t != existing.text: e = WriteupDocEvent(doc=charter, rev=charter.rev) e.by = by @@ -532,11 +562,11 @@ def review_announcement_text(request, name): existing.save() events.append(existing) - t = form.cleaned_data['new_work_text'] + t = form.cleaned_data["new_work_text"] if t != existing_new_work.text: e = WriteupDocEvent(doc=charter, rev=charter.rev) e.by = by - e.type = "changed_new_work_text" + e.type = "changed_new_work_text" e.desc = "%s new work message text was changed" % (group.type.name) e.text = t e.time = now @@ -549,33 +579,71 @@ def review_announcement_text(request, name): charter.save_with_history(events) if request.GET.get("next", "") == "approve": - return redirect('ietf.doc.views_charter.approve', name=charter.canonical_name()) + return redirect( + "ietf.doc.views_charter.approve", name=charter.name + ) - return redirect('ietf.doc.views_doc.document_writeup', name=charter.canonical_name()) + return redirect( + "ietf.doc.views_doc.document_writeup", name=charter.name + ) if "regenerate_text" in request.POST: (existing, existing_new_work) = default_review_text(group, charter, by) existing.save() existing_new_work.save() - form = ReviewAnnouncementTextForm(initial=dict(announcement_text=escape(existing.text), - new_work_text=escape(existing_new_work.text))) + form = ReviewAnnouncementTextForm( + initial=dict( + announcement_text=escape(existing.text), + new_work_text=escape(existing_new_work.text), + ) + ) - if any(x in request.POST for x in ['send_annc_only','send_nw_only','send_both']) and form.is_valid(): - if any(x in request.POST for x in ['send_annc_only','send_both']): - parsed_msg = send_mail_preformatted(request, form.cleaned_data['announcement_text']) - messages.success(request, "The email To: '%s' with Subject: '%s' has been sent." % (parsed_msg["To"],parsed_msg["Subject"],)) - if any(x in request.POST for x in ['send_nw_only','send_both']): - parsed_msg = send_mail_preformatted(request, form.cleaned_data['new_work_text']) - messages.success(request, "The email To: '%s' with Subject: '%s' has been sent." % (parsed_msg["To"],parsed_msg["Subject"],)) - return redirect('ietf.doc.views_doc.document_writeup', name=charter.name) + if ( + any( + x in request.POST + for x in ["send_annc_only", "send_nw_only", "send_both"] + ) + and form.is_valid() + ): + if any(x in request.POST for x in ["send_annc_only", "send_both"]): + parsed_msg = send_mail_preformatted( + request, form.cleaned_data["announcement_text"] + ) + messages.success( + request, + "The email To: '%s' with Subject: '%s' has been sent." + % ( + parsed_msg["To"], + parsed_msg["Subject"], + ), + ) + if any(x in request.POST for x in ["send_nw_only", "send_both"]): + parsed_msg = send_mail_preformatted( + request, form.cleaned_data["new_work_text"] + ) + messages.success( + request, + "The email To: '%s' with Subject: '%s' has been sent." + % ( + parsed_msg["To"], + parsed_msg["Subject"], + ), + ) + return redirect("ietf.doc.views_doc.document_writeup", name=charter.name) - return render(request, 'doc/charter/review_announcement_text.html', - dict(charter=charter, - back_url=urlreverse('ietf.doc.views_doc.document_writeup', kwargs=dict(name=charter.name)), - announcement_text_form=form, - )) + return render( + request, + "doc/charter/review_announcement_text.html", + dict( + charter=charter, + back_url=urlreverse( + "ietf.doc.views_doc.document_writeup", kwargs=dict(name=charter.name) + ), + announcement_text_form=form, + ), + ) -@role_required('Area Director','Secretariat') +@role_required("Area Director", "Secretariat") def action_announcement_text(request, name): """Editing of action announcement text""" charter = get_object_or_404(Document, type="charter", name=name) @@ -590,16 +658,18 @@ def action_announcement_text(request, name): if not existing: raise Http404 - form = ActionAnnouncementTextForm(initial=dict(announcement_text=escape(existing.text))) + form = ActionAnnouncementTextForm( + initial=dict(announcement_text=escape(existing.text)) + ) - if request.method == 'POST': + if request.method == "POST": form = ActionAnnouncementTextForm(request.POST) if "save_text" in request.POST and form.is_valid(): - t = form.cleaned_data['announcement_text'] + t = form.cleaned_data["announcement_text"] if t != existing.text: e = WriteupDocEvent(doc=charter, rev=charter.rev) e.by = by - e.type = "changed_action_announcement" + e.type = "changed_action_announcement" e.desc = "%s action text was changed" % group.type.name e.text = t e.save() @@ -607,25 +677,46 @@ def action_announcement_text(request, name): existing.save() if request.GET.get("next", "") == "approve": - return redirect('ietf.doc.views_charter.approve', name=charter.canonical_name()) + return redirect( + "ietf.doc.views_charter.approve", name=charter.name + ) - return redirect('ietf.doc.views_doc.document_writeup', name=charter.canonical_name()) + return redirect( + "ietf.doc.views_doc.document_writeup", name=charter.name + ) if "regenerate_text" in request.POST: e = default_action_text(group, charter, by) e.save() - form = ActionAnnouncementTextForm(initial=dict(announcement_text=escape(e.text))) + form = ActionAnnouncementTextForm( + initial=dict(announcement_text=escape(e.text)) + ) if "send_text" in request.POST and form.is_valid(): - parsed_msg = send_mail_preformatted(request, form.cleaned_data['announcement_text']) - messages.success(request, "The email To: '%s' with Subject: '%s' has been sent." % (parsed_msg["To"],parsed_msg["Subject"],)) - return redirect('ietf.doc.views_doc.document_writeup', name=charter.name) + parsed_msg = send_mail_preformatted( + request, form.cleaned_data["announcement_text"] + ) + messages.success( + request, + "The email To: '%s' with Subject: '%s' has been sent." + % ( + parsed_msg["To"], + parsed_msg["Subject"], + ), + ) + return redirect("ietf.doc.views_doc.document_writeup", name=charter.name) - return render(request, 'doc/charter/action_announcement_text.html', - dict(charter=charter, - back_url=urlreverse('ietf.doc.views_doc.document_writeup', kwargs=dict(name=charter.name)), - announcement_text_form=form, - )) + return render( + request, + "doc/charter/action_announcement_text.html", + dict( + charter=charter, + back_url=urlreverse( + "ietf.doc.views_doc.document_writeup", kwargs=dict(name=charter.name) + ), + announcement_text_form=form, + ), + ) class BallotWriteupForm(forms.Form): ballot_writeup = forms.CharField(widget=forms.Textarea, required=True, strip=False) @@ -806,33 +897,37 @@ def approve(request, name): dict(charter=charter, announcement=escape(announcement))) -def charter_with_milestones_txt(request, name, rev): - charter = get_object_or_404(Document, type="charter", docalias__name=name) - revision_event = charter.latest_event(NewRevisionDocEvent, type="new_revision", rev=rev) +def charter_with_milestones_txt(request, name, rev): + charter = get_object_or_404(Document, type="charter", name=name) + + revision_event = charter.latest_event( + NewRevisionDocEvent, type="new_revision", rev=rev + ) if not revision_event: return HttpResponseNotFound("Revision %s not found in database" % rev) # read charter text c = find_history_active_at(charter, revision_event.time) or charter - filename = '%s-%s.txt' % (c.canonical_name(), rev) - - charter_text = "" - + filename = Path(settings.CHARTER_PATH) / f"{c.name}-{rev}.txt" try: - with io.open(os.path.join(settings.CHARTER_PATH, filename), 'r') as f: - charter_text = force_str(f.read(), errors='ignore') + with filename.open() as f: + charter_text = force_str(f.read(), errors="ignore") except IOError: - charter_text = "Error reading charter text %s" % filename + charter_text = f"Error reading charter text {filename.name}" milestones = historic_milestones_for_charter(charter, rev) # wrap the output nicely - wrapper = textwrap.TextWrapper(initial_indent="", subsequent_indent=" " * 11, width=80, break_long_words=False) + wrapper = textwrap.TextWrapper( + initial_indent="", subsequent_indent=" " * 11, width=80, break_long_words=False + ) for m in milestones: m.desc_filled = wrapper.fill(m.desc) - return render(request, 'doc/charter/charter_with_milestones.txt', - dict(charter_text=charter_text, - milestones=milestones), - content_type="text/plain; charset=%s"%settings.DEFAULT_CHARSET) + return render( + request, + "doc/charter/charter_with_milestones.txt", + dict(charter_text=charter_text, milestones=milestones), + content_type="text/plain; charset=%s" % settings.DEFAULT_CHARSET, + ) diff --git a/ietf/doc/views_conflict_review.py b/ietf/doc/views_conflict_review.py index 5efd62dbe..b25f1ad6b 100644 --- a/ietf/doc/views_conflict_review.py +++ b/ietf/doc/views_conflict_review.py @@ -113,7 +113,7 @@ def send_conflict_review_ad_changed_email(request, review, event): by = request.user.person, event = event, review = review, - reviewed_doc = review.relateddocument_set.get(relationship__slug='conflrev').target.document, + reviewed_doc = review.relateddocument_set.get(relationship__slug='conflrev').target, review_url = settings.IDTRACKER_BASE_URL+review.get_absolute_url(), ) ) @@ -128,7 +128,7 @@ def send_conflict_review_started_email(request, review): cc = addrs.cc, by = request.user.person, review = review, - reviewed_doc = review.relateddocument_set.get(relationship__slug='conflrev').target.document, + reviewed_doc = review.relateddocument_set.get(relationship__slug='conflrev').target, review_url = settings.IDTRACKER_BASE_URL+review.get_absolute_url(), ) ) @@ -137,7 +137,7 @@ def send_conflict_review_started_email(request, review): addrs = gather_address_lists('conflrev_requested_iana',doc=review).as_strings(compact=False) email_iana(request, - review.relateddocument_set.get(relationship__slug='conflrev').target.document, + review.relateddocument_set.get(relationship__slug='conflrev').target, addrs.to, msg, cc=addrs.cc) @@ -155,7 +155,7 @@ def send_conflict_eval_email(request,review): send_mail_preformatted(request,msg,override=override) addrs = gather_address_lists('ballot_issued_iana',doc=review).as_strings() email_iana(request, - review.relateddocument_set.get(relationship__slug='conflrev').target.document, + review.relateddocument_set.get(relationship__slug='conflrev').target, addrs.to, msg, addrs.cc) @@ -247,7 +247,7 @@ def submit(request, name): {'form': form, 'next_rev': next_rev, 'review' : review, - 'conflictdoc' : review.relateddocument_set.get(relationship__slug='conflrev').target.document, + 'conflictdoc' : review.relateddocument_set.get(relationship__slug='conflrev').target, }) @role_required("Area Director", "Secretariat") @@ -275,7 +275,7 @@ def edit_ad(request, name): form = AdForm(initial=init) - conflictdoc = review.relateddocument_set.get(relationship__slug='conflrev').target.document + conflictdoc = review.relateddocument_set.get(relationship__slug='conflrev').target titletext = 'the conflict review of %s-%s' % (conflictdoc.canonical_name(),conflictdoc.rev) return render(request, 'doc/change_ad.html', {'form': form, @@ -287,7 +287,7 @@ def edit_ad(request, name): def default_approval_text(review): current_text = review.text_or_error() # pyflakes:ignore - conflictdoc = review.relateddocument_set.get(relationship__slug='conflrev').target.document + conflictdoc = review.relateddocument_set.get(relationship__slug='conflrev').target if conflictdoc.stream_id=='ise': receiver = 'Independent Submissions Editor' elif conflictdoc.stream_id=='irtf': @@ -365,7 +365,7 @@ def approve_conflict_review(request, name): return render(request, 'doc/conflict_review/approve.html', dict( review = review, - conflictdoc = review.relateddocument_set.get(relationship__slug='conflrev').target.document, + conflictdoc = review.relateddocument_set.get(relationship__slug='conflrev').target, form = form, )) @@ -416,7 +416,7 @@ def start_review_sanity_check(request, name): raise Http404 # sanity check that there's not already a conflict review document for this document - if [ rel.source for alias in doc_to_review.docalias.all() for rel in alias.relateddocument_set.filter(relationship='conflrev') ]: + if [ rel.source for rel in doc_to_review.targets_related.filter(relationship='conflrev') ]: raise Http404 return doc_to_review @@ -452,7 +452,7 @@ def build_conflict_review_document(login, doc_to_review, ad, notify, create_in_s DocAlias.objects.create( name=review_name).docs.add( conflict_review ) - conflict_review.relateddocument_set.create(target=DocAlias.objects.get(name=doc_to_review.name),relationship_id='conflrev') + conflict_review.relateddocument_set.create(target=doc_to_review, relationship_id='conflrev') c = DocEvent(type="added_comment", doc=conflict_review, rev=conflict_review.rev, by=login) c.desc = "IETF conflict review requested" diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 5a20bdc12..de67da3b2 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -54,13 +54,13 @@ from django.contrib.staticfiles import finders import debug # pyflakes:ignore -from ietf.doc.models import ( Document, DocAlias, DocHistory, DocEvent, BallotDocEvent, BallotType, +from ietf.doc.models import ( Document, DocHistory, DocEvent, BallotDocEvent, BallotType, ConsensusDocEvent, NewRevisionDocEvent, TelechatDocEvent, WriteupDocEvent, IanaExpertDocEvent, IESG_BALLOT_ACTIVE_STATES, STATUSCHANGE_RELATIONS, DocumentActionHolder, DocumentAuthor, RelatedDocument, RelatedDocHistory) from ietf.doc.utils import (augment_events_with_revision, can_adopt_draft, can_unadopt_draft, get_chartering_type, get_tags_for_stream_id, - needed_ballot_positions, nice_consensus, prettify_std_name, update_telechat, has_same_ballot, + needed_ballot_positions, nice_consensus, update_telechat, has_same_ballot, get_initial_notify, make_notify_changed_event, make_rev_history, default_consensus, add_events_message_info, get_unicode_document_content, augment_docs_and_user_with_user_info, irsg_needed_ballot_positions, add_action_holder_change_event, @@ -180,13 +180,12 @@ def interesting_doc_relations(doc): else: raise TypeError("Expected this method to be called with a Document or DocHistory object") - that_relationships = STATUSCHANGE_RELATIONS + ('conflrev', 'replaces', 'possibly_replaces', 'updates', 'obs') + that_relationships = STATUSCHANGE_RELATIONS + ('conflrev', 'replaces', 'possibly_replaces', 'updates', 'obs', 'became_rfc') - that_doc_relationships = ('replaces', 'possibly_replaces', 'updates', 'obs') + that_doc_relationships = ('replaces', 'possibly_replaces', 'updates', 'obs', 'became_rfc') - # TODO: This returns the relationships in database order, which may not be the order we want to display them in. - interesting_relations_that = cls.objects.filter(target__docs=target, relationship__in=that_relationships).select_related('source') - interesting_relations_that_doc = cls.objects.filter(source=doc, relationship__in=that_doc_relationships).prefetch_related('target__docs') + interesting_relations_that = cls.objects.filter(target=target, relationship__in=that_relationships).select_related('source') + interesting_relations_that_doc = cls.objects.filter(source=doc, relationship__in=that_doc_relationships).prefetch_related('target') return interesting_relations_that, interesting_relations_that_doc @@ -197,11 +196,10 @@ def document_main(request, name, rev=None, document_html=False): doc = get_object_or_404(Document.objects.select_related(), docalias__name=name) # take care of possible redirections - aliases = DocAlias.objects.filter(docs=doc).values_list("name", flat=True) - if document_html is False and rev==None and doc.type_id == "draft" and not name.startswith("rfc"): - for a in aliases: - if a.startswith("rfc"): - return redirect("ietf.doc.views_doc.document_main", name=a) + if document_html is False and rev is None: + became_rfc = next(iter(doc.related_that_doc("became_rfc")), None) + if became_rfc: + return redirect("ietf.doc.views_doc.document_main", name=became_rfc.name) revisions = [] for h in doc.history_set.order_by("time", "id"): @@ -243,7 +241,163 @@ def document_main(request, name, rev=None, document_html=False): # specific document types - if doc.type_id == "draft": + if doc.type_id == "rfc": + split_content = request.COOKIES.get("full_draft", settings.USER_PREFERENCE_DEFAULTS["full_draft"]) == "off" + if request.GET.get('include_text') == "0": + split_content = True + elif request.GET.get('include_text') == "1": + split_content = False + else: + pass + + interesting_relations_that, interesting_relations_that_doc = interesting_doc_relations(doc) + + can_edit = has_role(request.user, ("Area Director", "Secretariat")) + can_edit_authors = has_role(request.user, ("Secretariat")) + + stream_slugs = StreamName.objects.values_list("slug", flat=True) + # For some reason, AnonymousUser has __iter__, but is not iterable, + # which causes problems in the filter() below. Work around this: + if request.user.is_authenticated: + roles = Role.objects.filter(group__acronym__in=stream_slugs, person__user=request.user) + roles = group_features_role_filter(roles, request.user.person, 'docman_roles') + else: + roles = [] + + can_change_stream = bool(can_edit or roles) + + file_urls, found_types = build_file_urls(doc) + content = doc.text_or_error() # pyflakes:ignore + content = markup_txt.markup(maybe_split(content, split=split_content)) + + if not found_types: + content = "This RFC is not currently available online." + split_content = False + elif "txt" not in found_types: + content = "This RFC is not available in plain text format." + split_content = False + + # mailing list search archive + search_archive = "www.ietf.org/mail-archive/web/" + if doc.stream_id == "ietf" and group.type_id == "wg" and group.list_archive: + search_archive = group.list_archive + + search_archive = quote(search_archive, safe="~") + + # status changes + status_changes = [] + proposed_status_changes = [] + for r in interesting_relations_that.filter(relationship__in=STATUSCHANGE_RELATIONS): + state_slug = r.source.get_state_slug() + if state_slug in ('appr-sent', 'appr-pend'): + status_changes.append(r) + elif state_slug in ('needshep','adrev','iesgeval','defer','appr-pr'): + proposed_status_changes.append(r) + else: + pass + + presentations = doc.future_presentations() + + augment_docs_and_user_with_user_info([doc], request.user) + + exp_comment = doc.latest_event(IanaExpertDocEvent,type="comment") + iana_experts_comment = exp_comment and exp_comment.desc + + # Do not show the Auth48 URL in the "Additional URLs" section + additional_urls = doc.documenturl_set.exclude(tag_id='auth48') + + html = None + js = None + css = None + diff_revisions = None + simple_diff_revisions = None + if document_html: + diff_revisions=get_diff_revisions(request, name, doc if isinstance(doc,Document) else doc.doc) + simple_diff_revisions = [t[1] for t in diff_revisions if t[0] == doc.name] + simple_diff_revisions.reverse() + if rev and rev != doc.rev: + # No DocHistory was found matching rev - snapshot will be false + # and doc will be a Document object, not a DocHistory + snapshot = True + doc = doc.fake_history_obj(rev) + else: + html = doc.html_body() + if request.COOKIES.get("pagedeps") == "inline": + js = Path(finders.find("ietf/js/document_html.js")).read_text() + css = Path(finders.find("ietf/css/document_html_inline.css")).read_text() + if html: + css += Path(finders.find("ietf/css/document_html_txt.css")).read_text() + draft_that_became_rfc = None + became_rfc_alias = next(iter(doc.related_that("became_rfc")), None) + if became_rfc_alias: + draft_that_became_rfc = became_rfc_alias.document + # submission + submission = "" + if group is None: + submission = "unknown" + elif group.type_id == "individ": + submission = "individual" + elif group.type_id == "area" and doc.stream_id == "ietf": + submission = "individual in %s area" % group.acronym + else: + if group.features.acts_like_wg and not group.type_id == "edwg": + submission = "%s %s" % (group.acronym, group.type) + else: + submission = group.acronym + submission = '%s' % (group.about_url(), submission) + # Should be unreachable? + if ( + draft_that_became_rfc + and draft_that_became_rfc.stream_id + and draft_that_became_rfc.get_state_slug( + "draft-stream-%s" % draft_that_became_rfc.stream_id + ) + == "c-adopt" + ): + submission = "candidate for %s" % submission + + + # todo replace document_html? + return render(request, "doc/document_rfc.html" if document_html is False else "doc/document_html.html", + dict(doc=doc, + document_html=document_html, + css=css, + js=js, + html=html, + group=group, + top=top, + name=doc.name, + content=content, + split_content=split_content, + revisions=simple_diff_revisions if document_html else revisions, + snapshot=snapshot, + latest_rev=latest_rev, + can_edit=can_edit, + can_edit_authors=can_edit_authors, + can_change_stream=can_change_stream, + rfc_number=doc.rfc_number, + draft_name=draft_that_became_rfc and draft_that_became_rfc.name, + updates=interesting_relations_that_doc.filter(relationship="updates"), + updated_by=interesting_relations_that.filter(relationship="updates"), + obsoletes=interesting_relations_that_doc.filter(relationship="obs"), + obsoleted_by=interesting_relations_that.filter(relationship="obs"), + status_changes=status_changes, + proposed_status_changes=proposed_status_changes, + has_errata=doc.pk and doc.tags.filter(slug="errata"), # doc.pk == None if using a fake_history_obj + file_urls=file_urls, + additional_urls=additional_urls, + rfc_editor_state=doc.get_state("draft-rfceditor"), + iana_review_state=doc.get_state("draft-iana-review"), + iana_action_state=doc.get_state("draft-iana-action"), + iana_experts_state=doc.get_state("draft-iana-experts"), + iana_experts_comment=iana_experts_comment, + search_archive=search_archive, + presentations=presentations, + diff_revisions=diff_revisions, + submission=submission + )) + + elif doc.type_id == "draft": split_content = request.COOKIES.get("full_draft", settings.USER_PREFERENCE_DEFAULTS["full_draft"]) == "off" if request.GET.get('include_text') == "0": split_content = True @@ -281,43 +435,13 @@ def document_main(request, name, rev=None, document_html=False): is_author = request.user.is_authenticated and doc.documentauthor_set.filter(person__user=request.user).exists() can_view_possibly_replaces = can_edit_replaces or is_author - rfc_number = name[3:] if name.startswith("rfc") else None - draft_name = None - for a in aliases: - if a.startswith("draft"): - draft_name = a - - rfc_aliases = [prettify_std_name(a) for a in aliases - if a.startswith("fyi") or a.startswith("std") or a.startswith("bcp")] - latest_revision = None - # Workaround to allow displaying last rev of draft that became rfc as a draft - # This should be unwound when RFCs become their own documents. - if snapshot: - doc.name = doc.doc.name - name = doc.doc.name - else: - name = doc.name - file_urls, found_types = build_file_urls(doc) - if not snapshot and doc.get_state_slug() == "rfc": - # content - content = doc.text_or_error() # pyflakes:ignore - content = markup_txt.markup(maybe_split(content, split=split_content)) - content = doc.text_or_error() # pyflakes:ignore content = markup_txt.markup(maybe_split(content, split=split_content)) - if not snapshot and doc.get_state_slug() == "rfc": - if not found_types: - content = "This RFC is not currently available online." - split_content = False - elif "txt" not in found_types: - content = "This RFC is not available in plain text format." - split_content = False - else: - latest_revision = doc.latest_event(NewRevisionDocEvent, type="new_revision") + latest_revision = doc.latest_event(NewRevisionDocEvent, type="new_revision") # ballot iesg_ballot_summary = None @@ -497,7 +621,7 @@ def document_main(request, name, rev=None, document_html=False): augment_docs_and_user_with_user_info([doc], request.user) - published = doc.latest_event(type="published_rfc") + published = doc.latest_event(type="published_rfc") # todo rethink this now that published_rfc is on rfc started_iesg_process = doc.latest_event(type="started_iesg_process") review_assignments = review_assignments_to_list_for_docs([doc]).get(doc.name, []) @@ -516,12 +640,6 @@ def document_main(request, name, rev=None, document_html=False): # Do not show the Auth48 URL in the "Additional URLs" section additional_urls = doc.documenturl_set.exclude(tag_id='auth48') - # Stream description passing test - if doc.stream != None: - stream_desc = doc.stream.desc - else: - stream_desc = "(None)" - html = None js = None css = None @@ -552,12 +670,11 @@ def document_main(request, name, rev=None, document_html=False): html=html, group=group, top=top, - name=name, + name=doc.name, content=content, split_content=split_content, revisions=simple_diff_revisions if document_html else revisions, snapshot=snapshot, - stream_desc=stream_desc, latest_revision=latest_revision, latest_rev=latest_rev, can_edit=can_edit, @@ -575,8 +692,7 @@ def document_main(request, name, rev=None, document_html=False): can_request_review=can_request_review, can_submit_unsolicited_review_for_teams=can_submit_unsolicited_review_for_teams, - rfc_number=rfc_number, - draft_name=draft_name, + draft_name=doc.name, telechat=telechat, iesg_ballot_summary=iesg_ballot_summary, submission=submission, @@ -593,7 +709,6 @@ def document_main(request, name, rev=None, document_html=False): conflict_reviews=conflict_reviews, status_changes=status_changes, proposed_status_changes=proposed_status_changes, - rfc_aliases=rfc_aliases, has_errata=doc.pk and doc.tags.filter(slug="errata"), # doc.pk == None if using a fake_history_obj published=published, file_urls=file_urls, @@ -623,7 +738,7 @@ def document_main(request, name, rev=None, document_html=False): diff_revisions=diff_revisions )) - if doc.type_id == "charter": + elif doc.type_id == "charter": content = doc.text_or_error() # pyflakes:ignore content = markup_txt.markup(content) @@ -660,7 +775,7 @@ def document_main(request, name, rev=None, document_html=False): can_manage=can_manage, )) - if doc.type_id == "bofreq": + elif doc.type_id == "bofreq": content = markdown.markdown(doc.text_or_error()) editors = bofreq_editors(doc) responsible = bofreq_responsible(doc) @@ -680,7 +795,7 @@ def document_main(request, name, rev=None, document_html=False): editor_can_manage=editor_can_manage, )) - if doc.type_id == "conflrev": + elif doc.type_id == "conflrev": filename = "%s-%s.txt" % (doc.canonical_name(), doc.rev) pathname = os.path.join(settings.CONFLICT_REVIEW_PATH,filename) @@ -695,7 +810,7 @@ def document_main(request, name, rev=None, document_html=False): if doc.get_state_slug() in ("iesgeval", ) and doc.active_ballot(): ballot_summary = needed_ballot_positions(doc, list(doc.active_ballot().active_balloter_positions().values())) - conflictdoc = doc.related_that_doc('conflrev')[0].document + conflictdoc = doc.related_that_doc('conflrev')[0] return render(request, "doc/document_conflict_review.html", dict(doc=doc, @@ -710,7 +825,7 @@ def document_main(request, name, rev=None, document_html=False): approved_states=('appr-reqnopub-pend','appr-reqnopub-sent','appr-noprob-pend','appr-noprob-sent'), )) - if doc.type_id == "statchg": + elif doc.type_id == "statchg": filename = "%s-%s.txt" % (doc.canonical_name(), doc.rev) pathname = os.path.join(settings.STATUS_CHANGE_PATH,filename) @@ -744,7 +859,7 @@ def document_main(request, name, rev=None, document_html=False): sorted_relations=sorted_relations, )) - if doc.type_id in ("slides", "agenda", "minutes", "bluesheets", "procmaterials",): + elif doc.type_id in ("slides", "agenda", "minutes", "bluesheets", "procmaterials",): can_manage_material = can_manage_materials(request.user, doc.group) presentations = doc.future_presentations() if doc.uploaded_filename: @@ -800,7 +915,7 @@ def document_main(request, name, rev=None, document_html=False): )) - if doc.type_id == "review": + elif doc.type_id == "review": basename = "{}.txt".format(doc.name) pathname = os.path.join(doc.get_file_path(), basename) content = get_unicode_document_content(basename, pathname) @@ -826,7 +941,7 @@ def document_main(request, name, rev=None, document_html=False): assignments=assignments, )) - if doc.type_id in ("chatlog", "polls"): + elif doc.type_id in ("chatlog", "polls"): if isinstance(doc,DocHistory): session = doc.doc.sessionpresentation_set.last().session else: @@ -934,7 +1049,7 @@ def document_html(request, name, rev=None): doc = found.documents.get() rev = found.matched_rev - if not requested_rev and doc.is_rfc(): # Someone asked for /doc/html/8989 + if not requested_rev and doc.type_id == "rfc": # Someone asked for /doc/html/8989 if not name.startswith('rfc'): return redirect('ietf.doc.views_doc.document_html', name=doc.canonical_name()) @@ -944,7 +1059,12 @@ def document_html(request, name, rev=None): if not os.path.exists(doc.get_file_name()): raise Http404("File not found: %s" % doc.get_file_name()) - return document_main(request, name=doc.name if requested_rev else doc.canonical_name(), rev=doc.rev if requested_rev or not doc.is_rfc() else None, document_html=True) + return document_main( + request, + name=doc.name if requested_rev else doc.canonical_name(), + rev=doc.rev if requested_rev or doc.type_id != "rfc" else None, + document_html=True, + ) def document_pdfized(request, name, rev=None, ext=None): @@ -1044,8 +1164,8 @@ def get_diff_revisions(request, name, doc): diff_documents = [doc] diff_documents.extend( Document.objects.filter( - docalias__relateddocument__source=doc, - docalias__relateddocument__relationship="replaces", + relateddocument__source=doc, + relateddocument__relationship="replaces", ) ) @@ -1100,21 +1220,46 @@ def document_history(request, name): add_events_message_info(events) # figure out if the current user can add a comment to the history - if doc.type_id == "draft" and doc.group != None: - can_add_comment = bool(has_role(request.user, ("Area Director", "Secretariat", "IRTF Chair", "IANA", "RFC Editor")) or ( - request.user.is_authenticated and - Role.objects.filter(name__in=("chair", "secr"), - group__acronym=doc.group.acronym, - person__user=request.user))) + if doc.type_id in ("draft", "rfc") and doc.group is not None: + can_add_comment = bool( + has_role( + request.user, + ("Area Director", "Secretariat", "IRTF Chair", "IANA", "RFC Editor"), + ) + or ( + request.user.is_authenticated + and Role.objects.filter( + name__in=("chair", "secr"), + group__acronym=doc.group.acronym, + person__user=request.user, + ) + ) + ) else: - can_add_comment = has_role(request.user, ("Area Director", "Secretariat", "IRTF Chair")) - return render(request, "doc/document_history.html", - dict(doc=doc, - top=top, - diff_revisions=diff_revisions, - events=events, - can_add_comment=can_add_comment, - )) + can_add_comment = has_role( + request.user, ("Area Director", "Secretariat", "IRTF Chair") + ) + + # Get related docs whose history should be linked + if doc.type_id == "draft": + related = doc.related_that_doc("became_rfc") + elif doc.type_id == "rfc": + related = doc.related_that("became_rfc") + else: + related = [] + + return render( + request, + "doc/document_history.html", + { + "doc": doc, + "top": top, + "diff_revisions": diff_revisions, + "events": events, + "related": related, + "can_add_comment": can_add_comment, + }, + ) def document_bibtex(request, name, rev=None): @@ -1133,25 +1278,30 @@ def document_bibtex(request, name, rev=None): doc = get_object_or_404(Document, docalias__name=name) - latest_revision = doc.latest_event(NewRevisionDocEvent, type="new_revision") - replaced_by = [d.name for d in doc.related_that("replaces")] - published = doc.latest_event(type="published_rfc") is not None - rfc = latest_revision.doc if latest_revision and latest_revision.doc.get_state_slug() == "rfc" else None + doi = None + draft_became_rfc = None + replaced_by = None + latest_revision = None + if doc.type_id == "draft": + latest_revision = doc.latest_event(NewRevisionDocEvent, type="new_revision") + replaced_by = [d.name for d in doc.related_that("replaces")] + draft_became_rfc_alias = next(iter(doc.related_that_doc("became_rfc")), None) - if rev != None and rev != doc.rev: - # find the entry in the history - for h in doc.history_set.order_by("-time"): - if rev == h.rev: - doc = h - break - - if doc.is_rfc(): + if rev != None and rev != doc.rev: + # find the entry in the history + for h in doc.history_set.order_by("-time"): + if rev == h.rev: + doc = h + break + + if draft_became_rfc_alias: + draft_became_rfc = draft_became_rfc_alias.document + + elif doc.type_id == "rfc": # This needs to be replaced with a lookup, as the mapping may change # over time. Probably by updating ietf/sync/rfceditor.py to add the # as a DocAlias, and use a method on Document to retrieve it. - doi = "10.17487/RFC%04d" % int(doc.rfc_number()) - else: - doi = None + doi = f"10.17487/RFC{doc.rfc_number:04d}" if doc.is_dochistory(): latest_event = doc.latest_event(type='new_revision', rev=rev) @@ -1161,8 +1311,7 @@ def document_bibtex(request, name, rev=None): return render(request, "doc/document_bibtex.bib", dict(doc=doc, replaced_by=replaced_by, - published=published, - rfc=rfc, + published_as=draft_became_rfc, latest_revision=latest_revision, doi=doi, ), @@ -1618,7 +1767,7 @@ def telechat_date(request, name): def doc_titletext(doc): if doc.type.slug=='conflrev': - conflictdoc = doc.relateddocument_set.get(relationship__slug='conflrev').target.document + conflictdoc = doc.relateddocument_set.get(relationship__slug='conflrev').target return 'the conflict review of %s' % conflictdoc.canonical_name() return doc.canonical_name() @@ -2025,9 +2174,16 @@ def idnits2_rfc_status(request): def idnits2_state(request, name, rev=None): doc = get_object_or_404(Document, docalias__name=name) - if doc.type_id!='draft': + if doc.type_id not in ["draft", "rfc"]: raise Http404 - zero_revision = NewRevisionDocEvent.objects.filter(doc=doc,rev='00').first() + zero_revision = None + if doc.type_id == "rfc": + draft_alias = next(iter(doc.related_that('became_rfc')), None) + if draft_alias: + draft = draft_alias.document + zero_revision = NewRevisionDocEvent.objects.filter(doc=draft,rev='00').first() + else: + zero_revision = NewRevisionDocEvent.objects.filter(doc=doc,rev='00').first() if zero_revision: doc.created = zero_revision.time else: diff --git a/ietf/doc/views_downref.py b/ietf/doc/views_downref.py index 1b7b51edb..2668baae3 100644 --- a/ietf/doc/views_downref.py +++ b/ietf/doc/views_downref.py @@ -19,7 +19,7 @@ def downref_registry(request): downref_doc_pairs = [ ] downref_relations = RelatedDocument.objects.filter(relationship_id='downref-approval') for rel in downref_relations: - downref_doc_pairs.append((rel.target.document, rel.source)) + downref_doc_pairs.append((rel.target, rel.source)) return render(request, 'doc/downref.html', { "doc_pairs": downref_doc_pairs, @@ -38,18 +38,18 @@ def downref_registry_add(request): if form.is_valid(): drafts = form.cleaned_data['drafts'] rfc = form.cleaned_data['rfc'] - for da in drafts: - RelatedDocument.objects.create(source=da.document, + for d in drafts: + RelatedDocument.objects.create(source=d, target=rfc, relationship_id='downref-approval') - c = DocEvent(type="downref_approved", doc=da.document, - rev=da.document.rev, by=login) + c = DocEvent(type="downref_approved", doc=d, + rev=d.rev, by=login) c.desc = "Downref to RFC %s approved by Last Call for %s-%s" % ( - rfc.document.rfc_number(), da.name, da.document.rev) + rfc.rfc_number, d.name, d.rev) c.save() - c = DocEvent(type="downref_approved", doc=rfc.document, - rev=rfc.document.rev, by=login) + c = DocEvent(type="downref_approved", doc=rfc, + rev=rfc.rev, by=login) c.desc = "Downref to RFC %s approved by Last Call for %s-%s" % ( - rfc.document.rfc_number(), da.name, da.document.rev) + rfc.rfc_number, d.name, d.rev) c.save() return HttpResponseRedirect(urlreverse('ietf.doc.views_downref.downref_registry')) diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index a71a586ed..d18876f3c 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -23,7 +23,7 @@ from django.utils import timezone import debug # pyflakes:ignore -from ietf.doc.models import ( Document, DocAlias, RelatedDocument, State, +from ietf.doc.models import ( Document, RelatedDocument, State, StateType, DocEvent, ConsensusDocEvent, TelechatDocEvent, WriteupDocEvent, StateDocEvent, IanaExpertDocEvent, IESG_SUBSTATE_TAGS) from ietf.doc.mails import ( email_pulled_from_rfc_queue, email_resurrect_requested, @@ -38,7 +38,7 @@ from ietf.doc.utils import ( add_state_change_event, can_adopt_draft, can_unadop set_replaces_for_document, default_consensus, tags_suffix, can_edit_docextresources, update_doc_extresources ) from ietf.doc.lastcall import request_last_call -from ietf.doc.fields import SearchableDocAliasesField +from ietf.doc.fields import SearchableDocumentsField from ietf.doc.forms import ExtResourceForm from ietf.group.models import Group, Role, GroupFeatures from ietf.iesg.models import TelechatDate @@ -333,7 +333,7 @@ def change_stream(request, name): )) class ReplacesForm(forms.Form): - replaces = SearchableDocAliasesField(required=False) + replaces = SearchableDocumentsField(required=False) comment = forms.CharField(widget=forms.Textarea, required=False, strip=False) def __init__(self, *args, **kwargs): @@ -343,9 +343,9 @@ class ReplacesForm(forms.Form): def clean_replaces(self): for d in self.cleaned_data['replaces']: - if d.document == self.doc: + if d == self.doc: raise forms.ValidationError("An Internet-Draft can't replace itself") - if d.document.type_id == "draft" and d.document.get_state_slug() == "rfc": + if d.type_id == "draft" and d.get_state_slug() == "rfc": raise forms.ValidationError("An Internet-Draft can't replace an RFC") return self.cleaned_data['replaces'] @@ -383,7 +383,7 @@ def replaces(request, name): )) class SuggestedReplacesForm(forms.Form): - replaces = forms.ModelMultipleChoiceField(queryset=DocAlias.objects.all(), + replaces = forms.ModelMultipleChoiceField(queryset=Document.objects.all(), label="Suggestions", required=False, widget=forms.CheckboxSelectMultiple, help_text="Select only the documents that are replaced by this document") comment = forms.CharField(label="Optional comment", widget=forms.Textarea, required=False, strip=False) @@ -673,7 +673,7 @@ def edit_info(request, name): e.save() events.append(e) - replaces = Document.objects.filter(docalias__relateddocument__source=doc, docalias__relateddocument__relationship="replaces") + replaces = Document.objects.filter(targets_related__source=doc, targets_related__relationship="replaces") if replaces: # this should perhaps be somewhere else, e.g. the # place where the replace relationship is established? diff --git a/ietf/doc/views_review.py b/ietf/doc/views_review.py index f758032f9..d1b91d18d 100644 --- a/ietf/doc/views_review.py +++ b/ietf/doc/views_review.py @@ -117,7 +117,7 @@ class RequestReviewForm(forms.ModelForm): @login_required def request_review(request, name): - doc = get_object_or_404(Document, name=name) + doc = get_object_or_404(Document, type_id="draft", name=name) if not can_request_review_of_doc(request.user, doc): permission_denied(request, "You do not have permission to perform this action") diff --git a/ietf/doc/views_search.py b/ietf/doc/views_search.py index 8b66c7c8d..853a106fa 100644 --- a/ietf/doc/views_search.py +++ b/ietf/doc/views_search.py @@ -409,8 +409,8 @@ def shorten_group_name(name): def ad_dashboard_sort_key(doc): - if doc.type.slug=='draft' and doc.get_state_slug('draft') == 'rfc': - return "21%04d" % int(doc.rfc_number()) + if doc.type.slug=='rfc': + return "21%04d" % int(doc.rfc_number) if doc.type.slug=='statchg' and doc.get_state_slug('statchg') == 'appr-sent': return "22%d" % 0 # TODO - get the date of the transition into this state here if doc.type.slug=='conflrev' and doc.get_state_slug('conflrev') in ('appr-reqnopub-sent','appr-noprob-sent'): @@ -805,21 +805,20 @@ def recent_drafts(request, days=7): }) -def index_all_drafts(request): +def index_all_drafts(request): # Should we rename this # try to be efficient since this view returns a lot of data categories = [] - for s in ("active", "rfc", "expired", "repl", "auth-rm", "ietf-rm"): + # Gather drafts + for s in ("active", "expired", "repl", "auth-rm", "ietf-rm"): state = State.objects.get(type="draft", slug=s) - if state.slug == "rfc": - heading = "RFCs" - elif state.slug in ("ietf-rm", "auth-rm"): + if state.slug in ("ietf-rm", "auth-rm"): heading = "Internet-Drafts %s" % state.name else: heading = "%s Internet-Drafts" % state.name - draft_names = DocAlias.objects.filter(docs__states=state).values_list("name", "docs__name") + draft_names = DocAlias.objects.filter(docs__type_id="draft", docs__states=state).values_list("name", "docs__name") names = [] names_to_skip = set() @@ -828,24 +827,52 @@ def index_all_drafts(request): if name != doc: if not name.startswith("rfc"): name, doc = doc, name - names_to_skip.add(doc) - - if name.startswith("rfc"): - name = name.upper() - sort_key = '%09d' % (100000000-int(name[3:])) + names_to_skip.add(doc) # this is filtering out subseries docaliases (which we will delete, so TODO clean this out after doing so) names.append((name, sort_key)) names.sort(key=lambda t: t[1]) names = [f'{n}' - for n, __ in names if n not in names_to_skip] + for n, __ in names if n not in names_to_skip] categories.append((state, heading, len(names), "
".join(names) )) + + # gather RFCs + rfc_names = DocAlias.objects.filter(docs__type_id="rfc").values_list("name", "docs__name") + names = [] + names_to_skip = set() + for name, doc in rfc_names: + sort_key = name + if name != doc: # There are some std docalias that pointed to rfc names pre-migration. + if not name.startswith("rfc"): + name, doc = doc, name + names_to_skip.add(doc) # this is filtering out those std docaliases (which we will delete, so TODO clean this out after doing so) + name = name.upper() + sort_key = '%09d' % (100000000-int(name[3:])) + + names.append((name, sort_key)) + + names.sort(key=lambda t: t[1]) + + names = [f'{n}' + for n, __ in names if n not in names_to_skip] + + state = State.objects.get(type_id="rfc", slug="published") + + categories.append((state, + "RFCs", + len(names), + "
".join(names) + )) + + # Return to the previous section ordering + categories = categories[0:1]+categories[5:]+categories[1:5] + return render(request, 'doc/index_all_drafts.html', { "categories": categories }) def index_active_drafts(request): diff --git a/ietf/doc/views_status_change.py b/ietf/doc/views_status_change.py index 5f9b6090f..8e3c61402 100644 --- a/ietf/doc/views_status_change.py +++ b/ietf/doc/views_status_change.py @@ -98,8 +98,8 @@ def change_state(request, name, option=None): relationship__slug__in=STATUSCHANGE_RELATIONS ) related_doc_info = [ - dict(title=rel_doc.target.document.title, - canonical_name=rel_doc.target.document.canonical_name(), + dict(title=rel_doc.target.title, + canonical_name=rel_doc.target.canonical_name(), newstatus=newstatus(rel_doc)) for rel_doc in related_docs ] @@ -309,7 +309,7 @@ def default_approval_text(status_change,relateddoc): current_text = status_change.text_or_error() # pyflakes:ignore - if relateddoc.target.document.std_level_id in ('std','ps','ds','bcp',): + if relateddoc.target.std_level_id in ('std','ps','ds','bcp',): action = "Protocol Action" else: action = "Document Action" @@ -320,7 +320,7 @@ def default_approval_text(status_change,relateddoc): dict(status_change=status_change, status_change_url = settings.IDTRACKER_BASE_URL+status_change.get_absolute_url(), relateddoc= relateddoc, - relateddoc_url = settings.IDTRACKER_BASE_URL+relateddoc.target.document.get_absolute_url(), + relateddoc_url = settings.IDTRACKER_BASE_URL+relateddoc.target.get_absolute_url(), approved_text = current_text, action=action, newstatus=newstatus(relateddoc), @@ -388,7 +388,7 @@ def approve(request, name): for rel in status_change.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS): # Add a document event to each target - c = DocEvent(type="added_comment", doc=rel.target.document, rev=rel.target.document.rev, by=login) + c = DocEvent(type="added_comment", doc=rel.target, rev=rel.target.rev, by=login) c.desc = "New status of %s approved by the IESG\n%s%s" % (newstatus(rel), settings.IDTRACKER_BASE_URL,reverse('ietf.doc.views_doc.document_main', kwargs={'name': status_change.name})) c.save() @@ -399,7 +399,7 @@ def approve(request, name): init = [] for rel in status_change.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS): init.append({"announcement_text" : escape(default_approval_text(status_change,rel)), - "label": "Announcement text for %s to %s"%(rel.target.document.canonical_name(),newstatus(rel)), + "label": "Announcement text for %s to %s"%(rel.target.canonical_name(),newstatus(rel)), }) formset = AnnouncementFormSet(initial=init) for form in formset.forms: @@ -439,7 +439,7 @@ def clean_helper(form, formtype): if not re.match(r'(?i)rfc\d{1,4}',key): errors.append(key+" is not a valid RFC - please use the form RFCn\n") - elif not DocAlias.objects.filter(name=key): + elif not Document.objects.filter(name=key): errors.append(key+" does not exist\n") if new_relations[key] not in STATUSCHANGE_RELATIONS: @@ -564,10 +564,9 @@ def start_rfc_status_change(request, name=None): DocAlias.objects.create( name= 'status-change-'+form.cleaned_data['document_name']).docs.add(status_change) for key in form.cleaned_data['relations']: - status_change.relateddocument_set.create(target=DocAlias.objects.get(name=key), + status_change.relateddocument_set.create(target=Document.objects.get(name=key), relationship_id=form.cleaned_data['relations'][key]) - tc_date = form.cleaned_data['telechat_date'] if tc_date: update_telechat(request, status_change, login, tc_date) @@ -605,11 +604,11 @@ def edit_relations(request, name): old_relations={} for rel in status_change.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS): - old_relations[rel.target.document.canonical_name()]=rel.relationship.slug + old_relations[rel.target.canonical_name()]=rel.relationship.slug new_relations=form.cleaned_data['relations'] status_change.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS).delete() for key in new_relations: - status_change.relateddocument_set.create(target=DocAlias.objects.get(name=key), + status_change.relateddocument_set.create(target=Document.objects.get(name=key), relationship_id=new_relations[key]) c = DocEvent(type="added_comment", doc=status_change, rev=status_change.rev, by=login) c.desc = "Affected RFC list changed.\nOLD:" @@ -626,7 +625,7 @@ def edit_relations(request, name): else: relations={} for rel in status_change.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS): - relations[rel.target.document.canonical_name()]=rel.relationship.slug + relations[rel.target.canonical_name()]=rel.relationship.slug init = { "relations":relations, } form = EditStatusChangeForm(initial=init) @@ -653,8 +652,8 @@ def generate_last_call_text(request, doc): settings=settings, requester=requester, expiration_date=expiration_date.strftime("%Y-%m-%d"), - changes=['%s from %s to %s\n (%s)'%(rel.target.name.upper(),rel.target.document.std_level.name,newstatus(rel),rel.target.document.title) for rel in doc.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS)], - urls=[rel.target.document.get_absolute_url() for rel in doc.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS)], + changes=['%s from %s to %s\n (%s)'%(rel.target.name.upper(),rel.target.std_level.name,newstatus(rel),rel.target.title) for rel in doc.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS)], + urls=[rel.target.get_absolute_url() for rel in doc.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS)], cc=cc ) ) diff --git a/ietf/group/milestones.py b/ietf/group/milestones.py index 64ebb389e..039fdb44c 100644 --- a/ietf/group/milestones.py +++ b/ietf/group/milestones.py @@ -369,7 +369,7 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"): email_milestones_changed(request, group, changes, states) if milestone_set == "charter": - return redirect('ietf.doc.views_doc.document_main', name=group.charter.canonical_name()) + return redirect('ietf.doc.views_doc.document_main', name=group.charter.name) else: return HttpResponseRedirect(group.about_url()) else: diff --git a/ietf/group/tests.py b/ietf/group/tests.py index 233cde55e..b11ed8e5f 100644 --- a/ietf/group/tests.py +++ b/ietf/group/tests.py @@ -69,7 +69,7 @@ class GroupStatsTests(TestCase): a = WgDraftFactory() b = WgDraftFactory() RelatedDocument.objects.create( - source=a, target=b.docalias.first(), relationship_id="refnorm" + source=a, target=b, relationship_id="refnorm" ) def test_group_stats(self): @@ -95,7 +95,7 @@ class GroupDocDependencyTests(TestCase): a = WgDraftFactory() b = WgDraftFactory() RelatedDocument.objects.create( - source=a, target=b.docalias.first(), relationship_id="refnorm" + source=a, target=b, relationship_id="refnorm" ) def test_group_document_dependencies(self): diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py index 01d1cc3f1..76d1b9cb1 100644 --- a/ietf/group/tests_info.py +++ b/ietf/group/tests_info.py @@ -117,8 +117,9 @@ class GroupPagesTests(TestCase): chair = Email.objects.filter(role__group=group, role__name="chair")[0] - with (Path(settings.CHARTER_PATH) / ("%s-%s.txt" % (group.charter.canonical_name(), group.charter.rev))).open("w") as f: - f.write("This is a charter.") + ( + Path(settings.CHARTER_PATH) / f"{group.charter.name}-{group.charter.rev}.txt" + ).write_text("This is a charter.") url = urlreverse('ietf.group.views.wg_summary_area', kwargs=dict(group_type="wg")) r = self.client.get(url) @@ -264,8 +265,9 @@ class GroupPagesTests(TestCase): group = CharterFactory().group draft = WgDraftFactory(group=group) - with (Path(settings.CHARTER_PATH) / ("%s-%s.txt" % (group.charter.canonical_name(), group.charter.rev))).open("w") as f: - f.write("This is a charter.") + ( + Path(settings.CHARTER_PATH) / f"{group.charter.name}-{group.charter.rev}.txt" + ).write_text("This is a charter.") milestone = GroupMilestone.objects.create( group=group, @@ -668,8 +670,9 @@ class GroupEditTests(TestCase): self.assertTrue(len(q('form .is-invalid')) > 0) # edit info - with (Path(settings.CHARTER_PATH) / ("%s-%s.txt" % (group.charter.canonical_name(), group.charter.rev))).open("w") as f: - f.write("This is a charter.") + ( + Path(settings.CHARTER_PATH) / f"{group.charter.name}-{group.charter.rev}.txt" + ).write_text("This is a charter.") area = group.parent ad = Person.objects.get(name="Areað Irector") state = GroupStateName.objects.get(slug="bof") @@ -711,7 +714,9 @@ class GroupEditTests(TestCase): self.assertEqual(group.list_archive, "archive.mars") self.assertEqual(group.description, '') - self.assertTrue((Path(settings.CHARTER_PATH) / ("%s-%s.txt" % (group.charter.canonical_name(), group.charter.rev))).exists()) + self.assertTrue( + (Path(settings.CHARTER_PATH) / f"{group.charter.name}-{group.charter.rev}.txt").exists() + ) self.assertEqual(len(outbox), 2) self.assertTrue('Personnel change' in outbox[0]['Subject']) for prefix in ['ad1','ad2','aread','marschairman','marsdelegate']: diff --git a/ietf/group/utils.py b/ietf/group/utils.py index b82474b49..42ae4554b 100644 --- a/ietf/group/utils.py +++ b/ietf/group/utils.py @@ -2,8 +2,7 @@ # -*- coding: utf-8 -*- -import io -import os +from pathlib import Path from django.db.models import Q from django.shortcuts import get_object_or_404 @@ -55,15 +54,14 @@ def get_charter_text(group): if (h.rev > c.rev and not (c_appr and not h_appr)) or (h_appr and not c_appr): c = h - filename = os.path.join(c.get_file_path(), "%s-%s.txt" % (c.canonical_name(), c.rev)) + filename = Path(c.get_file_path()) / f"{c.name}-{c.rev}.txt" try: - with io.open(filename, 'rb') as f: - text = f.read() - try: - text = text.decode('utf8') - except UnicodeDecodeError: - text = text.decode('latin1') - return text + text = filename.read_bytes() + try: + text = text.decode('utf8') + except UnicodeDecodeError: + text = text.decode('latin1') + return text except IOError: return 'Error Loading Group Charter' diff --git a/ietf/group/views.py b/ietf/group/views.py index 19c277c76..72a8fab30 100644 --- a/ietf/group/views.py +++ b/ietf/group/views.py @@ -534,9 +534,8 @@ def group_documents_txt(request, acronym, group_type=None): rows = [] for d in itertools.chain(docs, docs_related): - rfc_number = d.rfc_number() - if rfc_number != None: - name = rfc_number + if d.type_id == "rfc": + name = str(d.rfc_number) else: name = "%s-%s" % (d.name, d.rev) @@ -746,7 +745,7 @@ def dependencies(request, acronym, group_type=None): relationship__slug__startswith="ref", ) - both_rfcs = Q(source__states__slug="rfc", target__docs__states__slug="rfc") + both_rfcs = Q(source__states__slug="rfc", target__states__slug="rfc") inactive = Q(source__states__slug__in=["expired", "repl"]) attractor = Q(target__name__in=["rfc5000", "rfc5741"]) removed = Q(source__states__slug__in=["auth-rm", "ietf-rm"]) @@ -760,19 +759,19 @@ def dependencies(request, acronym, group_type=None): links = set() for x in relations: - target_state = x.target.document.get_state_slug("draft") + target_state = x.target.get_state_slug("draft") if target_state != "rfc" or x.is_downref(): links.add(x) replacements = RelatedDocument.objects.filter( relationship__slug="replaces", - target__docs__in=[x.target.document for x in links], + target__in=[x.target for x in links], ) for x in replacements: links.add(x) - nodes = set([x.source for x in links]).union([x.target.document for x in links]) + nodes = set([x.source for x in links]).union([x.target for x in links]) graph = { "nodes": [ { @@ -795,7 +794,7 @@ def dependencies(request, acronym, group_type=None): "links": [ { "source": x.source.canonical_name(), - "target": x.target.document.canonical_name(), + "target": x.target.canonical_name(), "rel": "downref" if x.is_downref() else x.relationship.slug, } for x in links diff --git a/ietf/idindex/index.py b/ietf/idindex/index.py index cda8310b4..962ef9184 100644 --- a/ietf/idindex/index.py +++ b/ietf/idindex/index.py @@ -34,7 +34,7 @@ def all_id_txt(): rfc_aliases = dict(DocAlias.objects.filter(name__startswith="rfc", docs__states=State.objects.get(type="draft", slug="rfc")).values_list("docs__name", "name")) - replacements = dict(RelatedDocument.objects.filter(target__docs__states=State.objects.get(type="draft", slug="repl"), + replacements = dict(RelatedDocument.objects.filter(target__states=State.objects.get(type="draft", slug="repl"), relationship="replaces").values_list("target__name", "source__name")) @@ -115,7 +115,7 @@ def all_id2_txt(): rfc_aliases = dict(DocAlias.objects.filter(name__startswith="rfc", docs__states=State.objects.get(type="draft", slug="rfc")).values_list("docs__name", "name")) - replacements = dict(RelatedDocument.objects.filter(target__docs__states=State.objects.get(type="draft", slug="repl"), + replacements = dict(RelatedDocument.objects.filter(target__states=State.objects.get(type="draft", slug="repl"), relationship="replaces").values_list("target__name", "source__name")) revision_time = dict(DocEvent.objects.filter(type="new_revision", doc__name__startswith="draft-").order_by('time').values_list("doc__name", "time")) diff --git a/ietf/idindex/tests.py b/ietf/idindex/tests.py index f207fa562..3dabcc642 100644 --- a/ietf/idindex/tests.py +++ b/ietf/idindex/tests.py @@ -52,8 +52,13 @@ class IndexTests(TestCase): RelatedDocument.objects.create( relationship=DocRelationshipName.objects.get(slug="replaces"), - source=Document.objects.create(type_id="draft", rev="00", name="draft-test-replacement"), - target=draft.docalias.get(name__startswith="draft")) + source=Document.objects.create( + type_id="draft", + rev="00", + name="draft-test-replacement" + ), + target=draft + ) txt = all_id_txt() self.assertTrue(draft.name + "-" + draft.rev in txt) @@ -111,8 +116,12 @@ class IndexTests(TestCase): draft.set_state(State.objects.get(type="draft", slug="repl")) RelatedDocument.objects.create( relationship=DocRelationshipName.objects.get(slug="replaces"), - source=Document.objects.create(type_id="draft", rev="00", name="draft-test-replacement"), - target=draft.docalias.get(name__startswith="draft")) + source=Document.objects.create( + type_id="draft", + rev="00", + name="draft-test-replacement" + ), + target=draft) t = get_fields(all_id2_txt()) self.assertEqual(t[5], "draft-test-replacement") diff --git a/ietf/iesg/agenda.py b/ietf/iesg/agenda.py index 0abc5e6cd..587713089 100644 --- a/ietf/iesg/agenda.py +++ b/ietf/iesg/agenda.py @@ -66,7 +66,7 @@ def get_doc_section(doc): elif doc.type_id == 'statchg': protocol_action = False for relation in doc.relateddocument_set.filter(relationship__slug__in=('tops','tois','tohist','toinf','tobcp','toexp')): - if relation.relationship_id in ('tops','tois') or relation.target.document.std_level_id in ('std','ds','ps'): + if relation.relationship_id in ('tops','tois') or relation.target.std_level_id in ('std','ds','ps'): protocol_action = True if protocol_action: s = "2.3" @@ -186,7 +186,7 @@ def fill_in_agenda_docs(date, sections, docs=None): doc.review_assignments = review_assignments_for_docs.get(doc.name, []) elif doc.type_id == "conflrev": - doc.conflictdoc = doc.relateddocument_set.get(relationship__slug='conflrev').target.document + doc.conflictdoc = doc.relateddocument_set.get(relationship__slug='conflrev').target elif doc.type_id == "charter": pass @@ -219,4 +219,4 @@ def agenda_data(date=None): fill_in_agenda_docs(date, sections) fill_in_agenda_management_issues(date, sections) - return { 'date': date.isoformat(), 'sections': sections } \ No newline at end of file + return { 'date': date.isoformat(), 'sections': sections } diff --git a/ietf/iesg/tests.py b/ietf/iesg/tests.py index 53172e645..e58ef1bac 100644 --- a/ietf/iesg/tests.py +++ b/ietf/iesg/tests.py @@ -17,7 +17,7 @@ from django.utils.html import escape import debug # pyflakes:ignore from ietf.doc.models import DocEvent, BallotPositionDocEvent, TelechatDocEvent -from ietf.doc.models import Document, DocAlias, State, RelatedDocument +from ietf.doc.models import Document, State, RelatedDocument from ietf.doc.factories import WgDraftFactory, IndividualDraftFactory, ConflictReviewFactory, BaseDocumentFactory, CharterFactory, WgRfcFactory, IndividualRfcFactory from ietf.doc.utils import create_ballot_if_not_open from ietf.group.factories import RoleFactory, GroupFactory @@ -107,8 +107,8 @@ class IESGAgendaTests(TestCase): super().setUp() mars = GroupFactory(acronym='mars',parent=Group.objects.get(acronym='farfut')) wgdraft = WgDraftFactory(name='draft-ietf-mars-test', group=mars, intended_std_level_id='ps') - rfc = IndividualRfcFactory.create(stream_id='irtf', other_aliases=['rfc6666',], states=[('draft','rfc'),('draft-iesg','pub')], std_level_id='inf', ) - wgdraft.relateddocument_set.create(target=rfc.docalias.get(name='rfc6666'), relationship_id='refnorm') + rfc = IndividualRfcFactory.create(stream_id='irtf', rfc_number=6666, std_level_id='inf', ) + wgdraft.relateddocument_set.create(target=rfc, relationship_id='refnorm') ise_draft = IndividualDraftFactory(name='draft-imaginary-independent-submission') ise_draft.stream = StreamName.objects.get(slug="ise") ise_draft.save_with_history([DocEvent(doc=ise_draft, rev=ise_draft.rev, type="changed_stream", by=Person.objects.get(user__username="secretary"), desc="Test")]) @@ -238,7 +238,7 @@ class IESGAgendaTests(TestCase): relation = RelatedDocument.objects.create( source=statchg, - target=DocAlias.objects.filter(name__startswith='rfc', docs__std_level="ps")[0], + target=Document.objects.filter(type_id="rfc", std_level="ps").first(), relationship_id="tohist") statchg.group = Group.objects.get(acronym="mars") @@ -256,7 +256,7 @@ class IESGAgendaTests(TestCase): self.assertTrue(statchg in agenda_data(date_str)["sections"]["2.3.3"]["docs"]) # 3.3 document status changes - relation.target = DocAlias.objects.filter(name__startswith='rfc', docs__std_level="inf")[0] + relation.target = Document.objects.filter(type_id="rfc", std_level="inf").first() relation.save() statchg.group = Group.objects.get(acronym="mars") diff --git a/ietf/iesg/utils.py b/ietf/iesg/utils.py index 4ddc9cb40..3f4883798 100644 --- a/ietf/iesg/utils.py +++ b/ietf/iesg/utils.py @@ -32,10 +32,10 @@ def telechat_page_count(date=None, docs=None): pages_for_action += d.pages or 0 elif d.type_id == 'statchg': for rel in d.related_that_doc(STATUSCHANGE_RELATIONS): - pages_for_action += rel.document.pages or 0 + pages_for_action += rel.pages or 0 elif d.type_id == 'conflrev': for rel in d.related_that_doc('conflrev'): - pages_for_action += rel.document.pages or 0 + pages_for_action += rel.pages or 0 else: pass @@ -43,10 +43,10 @@ def telechat_page_count(date=None, docs=None): for d in for_approval-set(drafts): if d.type_id == 'statchg': for rel in d.related_that_doc(STATUSCHANGE_RELATIONS): - related_pages += rel.document.pages or 0 + related_pages += rel.pages or 0 elif d.type_id == 'conflrev': for rel in d.related_that_doc('conflrev'): - related_pages += rel.document.pages or 0 + related_pages += rel.pages or 0 else: # There's really nothing to rely on to give a reading load estimate for charters pass diff --git a/ietf/iesg/views.py b/ietf/iesg/views.py index cec22719d..3038a2092 100644 --- a/ietf/iesg/views.py +++ b/ietf/iesg/views.py @@ -151,8 +151,8 @@ def agenda_json(request, date=None): if doc.type_id == "draft": docinfo['rev'] = doc.rev docinfo['intended-std-level'] = str(doc.intended_std_level) - if doc.rfc_number(): - docinfo['rfc-number'] = doc.rfc_number() + if doc.type_id == "rfc": + docinfo['rfc-number'] = doc.rfc_number iana_state = doc.get_state("draft-iana-review") if iana_state and iana_state.slug in ("not-ok", "changed", "need-rev"): @@ -172,7 +172,7 @@ def agenda_json(request, date=None): elif doc.type_id == 'conflrev': docinfo['rev'] = doc.rev - td = doc.relateddocument_set.get(relationship__slug='conflrev').target.document + td = doc.relateddocument_set.get(relationship__slug='conflrev').target docinfo['target-docname'] = td.canonical_name() docinfo['target-title'] = td.title docinfo['target-rev'] = td.rev diff --git a/ietf/ipr/admin.py b/ietf/ipr/admin.py index a0185f58c..afc1952d7 100644 --- a/ietf/ipr/admin.py +++ b/ietf/ipr/admin.py @@ -94,7 +94,7 @@ admin.site.register(IprDocRel, IprDocRelAdmin) class RelatedIprAdmin(admin.ModelAdmin): list_display = ['source', 'target', 'relationship', ] - search_fields = ['source__name', 'target__name', 'target__docs__name', ] + search_fields = ['source__name', 'target__name', ] raw_id_fields = ['source', 'target', ] admin.site.register(RelatedIpr, RelatedIprAdmin) diff --git a/ietf/ipr/utils.py b/ietf/ipr/utils.py index f288803de..25c38ab94 100644 --- a/ietf/ipr/utils.py +++ b/ietf/ipr/utils.py @@ -41,24 +41,17 @@ def iprs_from_docs(aliases,**kwargs): iprdocrels += document.ipr(**kwargs) return list(set([i.disclosure for i in iprdocrels])) -def related_docs(alias, relationship=('replaces', 'obs')): +def related_docs(doc, relationship=('replaces', 'obs')): """Returns list of related documents""" - results = [] - for doc in alias.docs.all(): - results += list(doc.docalias.all()) - - rels = [] - for doc in alias.docs.all(): - rels += list(doc.all_relations_that_doc(relationship)) + results = [doc] + + rels = list(doc.document.all_relations_that_doc(relationship)) for rel in rels: - rel_aliases = list(rel.target.document.docalias.all()) - - for x in rel_aliases: - x.related = rel - x.relation = rel.relationship.revname - results += rel_aliases + rel.target.related = rel + rel.target.relation = rel.relationship.revname + results += [x.target for x in rels] return list(set(results)) diff --git a/ietf/mailtrigger/models.py b/ietf/mailtrigger/models.py index 171dbd85e..1cf84033b 100644 --- a/ietf/mailtrigger/models.py +++ b/ietf/mailtrigger/models.py @@ -96,35 +96,35 @@ class Recipient(models.Model): addrs = [] if 'doc' in kwargs: for reldoc in kwargs['doc'].related_that_doc(('conflrev','tohist','tois','tops')): - addrs.extend(Recipient.objects.get(slug='doc_authors').gather(**{'doc':reldoc.document})) + addrs.extend(Recipient.objects.get(slug='doc_authors').gather(**{'doc':reldoc})) return addrs def gather_doc_affecteddoc_group_chairs(self, **kwargs): addrs = [] if 'doc' in kwargs: for reldoc in kwargs['doc'].related_that_doc(('conflrev','tohist','tois','tops')): - addrs.extend(Recipient.objects.get(slug='doc_group_chairs').gather(**{'doc':reldoc.document})) + addrs.extend(Recipient.objects.get(slug='doc_group_chairs').gather(**{'doc':reldoc})) return addrs def gather_doc_affecteddoc_notify(self, **kwargs): addrs = [] if 'doc' in kwargs: for reldoc in kwargs['doc'].related_that_doc(('conflrev','tohist','tois','tops')): - addrs.extend(Recipient.objects.get(slug='doc_notify').gather(**{'doc':reldoc.document})) + addrs.extend(Recipient.objects.get(slug='doc_notify').gather(**{'doc':reldoc})) return addrs def gather_conflict_review_stream_manager(self, **kwargs): addrs = [] if 'doc' in kwargs: for reldoc in kwargs['doc'].related_that_doc(('conflrev',)): - addrs.extend(Recipient.objects.get(slug='doc_stream_manager').gather(**{'doc':reldoc.document})) + addrs.extend(Recipient.objects.get(slug='doc_stream_manager').gather(**{'doc':reldoc})) return addrs def gather_conflict_review_steering_group(self,**kwargs): addrs = [] if 'doc' in kwargs: for reldoc in kwargs['doc'].related_that_doc(('conflrev',)): - if reldoc.document.stream_id=='irtf': + if reldoc.stream_id=='irtf': addrs.append('"Internet Research Steering Group" ') return addrs diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index f44a446e2..74f7eb310 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -2565,6 +2565,19 @@ "model": "doc.state", "pk": 176 }, + { + "fields": { + "desc": "", + "name": "Published", + "next_states": [], + "order": 1, + "slug": "published", + "type": "rfc", + "used": true + }, + "model": "doc.state", + "pk": 177 + }, { "fields": { "label": "State" @@ -2747,6 +2760,13 @@ "model": "doc.statetype", "pk": "review" }, + { + "fields": { + "label": "State" + }, + "model": "doc.statetype", + "pk": "rfc" + }, { "fields": { "label": "Shepherd's Writeup State" @@ -9933,6 +9953,17 @@ "model": "name.dbtemplatetypename", "pk": "rst" }, + { + "fields": { + "desc": "", + "name": "Became RFC", + "order": 0, + "revname": "Became RFC as", + "used": true + }, + "model": "name.docrelationshipname", + "pk": "became_rfc" + }, { "fields": { "desc": "", @@ -10603,6 +10634,17 @@ "model": "name.doctypename", "pk": "review" }, + { + "fields": { + "desc": "", + "name": "RFC", + "order": 0, + "prefix": "rfc", + "used": true + }, + "model": "name.doctypename", + "pk": "rfc" + }, { "fields": { "desc": "", diff --git a/ietf/name/migrations/0004_rfc_doctype_names.py b/ietf/name/migrations/0004_rfc_doctype_names.py new file mode 100644 index 000000000..1e87feaf6 --- /dev/null +++ b/ietf/name/migrations/0004_rfc_doctype_names.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.2 on 2023-06-14 20:39 + +from django.db import migrations + + +def forward(apps, schema_editor): + DocTypeName = apps.get_model("name", "DocTypeName") + DocTypeName.objects.get_or_create( + slug="rfc", + name="RFC", + used=True, + prefix="rfc", + ) + + DocRelationshipName = apps.get_model("name", "DocRelationshipName") + DocRelationshipName.objects.get_or_create( + slug="became_rfc", + name="became RFC", + used=True, + revname="came from draft", + ) + +class Migration(migrations.Migration): + dependencies = [ + ("name", "0003_populate_telechatagendasectionname"), + ] + + operations = [ + migrations.RunPython(forward), + ] diff --git a/ietf/name/models.py b/ietf/name/models.py index b9e75e8f9..4b750a62a 100644 --- a/ietf/name/models.py +++ b/ietf/name/models.py @@ -42,7 +42,7 @@ class DocRelationshipName(NameModel): class DocTypeName(NameModel): """Draft, Agenda, Minutes, Charter, Discuss, Guideline, Email, - Review, Issue, Wiki""" + Review, Issue, Wiki, RFC""" prefix = models.CharField(max_length=16, default="") class DocTagName(NameModel): """Waiting for Reference, IANA Coordination, Revised ID Needed, diff --git a/ietf/review/utils.py b/ietf/review/utils.py index 058e8cee9..b136a39d7 100644 --- a/ietf/review/utils.py +++ b/ietf/review/utils.py @@ -50,6 +50,8 @@ def can_request_review_of_doc(user, doc): if not user.is_authenticated: return False + # This is in a strange place as it has nothing to do with the user + # but this utility is used in too many places to move this quickly. if doc.type_id == 'draft' and doc.get_state_slug() != 'active': return False diff --git a/ietf/secr/telechat/tests.py b/ietf/secr/telechat/tests.py index e4661b767..39949b83a 100644 --- a/ietf/secr/telechat/tests.py +++ b/ietf/secr/telechat/tests.py @@ -67,10 +67,8 @@ class SecrTelechatTestCase(TestCase): def test_doc_detail_draft_with_downref(self): ad = Person.objects.get(user__username="ad") draft = WgDraftFactory(ad=ad, intended_std_level_id='ps', states=[('draft-iesg','pub-req'),]) - rfc = IndividualRfcFactory.create(stream_id='irtf', other_aliases=['rfc6666',], - states=[('draft','rfc'),('draft-iesg','pub')], std_level_id='inf', ) - draft.relateddocument_set.create(target=rfc.docalias.get(name='rfc6666'), - relationship_id='refnorm') + rfc = IndividualRfcFactory.create(stream_id='irtf', rfc_number=6666, std_level_id='inf') + draft.relateddocument_set.create(target=rfc, relationship_id='refnorm') create_ballot_if_not_open(None, draft, ad, 'approve') d = get_next_telechat_date() date = d.strftime('%Y-%m-%d') diff --git a/ietf/secr/telechat/views.py b/ietf/secr/telechat/views.py index f13a082f2..a37f380e9 100644 --- a/ietf/secr/telechat/views.py +++ b/ietf/secr/telechat/views.py @@ -313,7 +313,7 @@ def doc_detail(request, date, name): # if this is a conflict review document add referenced document if doc.type_id == 'conflrev': - conflictdoc = doc.relateddocument_set.get(relationship__slug='conflrev').target.document + conflictdoc = doc.relateddocument_set.get(relationship__slug='conflrev').target else: conflictdoc = None diff --git a/ietf/secr/templates/telechat/doc.html b/ietf/secr/templates/telechat/doc.html index 9d37db4cb..9ed8693df 100644 --- a/ietf/secr/templates/telechat/doc.html +++ b/ietf/secr/templates/telechat/doc.html @@ -85,13 +85,13 @@ {% if downrefs %}

Downward References

{% for ref in downrefs %} -

Add {{ref.target.document.canonical_name}} - ({{ref.target.document.std_level}} - {{ref.target.document.stream.desc}}) +

Add {{ref.target.canonical_name}} + ({{ref.target.std_level}} - {{ref.target.stream.desc}}) to downref registry.
- {% if not ref.target.document.std_level %} + {% if not ref.target.std_level %} +++ Warning: The standards level has not been set yet!!!
{% endif %} - {% if not ref.target.document.stream %} + {% if not ref.target.stream %} +++ Warning: document stream has not been set yet!!!
{% endif %} {% endfor %}

diff --git a/ietf/secr/utils/group.py b/ietf/secr/utils/group.py index a4c1c0f98..40a9065ac 100644 --- a/ietf/secr/utils/group.py +++ b/ietf/secr/utils/group.py @@ -3,11 +3,8 @@ # Python imports -import io -import os # Django imports -from django.conf import settings from django.core.exceptions import ObjectDoesNotExist # Datatracker imports @@ -15,27 +12,6 @@ from ietf.group.models import Group from ietf.ietfauth.utils import has_role - - -def current_nomcom(): - qs = Group.objects.filter(acronym__startswith='nomcom',state__slug="active").order_by('-time') - if qs.count(): - return qs[0] - else: - return None - -def get_charter_text(group): - ''' - Takes a group object and returns the text or the group's charter as a string - ''' - charter = group.charter - path = os.path.join(settings.CHARTER_PATH, '%s-%s.txt' % (charter.canonical_name(), charter.rev)) - f = io.open(path,'r') - text = f.read() - f.close() - - return text - def get_my_groups(user,conclude=False): ''' Takes a Django user object (from request) diff --git a/ietf/settings.py b/ietf/settings.py index be2d62c7b..27cc2395f 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -705,7 +705,7 @@ LIAISON_ATTACH_URL = 'https://www.ietf.org/lib/dt/documents/LIAISON/' # should e DOC_HREFS = { "charter": "https://www.ietf.org/charter/{doc.name}-{doc.rev}.txt", "draft": "https://www.ietf.org/archive/id/{doc.name}-{doc.rev}.txt", - "rfc": "https://www.rfc-editor.org/rfc/rfc{doc.rfcnum}.txt", + "rfc": "https://www.rfc-editor.org/rfc/rfc{doc.rfc_number}.txt", "slides": "https://www.ietf.org/slides/{doc.name}-{doc.rev}", "procmaterials": "https://www.ietf.org/procmaterials/{doc.name}-{doc.rev}", "conflrev": "https://www.ietf.org/cr/{doc.name}-{doc.rev}.txt", diff --git a/ietf/stats/tests.py b/ietf/stats/tests.py index dae352752..fd3e0a05b 100644 --- a/ietf/stats/tests.py +++ b/ietf/stats/tests.py @@ -82,7 +82,7 @@ class StatisticsTests(TestCase): DocAlias.objects.create(name=referencing_draft.name).docs.add(referencing_draft) RelatedDocument.objects.create( source=referencing_draft, - target=draft.docalias.first(), + target=draft, relationship=DocRelationshipName.objects.get(slug="refinfo") ) NewRevisionDocEvent.objects.create( diff --git a/ietf/stats/views.py b/ietf/stats/views.py index 44fbfb717..3d7d62c06 100644 --- a/ietf/stats/views.py +++ b/ietf/stats/views.py @@ -563,11 +563,11 @@ def document_stats(request, stats_type=None): bins = defaultdict(set) cite_relationships = list(DocRelationshipName.objects.filter(slug__in=['refnorm', 'refinfo', 'refunk', 'refold'])) - person_filters &= Q(documentauthor__document__docalias__relateddocument__relationship__in=cite_relationships) + person_filters &= Q(documentauthor__document__relateddocument__relationship__in=cite_relationships) person_qs = Person.objects.filter(person_filters) - for name, citations in person_qs.values_list("name").annotate(Count("documentauthor__document__docalias__relateddocument")): + for name, citations in person_qs.values_list("name").annotate(Count("documentauthor__document__relateddocument")): bins[citations or 0].add(name) total_persons = count_bins(bins) @@ -587,11 +587,11 @@ def document_stats(request, stats_type=None): bins = defaultdict(set) cite_relationships = list(DocRelationshipName.objects.filter(slug__in=['refnorm', 'refinfo', 'refunk', 'refold'])) - person_filters &= Q(documentauthor__document__docalias__relateddocument__relationship__in=cite_relationships) + person_filters &= Q(documentauthor__document__relateddocument__relationship__in=cite_relationships) person_qs = Person.objects.filter(person_filters) - values = person_qs.values_list("name", "documentauthor__document").annotate(Count("documentauthor__document__docalias__relateddocument")) + values = person_qs.values_list("name", "documentauthor__document").annotate(Count("documentauthor__document__relateddocument")) for name, ts in itertools.groupby(values.order_by("name"), key=lambda t: t[0]): h_index = compute_hirsch_index([citations for _, document, citations in ts]) bins[h_index or 0].add(name) diff --git a/ietf/submit/forms.py b/ietf/submit/forms.py index 3950cdc04..bd9dfd0b8 100644 --- a/ietf/submit/forms.py +++ b/ietf/submit/forms.py @@ -28,8 +28,7 @@ import debug # pyflakes:ignore from ietf.doc.models import Document from ietf.group.models import Group from ietf.ietfauth.utils import has_role -from ietf.doc.fields import SearchableDocAliasesField -from ietf.doc.models import DocAlias +from ietf.doc.fields import SearchableDocumentsField from ietf.ipr.mail import utc_from_string from ietf.meeting.models import Meeting from ietf.message.models import Message @@ -684,9 +683,9 @@ class SubmissionAutoUploadForm(SubmissionBaseUploadForm): if self.cleaned_data['replaces']: names_replaced = [s.strip() for s in self.cleaned_data['replaces'].split(',')] self.cleaned_data['replaces'] = ','.join(names_replaced) - aliases_replaced = DocAlias.objects.filter(name__in=names_replaced) - if len(names_replaced) != len(aliases_replaced): - known_names = aliases_replaced.values_list('name', flat=True) + documents_replaced = Document.objects.filter(name__in=names_replaced) + if len(names_replaced) != len(documents_replaced): + known_names = documents_replaced.values_list('name', flat=True) unknown_names = [n for n in names_replaced if n not in known_names] self.add_error( 'replaces', @@ -694,27 +693,27 @@ class SubmissionAutoUploadForm(SubmissionBaseUploadForm): 'Unknown Internet-Draft name(s): ' + ', '.join(unknown_names) ), ) - for alias in aliases_replaced: - if alias.document.name == self.filename: + for doc in documents_replaced: + if doc.name == self.filename: self.add_error( 'replaces', forms.ValidationError("An Internet-Draft cannot replace itself"), ) - elif alias.document.type_id != "draft": + elif doc.type_id != "draft": self.add_error( 'replaces', forms.ValidationError("An Internet-Draft can only replace another Internet-Draft"), ) - elif alias.document.get_state_slug() == "rfc": + elif doc.get_state_slug() == "rfc": self.add_error( 'replaces', - forms.ValidationError("An Internet-Draft cannot replace an RFC"), + forms.ValidationError("An Internet-Draft cannot replace another Internet-Draft that has become an RFC"), ) - elif alias.document.get_state_slug('draft-iesg') in ('approved', 'ann', 'rfcqueue'): + elif doc.get_state_slug('draft-iesg') in ('approved', 'ann', 'rfcqueue'): self.add_error( 'replaces', forms.ValidationError( - alias.name + " is approved by the IESG and cannot be replaced" + doc.name + " is approved by the IESG and cannot be replaced" ), ) return cleaned_data @@ -755,22 +754,20 @@ class SubmitterForm(NameEmailForm): return line class ReplacesForm(forms.Form): - replaces = SearchableDocAliasesField(required=False, help_text="Any Internet-Drafts that this document replaces (approval required for replacing an Internet-Draft you are not the author of)") + replaces = SearchableDocumentsField(required=False, help_text="Any Internet-Drafts that this document replaces (approval required for replacing an Internet-Draft you are not the author of)") def __init__(self, *args, **kwargs): self.name = kwargs.pop("name") super(ReplacesForm, self).__init__(*args, **kwargs) def clean_replaces(self): - for alias in self.cleaned_data['replaces']: - if alias.document.name == self.name: + for doc in self.cleaned_data['replaces']: + if doc.name == self.name: raise forms.ValidationError("An Internet-Draft cannot replace itself.") - if alias.document.type_id != "draft": + if doc.type_id != "draft": raise forms.ValidationError("An Internet-Draft can only replace another Internet-Draft") - if alias.document.get_state_slug() == "rfc": - raise forms.ValidationError("An Internet-Draft cannot replace an RFC") - if alias.document.get_state_slug('draft-iesg') in ('approved','ann','rfcqueue'): - raise forms.ValidationError(alias.name+" is approved by the IESG and cannot be replaced") + if doc.get_state_slug('draft-iesg') in ('approved','ann','rfcqueue'): + raise forms.ValidationError(doc.name+" is approved by the IESG and cannot be replaced") return self.cleaned_data['replaces'] class EditSubmissionForm(forms.ModelForm): diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py index 81534aaad..1e6481b35 100644 --- a/ietf/submit/tests.py +++ b/ietf/submit/tests.py @@ -32,7 +32,7 @@ from ietf.submit.utils import (expirable_submissions, expire_submission, find_su process_and_accept_uploaded_submission, SubmissionError, process_submission_text, process_submission_xml, process_uploaded_submission, process_and_validate_submission) -from ietf.doc.factories import (DocumentFactory, WgDraftFactory, IndividualDraftFactory, IndividualRfcFactory, +from ietf.doc.factories import (DocumentFactory, WgDraftFactory, IndividualDraftFactory, ReviewFactory, WgRfcFactory) from ietf.doc.models import ( Document, DocAlias, DocEvent, State, BallotPositionDocEvent, DocumentAuthor, SubmissionDocEvent ) @@ -302,7 +302,7 @@ class SubmitTests(BaseSubmitTestCase): submission = Submission.objects.get(name=name) self.assertEqual(submission.submitter, email.utils.formataddr((submitter_name, submitter_email))) self.assertEqual([] if submission.replaces == "" else submission.replaces.split(','), - [ d.name for d in DocAlias.objects.filter(pk__in=replaces) ]) + [ d.name for d in Document.objects.filter(pk__in=replaces) ]) self.assertCountEqual( [str(r) for r in submission.external_resources.all()], [str(r) for r in extresources] if extresources else [], @@ -369,9 +369,8 @@ class SubmitTests(BaseSubmitTestCase): # supply submitter info, then draft should be in and ready for approval mailbox_before = len(outbox) - replaced_alias = draft.docalias.first() r = self.supply_extra_metadata(name, status_url, author.ascii, author.email().address.lower(), - replaces=[str(replaced_alias.pk), str(sug_replaced_alias.pk)]) + replaces=[str(draft.pk), str(sug_replaced_draft.pk)]) self.assertEqual(r.status_code, 302) status_url = r["Location"] @@ -419,7 +418,7 @@ class SubmitTests(BaseSubmitTestCase): self.assertEqual(authors[0].person, author) self.assertEqual(set(draft.formal_languages.all()), set(FormalLanguageName.objects.filter(slug="json"))) self.assertEqual(draft.relations_that_doc("replaces").count(), 1) - self.assertTrue(draft.relations_that_doc("replaces").first().target, replaced_alias) + self.assertTrue(draft.relations_that_doc("replaces").first().target, draft) self.assertEqual(draft.relations_that_doc("possibly-replaces").count(), 1) self.assertTrue(draft.relations_that_doc("possibly-replaces").first().target, sug_replaced_alias) self.assertEqual(len(outbox), mailbox_before + 5) @@ -1142,7 +1141,7 @@ class SubmitTests(BaseSubmitTestCase): self.verify_bibxml_ids_creation(draft) def test_submit_update_individual(self): - IndividualDraftFactory(name='draft-ietf-random-thing', states=[('draft','rfc')], other_aliases=['rfc9999',], pages=5) + IndividualDraftFactory(name='draft-ietf-random-thing', states=[('draft','active'),('draft-iesg','approved')], pages=5) ad=Person.objects.get(user__username='ad') # Group of None here does not reflect real individual submissions draft = IndividualDraftFactory(group=None, ad = ad, authors=[ad,], notify='aliens@example.mars', pages=5) @@ -1152,23 +1151,14 @@ class SubmitTests(BaseSubmitTestCase): status_url, author = self.do_submission(name,rev) mailbox_before = len(outbox) - replaced_alias = draft.docalias.first() - r = self.supply_extra_metadata(name, status_url, "Submitter Name", "author@example.com", replaces=[str(replaced_alias.pk)]) + r = self.supply_extra_metadata(name, status_url, "Submitter Name", "author@example.com", replaces=[str(draft.pk)]) self.assertEqual(r.status_code, 200) self.assertContains(r, 'cannot replace itself') self._assert_extresources_in_table(r, []) self._assert_extresources_form(r, []) - replaced_alias = DocAlias.objects.get(name='draft-ietf-random-thing') - r = self.supply_extra_metadata(name, status_url, "Submitter Name", "author@example.com", replaces=[str(replaced_alias.pk)]) - self.assertEqual(r.status_code, 200) - self.assertContains(r, 'cannot replace an RFC') - self._assert_extresources_in_table(r, []) - self._assert_extresources_form(r, []) - - replaced_alias.document.set_state(State.objects.get(type='draft-iesg',slug='approved')) - replaced_alias.document.set_state(State.objects.get(type='draft',slug='active')) - r = self.supply_extra_metadata(name, status_url, "Submitter Name", "author@example.com", replaces=[str(replaced_alias.pk)]) + replaced = Document.objects.get(name='draft-ietf-random-thing') + r = self.supply_extra_metadata(name, status_url, "Submitter Name", "author@example.com", replaces=[str(replaced.pk)]) self.assertEqual(r.status_code, 200) self.assertContains(r, 'approved by the IESG and cannot') self._assert_extresources_in_table(r, []) @@ -1262,7 +1252,7 @@ class SubmitTests(BaseSubmitTestCase): status_url, "Submitter Name", "submitter@example.com", - replaces=[str(replaced_draft.docalias.first().pk)], + replaces=[str(replaced_draft.pk)], ) submission = Submission.objects.get(name=name, rev=rev) @@ -1412,7 +1402,7 @@ class SubmitTests(BaseSubmitTestCase): "edit-pages": "123", "submitter-name": "Some Random Test Person", "submitter-email": "random@example.com", - "replaces": [str(draft.docalias.first().pk)], + "replaces": [str(draft.pk)], "edit-note": "no comments", "authors-0-name": "Person 1", "authors-0-email": "person1@example.com", @@ -1431,7 +1421,7 @@ class SubmitTests(BaseSubmitTestCase): self.assertEqual(submission.pages, 123) self.assertEqual(submission.note, "no comments") self.assertEqual(submission.submitter, "Some Random Test Person ") - self.assertEqual(submission.replaces, draft.docalias.first().name) + self.assertEqual(submission.replaces, draft.name) self.assertEqual(submission.state_id, "manual") authors = submission.authors @@ -3099,13 +3089,15 @@ class SubmissionUploadFormTests(BaseSubmitTestCase): # can't replace RFC rfc = WgRfcFactory() + draft = WgDraftFactory(states=[("draft", "rfc")]) + draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) form = SubmissionAutoUploadForm( request_factory.get('/some/url'), - data={'user': auth.user.username, 'replaces': rfc.name}, + data={'user': auth.user.username, 'replaces': draft.name}, files=files_dict, ) self.assertFalse(form.is_valid()) - self.assertIn('An Internet-Draft cannot replace an RFC', form.errors['replaces']) + self.assertIn('An Internet-Draft cannot replace another Internet-Draft that has become an RFC', form.errors['replaces']) # can't replace draft approved by iesg existing_drafts[0].set_state(State.objects.get(type='draft-iesg', slug='approved')) @@ -3697,25 +3689,9 @@ class RefsTests(BaseSubmitTestCase): class PostSubmissionTests(BaseSubmitTestCase): - @override_settings(RFC_FILE_TYPES=('txt', 'xml'), IDSUBMIT_FILE_TYPES=('pdf', 'md')) - def test_find_submission_filenames_rfc(self): - """Posting an RFC submission should use RFC_FILE_TYPES""" - rfc = IndividualRfcFactory() - path = Path(self.staging_dir) - for ext in ['txt', 'xml', 'pdf', 'md']: - (path / f'{rfc.name}-{rfc.rev}.{ext}').touch() - files = find_submission_filenames(rfc) - self.assertCountEqual( - files, - { - 'txt': f'{path}/{rfc.name}-{rfc.rev}.txt', - 'xml': f'{path}/{rfc.name}-{rfc.rev}.xml', - # should NOT find the pdf or md - } - ) @override_settings(RFC_FILE_TYPES=('txt', 'xml'), IDSUBMIT_FILE_TYPES=('pdf', 'md')) - def test_find_submission_filenames_draft(self): + def test_find_submission_filenames(self): """Posting an I-D submission should use IDSUBMIT_FILE_TYPES""" draft = WgDraftFactory() path = Path(self.staging_dir) diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index ec5af8efe..17127d474 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -287,7 +287,7 @@ def find_submission_filenames(draft): """ path = pathlib.Path(settings.IDSUBMIT_STAGING_PATH) stem = f'{draft.name}-{draft.rev}' - allowed_types = settings.RFC_FILE_TYPES if draft.get_state_slug() == 'rfc' else settings.IDSUBMIT_FILE_TYPES + allowed_types = settings.IDSUBMIT_FILE_TYPES candidates = {ext: path / f'{stem}.{ext}' for ext in allowed_types} return {ext: str(filename) for ext, filename in candidates.items() if filename.exists()} @@ -504,7 +504,7 @@ def update_replaces_from_submission(request, submission, draft): if request.user.is_authenticated: is_chair_of = list(Group.objects.filter(role__person__user=request.user, role__name="chair")) - replaces = DocAlias.objects.filter(name__in=submission.replaces.split(",")).prefetch_related("docs", "docs__group") + replaces = Document.objects.filter(name__in=submission.replaces.split(",")).prefetch_related("group") existing_replaces = list(draft.related_that_doc("replaces")) existing_suggested = set(draft.related_that_doc("possibly-replaces")) @@ -516,14 +516,12 @@ def update_replaces_from_submission(request, submission, draft): if r in existing_replaces: continue - rdoc = r.document - - if rdoc == draft: + if r == draft: continue if (is_secretariat - or (draft.group in is_chair_of and (rdoc.group.type_id == "individ" or rdoc.group in is_chair_of)) - or (submitter_email and rdoc.documentauthor_set.filter(email__address__iexact=submitter_email).exists())): + or (draft.group in is_chair_of and (r.group.type_id == "individ" or r.group in is_chair_of)) + or (submitter_email and r.documentauthor_set.filter(email__address__iexact=submitter_email).exists())): approved.append(r) else: if r not in existing_suggested: diff --git a/ietf/sync/rfceditor.py b/ietf/sync/rfceditor.py index 784e7a2f0..dab5cb17c 100644 --- a/ietf/sync/rfceditor.py +++ b/ietf/sync/rfceditor.py @@ -488,12 +488,15 @@ def update_docs_from_rfc_index(index_data, errata_data, skip_older_than_date=Non def parse_relation_list(l): res = [] for x in l: - if x[:3] in ("NIC", "IEN", "STD", "RTR"): - # try translating this to RFCs that we can handle - # sensibly; otherwise we'll have to ignore them - l = DocAlias.objects.filter(name__startswith="rfc", docs__docalias__name=x.lower()) - else: - l = DocAlias.objects.filter(name=x.lower()) + # This lookup wasn't finding anything but maybe some STD and we know + # if the STD had more than one RFC the wrong thing happens + # + #if x[:3] in ("NIC", "IEN", "STD", "RTR"): + # # try translating this to RFCs that we can handle + # # sensibly; otherwise we'll have to ignore them + # l = DocAlias.objects.filter(name__startswith="rfc", docs__docalias__name=x.lower()) + #else: + l = Document.objects.filter(name=x.lower()) for a in l: if a not in res: diff --git a/ietf/sync/tests.py b/ietf/sync/tests.py index f245145d2..4ad26e3e4 100644 --- a/ietf/sync/tests.py +++ b/ietf/sync/tests.py @@ -14,7 +14,7 @@ from django.utils import timezone import debug # pyflakes:ignore -from ietf.doc.factories import WgDraftFactory +from ietf.doc.factories import WgDraftFactory, RfcFactory from ietf.doc.models import Document, DocAlias, DocEvent, DeletedEvent, DocTagName, RelatedDocument, State, StateDocEvent from ietf.doc.utils import add_state_change_event from ietf.group.factories import GroupFactory @@ -235,9 +235,7 @@ class RFCSyncTests(TestCase): # too, but for testing purposes ... doc.action_holders.add(doc.ad) # not normally set, but add to be sure it's cleared - updated_doc = Document.objects.create(name="draft-ietf-something") - DocAlias.objects.create(name=updated_doc.name).docs.add(updated_doc) - DocAlias.objects.create(name="rfc123").docs.add(updated_doc) + RfcFactory(rfc_number=123) today = date_today() @@ -360,7 +358,7 @@ class RFCSyncTests(TestCase): self.assertTrue(DocAlias.objects.filter(name="bcp1", docs=doc)) self.assertTrue(DocAlias.objects.filter(name="fyi1", docs=doc)) self.assertTrue(DocAlias.objects.filter(name="std1", docs=doc)) - self.assertTrue(RelatedDocument.objects.filter(source=doc, target__name="rfc123", relationship="updates")) + self.assertTrue(RelatedDocument.objects.filter(source=doc, target__name="rfc123", relationship="updates").exists()) self.assertEqual(doc.title, "A Testing RFC") self.assertEqual(doc.abstract, "This is some interesting text.") self.assertEqual(doc.get_state_slug(), "rfc") @@ -602,4 +600,4 @@ class RFCEditorUndoTests(TestCase): e = DeletedEvent.objects.all().order_by("-time", "-id")[0] e.content_type.model_class().objects.create(**json.loads(e.json)) - self.assertTrue(StateDocEvent.objects.filter(desc="First", doc=draft)) \ No newline at end of file + self.assertTrue(StateDocEvent.objects.filter(desc="First", doc=draft)) diff --git a/ietf/templates/doc/charter/action_announcement_text.html b/ietf/templates/doc/charter/action_announcement_text.html index 88a1b6141..e087b175b 100644 --- a/ietf/templates/doc/charter/action_announcement_text.html +++ b/ietf/templates/doc/charter/action_announcement_text.html @@ -21,7 +21,7 @@ {% if user|has_role:"Secretariat" %} + href="{% url 'ietf.doc.views_charter.approve' name=charter.name %}"> Charter approval page {% endif %} diff --git a/ietf/templates/doc/charter/approve.html b/ietf/templates/doc/charter/approve.html index f109da687..2a8654482 100644 --- a/ietf/templates/doc/charter/approve.html +++ b/ietf/templates/doc/charter/approve.html @@ -2,16 +2,16 @@ {# Copyright The IETF Trust 2015, All Rights Reserved #} {% load origin %} {% load django_bootstrap5 %} -{% block title %}Approve {{ charter.canonical_name }}{% endblock %} +{% block title %}Approve {{ charter.name }}{% endblock %} {% block content %} {% origin %} -

Approve {{ charter.canonical_name }}-{{ charter.rev }}

+

Approve {{ charter.name }}-{{ charter.rev }}

{% csrf_token %}
{{ announcement }}
+ href="{% url "ietf.doc.views_charter.action_announcement_text" name=charter.name %}?next=approve"> Edit/regenerate announcement Change responsible AD
- {{ charter.canonical_name }}-{{ charter.rev }} + {{ charter.name }}-{{ charter.rev }} {% csrf_token %} {% bootstrap_form form %}
+ href="{% url "ietf.doc.views_doc.document_main" name=charter.name %}"> Back
diff --git a/ietf/templates/doc/document_bibtex.bib b/ietf/templates/doc/document_bibtex.bib index 5dda4649e..2e4dc9c87 100644 --- a/ietf/templates/doc/document_bibtex.bib +++ b/ietf/templates/doc/document_bibtex.bib @@ -3,7 +3,7 @@ {% load ietf_filters %} {% load textfilters %} -{% if doc.get_state_slug == "rfc" %} +{% if doc.type_id == "rfc" %} {% if doc.stream|slugify == "legacy" %} % Datatracker information for RFCs on the Legacy Stream is unfortunately often % incorrect. Please correct the bibtex below based on the information in the @@ -16,7 +16,7 @@ publisher = {RFC Editor}, doi = {% templatetag openbrace %}{{ doi }}{% templatetag closebrace %}, url = {% templatetag openbrace %}{{ doc.rfc_number|rfceditor_info_url }}{% templatetag closebrace %},{% else %} -{% if published %}%% You should probably cite rfc{{ latest_revision.doc.rfc_number }} instead of this I-D.{% else %}{% if replaced_by %}%% You should probably cite {{replaced_by|join:" or "}} instead of this I-D.{% else %} +{% if published_as %}%% You should probably cite rfc{{ published_as.rfc_number }} instead of this I-D.{% else %}{% if replaced_by %}%% You should probably cite {{replaced_by|join:" or "}} instead of this I-D.{% else %} {% if doc.rev != latest_revision.rev %}%% You should probably cite {{latest_revision.doc.name}}-{{latest_revision.rev}} instead of this revision.{%endif%}{% endif %}{% endif %} @techreport{% templatetag openbrace %}{{doc.name|slice:"6:"}}-{{doc.rev}}, number = {% templatetag openbrace %}{{doc.name}}-{{doc.rev}}{% templatetag closebrace %}, @@ -29,7 +29,7 @@ title = {% templatetag openbrace %}{% templatetag openbrace %}{{doc.title|texescape}}{% templatetag closebrace %}{% templatetag closebrace %}, pagetotal = {{ doc.pages }}, year = {{ doc.pub_date.year }}, - month = {{ doc.pub_date|date:"b" }},{% if not doc.rfc_number or doc.pub_date.day == 1 and doc.pub_date.month == 4 %} + month = {{ doc.pub_date|date:"b" }},{% if not doc.type_id == "rfc" or doc.pub_date.day == 1 and doc.pub_date.month == 4 %} day = {{ doc.pub_date.day }},{% endif %} abstract = {% templatetag openbrace %}{{ doc.abstract|clean_whitespace|texescape }}{% templatetag closebrace %}, {% templatetag closebrace %} diff --git a/ietf/templates/doc/document_history.html b/ietf/templates/doc/document_history.html index 9c76774b8..91d5d393c 100644 --- a/ietf/templates/doc/document_history.html +++ b/ietf/templates/doc/document_history.html @@ -11,7 +11,7 @@ - + {% endblock %} {% block content %} {% origin %} @@ -20,7 +20,17 @@

Revision differences

{% include "doc/document_history_form.html" with doc=doc diff_revisions=diff_revisions action=rfcdiff_base_url snapshot=snapshot only %} {% endif %} -

Document history

+

Document history + {% if related %} +
+ {% for related_docalias in related %} + + Related history for {{ related_docalias.name }} + + {% endfor %} +
+ {% endif %}

{% if can_add_comment %}
Date - Rev. + {% if doc.type_id != "rfc" %}Rev.{% endif %} By Action @@ -45,7 +55,7 @@
{{ e.time|date:"Y-m-d" }}
- {{ e.rev }} + {% if doc.type_id != "rfc" %}{{ e.rev }}{% endif %} {{ e.by|escape }} {{ e.desc|format_history_text }} diff --git a/ietf/templates/doc/document_html.html b/ietf/templates/doc/document_html.html index 3b7e63568..4f7eaca0d 100644 --- a/ietf/templates/doc/document_html.html +++ b/ietf/templates/doc/document_html.html @@ -11,7 +11,7 @@ - {% if not snapshot and doc.get_state_slug == "rfc" %} + {% if doc.type_id == "rfc" %} RFC {{ doc.rfc_number }} - {{ doc.title }} {% else %} {{ doc.name }}-{{ doc.rev }} diff --git a/ietf/templates/doc/document_info.html b/ietf/templates/doc/document_info.html index 2807d275d..287f083c3 100644 --- a/ietf/templates/doc/document_info.html +++ b/ietf/templates/doc/document_info.html @@ -11,15 +11,15 @@ <th scope="row">{% if document_html %}Document type{% else %}Type{% endif %}</th> <td class="edit"></td> <td> - {% if doc.get_state_slug == "rfc" and not snapshot %} + {% if doc.type_id == "rfc" %} <span class="text-success">RFC {% if not document_html %} - {{ doc.std_level }} - {% else %} - <span class="badge rounded-pill badge-{% if not snapshot %}{{ doc|std_level_to_label_format }}{% else %}draft{% endif %}">{{ doc.std_level }}</span> - {% endif %} + {% else %} + <span class="badge rounded-pill badge-{% if not snapshot %}{{ doc|std_level_to_label_format }}{% else %}draft{% endif %}">{{ doc.std_level }}</span> + {% endif %} </span> - {% if published %} + {% if doc.pub_date %} {% if document_html %}<br>{% else %}({% endif %}{{ doc.pub_date|date:"F Y" }}{% if not document_html %}){% endif %} {% else %} <span class="text-body-secondary">(Publication date unknown)</span> @@ -52,7 +52,6 @@ {% if proposed_status_changes %} <div>Proposed status changed by {{ proposed_status_changes|urlize_related_source_list|join:", " }}</div> {% endif %} - {% if rfc_aliases %}<div>Also known as {{ rfc_aliases|join:", "|urlize_ietf_docs }}</div>{% endif %} {% if draft_name %} <div> Was @@ -132,91 +131,93 @@ </td> </tr> {% endif %} - {% if replaces or not document_html and can_edit_stream_info %} - <tr> - <td></td> - <th scope="row">Replaces</th> - <td class="edit"> - {% if can_edit_stream_info and not snapshot %} - <a class="btn btn-primary btn-sm" - href="{% url 'ietf.doc.views_draft.replaces' name=doc.name %}">Edit</a> - {% endif %} - </td> - <td> - {% if replaces %} - {% if document_html %} - {{ replaces|urlize_related_target_list:document_html|join:"<br>" }} - {% else %} - {{ replaces|urlize_related_target_list:document_html|join:", " }} - {% endif %} - {% else %} - <span class="text-body-secondary">(None)</span> - {% endif %} - </td> - </tr> - {% endif %} - {% if replaced_by %} - <tr> - <td></td> - <th scope="row"> - Replaced by - </th> - <td class="edit"> - </td> - <td> - {% if document_html %} - {{ replaced_by|urlize_related_source_list:document_html|join:"<br>" }} - {% else %} - {{ replaced_by|urlize_related_source_list:document_html|join:", " }} - {% endif %} - </td> - </tr> - {% endif %} - {% if can_view_possibly_replaces %} - {% if possibly_replaces %} + {% if doc.type_id != "rfc" %} + {% if replaces or not document_html and can_edit_stream_info %} <tr> <td></td> - <th scope="row"> - Possibly Replaces - </th> + <th scope="row">Replaces</th> <td class="edit"> - {% if can_edit_replaces and not snapshot %} + {% if can_edit_stream_info and not snapshot %} <a class="btn btn-primary btn-sm" - href="{% url 'ietf.doc.views_draft.review_possibly_replaces' name=doc.name %}"> - Edit - </a> + href="{% url 'ietf.doc.views_draft.replaces' name=doc.name %}">Edit</a> {% endif %} </td> <td> - {% if document_html %} - {{ possibly_replaces|urlize_related_target_list:document_html|join:"<br>" }} + {% if replaces %} + {% if document_html %} + {{ replaces|urlize_related_target_list:document_html|join:"<br>" }} + {% else %} + {{ replaces|urlize_related_target_list:document_html|join:", " }} + {% endif %} {% else %} - {{ possibly_replaces|urlize_related_target_list:document_html|join:", " }} + <span class="text-body-secondary">(None)</span> {% endif %} </td> </tr> {% endif %} - {% if possibly_replaced_by %} + {% if replaced_by %} <tr> <td></td> <th scope="row"> - Possibly Replaced By + Replaced by </th> <td class="edit"> - {% if can_edit_replaces and not snapshot %} - {% comment %}<a class="btn btn-primary btn-sm" -href="{% url 'ietf.doc.views_draft.review_possibly_replaces' name=doc.name %}">Edit</a>{% endcomment %} - {% endif %} </td> <td> {% if document_html %} - {{ possibly_replaced_by|urlize_related_source_list:document_html|join:"<br>" }} + {{ replaced_by|urlize_related_source_list:document_html|join:"<br>" }} {% else %} - {{ possibly_replaced_by|urlize_related_source_list:document_html|join:", " }} + {{ replaced_by|urlize_related_source_list:document_html|join:", " }} {% endif %} </td> </tr> {% endif %} + {% if can_view_possibly_replaces %} + {% if possibly_replaces %} + <tr> + <td></td> + <th scope="row"> + Possibly Replaces + </th> + <td class="edit"> + {% if can_edit_replaces and not snapshot %} + <a class="btn btn-primary btn-sm" + href="{% url 'ietf.doc.views_draft.review_possibly_replaces' name=doc.name %}"> + Edit + </a> + {% endif %} + </td> + <td> + {% if document_html %} + {{ possibly_replaces|urlize_related_target_list:document_html|join:"<br>" }} + {% else %} + {{ possibly_replaces|urlize_related_target_list:document_html|join:", " }} + {% endif %} + </td> + </tr> + {% endif %} + {% if possibly_replaced_by %} + <tr> + <td></td> + <th scope="row"> + Possibly Replaced By + </th> + <td class="edit"> + {% if can_edit_replaces and not snapshot %} + {% comment %}<a class="btn btn-primary btn-sm" + href="{% url 'ietf.doc.views_draft.review_possibly_replaces' name=doc.name %}">Edit</a>{% endcomment %} + {% endif %} + </td> + <td> + {% if document_html %} + {{ possibly_replaced_by|urlize_related_source_list:document_html|join:"<br>" }} + {% else %} + {{ possibly_replaced_by|urlize_related_source_list:document_html|join:", " }} + {% endif %} + </td> + </tr> + {% endif %} + {% endif %} {% endif %} <tr> <td></td> @@ -231,15 +232,15 @@ href="{% url 'ietf.doc.views_draft.review_possibly_replaces' name=doc.name %}">E </a> {% endif %} </td> - <td {% if stream_desc == "(None)" %}class="text-body-secondary"{%endif%}> - {% if stream_desc != "(None)" %} + <td {% if doc.stream is None %}class="text-body-secondary"{%endif%}> + {% if doc.stream is not None %} {% if doc.stream.name|lower in 'iab,irtf,ise,editorial' %} <a href="{% url 'ietf.group.views.stream_documents' acronym=doc.stream.name|lower %}"> {% endif %} {% if document_html %} {% if doc.stream.name|lower in 'iab,ietf,irtf' %} <img alt="{{ doc.stream.name|upper }} Logo" - title="{{ stream_desc }}" + title="{{ doc.stream.desc }}" class="w-25 mt-1" {% if doc.stream.name|lower == 'iab' %} src="{% static 'ietf/images/iab-logo.svg' %}" @@ -250,20 +251,20 @@ href="{% url 'ietf.doc.views_draft.review_possibly_replaces' name=doc.name %}">E {% endif %} > {% else %} - {{ stream_desc }} + {{ doc.stream.desc }} {% endif %} {% else %} - {{ stream_desc }} + {{ doc.stream.desc }} {% endif %} {% if doc.stream.name|lower in 'iab,irtf,ise,editorial' %} </a> {% endif %} {% else %} - {{ stream_desc }} + (None) {% endif %} </td> </tr> - {% if doc.get_state_slug != "rfc" and not snapshot %} + {% if doc.type_id != "rfc" and not snapshot %} <tr> <td></td> <th scope="row"> @@ -339,70 +340,72 @@ href="{% url 'ietf.doc.views_draft.review_possibly_replaces' name=doc.name %}">E </tr> {% endif %} {% endfor %} - {% if review_assignments or can_request_review %} - <tr> - <td></td> - <th scope="row"> - Reviews - </th> - <td class="edit"> - </td> - <td> - {% for review_assignment in review_assignments %} + {% if doc.type_id != "rfc" %}{# do not show reviews or conflict_reviews for RFCs, even if present #} + {% if review_assignments or can_request_review %} + <tr> + <td></td> + <th scope="row"> + Reviews + </th> + <td class="edit"> + </td> + <td> + {% for review_assignment in review_assignments %} {% include "doc/review_assignment_summary.html" with current_doc_name=doc.name current_rev=doc.rev review_assignment=review_assignment only %} {% endfor %} {% for review_request in review_requests %} {% include "doc/review_request_summary.html" with review_request=review_request only %} - {% endfor %} - {% if no_review_from_teams %} - {% for team in no_review_from_teams %} - {{ team.acronym.upper }}{% if not forloop.last %},{% endif %} - {% endfor %} - will not review this version - {% endif %} - {% if can_request_review or can_submit_unsolicited_review_for_teams %} - <div {% if review_assignments or no_review_from_teams %}class="mt-3"{% endif %}> - {% if can_request_review %} - <a class="btn btn-primary btn-sm" - href="{% url "ietf.doc.views_review.request_review" doc.name %}"> - <i class="bi bi-check-circle"> - </i> - Request review - </a> + {% endfor %} + {% if no_review_from_teams %} + {% for team in no_review_from_teams %} + {{ team.acronym.upper }}{% if not forloop.last %},{% endif %} + {% endfor %} + will not review this version {% endif %} - {% if can_submit_unsolicited_review_for_teams|length == 1 %} - <a class="btn btn-primary btn-sm" - href="{% url "ietf.doc.views_review.complete_review" doc.name can_submit_unsolicited_review_for_teams.0.acronym %}"> - <i class="bi bi-pencil-square"> - </i> - Submit unsolicited review - </a> - {% elif can_submit_unsolicited_review_for_teams %} - <a class="btn btn-primary btn-sm" - href="{% url "ietf.doc.views_review.submit_unsolicited_review_choose_team" doc.name %}"> - <i class="bi bi-pencil-square"> - </i> - Submit unsolicited review - </a> + {% if can_request_review or can_submit_unsolicited_review_for_teams %} + <div {% if review_assignments or no_review_from_teams %}class="mt-3"{% endif %}> + {% if can_request_review %} + <a class="btn btn-primary btn-sm" + href="{% url "ietf.doc.views_review.request_review" doc.name %}"> + <i class="bi bi-check-circle"> + </i> + Request review + </a> + {% endif %} + {% if can_submit_unsolicited_review_for_teams|length == 1 %} + <a class="btn btn-primary btn-sm" + href="{% url "ietf.doc.views_review.complete_review" doc.name can_submit_unsolicited_review_for_teams.0.acronym %}"> + <i class="bi bi-pencil-square"> + </i> + Submit unsolicited review + </a> + {% elif can_submit_unsolicited_review_for_teams %} + <a class="btn btn-primary btn-sm" + href="{% url "ietf.doc.views_review.submit_unsolicited_review_choose_team" doc.name %}"> + <i class="bi bi-pencil-square"> + </i> + Submit unsolicited review + </a> + {% endif %} + </div> {% endif %} - </div> - {% endif %} - </td> - </tr> - {% endif %} - {% if conflict_reviews %} - <tr> - <td></td> - <th scope="row"> - IETF conflict review - </th> - <td class="edit"> - </td> - <td> - {{ conflict_reviews|join:", "|urlize_ietf_docs }} - </td> - </tr> - {% endif %} + </td> + </tr> + {% endif %} + {% if conflict_reviews %} + <tr> + <td></td> + <th scope="row"> + IETF conflict review + </th> + <td class="edit"> + </td> + <td> + {{ conflict_reviews|join:", "|urlize_ietf_docs }} + </td> + </tr> + {% endif %} + {% endif %} {% endif %} {% with doc.docextresource_set.all as resources %} {% if resources or doc.group and doc.group.list_archive or can_edit_stream_info or can_edit_individual %} diff --git a/ietf/templates/doc/document_referenced_by.html b/ietf/templates/doc/document_referenced_by.html index 958108bdd..7f7fe13c3 100644 --- a/ietf/templates/doc/document_referenced_by.html +++ b/ietf/templates/doc/document_referenced_by.html @@ -64,7 +64,7 @@ </a> </td> <td> - {% if ref.source.get_state.slug == 'rfc' %} + {% if ref.source.type_id == "rfc" %} {% with ref.source.std_level as lvl %} {% if lvl %}{{ lvl }}{% endif %} {% endwith %} diff --git a/ietf/templates/doc/document_references.html b/ietf/templates/doc/document_references.html index d9134be6f..268168a11 100644 --- a/ietf/templates/doc/document_references.html +++ b/ietf/templates/doc/document_references.html @@ -35,7 +35,7 @@ <a href="{% url 'ietf.doc.views_doc.document_main' name=name %}">{{ name|prettystdname }}</a> </td> <td> - <b>{{ ref.target.document.title }}</b> + <b>{{ ref.target.title }}</b> <br> <a class="btn btn-primary btn-sm" href="{% url 'ietf.doc.views_doc.document_references' name %}" @@ -51,12 +51,12 @@ </a> </td> <td> - {% if ref.target.document.get_state.slug == 'rfc' %} - {% with ref.target.document.std_level as lvl %} + {% if ref.target.type_id == "rfc" %} + {% with ref.target.std_level as lvl %} {% if lvl %}{{ lvl }}{% endif %} {% endwith %} {% else %} - {% with ref.target.document.intended_std_level as lvl %} + {% with ref.target.intended_std_level as lvl %} {% if lvl %}{{ lvl }}{% endif %} {% endwith %} {% endif %} diff --git a/ietf/templates/doc/document_rfc.html b/ietf/templates/doc/document_rfc.html new file mode 100644 index 000000000..6ceb4427c --- /dev/null +++ b/ietf/templates/doc/document_rfc.html @@ -0,0 +1,339 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2016-2020, All Rights Reserved #} +{% load origin %} +{% load static %} +{% load ietf_filters %} +{% load person_filters %} +{% load textfilters %} +{% block html_attrs %}prefix="og: http://ogp.me/ns# article: http://ogp.me/ns/article#"{% endblock %} +{% block pagehead %} + {% include "doc/opengraph.html" %} + <link rel="alternate" + type="application/atom+xml" + title="Document changes" + href="/feed/document-changes/{{ name }}/"> + <meta name="description" + content="{{ doc.title }} {% if doc.get_state_slug == 'rfc' and not snapshot %}(RFC {{ rfc_number }}{% if published %}, {{ doc.pub_date|date:'F Y' }}{% endif %}{% if obsoleted_by %}; obsoleted by {% for rel in obsoleted_by %}{{ rel.source.canonical_name|prettystdname}}{% if not forloop.last%}, {% endif %}{% endfor %}{% endif %}){% endif %}"> +{% endblock %} +{% block morecss %}.inline { display: inline; }{% endblock %} +{% block title %} + RFC {{ rfc_number }} - {{ doc.title }} +{% endblock %} +{% block content %} + {% origin %} + {{ top|safe }} + <div id="timeline"></div> + {% if doc.rev != latest_rev %} + <div class="alert alert-warning my-3">The information below is for an old version of the document.</div> + {% endif %} + <table class="table table-sm table-borderless"> + {% include "doc/document_info.html" %} + <tbody class="meta border-top"> + {% if milestones %} + <tr> + <td></td> + <th scope="row"> + Associated + {% if doc.stream_id == 'ietf' %} + WG + {% else %} + {{ doc.stream }} + {% endif %} milestone{{ milestones|pluralize }}</th> + <td class="edit"></td> + <td> + <dl class="row"> + {% for m in milestones %} + <dt class="col-sm-2 my-0">{{ m.due|date:"M Y" }}</dt> + <dd class="col-sm-10 my-0">{{ m.desc }}</dd> + {% endfor %} + </dl> + </td> + </tr> + {% endif %} + </tbody> + {% if doc.stream_id != 'iab' %} + <tbody class="meta border-top"> + <tr> + <th scope="row"> + IESG + </th> + <th scope="row"> + Responsible AD + </th> + <td class="edit"> + {% if can_edit %} + <a class="btn btn-primary btn-sm" + href="{% url 'ietf.doc.views_draft.edit_ad' name=doc.name %}"> + Edit + </a> + {% endif %} + </td> + <td> + {% if doc.ad %} + {% person_link doc.ad %} + {% else %} + <span class="text-body-secondary"> + (None) + </span> + {% endif %} + </td> + </tr> + <tr> + <td></td> + <th scope="row"> + Send notices to + </th> + <td class="edit"> + {% if can_edit_notify and not snapshot %} + <a class="btn btn-primary btn-sm" + href="{% url 'ietf.doc.views_doc.edit_notify' name=doc.name %}"> + Edit + </a> + {% endif %} + </td> + <td> + {% if doc.notify %} + {{ doc.notify|linkify }} + {% else %} + <span class="text-body-secondary"> + (None) + </span> + {% endif %} + </td> + </tr> + </tbody> + {% endif %} + {% if rfc_editor_state %} + <tbody class="meta border-top"> + <tr> + <th scope="row"> + RFC Editor + </th> + <th scope="row"> + <a href="{% url "ietf.help.views.state" doc=doc.type.slug type="rfceditor" %}"> + RFC Editor state + </a> + </th> + <td class="edit"> + </td> + <td> + {{ rfc_editor_state }} + </td> + </tr> + <tr> + <td></td> + <th scope="row"> + Details + </th> + <td class="edit"> + </td> + <td> + <div> + <a href="https://www.rfc-editor.org/queue2.html#{{ doc.name }}"> + Publication queue entry + </a> + </div> + {% if rfc_editor_auth48_url %} + <div> + <a href="{{ rfc_editor_auth48_url }}"> + Auth48 status + </a> + </div> + {% endif %} + </td> + </tr> + </tbody> + {% endif %} + </table> + <div class="buttonlist"> + <a class="btn btn-primary btn-sm" + href="mailto:{{ doc.name }}@ietf.org?subject=Mail%20regarding%20{{ doc.name }}"> + <i class="bi bi-envelope"> + </i> + Email authors + </a> + {% if doc.group.type.slug == 'wg' or doc.group.type.slug == 'rg' %} + <a class="btn btn-primary btn-sm" + href="mailto:{{ doc.group.list_email }}?subject=Mail%20regarding%20{{ doc.name }}"> + <i class="bi bi-envelope"> + </i> + Email {{ doc.group.type }} + </a> + {% endif %} + <a class="btn btn-primary btn-sm" + href="{% url "ietf.ipr.views.search" %}?submit=draft&id={{ doc.name }}" + rel="nofollow"> + <i class="bi bi-lightning"> + </i> + IPR + {% if doc.related_ipr %} + <span class="badge rounded-pill"> + {{ doc.related_ipr|length }} + </span> + {% endif %} + </a> + <a class="btn btn-primary btn-sm" + href="{% url 'ietf.doc.views_doc.document_references' doc.canonical_name %}" + rel="nofollow"> + <i class="bi bi-arrow-left"> + </i> + References + </a> + <a class="btn btn-primary btn-sm" + href="{% url 'ietf.doc.views_doc.document_referenced_by' doc.canonical_name %}" + rel="nofollow"> + <i class="bi bi-arrow-right"> + </i> + Referenced by + </a> + {# document_draft shows Nits here, excluded for RFCs #} + <div class="dropdown inline"> + <button class="btn btn-primary btn-sm dropdown-toggle" + type="button" + id="ddSearchMenu" + data-bs-toggle="dropdown" + aria-expanded="true"> + <i class="bi bi-search"> + </i> + Search lists + </button> + <ul class="dropdown-menu" role="menu"> + <li role="presentation"> + <a class="dropdown-item" + href="https://mailarchive.ietf.org/arch/search?q=%22{{ doc.name }}%22" + rel="nofollow" + target="_blank"> + IETF Mail Archive + </a> + </li> + <li role="presentation"> + <a class="dropdown-item" + href="https://www.google.com/search?as_q={{ doc.name }}&as_sitesearch={{ search_archive }}" + rel="nofollow" + target="_blank"> + Google + </a> + </li> + {% if user|has_role:"Area Director" %} + <li role="presentation"> + <a class="dropdown-item" + href="https://www.iesg.org/bin/c5i?mid=6&rid=77&target={{ doc.name }}" + rel="nofollow" + target="_blank"> + ARO + </a> + </li> + {% endif %} + </ul> + </div> + {% if user.is_authenticated %} + <a class="btn btn-primary btn-sm track-untrack-doc {% if not doc.tracked_in_personal_community_list %}hide{% endif %}" + href="{% url "ietf.community.views.untrack_document" username=user.username name=doc.name %}" + title="Remove from your personal I-D list"> + <i class="bi bi-bookmark-check-fill"> + </i> + Untrack + </a> + <a class="btn btn-primary btn-sm track-untrack-doc {% if doc.tracked_in_personal_community_list %}hide{% endif %}" + href="{% url "ietf.community.views.track_document" username=user.username name=doc.name %}" + title="Add to your personal I-D list"> + <i class="bi bi-bookmark"> + </i> + Track + </a> + {% endif %} + {% if user.review_teams %} + <a class="btn btn-primary btn-sm review-wish-add-remove-doc ajax {% if not doc.has_review_wish %}hide{% endif %}" + href="{% url "ietf.doc.views_review.review_wishes_remove" name=doc.name %}?next={{ request.get_full_path|urlencode }}" + title="Remove from your review wishes for all teams"> + <i class="bi bi-chat-left-heart-fill"> + </i> + Remove review wishes + </a> + <a class="btn btn-primary btn-sm review-wish-add-remove-doc {% if user.review_teams|length_is:"1" %}ajax {% endif %}{% if doc.has_review_wish %}hide{% endif %}" + href="{% url "ietf.doc.views_review.review_wish_add" name=doc.name %}?next={{ request.get_full_path|urlencode }}" + title="Add to your review wishes"> + <i class="bi bi-chat-left-heart"> + </i> + Add review wish + </a> + {% endif %} + {% if can_edit and iesg_state.slug != 'idexists' %} + <a class="btn btn-primary btn-sm" + href="{% url 'ietf.doc.views_ballot.lastcalltext' name=doc.name %}"> + Last call text + </a> + <a class="btn btn-primary btn-sm" + href="{% url 'ietf.doc.views_ballot.ballot_writeupnotes' name=doc.name %}"> + Ballot text + </a> + <a class="btn btn-primary btn-sm" + href="{% url 'ietf.doc.views_ballot.ballot_approvaltext' name=doc.name %}"> + Announcement text + </a> + {% endif %} + {% if actions %} + {% for label, url in actions %} + <a class="btn btn-primary btn-sm" href="{{ url }}"> + {{ label|capfirst_allcaps }} + </a> + {% endfor %} + {% endif %} + </div> + {% if doc.get_state_slug == "active" or doc.get_state_slug == "rfc" %} + <div class="card mt-5"> + <div class="card-header"> + {% if doc.get_state_slug == "rfc" and not snapshot %} + RFC {{ rfc_number }} + {% else %} + {{ name }}-{{ doc.rev }} + {% endif %} + </div> + <div class="card-body"> + <pre>{{ content|sanitize|safe|default:"(Unavailable)" }}</pre> + </div> + </div> + {% if split_content %} + <a class="btn btn-primary my-3" href="?include_text=1"> + <i class="bi bi-caret-down"> + </i> + Show full document + </a> + {% endif %} + {% else %} + <div class="card border-warning mt-5"> + <div class="card-header bg-warning"> + <p><b>This Internet-Draft is no longer active. A copy of + the expired Internet-Draft is available in these formats:</b></p> + + {% include "doc/document_format_buttons.html" %} + </div> + <div class="card-body"> + <p class="h5 card-title"> + Abstract + </p> + <p class="card-text"> + {{ doc.abstract }} + </p> + <p class="h5 card-title"> + Authors + </p> + <p class="card-text"> + {% for author in doc.documentauthor_set.all %} + {% person_link author.person %} + {% if not forloop.last %}<br>{% endif %} + {% endfor %} + </p> + <p class="text-body-secondary card-text"> + (Note: The e-mail addresses provided for the authors of this Internet-Draft may no longer be valid.) + </p> + </div> + </div> + {% endif %} + {% endblock %} + {% block js %} + <script src="{% static 'ietf/js/d3.js' %}"> + </script> + <script src="{% static 'ietf/js/document_timeline.js' %}"> + </script> + {% endblock %} diff --git a/ietf/templates/doc/document_status_change.html b/ietf/templates/doc/document_status_change.html index 87e0384b7..bfcad6de6 100644 --- a/ietf/templates/doc/document_status_change.html +++ b/ietf/templates/doc/document_status_change.html @@ -52,7 +52,7 @@ <td class="edit"></td> <td> {% for rel in relation_group.list %} - {{ rel.target.document.canonical_name|upper|urlize_ietf_docs }}{% if not forloop.last %},{% endif %} + {{ rel.target.canonical_name|upper|urlize_ietf_docs }}{% if not forloop.last %},{% endif %} {% endfor %} </td> </tr> diff --git a/ietf/templates/doc/idnits2-state.txt b/ietf/templates/doc/idnits2-state.txt index 770654979..55fc78927 100644 --- a/ietf/templates/doc/idnits2-state.txt +++ b/ietf/templates/doc/idnits2-state.txt @@ -1,7 +1,7 @@ {% load ietf_filters %}{% filter linebreaks_lf %}{% comment %} -{% endcomment %}Doc-tag: {{doc.name}};datatracker{% if doc.rfcnum %} -Doc-rfcnum: {{doc.rfcnum}}{% endif %} +{% endcomment %}Doc-tag: {{doc.name}};datatracker{% if doc.type_id == "rfc" %} +Doc-rfcnum: {{doc.rfc_number}}{% endif %} Doc-created: {{doc.created|date:"Y-m-d"}};datatracker{% if doc.deststatus %} Doc-deststatus: {{doc.deststatus}};datatracker{% endif %} Doc-rev: {{doc.rev}};datatracker -{% endfilter %} \ No newline at end of file +{% endfilter %} diff --git a/ietf/templates/doc/mail/last_call_announcement.txt b/ietf/templates/doc/mail/last_call_announcement.txt index 79abcc148..ee6e9d378 100644 --- a/ietf/templates/doc/mail/last_call_announcement.txt +++ b/ietf/templates/doc/mail/last_call_announcement.txt @@ -33,7 +33,7 @@ No IPR declarations have been submitted directly on this I-D. {% if downrefs %} The document contains these normative downward references. See RFC 3967 for additional information: -{% for ref in downrefs %} {{ref.target.document.canonical_name}}: {{ref.target.document.title}} ({{ref.target.document.std_level}} - {{ref.target.document.stream.desc}}) +{% for ref in downrefs %} {{ref.target.canonical_name}}: {{ref.target.title}} ({{ref.target.std_level}} - {{ref.target.stream.desc}}) {% endfor %}{%endif%} {% endautoescape %} diff --git a/ietf/templates/doc/search/search_result_row.html b/ietf/templates/doc/search/search_result_row.html index 57632651f..5c65a4983 100644 --- a/ietf/templates/doc/search/search_result_row.html +++ b/ietf/templates/doc/search/search_result_row.html @@ -49,13 +49,13 @@ {% if doc.pages %}<small class="float-end text-body-secondary d-none d-sm-block">{{ doc.pages }} page{{ doc.pages|pluralize }}</small>{% endif %} <div> <a href="{{ doc.get_absolute_url }}"> - {% if doc.get_state_slug == "rfc" %} + {% if doc.type_id == "rfc" %} RFC {{ doc.rfc_number }} {% else %} {{ doc.name }}-{{ doc.rev }} {% endif %} </a> - {% if doc.get_state_slug == "rfc" and "draft" in doc.name %}<i class="text-body-secondary">(was {{ doc.name }})</i>{% endif %} +{# {% if doc.type_id == "rfc" and "draft" in doc.name %}<i class="text-body-secondary">(was {{ doc.name }})</i>{% endif %} TODO drop this or look up the corresponding draft name#} <br> {% comment %} <div class="float-end"> @@ -106,19 +106,19 @@ {% endif %} </td> <td> - {% if doc.latest_revision_date|timesince_days|new_enough:request and doc.get_state_slug != "rfc" %} + {% if doc.latest_revision_date|timesince_days|new_enough:request and doc.type_id != "rfc" %} {% if doc.rev != "00" %} <a href="{{ rfcdiff_base_url }}?url2={{ doc.name }}-{{ doc.rev }}"> {% elif doc.replaces %} <a href="{{ rfcdiff_base_url }}?url1={{ doc.replaces_canonical_name }}&url2={{ doc.name }}-{{ doc.rev }}"> {% endif %} {% endif %} - {% if doc.get_state_slug == "rfc" %} + {% if doc.type_id == "rfc" %} {{ doc.latest_revision_date|date:"Y-m" }} {% else %} {{ doc.latest_revision_date|date:"Y-m-d" }} {% endif %} - {% if doc.latest_revision_date|timesince_days|new_enough:request and doc.get_state_slug != "rfc" %} + {% if doc.latest_revision_date|timesince_days|new_enough:request and doc.type_id != "rfc" %} {% if doc.rev != "00" or doc.replaces %}</a>{% endif %} {% endif %} {% if doc.latest_revision_date|timesince_days|new_enough:request %} @@ -127,7 +127,7 @@ <span class="badge rounded-pill bg-success">New</span> </div> {% endif %} - {% if doc.get_state_slug == "active" and doc.expirable and doc.expires|timesince_days|expires_soon:request %} + {% if doc.type_id == "draft" and doc.get_state_slug == "active" and doc.expirable and doc.expires|timesince_days|expires_soon:request %} <br> <span class="badge rounded-pill bg-warning">Expires soon</span> {% endif %} diff --git a/ietf/templates/doc/status_change/approval_text.txt b/ietf/templates/doc/status_change/approval_text.txt index bf722dcce..8327f8fe8 100644 --- a/ietf/templates/doc/status_change/approval_text.txt +++ b/ietf/templates/doc/status_change/approval_text.txt @@ -1,11 +1,11 @@ {% load ietf_filters %}{% load mail_filters %}{% autoescape off %}From: The IESG <iesg-secretary@ietf.org> To: {{ to }}{% if cc %} Cc: {{ cc }}{% endif %} -Subject: {{action}}: {{relateddoc.target.document.title}} to {{newstatus}} +Subject: {{action}}: {{relateddoc.target.title}} to {{newstatus}} {% filter wordwrap:78 %}The IESG has approved changing the status of the following document: -- {{relateddoc.target.document.title }} - ({{relateddoc.target.document.canonical_name }}) to {{ newstatus }} +- {{relateddoc.target.title }} + ({{relateddoc.target.canonical_name }}) to {{ newstatus }} This {{action|lower}} is documented at: {{status_change_url}} diff --git a/ietf/templates/group/edit_milestones.html b/ietf/templates/group/edit_milestones.html index 36a4a714c..1ae1c57a5 100644 --- a/ietf/templates/group/edit_milestones.html +++ b/ietf/templates/group/edit_milestones.html @@ -14,8 +14,8 @@ <a class="btn btn-primary" href="{{ group.about_url }}">{{ group.acronym }} {{ group.type.name }}</a> {% if group.charter %} <a class="btn btn-primary" - href="{% url "ietf.doc.views_doc.document_main" name=group.charter.canonical_name %}"> - {{ group.charter.canonical_name }} + href="{% url "ietf.doc.views_doc.document_main" name=group.charter.name %}"> + {{ group.charter.name }} </a> {% endif %} {% if can_change_uses_milestone_dates %} @@ -106,7 +106,7 @@ </div> </div> <a class="btn btn-secondary float-end" - href="{% if milestone_set == "charter" %}{% url "ietf.doc.views_doc.document_main" name=group.charter.canonical_name %}{% else %}{{ group.about_url }}{% endif %}"> + href="{% if milestone_set == "charter" %}{% url "ietf.doc.views_doc.document_main" name=group.charter.name %}{% else %}{{ group.about_url }}{% endif %}"> Cancel </a> <button class="btn btn-primary hidden" diff --git a/ietf/templates/iesg/moderator_doc.html b/ietf/templates/iesg/moderator_doc.html index fb48b75be..051124e47 100644 --- a/ietf/templates/iesg/moderator_doc.html +++ b/ietf/templates/iesg/moderator_doc.html @@ -165,13 +165,13 @@ Parts Copyright (c) 2009 The IETF Trust, all rights reserved. {% if downrefs %} <p>If APPROVED - The Secretariat will add to the downref registry:<br> {% for ref in downrefs %} - + Add {{ref.target.document.canonical_name}} - ({{ref.target.document.std_level}} - {{ref.target.document.stream.desc}}) + + Add {{ref.target.canonical_name}} + ({{ref.target.std_level}} - {{ref.target.stream.desc}}) to downref registry.<br> - {% if not ref.target.document.std_level %} + {% if not ref.target.std_level %} +++ Warning: The standards level has not been set yet!!!<br> {% endif %} - {% if not ref.target.document.stream %} + {% if not ref.target.stream %} +++ Warning: document stream has not been set yet!!!<br> {% endif %} {% endfor %}</p> diff --git a/ietf/utils/management/commands/check_draft_event_revision_integrity.py b/ietf/utils/management/commands/check_draft_event_revision_integrity.py index c8d2cbd21..c2d427278 100644 --- a/ietf/utils/management/commands/check_draft_event_revision_integrity.py +++ b/ietf/utils/management/commands/check_draft_event_revision_integrity.py @@ -54,7 +54,7 @@ class Command(BaseCommand): doc = getattr(obj, docattr) time = getattr(obj, timeattr) if not obj.rev: - if not doc.is_rfc(): + if doc.type_id != "rfc": self.stdout.write("Bad revision number: %-52s: '%s'" % (doc.name, obj.rev)) continue rev = int(obj.rev.lstrip('0') or '0') diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index 1e7097625..2efe4ed1c 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -12,11 +12,12 @@ from django.utils.encoding import smart_str import debug # pyflakes:ignore from ietf.doc.models import Document, DocAlias, State, DocumentAuthor, DocEvent, RelatedDocument, NewRevisionDocEvent +from ietf.doc.factories import IndividualDraftFactory, ConflictReviewFactory, StatusChangeFactory, WgDraftFactory, WgRfcFactory from ietf.group.models import Group, GroupHistory, Role, RoleHistory from ietf.iesg.models import TelechatDate from ietf.ipr.models import HolderIprDisclosure, IprDocRel, IprDisclosureStateName, IprLicenseTypeName from ietf.meeting.models import Meeting, ResourceAssociation -from ietf.name.models import StreamName, DocRelationshipName, RoomResourceName, ConstraintName +from ietf.name.models import DocRelationshipName, RoomResourceName, ConstraintName from ietf.person.models import Person, Email from ietf.group.utils import setup_default_community_list_for_group from ietf.review.models import (ReviewRequest, ReviewerSettings, ReviewResultName, ReviewTypeName, ReviewTeamSettings ) @@ -316,7 +317,7 @@ def make_test_data(): doc_alias = DocAlias.objects.create(name=draft.name) doc_alias.docs.add(draft) - RelatedDocument.objects.create(source=draft, target=old_alias, relationship=DocRelationshipName.objects.get(slug='replaces')) + RelatedDocument.objects.create(source=draft, target=old_draft, relationship=DocRelationshipName.objects.get(slug='replaces')) old_draft.set_state(State.objects.get(type='draft', slug='repl')) DocumentAuthor.objects.create( @@ -391,37 +392,27 @@ def make_test_data(): ) # an independent submission before review - doc = Document.objects.create(name='draft-imaginary-independent-submission',type_id='draft',rev='00', - title="Some Independent Notes on Imagination") - doc.set_state(State.objects.get(used=True, type="draft", slug="active")) - DocAlias.objects.create(name=doc.name).docs.add(doc) + IndividualDraftFactory(title="Some Independent Notes on Imagination") # an irtf submission mid review - doc = Document.objects.create(name='draft-imaginary-irtf-submission', type_id='draft',rev='00', - stream=StreamName.objects.get(slug='irtf'), title="The Importance of Research Imagination") - docalias = DocAlias.objects.create(name=doc.name) - docalias.docs.add(doc) - doc.set_state(State.objects.get(type="draft", slug="active")) - crdoc = Document.objects.create(name='conflict-review-imaginary-irtf-submission', type_id='conflrev', - rev='00', notify="fsm@ietf.org", title="Conflict Review of IRTF Imagination Document") - DocAlias.objects.create(name=crdoc.name).docs.add(crdoc) - crdoc.set_state(State.objects.get(name='Needs Shepherd', type__slug='conflrev')) - crdoc.relateddocument_set.create(target=docalias,relationship_id='conflrev') + doc = IndividualDraftFactory(name="draft-imaginary-irtf-submission", stream_id="irtf", title="The Importance of Research Imagination") + ConflictReviewFactory(name="conflict-review-imaginary-irtf-submission", review_of=doc, notify="fsm@ietf.org", title="Conflict Review of IRTF Imagination Document") # A status change mid review iesg = Group.objects.get(acronym='iesg') - doc = Document.objects.create(name='status-change-imaginary-mid-review',type_id='statchg', rev='00', - notify="fsm@ietf.org", group=iesg, title="Status Change Review without Imagination") - doc.set_state(State.objects.get(slug='needshep',type__slug='statchg')) - docalias = DocAlias.objects.create(name='status-change-imaginary-mid-review') - docalias.docs.add(doc) + doc = StatusChangeFactory( + name='status-change-imaginary-mid-review', + notify="fsm@ietf.org", + group=iesg, + title="Status Change Review without Imagination", + states= [State.objects.get(type_id="statchg",slug="needshep")] + ) # Some things for a status change to affect def rfc_for_status_change_test_factory(name,rfc_num,std_level_id): - target_rfc = Document.objects.create(name=name, type_id='draft', std_level_id=std_level_id, notify="%s@ietf.org"%name) - target_rfc.set_state(State.objects.get(slug='rfc',type__slug='draft')) - DocAlias.objects.create(name=name).docs.add(target_rfc) - DocAlias.objects.create(name='rfc%d'%rfc_num).docs.add(target_rfc) + target_rfc = WgRfcFactory(rfc_number=rfc_num, std_level_id=std_level_id) + source_draft = WgDraftFactory(name=name, states=[("draft","rfc")], notify=f"{name}@ietf.org") + source_draft.relateddocument_set.create(relationship_id="became_rfc", target=target_rfc) return target_rfc rfc_for_status_change_test_factory('draft-ietf-random-thing',9999,'ps') rfc_for_status_change_test_factory('draft-ietf-random-otherthing',9998,'inf')