Skip to content

Commit 86c3a43

Browse files
committed
Merge in ^/branch/iola/event-saving-refactor-r10076, fixing a few problems
- Legacy-Id: 10298
2 parents e3ea4d8 + 73a41bb commit 86c3a43

79 files changed

Lines changed: 1557 additions & 1196 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bin/test-crawl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env python
22

3-
import os, sys, re, datetime, argparse, traceback, tempfile, json, subprocess
3+
import os, sys, re, datetime, argparse, traceback, json, subprocess
44
import html5lib
55
import random
66

ietf/bin/rfc-editor-index-updates

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env python
22

3-
import os, sys, re, json, datetime
3+
import os, sys, datetime
44
import syslog
55
import traceback
66

@@ -29,22 +29,28 @@ if options.skip_date:
2929
skip_date = datetime.datetime.strptime(options.skip_date, "%Y-%m-%d").date()
3030

3131
from ietf.utils.pipe import pipe
32-
from ietf.sync.rfceditor import *
3332
from ietf.doc.utils import rebuild_reference_relations
33+
import ietf.sync.rfceditor
3434

3535
syslog.syslog("Updating document metadata from RFC index from %s" % settings.RFC_EDITOR_QUEUE_URL)
3636

37-
response = fetch_index_xml(settings.RFC_EDITOR_INDEX_URL)
38-
data = parse_index(response)
37+
response = ietf.sync.rfceditor.fetch_index_xml(settings.RFC_EDITOR_INDEX_URL)
38+
data = ietf.sync.rfceditor.parse_index(response)
3939

40-
if len(data) < MIN_INDEX_RESULTS:
40+
if len(data) < ietf.sync.rfceditor.MIN_INDEX_RESULTS:
4141
syslog.syslog("Not enough results, only %s" % len(data))
4242
sys.exit(1)
4343

44-
changed, new_rfcs = update_docs_from_rfc_index(data, skip_older_than_date=skip_date)
44+
new_rfcs = []
45+
for changes, doc, rfc_published in ietf.sync.rfceditor.update_docs_from_rfc_index(data, skip_older_than_date=skip_date):
46+
if rfc_published:
47+
new_rfcs.append(doc)
4548

46-
for c in changed:
47-
syslog.syslog(c)
49+
for c in changes:
50+
syslog.syslog("%s: %s" % (doc.name, c))
51+
print "%s: %s" % (doc.name, c)
52+
53+
sys.exit(0)
4854

4955
# This can be called while processing a notifying POST from the RFC Editor
5056
# Spawn a child to sync the rfcs and calculate new reference relationships

ietf/doc/expire.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pathlib import Path
77

88
from ietf.utils.mail import send_mail
9-
from ietf.doc.models import Document, DocEvent, State, save_document_in_history, IESG_SUBSTATE_TAGS
9+
from ietf.doc.models import Document, DocEvent, State, IESG_SUBSTATE_TAGS
1010
from ietf.person.models import Person
1111
from ietf.meeting.models import Meeting
1212
from ietf.doc.utils import add_state_change_event
@@ -129,8 +129,9 @@ def expire_draft(doc):
129129

130130
system = Person.objects.get(name="(System)")
131131

