diff --git a/ietf/bin/rfc-editor-index-updates b/ietf/bin/rfc-editor-index-updates index 430f1fe1a..4387805f0 100755 --- a/ietf/bin/rfc-editor-index-updates +++ b/ietf/bin/rfc-editor-index-updates @@ -1,6 +1,6 @@ #!/usr/bin/env python -import os, sys, re, json, datetime +import os, sys, datetime import syslog import traceback @@ -29,22 +29,28 @@ if options.skip_date: skip_date = datetime.datetime.strptime(options.skip_date, "%Y-%m-%d").date() from ietf.utils.pipe import pipe -from ietf.sync.rfceditor import * from ietf.doc.utils import rebuild_reference_relations +import ietf.sync.rfceditor syslog.syslog("Updating document metadata from RFC index from %s" % settings.RFC_EDITOR_QUEUE_URL) -response = fetch_index_xml(settings.RFC_EDITOR_INDEX_URL) -data = parse_index(response) +response = ietf.sync.rfceditor.fetch_index_xml(settings.RFC_EDITOR_INDEX_URL) +data = ietf.sync.rfceditor.parse_index(response) -if len(data) < MIN_INDEX_RESULTS: +if len(data) < ietf.sync.rfceditor.MIN_INDEX_RESULTS: syslog.syslog("Not enough results, only %s" % len(data)) sys.exit(1) -changed, new_rfcs = update_docs_from_rfc_index(data, skip_older_than_date=skip_date) +new_rfcs = [] +for changes, doc, rfc_published in ietf.sync.rfceditor.update_docs_from_rfc_index(data, skip_older_than_date=skip_date): + if rfc_published: + new_rfcs.append(doc) -for c in changed: - syslog.syslog(c) + for c in changes: + syslog.syslog("%s: %s" % (doc.name, c)) + print "%s: %s" % (doc.name, c) + +sys.exit(0) # This can be called while processing a notifying POST from the RFC Editor # Spawn a child to sync the rfcs and calculate new reference relationships diff --git a/ietf/doc/expire.py b/ietf/doc/expire.py index b2a62ff91..a57ca6203 100644 --- a/ietf/doc/expire.py +++ b/ietf/doc/expire.py @@ -6,7 +6,7 @@ import datetime, os, shutil, glob, re from pathlib import Path from ietf.utils.mail import send_mail -from ietf.doc.models import Document, DocEvent, State, save_document_in_history, IESG_SUBSTATE_TAGS +from ietf.doc.models import Document, DocEvent, State, IESG_SUBSTATE_TAGS from ietf.person.models import Person, Email from ietf.meeting.models import Meeting from ietf.doc.utils import add_state_change_event @@ -131,8 +131,9 @@ def expire_draft(doc): system = Person.objects.get(name="(System)") + events = [] + # change the state - save_document_in_history(doc) if doc.latest_event(type='started_iesg_process'): new_state = State.objects.get(used=True, type="draft-iesg", slug="dead") prev_state = doc.get_state(new_state.type_id) @@ -141,15 +142,13 @@ def expire_draft(doc): doc.set_state(new_state) doc.tags.remove(*prev_tags) e = add_state_change_event(doc, system, prev_state, new_state, prev_tags=prev_tags, new_tags=[]) + if e: + events.append(e) - e = DocEvent(doc=doc, by=system) - e.type = "expired_document" - e.desc = "Document has expired" - e.save() + events.append(DocEvent.objects.create(doc=doc, by=system, type="expired_document", desc="Document has expired")) doc.set_state(State.objects.get(used=True, type="draft", slug="expired")) - doc.time = datetime.datetime.now() - doc.save() + doc.save_with_history(events) def clean_up_draft_files(): """Move unidentified and old files out of the Internet Draft directory.""" diff --git a/ietf/doc/lastcall.py b/ietf/doc/lastcall.py index 6310de4a4..016b85e41 100644 --- a/ietf/doc/lastcall.py +++ b/ietf/doc/lastcall.py @@ -5,7 +5,6 @@ import datetime from django.db.models import Q from ietf.doc.models import Document, State, DocEvent, LastCallDocEvent, WriteupDocEvent -from ietf.doc.models import save_document_in_history from ietf.doc.models import IESG_SUBSTATE_TAGS from ietf.person.models import Person from ietf.doc.utils import add_state_change_event @@ -50,8 +49,6 @@ def expire_last_call(doc): else: raise ValueError("Unexpected document type to expire_last_call(): %s" % doc.type) - save_document_in_history(doc) - prev_state = doc.get_state(new_state.type_id) doc.set_state(new_state) @@ -60,8 +57,7 @@ def expire_last_call(doc): system = Person.objects.get(name="(System)") e = add_state_change_event(doc, system, prev_state, new_state, prev_tags=prev_tags, new_tags=[]) - - doc.time = (e and e.time) or datetime.datetime.now() - doc.save() + if e: + doc.save_with_history([e]) email_last_call_expired(doc) diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 6184458d8..2fdea8310 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -87,6 +87,20 @@ def email_ad(request, doc, ad, changed_by, text, subject=None): doc=doc, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url())) +def email_update_telechat(request, doc, text): + to = set(['iesg@ietf.org','iesg-secretary@ietf.org']) + to.update(set([x.strip() for x in doc.notify.replace(';', ',').split(',')])) + + if not to: + return + + text = strip_tags(text) + send_mail(request, list(to), None, + "Telechat update notice: %s" % doc.file_tag(), + "doc/mail/update_telechat.txt", + dict(text=text, + url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url())) + def generate_ballot_writeup(request, doc): e = doc.latest_event(type="iana_review") diff --git a/ietf/doc/migrations/0006_auto_20150924_0800.py b/ietf/doc/migrations/0006_auto_20150924_0800.py new file mode 100644 index 000000000..2440cf776 --- /dev/null +++ b/ietf/doc/migrations/0006_auto_20150924_0800.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + +def fix_buggy_author_foreignkey(apps, schema_editor): + DocumentAuthor = apps.get_model("doc", "DocumentAuthor") + # apparently, we have a buggy key in the DB, fix it + DocumentAuthor.objects.filter(author="[]").update(author="d3e3e3@gmail.com") + +def save_all_documents_in_history(apps, schema_editor): + State = apps.get_model("doc", "State") + Document = apps.get_model("doc", "Document") + DocHistory = apps.get_model("doc", "DocHistory") + RelatedDocument = apps.get_model("doc", "RelatedDocument") + RelatedDocHistory = apps.get_model("doc", "RelatedDocHistory") + DocumentAuthor = apps.get_model("doc", "DocumentAuthor") + DocHistoryAuthor = apps.get_model("doc", "DocHistoryAuthor") + + def canonical_name(self): + name = self.name + state = State.objects.filter(document=self, type_id=self.type_id).first() + if self.type_id == "draft" and state.slug == "rfc": + a = self.docalias_set.filter(name__startswith="rfc") + if a: + name = a[0].name + elif self.type_id == "charter": + return charter_name_for_group(self.chartered_group) + return name + + def charter_name_for_group(group): + if group.type_id == "rg": + top_org = "irtf" + else: + top_org = "ietf" + + return "charter-%s-%s" % (top_org, group.acronym) + + def save_document_in_history(doc): + """Save a snapshot of document and related objects in the database.""" + def get_model_fields_as_dict(obj): + return dict((field.name, getattr(obj, field.name)) + for field in obj._meta.fields + if field is not obj._meta.pk) + + # copy fields + fields = get_model_fields_as_dict(doc) + fields["doc"] = doc + fields["name"] = canonical_name(doc) + + dochist = DocHistory(**fields) + dochist.save() + + # copy many to many + for field in doc._meta.many_to_many: + if field.rel.through and field.rel.through._meta.auto_created: + setattr(dochist, field.name, getattr(doc, field.name).all()) + + # copy remaining tricky many to many + def transfer_fields(obj, HistModel): + mfields = get_model_fields_as_dict(item) + # map doc -> dochist + for k, v in mfields.iteritems(): + if v == doc: + mfields[k] = dochist + HistModel.objects.create(**mfields) + + for item in RelatedDocument.objects.filter(source=doc): + transfer_fields(item, RelatedDocHistory) + + for item in DocumentAuthor.objects.filter(document=doc): + transfer_fields(item, DocHistoryAuthor) + + return dochist + + from django.conf import settings + settings.DEBUG = False # prevent out-of-memory problems + + for d in Document.objects.iterator(): + save_document_in_history(d) + +class Migration(migrations.Migration): + + dependencies = [ + ('doc', '0005_auto_20150721_0230'), + ] + + operations = [ + migrations.RunPython(fix_buggy_author_foreignkey), + migrations.RunPython(save_all_documents_in_history) + ] diff --git a/ietf/doc/models.py b/ietf/doc/models.py index 70bbe2142..5083c8fa8 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -18,7 +18,6 @@ from ietf.name.models import ( DocTypeName, DocTagName, StreamName, IntendedStdL from ietf.person.models import Email, Person from ietf.utils.admin import admin_link - class StateType(models.Model): slug = models.CharField(primary_key=True, max_length=30) # draft, draft-iesg, charter, ... label = models.CharField(max_length=255, help_text="Label that should be used (e.g. in admin) for state drop-down for this type of state") # State, IESG state, WG state, ... @@ -433,6 +432,27 @@ class Document(DocumentInfo): name = name.upper() return name + def save_with_history(self, events): + """Save document and put a snapshot in the history models where they + can be retrieved later. You must pass in at least one event + with a description of what happened.""" + + assert events, "You must always add at least one event to describe the changes in the history log" + self.time = max(self.time, events[0].time) + + self._has_an_event_so_saving_is_allowed = True + self.save() + del self._has_an_event_so_saving_is_allowed + + from ietf.doc.utils import save_document_in_history + save_document_in_history(self) + + def save(self, *args, **kwargs): + # if there's no primary key yet, we can allow the save to go + # through to break the cycle between the document and any + # events + assert kwargs.get("force_insert", False) or getattr(self, "_has_an_event_so_saving_is_allowed", None), "Use .save_with_history to save documents" + super(Document, self).save(*args, **kwargs) def telechat_date(self, e=None): if not e: @@ -570,50 +590,6 @@ class DocHistory(DocumentInfo): verbose_name = "document history" verbose_name_plural = "document histories" -def save_document_in_history(doc): - """This should be called before saving changes to a Document instance, - so that the DocHistory entries contain all previous states, while - the Group entry contain the current state. XXX TODO: Call this - directly from Document.save(), and add event listeners for save() - on related objects so we can save as needed when they change, too. - """ - def get_model_fields_as_dict(obj): - return dict((field.name, getattr(obj, field.name)) - for field in obj._meta.fields - if field is not obj._meta.pk) - - # copy fields - fields = get_model_fields_as_dict(doc) - fields["doc"] = doc - fields["name"] = doc.canonical_name() - - dochist = DocHistory(**fields) - dochist.save() - - # copy many to many - for field in doc._meta.many_to_many: - if field.rel.through and field.rel.through._meta.auto_created: - setattr(dochist, field.name, getattr(doc, field.name).all()) - - # copy remaining tricky many to many - def transfer_fields(obj, HistModel): - mfields = get_model_fields_as_dict(item) - # map doc -> dochist - for k, v in mfields.iteritems(): - if v == doc: - mfields[k] = dochist - HistModel.objects.create(**mfields) - - for item in RelatedDocument.objects.filter(source=doc): - transfer_fields(item, RelatedDocHistory) - - for item in DocumentAuthor.objects.filter(document=doc): - transfer_fields(item, DocHistoryAuthor) - - 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 @@ -695,7 +671,8 @@ EVENT_TYPES = [ # RFC Editor ("rfc_editor_received_announcement", "Announcement was received by RFC Editor"), - ("requested_publication", "Publication at RFC Editor requested") + ("requested_publication", "Publication at RFC Editor requested"), + ("sync_from_rfc_editor", "Received updated information from RFC Editor"), ] class DocEvent(models.Model): diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index 53b023bf9..2df6ce63c 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -17,8 +17,7 @@ from django.conf import settings import debug # pyflakes:ignore from ietf.doc.models import ( Document, DocAlias, DocRelationshipName, RelatedDocument, State, - DocEvent, BallotPositionDocEvent, LastCallDocEvent, WriteupDocEvent, NewRevisionDocEvent, - save_document_in_history ) + DocEvent, BallotPositionDocEvent, LastCallDocEvent, WriteupDocEvent, NewRevisionDocEvent ) from ietf.group.models import Group from ietf.meeting.models import Meeting, Session, SessionPresentation from ietf.name.models import SessionStatusName @@ -432,9 +431,8 @@ 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() + draft.save_with_history([DocEvent.objects.create(doc=draft, type="published_rfc", by=Person.objects.get(name="(System)"))]) - DocEvent.objects.create(doc=draft, type="published_rfc", by=Person.objects.get(name="(System)")) rfc_alias = DocAlias.objects.create(name="rfc123456", document=draft) bcp_alias = DocAlias.objects.create(name="bcp123456", document=draft) @@ -480,9 +478,10 @@ Man Expires September 22, 2015 [Page 3] ]: doc = Document.objects.get(name=docname) # give it some history - save_document_in_history(doc) + doc.save_with_history([DocEvent(doc=doc)]) + doc.rev="01" - doc.save() + doc.save_with_history([DocEvent(doc=doc)]) r = self.client.get(urlreverse("doc_view", kwargs=dict(name=doc.name))) self.assertEqual(r.status_code, 200) @@ -539,7 +538,8 @@ class DocTestCase(TestCase): doc = make_test_data() ballot = doc.active_ballot() - save_document_in_history(doc) + # make sure we have some history + doc.save_with_history([DocEvent.objects.create(doc=doc, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) pos = BallotPositionDocEvent.objects.create( doc=doc, @@ -567,9 +567,8 @@ class DocTestCase(TestCase): # Now simulate a new revision and make sure positions on older revisions are marked as such oldrev = doc.rev e = NewRevisionDocEvent.objects.create(doc=doc,rev='%02d'%(int(doc.rev)+1),type='new_revision',by=Person.objects.get(name="(System)")) - save_document_in_history(doc) doc.rev = e.rev - doc.save() + doc.save_with_history([e]) r = self.client.get(urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name))) self.assertEqual(r.status_code, 200) self.assertTrue( '(%s for -%s)' % (pos.comment_time.strftime('%Y-%m-%d'), oldrev) in r.content) diff --git a/ietf/doc/tests_ballot.py b/ietf/doc/tests_ballot.py index 5ba9fa867..75551e76b 100644 --- a/ietf/doc/tests_ballot.py +++ b/ietf/doc/tests_ballot.py @@ -125,7 +125,7 @@ class EditPositionTests(TestCase): def test_send_ballot_comment(self): draft = make_test_data() draft.notify = "somebody@example.com" - draft.save() + draft.save_with_history([DocEvent.objects.create(doc=draft, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) ad = Person.objects.get(name="Aread Irector") @@ -355,8 +355,8 @@ class BallotWriteupsTests(TestCase): # test regenerate when it's a conflict review draft.group = Group.objects.get(type="individ") draft.stream_id = "irtf" - draft.save() draft.set_state(State.objects.get(used=True, type="draft-iesg", slug="iesg-eva")) + draft.save_with_history([DocEvent.objects.create(doc=draft, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) r = self.client.post(url, dict(regenerate_approval_text="1")) self.assertEqual(r.status_code, 200) @@ -492,8 +492,8 @@ class DeferUndeferTestCase(TestCase): self.assertEqual(doc.get_state(defer_states[doc.type_id][0]).slug,defer_states[doc.type_id][1]) self.assertTrue(doc.active_defer_event()) self.assertEqual(len(outbox), mailbox_before + 3) - self.assertTrue("State Update" in outbox[-3]['Subject']) - self.assertTrue("Telechat update" in outbox[-2]['Subject']) + self.assertTrue("Telechat update" in outbox[-3]['Subject']) + self.assertTrue("State Update" in outbox[-2]['Subject']) self.assertTrue("Deferred" in outbox[-1]['Subject']) self.assertTrue(doc.file_tag() in outbox[-1]['Subject']) @@ -526,7 +526,6 @@ class DeferUndeferTestCase(TestCase): defer_states = dict(draft=['draft-iesg','defer'],conflrev=['conflrev','defer'],statchg=['statchg','defer']) if doc.type_id in defer_states: doc.set_state(State.objects.get(used=True, type=defer_states[doc.type_id][0],slug=defer_states[doc.type_id][1])) - doc.save() # get r = self.client.get(url) diff --git a/ietf/doc/tests_charter.py b/ietf/doc/tests_charter.py index ac99b7552..912d788db 100644 --- a/ietf/doc/tests_charter.py +++ b/ietf/doc/tests_charter.py @@ -27,6 +27,10 @@ class EditCharterTests(TestCase): def tearDown(self): shutil.rmtree(self.charter_dir) + def write_charter_file(self, charter): + with open(os.path.join(self.charter_dir, "%s-%s.txt" % (charter.canonical_name(), charter.rev)), "w") as f: + f.write("This is a charter.") + def test_startstop_process(self): make_test_data() @@ -43,6 +47,8 @@ class EditCharterTests(TestCase): self.assertEqual(r.status_code, 200) # post + self.write_charter_file(charter) + r = self.client.post(url, dict(message="test message")) self.assertEqual(r.status_code, 302) if option == "abandon": @@ -349,8 +355,7 @@ class EditCharterTests(TestCase): url = urlreverse('charter_approve', kwargs=dict(name=charter.name)) login_testing_unauthorized(self, "secretary", url) - with open(os.path.join(self.charter_dir, "%s-%s.txt" % (charter.canonical_name(), charter.rev)), "w") as f: - f.write("This is a charter.") + self.write_charter_file(charter) p = Person.objects.get(name="Aread Irector") diff --git a/ietf/doc/tests_conflict_review.py b/ietf/doc/tests_conflict_review.py index 6de5ac13f..5d6847832 100644 --- a/ietf/doc/tests_conflict_review.py +++ b/ietf/doc/tests_conflict_review.py @@ -33,7 +33,7 @@ class ConflictReviewTests(TestCase): self.assertEqual(r.status_code, 404) doc.stream=StreamName.objects.get(slug='ise') - doc.save() + doc.save_with_history([DocEvent.objects.create(doc=doc, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) # normal get should succeed and get a reasonable form r = self.client.get(url) @@ -91,13 +91,13 @@ class ConflictReviewTests(TestCase): # can't start conflict reviews on documents in some other stream doc.stream=StreamName.objects.get(slug='irtf') - doc.save() + doc.save_with_history([DocEvent.objects.create(doc=doc, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) r = self.client.get(url) self.assertEquals(r.status_code, 404) # successful get doc.stream=StreamName.objects.get(slug='ise') - doc.save() + doc.save_with_history([DocEvent.objects.create(doc=doc, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) r = self.client.get(url) self.assertEquals(r.status_code, 200) q = PyQuery(r.content) @@ -268,7 +268,6 @@ class ConflictReviewTests(TestCase): # Some additional setup create_ballot_if_not_open(doc,Person.objects.get(name="Sec Retary"),"conflrev") doc.set_state(State.objects.get(used=True, slug=approve_type+'-pend',type='conflrev')) - doc.save() # get r = self.client.get(url) diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py index 44a9b0b61..987c9eefb 100644 --- a/ietf/doc/tests_draft.py +++ b/ietf/doc/tests_draft.py @@ -353,16 +353,16 @@ class EditInfoTests(TestCase): self.assertEqual(draft.ad, ad) self.assertEqual(draft.note, "This is a note") self.assertTrue(not draft.latest_event(TelechatDocEvent, type="scheduled_for_telechat")) - self.assertEqual(draft.docevent_set.count(), events_before + 3) + self.assertEqual(draft.docevent_set.count(), events_before + 4) events = list(draft.docevent_set.order_by('time', 'id')) - self.assertEqual(events[-3].type, "started_iesg_process") + self.assertEqual(events[-4].type, "started_iesg_process") self.assertEqual(len(outbox), mailbox_before) # Redo, starting in publication requested to make sure WG state is also set draft.unset_state('draft-iesg') draft.set_state(State.objects.get(type='draft-stream-ietf',slug='writeupw')) draft.stream = StreamName.objects.get(slug='ietf') - draft.save() + draft.save_with_history([DocEvent.objects.create(doc=draft, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) r = self.client.post(url, dict(intended_std_level=str(draft.intended_std_level_id), ad=ad.pk, @@ -514,7 +514,7 @@ class ExpireIDsTests(TestCase): # hack into expirable state draft.unset_state("draft-iesg") draft.expires = datetime.datetime.now() + datetime.timedelta(days=10) - draft.save() + draft.save_with_history([DocEvent.objects.create(doc=draft, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) self.assertEqual(len(list(get_soon_to_expire_drafts(14))), 1) @@ -537,7 +537,7 @@ class ExpireIDsTests(TestCase): # hack into expirable state draft.unset_state("draft-iesg") draft.expires = datetime.datetime.now() - draft.save() + draft.save_with_history([DocEvent.objects.create(doc=draft, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) self.assertEqual(len(list(get_expired_drafts())), 1) @@ -597,7 +597,6 @@ class ExpireIDsTests(TestCase): # RFC draft draft.set_state(State.objects.get(used=True, type="draft", slug="rfc")) - draft.save() txt = "%s-%s.txt" % (draft.name, draft.rev) self.write_draft_file(txt, 5000) @@ -615,7 +614,7 @@ class ExpireIDsTests(TestCase): # expire draft draft.set_state(State.objects.get(used=True, type="draft", slug="expired")) draft.expires = datetime.datetime.now() - datetime.timedelta(days=1) - draft.save() + draft.save_with_history([DocEvent.objects.create(doc=draft, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) e = DocEvent() e.doc = draft @@ -822,7 +821,7 @@ class IndividualInfoFormsTests(TestCase): def test_doc_change_shepherd(self): self.doc.shepherd = None - self.doc.save() + self.doc.save_with_history([DocEvent.objects.create(doc=self.doc, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) url = urlreverse('doc_edit_shepherd',kwargs=dict(name=self.docname)) @@ -874,19 +873,19 @@ class IndividualInfoFormsTests(TestCase): def test_doc_change_shepherd_email(self): self.doc.shepherd = None - self.doc.save() + self.doc.save_with_history([DocEvent.objects.create(doc=self.doc, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) url = urlreverse('doc_change_shepherd_email',kwargs=dict(name=self.docname)) r = self.client.get(url) self.assertEqual(r.status_code, 404) self.doc.shepherd = Email.objects.get(person__user__username="ad1") - self.doc.save() + self.doc.save_with_history([DocEvent.objects.create(doc=self.doc, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) login_testing_unauthorized(self, "plain", url) self.doc.shepherd = Email.objects.get(person__user__username="plain") - self.doc.save() + self.doc.save_with_history([DocEvent.objects.create(doc=self.doc, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) new_email = Email.objects.create(address="anotheremail@example.com", person=self.doc.shepherd.person) @@ -921,7 +920,7 @@ class IndividualInfoFormsTests(TestCase): # Try again when no longer a shepherd. self.doc.shepherd = None - self.doc.save() + self.doc.save_with_history([DocEvent.objects.create(doc=self.doc, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) r = self.client.get(url) self.assertEqual(r.status_code,200) q = PyQuery(r.content) @@ -1028,7 +1027,7 @@ class RequestPublicationTests(TestCase): draft.stream = StreamName.objects.get(slug="iab") draft.group = Group.objects.get(acronym="iab") draft.intended_std_level = IntendedStdLevelName.objects.get(slug="inf") - draft.save() + draft.save_with_history([DocEvent.objects.create(doc=draft, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) draft.set_state(State.objects.get(used=True, type="draft-stream-iab", slug="approved")) url = urlreverse('doc_request_publication', kwargs=dict(name=draft.name)) @@ -1064,8 +1063,8 @@ class AdoptDraftTests(TestCase): draft = make_test_data() draft.stream = None draft.group = Group.objects.get(type="individ") - draft.save() draft.unset_state("draft-stream-ietf") + draft.save_with_history([DocEvent.objects.create(doc=draft, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) url = urlreverse('doc_adopt_draft', kwargs=dict(name=draft.name)) login_testing_unauthorized(self, "marschairman", url) diff --git a/ietf/doc/tests_status_change.py b/ietf/doc/tests_status_change.py index b22cd9f31..792a83b70 100644 --- a/ietf/doc/tests_status_change.py +++ b/ietf/doc/tests_status_change.py @@ -104,7 +104,7 @@ class StatusChangeTests(TestCase): # successful change to Last Call Requested messages_before = len(outbox) doc.ad = Person.objects.get(user__username='ad') - doc.save() + doc.save_with_history([DocEvent.objects.create(doc=doc, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) lc_req_pk = str(State.objects.get(slug='lc-req',type__slug='statchg').pk) r = self.client.post(url,dict(new_state=lc_req_pk)) self.assertEquals(r.status_code, 200) @@ -255,7 +255,7 @@ class StatusChangeTests(TestCase): doc.relateddocument_set.create(target=DocAlias.objects.get(name='rfc9999'),relationship_id='tois') doc.relateddocument_set.create(target=DocAlias.objects.get(name='rfc9998'),relationship_id='tohist') doc.ad = Person.objects.get(name='Ad No2') - doc.save() + doc.save_with_history([DocEvent.objects.create(doc=doc, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) # get r = self.client.get(url) @@ -299,9 +299,8 @@ class StatusChangeTests(TestCase): # Some additional setup doc.relateddocument_set.create(target=DocAlias.objects.get(name='rfc9999'),relationship_id='tois') doc.relateddocument_set.create(target=DocAlias.objects.get(name='rfc9998'),relationship_id='tohist') - create_ballot_if_not_open(doc,Person.objects.get(name="Sec Retary"),"statchg") + create_ballot_if_not_open(doc,Person.objects.get(user__username="secretary"),"statchg") doc.set_state(State.objects.get(slug='appr-pend',type='statchg')) - doc.save() # get r = self.client.get(url) diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index bd0ef5c96..37ee196bf 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -8,12 +8,13 @@ from django.conf import settings from django.db.models import Q from django.db.models.query import EmptyQuerySet from django.forms import ValidationError -from django.utils.html import strip_tags, escape +from django.utils.html import escape -from ietf.doc.models import Document, DocHistory, State -from ietf.doc.models import DocAlias, RelatedDocument, BallotType, DocReminder +from ietf.doc.models import Document, DocHistory, State, DocumentAuthor, DocHistoryAuthor +from ietf.doc.models import DocAlias, RelatedDocument, RelatedDocHistory, BallotType, DocReminder from ietf.doc.models import DocEvent, BallotDocEvent, NewRevisionDocEvent, StateDocEvent -from ietf.doc.models import save_document_in_history, STATUSCHANGE_RELATIONS +from ietf.doc.models import TelechatDocEvent +from ietf.doc.models import STATUSCHANGE_RELATIONS from ietf.name.models import DocReminderTypeName, DocRelationshipName from ietf.group.models import Role from ietf.person.models import Email @@ -21,21 +22,43 @@ from ietf.ietfauth.utils import has_role from ietf.utils import draft, markup_txt from ietf.utils.mail import send_mail -#FIXME - it would be better if this lived in ietf/doc/mails.py, but there's -# an import order issue to work out. -def email_update_telechat(request, doc, text): - to = set(['iesg@ietf.org','iesg-secretary@ietf.org']) - to.update(set([x.strip() for x in doc.notify.replace(';', ',').split(',')])) +def save_document_in_history(doc): + """Save a snapshot of document and related objects in the database.""" + def get_model_fields_as_dict(obj): + return dict((field.name, getattr(obj, field.name)) + for field in obj._meta.fields + if field is not obj._meta.pk) - if not to: - return + # copy fields + fields = get_model_fields_as_dict(doc) + fields["doc"] = doc + fields["name"] = doc.canonical_name() - text = strip_tags(text) - send_mail(request, list(to), None, - "Telechat update notice: %s" % doc.file_tag(), - "doc/mail/update_telechat.txt", - dict(text=text, - url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url())) + dochist = DocHistory(**fields) + dochist.save() + + # copy many to many + for field in doc._meta.many_to_many: + if field.rel.through and field.rel.through._meta.auto_created: + setattr(dochist, field.name, getattr(doc, field.name).all()) + + # copy remaining tricky many to many + def transfer_fields(obj, HistModel): + mfields = get_model_fields_as_dict(item) + # map doc -> dochist + for k, v in mfields.iteritems(): + if v == doc: + mfields[k] = dochist + HistModel.objects.create(**mfields) + + for item in RelatedDocument.objects.filter(source=doc): + transfer_fields(item, RelatedDocHistory) + + for item in DocumentAuthor.objects.filter(document=doc): + transfer_fields(item, DocHistoryAuthor) + + return dochist + def get_state_types(doc): res = [] @@ -344,7 +367,6 @@ def make_notify_changed_event(request, doc, by, new_notify, time=None): # events to match if doc.type.slug=='charter': event_type = 'changed_document' - save_document_in_history(doc) else: event_type = 'added_comment' @@ -359,8 +381,6 @@ def make_notify_changed_event(request, doc, by, new_notify, time=None): return e def update_telechat(request, doc, by, new_telechat_date, new_returning_item=None): - from ietf.doc.models import TelechatDocEvent - on_agenda = bool(new_telechat_date) prev = doc.latest_event(TelechatDocEvent, type="scheduled_for_telechat") @@ -411,8 +431,12 @@ def update_telechat(request, doc, by, new_telechat_date, new_returning_item=None e.desc = "Removed telechat returning item indication" e.save() + + from ietf.doc.mails import email_update_telechat email_update_telechat(request, doc, e.desc) + return e + def rebuild_reference_relations(doc,filename=None): if doc.type.slug != 'draft': return None @@ -477,13 +501,26 @@ def collect_email_addresses(emails, doc): if doc.shepherd and doc.shepherd.address not in emails: emails[doc.shepherd.address] = u'"%s"' % (doc.shepherd.person.name or "") -def set_replaces_for_document(request, doc, new_replaces, by, email_subject, email_comment=""): +def set_replaces_for_document(request, doc, new_replaces, by, email_subject, comment=""): emails = {} collect_email_addresses(emails, doc) relationship = DocRelationshipName.objects.get(slug='replaces') old_replaces = doc.related_that_doc("replaces") + events = [] + + e = DocEvent(doc=doc, by=by, type='changed_document') + new_replaces_names = u", ".join(d.name for d in new_replaces) or u"None" + old_replaces_names = u", ".join(d.name for d in old_replaces) or u"None" + e.desc = u"This document now replaces %s instead of %s" % (new_replaces_names, old_replaces_names) + e.save() + + events.append(e) + + if comment: + events.append(DocEvent.objects.create(doc=doc, by=by, type="added_comment", desc=comment)) + for d in old_replaces: if d not in new_replaces: collect_email_addresses(emails, d.document) @@ -498,19 +535,13 @@ def set_replaces_for_document(request, doc, new_replaces, by, email_subject, ema RelatedDocument.objects.create(source=doc, target=d, relationship=relationship) d.document.set_state(State.objects.get(type='draft', slug='repl')) - e = DocEvent(doc=doc, by=by, type='changed_document') - new_replaces_names = u", ".join(d.name for d in new_replaces) or u"None" - old_replaces_names = u", ".join(d.name for d in old_replaces) or u"None" - e.desc = u"This document now replaces %s instead of %s" % (new_replaces_names, old_replaces_names) - e.save() - # make sure there are no lingering suggestions duplicating new replacements RelatedDocument.objects.filter(source=doc, target__in=new_replaces, relationship="possibly-replaces").delete() email_desc = e.desc.replace(", ", "\n ") - if email_comment: - email_desc += "\n" + email_comment + if comment: + email_desc += "\n" + comment to = [ u'%s <%s>' % (emails[email], email) if emails[email] else u'<%s>' % email @@ -527,6 +558,8 @@ def set_replaces_for_document(request, doc, new_replaces, by, email_subject, ema doc=doc, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url())) + return events + def check_common_doc_name_rules(name): """Check common rules for document names for use in forms, throws ValidationError in case there's a problem.""" diff --git a/ietf/doc/utils_charter.py b/ietf/doc/utils_charter.py index b3812b11c..02cea0872 100644 --- a/ietf/doc/utils_charter.py +++ b/ietf/doc/utils_charter.py @@ -1,4 +1,4 @@ -import re, datetime, os +import re, datetime, os, shutil from django.template.loader import render_to_string from django.utils.html import strip_tags @@ -6,8 +6,12 @@ from django.conf import settings from ietf.doc.models import NewRevisionDocEvent, WriteupDocEvent, BallotPositionDocEvent from ietf.person.models import Person +from ietf.group.models import ChangeStateGroupEvent +from ietf.name.models import GroupStateName from ietf.utils.history import find_history_active_at from ietf.utils.mail import send_mail_text +from ietf.utils.log import log +from ietf.group.utils import save_group_in_history def charter_name_for_group(group): if group.type_id == "rg": @@ -46,6 +50,45 @@ def read_charter_text(doc): except IOError: return "Error: couldn't read charter text" +def change_group_state_after_charter_approval(group, by): + new_state = GroupStateName.objects.get(slug="active") + if group.state == new_state: + return None + + save_group_in_history(group) + group.state = new_state + group.time = datetime.datetime.now() + group.save() + + # create an event for the group state change, too + e = ChangeStateGroupEvent(group=group, type="changed_state") + e.time = group.time + e.by = by + e.state_id = "active" + e.desc = "Charter approved, group active" + e.save() + + return e + +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))) + shutil.copy(old, new) + except IOError: + log("There was an error copying %s to %s" % (old, new)) + + 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.save() + events.append(e) + + charter.rev = e.rev + charter.save_with_history(events) + def historic_milestones_for_charter(charter, rev): """Return GroupMilestone/GroupMilestoneHistory objects for charter document at rev by looking through the history.""" diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py index 79f73cca5..e614c51fe 100644 --- a/ietf/doc/views_ballot.py +++ b/ietf/doc/views_ballot.py @@ -4,7 +4,7 @@ import datetime, json from django.http import HttpResponseForbidden, HttpResponseRedirect, Http404 -from django.shortcuts import render_to_response, get_object_or_404, redirect +from django.shortcuts import render, render_to_response, get_object_or_404, redirect from django.core.urlresolvers import reverse as urlreverse from django.template.loader import render_to_string from django.template import RequestContext @@ -14,7 +14,7 @@ from django.conf import settings import debug # pyflakes:ignore from ietf.doc.models import ( Document, State, DocEvent, BallotDocEvent, BallotPositionDocEvent, - BallotType, LastCallDocEvent, WriteupDocEvent, save_document_in_history, IESG_SUBSTATE_TAGS ) + BallotType, LastCallDocEvent, WriteupDocEvent, IESG_SUBSTATE_TAGS ) from ietf.doc.utils import ( add_state_change_event, close_ballot, close_open_ballots, create_ballot_if_not_open, update_telechat ) from ietf.doc.mails import ( email_ad, email_ballot_deferred, email_ballot_undeferred, @@ -44,12 +44,12 @@ def do_undefer_ballot(request, doc): Helper function to perform undefer of ballot. Takes the Request object, for use in logging, and the Document object. ''' - login = request.user.person + by = request.user.person telechat_date = TelechatDate.objects.active().order_by("date")[0].date - save_document_in_history(doc) new_state = doc.get_state() - prev_tags = new_tags = [] + prev_tags = [] + new_tags = [] if doc.type_id == 'draft': new_state = State.objects.get(used=True, type="draft-iesg", slug='iesg-eva') @@ -62,15 +62,21 @@ def do_undefer_ballot(request, doc): doc.set_state(new_state) doc.tags.remove(*prev_tags) - e = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags) - - doc.time = (e and e.time) or datetime.datetime.now() - doc.save() + events = [] + state_change_event = add_state_change_event(doc, by, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags) + if state_change_event: + events.append(state_change_event) - update_telechat(request, doc, login, telechat_date) + e = update_telechat(request, doc, by, telechat_date) if e: - email_state_changed(request, doc, e.desc) - email_ballot_undeferred(request, doc, login.plain_name(), telechat_date) + events.append(e) + + if events: + doc.save_with_history(events) + + if state_change_event: + email_state_changed(request, doc, state_change_event.desc) + email_ballot_undeferred(request, doc, by.plain_name(), telechat_date) def position_to_ballot_choice(position): for v, label in BALLOT_CHOICES: @@ -342,10 +348,9 @@ def defer_ballot(request, name): telechat_date = TelechatDate.objects.active().order_by("date")[1].date if request.method == 'POST': - save_document_in_history(doc) - new_state = doc.get_state() - prev_tags = new_tags = [] + prev_tags = [] + new_tags = [] if doc.type_id == 'draft': new_state = State.objects.get(used=True, type="draft-iesg", slug='defer') @@ -358,15 +363,20 @@ def defer_ballot(request, name): doc.set_state(new_state) doc.tags.remove(*prev_tags) - e = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags) - - doc.time = (e and e.time) or datetime.datetime.now() - doc.save() + events = [] + state_change_event = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags) + if state_change_event: + events.append(state_change_event) + + e = update_telechat(request, doc, login, telechat_date) if e: - email_state_changed(request, doc, e.desc) + events.append(e) - update_telechat(request, doc, login, telechat_date) + doc.save_with_history(events) + + if state_change_event: + email_state_changed(request, doc, e.desc) email_ballot_deferred(request, doc, login.plain_name(), telechat_date) return HttpResponseRedirect(doc.get_absolute_url()) @@ -443,8 +453,6 @@ def lastcalltext(request, name): e.save() if "send_last_call_request" in request.POST: - save_document_in_history(doc) - prev_state = doc.get_state("draft-iesg") new_state = State.objects.get(used=True, type="draft-iesg", slug='lc-req') @@ -455,10 +463,9 @@ def lastcalltext(request, name): e = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=[]) - doc.time = (e and e.time) or datetime.datetime.now() - doc.save() - if e: + doc.save_with_history([e]) + email_state_changed(request, doc, e.desc) email_ad(request, doc, doc.ad, login, e.desc) @@ -666,19 +673,17 @@ def approve_ballot(request, name): prev_state = doc.get_state("draft-iesg") prev_tags = doc.tags.filter(slug__in=IESG_SUBSTATE_TAGS) + events = [] if new_state.slug == "ann" and new_state.slug != prev_state.slug and not request.REQUEST.get("skiprfceditorpost"): # start by notifying the RFC Editor import ietf.sync.rfceditor response, error = ietf.sync.rfceditor.post_approved_draft(settings.RFC_EDITOR_SYNC_NOTIFICATION_URL, doc.name) if error: - return render_to_response('doc/draft/rfceditor_post_approved_draft_failed.html', - dict(name=doc.name, - response=response, - error=error), - context_instance=RequestContext(request)) - - save_document_in_history(doc) + return render(request, 'doc/draft/rfceditor_post_approved_draft_failed.html', + dict(name=doc.name, + response=response, + error=error)) doc.set_state(new_state) doc.tags.remove(*prev_tags) @@ -693,15 +698,17 @@ def approve_ballot(request, name): else: e.type = "iesg_approved" e.desc = "IESG has approved the document" - e.save() + events.append(e) change_description = e.desc + " and state has been changed to %s" % doc.get_state("draft-iesg").name e = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=[]) - doc.time = (e and e.time) or datetime.datetime.now() - doc.save() + if e: + events.append(e) + + doc.save_with_history(events) email_state_changed(request, doc, change_description) email_ad(request, doc, doc.ad, login, change_description) @@ -761,10 +768,10 @@ def make_last_call(request, name): msg.save() msg.related_docs.add(doc) - save_document_in_history(doc) - new_state = doc.get_state() - prev_tags = new_tags = [] + prev_tags = [] + new_tags = [] + events = [] if doc.type.slug == 'draft': new_state = State.objects.get(used=True, type="draft-iesg", slug='lc') @@ -778,10 +785,8 @@ def make_last_call(request, name): doc.tags.remove(*prev_tags) e = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags) - - doc.time = (e and e.time) or datetime.datetime.now() - doc.save() - + if e: + events.append(e) change_description = "Last call has been made for %s and state has been changed to %s" % (doc.name, new_state.name) email_state_changed(request, doc, change_description) @@ -796,6 +801,7 @@ def make_last_call(request, name): e.time = datetime.datetime.combine(form.cleaned_data['last_call_sent_date'], e.time.time()) e.expires = form.cleaned_data['last_call_expiration_date'] e.save() + events.append(e) # update IANA Review state if doc.type.slug == 'draft': @@ -803,7 +809,11 @@ def make_last_call(request, name): if not prev_state: next_state = State.objects.get(used=True, type="draft-iana-review", slug="need-rev") doc.set_state(next_state) - add_state_change_event(doc, login, prev_state, next_state) + e = add_state_change_event(doc, login, prev_state, next_state) + if e: + events.append(e) + + doc.save_with_history(events) return HttpResponseRedirect(doc.get_absolute_url()) else: diff --git a/ietf/doc/views_charter.py b/ietf/doc/views_charter.py index fe6380481..01767577d 100644 --- a/ietf/doc/views_charter.py +++ b/ietf/doc/views_charter.py @@ -1,6 +1,6 @@ -import os, datetime, shutil, textwrap, json +import os, datetime, textwrap, json -from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound, HttpResponseForbidden, Http404 +from django.http import HttpResponseRedirect, HttpResponseNotFound, HttpResponseForbidden, Http404 from django.shortcuts import render_to_response, get_object_or_404, redirect from django.core.urlresolvers import reverse as urlreverse from django.template import RequestContext @@ -14,12 +14,13 @@ import debug # pyflakes:ignore from ietf.doc.models import ( Document, DocHistory, State, DocEvent, BallotDocEvent, BallotPositionDocEvent, InitialReviewDocEvent, NewRevisionDocEvent, - WriteupDocEvent, save_document_in_history ) + WriteupDocEvent ) from ietf.doc.utils import ( add_state_change_event, close_open_ballots, create_ballot_if_not_open, get_chartering_type ) from ietf.doc.utils_charter import ( historic_milestones_for_charter, approved_revision, default_review_text, default_action_text, email_state_changed, - generate_ballot_writeup, generate_issue_ballot_mail, next_approved_revision, next_revision ) + generate_ballot_writeup, generate_issue_ballot_mail, next_revision, + change_group_state_after_charter_approval, fix_charter_revision_after_approval) from ietf.group.models import ChangeStateGroupEvent, MilestoneGroupEvent from ietf.group.utils import save_group_in_history, save_milestone_in_history, can_manage_group_type from ietf.ietfauth.utils import has_role, role_required @@ -65,7 +66,7 @@ def change_state(request, name, option=None): if charter.get_state_slug() != "infrev" or (initial_review and initial_review.expires < datetime.datetime.now()) or chartering_type == "rechartering": initial_review = None - login = request.user.person + by = request.user.person if request.method == 'POST': form = ChangeStateForm(request.POST, group=group) @@ -97,7 +98,7 @@ def change_state(request, name, option=None): group.save() e = ChangeStateGroupEvent(group=group, type="changed_state") e.time = group.time - e.by = login + e.by = by e.state_id = group.state.slug e.desc = "Group state changed to %s from %s" % (group.state, oldstate) e.save() @@ -112,32 +113,30 @@ def change_state(request, name, option=None): message = clean['message'] if charter_state != charter.get_state(): - # Charter state changed - save_document_in_history(charter) - + events = [] prev_state = charter.get_state() new_state = charter_state charter.set_state(new_state) charter.rev = charter_rev if option != "abandon": - add_state_change_event(charter, login, prev_state, new_state) + e = add_state_change_event(charter, by, prev_state, new_state) + if e: + events.append(e) else: # kill hanging ballots - close_open_ballots(charter, login) + close_open_ballots(charter, by) # Special log for abandoned efforts - e = DocEvent(type="changed_document", doc=charter, by=login) + e = DocEvent(type="changed_document", doc=charter, by=by) e.desc = "IESG has abandoned the chartering effort" e.save() + events.append(e) if comment: - c = DocEvent(type="added_comment", doc=charter, by=login) - c.desc = comment - c.save() + events.append(DocEvent.objects.create(type="added_comment", doc=charter, by=by, desc=comment)) - charter.time = datetime.datetime.now() - charter.save() + charter.save_with_history(events) if message or charter_state.slug == "intrev" or charter_state.slug == "extrev": email_iesg_secretary_re_charter(request, group, "Charter state changed to %s" % charter_state.name, message) @@ -146,19 +145,19 @@ def change_state(request, name, option=None): if charter_state.slug == "intrev" and group.type_id == "wg": if request.POST.get("ballot_wo_extern"): - create_ballot_if_not_open(charter, login, "r-wo-ext") + create_ballot_if_not_open(charter, by, "r-wo-ext") else: - create_ballot_if_not_open(charter, login, "r-extrev") - default_review_text(group, charter, login) - default_action_text(group, charter, login) + create_ballot_if_not_open(charter, by, "r-extrev") + default_review_text(group, charter, by) + default_action_text(group, charter, by) elif charter_state.slug == "iesgrev": - create_ballot_if_not_open(charter, login, "approve") + create_ballot_if_not_open(charter, by, "approve") elif charter_state.slug == "approved": - change_group_state_after_charter_approval(group, login) - fix_charter_revision_after_approval(charter, login) + change_group_state_after_charter_approval(group, by) + fix_charter_revision_after_approval(charter, by) if charter_state.slug == "infrev" and clean["initial_time"] and clean["initial_time"] != 0: - e = InitialReviewDocEvent(type="initial_review", by=login, doc=charter) + e = InitialReviewDocEvent(type="initial_review", by=by, doc=charter) e.expires = datetime.datetime.now() + datetime.timedelta(weeks=clean["initial_time"]) e.desc = "Initial review time expires %s" % e.expires.strftime("%Y-%m-%d") e.save() @@ -178,10 +177,10 @@ def change_state(request, name, option=None): init = dict() elif option == "initcharter": hide = ['charter_state'] - init = dict(initial_time=1, message='%s has initiated chartering of the proposed %s:\n "%s" (%s).' % (login.plain_name(), group.type.name, group.name, group.acronym)) + init = dict(initial_time=1, message='%s has initiated chartering of the proposed %s:\n "%s" (%s).' % (by.plain_name(), group.type.name, group.name, group.acronym)) elif option == "abandon": hide = ['initial_time', 'charter_state'] - init = dict(message='%s has abandoned the chartering effort on the %s:\n "%s" (%s).' % (login.plain_name(), group.type.name, group.name, group.acronym)) + init = dict(message='%s has abandoned the chartering effort on the %s:\n "%s" (%s).' % (by.plain_name(), group.type.name, group.name, group.acronym)) form = ChangeStateForm(hide=hide, initial=init, group=group) prev_charter_state = None @@ -202,9 +201,9 @@ def change_state(request, name, option=None): info_msg = {} if group.type_id == "wg": - info_msg[state_pk("infrev")] = 'The %s "%s" (%s) has been set to Informal IESG review by %s.' % (group.type.name, group.name, group.acronym, login.plain_name()) - info_msg[state_pk("intrev")] = 'The %s "%s" (%s) has been set to Internal review by %s.\nPlease place it on the next IESG telechat and inform the IAB.' % (group.type.name, group.name, group.acronym, login.plain_name()) - info_msg[state_pk("extrev")] = 'The %s "%s" (%s) has been set to External review by %s.\nPlease send out the external review announcement to the appropriate lists.\n\nSend the announcement to other SDOs: Yes\nAdditional recipients of the announcement: ' % (group.type.name, group.name, group.acronym, login.plain_name()) + info_msg[state_pk("infrev")] = 'The %s "%s" (%s) has been set to Informal IESG review by %s.' % (group.type.name, group.name, group.acronym, by.plain_name()) + info_msg[state_pk("intrev")] = 'The %s "%s" (%s) has been set to Internal review by %s.\nPlease place it on the next IESG telechat and inform the IAB.' % (group.type.name, group.name, group.acronym, by.plain_name()) + info_msg[state_pk("extrev")] = 'The %s "%s" (%s) has been set to External review by %s.\nPlease send out the external review announcement to the appropriate lists.\n\nSend the announcement to other SDOs: Yes\nAdditional recipients of the announcement: ' % (group.type.name, group.name, group.acronym, by.plain_name()) states_for_ballot_wo_extern = State.objects.none() if group.type_id == "wg": @@ -213,7 +212,6 @@ def change_state(request, name, option=None): return render_to_response('doc/charter/change_state.html', dict(form=form, doc=group.charter, - login=login, option=option, prev_charter_state=prev_charter_state, title=title, @@ -242,7 +240,7 @@ def change_title(request, name, option=None): group = charter.group if not can_manage_group_type(request.user, group.type_id): return HttpResponseForbidden("You don't have permission to access this view") - login = request.user.person + by = request.user.person if request.method == 'POST': form = ChangeTitleForm(request.POST, charter=charter) if form.is_valid(): @@ -253,17 +251,19 @@ def change_title(request, name, option=None): message = clean['message'] prev_title = charter.title if new_title != prev_title: - # Charter title changed - save_document_in_history(charter) - charter.title=new_title + events = [] + charter.title = new_title charter.rev = charter_rev + if not comment: comment = "Changed charter title from '%s' to '%s'." % (prev_title, new_title) - event = DocEvent(type="added_comment", doc=charter, by=login) - event.desc = comment - event.save() - charter.time = datetime.datetime.now() - charter.save() + e = DocEvent(type="added_comment", doc=charter, by=by) + e.desc = comment + e.save() + events.append(e) + + charter.save_with_history(events) + if message: email_iesg_secretary_re_charter(request, group, "Charter title changed to %s" % new_title, message) email_state_changed(request, charter, "Title changed to %s." % new_title) @@ -274,7 +274,6 @@ def change_title(request, name, option=None): return render_to_response('doc/charter/change_title.html', dict(form=form, doc=group.charter, - login=login, title=title, ), context_instance=RequestContext(request)) @@ -297,23 +296,24 @@ def edit_ad(request, name): """Change the responsible Area Director for this charter.""" charter = get_object_or_404(Document, type="charter", name=name) - login = request.user.person + by = request.user.person if request.method == 'POST': form = AdForm(request.POST) if form.is_valid(): new_ad = form.cleaned_data['ad'] if new_ad != charter.ad: - save_document_in_history(charter) - e = DocEvent(doc=charter, by=login) + events = [] + e = DocEvent(doc=charter, by=by) e.desc = "Responsible AD changed to %s" % new_ad.plain_name() if charter.ad: e.desc += " from %s" % charter.ad.plain_name() e.type = "changed_document" e.save() + events.append(e) + charter.ad = new_ad - charter.time = e.time - charter.save() + charter.save_with_history(events) return redirect('doc_view', name=charter.name) else: @@ -372,16 +372,17 @@ def submit(request, name=None, option=None): if request.method == 'POST': form = UploadForm(request.POST, request.FILES) if form.is_valid(): - save_document_in_history(charter) # Also save group history so we can search for it save_group_in_history(group) 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.rev = charter.rev e.save() + events.append(e) # Save file on disk form.save(group, charter.rev) @@ -389,8 +390,7 @@ def submit(request, name=None, option=None): if option in ['initcharter','recharter'] and charter.ad == None: charter.ad = getattr(group.ad_role(),'person',None) - charter.time = datetime.datetime.now() - charter.save() + charter.save_with_history(events) if option: return redirect('charter_startstop_process', name=charter.name, option=option) @@ -434,15 +434,15 @@ def announcement_text(request, name, ann): charter = get_object_or_404(Document, type="charter", name=name) group = charter.group - login = request.user.person + by = request.user.person if ann in ("action", "review"): existing = charter.latest_event(WriteupDocEvent, type="changed_%s_announcement" % ann) if not existing: if ann == "action": - existing = default_action_text(group, charter, login) + existing = default_action_text(group, charter, by) elif ann == "review": - existing = default_review_text(group, charter, login) + existing = default_review_text(group, charter, by) if not existing: raise Http404 @@ -454,15 +454,14 @@ def announcement_text(request, name, ann): if "save_text" in request.POST and form.is_valid(): t = form.cleaned_data['announcement_text'] if t != existing.text: - e = WriteupDocEvent(doc=charter, by=login) - e.by = login + e = WriteupDocEvent(doc=charter, by=by) + e.by = by e.type = "changed_%s_announcement" % ann e.desc = "%s %s text was changed" % (group.type.name, ann) e.text = t e.save() - charter.time = e.time - charter.save() + charter.save_with_history([e]) if request.GET.get("next", "") == "approve": return redirect('charter_approve', name=charter.canonical_name()) @@ -471,9 +470,9 @@ def announcement_text(request, name, ann): if "regenerate_text" in request.POST: if ann == "action": - e = default_action_text(group, charter, login) + e = default_action_text(group, charter, by) elif ann == "review": - e = default_review_text(group, charter, login) + e = default_review_text(group, charter, by) # make sure form has the updated text form = AnnouncementTextForm(initial=dict(announcement_text=e.text)) @@ -505,7 +504,7 @@ def ballot_writeupnotes(request, name): if not ballot: raise Http404 - login = request.user.person + by = request.user.person approval = charter.latest_event(WriteupDocEvent, type="changed_action_announcement") @@ -522,8 +521,7 @@ def ballot_writeupnotes(request, name): if form.is_valid(): t = form.cleaned_data["ballot_writeup"] if t != existing.text: - e = WriteupDocEvent(doc=charter, by=login) - e.by = login + e = WriteupDocEvent(doc=charter, by=by) e.type = "changed_ballot_writeup_text" e.desc = "Ballot writeup was changed" e.text = t @@ -532,11 +530,11 @@ def ballot_writeupnotes(request, name): existing = e if "send_ballot" in request.POST and approval: - if has_role(request.user, "Area Director") and not charter.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=login, ballot=ballot): + if has_role(request.user, "Area Director") and not charter.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=by, ballot=ballot): # sending the ballot counts as a yes - pos = BallotPositionDocEvent(doc=charter, by=login) + pos = BallotPositionDocEvent(doc=charter, by=by) pos.type = "changed_ballot_position" - pos.ad = login + pos.ad = by pos.pos_id = "yes" pos.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.ad.plain_name()) pos.save() @@ -544,8 +542,8 @@ def ballot_writeupnotes(request, name): msg = generate_issue_ballot_mail(request, charter, ballot) send_mail_preformatted(request, msg) - e = DocEvent(doc=charter, by=login) - e.by = login + e = DocEvent(doc=charter, by=by) + e.by = by e.type = "sent_ballot_announcement" e.desc = "Ballot has been sent" e.save() @@ -564,57 +562,17 @@ def ballot_writeupnotes(request, name): ), context_instance=RequestContext(request)) -def change_group_state_after_charter_approval(group, by): - new_state = GroupStateName.objects.get(slug="active") - if group.state == new_state: - return None - - save_group_in_history(group) - group.state = new_state - group.time = datetime.datetime.now() - group.save() - - # create an event for the group state change, too - e = ChangeStateGroupEvent(group=group, type="changed_state") - e.time = group.time - e.by = by - e.state_id = "active" - e.desc = "Charter approved, group active" - e.save() - - return e - -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))) - shutil.copy(old, new) - except IOError: - return HttpResponse("There was an error copying %s to %s" % - ('%s-%s.txt' % (charter.canonical_name(), charter.rev), - '%s-%s.txt' % (charter.canonical_name(), next_approved_revision(charter.rev)))) - - 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.save() - - charter.rev = e.rev - charter.time = e.time - charter.save() - @role_required("Secretariat") def approve(request, name): """Approve charter, changing state, fixing revision, copying file to final location.""" charter = get_object_or_404(Document, type="charter", name=name) group = charter.group - login = request.user.person + by = request.user.person e = charter.latest_event(WriteupDocEvent, type="changed_action_announcement") if not e: - announcement = default_action_text(group, charter, login).text + announcement = default_action_text(group, charter, by).text else: announcement = e.text @@ -622,26 +580,31 @@ def approve(request, name): new_charter_state = State.objects.get(used=True, type="charter", slug="approved") prev_charter_state = charter.get_state() - save_document_in_history(charter) charter.set_state(new_charter_state) - close_open_ballots(charter, login) + close_open_ballots(charter, by) + events = [] # approve - e = DocEvent(doc=charter, by=login) + e = DocEvent(doc=charter, by=by) e.type = "iesg_approved" e.desc = "IESG has approved the charter" e.save() + events.append(e) change_description = e.desc - group_state_change_event = change_group_state_after_charter_approval(group, login) + group_state_change_event = change_group_state_after_charter_approval(group, by) if group_state_change_event: change_description += " and group state has been changed to %s" % group.state.name - add_state_change_event(charter, login, prev_charter_state, new_charter_state) + e = add_state_change_event(charter, by, prev_charter_state, new_charter_state) + if e: + events.append(e) - fix_charter_revision_after_approval(charter, login) + fix_charter_revision_after_approval(charter, by) + + charter.save_with_history(events) email_iesg_secretary_re_charter(request, group, "Charter state changed to %s" % new_charter_state.name, change_description) @@ -664,7 +627,7 @@ def approve(request, name): o.state_id = "active" o.save() MilestoneGroupEvent.objects.create( - group=group, type="changed_milestone", by=login, + group=group, type="changed_milestone", by=by, desc="Changed milestone \"%s\", set state to active from review" % o.desc, milestone=o) @@ -681,7 +644,7 @@ def approve(request, name): m.save() MilestoneGroupEvent.objects.create( - group=group, type="changed_milestone", by=login, + group=group, type="changed_milestone", by=by, desc="Added milestone \"%s\", due %s, from approved charter" % (m.desc, m.due), milestone=m) @@ -691,7 +654,7 @@ def approve(request, name): m.save() MilestoneGroupEvent.objects.create( - group=group, type="changed_milestone", by=login, + group=group, type="changed_milestone", by=by, desc="Deleted milestone \"%s\", not present in approved charter" % m.desc, milestone=m) diff --git a/ietf/doc/views_conflict_review.py b/ietf/doc/views_conflict_review.py index 23de7310f..1beca11fe 100644 --- a/ietf/doc/views_conflict_review.py +++ b/ietf/doc/views_conflict_review.py @@ -9,7 +9,7 @@ from django.template.loader import render_to_string from django.conf import settings from ietf.doc.models import ( BallotDocEvent, BallotPositionDocEvent, DocAlias, DocEvent, - Document, NewRevisionDocEvent, State, save_document_in_history ) + Document, NewRevisionDocEvent, State ) from ietf.doc.utils import ( add_state_change_event, close_open_ballots, create_ballot_if_not_open, get_document_content, update_telechat ) from ietf.doc.mails import email_iana @@ -47,13 +47,12 @@ def change_state(request, name, option=None): prev_state = review.get_state() if new_state != prev_state: - save_document_in_history(review) + events = [] review.set_state(new_state) - add_state_change_event(review, login, prev_state, new_state) + events.append(add_state_change_event(review, login, prev_state, new_state)) - review.time = datetime.datetime.now() - review.save() + review.save_with_history(events) if new_state.slug == "iesgeval": create_ballot_if_not_open(review, login, "conflrev") @@ -151,20 +150,19 @@ def submit(request, name): if "submit_response" in request.POST: form = UploadForm(request.POST, request.FILES) if form.is_valid(): - save_document_in_history(review) - review.rev = next_rev + events = [] e = NewRevisionDocEvent(doc=review, by=login, type="new_revision") e.desc = "New version available: %s-%s.txt" % (review.canonical_name(), review.rev) e.rev = review.rev e.save() + events.append(e) # Save file on disk form.save(review) - review.time = datetime.datetime.now() - review.save() + review.save_with_history(events) return redirect('doc_view', name=review.name) @@ -214,15 +212,14 @@ def edit_ad(request, name): if request.method == 'POST': form = AdForm(request.POST) if form.is_valid(): - review.ad = form.cleaned_data['ad'] - review.save() - - login = request.user.person - c = DocEvent(type="added_comment", doc=review, by=login) + + c = DocEvent(type="added_comment", doc=review, by=request.user.person) c.desc = "Shepherding AD changed to "+review.ad.name c.save() + review.save_with_history([c]) + return redirect('doc_view', name=review.name) else: @@ -283,13 +280,14 @@ def approve(request, name): if form.is_valid(): prev_state = review.get_state() + events = [] new_state_slug = 'appr-reqnopub-sent' if prev_state.slug == 'appr-reqnopub-pend' else 'appr-noprob-sent' new_state = State.objects.get(used=True, type="conflrev", slug=new_state_slug) - save_document_in_history(review) review.set_state(new_state) - add_state_change_event(review, login, prev_state, new_state) + e = add_state_change_event(review, login, prev_state, new_state) + events.append(e) close_open_ballots(review, login) @@ -297,9 +295,9 @@ def approve(request, name): e.type = "iesg_approved" e.desc = "IESG has approved the conflict review response" e.save() + events.append(e) - review.time = e.time - review.save() + review.save_with_history(events) # send announcement send_mail_preformatted(request, form.cleaned_data['announcement_text']) @@ -379,16 +377,16 @@ def build_conflict_review_document(login, doc_to_review, ad, notify, create_in_s iesg_group = Group.objects.get(acronym='iesg') - conflict_review=Document( type_id = "conflrev", - title = "IETF conflict review for %s" % doc_to_review.name, - name = review_name, - rev = "00", - ad = ad, - notify = notify, - stream_id = 'ietf', - group = iesg_group, - ) - conflict_review.save() + conflict_review = Document.objects.create( + type_id="conflrev", + title="IETF conflict review for %s" % doc_to_review.name, + name=review_name, + rev="00", + ad=ad, + notify=notify, + stream_id='ietf', + group=iesg_group, + ) conflict_review.set_state(create_in_state) DocAlias.objects.create( name=review_name , document=conflict_review ) diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index d18221f11..0ac328612 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -953,8 +953,7 @@ def edit_notify(request, name): if set(new_notify.split(',')) != set(doc.notify.split(',')): e = make_notify_changed_event(request, doc, login.person, new_notify) doc.notify = new_notify - doc.time = e.time - doc.save() + doc.save_with_history([e]) return redirect('doc_view', name=doc.name) elif "regenerate_addresses" in request.POST: diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index 8e3d4dd66..6402c4592 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -16,8 +16,7 @@ from django.contrib import messages import debug # pyflakes:ignore from ietf.doc.models import ( Document, DocAlias, RelatedDocument, State, - StateType, DocEvent, ConsensusDocEvent, TelechatDocEvent, WriteupDocEvent, IESG_SUBSTATE_TAGS, - save_document_in_history ) + StateType, DocEvent, ConsensusDocEvent, TelechatDocEvent, WriteupDocEvent, IESG_SUBSTATE_TAGS) from ietf.doc.mails import ( email_ad, email_pulled_from_rfc_queue, email_resurrect_requested, email_resurrection_completed, email_state_changed, email_stream_changed, email_stream_state_changed, email_stream_tags_changed, extra_automation_headers, @@ -87,17 +86,17 @@ def change_state(request, name): prev_tags = doc.tags.filter(slug__in=IESG_SUBSTATE_TAGS) new_tags = [tag] if tag else [] if new_state != prev_state or set(new_tags) != set(prev_tags): - save_document_in_history(doc) - doc.set_state(new_state) doc.tags.remove(*prev_tags) doc.tags.add(*new_tags) + events = [] + e = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags) - msg = e.desc + events.append(e) if comment: c = DocEvent(type="added_comment") @@ -106,10 +105,11 @@ def change_state(request, name): c.desc = comment c.save() - msg += "\n" + comment - - doc.time = e.time - doc.save() + events.append(c) + + doc.save_with_history(events) + + msg = u"\n".join(e.desc for e in events) email_state_changed(request, doc, msg) email_ad(request, doc, doc.ad, login, msg) @@ -187,14 +187,11 @@ def change_iana_state(request, name, state_type): new_state = form.cleaned_data['state'] if new_state != prev_state: - save_document_in_history(doc) - doc.set_state(new_state) - e = add_state_change_event(doc, request.user.person, prev_state, new_state) + events = [add_state_change_event(doc, request.user.person, prev_state, new_state)] - doc.time = e.time - doc.save() + doc.save_with_history(events) return HttpResponseRedirect(doc.get_absolute_url()) @@ -243,27 +240,28 @@ def change_stream(request, name): elif "irsg@irtf.org" not in doc.notify: doc.notify += ", irsg@irtf.org" - save_document_in_history(doc) - doc.stream = new_stream doc.group = Group.objects.get(type="individ") + events = [] + e = DocEvent(doc=doc,by=login,type='changed_document') e.desc = u"Stream changed to %s from %s"% (new_stream, old_stream or "None") e.save() - email_desc = e.desc + events.append(e) if comment: c = DocEvent(doc=doc,by=login,type="added_comment") c.desc = comment c.save() - email_desc += "\n"+c.desc - - doc.time = e.time - doc.save() + events.append(c) - email_stream_changed(request, doc, old_stream, new_stream, email_desc) + doc.save_with_history(events) + + msg = u"\n".join(e.desc for e in events) + + email_stream_changed(request, doc, old_stream, new_stream, msg) return HttpResponseRedirect(doc.get_absolute_url()) @@ -326,16 +324,11 @@ def replaces(request, name): by = request.user.person if new_replaces != old_replaces: - save_document_in_history(doc) - doc.time = datetime.datetime.now() - doc.save() + events = set_replaces_for_document(request, doc, new_replaces, by=by, + email_subject="%s replacement status updated by %s" % (doc.name, by), + comment=comment) - set_replaces_for_document(request, doc, new_replaces, by=by, - email_subject="%s replacement status updated by %s" % (doc.name, by), - email_comment=comment) - - if comment: - DocEvent.objects.create(doc=doc, by=by, type="added_comment", desc=comment) + doc.save_with_history(events) return HttpResponseRedirect(doc.get_absolute_url()) else: @@ -380,22 +373,22 @@ def review_possibly_replaces(request, name): comment = form.cleaned_data['comment'].strip() by = request.user.person - save_document_in_history(doc) - doc.time = datetime.datetime.now() - doc.save() + events = [] # all suggestions reviewed, so get rid of them - DocEvent.objects.create(doc=doc, by=by, type="reviewed_suggested_replaces", - desc="Reviewed suggested replacement relationships: %s" % ", ".join(d.name for d in suggested)) + events.append(DocEvent.objects.create(doc=doc, by=by, type="reviewed_suggested_replaces", + desc="Reviewed suggested replacement relationships: %s" % ", ".join(d.name for d in suggested))) RelatedDocument.objects.filter(source=doc, target__in=suggested,relationship__slug='possibly-replaces').delete() if new_replaces != old_replaces: - set_replaces_for_document(request, doc, new_replaces, by=by, - email_subject="%s replacement status updated by %s" % (doc.name, by), - email_comment=comment) + events.extend(set_replaces_for_document(request, doc, new_replaces, by=by, + email_subject="%s replacement status updated by %s" % (doc.name, by), + comment=comment)) if comment: - DocEvent.objects.create(doc=doc, by=by, type="added_comment", desc=comment) + events.append(DocEvent.objects.create(doc=doc, by=by, type="added_comment", desc=comment)) + + doc.save_with_history(events) return HttpResponseRedirect(doc.get_absolute_url()) else: @@ -432,26 +425,25 @@ def change_intention(request, name): old_level = doc.intended_std_level if new_level != old_level: - save_document_in_history(doc) - doc.intended_std_level = new_level + events = [] e = DocEvent(doc=doc,by=login,type='changed_document') e.desc = u"Intended Status changed to %s from %s"% (new_level,old_level) e.save() - - email_desc = e.desc + events.append(e) if comment: c = DocEvent(doc=doc,by=login,type="added_comment") c.desc = comment c.save() - email_desc += "\n"+c.desc - - doc.time = e.time - doc.save() + events.append(c) - email_ad(request, doc, doc.ad, login, email_desc) + doc.save_with_history(events) + + msg = u"\n".join(e.desc for e in events) + + email_ad(request, doc, doc.ad, login, msg) return HttpResponseRedirect(doc.get_absolute_url()) @@ -538,27 +530,27 @@ def to_iesg(request,name): if request.POST.get("confirm", ""): - save_document_in_history(doc) + by = request.user.person - login = request.user.person + events = [] changes = [] if not doc.get_state("draft-iesg"): - e = DocEvent() e.type = "started_iesg_process" - e.by = login + e.by = by e.doc = doc e.desc = "IESG process started in state %s" % target_state['iesg'].name e.save() + events.append(e) for state_type in ['draft-iesg','draft-stream-ietf']: prev_state=doc.get_state(state_type) new_state = target_state[target_map[state_type]] if not prev_state==new_state: doc.set_state(new_state) - add_state_change_event(doc=doc,by=login,prev_state=prev_state,new_state=new_state) + events.append(add_state_change_event(doc=doc,by=by,prev_state=prev_state,new_state=new_state)) if not doc.ad == ad : doc.ad = ad @@ -574,23 +566,22 @@ def to_iesg(request,name): changes.append(previous_writeup.text) for c in changes: - e = DocEvent(doc=doc, by=login) + e = DocEvent(doc=doc, by=by) e.desc = c e.type = "changed_document" e.save() + events.append(e) - doc.time = datetime.datetime.now() - - doc.save() + doc.save_with_history(events) extra = {} extra['Cc'] = "%s-chairs@ietf.org, iesg-secretary@ietf.org, %s" % (doc.group.acronym,doc.notify) send_mail(request=request, to = doc.ad.email_address(), - frm = login.formatted_email(), + frm = by.formatted_email(), subject = "Publication has been requested for %s-%s" % (doc.name,doc.rev), template = "doc/submit_to_iesg_email.txt", - context = dict(doc=doc,login=login,url="%s%s"%(settings.IDTRACKER_BASE_URL,doc.get_absolute_url()),), + context = dict(doc=doc,by=by,url="%s%s"%(settings.IDTRACKER_BASE_URL,doc.get_absolute_url()),), extra = extra) return HttpResponseRedirect(doc.get_absolute_url()) @@ -614,8 +605,6 @@ def edit_info(request, name): if doc.get_state_slug() == "expired": raise Http404 - login = request.user.person - new_document = False if not doc.get_state("draft-iesg"): # FIXME: should probably receive "new document" as argument to view instead of this new_document = True @@ -630,27 +619,26 @@ def edit_info(request, name): initial=dict(ad=doc.ad_id, telechat_date=initial_telechat_date)) if form.is_valid(): - save_document_in_history(doc) - + by = request.user.person + r = form.cleaned_data + events = [] + if new_document: doc.set_state(r['create_in_state']) # Is setting the WG state here too much of a hidden side-effect? if r['create_in_state'].slug=='pub-req': - if doc.stream and ( doc.stream.slug=='ietf' ) and doc.group and ( doc.group.type.name=='WG'): + if doc.stream and doc.stream.slug=='ietf' and doc.group and doc.group.type_id == 'wg': submitted_state = State.objects.get(type='draft-stream-ietf',slug='sub-pub') doc.set_state(submitted_state) e = DocEvent() e.type = "changed_document" - e.by = login + e.by = by e.doc = doc e.desc = "Working group state set to %s" % submitted_state.name e.save() - - # fix so Django doesn't barf in the diff below because these - # fields can't be NULL - doc.ad = r['ad'] + events.append(e) replaces = Document.objects.filter(docalias__relateddocument__source=doc, docalias__relateddocument__relationship="replaces") if replaces: @@ -662,14 +650,16 @@ def edit_info(request, name): e.doc = doc e.desc = "Earlier history may be found in the Comment Log for %s" % (replaces[0], replaces[0].get_absolute_url()) e.save() + events.append(e) e = DocEvent() e.type = "started_iesg_process" - e.by = login + e.by = by e.doc = doc e.desc = "IESG process started in state %s" % doc.get_state("draft-iesg").name e.save() - + events.append(e) + orig_ad = doc.ad changes = [] @@ -716,20 +706,18 @@ def edit_info(request, name): doc.group = r["area"] for c in changes: - e = DocEvent(doc=doc, by=login) - e.desc = c - e.type = "changed_document" - e.save() + events.append(DocEvent.objects.create(doc=doc, by=by, desc=c, type="changed_document")) - update_telechat(request, doc, login, - r['telechat_date'], r['returning_item']) + e = update_telechat(request, doc, by, + r['telechat_date'], r['returning_item']) + if e: + events.append(e) - doc.time = datetime.datetime.now() + doc.save_with_history(events) if changes and not new_document: - email_ad(request, doc, orig_ad, login, "\n".join(changes)) - - doc.save() + email_ad(request, doc, orig_ad, by, "\n".join(changes)) + return HttpResponseRedirect(doc.get_absolute_url()) else: init = dict(intended_std_level=doc.intended_std_level_id, @@ -753,7 +741,6 @@ def edit_info(request, name): dict(doc=doc, form=form, user=request.user, - login=login, ballot_issued=doc.latest_event(type="sent_ballot_announcement")), context_instance=RequestContext(request)) @@ -764,12 +751,12 @@ def request_resurrect(request, name): if doc.get_state_slug() != "expired": raise Http404 - login = request.user.person - if request.method == 'POST': - email_resurrect_requested(request, doc, login) + by = request.user.person + + email_resurrect_requested(request, doc, by) - e = DocEvent(doc=doc, by=login) + e = DocEvent(doc=doc, by=by) e.type = "requested_resurrect" e.desc = "Resurrection was requested" e.save() @@ -788,24 +775,22 @@ def resurrect(request, name): if doc.get_state_slug() != "expired": raise Http404 - login = request.user.person - if request.method == 'POST': - save_document_in_history(doc) - e = doc.latest_event(type__in=('requested_resurrect', "completed_resurrect")) if e and e.type == 'requested_resurrect': email_resurrection_completed(request, doc, requester=e.by) - e = DocEvent(doc=doc, by=login) + events = [] + e = DocEvent(doc=doc, by=request.user.person) e.type = "completed_resurrect" e.desc = "Resurrection was completed" e.save() - + events.append(e) + doc.set_state(State.objects.get(used=True, type="draft", slug="active")) doc.expires = datetime.datetime.now() + datetime.timedelta(settings.INTERNET_DRAFT_DAYS_TO_EXPIRE) - doc.time = datetime.datetime.now() - doc.save() + doc.save_with_history(events) + return HttpResponseRedirect(doc.get_absolute_url()) return render_to_response('doc/draft/resurrect.html', @@ -817,7 +802,7 @@ class IESGNoteForm(forms.Form): note = forms.CharField(widget=forms.Textarea, label="IESG note", required=False) def clean_note(self): - # not muning the database content to use html line breaks -- + # not munging the database content to use html line breaks -- # that has caused a lot of pain in the past. return self.cleaned_data['note'].replace('\r', '').strip() @@ -843,13 +828,13 @@ def edit_iesg_note(request, name): else: log_message = "Note added '%s'" % new_note - doc.note = new_note - doc.save() - c = DocEvent(type="added_comment", doc=doc, by=login) c.desc = log_message c.save() + doc.note = new_note + doc.save_with_history([c]) + return redirect('doc_view', name=doc.name) else: form = IESGNoteForm(initial=initial) @@ -956,27 +941,24 @@ def edit_shepherd(request, name): if form.is_valid(): if form.cleaned_data['shepherd'] != doc.shepherd: + events = [] - save_document_in_history(doc) - doc.shepherd = form.cleaned_data['shepherd'] - doc.save() - + c = DocEvent(type="added_comment", doc=doc, by=request.user.person) c.desc = "Document shepherd changed to "+ (doc.shepherd.person.name if doc.shepherd else "(None)") c.save() + events.append(c) if doc.shepherd and (doc.shepherd.formatted_email() not in doc.notify): - login = request.user.person addrs = doc.notify if addrs: addrs += ', ' addrs += doc.shepherd.formatted_email() - make_notify_changed_event(request, doc, login, addrs, c.time) + events.append(make_notify_changed_event(request, doc, request.user.person, addrs, c.time)) doc.notify = addrs - doc.time = c.time - doc.save() + doc.save_with_history(events) else: messages.info(request,"The selected shepherd was already assigned - no changes have been made.") @@ -1015,14 +997,15 @@ def change_shepherd_email(request, name): form = ChangeShepherdEmailForm(request.POST, initial=initial) if form.is_valid(): if form.cleaned_data['shepherd'] != doc.shepherd: - save_document_in_history(doc) - doc.shepherd = form.cleaned_data['shepherd'] - doc.save() - + + events = [] c = DocEvent(type="added_comment", doc=doc, by=request.user.person) c.desc = "Document shepherd email changed" c.save() + events.append(c) + + doc.save_with_history(events) else: messages.info(request,"The selected shepherd address was already assigned - no changes have been made.") @@ -1058,15 +1041,14 @@ def edit_ad(request, name): if request.method == 'POST': form = AdForm(request.POST) if form.is_valid(): - doc.ad = form.cleaned_data['ad'] - doc.save() - - login = request.user.person - c = DocEvent(type="added_comment", doc=doc, by=login) + + c = DocEvent(type="added_comment", doc=doc, by=request.user.person) c.desc = "Shepherding AD changed to "+doc.ad.name c.save() + doc.save_with_history([c]) + return redirect('doc_view', name=doc.name) else: @@ -1142,6 +1124,8 @@ def request_publication(request, name): if request.method == 'POST' and not request.POST.get("reset"): form = PublicationForm(request.POST) if form.is_valid(): + events = [] + if not request.REQUEST.get("skiprfceditorpost"): # start by notifying the RFC Editor import ietf.sync.rfceditor @@ -1170,14 +1154,16 @@ def request_publication(request, name): e = DocEvent(doc=doc, type="requested_publication", by=request.user.person) e.desc = "Sent request for publication to the RFC Editor" e.save() + events.append(e) # change state prev_state = doc.get_state(next_state.type_id) if next_state != prev_state: doc.set_state(next_state) e = add_state_change_event(doc, request.user.person, prev_state, next_state) - doc.time = e.time - doc.save() + if e: + events.append(e) + doc.save_with_history(events) return redirect('doc_view', name=doc.name) @@ -1246,10 +1232,7 @@ def adopt_draft(request, name): if form.is_valid(): # adopt by = request.user.person - - save_document_in_history(doc) - - doc.time = datetime.datetime.now() + events = [] group = form.cleaned_data["group"] if group.type.slug == "rg": @@ -1266,6 +1249,7 @@ def adopt_draft(request, name): if doc.stream: e.desc += u" from %s" % doc.stream.name e.save() + events.append(e) old_stream = doc.stream doc.stream = new_stream if old_stream != None: @@ -1278,14 +1262,13 @@ def adopt_draft(request, name): if doc.group.type_id != "individ": e.desc += " from %s (%s)" % (doc.group.name, doc.group.acronym.upper()) e.save() + events.append(e) doc.group = group new_notify = get_initial_notify(doc,extra=doc.notify) - make_notify_changed_event(request, doc, by, new_notify, doc.time) + events.append(make_notify_changed_event(request, doc, by, new_notify, doc.time)) doc.notify = new_notify - doc.save() - comment = form.cleaned_data["comment"].strip() # state @@ -1293,6 +1276,7 @@ def adopt_draft(request, name): if new_state != prev_state: doc.set_state(new_state) e = add_state_change_event(doc, by, prev_state, new_state, timestamp=doc.time) + events.append(e) due_date = None if form.cleaned_data["weeks"] != None: @@ -1307,6 +1291,9 @@ def adopt_draft(request, name): e = DocEvent(type="added_comment", time=doc.time, by=by, doc=doc) e.desc = comment e.save() + events.append(e) + + doc.save_with_history(events) return HttpResponseRedirect(doc.get_absolute_url()) else: @@ -1382,10 +1369,8 @@ def change_stream_state(request, name, state_type): form = ChangeStreamStateForm(request.POST, doc=doc, state_type=state_type) if form.is_valid(): by = request.user.person + events = [] - save_document_in_history(doc) - - doc.time = datetime.datetime.now() comment = form.cleaned_data["comment"].strip() # state @@ -1393,6 +1378,7 @@ def change_stream_state(request, name, state_type): if new_state != prev_state: doc.set_state(new_state) e = add_state_change_event(doc, by, prev_state, new_state, timestamp=doc.time) + events.append(e) due_date = None if form.cleaned_data["weeks"] != None: @@ -1419,6 +1405,7 @@ def change_stream_state(request, name, state_type): l.append(u"Tag%s %s cleared." % (pluralize(removed_tags), ", ".join(t.name for t in removed_tags))) e.desc = " ".join(l) e.save() + events.append(e) email_stream_tags_changed(request, doc, added_tags, removed_tags, by, comment) @@ -1427,6 +1414,9 @@ def change_stream_state(request, name, state_type): e = DocEvent(type="added_comment", time=doc.time, by=by, doc=doc) e.desc = comment e.save() + events.append(e) + + doc.save_with_history(events) return HttpResponseRedirect(doc.get_absolute_url()) else: diff --git a/ietf/doc/views_material.py b/ietf/doc/views_material.py index c1c5f6c3c..0c8fadbaf 100644 --- a/ietf/doc/views_material.py +++ b/ietf/doc/views_material.py @@ -13,7 +13,7 @@ from django.core.urlresolvers import reverse as urlreverse import debug # pyflakes:ignore from ietf.doc.models import Document, DocAlias, DocTypeName, DocEvent, State -from ietf.doc.models import NewRevisionDocEvent, save_document_in_history +from ietf.doc.models import NewRevisionDocEvent from ietf.doc.utils import add_state_change_event, check_common_doc_name_rules from ietf.group.models import Group from ietf.group.utils import can_manage_materials @@ -104,15 +104,17 @@ def edit_material(request, name=None, acronym=None, action=None, doc_type=None): form = UploadMaterialForm(document_type, action, group, doc, request.POST, request.FILES) if form.is_valid(): + events = [] + if action == "new": doc = Document() doc.type = document_type doc.group = group doc.rev = "00" doc.name = form.cleaned_data["name"] + prev_rev = None else: - save_document_in_history(doc) prev_rev = doc.rev prev_title = doc.title @@ -124,8 +126,6 @@ def edit_material(request, name=None, acronym=None, action=None, doc_type=None): if "abstract" in form.cleaned_data: doc.abstract = form.cleaned_data["abstract"] - doc.time = datetime.datetime.now() - if "material" in form.fields: if action != "new": doc.rev = "%02d" % (int(doc.rev) + 1) @@ -137,8 +137,6 @@ def edit_material(request, name=None, acronym=None, action=None, doc_type=None): for chunk in f.chunks(): dest.write(chunk) - doc.save() - if action == "new": DocAlias.objects.get_or_create(name=doc.name, document=doc) @@ -148,7 +146,8 @@ def edit_material(request, name=None, acronym=None, action=None, doc_type=None): e.by = request.user.person e.desc = "New version available: %s-%s" % (doc.name, doc.rev) e.save() - + events.append(e) + if prev_title != doc.title: e = DocEvent(doc=doc, by=request.user.person, type='changed_document') e.desc = u"Changed title to %s" % doc.title @@ -156,10 +155,15 @@ def edit_material(request, name=None, acronym=None, action=None, doc_type=None): e.desc += u" from %s" % prev_title e.time = doc.time e.save() + events.append(e) if "state" in form.cleaned_data and form.cleaned_data["state"] != prev_state: doc.set_state(form.cleaned_data["state"]) - add_state_change_event(doc, request.user.person, prev_state, form.cleaned_data["state"]) + e = add_state_change_event(doc, request.user.person, prev_state, form.cleaned_data["state"]) + events.append(e) + + if events: + doc.save_with_history(events) return redirect("doc_view", name=doc.name) else: diff --git a/ietf/doc/views_status_change.py b/ietf/doc/views_status_change.py index 39bab4214..bfc3a324a 100644 --- a/ietf/doc/views_status_change.py +++ b/ietf/doc/views_status_change.py @@ -9,8 +9,7 @@ from django.template.loader import render_to_string from django.conf import settings from ietf.doc.models import ( Document, DocAlias, State, DocEvent, BallotDocEvent, - BallotPositionDocEvent, NewRevisionDocEvent, WriteupDocEvent, - save_document_in_history, STATUSCHANGE_RELATIONS ) + BallotPositionDocEvent, NewRevisionDocEvent, WriteupDocEvent, STATUSCHANGE_RELATIONS ) from ietf.doc.forms import AdForm from ietf.doc.lastcall import request_last_call from ietf.doc.utils import get_document_content, add_state_change_event, update_telechat, close_open_ballots, create_ballot_if_not_open @@ -50,16 +49,13 @@ def change_state(request, name, option=None): prev_state = status_change.get_state() if new_state != prev_state: - save_document_in_history(status_change) - status_change.set_state(new_state) - e = add_state_change_event(status_change, login, prev_state, new_state) - - status_change.time = e.time - status_change.save() + events = [] + events.append(add_state_change_event(status_change, login, prev_state, new_state)) + status_change.save_with_history(events) if new_state.slug == "iesgeval": - create_ballot_if_not_open(status_change, login, "statchg", e.time) + create_ballot_if_not_open(status_change, login, "statchg", status_change.time) ballot = status_change.latest_event(BallotDocEvent, type="created_ballot") if has_role(request.user, "Area Director") and not status_change.latest_event(BallotPositionDocEvent, ad=login, ballot=ballot, type="changed_ballot_position"): @@ -146,20 +142,19 @@ def submit(request, name): if "submit_response" in request.POST: form = UploadForm(request.POST, request.FILES) if form.is_valid(): - save_document_in_history(doc) - doc.rev = next_rev + events = [] e = NewRevisionDocEvent(doc=doc, by=login, type="new_revision") e.desc = "New version available: %s-%s.txt" % (doc.canonical_name(), doc.rev) e.rev = doc.rev e.save() + events.append(e) # Save file on disk form.save(doc) - doc.time = datetime.datetime.now() - doc.save() + doc.save_with_history(events) return redirect('doc_view', name=doc.name) @@ -213,13 +208,13 @@ def edit_title(request, name): if form.is_valid(): status_change.title = form.cleaned_data['title'] - status_change.save() - - login = request.user.person - c = DocEvent(type="added_comment", doc=status_change, by=login) + + c = DocEvent(type="added_comment", doc=status_change, by=request.user.person) c.desc = "Title changed to '%s'"%status_change.title c.save() + status_change.save_with_history([c]) + return redirect("doc_view", name=status_change.name) else: @@ -243,15 +238,14 @@ def edit_ad(request, name): if request.method == 'POST': form = AdForm(request.POST) if form.is_valid(): - status_change.ad = form.cleaned_data['ad'] - status_change.save() - - login = request.user.person - c = DocEvent(type="added_comment", doc=status_change, by=login) + + c = DocEvent(type="added_comment", doc=status_change, by=request.user.person) c.desc = "Shepherding AD changed to "+status_change.ad.name c.save() + status_change.save_with_history([c]) + return redirect("doc_view", name=status_change.name) else: @@ -331,13 +325,13 @@ def approve(request, name): if formset.is_valid(): - save_document_in_history(status_change) - prev_state = status_change.get_state() new_state = State.objects.get(type='statchg', slug='appr-sent') status_change.set_state(new_state) - add_state_change_event(status_change, login, prev_state, new_state) + + events = [] + events.append(add_state_change_event(status_change, login, prev_state, new_state)) close_open_ballots(status_change, login) @@ -345,9 +339,9 @@ def approve(request, name): e.type = "iesg_approved" e.desc = "IESG has approved the status change" e.save() + events.append(e) - status_change.time = e.time - status_change.save() + status_change.save_with_history(events) for form in formset.forms: @@ -516,16 +510,16 @@ def start_rfc_status_change(request,name): iesg_group = Group.objects.get(acronym='iesg') - status_change=Document( type_id = "statchg", - name = 'status-change-'+form.cleaned_data['document_name'], - title = form.cleaned_data['title'], - rev = "00", - ad = form.cleaned_data['ad'], - notify = form.cleaned_data['notify'], - stream_id = 'ietf', - group = iesg_group, - ) - status_change.save() + status_change = Document.objects.create( + type_id="statchg", + name='status-change-'+form.cleaned_data['document_name'], + title=form.cleaned_data['title'], + rev="00", + ad=form.cleaned_data['ad'], + notify=form.cleaned_data['notify'], + stream_id='ietf', + group=iesg_group, + ) status_change.set_state(form.cleaned_data['create_in_state']) DocAlias.objects.create( name= 'status-change-'+form.cleaned_data['document_name'], document=status_change ) @@ -654,6 +648,8 @@ def last_call(request, name): if "save_last_call_text" in request.POST or "send_last_call_request" in request.POST: form = LastCallTextForm(request.POST) if form.is_valid(): + events = [] + t = form.cleaned_data['last_call_text'] if t != last_call_event.text: e = WriteupDocEvent(doc=status_change, by=login) @@ -663,17 +659,19 @@ def last_call(request, name): e.text = t e.save() - if "send_last_call_request" in request.POST: - save_document_in_history(status_change) + events.append(e) + if "send_last_call_request" in request.POST: prev_state = status_change.get_state() new_state = State.objects.get(type='statchg', slug='lc-req') status_change.set_state(new_state) e = add_state_change_event(status_change, login, prev_state, new_state) + if e: + events.append(e) - status_change.time = (e and e.time) or datetime.datetime.now() - status_change.save() + if events: + status_change.save_with_history(events) request_last_call(request, status_change) diff --git a/ietf/group/edit.py b/ietf/group/edit.py index e4a40aea9..5133c3d4d 100644 --- a/ietf/group/edit.py +++ b/ietf/group/edit.py @@ -150,7 +150,7 @@ def get_or_create_initial_charter(group, group_type): try: charter = Document.objects.get(docalias__name=charter_name) except Document.DoesNotExist: - charter = Document( + charter = Document.objects.create( name=charter_name, type_id="charter", title=group.name, @@ -158,7 +158,6 @@ def get_or_create_initial_charter(group, group_type): abstract=group.name, rev="00-00", ) - charter.save() charter.set_state(State.objects.get(used=True, type="charter", slug="notrev")) # Create an alias as well diff --git a/ietf/group/tests.py b/ietf/group/tests.py index 36f9fdd48..d7bd00615 100644 --- a/ietf/group/tests.py +++ b/ietf/group/tests.py @@ -6,7 +6,9 @@ from django.core.urlresolvers import reverse as urlreverse from django.db.models import Q from django.test import Client +from ietf.doc.models import DocEvent from ietf.group.models import Role, Group +from ietf.person.models import Person from ietf.utils.test_data import make_test_data from ietf.utils.test_utils import login_testing_unauthorized, TestCase @@ -33,7 +35,7 @@ class StreamTests(TestCase): def test_stream_documents(self): draft = make_test_data() draft.stream_id = "iab" - draft.save() + draft.save_with_history([DocEvent.objects.create(doc=draft, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) r = self.client.get(urlreverse("ietf.group.views_stream.stream_documents", kwargs=dict(acronym="iab"))) self.assertEqual(r.status_code, 200) diff --git a/ietf/iesg/tests.py b/ietf/iesg/tests.py index 295c00c67..a9d573714 100644 --- a/ietf/iesg/tests.py +++ b/ietf/iesg/tests.py @@ -71,7 +71,7 @@ class IESGAgendaTests(TestCase): ise_draft = Document.objects.get(name="draft-imaginary-independent-submission") ise_draft.stream = StreamName.objects.get(slug="ise") - ise_draft.save() + ise_draft.save_with_history([DocEvent.objects.create(doc=ise_draft, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) self.telechat_docs = { "ietf_draft": Document.objects.get(name="draft-ietf-mars-test"), @@ -120,7 +120,7 @@ class IESGAgendaTests(TestCase): # 2.1 protocol WG submissions draft.intended_std_level_id = "ps" draft.group = Group.objects.get(acronym="mars") - draft.save() + draft.save_with_history([DocEvent.objects.create(doc=draft, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) draft.set_state(State.objects.get(type="draft-iesg", slug="iesg-eva")) self.assertTrue(draft in agenda_data(date_str)["sections"]["2.1.1"]["docs"]) @@ -135,7 +135,7 @@ class IESGAgendaTests(TestCase): # 2.2 protocol individual submissions draft.group = Group.objects.get(type="individ") - draft.save() + draft.save_with_history([DocEvent.objects.create(doc=draft, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) draft.set_state(State.objects.get(type="draft-iesg", slug="iesg-eva")) self.assertTrue(draft in agenda_data(date_str)["sections"]["2.2.1"]["docs"]) @@ -151,7 +151,7 @@ class IESGAgendaTests(TestCase): # 3.1 document WG submissions draft.intended_std_level_id = "inf" draft.group = Group.objects.get(acronym="mars") - draft.save() + draft.save_with_history([DocEvent.objects.create(doc=draft, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) draft.set_state(State.objects.get(type="draft-iesg", slug="iesg-eva")) self.assertTrue(draft in agenda_data(date_str)["sections"]["3.1.1"]["docs"]) @@ -166,7 +166,7 @@ class IESGAgendaTests(TestCase): # 3.2 document individual submissions draft.group = Group.objects.get(type="individ") - draft.save() + draft.save_with_history([DocEvent.objects.create(doc=draft, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) draft.set_state(State.objects.get(type="draft-iesg", slug="iesg-eva")) self.assertTrue(draft in agenda_data(date_str)["sections"]["3.2.1"]["docs"]) @@ -189,7 +189,7 @@ class IESGAgendaTests(TestCase): relationship_id="tohist") statchg.group = Group.objects.get(acronym="mars") - statchg.save() + statchg.save_with_history([DocEvent.objects.create(doc=statchg, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) statchg.set_state(State.objects.get(type="statchg", slug="iesgeval")) self.assertTrue(statchg in agenda_data(date_str)["sections"]["2.3.1"]["docs"]) @@ -207,7 +207,7 @@ class IESGAgendaTests(TestCase): relation.save() statchg.group = Group.objects.get(acronym="mars") - statchg.save() + statchg.save_with_history([DocEvent.objects.create(doc=statchg, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) statchg.set_state(State.objects.get(type="statchg", slug="iesgeval")) self.assertTrue(statchg in agenda_data(date_str)["sections"]["3.3.1"]["docs"]) @@ -225,7 +225,7 @@ class IESGAgendaTests(TestCase): telechat_event.save() conflrev.group = Group.objects.get(acronym="mars") - conflrev.save() + conflrev.save_with_history([DocEvent.objects.create(doc=conflrev, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) conflrev.set_state(State.objects.get(type="conflrev", slug="iesgeval")) self.assertTrue(conflrev in agenda_data(date_str)["sections"]["3.4.1"]["docs"]) @@ -244,7 +244,7 @@ class IESGAgendaTests(TestCase): telechat_event.save() charter.group = Group.objects.get(acronym="mars") - charter.save() + charter.save_with_history([DocEvent.objects.create(doc=charter, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")]) charter.group.state_id = "bof" charter.group.save() diff --git a/ietf/iesg/views.py b/ietf/iesg/views.py index 6fbd07cc2..97496e46d 100644 --- a/ietf/iesg/views.py +++ b/ietf/iesg/views.py @@ -327,11 +327,11 @@ def handle_reschedule_form(request, doc, dates, status): if request.method == 'POST': form = RescheduleForm(request.POST, **formargs) if form.is_valid(): - update_telechat(request, doc, request.user.person, - form.cleaned_data['telechat_date'], - False if form.cleaned_data['clear_returning_item'] else None) - doc.time = datetime.datetime.now() - doc.save() + e = update_telechat(request, doc, request.user.person, + form.cleaned_data['telechat_date'], + False if form.cleaned_data['clear_returning_item'] else None) + if e: + doc.save_with_history([e]) status["changed"] = True else: diff --git a/ietf/secr/drafts/forms.py b/ietf/secr/drafts/forms.py index 5ba5dd247..228f98bfb 100644 --- a/ietf/secr/drafts/forms.py +++ b/ietf/secr/drafts/forms.py @@ -155,7 +155,7 @@ class EditModelForm(forms.ModelForm): # setup replaced self.fields['review_by_rfc_editor'].initial = bool(self.instance.tags.filter(slug='rfc-rev')) - def save(self, force_insert=False, force_update=False, commit=True): + def save(self, commit=False): m = super(EditModelForm, self).save(commit=False) state = self.cleaned_data['state'] iesg_state = self.cleaned_data['iesg_state'] @@ -176,11 +176,8 @@ class EditModelForm(forms.ModelForm): else: m.tags.remove('rfc-rev') - m.time = datetime.datetime.now() # handle replaced by - if commit: - m.save() return m # field must contain filename of existing draft @@ -274,14 +271,12 @@ class RfcModelForm(forms.ModelForm): self.fields['title'].widget = forms.Textarea() self.fields['std_level'].required = True - def save(self, force_insert=False, force_update=False, commit=True): + def save(self, force_insert=False, force_update=False, commit=False): obj = super(RfcModelForm, self).save(commit=False) # create DocAlias DocAlias.objects.create(document=self.instance,name="rfc%d" % self.cleaned_data['rfc_number']) - if commit: - obj.save() return obj def clean_rfc_number(self): diff --git a/ietf/secr/drafts/views.py b/ietf/secr/drafts/views.py index ee0b9c146..39f0d621b 100644 --- a/ietf/secr/drafts/views.py +++ b/ietf/secr/drafts/views.py @@ -14,7 +14,7 @@ from django.template.loader import render_to_string #from email import * from ietf.doc.models import Document, DocumentAuthor, DocAlias, DocRelationshipName, RelatedDocument, State from ietf.doc.models import DocEvent, NewRevisionDocEvent -from ietf.doc.models import save_document_in_history +from ietf.doc.utils import add_state_change_event from ietf.ietfauth.utils import role_required from ietf.meeting.models import Meeting from ietf.meeting.helpers import get_meeting @@ -176,18 +176,17 @@ def do_extend(draft, request): - update revision_date - set extension_date ''' - save_document_in_history(draft) + e = DocEvent.objects.create( + type='changed_document', + by=request.user.person, + doc=draft, + time=draft.time, + desc='Extended expiry', + ) draft.expires = request.session['data']['expiration_date'] - draft.time = datetime.datetime.now() - draft.save() - - DocEvent.objects.create(type='changed_document', - by=request.user.person, - doc=draft, - time=draft.time, - desc='extend_expiry') - + draft.save_with_history([e]) + # save scheduled announcement announcement_from_form(request.session['email'],by=request.user.person) @@ -196,24 +195,28 @@ def do_extend(draft, request): def do_replace(draft, request): 'Perform document replace' - save_document_in_history(draft) - replaced = request.session['data']['replaced'] # a DocAlias replaced_by = request.session['data']['replaced_by'] # a Document - # change state and update last modified - draft.set_state(State.objects.get(type="draft", slug="repl")) - draft.time = datetime.datetime.now() - draft.save() - # create relationship RelatedDocument.objects.create(source=replaced_by, target=replaced, relationship=DocRelationshipName.objects.get(slug='replaces')) - # create DocEvent - # no replace DocEvent at this time, Jan 2012 - + + + draft.set_state(State.objects.get(type="draft", slug="repl")) + + e = DocEvent.objects.create( + type='changed_document', + by=request.user.person, + doc=replaced_by, + time=draft.time, + desc='This document now replaces %s' % request.session['data']['replaced'], + ) + + draft.save_with_history([e]) + # move replaced document to archive archive_draft_files(replaced.document.name + '-' + replaced.document.rev) @@ -243,16 +246,16 @@ def do_resurrect(draft, request): # set expires draft.expires = datetime.datetime.now() + datetime.timedelta(settings.INTERNET_DRAFT_DAYS_TO_EXPIRE) - draft.time = datetime.datetime.now() - draft.save() # create DocEvent - NewRevisionDocEvent.objects.create(type='completed_resurrect', - by=request.user.person, - doc=draft, - rev=draft.rev, - time=draft.time) + e = NewRevisionDocEvent.objects.create(type='completed_resurrect', + by=request.user.person, + doc=draft, + rev=draft.rev, + time=draft.time) + draft.save_with_history([e]) + # send announcement announcement_from_form(request.session['email'],by=request.user.person) @@ -278,12 +281,10 @@ def do_revision(draft, request): # TODO this behavior may change with archive strategy archive_draft_files(draft.name + '-' + draft.rev) - save_document_in_history(draft) - # save form data form = BaseRevisionModelForm(request.session['data'],instance=draft) if form.is_valid(): - new_draft = form.save() + new_draft = form.save(commit=False) else: raise Exception(form.errors) raise Exception('Problem with input data %s' % form.data) @@ -291,16 +292,16 @@ def do_revision(draft, request): # set revision and expires new_draft.rev = request.session['filename'][-2:] new_draft.expires = datetime.datetime.now() + datetime.timedelta(settings.INTERNET_DRAFT_DAYS_TO_EXPIRE) - new_draft.time = datetime.datetime.now() - new_draft.save() - + # create DocEvent - NewRevisionDocEvent.objects.create(type='new_revision', - by=request.user.person, - doc=draft, - rev=new_draft.rev, - desc='New revision available', - time=draft.time) + e = NewRevisionDocEvent.objects.create(type='new_revision', + by=request.user.person, + doc=draft, + rev=new_draft.rev, + desc='New revision available', + time=draft.time) + + new_draft.save_with_history([e]) handle_substate(new_draft) @@ -325,12 +326,10 @@ def do_update(draft,request): - do substate check - change state to Active ''' - save_document_in_history(draft) - # save form data form = BaseRevisionModelForm(request.session['data'],instance=draft) if form.is_valid(): - new_draft = form.save() + new_draft = form.save(commit=False) else: raise Exception('Problem with input data %s' % form.data) @@ -339,19 +338,19 @@ def do_update(draft,request): # update draft record new_draft.rev = os.path.splitext(request.session['data']['filename'])[0][-2:] new_draft.expires = datetime.datetime.now() + datetime.timedelta(settings.INTERNET_DRAFT_DAYS_TO_EXPIRE) - new_draft.time = datetime.datetime.now() - new_draft.save() - + new_draft.set_state(State.objects.get(type="draft", slug="active")) # create DocEvent - NewRevisionDocEvent.objects.create(type='new_revision', - by=request.user.person, - doc=new_draft, - rev=new_draft.rev, - desc='New revision available', - time=new_draft.time) + e = NewRevisionDocEvent.objects.create(type='new_revision', + by=request.user.person, + doc=new_draft, + rev=new_draft.rev, + desc='New revision available', + time=new_draft.time) + new_draft.save_with_history([e]) + # move uploaded files to production directory promote_files(new_draft, request.session['file_type']) @@ -370,15 +369,22 @@ def do_withdraw(draft,request): - TODO move file to archive ''' withdraw_type = request.session['data']['type'] + + prev_state = draft.get_state("draft") + new_state = None if withdraw_type == 'ietf': - draft.set_state(State.objects.get(type="draft", slug="ietf-rm")) + new_state = State.objects.get(type="draft", slug="ietf-rm") elif withdraw_type == 'author': - draft.set_state(State.objects.get(type="draft", slug="auth-rm")) - - draft.time = datetime.datetime.now() - draft.save() - - # no DocEvent ? + new_state = State.objects.get(type="draft", slug="auth-rm") + + if not new_state: + return + + draft.set_state(new_state) + + e = add_state_change_event(draft, request.user.person, prev_state, new_state) + if e: + draft.save_with_history([e]) # send announcement announcement_from_form(request.session['email'],by=request.user.person) @@ -538,8 +544,7 @@ def add(request): draft.rev = revision draft.name = name draft.type_id = 'draft' - draft.time = datetime.datetime.now() - + # set stream based on document name if not draft.stream: stream_slug = None @@ -556,7 +561,7 @@ def add(request): # set expires draft.expires = datetime.datetime.now() + datetime.timedelta(settings.INTERNET_DRAFT_DAYS_TO_EXPIRE) - draft.save() + draft.save(force_insert=True) # set state draft.set_state(State.objects.get(type="draft", slug="active")) @@ -792,13 +797,13 @@ def edit(request, id): form = EditModelForm(request.POST, instance=draft) if form.is_valid(): if form.changed_data: - save_document_in_history(draft) - DocEvent.objects.create(type='changed_document', - by=request.user.person, - doc=draft, - desc='Changed field(s): %s' % ','.join(form.changed_data)) + e = DocEvent.objects.create(type='changed_document', + by=request.user.person, + doc=draft, + desc='Changed field(s): %s' % ','.join(form.changed_data)) # see EditModelForm.save() for detailed logic - form.save() + form.save(commit=False) + draft.save_with_history([e]) messages.success(request, 'Draft modified successfully!') @@ -924,16 +929,16 @@ def makerfc(request, id): if form.is_valid() and obs_formset.is_valid(): # TODO - save_document_in_history(draft) archive_draft_files(draft.name + '-' + draft.rev) - rfc = form.save() + rfc = form.save(commit=False) # create DocEvent - DocEvent.objects.create(type='published_rfc', - by=request.user.person, - doc=rfc) - + e = DocEvent.objects.create(type='published_rfc', + by=request.user.person, + doc=rfc, + desc="Published RFC") + # change state draft.set_state(State.objects.get(type="draft", slug="rfc")) @@ -950,7 +955,9 @@ def makerfc(request, id): RelatedDocument.objects.create(source=draft, target=target, relationship=DocRelationshipName.objects.get(slug=relation)) - + + rfc.save_with_history([e]) + messages.success(request, 'RFC created successfully!') return redirect('drafts_view', id=id) else: diff --git a/ietf/secr/proceedings/views.py b/ietf/secr/proceedings/views.py index d25bc5a2f..78bc44002 100644 --- a/ietf/secr/proceedings/views.py +++ b/ietf/secr/proceedings/views.py @@ -25,6 +25,7 @@ from ietf.secr.utils.group import get_my_groups, groups_by_session from ietf.secr.utils.meeting import get_upload_root, get_materials, get_timeslot, get_proceedings_path, get_proceedings_url from ietf.doc.models import Document, DocAlias, DocEvent, State, NewRevisionDocEvent from ietf.group.models import Group +from ietf.person.models import Person from ietf.ietfauth.utils import has_role, role_required from ietf.meeting.models import Meeting, Session, TimeSlot, ScheduledSession from ietf.secr.proceedings.forms import EditSlideForm, InterimMeetingForm, RecordingForm, RecordingEditForm, ReplaceSlideForm, UnifiedUploadForm @@ -212,7 +213,14 @@ def post_process(doc): # change extension base,ext = os.path.splitext(doc.external_url) doc.external_url = base + '.pdf' - doc.save() + + e = DocEvent.objects.create( + type='changed_document', + by=Person.objects.get(name="(System)"), + doc=doc, + desc='Converted document to PDF', + ) + doc.save_with_history([e]) # ------------------------------------------------- # AJAX Functions @@ -362,7 +370,8 @@ def delete_material(request,slide_id): # create deleted_document DocEvent.objects.create(doc=doc, by=request.user.person, - type='deleted') + type='deleted', + desc="State set to deleted") create_proceedings(meeting,group) @@ -582,7 +591,13 @@ def process_pdfs(request, meeting_num): path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting_num,'slides',pdf_file) if os.path.exists(path): doc.external_url = pdf_file - doc.save() + e = DocEvent.objects.create( + type='changed_document', + by=Person.objects.get(name="(System)"), + doc=doc, + desc='Set URL to PDF version', + ) + doc.save_with_history([e]) count += 1 else: warn_count += 1 @@ -658,7 +673,15 @@ def recording_edit(request, meeting_num, name): form = RecordingEditForm(request.POST, instance=recording) if form.is_valid(): # save record and rebuild proceedings - form.save() + form.save(commit=False) + e = DocEvent.objects.create( + type='changed_document', + by=request.user.person, + doc=recording, + desc=u'Changed URL to %s' % recording.external_url, + ) + recording.save_with_history([e]) + create_proceedings(meeting,recording.group) messages.success(request,'Recording saved') return redirect('proceedings_recording', meeting_num=meeting_num) @@ -704,13 +727,15 @@ def replace_slide(request, slide_id): handle_upload_file(file,disk_filename,meeting,'slides') new_slide.external_url = disk_filename - new_slide.save() - post_process(new_slide) - + # create DocEvent uploaded - DocEvent.objects.create(doc=slide, - by=request.user.person, - type='uploaded') + e = DocEvent.objects.create(doc=slide, + by=request.user.person, + type='uploaded', + desc="Uploaded") + new_slide.save_with_history([e]) + + post_process(new_slide) # rebuild proceedings.html create_proceedings(meeting,group) @@ -936,7 +961,6 @@ def upload_unified(request, meeting_num, acronym=None, session_id=None): doc.title = doc.name else: doc.title = '%s for %s at %s' % (material_type.slug.capitalize(), group.acronym.upper(), meeting) - doc.save() DocAlias.objects.get_or_create(name=doc.name, document=doc) @@ -960,12 +984,13 @@ def upload_unified(request, meeting_num, acronym=None, session_id=None): s.sessionpresentation_set.create(document=doc,rev=doc.rev) # create NewRevisionDocEvent instead of uploaded, per Ole - NewRevisionDocEvent.objects.create(type='new_revision', - by=request.user.person, - doc=doc, - rev=doc.rev, - desc='New revision available', - time=now) + e = NewRevisionDocEvent.objects.create(type='new_revision', + by=request.user.person, + doc=doc, + rev=doc.rev, + desc='New revision available') + + doc.save_with_history([e]) post_process(doc) create_proceedings(meeting,group) diff --git a/ietf/secr/telechat/views.py b/ietf/secr/telechat/views.py index c3f2be7df..750daae51 100644 --- a/ietf/secr/telechat/views.py +++ b/ietf/secr/telechat/views.py @@ -6,7 +6,7 @@ from django.forms.formsets import formset_factory from django.shortcuts import render_to_response, get_object_or_404, redirect from django.template import RequestContext -from ietf.doc.models import DocEvent, Document, BallotDocEvent, BallotPositionDocEvent, WriteupDocEvent, save_document_in_history +from ietf.doc.models import DocEvent, Document, BallotDocEvent, BallotPositionDocEvent, WriteupDocEvent from ietf.doc.utils import get_document_content, add_state_change_event from ietf.person.models import Person from ietf.doc.lastcall import request_last_call @@ -247,8 +247,6 @@ def doc_detail(request, date, name): new_tags = [tag] if tag else [] if state_form.changed_data: - save_document_in_history(doc) - if 'state' in state_form.changed_data: doc.set_state(new_state) @@ -258,8 +256,8 @@ def doc_detail(request, date, name): e = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags) - doc.time = (e and e.time) or datetime.datetime.now() - doc.save() + if e: + doc.save_with_history([e]) email_state_changed(request, doc, e.desc) email_ad(request, doc, doc.ad, login, e.desc) diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index 939d1b185..0a3461335 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -5,7 +5,7 @@ import datetime from django.conf import settings from ietf.doc.models import Document, State, DocAlias, DocEvent, DocumentAuthor -from ietf.doc.models import NewRevisionDocEvent, save_document_in_history +from ietf.doc.models import NewRevisionDocEvent from ietf.doc.models import RelatedDocument, DocRelationshipName from ietf.doc.utils import add_state_change_event, rebuild_reference_relations from ietf.doc.utils import set_replaces_for_document @@ -126,19 +126,23 @@ def create_submission_event(request, submission, desc): def post_submission(request, submission): + # find out who did it system = Person.objects.get(name="(System)") + submitter_parsed = submission.submitter_parsed() + if submitter_parsed["name"] and submitter_parsed["email"]: + submitter = ensure_person_email_info_exists(submitter_parsed["name"], submitter_parsed["email"]).person + else: + submitter = system + # update draft attributes try: draft = Document.objects.get(name=submission.name) - save_document_in_history(draft) except Document.DoesNotExist: - draft = Document(name=submission.name) - draft.intended_std_level = None + draft = Document.objects.create(name=submission.name, type_id="draft") prev_rev = draft.rev draft.type_id = "draft" - draft.time = datetime.datetime.now() draft.title = submission.title group = submission.group or Group.objects.get(type="individ") if not (group.type_id == "individ" and draft.group and draft.group.type_id == "area"): @@ -163,16 +167,23 @@ def post_submission(request, submission): draft.stream = StreamName.objects.get(slug=stream_slug) draft.expires = datetime.datetime.now() + datetime.timedelta(settings.INTERNET_DRAFT_DAYS_TO_EXPIRE) - draft.save() - submitter_parsed = submission.submitter_parsed() - if submitter_parsed["name"] and submitter_parsed["email"]: - submitter = ensure_person_email_info_exists(submitter_parsed["name"], submitter_parsed["email"]).person - else: - submitter = system + events = [] + + # new revision event + e = NewRevisionDocEvent.objects.create( + type="new_revision", + doc=draft, + rev=draft.rev, + by=submitter, + desc="New version available: %s-%s.txt" % (draft.name, draft.rev), + ) + events.append(e) + + # update related objects + DocAlias.objects.get_or_create(name=submission.name, document=draft) draft.set_state(State.objects.get(used=True, type="draft", slug="active")) - DocAlias.objects.get_or_create(name=submission.name, document=draft) update_authors(draft, submission) @@ -180,29 +191,15 @@ def post_submission(request, submission): if trouble: log('Rebuild_reference_relations trouble: %s'%trouble) - # new revision event - e = NewRevisionDocEvent(type="new_revision", doc=draft, rev=draft.rev) - e.time = draft.time #submission.submission_date - e.by = submitter - e.desc = "New version available: %s-%s.txt" % (draft.name, draft.rev) - e.save() - - if draft.stream_id == "ietf" and draft.group.type_id == "wg" and draft.rev == "00": - # automatically set state "WG Document" - draft.set_state(State.objects.get(used=True, type="draft-stream-%s" % draft.stream_id, slug="wg-doc")) - + # automatic state changes if draft.get_state_slug("draft-iana-review") in ("ok-act", "ok-noact", "not-ok"): prev_state = draft.get_state("draft-iana-review") next_state = State.objects.get(used=True, type="draft-iana-review", slug="changed") draft.set_state(next_state) - add_state_change_event(draft, submitter, prev_state, next_state) + e = add_state_change_event(draft, submitter, prev_state, next_state) + if e: + events.append(e) - # clean up old files - if prev_rev != draft.rev: - from ietf.doc.expire import move_draft_files_to_archive - move_draft_files_to_archive(draft, prev_rev) - - # automatic state changes state_change_msg = "" if not was_rfc and draft.tags.filter(slug="need-rev"): @@ -213,9 +210,22 @@ def post_submission(request, submission): e.desc = "Sub state has been changed to AD Followup from Revised ID Needed" e.by = system e.save() + events.append(e) state_change_msg = e.desc + if draft.stream_id == "ietf" and draft.group.type_id == "wg" and draft.rev == "00": + # automatically set state "WG Document" + draft.set_state(State.objects.get(used=True, type="draft-stream-%s" % draft.stream_id, slug="wg-doc")) + + # save history now that we're done with changes to the draft itself + draft.save_with_history(events) + + # clean up old files + if prev_rev != draft.rev: + from ietf.doc.expire import move_draft_files_to_archive + move_draft_files_to_archive(draft, prev_rev) + move_files_to_repository(submission) submission.state = DraftSubmissionStateName.objects.get(slug="posted") diff --git a/ietf/sync/iana.py b/ietf/sync/iana.py index 3772ecfca..72548a8fd 100644 --- a/ietf/sync/iana.py +++ b/ietf/sync/iana.py @@ -9,7 +9,7 @@ from django.utils.http import urlquote from django.conf import settings from ietf.doc.mails import email_ad, email_state_changed -from ietf.doc.models import Document, DocEvent, State, StateDocEvent, StateType, save_document_in_history +from ietf.doc.models import Document, DocEvent, State, StateDocEvent, StateType from ietf.doc.utils import add_state_change_event from ietf.person.models import Person from ietf.utils.timezone import local_timezone_to_utc, email_time_to_local_timezone, utc_to_local_timezone @@ -201,16 +201,15 @@ def update_history_with_changes(changes, send_email=True): added_events.append(e) if not StateDocEvent.objects.filter(doc=doc, time__gt=timestamp, state_type=state_type): - save_document_in_history(doc) doc.set_state(state) + if e: + doc.save_with_history([e]) + if send_email and (state != prev_state): email_state_changed(None, doc, "IANA %s state changed to %s" % (kind, state.name)) email_ad(None, doc, doc.ad, system, "IANA %s state changed to %s" % (kind, state.name)) - if doc.time < timestamp: - doc.time = timestamp - doc.save() return added_events, warnings diff --git a/ietf/sync/rfceditor.py b/ietf/sync/rfceditor.py index 32ecc3838..0772996dc 100644 --- a/ietf/sync/rfceditor.py +++ b/ietf/sync/rfceditor.py @@ -9,9 +9,9 @@ from xml.dom import pulldom, Node from django.conf import settings from ietf.doc.models import ( Document, DocAlias, State, StateType, DocEvent, DocRelationshipName, - DocTagName, DocTypeName, RelatedDocument, save_document_in_history ) + DocTagName, DocTypeName, RelatedDocument ) from ietf.doc.expire import move_draft_files_to_archive -from ietf.doc.utils import add_state_change_event +from ietf.doc.utils import add_state_change_event, prettify_std_name from ietf.group.models import Group from ietf.name.models import StdLevelName, StreamName from ietf.person.models import Person @@ -37,6 +37,8 @@ def fetch_queue_xml(url): return urllib2.urlopen(url) def parse_queue(response): + """Parse RFC Editor queue XML into a bunch of tuples + warnings.""" + events = pulldom.parse(response) drafts = [] warnings = [] @@ -108,6 +110,9 @@ def parse_queue(response): return drafts, warnings def update_drafts_from_queue(drafts): + """Given a list of parsed drafts from the RFC Editor queue, update the + documents in the database. Return those that were changed.""" + tag_mapping = { 'IANA': DocTagName.objects.get(slug='iana'), 'REF': DocTagName.objects.get(slug='ref') @@ -154,6 +159,7 @@ def update_drafts_from_queue(drafts): prev_state = d.get_state("draft-rfceditor") next_state = state_mapping[state] + events = [] # check if we've noted it's been received if d.get_state_slug("draft-iesg") == "ann" and not prev_state and not d.latest_event(DocEvent, type="rfc_editor_received_announcement"): @@ -166,15 +172,15 @@ def update_drafts_from_queue(drafts): # change draft-iesg state to RFC Ed Queue prev_iesg_state = State.objects.get(used=True, type="draft-iesg", slug="ann") next_iesg_state = State.objects.get(used=True, type="draft-iesg", slug="rfcqueue") - save_document_in_history(d) + d.set_state(next_iesg_state) - add_state_change_event(d, system, prev_iesg_state, next_iesg_state) + e = add_state_change_event(d, system, prev_iesg_state, next_iesg_state) + if e: + events.append(e) changed.add(name) # check draft-rfceditor state if prev_state != next_state: - save_document_in_history(d) - d.set_state(next_state) e = add_state_change_event(d, system, prev_state, next_state) @@ -183,6 +189,9 @@ def update_drafts_from_queue(drafts): e.desc = re.sub(r"(.*)", "\\1" % auth48, e.desc) e.save() + if e: + events.append(e) + changed.add(name) t = DocTagName.objects.filter(slug__in=tags) @@ -190,6 +199,9 @@ def update_drafts_from_queue(drafts): d.tags = t changed.add(name) + if events: + d.save_with_history(events) + # remove tags and states for those not in the queue anymore for d in Document.objects.exclude(docalias__name__in=names).filter(states__type="draft-rfceditor").distinct(): @@ -207,6 +219,8 @@ def fetch_index_xml(url): return urllib2.urlopen(url) def parse_index(response): + """Parse RFC Editor index XML into a bunch of tuples.""" + def normalize_std_name(std_name): # remove zero padding prefix = std_name[:3] @@ -297,6 +311,10 @@ def parse_index(response): def update_docs_from_rfc_index(data, skip_older_than_date=None): + """Given parsed data from the RFC Editor index, update the documents + in the database. Yields a list of change descriptions for each + document, if any.""" + std_level_mapping = { "Standard": StdLevelName.objects.get(slug="std"), "Internet Standard": StdLevelName.objects.get(slug="std"), @@ -323,9 +341,6 @@ def update_docs_from_rfc_index(data, skip_older_than_date=None): system = Person.objects.get(name="(System)") - results = [] - new_rfcs = [] - for rfc_number, title, authors, rfc_published_date, current_status, updates, updated_by, obsoletes, obsoleted_by, also, draft, has_errata, stream, wg, file_formats, pages, abstract in data: if skip_older_than_date and rfc_published_date < skip_older_than_date: @@ -335,6 +350,9 @@ def update_docs_from_rfc_index(data, skip_older_than_date=None): # we assume two things can happen: we get a new RFC, or an # attribute has been updated at the RFC Editor (RFC Editor # attributes take precedence over our local attributes) + events = [] + changes = [] + rfc_published = False # make sure we got the document and alias doc = None @@ -350,42 +368,45 @@ def update_docs_from_rfc_index(data, skip_older_than_date=None): pass if not doc: - results.append("created document %s" % name) + changes.append("created document %s" % prettify_std_name(name)) doc = Document.objects.create(name=name, type=DocTypeName.objects.get(slug="draft")) # add alias DocAlias.objects.get_or_create(name=name, document=doc) - results.append("created alias %s to %s" % (name, doc.name)) + changes.append("created alias %s" % prettify_std_name(name)) # check attributes - changed_attributes = {} - changed_states = [] - created_relations = [] - other_changes = False if title != doc.title: - changed_attributes["title"] = title + doc.title = title + changes.append("changed title to '%s'" % doc.title) if abstract and abstract != doc.abstract: - changed_attributes["abstract"] = abstract + doc.abstract = abstract + changes.append("changed abstract to '%s'" % doc.abstract) if pages and int(pages) != doc.pages: - changed_attributes["pages"] = int(pages) + doc.pages = int(pages) + changes.append("changed pages to %s" % doc.pages) if std_level_mapping[current_status] != doc.std_level: - changed_attributes["std_level"] = std_level_mapping[current_status] + doc.std_level = std_level_mapping[current_status] + changes.append("changed standardization level to %s" % doc.std_level) if doc.get_state_slug() != "rfc": - changed_states.append(State.objects.get(used=True, type="draft", slug="rfc")) + doc.set_state(State.objects.get(used=True, type="draft", slug="rfc")) move_draft_files_to_archive(doc, doc.rev) + changes.append("changed state to %s" % doc.get_state()) if doc.stream != stream_mapping[stream]: - changed_attributes["stream"] = stream_mapping[stream] + doc.stream = stream_mapping[stream] + changes.append("changed stream to %s" % doc.stream) if not doc.group: # if we have no group assigned, check if RFC Editor has a suggestion if wg: - changed_attributes["group"] = Group.objects.get(acronym=wg) + doc.group = Group.objects.get(acronym=wg) + changes.append("set group to %s" % doc.group) else: - changed_attributes["group"] = Group.objects.get(type="individ") + doc.group = Group.objects.get(type="individ") # fallback for newly created doc if not doc.latest_event(type="published_rfc"): e = DocEvent(doc=doc, type="published_rfc") @@ -404,15 +425,17 @@ def update_docs_from_rfc_index(data, skip_older_than_date=None): e.by = system e.desc = "RFC published" e.save() - other_changes = True + events.append(e) - results.append("Added RFC published event: %s" % e.time.strftime("%Y-%m-%d")) - new_rfcs.append(doc) + changes.append("added RFC published event at %s" % e.time.strftime("%Y-%m-%d")) + rfc_published = True for t in ("draft-iesg", "draft-stream-iab", "draft-stream-irtf", "draft-stream-ise"): slug = doc.get_state_slug(t) if slug and slug != "pub": - changed_states.append(State.objects.get(used=True, type=t, slug="pub")) + new_state = State.objects.select_related("type").get(used=True, type=t, slug="pub") + doc.set_state(new_state) + changes.append("changed %s to %s" % (new_state.type.label, new_state)) def parse_relation_list(l): res = [] @@ -431,46 +454,42 @@ def update_docs_from_rfc_index(data, skip_older_than_date=None): for x in parse_relation_list(obsoletes): if not RelatedDocument.objects.filter(source=doc, target=x, relationship=relationship_obsoletes): - created_relations.append(RelatedDocument(source=doc, target=x, relationship=relationship_obsoletes)) + r = RelatedDocument.objects.create(RelatedDocument(source=doc, target=x, relationship=relationship_obsoletes)) + changes.append("created %s relation between %s and %s" % (r.relationship.name.lower(), prettify_std_name(r.source.name), prettify_std_name(r.target.name))) for x in parse_relation_list(updates): if not RelatedDocument.objects.filter(source=doc, target=x, relationship=relationship_updates): - created_relations.append(RelatedDocument(source=doc, target=x, relationship=relationship_updates)) + r = RelatedDocument.objects.create(source=doc, target=x, relationship=relationship_updates) + changes.append("created %s relation between %s and %s" % (r.relationship.name.lower(), prettify_std_name(r.source.name), prettify_std_name(r.target.name))) if also: for a in also: a = a.lower() if not DocAlias.objects.filter(name=a): DocAlias.objects.create(name=a, document=doc) - other_changes = True - results.append("Created alias %s to %s" % (a, doc.name)) + changes.append("created alias %s" % prettify_std_name(a)) if has_errata: if not doc.tags.filter(pk=tag_has_errata.pk): - changed_attributes["tags"] = list(doc.tags.all()) + [tag_has_errata] + doc.tags.add(tag_has_errata) + changes.append("added Errata tag") else: if doc.tags.filter(pk=tag_has_errata.pk): - changed_attributes["tags"] = set(doc.tags.all()) - set([tag_has_errata]) + doc.tags.remove(tag_has_errata) + changes.append("removed Errata tag") - if changed_attributes or changed_states or created_relations or other_changes: - # apply changes - save_document_in_history(doc) - for k, v in changed_attributes.iteritems(): - setattr(doc, k, v) - results.append("Changed %s to %s on %s" % (k, v, doc.name)) + if changes: + events.append(DocEvent.objects.create( + doc=doc, + by=system, + type="sync_from_rfc_editor", + desc=u"Received changes through RFC Editor sync (%s)" % u", ".join(changes), + )) - for s in changed_states: - doc.set_state(s) - results.append("Set state %s on %s" % (s, doc.name)) + doc.save_with_history(events) - for o in created_relations: - o.save() - results.append("Created %s" % o) - - doc.time = datetime.datetime.now() - doc.save() - - return results, new_rfcs + if changes: + yield changes, doc, rfc_published def post_approved_draft(url, name): diff --git a/ietf/sync/tests.py b/ietf/sync/tests.py index c84a04109..108af7457 100644 --- a/ietf/sync/tests.py +++ b/ietf/sync/tests.py @@ -325,12 +325,15 @@ class RFCSyncTests(TestCase): draft_filename = "%s-%s.txt" % (doc.name, doc.rev) self.write_draft_file(draft_filename, 5000) - changed,_ = rfceditor.update_docs_from_rfc_index(data, today - datetime.timedelta(days=30)) + changes = [] + for cs, d, rfc_published in rfceditor.update_docs_from_rfc_index(data, today - datetime.timedelta(days=30)): + changes.append(cs) doc = Document.objects.get(name=doc.name) - self.assertEqual(doc.docevent_set.all()[0].type, "published_rfc") - self.assertEqual(doc.docevent_set.all()[0].time.date(), today) + self.assertEqual(doc.docevent_set.all()[0].type, "sync_from_rfc_editor") + self.assertEqual(doc.docevent_set.all()[1].type, "published_rfc") + self.assertEqual(doc.docevent_set.all()[1].time.date(), today) self.assertTrue("errata" in doc.tags.all().values_list("slug", flat=True)) self.assertTrue(DocAlias.objects.filter(name="rfc1234", document=doc)) self.assertTrue(DocAlias.objects.filter(name="bcp1", document=doc)) @@ -348,7 +351,7 @@ class RFCSyncTests(TestCase): self.assertTrue(os.path.exists(os.path.join(self.archive_dir, draft_filename))) # make sure we can apply it again with no changes - changed,_ = rfceditor.update_docs_from_rfc_index(data, today - datetime.timedelta(days=30)) + changed = list(rfceditor.update_docs_from_rfc_index(data, today - datetime.timedelta(days=30))) self.assertEqual(len(changed), 0) diff --git a/ietf/sync/views.py b/ietf/sync/views.py index ac46eebf2..c6a9ae0e3 100644 --- a/ietf/sync/views.py +++ b/ietf/sync/views.py @@ -4,14 +4,13 @@ import os import json from django.http import HttpResponse, HttpResponseForbidden, HttpResponseServerError, HttpResponseRedirect, Http404 -from django.shortcuts import render_to_response -from django.template import RequestContext +from django.shortcuts import render from django.conf import settings from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.views.decorators.csrf import csrf_exempt -from ietf.doc.models import DeletedEvent, StateDocEvent +from ietf.doc.models import DeletedEvent, StateDocEvent, DocEvent from ietf.ietfauth.utils import role_required, has_role from ietf.sync.discrepancies import find_discrepancies from ietf.utils.serialize import object_as_shallow_dict @@ -22,9 +21,7 @@ SYNC_BIN_PATH = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__f def discrepancies(request): sections = find_discrepancies() - return render_to_response("sync/discrepancies.html", - dict(sections=sections), - context_instance=RequestContext(request)) + return render(request, "sync/discrepancies.html", dict(sections=sections)) @csrf_exempt # external API so we can't expect the other end to have a token def notify(request, org, notification): @@ -100,19 +97,27 @@ def notify(request, org, notification): else: return HttpResponse("OK", content_type="text/plain; charset=%s"%settings.DEFAULT_CHARSET) - return render_to_response('sync/notify.html', - dict(org=known_orgs[org], - notification=notification, - help_text=known_notifications[notification], - ), - context_instance=RequestContext(request)) + return render(request, 'sync/notify.html', + dict(org=known_orgs[org], + notification=notification, + help_text=known_notifications[notification], + )) @role_required('Secretariat', 'RFC Editor') def rfceditor_undo(request): """Undo a DocEvent.""" - events = StateDocEvent.objects.filter(state_type="draft-rfceditor", - time__gte=datetime.datetime.now() - datetime.timedelta(weeks=1) - ).order_by("-time", "-id") + events = [] + events.extend(StateDocEvent.objects.filter( + state_type="draft-rfceditor", + time__gte=datetime.datetime.now() - datetime.timedelta(weeks=1) + ).order_by("-time", "-id")) + + events.extend(DocEvent.objects.filter( + type="sync_from_rfc_editor", + time__gte=datetime.datetime.now() - datetime.timedelta(weeks=1) + ).order_by("-time", "-id")) + + events.sort(key=lambda e: (e.time, e.id), reverse=True) if request.method == "POST": try: @@ -120,9 +125,10 @@ def rfceditor_undo(request): except ValueError: return HttpResponse("Could not parse event id") - try: - e = events.get(id=eid) - except StateDocEvent.DoesNotExist: + for e in events: + if e.id == eid: + break + else: return HttpResponse("Event does not exist") doc = e.doc @@ -145,7 +151,4 @@ def rfceditor_undo(request): return HttpResponseRedirect("") - return render_to_response('sync/rfceditor_undo.html', - dict(events=events, - ), - context_instance=RequestContext(request)) + return render(request, 'sync/rfceditor_undo.html', dict(events=events)) diff --git a/ietf/templates/doc/submit_to_iesg_email.txt b/ietf/templates/doc/submit_to_iesg_email.txt index 807cdd351..dd62f1762 100644 --- a/ietf/templates/doc/submit_to_iesg_email.txt +++ b/ietf/templates/doc/submit_to_iesg_email.txt @@ -1,4 +1,4 @@ -{{login}} has requested publication of {{doc.name}}-{{doc.rev}} as {{doc.intended_std_level}} on behalf of the {{doc.group.acronym|upper}} working group. +{{by}} has requested publication of {{doc.name}}-{{doc.rev}} as {{doc.intended_std_level}} on behalf of the {{doc.group.acronym|upper}} working group. Please verify the document's state at {{url}} diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index 4a5790765..20dce0c1a 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -288,10 +288,8 @@ def make_test_data(): 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',rev='00') + doc = Document.objects.create(name='draft-imaginary-irtf-submission', type_id='draft',rev='00', stream=StreamName.objects.get(slug='irtf')) docalias = DocAlias.objects.create(name=doc.name, document=doc) - doc.stream = StreamName.objects.get(slug='irtf') - doc.save() 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") DocAlias.objects.create(name=crdoc.name, document=crdoc) @@ -302,15 +300,12 @@ def make_test_data(): 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) doc.set_state(State.objects.get(slug='needshep',type__slug='statchg')) - doc.save() docalias = DocAlias.objects.create(name='status-change-imaginary-mid-review',document=doc) # 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) + 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')) - target_rfc.notify = "%s@ietf.org"%name - target_rfc.save() docalias = DocAlias.objects.create(name=name,document=target_rfc) docalias = DocAlias.objects.create(name='rfc%d'%rfc_num,document=target_rfc) # pyflakes:ignore return target_rfc