From f15ba3346b06196e9c64d3a98491b36339c55704 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz Date: Wed, 21 Jan 2015 19:01:47 +0000 Subject: [PATCH] Merged in [8846] from rjsparks@nostrum.com: Added tests for document urls that provide a revision for all the document types the view code currently handles. Refactored parts of Document and DocHistory into DocumentInfo to get the tests to pass. (but careful review is probably warranted). - Legacy-Id: 8880 Note: SVN reference [8846] has been migrated to Git commit ce5bda1835b32640d17136d09262f654fade94d4 --- ietf/doc/models.py | 214 ++++++++++++++++++++++------------------ ietf/doc/tests.py | 31 +++++- ietf/utils/test_data.py | 13 ++- 3 files changed, 157 insertions(+), 101 deletions(-) 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