132+
events = []
133+
132134
# change the state
133-
save_document_in_history(doc)
134135
if doc.latest_event(type='started_iesg_process'):
135136
new_state = State.objects.get(used=True, type="draft-iesg", slug="dead")
136137
prev_state = doc.get_state(new_state.type_id)
@@ -139,15 +140,13 @@ def expire_draft(doc):
139140
doc.set_state(new_state)
140141
doc.tags.remove(*prev_tags)
141142
e = add_state_change_event(doc, system, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
143+
if e:
144+
events.append(e)
142145

143-
e = DocEvent(doc=doc, by=system)
144-
e.type = "expired_document"
145-
e.desc = "Document has expired"
146-
e.save()
146+
events.append(DocEvent.objects.create(doc=doc, by=system, type="expired_document", desc="Document has expired"))
147147

148148
doc.set_state(State.objects.get(used=True, type="draft", slug="expired"))
149-
doc.time = datetime.datetime.now()
150-
doc.save()
149+
doc.save_with_history(events)
151150

152151
def clean_up_draft_files():
153152
"""Move unidentified and old files out of the Internet Draft directory."""

ietf/doc/lastcall.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from django.db.models import Q
66

77
from ietf.doc.models import Document, State, DocEvent, LastCallDocEvent, WriteupDocEvent
8-
from ietf.doc.models import save_document_in_history
98
from ietf.doc.models import IESG_SUBSTATE_TAGS
109
from ietf.person.models import Person
1110
from ietf.doc.utils import add_state_change_event
@@ -14,12 +13,15 @@
1413

1514
def request_last_call(request, doc):
1615
if not doc.latest_event(type="changed_ballot_writeup_text"):
17-
generate_ballot_writeup(request, doc)
16+
e = generate_ballot_writeup(request, doc)
17+
e.save()
1818
if not doc.latest_event(type="changed_ballot_approval_text"):
19-
generate_approval_mail(request, doc)
19+
e = generate_approval_mail(request, doc)
20+
e.save()
2021
if not doc.latest_event(type="changed_last_call_text"):
21-
generate_last_call_announcement(request, doc)
22-
22+
e = generate_last_call_announcement(request, doc)
23+
e.save()
24+
2325
send_last_call_request(request, doc)
2426

2527
e = DocEvent()
@@ -50,8 +52,6 @@ def expire_last_call(doc):
5052
else:
5153
raise ValueError("Unexpected document type to expire_last_call(): %s" % doc.type)
5254

53-
save_document_in_history(doc)
54-
5555
prev_state = doc.get_state(new_state.type_id)
5656
doc.set_state(new_state)
5757

@@ -60,8 +60,7 @@ def expire_last_call(doc):
6060

6161
system = Person.objects.get(name="(System)")
6262
e = add_state_change_event(doc, system, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
63-
64-
doc.time = (e and e.time) or datetime.datetime.now()
65-
doc.save()
63+
if e:
64+
doc.save_with_history([e])
6665

6766
email_last_call_expired(doc)

ietf/doc/mails.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,21 @@ def email_iesg_processing_document(request, doc, changes):
7878
def html_to_text(html):
7979
return strip_tags(html.replace("&lt;", "<").replace("&gt;", ">").replace("&amp;", "&").replace("<br>", "\n"))
8080

81+
def email_update_telechat(request, doc, text):
82+
(to, cc) = gather_address_lists('doc_telechat_details_changed',doc=doc)
83+
84+
if not to:
85+
return
86+
87+
text = strip_tags(text)
88+
send_mail(request, to, None,
89+
"Telechat update notice: %s" % doc.file_tag(),
90+
"doc/mail/update_telechat.txt",
91+
dict(text=text,
92+
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()),
93+
cc=cc)
94+
95+
8196
def generate_ballot_writeup(request, doc):
8297
e = doc.latest_event(type="iana_review")
8398
iana = e.desc if e else ""
@@ -88,8 +103,8 @@ def generate_ballot_writeup(request, doc):
88103
e.doc = doc
89104
e.desc = u"Ballot writeup was generated"
90105
e.text = unicode(render_to_string("doc/mail/ballot_writeup.txt", {'iana': iana}))
91-
e.save()
92-
106+
107+
# caller is responsible for saving, if necessary
93108
return e
94109

95110
def generate_last_call_announcement(request, doc):
@@ -130,8 +145,8 @@ def generate_last_call_announcement(request, doc):
130145
e.doc = doc
131146
e.desc = u"Last call announcement was generated"
132147
e.text = unicode(mail)
133-
e.save()
134148

149+
# caller is responsible for saving, if necessary
135150
return e
136151

137152

@@ -149,8 +164,8 @@ def generate_approval_mail(request, doc):
149164
e.doc = doc
150165
e.desc = u"Ballot approval text was generated"
151166
e.text = unicode(mail)
152-
e.save()
153167

168+
# caller is responsible for saving, if necessary
154169
return e
155170

156171
def generate_approval_mail_approved(request, doc):
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import migrations
5+
6+
def fix_buggy_author_foreignkey(apps, schema_editor):
7+
DocumentAuthor = apps.get_model("doc", "DocumentAuthor")
8+
# apparently, we have a buggy key in the DB, fix it
9+
DocumentAuthor.objects.filter(author="[<Email: d3e3e3@gmail.com>]").update(author="d3e3e3@gmail.com")
10+
11+
def save_all_documents_in_history(apps, schema_editor):
12+
State = apps.get_model("doc", "State")
13+
Document = apps.get_model("doc", "Document")
14+
DocHistory = apps.get_model("doc", "DocHistory")
15+
RelatedDocument = apps.get_model("doc", "RelatedDocument")
16+
RelatedDocHistory = apps.get_model("doc", "RelatedDocHistory")
17+
DocumentAuthor = apps.get_model("doc", "DocumentAuthor")
18+
DocHistoryAuthor = apps.get_model("doc", "DocHistoryAuthor")
19+
20+
def canonical_name(self):
21+
name = self.name
22+
state = State.objects.filter(document=self, type_id=self.type_id).first()
23+
if self.type_id == "draft" and state.slug == "rfc":
24+
a = self.docalias_set.filter(name__startswith="rfc")
25+
if a:
26+
name = a[0].name
27+
elif self.type_id == "charter":
28+
return charter_name_for_group(self.chartered_group)
29+
return name
30+
31+
def charter_name_for_group(group):
32+
if group.type_id == "rg":
33+
top_org = "irtf"
34+
else:
35+
top_org = "ietf"
36+
37+
return "charter-%s-%s" % (top_org, group.acronym)
38+
39+
def save_document_in_history(doc):
40+
"""Save a snapshot of document and related objects in the database."""
41+
def get_model_fields_as_dict(obj):
42+
return dict((field.name, getattr(obj, field.name))
43+
for field in obj._meta.fields
44+
if field is not obj._meta.pk)
45+
46+
# copy fields
47+
fields = get_model_fields_as_dict(doc)
48+
fields["doc"] = doc
49+
fields["name"] = canonical_name(doc)
50+
51+
dochist = DocHistory(**fields)
52+
dochist.save()
53+
54+
# copy many to many
55+
for field in doc._meta.many_to_many:
56+
if field.rel.through and field.rel.through._meta.auto_created:
57+
setattr(dochist, field.name, getattr(doc, field.name).all())
58+
59+
# copy remaining tricky many to many
60+
def transfer_fields(obj, HistModel):
61+
mfields = get_model_fields_as_dict(item)
62+
# map doc -> dochist
63+
for k, v in mfields.iteritems():
64+
if v == doc:
65+
mfields[k] = dochist
66+
HistModel.objects.create(**mfields)
67+
68+
for item in RelatedDocument.objects.filter(source=doc):
69+
transfer_fields(item, RelatedDocHistory)
70+
71+
for item in DocumentAuthor.objects.filter(document=doc):
72+
transfer_fields(item, DocHistoryAuthor)
73+
74+
return dochist
75+
76+
from django.conf import settings
77+
settings.DEBUG = False # prevent out-of-memory problems
78+
79+
for d in Document.objects.iterator():
80+
save_document_in_history(d)
81+
82+
class Migration(migrations.Migration):
83+
84+
dependencies = [
85+
('doc', '0010_auto_20150930_0251'),
86+
]
87+
88+
operations = [
89+
migrations.RunPython(fix_buggy_author_foreignkey),
90+
migrations.RunPython(save_all_documents_in_history)
91+
]

ietf/doc/models.py

Lines changed: 26 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from ietf.person.models import Email, Person
1919
from ietf.utils.admin import admin_link
2020

21-
2221
class StateType(models.Model):
2322
slug = models.CharField(primary_key=True, max_length=30) # draft, draft-iesg, charter, ...
2423
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, ...
@@ -432,6 +431,27 @@ def display_name(self):
432431
name = name.upper()
433432
return name
434433

434+
def save_with_history(self, events):
435+
"""Save document and put a snapshot in the history models where they
436+
can be retrieved later. You must pass in at least one event
437+
with a description of what happened."""
438+
439+
assert events, "You must always add at least one event to describe the changes in the history log"
440+
self.time = max(self.time, events[0].time)
441+
442+
self._has_an_event_so_saving_is_allowed = True
443+
self.save()
444+
del self._has_an_event_so_saving_is_allowed
445+
446+
from ietf.doc.utils import save_document_in_history
447+
save_document_in_history(self)
448+
449+
def save(self, *args, **kwargs):
450+
# if there's no primary key yet, we can allow the save to go
451+
# through to break the cycle between the document and any
452+
# events
453+
assert kwargs.get("force_insert", False) or getattr(self, "_has_an_event_so_saving_is_allowed", None), "Use .save_with_history to save documents"
454+
super(Document, self).save(*args, **kwargs)
435455

436456
def telechat_date(self, e=None):
437457
if not e:
@@ -568,50 +588,6 @@ class Meta:
568588
verbose_name = "document history"
569589
verbose_name_plural = "document histories"
570590

571-
def save_document_in_history(doc):
572-
"""This should be called before saving changes to a Document instance,
573-
so that the DocHistory entries contain all previous states, while
574-
the Group entry contain the current state. XXX TODO: Call this
575-
directly from Document.save(), and add event listeners for save()
576-
on related objects so we can save as needed when they change, too.
577-
"""
578-
def get_model_fields_as_dict(obj):
579-
return dict((field.name, getattr(obj, field.name))
580-
for field in obj._meta.fields
581-
if field is not obj._meta.pk)
582-
583-
# copy fields
584-
fields = get_model_fields_as_dict(doc)
585-
fields["doc"] = doc
586-
fields["name"] = doc.canonical_name()
587-
588-
dochist = DocHistory(**fields)
589-
dochist.save()
590-
591-
# copy many to many
592-
for field in doc._meta.many_to_many:
593-
if field.rel.through and field.rel.through._meta.auto_created:
594-
setattr(dochist, field.name, getattr(doc, field.name).all())
595-
596-
# copy remaining tricky many to many
597-
def transfer_fields(obj, HistModel):
598-
mfields = get_model_fields_as_dict(item)
599-
# map doc -> dochist
600-
for k, v in mfields.iteritems():
601-
if v == doc:
602-
mfields[k] = dochist
603-
HistModel.objects.create(**mfields)
604-
605-
for item in RelatedDocument.objects.filter(source=doc):
606-
transfer_fields(item, RelatedDocHistory)
607-
608-
for item in DocumentAuthor.objects.filter(document=doc):
609-
transfer_fields(item, DocHistoryAuthor)
610-
611-
return dochist
612-
613-
614-
615591
class DocAlias(models.Model):
616592
"""This is used for documents that may appear under multiple names,
617593
and in particular for RFCs, which for continuity still keep the
@@ -693,7 +669,8 @@ class DocReminder(models.Model):
693669

694670
# RFC Editor
695671
("rfc_editor_received_announcement", "Announcement was received by RFC Editor"),
696-
("requested_publication", "Publication at RFC Editor requested")
672+
("requested_publication", "Publication at RFC Editor requested"),
673+
("sync_from_rfc_editor", "Received updated information from RFC Editor"),
697674
]
698675

699676
class DocEvent(models.Model):
@@ -705,10 +682,11 @@ class DocEvent(models.Model):
705682
desc = models.TextField()
706683

707684
def for_current_revision(self):
708-
return self.time >= self.doc.latest_event(NewRevisionDocEvent,type='new_revision').time
685+
e = self.doc.latest_event(NewRevisionDocEvent,type='new_revision')
686+
return not e or (self.time, self.pk) >= (e.time, e.pk)
709687

710688
def get_dochistory(self):
711-
return DocHistory.objects.filter(time__lte=self.time,doc__name=self.doc.name).order_by('-time').first()
689+
return DocHistory.objects.filter(time__lte=self.time,doc__name=self.doc.name).order_by('-time', '-pk').first()
712690

713691
def __unicode__(self):
714692
return u"%s %s by %s at %s" % (self.doc.name, self.get_type_display().lower(), self.by.plain_name(), self.time)

0 commit comments

Comments
 (0)