datatracker/ietf/doc/models.py
2012-01-24 17:17:24 +00:00

359 lines
14 KiB
Python

# Copyright The IETF Trust 2007, All Rights Reserved
from django.db import models
from django.core.urlresolvers import reverse as urlreverse
from django.conf import settings
from ietf.group.models import *
from ietf.name.models import *
from ietf.person.models import Email, Person
import datetime, os
class StateType(models.Model):
slug = models.CharField(primary_key=True, max_length=30) # draft, draft_iesg, charter, ...
label = models.CharField(max_length=255) # State, IESG state, WG state, ...
def __unicode__(self):
return self.label
class State(models.Model):
type = models.ForeignKey(StateType)
slug = models.SlugField()
name = models.CharField(max_length=255)
used = models.BooleanField(default=True)
desc = models.TextField(blank=True)
order = models.IntegerField(default=0)
next_states = models.ManyToManyField('State', related_name="previous_states")
def __unicode__(self):
return self.name
class Meta:
ordering = ["type", "order"]
class DocumentInfo(models.Model):
"""Any kind of document. Draft, RFC, Charter, IPR Statement, Liaison Statement"""
time = models.DateTimeField(default=datetime.datetime.now) # should probably have auto_now=True
type = models.ForeignKey(DocTypeName, blank=True, null=True) # Draft, Agenda, Minutes, Charter, Discuss, Guideline, Email, Review, Issue, Wiki, External ...
title = models.CharField(max_length=255)
states = models.ManyToManyField(State, blank=True) # plain state (Active/Expired/...), IESG state, stream state
tags = models.ManyToManyField(DocTagName, blank=True, null=True) # Revised ID Needed, ExternalParty, AD Followup, ...
stream = models.ForeignKey(StreamName, blank=True, null=True) # IETF, IAB, IRTF, Independent Submission
group = models.ForeignKey(Group, blank=True, null=True) # WG, RG, IAB, IESG, Edu, Tools
abstract = models.TextField()
rev = models.CharField(verbose_name="revision", max_length=16, blank=True)
pages = models.IntegerField(blank=True, null=True)
order = models.IntegerField(default=1)
intended_std_level = models.ForeignKey(IntendedStdLevelName, blank=True, null=True)
std_level = models.ForeignKey(StdLevelName, blank=True, null=True)
ad = models.ForeignKey(Person, verbose_name="area director", related_name='ad_%(class)s_set', blank=True, null=True)
shepherd = models.ForeignKey(Person, related_name='shepherd_%(class)s_set', blank=True, null=True)
expires = models.DateTimeField(blank=True, null=True)
notify = models.CharField(max_length=255, blank=True)
external_url = models.URLField(blank=True) # Should be set for documents with type 'External'.
note = models.TextField(blank=True)
internal_comments = models.TextField(blank=True)
def get_file_path(self):
if self.type_id == "draft":
return settings.INTERNET_DRAFT_PATH
elif self.type_id in ("agenda", "minutes", "slides"):
meeting = self.name.split("-")[1]
return os.path.join(settings.AGENDA_PATH, meeting, self.type_id) + "/"
elif self.type_id == "charter":
return settings.CHARTER_PATH
else:
raise NotImplemented
def set_state(self, state):
"""Switch state type implicit in state to state. This just
sets the state, doesn't log the change."""
already_set = self.states.filter(type=state.type)
others = [s for s in already_set if s != state]
if others:
self.states.remove(*others)
if state not in already_set:
self.states.add(state)
def unset_state(self, state_type):
"""Unset state of type so no state of that type is any longer set."""
self.states.remove(*self.states.filter(type=state_type))
def get_state(self, state_type=None):
"""Get state of type, or default state for document type if not specified."""
if state_type == None:
state_type = self.type_id
try:
return self.states.get(type=state_type)
except State.DoesNotExist:
return None
def get_state_slug(self, state_type=None):
"""Get state of type, or default if not specified, returning
the slug of the state or None. This frees the caller of having
to check against None before accessing the slug for a
comparison."""
s = self.get_state(state_type)
if s:
return s.slug
else:
return None
def author_list(self):
return ", ".join(email.address for email in self.authors.all())
class Meta:
abstract = True
class RelatedDocument(models.Model):
source = models.ForeignKey('Document')
target = models.ForeignKey('DocAlias')
relationship = models.ForeignKey(DocRelationshipName)
def action(self):
return self.relationship.name
def inverse_action():
infinitive = self.relationship.name[:-1]
return u"%sd by" % infinitive
def __unicode__(self):
return u"%s %s %s" % (self.source.name, self.relationship.name.lower(), self.target.name)
class DocumentAuthor(models.Model):
document = models.ForeignKey('Document')
author = models.ForeignKey(Email, help_text="Email address used by author for submission")
order = models.IntegerField(default=1)
def __unicode__(self):
return u"%s %s (%s)" % (self.document.name, self.author.get_name(), self.order)
class Meta:
ordering = ["document", "order"]
class Document(DocumentInfo):
name = models.CharField(max_length=255, primary_key=True) # immutable
related = models.ManyToManyField('DocAlias', through=RelatedDocument, blank=True, related_name="reversely_related_document_set")
authors = models.ManyToManyField(Email, through=DocumentAuthor, blank=True)
def __unicode__(self):
return self.name
def get_absolute_url(self):
name = self.name
if self.type_id == "charter":
return urlreverse('wg_view', kwargs={ 'name': self.group.acronym })
elif self.type_id == "draft":
if self.get_state_slug() == "rfc":
aliases = self.docalias_set.filter(name__startswith="rfc")
if aliases:
name = aliases[0].name
return urlreverse('doc_view', kwargs={ 'name': name })
def file_tag(self):
return u"<%s>" % self.filename_with_rev()
def filename_with_rev(self):
return u"%s-%s.txt" % (self.name, self.rev)
def latest_event(self, *args, **filter_args):
"""Get latest event of optional Python type and with filter
arguments, e.g. d.latest_event(type="xyz") returns an DocEvent
while d.latest_event(WriteupDocEvent, type="xyz") returns a
WriteupDocEvent event."""
model = args[0] if args else DocEvent
e = model.objects.filter(doc=self).filter(**filter_args).order_by('-time', '-id')[:1]
return e[0] if e else None
def canonical_name(self):
name = self.name
if self.type_id == "draft" and self.get_state_slug() == "rfc":
a = self.docalias_set.filter(name__startswith="rfc")
if a:
name = a[0].name
return name
class RelatedDocHistory(models.Model):
source = models.ForeignKey('DocHistory')
target = models.ForeignKey('DocAlias', related_name="reversely_related_document_history_set")
relationship = models.ForeignKey(DocRelationshipName)
def __unicode__(self):
return u"%s %s %s" % (self.source.doc.name, self.relationship.name.lower(), self.target.name)
class DocHistoryAuthor(models.Model):
document = models.ForeignKey('DocHistory')
author = models.ForeignKey(Email)
order = models.IntegerField()
def __unicode__(self):
return u"%s %s (%s)" % (self.document.doc.name, self.author.get_name(), self.order)
class Meta:
ordering = ["document", "order"]
class DocHistory(DocumentInfo):
doc = models.ForeignKey(Document, related_name="history_set")
name = models.CharField(max_length=255) # WG charter names can change if the group acronym changes
related = models.ManyToManyField('DocAlias', through=RelatedDocHistory, blank=True)
authors = models.ManyToManyField(Email, through=DocHistoryAuthor, blank=True)
def __unicode__(self):
return unicode(self.doc.name)
class Meta:
verbose_name = "document history"
verbose_name_plural = "document histories"
def save_document_in_history(doc):
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.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
same immutable Document.name, in the tables, but will be referred
to by RFC number, primarily, after achieving RFC status.
"""
document = models.ForeignKey(Document)
name = models.CharField(max_length=255, db_index=True)
def __unicode__(self):
return "%s-->%s" % (self.name, self.document.name)
class Meta:
verbose_name = "document alias"
verbose_name_plural = "document aliases"
class DocReminder(models.Model):
event = models.ForeignKey('DocEvent')
type = models.ForeignKey(DocReminderTypeName)
due = models.DateTimeField()
active = models.BooleanField(default=True)
EVENT_TYPES = [
# core events
("new_revision", "Added new revision"),
("changed_document", "Changed document metadata"),
("added_comment", "Added comment"),
("uploaded", "Uploaded document"),
("deleted", "Deleted document"),
# misc draft/RFC events
("changed_stream", "Changed document stream"),
("expired_document", "Expired document"),
("extended_expiry", "Extended expiry of document"),
("requested_resurrect", "Requested resurrect"),
("completed_resurrect", "Completed resurrect"),
("published_rfc", "Published RFC"),
# WG events
("changed_group", "Changed group"),
("changed_protocol_writeup", "Changed protocol writeup"),
# charter events
("initial_review", "Set initial review time"),
("changed_review_announcement", "Changed WG Review text"),
("changed_action_announcement", "Changed WG Action text"),
# IESG events
("started_iesg_process", "Started IESG process on document"),
("sent_ballot_announcement", "Sent ballot announcement"),
("changed_ballot_position", "Changed ballot position"),
("changed_ballot_approval_text", "Changed ballot approval text"),
("changed_ballot_writeup_text", "Changed ballot writeup text"),
("changed_last_call_text", "Changed last call text"),
("requested_last_call", "Requested last call"),
("sent_last_call", "Sent last call"),
("scheduled_for_telechat", "Scheduled for telechat"),
("iesg_approved", "IESG approved document (no problem)"),
("iesg_disapproved", "IESG disapproved document (do not publish)"),
("approved_in_minute", "Approved in minute"),
]
class DocEvent(models.Model):
"""An occurrence for a document, used for tracking who, when and what."""
time = models.DateTimeField(default=datetime.datetime.now, help_text="When the event happened")
type = models.CharField(max_length=50, choices=EVENT_TYPES)
by = models.ForeignKey(Person)
doc = models.ForeignKey('doc.Document')
desc = models.TextField()
def __unicode__(self):
return u"%s %s at %s" % (self.by.plain_name(), self.get_type_display().lower(), self.time)
class Meta:
ordering = ['-time', '-id']
class NewRevisionDocEvent(DocEvent):
rev = models.CharField(max_length=16)
# IESG events
class BallotPositionDocEvent(DocEvent):
ad = models.ForeignKey(Person)
pos = models.ForeignKey(BallotPositionName, verbose_name="position", default="norecord")
discuss = models.TextField(help_text="Discuss text if position is discuss", blank=True)
discuss_time = models.DateTimeField(help_text="Time discuss text was written", blank=True, null=True)
comment = models.TextField(help_text="Optional comment", blank=True)
comment_time = models.DateTimeField(help_text="Time optional comment was written", blank=True, null=True)
class WriteupDocEvent(DocEvent):
text = models.TextField(blank=True)
class LastCallDocEvent(DocEvent):
expires = models.DateTimeField(blank=True, null=True)
class TelechatDocEvent(DocEvent):
telechat_date = models.DateField(blank=True, null=True)
returning_item = models.BooleanField(default=False)
# Charter ballot events
class GroupBallotPositionDocEvent(DocEvent):
ad = models.ForeignKey(Person)
pos = models.ForeignKey(GroupBallotPositionName, verbose_name="position", default="norecord")
block_comment = models.TextField(help_text="Blocking comment if position is comment", blank=True)
block_comment_time = models.DateTimeField(help_text="Blocking comment was written", blank=True, null=True)
comment = models.TextField(help_text="Non-blocking comment", blank=True)
comment_time = models.DateTimeField(help_text="Time non-blocking comment was written", blank=True, null=True)
class InitialReviewDocEvent(DocEvent):
expires = models.DateTimeField(blank=True, null=True)