in the AD edit form, change search to define area = doc.group or doc.group.parent (so don't take AD into account), plus a bunch of small tweaks to make a document with an area set as group behave like an individually submitted document, at least for the time being (they are presented as "Individual in xyz area" on their document page) - Legacy-Id: 3968
1005 lines
35 KiB
Python
1005 lines
35 KiB
Python
from ietf.doc.models import *
|
|
from ietf.person.models import Email
|
|
from ietf.utils.proxy import TranslatingManager
|
|
from ietf.name.proxy import *
|
|
|
|
from django.conf import settings
|
|
|
|
import glob, os
|
|
|
|
|
|
class InternetDraft(Document):
|
|
objects = TranslatingManager(dict(filename="name",
|
|
filename__contains="name__contains",
|
|
id_document_tag="pk",
|
|
status=lambda v: ("states__slug", { 1: 'active', 2: 'expired', 3: 'rfc', 4: 'auth-rm', 5: 'repl', 6: 'ietf-rm'}[v], "states__type", "draft"),
|
|
job_owner="ad",
|
|
rfc_number=lambda v: ("docalias__name", "rfc%s" % v),
|
|
cur_state=lambda v: ("states__order", v, 'states__type', 'draft-iesg'),
|
|
idinternal__primary_flag=None,
|
|
idinternal__cur_state__state=lambda v: ("states__name", v, 'states__type', 'draft-iesg'),
|
|
), always_filter=dict(type="draft"))
|
|
|
|
DAYS_TO_EXPIRE=185
|
|
|
|
# things from InternetDraft
|
|
|
|
#id_document_tag = models.AutoField(primary_key=True)
|
|
@property
|
|
def id_document_tag(self):
|
|
return self.name # Will only work for some use cases
|
|
#title = models.CharField(max_length=255, db_column='id_document_name') # same name
|
|
#id_document_key = models.CharField(max_length=255, editable=False)
|
|
@property
|
|
def id_document_key(self):
|
|
return self.title.upper()
|
|
#group = models.ForeignKey(Acronym, db_column='group_acronym_id')
|
|
@property
|
|
def group(self):
|
|
from ietf.group.proxy import Acronym as AcronymProxy
|
|
g = super(InternetDraft, self).group
|
|
return AcronymProxy().from_object(g) if g else None
|
|
#filename = models.CharField(max_length=255, unique=True)
|
|
@property
|
|
def filename(self):
|
|
return self.name
|
|
#revision = models.CharField(max_length=2)
|
|
@property
|
|
def revision(self):
|
|
return self.rev
|
|
#revision_date = models.DateField()
|
|
@property
|
|
def revision_date(self):
|
|
if hasattr(self, "new_revision"):
|
|
e = self.new_revision
|
|
else:
|
|
e = self.latest_event(type="new_revision")
|
|
return e.time.date() if e else None
|
|
# helper function
|
|
def get_file_type_matches_from(self, glob_path):
|
|
possible_types = [".txt", ".pdf", ".xml", ".ps"]
|
|
res = []
|
|
for m in glob.glob(glob_path):
|
|
for t in possible_types:
|
|
if m.endswith(t):
|
|
res.append(t)
|
|
return ",".join(res)
|
|
#file_type = models.CharField(max_length=20)
|
|
@property
|
|
def file_type(self):
|
|
return self.get_file_type_matches_from(os.path.join(settings.INTERNET_DRAFT_PATH, self.name + "-" + self.rev + ".*")) or ".txt"
|
|
#txt_page_count = models.IntegerField()
|
|
@property
|
|
def txt_page_count(self):
|
|
return self.pages
|
|
#local_path = models.CharField(max_length=255, blank=True) # unused
|
|
#start_date = models.DateField()
|
|
@property
|
|
def start_date(self):
|
|
e = NewRevisionDocEvent.objects.filter(doc=self).order_by("time")[:1]
|
|
return e[0].time.date() if e else None
|
|
#expiration_date = models.DateField()
|
|
@property
|
|
def expiration_date(self):
|
|
e = self.latest_event(type__in=('expired_document', 'new_revision', "completed_resurrect"))
|
|
return e.time.date() if e and e.type == "expired_document" else None
|
|
#abstract = models.TextField() # same name
|
|
#dunn_sent_date = models.DateField(null=True, blank=True) # unused
|
|
#extension_date = models.DateField(null=True, blank=True) # unused
|
|
#status = models.ForeignKey(IDStatus)
|
|
@property
|
|
def status(self):
|
|
s = self.get_state()
|
|
return IDStatus().from_object(s) if s else None
|
|
|
|
@property
|
|
def status_id(self):
|
|
return { 'active': 1, 'repl': 5, 'expired': 2, 'rfc': 3, 'auth-rm': 4, 'ietf-rm': 6 }[self.get_state_slug()]
|
|
|
|
#intended_status = models.ForeignKey(IDIntendedStatus)
|
|
@property
|
|
def intended_status(self):
|
|
return self.intended_std_level
|
|
|
|
#lc_sent_date = models.DateField(null=True, blank=True)
|
|
@property
|
|
def lc_sent_date(self):
|
|
e = self.latest_event(type="sent_last_call")
|
|
return e.time.date() if e else None
|
|
|
|
#lc_changes = models.CharField(max_length=3) # used in DB, unused in Django code?
|
|
|
|
#lc_expiration_date = models.DateField(null=True, blank=True)
|
|
@property
|
|
def lc_expiration_date(self):
|
|
e = self.latest_event(LastCallDocEvent, type="sent_last_call")
|
|
return e.expires.date() if e else None
|
|
|
|
#b_sent_date = models.DateField(null=True, blank=True)
|
|
@property
|
|
def b_sent_date(self):
|
|
e = self.latest_event(type="sent_ballot_announcement")
|
|
return e.time.date() if e else None
|
|
|
|
#b_discussion_date = models.DateField(null=True, blank=True) # unused
|
|
|
|
#b_approve_date = models.DateField(null=True, blank=True)
|
|
@property
|
|
def b_approve_date(self):
|
|
e = self.latest_event(type="iesg_approved")
|
|
return e.time.date() if e else None
|
|
|
|
#wgreturn_date = models.DateField(null=True, blank=True) # unused
|
|
|
|
#rfc_number = models.IntegerField(null=True, blank=True, db_index=True)
|
|
@property
|
|
def rfc_number(self):
|
|
n = self.canonical_name()
|
|
return int(n[3:]) if n.startswith("rfc") else None
|
|
|
|
#comments = models.TextField(blank=True) # unused
|
|
|
|
#last_modified_date = models.DateField()
|
|
@property
|
|
def last_modified_date(self):
|
|
return self.time.date()
|
|
|
|
#replaced_by = models.ForeignKey('self', db_column='replaced_by', blank=True, null=True, related_name='replaces_set')
|
|
@property
|
|
def replaced_by(self):
|
|
r = InternetDraft.objects.filter(relateddocument__target__document=self, relateddocument__relationship="replaces")
|
|
return r[0] if r else None
|
|
|
|
@property
|
|
def replaced_by_id(self):
|
|
r = self.replaced_by
|
|
return r.id_document_tag if r else None
|
|
|
|
#replaces = FKAsOneToOne('replaces', reverse=True)
|
|
@property
|
|
def replaces(self):
|
|
r = self.replaces_set
|
|
return r[0] if r else None
|
|
|
|
@property
|
|
def replaces_set(self):
|
|
return InternetDraft.objects.filter(docalias__relateddocument__source=self, docalias__relateddocument__relationship="replaces")
|
|
|
|
#review_by_rfc_editor = models.BooleanField()
|
|
@property
|
|
def review_by_rfc_editor(self):
|
|
return bool(self.tags.filter(slug='rfc-rev'))
|
|
|
|
#expired_tombstone = models.BooleanField()
|
|
@property
|
|
def expired_tombstone(self):
|
|
return False
|
|
|
|
def calc_process_start_end(self):
|
|
import datetime
|
|
start, end = datetime.datetime.min, datetime.datetime.max
|
|
e = self.latest_event(type="started_iesg_process")
|
|
if e:
|
|
start = e.time
|
|
if self.get_state_slug() == "rfc" and self.name.startswith("draft") and not hasattr(self, "viewing_as_rfc"):
|
|
previous_process = self.latest_event(type="started_iesg_process", time__lt=e.time)
|
|
if previous_process:
|
|
start = previous_process.time
|
|
end = e.time
|
|
self._process_start = start
|
|
self._process_end = end
|
|
|
|
@property
|
|
def process_start(self):
|
|
if not hasattr(self, "_process_start"):
|
|
self.calc_process_start_end()
|
|
return self._process_start
|
|
|
|
@property
|
|
def process_end(self):
|
|
if not hasattr(self, "_process_end"):
|
|
self.calc_process_start_end()
|
|
return self._process_end
|
|
|
|
#shepherd = BrokenForeignKey('PersonOrOrgInfo', null=True, blank=True, null_values=(0, )) # same name
|
|
|
|
#idinternal = FKAsOneToOne('idinternal', reverse=True, query=models.Q(rfc_flag = 0))
|
|
@property
|
|
def idinternal(self):
|
|
# since IDInternal is now merged into the document, we try to
|
|
# guess here
|
|
if hasattr(self, "changed_ballot_position"):
|
|
e = self.changed_ballot_position
|
|
else:
|
|
e = self.latest_event(type="changed_ballot_position")
|
|
return self if e or self.get_state("draft-iesg") else None
|
|
|
|
# reverse relationship
|
|
@property
|
|
def authors(self):
|
|
return IDAuthor.objects.filter(document=self)
|
|
|
|
@property
|
|
def protowriteup_set(self):
|
|
from ietf.wgchairs.models import ProtoWriteUpProxy
|
|
return ProtoWriteUpProxy.objects.filter(doc=self, type="changed_protocol_writeup")
|
|
|
|
# methods from InternetDraft
|
|
def displayname(self):
|
|
return self.name
|
|
def file_tag(self):
|
|
return "<%s>" % self.filename_with_rev()
|
|
def filename_with_rev(self):
|
|
return "%s-%s.txt" % (self.filename, self.revision_display())
|
|
def group_acronym(self):
|
|
g = super(Document, self).group
|
|
if g.type_id == "area":
|
|
return "none"
|
|
else:
|
|
return g.acronym
|
|
def group_ml_archive(self):
|
|
return self.group.list_archive
|
|
def idstate(self):
|
|
return self.docstate()
|
|
def revision_display(self):
|
|
return self.rev
|
|
def expiration(self):
|
|
return self.expires.date()
|
|
def can_expire(self):
|
|
# Copying the logic from expire-ids-1 without thinking
|
|
# much about it.
|
|
if self.review_by_rfc_editor:
|
|
return False
|
|
idinternal = self.idinternal
|
|
if idinternal:
|
|
cur_state_id = idinternal.cur_state_id
|
|
# 42 is "AD is Watching"; this matches what's in the
|
|
# expire-ids-1 perl script.
|
|
# A better way might be to add a column to the table
|
|
# saying whether or not a document is prevented from
|
|
# expiring.
|
|
if cur_state_id < 42:
|
|
return False
|
|
return True
|
|
|
|
def clean_abstract(self):
|
|
# Cleaning based on what "id-abstracts-text" script does
|
|
import re
|
|
a = self.abstract
|
|
a = re.sub(" *\r\n *", "\n", a) # get rid of DOS line endings
|
|
a = re.sub(" *\r *", "\n", a) # get rid of MAC line endings
|
|
a = re.sub("(\n *){3,}", "\n\n", a) # get rid of excessive vertical whitespace
|
|
a = re.sub("\f[\n ]*[^\n]*\n", "", a) # get rid of page headers
|
|
# Get rid of 'key words' boilerplate and anything which follows it:
|
|
# (No way that is part of the abstract...)
|
|
a = re.sub("(?s)(Conventions [Uu]sed in this [Dd]ocument|Requirements [Ll]anguage)?[\n ]*The key words \"MUST\", \"MUST NOT\",.*$", "", a)
|
|
# Get rid of status/copyright boilerplate
|
|
a = re.sub("(?s)\nStatus of [tT]his Memo\n.*$", "", a)
|
|
# wrap long lines without messing up formatting of Ok paragraphs:
|
|
while re.match("([^\n]{72,}?) +", a):
|
|
a = re.sub("([^\n]{72,}?) +([^\n ]*)(\n|$)", "\\1\n\\2 ", a)
|
|
# Remove leading and trailing whitespace
|
|
a = a.strip()
|
|
return a
|
|
|
|
|
|
# things from IDInternal
|
|
|
|
#draft = models.ForeignKey(InternetDraft, primary_key=True, unique=True, db_column='id_document_tag')
|
|
@property
|
|
def draft(self):
|
|
return self
|
|
|
|
@property
|
|
def draft_id(self):
|
|
return self.name
|
|
|
|
#rfc_flag = models.IntegerField(null=True)
|
|
@property
|
|
def rfc_flag(self):
|
|
return self.get_state_slug() == "rfc"
|
|
|
|
#ballot = models.ForeignKey(BallotInfo, related_name='drafts', db_column="ballot_id")
|
|
@property
|
|
def ballot(self):
|
|
if not self.idinternal:
|
|
raise BallotInfo.DoesNotExist()
|
|
return self
|
|
@property
|
|
def ballot_id(self):
|
|
return self.ballot.name
|
|
|
|
#primary_flag = models.IntegerField(blank=True, null=True)
|
|
@property
|
|
def primary_flag(self):
|
|
# left-over from multi-ballot documents which we don't really
|
|
# support anymore, just pretend we're always primary
|
|
return True
|
|
|
|
#group_flag = models.IntegerField(blank=True, default=0) # not used anymore, contained the group acronym_id once upon a time (so it wasn't a flag)
|
|
|
|
#token_name = models.CharField(blank=True, max_length=25)
|
|
@property
|
|
def token_name(self):
|
|
return self.ad.name
|
|
|
|
#token_email = models.CharField(blank=True, max_length=255)
|
|
@property
|
|
def token_email(self):
|
|
return self.ad.role_email("ad")
|
|
|
|
#note = models.TextField(blank=True) # same name
|
|
|
|
#status_date = models.DateField(blank=True,null=True)
|
|
@property
|
|
def status_date(self):
|
|
return self.time.date()
|
|
|
|
#email_display = models.CharField(blank=True, max_length=50) # unused
|
|
#agenda = models.IntegerField(null=True, blank=True)
|
|
@property
|
|
def agenda(self):
|
|
e = self.latest_event(TelechatDocEvent, type="scheduled_for_telechat")
|
|
return bool(e and e.telechat_date and e.telechat_date >= datetime.date.today())
|
|
|
|
#cur_state = models.ForeignKey(IDState, db_column='cur_state', related_name='docs')
|
|
@property
|
|
def cur_state(self):
|
|
s = self.get_state("draft-iesg")
|
|
return IDState().from_object(s) if s else None
|
|
|
|
@property
|
|
def cur_state_id(self):
|
|
s = self.get_state("draft-iesg")
|
|
return s.order if s else None
|
|
|
|
#prev_state = models.ForeignKey(IDState, db_column='prev_state', related_name='docs_prev')
|
|
@property
|
|
def prev_state(self):
|
|
ds = self.history_set.exclude(states=self.get_state("draft-iesg")).order_by('-time')[:1]
|
|
if ds:
|
|
s = ds[0].get_state("draft-iesg")
|
|
if s:
|
|
return IDState().from_object(s) if ds else None
|
|
return None
|
|
|
|
#assigned_to = models.CharField(blank=True, max_length=25) # unused
|
|
|
|
#mark_by = models.ForeignKey(IESGLogin, db_column='mark_by', related_name='marked')
|
|
@property
|
|
def mark_by(self):
|
|
e = self.latest_event()
|
|
from ietf.person.proxy import IESGLogin as IESGLoginProxy
|
|
return IESGLoginProxy().from_object(e.by) if e else None
|
|
|
|
# job_owner = models.ForeignKey(IESGLogin, db_column='job_owner', related_name='documents')
|
|
@property
|
|
def job_owner(self):
|
|
from ietf.person.proxy import IESGLogin as IESGLoginProxy
|
|
return IESGLoginProxy().from_object(self.ad) if self.ad else None
|
|
|
|
#event_date = models.DateField(null=True)
|
|
@property
|
|
def event_date(self):
|
|
e = self.latest_event()
|
|
return e.time if e else None
|
|
|
|
#area_acronym = models.ForeignKey(Area)
|
|
@property
|
|
def area_acronym(self):
|
|
from ietf.group.proxy import Area
|
|
g = super(InternetDraft, self).group # be careful with group which is proxied
|
|
if g:
|
|
if g.type_id == "area":
|
|
return Area().from_object(g)
|
|
elif g.type_id != "individ":
|
|
return Area().from_object(g.parent)
|
|
else:
|
|
return None
|
|
|
|
#cur_sub_state = BrokenForeignKey(IDSubState, related_name='docs', null=True, blank=True, null_values=(0, -1))
|
|
@property
|
|
def cur_sub_state(self):
|
|
s = self.tags.filter(slug__in=['extpty', 'need-rev', 'ad-f-up', 'point'])
|
|
return IDSubState().from_object(s[0]) if s else None
|
|
@property
|
|
def cur_sub_state_id(self):
|
|
s = self.cur_sub_state
|
|
return s.order if s else None
|
|
|
|
#prev_sub_state = BrokenForeignKey(IDSubState, related_name='docs_prev', null=True, blank=True, null_values=(0, -1))
|
|
@property
|
|
def prev_sub_state(self):
|
|
ds = self.history_set.all().order_by('-time')[:1]
|
|
substates = ds[0].tags.filter(slug__in=['extpty', 'need-rev', 'ad-f-up', 'point']) if ds else None
|
|
return IDSubState().from_object(substates[0]) if substates else None
|
|
@property
|
|
def prev_sub_state_id(self):
|
|
s = self.prev_sub_state
|
|
return s.order if s else None
|
|
|
|
#returning_item = models.IntegerField(null=True, blank=True)
|
|
@property
|
|
def returning_item(self):
|
|
e = self.latest_event(TelechatDocEvent, type="scheduled_for_telechat")
|
|
return e.returning_item if e else None
|
|
|
|
#telechat_date = models.DateField(null=True, blank=True)
|
|
@property
|
|
def telechat_date(self):
|
|
e = self.latest_event(TelechatDocEvent, type="scheduled_for_telechat")
|
|
return e.telechat_date if e else None
|
|
|
|
#via_rfc_editor = models.IntegerField(null=True, blank=True)
|
|
@property
|
|
def via_rfc_editor(self):
|
|
return bool(self.tags.filter(slug='via-rfc'))
|
|
|
|
#state_change_notice_to = models.CharField(blank=True, max_length=255)
|
|
@property
|
|
def state_change_notice_to(self):
|
|
return self.notify
|
|
|
|
#dnp = models.IntegerField(null=True, blank=True)
|
|
@property
|
|
def dnp(self):
|
|
e = self.latest_event(type__in=("iesg_disapproved", "iesg_approved"))
|
|
return e != None and e.type == "iesg_disapproved"
|
|
|
|
#dnp_date = models.DateField(null=True, blank=True)
|
|
@property
|
|
def dnp_date(self):
|
|
e = self.latest_event(type__in=("iesg_disapproved", "iesg_approved"))
|
|
return e.time.date() if e != None and e.type == "iesg_disapproved" else None
|
|
|
|
#noproblem = models.IntegerField(null=True, blank=True)
|
|
@property
|
|
def noproblem(self):
|
|
e = self.latest_event(type__in=("iesg_disapproved", "iesg_approved"))
|
|
return e != None and e.type == "iesg_approved"
|
|
|
|
#resurrect_requested_by = BrokenForeignKey(IESGLogin, db_column='resurrect_requested_by', related_name='docsresurrected', null=True, blank=True)
|
|
@property
|
|
def resurrect_requested_by(self):
|
|
e = self.latest_event(type__in=("requested_resurrect", "completed_resurrect"))
|
|
from ietf.person.proxy import IESGLogin as IESGLoginProxy
|
|
return IESGLoginProxy().from_object(e.by) if e and e.type == "requested_resurrect" else None
|
|
|
|
#approved_in_minute = models.IntegerField(null=True, blank=True)
|
|
@property
|
|
def approved_in_minute(self):
|
|
return self.latest_event(type="approved_in_minute")
|
|
|
|
|
|
def get_absolute_url(self):
|
|
if self.rfc_flag and self.rfc_number:
|
|
return "/doc/rfc%d/" % self.rfc_number
|
|
else:
|
|
return "/doc/%s/" % self.name
|
|
|
|
def document(self):
|
|
return self
|
|
|
|
def comments(self):
|
|
return DocumentComment.objects.filter(doc=self).order_by('-time')
|
|
|
|
def public_comments(self):
|
|
return self.comments()
|
|
|
|
def ballot_set(self):
|
|
return [self]
|
|
def ballot_primary(self):
|
|
return [self]
|
|
def ballot_others(self):
|
|
return []
|
|
def docstate(self):
|
|
s = self.get_state("draft-iesg")
|
|
if s:
|
|
return s.name
|
|
else:
|
|
return "I-D Exists"
|
|
|
|
# things from BallotInfo
|
|
#active = models.BooleanField()
|
|
@property
|
|
def active(self):
|
|
# taken from BallotWrapper
|
|
s = self.get_state("draft-iesg")
|
|
return self.latest_event(type="sent_ballot_announcement") and s and s.name in ['In Last Call', 'Waiting for Writeup', 'Waiting for AD Go-Ahead', 'IESG Evaluation', 'IESG Evaluation - Defer'] and (self.get_state_slug() in ("rfc", "active"))
|
|
|
|
#an_sent = models.BooleanField()
|
|
@property
|
|
def an_sent(self):
|
|
return bool(self.latest_event(type="iesg_approved"))
|
|
|
|
#an_sent_date = models.DateField(null=True, blank=True)
|
|
@property
|
|
def an_sent_date(self):
|
|
e = self.latest_event(type="iesg_approved")
|
|
return e.time if e else None
|
|
|
|
#an_sent_by = models.ForeignKey(IESGLogin, db_column='an_sent_by', related_name='ansent', null=True)
|
|
@property
|
|
def an_sent_by(self):
|
|
e = self.latest_event(type="iesg_approved")
|
|
from ietf.person.proxy import IESGLogin as IESGLoginProxy
|
|
return IESGLoginProxy().from_object(e.by) if e else None
|
|
|
|
#defer = models.BooleanField()
|
|
@property
|
|
def defer(self):
|
|
# we're deferred if we're in the deferred state
|
|
return self.get_state_slug("draft-iesg") == "defer"
|
|
|
|
#defer_by = models.ForeignKey(IESGLogin, db_column='defer_by', related_name='deferred', null=True)
|
|
@property
|
|
def defer_by(self):
|
|
e = self.latest_event(type="changed_document", desc__startswith="State changed to <b>IESG Evaluation - Defer</b>")
|
|
from ietf.person.proxy import IESGLogin as IESGLoginProxy
|
|
return IESGLoginProxy().from_object(e.by) if e else None
|
|
|
|
#defer_date = models.DateField(null=True, blank=True)
|
|
@property
|
|
def defer_date(self):
|
|
e = self.latest_event(type="changed_document", desc__startswith="State changed to <b>IESG Evaluation - Defer</b>")
|
|
return e.time.date() if e else None
|
|
|
|
#approval_text = models.TextField(blank=True)
|
|
@property
|
|
def approval_text(self):
|
|
e = self.latest_event(WriteupDocEvent, type="changed_ballot_approval_text")
|
|
return e.text if e else ""
|
|
|
|
#last_call_text = models.TextField(blank=True)
|
|
@property
|
|
def last_call_text(self):
|
|
e = self.latest_event(WriteupDocEvent, type="changed_last_call_text")
|
|
return e.text if e else ""
|
|
|
|
#ballot_writeup = models.TextField(blank=True)
|
|
@property
|
|
def ballot_writeup(self):
|
|
e = self.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text")
|
|
return e.text if e else ""
|
|
|
|
#ballot_issued = models.IntegerField(null=True, blank=True)
|
|
@property
|
|
def ballot_issued(self):
|
|
return bool(self.latest_event(type="sent_ballot_announcement"))
|
|
|
|
# def remarks(self): # apparently not used
|
|
# remarks = list(self.discusses.all()) + list(self.comments.all())
|
|
# return remarks
|
|
def active_positions(self):
|
|
"""Returns a list of dicts, with AD and Position tuples"""
|
|
from ietf.person.proxy import IESGLogin as IESGLoginProxy
|
|
from ietf.doc.utils import active_ballot_positions
|
|
|
|
res = []
|
|
for ad, pos in active_ballot_positions(self).iteritems():
|
|
res.append(dict(ad=IESGLoginProxy().from_object(ad), pos=Position().from_object(pos) if pos else None))
|
|
|
|
res.sort(key=lambda x: x["ad"].last_name)
|
|
|
|
return res
|
|
|
|
def needed(self, standardsTrack=True):
|
|
"""Returns text answering the question what does this document
|
|
need to pass?. The return value is only useful if the document
|
|
is currently in IESG evaluation."""
|
|
tmp = self.active_positions()
|
|
positions = [x["pos"] for x in tmp if x["pos"]]
|
|
ads = [x["ad"] for x in tmp]
|
|
|
|
yes = noobj = discuss = recuse = 0
|
|
for position in positions:
|
|
p = position.pos_id
|
|
if p == "yes":
|
|
yes += 1
|
|
if p == "noobj":
|
|
noobj += 1
|
|
if p == "discuss":
|
|
discuss += 1
|
|
if p == "recuse":
|
|
recuse += 1
|
|
answer = ''
|
|
if yes < 1:
|
|
answer += "Needs a YES. "
|
|
if discuss > 0:
|
|
if discuss == 1:
|
|
answer += "Has a DISCUSS. "
|
|
else:
|
|
answer += "Has %d DISCUSSes. " % discuss
|
|
if standardsTrack:
|
|
# For standards-track, need positions from 2/3 of the
|
|
# non-recused current IESG.
|
|
needed = int((len(ads) - recuse) * 2 / 3)
|
|
else:
|
|
# Info and experimental only need one position.
|
|
needed = 1
|
|
have = yes + noobj + discuss
|
|
if have < needed:
|
|
more = needed - have
|
|
if more == 1:
|
|
answer += "Needs %d more position. " % more
|
|
else:
|
|
answer += "Needs %d more positions. " % more
|
|
else:
|
|
answer += "Has enough positions to pass"
|
|
if discuss:
|
|
answer += " once DISCUSSes are resolved"
|
|
answer += ". "
|
|
|
|
return answer.rstrip()
|
|
|
|
|
|
# things from RfcIndex
|
|
|
|
#rfc_number = models.IntegerField(primary_key=True) # already taken care of
|
|
#title = models.CharField(max_length=250) # same name
|
|
#authors = models.CharField(max_length=250) # exists already
|
|
#rfc_published_date = models.DateField()
|
|
@property
|
|
def rfc_published_date(self):
|
|
if hasattr(self, 'published_rfc'):
|
|
e = self.published_rfc
|
|
else:
|
|
e = self.latest_event(type="published_rfc")
|
|
return e.time.date() if e else datetime.date(1990,1,1)
|
|
|
|
#current_status = models.CharField(max_length=50,null=True)
|
|
@property
|
|
def current_status(self):
|
|
return self.std_level.name
|
|
|
|
#updates = models.CharField(max_length=200,blank=True,null=True)
|
|
@property
|
|
def updates(self):
|
|
return ",".join("RFC%s" % n for n in sorted(d.rfc_number for d in InternetDraft.objects.filter(docalias__relateddocument__source=self, docalias__relateddocument__relationship="updates")))
|
|
|
|
#updated_by = models.CharField(max_length=200,blank=True,null=True)
|
|
@property
|
|
def updated_by(self):
|
|
if not hasattr(self, "updated_by_list"):
|
|
self.updated_by_list = [d.rfc_number for d in InternetDraft.objects.filter(relateddocument__target__document=self, relateddocument__relationship="updates")]
|
|
return ",".join("RFC%s" % n for n in sorted(self.updated_by_list))
|
|
|
|
#obsoletes = models.CharField(max_length=200,blank=True,null=True)
|
|
@property
|
|
def obsoletes(self):
|
|
return ",".join("RFC%s" % n for n in sorted(d.rfc_number for d in InternetDraft.objects.filter(docalias__relateddocument__source=self, docalias__relateddocument__relationship="obs")))
|
|
|
|
#obsoleted_by = models.CharField(max_length=200,blank=True,null=True)
|
|
@property
|
|
def obsoleted_by(self):
|
|
if not hasattr(self, "obsoleted_by_list"):
|
|
self.obsoleted_by_list = [d.rfc_number for d in InternetDraft.objects.filter(relateddocument__target__document=self, relateddocument__relationship="obs")]
|
|
return ",".join("RFC%s" % n for n in sorted(self.obsoleted_by_list))
|
|
|
|
#also = models.CharField(max_length=50,blank=True,null=True)
|
|
@property
|
|
def also(self):
|
|
aliases = self.docalias_set.filter(models.Q(name__startswith="bcp") |
|
|
models.Q(name__startswith="std") |
|
|
models.Q(name__startswith="bcp"))
|
|
return aliases[0].name.upper() if aliases else None
|
|
|
|
#draft = models.CharField(max_length=200,null=True) # have to ignore this, it's already implemented
|
|
|
|
#has_errata = models.BooleanField()
|
|
@property
|
|
def has_errata(self):
|
|
return bool(self.tags.filter(slug="errata"))
|
|
|
|
#stream = models.CharField(max_length=15,blank=True,null=True)
|
|
@property
|
|
def stream(self):
|
|
s = super(InternetDraft, self).stream
|
|
return s.name if s else None
|
|
|
|
#wg = models.CharField(max_length=15,blank=True,null=True)
|
|
@property
|
|
def wg(self):
|
|
return self.group.acronym
|
|
|
|
#file_formats = models.CharField(max_length=20,blank=True,null=True)
|
|
@property
|
|
def file_formats(self):
|
|
return self.get_file_type_matches_from(os.path.join(settings.RFC_PATH, "rfc" + str(self.rfc_number) + ".*")).replace(".", "").replace("txt", "ascii")
|
|
|
|
@property
|
|
def positions(self):
|
|
res = []
|
|
found = set()
|
|
for pos in Position.objects.filter(doc=self, type="changed_ballot_position").select_related('ad').order_by("-time", "-id"):
|
|
if pos.ad not in found:
|
|
found.add(pos.ad)
|
|
res.append(pos)
|
|
|
|
class Dummy:
|
|
def all(self):
|
|
return self.res
|
|
d = Dummy()
|
|
d.res = res
|
|
return d
|
|
|
|
@property
|
|
def ipr(self):
|
|
from ietf.ipr.models import IprDraftProxy
|
|
return IprDraftProxy.objects.filter(doc_alias__document=self.pk)
|
|
|
|
class Meta:
|
|
proxy = True
|
|
|
|
IDInternal = InternetDraft
|
|
BallotInfo = InternetDraft
|
|
RfcIndex = InternetDraft
|
|
Rfc = InternetDraft
|
|
|
|
|
|
class IDAuthor(DocumentAuthor):
|
|
#document = models.ForeignKey(InternetDraft, db_column='id_document_tag', related_name='authors') # same name
|
|
#person = models.ForeignKey(PersonOrOrgInfo, db_column='person_or_org_tag')
|
|
@property
|
|
def person(self):
|
|
return self.author.person
|
|
|
|
#author_order = models.IntegerField()
|
|
@property
|
|
def author_order(self):
|
|
return self.order
|
|
|
|
def email(self):
|
|
return None if self.author.address.startswith("unknown-email") else self.author.address
|
|
|
|
def final_author_order(self):
|
|
return self.order
|
|
|
|
class Meta:
|
|
proxy = True
|
|
|
|
class DocumentComment(DocEvent):
|
|
objects = TranslatingManager(dict(comment_text="desc",
|
|
date="time"
|
|
))
|
|
|
|
BALLOT_DISCUSS = 1
|
|
BALLOT_COMMENT = 2
|
|
BALLOT_CHOICES = (
|
|
(BALLOT_DISCUSS, 'discuss'),
|
|
(BALLOT_COMMENT, 'comment'),
|
|
)
|
|
#document = models.ForeignKey(IDInternal)
|
|
@property
|
|
def document(self):
|
|
return self.doc
|
|
#rfc_flag = models.IntegerField(null=True, blank=True)
|
|
#public_flag = models.BooleanField() #unused
|
|
#date = models.DateField(db_column='comment_date', default=datetime.date.today)
|
|
@property
|
|
def date(self):
|
|
return self.time.date()
|
|
#time = models.CharField(db_column='comment_time', max_length=20, default=lambda: datetime.datetime.now().strftime("%H:%M:%S"))
|
|
#version = models.CharField(blank=True, max_length=3)
|
|
@property
|
|
def version(self):
|
|
e = self.doc.latest_event(NewRevisionDocEvent, type="new_revision", time__lte=self.time)
|
|
return e.rev if e else "0"
|
|
#comment_text = models.TextField(blank=True)
|
|
@property
|
|
def comment_text(self):
|
|
return self.desc
|
|
#created_by = BrokenForeignKey(IESGLogin, db_column='created_by', null=True, null_values=(0, 999))
|
|
#result_state = BrokenForeignKey(IDState, db_column='result_state', null=True, related_name="comments_leading_to_state", null_values=(0, 99))
|
|
#origin_state = models.ForeignKey(IDState, db_column='origin_state', null=True, related_name="comments_coming_from_state")
|
|
#ballot = models.IntegerField(null=True, choices=BALLOT_CHOICES)
|
|
def get_absolute_url(self):
|
|
return "/doc/%s/" % self.doc.name
|
|
def get_author(self):
|
|
return self.by.plain_name()
|
|
def get_username(self):
|
|
return unicode(self.by)
|
|
def get_fullname(self):
|
|
return self.by.plain_name()
|
|
def datetime(self):
|
|
return self.time
|
|
def doc_id(self):
|
|
return self.doc_id
|
|
def doc_name(self):
|
|
return self.doc.name
|
|
def __str__(self):
|
|
return "\"%s...\" by %s" % (self.comment_text[:20], self.get_author())
|
|
|
|
class Meta:
|
|
proxy = True
|
|
|
|
|
|
class Position(BallotPositionDocEvent):
|
|
def from_object(self, base):
|
|
for f in base._meta.fields:
|
|
if not f.name in ('discuss',): # don't overwrite properties
|
|
setattr(self, f.name, getattr(base, f.name))
|
|
|
|
self.orig = base
|
|
return self
|
|
|
|
#ballot = models.ForeignKey(BallotInfo, related_name='positions')
|
|
@property
|
|
def ballot(self):
|
|
return self.doc # FIXME: doesn't emulate old interface
|
|
|
|
# ad = models.ForeignKey(IESGLogin) # same name
|
|
#yes = models.IntegerField(db_column='yes_col')
|
|
@property
|
|
def yes(self):
|
|
return self.pos_id == "yes"
|
|
#noobj = models.IntegerField(db_column='no_col')
|
|
@property
|
|
def noobj(self):
|
|
return self.pos_id == "noobj"
|
|
#abstain = models.IntegerField()
|
|
@property
|
|
def abstain(self):
|
|
return self.pos_id == "abstain"
|
|
#approve = models.IntegerField(default=0) # unused
|
|
#discuss = models.IntegerField()
|
|
# needs special treatment because of clash with attribute on base class
|
|
def get_discuss(self):
|
|
return self.pos_id == "discuss"
|
|
def set_discuss(self, x):
|
|
pass
|
|
discuss = property(get_discuss, set_discuss)
|
|
#recuse = models.IntegerField()
|
|
@property
|
|
def recuse(self):
|
|
return self.pos_id == "recuse"
|
|
def __str__(self):
|
|
return "Position for %s on %s" % ( self.ad, self.ballot )
|
|
def abstain_ind(self):
|
|
if self.recuse:
|
|
return 'R'
|
|
if self.abstain:
|
|
return 'X'
|
|
else:
|
|
return ' '
|
|
def name(self):
|
|
return self.pos.name if self.pos else "No Record"
|
|
|
|
class Meta:
|
|
proxy = True
|
|
|
|
class DraftLikeDocAlias(DocAlias):
|
|
# this class is mostly useful for the IPR part
|
|
|
|
def __str__(self):
|
|
return str(unicode(self))
|
|
|
|
def __unicode__(self):
|
|
if self.name.startswith("rfc"):
|
|
return "RFC%04d" % int(self.name[3:])
|
|
else:
|
|
return self.name
|
|
|
|
@property
|
|
def id_document_tag(self):
|
|
return self.name
|
|
|
|
@property
|
|
def title(self):
|
|
return self.document.title
|
|
|
|
@property
|
|
def filename(self):
|
|
return self.name
|
|
|
|
@property
|
|
def ipr(self):
|
|
from ietf.ipr.models import IprDraftProxy
|
|
return IprDraftProxy.objects.filter(doc_alias=self.pk)
|
|
|
|
class Meta:
|
|
proxy = True
|
|
|
|
class ObjectHistoryEntryProxy(DocEvent):
|
|
#date = models.DateTimeField(_('Date'), auto_now_add=True)
|
|
@property
|
|
def date(self):
|
|
return self.time
|
|
#comment = models.TextField(_('Comment'))
|
|
@property
|
|
def comment(self):
|
|
return ""
|
|
#person = models.ForeignKey(PersonOrOrgInfo)
|
|
@property
|
|
def person(self):
|
|
return self.by
|
|
|
|
def get_real_instance(self):
|
|
return self
|
|
|
|
def describe_change(self):
|
|
return u"<p>%s</p>" % self.desc
|
|
|
|
class Meta:
|
|
proxy = True
|
|
|
|
class IDStatus(State):
|
|
def from_object(self, base):
|
|
for f in base._meta.fields:
|
|
setattr(self, f.name, getattr(base, f.name))
|
|
return self
|
|
|
|
#status_id = models.AutoField(primary_key=True)
|
|
|
|
#status = models.CharField(max_length=25, db_column='status_value')
|
|
@property
|
|
def status(self):
|
|
return self.name
|
|
|
|
def __unicode__(self):
|
|
return super(self.__class__, self).__unicode__()
|
|
|
|
class Meta:
|
|
proxy = True
|
|
|
|
class IDState(State):
|
|
PUBLICATION_REQUESTED = 10
|
|
LAST_CALL_REQUESTED = 15
|
|
IN_LAST_CALL = 16
|
|
WAITING_FOR_WRITEUP = 18
|
|
WAITING_FOR_AD_GO_AHEAD = 19
|
|
IESG_EVALUATION = 20
|
|
IESG_EVALUATION_DEFER = 21
|
|
APPROVED_ANNOUNCEMENT_SENT = 30
|
|
AD_WATCHING = 42
|
|
DEAD = 99
|
|
DO_NOT_PUBLISH_STATES = (33, 34)
|
|
|
|
objects = TranslatingManager(dict(pk=lambda v: ("order", v, "type", "draft-iesg"),
|
|
document_state_id=lambda v: ("order", v, "type", "draft-iesg"),
|
|
document_state_id__in=lambda v: ("order__in", v, "type", "draft-iesg")),
|
|
always_filter=dict(type="draft-iesg"))
|
|
|
|
def from_object(self, base):
|
|
for f in base._meta.fields:
|
|
setattr(self, f.name, getattr(base, f.name))
|
|
return self
|
|
|
|
#document_state_id = models.AutoField(primary_key=True)
|
|
@property
|
|
def document_state_id(self):
|
|
return self.order
|
|
|
|
#state = models.CharField(max_length=50, db_column='document_state_val')
|
|
@property
|
|
def state(self):
|
|
return self.name
|
|
|
|
#equiv_group_flag = models.IntegerField(null=True, blank=True) # unused
|
|
#description = models.TextField(blank=True, db_column='document_desc')
|
|
@property
|
|
def description(self):
|
|
return self.desc
|
|
|
|
@property
|
|
def nextstate(self):
|
|
# simulate related queryset
|
|
return IDState.objects.filter(pk__in=[x.pk for x in self.next_states.all()])
|
|
|
|
@property
|
|
def next_state(self):
|
|
# simulate IDNextState
|
|
return self
|
|
|
|
def __str__(self):
|
|
return self.state
|
|
|
|
@staticmethod
|
|
def choices():
|
|
return [(state.pk, state.name) for state in IDState.objects.all()]
|
|
|
|
class Meta:
|
|
proxy = True
|
|
|
|
|