|
| 1 | +# Copyright The IETF Trust 2007, All Rights Reserved |
| 2 | + |
| 3 | +from django.db import models |
| 4 | +from django.core.urlresolvers import reverse as urlreverse |
| 5 | + |
| 6 | +from redesign.group.models import * |
| 7 | +from redesign.name.models import * |
| 8 | +from redesign.person.models import Email, Person |
| 9 | +from redesign.util import admin_link |
| 10 | + |
| 11 | +import datetime |
| 12 | + |
| 13 | +class DocumentInfo(models.Model): |
| 14 | + """Any kind of document. Draft, RFC, Charter, IPR Statement, Liaison Statement""" |
| 15 | + time = models.DateTimeField(default=datetime.datetime.now) # should probably have auto_now=True |
| 16 | + # Document related |
| 17 | + type = models.ForeignKey(DocTypeName, blank=True, null=True) # Draft, Agenda, Minutes, Charter, Discuss, Guideline, Email, Review, Issue, Wiki, External ... |
| 18 | + title = models.CharField(max_length=255) |
| 19 | + # State |
| 20 | + state = models.ForeignKey(DocStateName, blank=True, null=True) # Active/Expired/RFC/Replaced/Withdrawn |
| 21 | + tags = models.ManyToManyField(DocInfoTagName, blank=True, null=True) # Revised ID Needed, ExternalParty, AD Followup, ... |
| 22 | + stream = models.ForeignKey(DocStreamName, blank=True, null=True) # IETF, IAB, IRTF, Independent Submission |
| 23 | + group = models.ForeignKey(Group, blank=True, null=True) # WG, RG, IAB, IESG, Edu, Tools |
| 24 | + wg_state = models.ForeignKey(WgDocStateName, verbose_name="WG state", blank=True, null=True) # Not/Candidate/Active/Parked/LastCall/WriteUp/Submitted/Dead |
| 25 | + iesg_state = models.ForeignKey(IesgDocStateName, verbose_name="IESG state", blank=True, null=True) # |
| 26 | + iana_state = models.ForeignKey(IanaDocStateName, verbose_name="IANA state", blank=True, null=True) |
| 27 | + rfc_state = models.ForeignKey(RfcDocStateName, verbose_name="RFC state", blank=True, null=True) |
| 28 | + # Other |
| 29 | + abstract = models.TextField() |
| 30 | + rev = models.CharField(verbose_name="revision", max_length=16, blank=True) |
| 31 | + pages = models.IntegerField(blank=True, null=True) |
| 32 | + intended_std_level = models.ForeignKey(IntendedStdLevelName, blank=True, null=True) |
| 33 | + std_level = models.ForeignKey(StdLevelName, blank=True, null=True) |
| 34 | + ad = models.ForeignKey(Person, verbose_name="area director", related_name='ad_%(class)s_set', blank=True, null=True) |
| 35 | + shepherd = models.ForeignKey(Person, related_name='shepherd_%(class)s_set', blank=True, null=True) |
| 36 | + notify = models.CharField(max_length=255, blank=True) |
| 37 | + external_url = models.URLField(blank=True) # Should be set for documents with type 'External'. |
| 38 | + note = models.TextField(blank=True) |
| 39 | + internal_comments = models.TextField(blank=True) |
| 40 | + |
| 41 | + class Meta: |
| 42 | + abstract = True |
| 43 | + def author_list(self): |
| 44 | + return ", ".join(email.address for email in self.authors.all()) |
| 45 | + |
| 46 | +class RelatedDocument(models.Model): |
| 47 | + source = models.ForeignKey('Document') |
| 48 | + target = models.ForeignKey('DocAlias') |
| 49 | + relationship = models.ForeignKey(DocRelationshipName) |
| 50 | + def action(self): |
| 51 | + return self.relationship.name |
| 52 | + def inverse_action(): |
| 53 | + infinitive = self.relationship.name[:-1] |
| 54 | + return u"%sd by" % infinitive |
| 55 | + def __unicode__(self): |
| 56 | + return u"%s %s %s" % (self.source.name, self.relationship.name.lower(), self.target.name) |
| 57 | + |
| 58 | +class DocumentAuthor(models.Model): |
| 59 | + document = models.ForeignKey('Document') |
| 60 | + author = models.ForeignKey(Email, help_text="Email address used by author for submission") |
| 61 | + order = models.IntegerField(default=1) |
| 62 | + |
| 63 | + def __unicode__(self): |
| 64 | + return u"%s %s (%s)" % (self.document.name, self.author.get_name(), self.order) |
| 65 | + |
| 66 | + class Meta: |
| 67 | + ordering = ["document", "order"] |
| 68 | + |
| 69 | +class Document(DocumentInfo): |
| 70 | + name = models.CharField(max_length=255, primary_key=True) # immutable |
| 71 | + related = models.ManyToManyField('DocAlias', through=RelatedDocument, blank=True, related_name="reversely_related_document_set") |
| 72 | + authors = models.ManyToManyField(Email, through=DocumentAuthor, blank=True) |
| 73 | + |
| 74 | + def __unicode__(self): |
| 75 | + return self.name |
| 76 | + |
| 77 | + def get_absolute_url(self): |
| 78 | + name = self.name |
| 79 | + if self.state == "rfc": |
| 80 | + aliases = self.docalias_set.filter(name__startswith="rfc") |
| 81 | + if aliases: |
| 82 | + name = aliases[0].name |
| 83 | + return urlreverse('doc_view', kwargs={ 'name': name }) |
| 84 | + |
| 85 | + def file_tag(self): |
| 86 | + return u"<%s>" % self.filename_with_rev() |
| 87 | + |
| 88 | + def filename_with_rev(self): |
| 89 | + # FIXME: compensate for tombstones? |
| 90 | + return u"%s-%s.txt" % (self.name, self.rev) |
| 91 | + |
| 92 | + def latest_event(self, *args, **filter_args): |
| 93 | + """Get latest event of optional Python type and with filter |
| 94 | + arguments, e.g. d.latest_event(type="xyz") returns an DocEvent |
| 95 | + while d.latest_event(WriteupDocEvent, type="xyz") returns a |
| 96 | + WriteupDocEvent event.""" |
| 97 | + model = args[0] if args else DocEvent |
| 98 | + e = model.objects.filter(doc=self).filter(**filter_args).order_by('-time', '-id')[:1] |
| 99 | + return e[0] if e else None |
| 100 | + |
| 101 | + def canonical_name(self): |
| 102 | + name = self.name |
| 103 | + if self.type_id == "draft" and self.state_id == "rfc": |
| 104 | + a = self.docalias_set.filter(name__startswith="rfc") |
| 105 | + if a: |
| 106 | + name = a[0].name |
| 107 | + return name |
| 108 | + |
| 109 | + |
| 110 | +class RelatedDocHistory(models.Model): |
| 111 | + source = models.ForeignKey('DocHistory') |
| 112 | + target = models.ForeignKey('DocAlias', related_name="reversely_related_document_history_set") |
| 113 | + relationship = models.ForeignKey(DocRelationshipName) |
| 114 | + def __unicode__(self): |
| 115 | + return u"%s %s %s" % (self.source.doc.name, self.relationship.name.lower(), self.target.name) |
| 116 | + |
| 117 | +class DocHistoryAuthor(models.Model): |
| 118 | + document = models.ForeignKey('DocHistory') |
| 119 | + author = models.ForeignKey(Email) |
| 120 | + order = models.IntegerField() |
| 121 | + |
| 122 | + def __unicode__(self): |
| 123 | + return u"%s %s (%s)" % (self.document.doc.name, self.author.get_name(), self.order) |
| 124 | + |
| 125 | + class Meta: |
| 126 | + ordering = ["document", "order"] |
| 127 | + |
| 128 | +class DocHistory(DocumentInfo): |
| 129 | + doc = models.ForeignKey(Document) # ID of the Document this relates to |
| 130 | + # Django 1.2 won't let us define these in the base class, so we have |
| 131 | + # to repeat them |
| 132 | + related = models.ManyToManyField('DocAlias', through=RelatedDocHistory, blank=True) |
| 133 | + authors = models.ManyToManyField(Email, through=DocHistoryAuthor, blank=True) |
| 134 | + def __unicode__(self): |
| 135 | + return unicode(self.doc.name) |
| 136 | + |
| 137 | +def save_document_in_history(doc): |
| 138 | + def get_model_fields_as_dict(obj): |
| 139 | + return dict((field.name, getattr(obj, field.name)) |
| 140 | + for field in obj._meta.fields |
| 141 | + if field is not obj._meta.pk) |
| 142 | + |
| 143 | + # copy fields |
| 144 | + fields = get_model_fields_as_dict(doc) |
| 145 | + fields["doc"] = doc |
| 146 | + |
| 147 | + dochist = DocHistory(**fields) |
| 148 | + dochist.save() |
| 149 | + |
| 150 | + # copy many to many |
| 151 | + for field in doc._meta.many_to_many: |
| 152 | + if not field.rel.through: |
| 153 | + # just add the attributes |
| 154 | + rel = getattr(dochist, field.name) |
| 155 | + for item in getattr(doc, field.name).all(): |
| 156 | + rel.add(item) |
| 157 | + |
| 158 | + # copy remaining tricky many to many |
| 159 | + def transfer_fields(obj, HistModel): |
| 160 | + mfields = get_model_fields_as_dict(item) |
| 161 | + # map doc -> dochist |
| 162 | + for k, v in mfields.iteritems(): |
| 163 | + if v == doc: |
| 164 | + mfields[k] = dochist |
| 165 | + HistModel.objects.create(**mfields) |
| 166 | + |
| 167 | + for item in RelatedDocument.objects.filter(source=doc): |
| 168 | + transfer_fields(item, RelatedDocHistory) |
| 169 | + |
| 170 | + for item in DocumentAuthor.objects.filter(document=doc): |
| 171 | + transfer_fields(item, DocHistoryAuthor) |
| 172 | + |
| 173 | + return dochist |
| 174 | + |
| 175 | +class DocAlias(models.Model): |
| 176 | + """This is used for documents that may appear under multiple names, |
| 177 | + and in particular for RFCs, which for continuity still keep the |
| 178 | + same immutable Document.name, in the tables, but will be referred |
| 179 | + to by RFC number, primarily, after achieving RFC status. |
| 180 | + """ |
| 181 | + document = models.ForeignKey(Document) |
| 182 | + name = models.CharField(max_length=255, db_index=True) |
| 183 | + def __unicode__(self): |
| 184 | + return "%s-->%s" % (self.name, self.document.name) |
| 185 | + document_link = admin_link("document") |
| 186 | + class Meta: |
| 187 | + verbose_name = "document alias" |
| 188 | + verbose_name_plural = "document aliases" |
| 189 | + |
| 190 | + |
| 191 | +EVENT_TYPES = [ |
| 192 | + # core events |
| 193 | + ("new_revision", "Added new revision"), |
| 194 | + ("changed_document", "Changed document metadata"), |
| 195 | + |
| 196 | + # misc document events |
| 197 | + ("added_comment", "Added comment"), |
| 198 | + ("expired_document", "Expired document"), |
| 199 | + ("requested_resurrect", "Requested resurrect"), |
| 200 | + ("completed_resurrect", "Completed resurrect"), |
| 201 | + ("published_rfc", "Published RFC"), |
| 202 | + |
| 203 | + # IESG events |
| 204 | + ("started_iesg_process", "Started IESG process on document"), |
| 205 | + |
| 206 | + ("sent_ballot_announcement", "Sent ballot announcement"), |
| 207 | + ("changed_ballot_position", "Changed ballot position"), |
| 208 | + |
| 209 | + ("changed_ballot_approval_text", "Changed ballot approval text"), |
| 210 | + ("changed_ballot_writeup_text", "Changed ballot writeup text"), |
| 211 | + |
| 212 | + ("changed_last_call_text", "Changed last call text"), |
| 213 | + ("requested_last_call", "Requested last call"), |
| 214 | + ("sent_last_call", "Sent last call"), |
| 215 | + |
| 216 | + ("changed_status_date", "Changed status date"), |
| 217 | + |
| 218 | + ("scheduled_for_telechat", "Scheduled for telechat"), |
| 219 | + |
| 220 | + ("iesg_approved", "IESG approved document (no problem)"), |
| 221 | + ("iesg_disapproved", "IESG disapproved document (do not publish)"), |
| 222 | + |
| 223 | + ("approved_in_minute", "Approved in minute"), |
| 224 | + ] |
| 225 | + |
| 226 | +class DocEvent(models.Model): |
| 227 | + """An occurrence for a document, used for tracking who, when and what.""" |
| 228 | + time = models.DateTimeField(default=datetime.datetime.now, help_text="When the event happened") |
| 229 | + type = models.CharField(max_length=50, choices=EVENT_TYPES) |
| 230 | + by = models.ForeignKey(Person) |
| 231 | + doc = models.ForeignKey('doc.Document') |
| 232 | + desc = models.TextField() |
| 233 | + |
| 234 | + def __unicode__(self): |
| 235 | + return u"%s %s at %s" % (self.by.name, self.get_type_display().lower(), self.time) |
| 236 | + |
| 237 | + class Meta: |
| 238 | + ordering = ['-time', '-id'] |
| 239 | + |
| 240 | +class NewRevisionDocEvent(DocEvent): |
| 241 | + rev = models.CharField(max_length=16) |
| 242 | + |
| 243 | +# IESG events |
| 244 | +class BallotPositionDocEvent(DocEvent): |
| 245 | + ad = models.ForeignKey(Person) |
| 246 | + pos = models.ForeignKey(BallotPositionName, verbose_name="position", default="norecord") |
| 247 | + discuss = models.TextField(help_text="Discuss text if position is discuss", blank=True) |
| 248 | + discuss_time = models.DateTimeField(help_text="Time discuss text was written", blank=True, null=True) |
| 249 | + comment = models.TextField(help_text="Optional comment", blank=True) |
| 250 | + comment_time = models.DateTimeField(help_text="Time optional comment was written", blank=True, null=True) |
| 251 | + |
| 252 | +class WriteupDocEvent(DocEvent): |
| 253 | + text = models.TextField(blank=True) |
| 254 | + |
| 255 | +class StatusDateDocEvent(DocEvent): |
| 256 | + date = models.DateField(blank=True, null=True) |
| 257 | + |
| 258 | +class LastCallDocEvent(DocEvent): |
| 259 | + expires = models.DateTimeField(blank=True, null=True) |
| 260 | + |
| 261 | +class TelechatDocEvent(DocEvent): |
| 262 | + telechat_date = models.DateField(blank=True, null=True) |
| 263 | + returning_item = models.BooleanField(default=False) |
| 264 | + |
0 commit comments