diff --git a/ietf/doc/models.py b/ietf/doc/models.py index 4a8823312..f9bc10746 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -168,6 +168,53 @@ class DocumentInfo(models.Model): else: return None + def friendly_state(self): + """ Return a concise text description of the document's current state.""" + state = self.get_state() + if not state: + return "Unknown state" + + if self.type_id == 'draft': + iesg_state = self.get_state("draft-iesg") + iesg_state_summary = None + if iesg_state: + iesg_substate = [t for t in self.tags.all() if t.slug in IESG_SUBSTATE_TAGS] + # There really shouldn't be more than one tag in iesg_substate, but this will do something sort-of-sensible if there is + iesg_state_summary = iesg_state.name + if iesg_substate: + 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) + elif state.slug == "repl": + rs = self.related_that("replaces") + if rs: + return mark_safe("Replaced by " + ", ".join("%s" % (urlreverse('doc_view', kwargs=dict(name=alias.document)), alias.document) for alias in rs)) + else: + return "Replaced" + elif state.slug == "active": + if iesg_state: + if iesg_state.slug == "dead": + # Many drafts in the draft-iesg "Dead" state are not dead + # in other state machines; they're just not currently under + # IESG processing. Show them as "I-D Exists (IESG: Dead)" instead... + return "I-D Exists (IESG: %s)" % iesg_state_summary + elif iesg_state.slug == "lc": + e = self.latest_event(LastCallDocEvent, type="sent_last_call") + if e: + return iesg_state_summary + " (ends %s)" % e.expires.date().isoformat() + + return iesg_state_summary + else: + return "I-D Exists" + else: + if iesg_state and iesg_state.slug == "dead": + return state.name + " (IESG: %s)" % iesg_state_summary + # Expired/Withdrawn by Submitter/IETF + return state.name + else: + return state.name + def author_list(self): return ", ".join(email.address for email in self.authors.all()) @@ -195,6 +242,69 @@ class DocumentInfo(models.Model): else: return False + def relations_that(self, relationship): + """Return the related-document objects that describe a given relationship targeting self.""" + if isinstance(relationship, str): + relationship = [ relationship ] + if isinstance(relationship, tuple): + relationship = list(relationship) + if not isinstance(relationship, list): + raise TypeError("Expected a string, tuple or list, received %s" % type(relationship)) + if isinstance(self, Document): + return RelatedDocument.objects.filter(target__document=self, relationship__in=relationship).select_related('source') + elif isinstance(self, DocHistory): + return RelatedDocHistory.objects.filter(target__document=self, relationship__in=relationship).select_related('source') + else: + return RelatedDocument.objects.none() + + def all_relations_that(self, relationship, related=None): + if not related: + related = [] + rels = self.relations_that(relationship) + for r in rels: + if not r in related: + related += [ r ] + related = r.source.all_relations_that(relationship, related) + return related + + def relations_that_doc(self, relationship): + """Return the related-document objects that describe a given relationship from self to other documents.""" + if isinstance(relationship, str): + relationship = [ relationship ] + if isinstance(relationship, tuple): + relationship = list(relationship) + if not isinstance(relationship, list): + raise TypeError("Expected a string, tuple or list, received %s" % type(relationship)) + if isinstance(self, Document): + return RelatedDocument.objects.filter(source=self, relationship__in=relationship).select_related('target__document') + elif isinstance(self, DocHistory): + return RelatedDocHistory.objects.filter(source=self, relationship__in=relationship).select_related('target__document') + else: + return RelatedDocument.objects.none() + + + def all_relations_that_doc(self, relationship, related=None): + if not related: + related = [] + rels = self.relations_that_doc(relationship) + for r in rels: + if not r in related: + related += [ r ] + related = r.target.document.all_relations_that_doc(relationship, related) + return related + + def related_that(self, relationship): + return list(set([x.source.docalias_set.get(name=x.source.name) for x in self.relations_that(relationship)])) + + def all_related_that(self, relationship, related=None): + return list(set([x.source.docalias_set.get(name=x.source.name) for x in self.all_relations_that(relationship)])) + + def related_that_doc(self, relationship): + return list(set([x.target for x in self.relations_that_doc(relationship)])) + + def all_related_that_doc(self, relationship, related=None): + return list(set([x.target for x in self.all_relations_that_doc(relationship)])) + class Meta: abstract = True @@ -324,57 +434,6 @@ class Document(DocumentInfo): name = name.upper() return name - def relations_that(self, relationship): - """Return the related-document objects that describe a given relationship targeting self.""" - if isinstance(relationship, str): - relationship = [ relationship ] - if isinstance(relationship, tuple): - relationship = list(relationship) - if not isinstance(relationship, list): - raise TypeError("Expected a string, tuple or list, received %s" % type(relationship)) - return RelatedDocument.objects.filter(target__document=self, relationship__in=relationship).select_related('source') - - def all_relations_that(self, relationship, related=None): - if not related: - related = [] - rels = self.relations_that(relationship) - for r in rels: - if not r in related: - related += [ r ] - related = r.source.all_relations_that(relationship, related) - return related - - def relations_that_doc(self, relationship): - """Return the related-document objects that describe a given relationship from self to other documents.""" - if isinstance(relationship, str): - relationship = [ relationship ] - if isinstance(relationship, tuple): - relationship = list(relationship) - if not isinstance(relationship, list): - raise TypeError("Expected a string, tuple or list, received %s" % type(relationship)) - return RelatedDocument.objects.filter(source=self, relationship__in=relationship).select_related('target__document') - - def all_relations_that_doc(self, relationship, related=None): - if not related: - related = [] - rels = self.relations_that_doc(relationship) - for r in rels: - if not r in related: - related += [ r ] - related = r.target.document.all_relations_that_doc(relationship, related) - return related - - def related_that(self, relationship): - return list(set([x.source.docalias_set.get(name=x.source.name) for x in self.relations_that(relationship)])) - - def all_related_that(self, relationship, related=None): - return list(set([x.source.docalias_set.get(name=x.source.name) for x in self.all_relations_that(relationship)])) - - def related_that_doc(self, relationship): - return list(set([x.target for x in self.relations_that_doc(relationship)])) - - def all_related_that_doc(self, relationship, related=None): - return list(set([x.target for x in self.all_relations_that_doc(relationship)])) def telechat_date(self, e=None): if not e: @@ -427,53 +486,6 @@ class Document(DocumentInfo): n = self.canonical_name() return n[3:] if n.startswith("rfc") else None - def friendly_state(self): - """ Return a concise text description of the document's current state.""" - state = self.get_state() - if not state: - return "Unknown state" - - if self.type_id == 'draft': - iesg_state = self.get_state("draft-iesg") - iesg_state_summary = None - if iesg_state: - iesg_substate = [t for t in self.tags.all() if t.slug in IESG_SUBSTATE_TAGS] - # There really shouldn't be more than one tag in iesg_substate, but this will do something sort-of-sensible if there is - iesg_state_summary = iesg_state.name - if iesg_substate: - 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) - elif state.slug == "repl": - rs = self.related_that("replaces") - if rs: - return mark_safe("Replaced by " + ", ".join("%s" % (urlreverse('doc_view', kwargs=dict(name=alias.document)), alias.document) for alias in rs)) - else: - return "Replaced" - elif state.slug == "active": - if iesg_state: - if iesg_state.slug == "dead": - # Many drafts in the draft-iesg "Dead" state are not dead - # in other state machines; they're just not currently under - # IESG processing. Show them as "I-D Exists (IESG: Dead)" instead... - return "I-D Exists (IESG: %s)" % iesg_state_summary - elif iesg_state.slug == "lc": - e = self.latest_event(LastCallDocEvent, type="sent_last_call") - if e: - return iesg_state_summary + " (ends %s)" % e.expires.date().isoformat() - - return iesg_state_summary - else: - return "I-D Exists" - else: - if iesg_state and iesg_state.slug == "dead": - return state.name + " (IESG: %s)" % iesg_state_summary - # Expired/Withdrawn by Submitter/IETF - return state.name - else: - return state.name - def ipr(self,states=('posted','removed')): """Returns the IPR disclosures against this document (as a queryset over IprDocRel).""" from ietf.ipr.models import IprDocRel @@ -547,6 +559,10 @@ class DocHistory(DocumentInfo): def last_presented(self): return self.doc.last_presented() + @property + def groupmilestone_set(self): + return self.doc.groupmilestone_set + class Meta: verbose_name = "document history" verbose_name_plural = "document histories" @@ -593,6 +609,8 @@ def save_document_in_history(doc): return dochist + + class DocAlias(models.Model): """This is used for documents that may appear under multiple names, and in particular for RFCs, which for continuity still keep the diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index 19e01887f..0fda63c48 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -9,7 +9,7 @@ from pyquery import PyQuery from django.core.urlresolvers import reverse as urlreverse from ietf.doc.models import ( Document, DocAlias, DocRelationshipName, RelatedDocument, State, - DocEvent, BallotPositionDocEvent, LastCallDocEvent, WriteupDocEvent ) + DocEvent, BallotPositionDocEvent, LastCallDocEvent, WriteupDocEvent, save_document_in_history ) from ietf.group.models import Group from ietf.person.models import Person from ietf.utils.mail import outbox @@ -205,6 +205,35 @@ class DocTestCase(TestCase): r = self.client.get(urlreverse("doc_view", kwargs=dict(name="draft-xyz123"))) self.assertEqual(r.status_code, 404) + def test_document_primary_and_history_views(self): + make_test_data() + + # Ensure primary views of both current and historic versions of documents works + for docname in ["draft-imaginary-independent-submission", + "conflict-review-imaginary-irtf-submission", + "status-change-imaginary-mid-review", + "charter-ietf-mars", + "agenda-42-mars", + "minutes-42-mars", + "slides-42-mars-1", + ]: + doc = Document.objects.get(name=docname) + # give it some history + save_document_in_history(doc) + doc.rev="01" + doc.save() + + r = self.client.get(urlreverse("doc_view", kwargs=dict(name=doc.name))) + self.assertEqual(r.status_code, 200) + self.assertTrue("%s-01"%docname in r.content) + + r = self.client.get(urlreverse("doc_view", kwargs=dict(name=doc.name,rev="01"))) + self.assertEqual(r.status_code, 302) + + r = self.client.get(urlreverse("doc_view", kwargs=dict(name=doc.name,rev="00"))) + self.assertEqual(r.status_code, 200) + self.assertTrue("%s-00"%docname in r.content) + def test_document_charter(self): make_test_data() diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index 262a4d0c9..238cbb60e 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -263,12 +263,12 @@ def make_test_data(): ) # an independent submission before review - doc = Document.objects.create(name='draft-imaginary-independent-submission',type_id='draft') + doc = Document.objects.create(name='draft-imaginary-independent-submission',type_id='draft',rev='00') doc.set_state(State.objects.get(used=True, type="draft", slug="active")) DocAlias.objects.create(name=doc.name, document=doc) # an irtf submission mid review - doc = Document.objects.create(name='draft-imaginary-irtf-submission', type_id='draft') + doc = Document.objects.create(name='draft-imaginary-irtf-submission', type_id='draft',rev='00') docalias = DocAlias.objects.create(name=doc.name, document=doc) doc.stream = StreamName.objects.get(slug='irtf') doc.save() @@ -298,4 +298,13 @@ def make_test_data(): rfc_for_status_change_test_factory('draft-ietf-random-otherthing',9998,'inf') rfc_for_status_change_test_factory('draft-was-never-issued',14,'unkn') + # Instances of the remaining document types + # (Except liaison, liai-att, and recording which the code in ietf.doc does not use...) + def other_doc_factory(type_id,name): + doc = Document.objects.create(type_id=type_id,name=name,rev='00',group=mars_wg) + DocAlias.objects.create(name=name,document=doc) + other_doc_factory('agenda','agenda-42-mars') + other_doc_factory('minutes','minutes-42-mars') + other_doc_factory('slides','slides-42-mars-1') + return draft