import os import re import urllib import math from django.conf import settings from ietf.utils import markup_txt from ietf.doc.models import DocAlias, RelatedDocument, BallotType, DocReminder from ietf.doc.models import DocEvent, BallotDocEvent, NewRevisionDocEvent, StateDocEvent from ietf.name.models import DocReminderTypeName, DocRelationshipName from ietf.group.models import Role from ietf.ietfauth.utils import has_role from ietf.person.models import Person from ietf.utils import draft def get_state_types(doc): res = [] if not doc: return res res.append(doc.type_id) if doc.type_id == "draft": if doc.stream_id and doc.stream_id != "legacy": res.append("draft-stream-%s" % doc.stream_id) res.append("draft-iesg") res.append("draft-iana-review") res.append("draft-iana-action") res.append("draft-rfceditor") return res def get_tags_for_stream_id(stream_id): if stream_id == "ietf": return ["w-expert", "w-extern", "w-merge", "need-aut", "w-refdoc", "w-refing", "rev-wg", "rev-wglc", "rev-ad", "rev-iesg", "sheph-u", "no-adopt", "other"] elif stream_id == "iab": return ["need-ed", "w-part", "w-review", "need-rev", "sh-f-up"] elif stream_id == "irtf": return ["need-ed", "need-sh", "w-dep", "need-rev", "iesg-com"] elif stream_id == "ise": return ["w-dep", "w-review", "need-rev", "iesg-com"] else: return [] def can_adopt_draft(user, doc): if not user.is_authenticated(): return False if has_role(user, "Secretariat"): return True return (doc.stream_id in (None, "ietf", "irtf") and doc.group.type_id == "individ" and Role.objects.filter(name__in=("chair", "delegate", "secr"), group__type__in=("wg", "rg"), group__state="active", person__user=user).exists()) def needed_ballot_positions(doc, active_positions): '''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.''' yes = [p for p in active_positions if p and p.pos_id == "yes"] noobj = [p for p in active_positions if p and p.pos_id == "noobj"] blocking = [p for p in active_positions if p and p.pos.blocking] recuse = [p for p in active_positions if p and p.pos_id == "recuse"] answer = [] if len(yes) < 1: answer.append("Needs a YES.") if blocking: if len(blocking) == 1: answer.append("Has a %s." % blocking[0].pos.name.upper()) else: if blocking[0].pos.name.upper().endswith('S'): answer.append("Has %d %ses." % (len(blocking), blocking[0].pos.name.upper())) else: answer.append("Has %d %ss." % (len(blocking), blocking[0].pos.name.upper())) needed = 1 if doc.type_id == "draft" and doc.intended_std_level_id in ("bcp", "ps", "ds", "std"): # For standards-track, need positions from 2/3 of the # non-recused current IESG. active = len(Person.objects.filter(role__name="ad", role__group__state="active").distinct()) needed = int(math.ceil((active - len(recuse)) * 2.0/3.0)) else: if len(yes) < 1: return " ".join(answer) have = len(yes) + len(noobj) if have < needed: more = needed - have if more == 1: answer.append("Needs one more YES or NO OBJECTION position to pass.") else: answer.append("Needs %d more YES or NO OBJECTION positions to pass." % more) else: if blocking: answer.append("Has enough positions to pass once %s positions are resolved." % blocking[0].pos.name.upper()) else: answer.append("Has enough positions to pass.") return " ".join(answer) def create_ballot_if_not_open(doc, by, ballot_slug): if not doc.ballot_open(ballot_slug): e = BallotDocEvent(type="created_ballot", by=by, doc=doc) e.ballot_type = BallotType.objects.get(doc_type=doc.type, slug=ballot_slug) e.desc = u'Created "%s" ballot' % e.ballot_type.name e.save() def close_ballot(doc, by, ballot_slug): if doc.ballot_open(ballot_slug): e = BallotDocEvent(type="closed_ballot", doc=doc, by=by) e.ballot_type = BallotType.objects.get(doc_type=doc.type,slug=ballot_slug) e.desc = 'Closed "%s" ballot' % e.ballot_type.name e.save() def close_open_ballots(doc, by): for t in BallotType.objects.filter(doc_type=doc.type_id): close_ballot(doc, by, t.slug ) def augment_with_start_time(docs): """Add a started_time attribute to each document with the time of the first revision.""" docs = list(docs) docs_dict = {} for d in docs: docs_dict[d.pk] = d d.start_time = None seen = set() for e in DocEvent.objects.filter(type="new_revision", doc__in=docs).order_by('time'): if e.doc_id in seen: continue docs_dict[e.doc_id].start_time = e.time seen.add(e.doc_id) return docs def get_chartering_type(doc): chartering = "" if doc.get_state_slug() not in ("notrev", "approved"): if doc.group.state_id in ("proposed", "bof"): chartering = "initial" elif doc.group.state_id == "active": chartering = "rechartering" return chartering def augment_events_with_revision(doc, events): """Take a set of events for doc and add a .rev attribute with the revision they refer to by checking NewRevisionDocEvents.""" event_revisions = list(NewRevisionDocEvent.objects.filter(doc=doc).order_by('time', 'id').values('id', 'rev', 'time')) if doc.type_id == "draft" and doc.get_state_slug() == "rfc": # add fake "RFC" revision e = doc.latest_event(type="published_rfc") if e: event_revisions.append(dict(id=e.id, time=e.time, rev="RFC")) event_revisions.sort(key=lambda x: (x["time"], x["id"])) for e in sorted(events, key=lambda e: (e.time, e.id), reverse=True): while event_revisions and (e.time, e.id) < (event_revisions[-1]["time"], event_revisions[-1]["id"]): event_revisions.pop() if event_revisions: cur_rev = event_revisions[-1]["rev"] else: cur_rev = "00" e.rev = cur_rev def add_links_in_new_revision_events(doc, events, diff_revisions): """Add direct .txt links and diff links to new_revision events.""" prev = None diff_urls = dict(((name, revision), url) for name, revision, time, url in diff_revisions) for e in sorted(events, key=lambda e: (e.time, e.id)): if not e.type == "new_revision": continue if not (e.doc.name, e.rev) in diff_urls: continue full_url = diff_url = diff_urls[(e.doc.name, e.rev)] if doc.type_id in "draft": # work around special diff url for drafts full_url = "http://tools.ietf.org/id/" + diff_url + ".txt" # build links links = r'\1' % full_url if prev: links += "" if prev != None: links += ' (diff from previous)' % (settings.RFCDIFF_PREFIX, urllib.quote(prev, safe="~"), urllib.quote(diff_url, safe="~")) # replace the bold filename part e.desc = re.sub(r"(.+-[0-9][0-9].txt)", links, e.desc) prev = diff_url def get_document_content(key, filename, split=True, markup=True): f = None try: f = open(filename, 'rb') raw_content = f.read() except IOError: error = "Error; cannot read ("+key+")" return error finally: if f: f.close() if markup: return markup_txt.markup(raw_content, split) else: return raw_content def add_state_change_event(doc, by, prev_state, new_state, prev_tags=[], new_tags=[], timestamp=None): """Add doc event to explain that state change just happened.""" if prev_state and new_state: assert prev_state.type_id == new_state.type_id if prev_state == new_state and set(prev_tags) == set(new_tags): return None def tags_suffix(tags): return (u"::" + u"::".join(t.name for t in tags)) if tags else u"" e = StateDocEvent(doc=doc, by=by) e.type = "changed_state" e.state_type = (prev_state or new_state).type e.state = new_state e.desc = "%s changed to %s" % (e.state_type.label, new_state.name + tags_suffix(new_tags)) if prev_state: e.desc += " from %s" % (prev_state.name + tags_suffix(prev_tags)) if timestamp: e.time = timestamp e.save() return e def update_reminder(doc, reminder_type_slug, event, due_date): reminder_type = DocReminderTypeName.objects.get(slug=reminder_type_slug) try: reminder = DocReminder.objects.get(event__doc=doc, type=reminder_type, active=True) except DocReminder.DoesNotExist: reminder = None if due_date: # activate/update reminder if not reminder: reminder = DocReminder(type=reminder_type) reminder.event = event reminder.due = due_date reminder.active = True reminder.save() else: # deactivate reminder if reminder: reminder.active = False reminder.save() def prettify_std_name(n): if re.match(r"(rfc|bcp|fyi|std)[0-9]+", n): return n[:3].upper() + " " + n[3:] else: return n def nice_consensus(consensus): mapping = { None: "Unknown", True: "Yes", False: "No" } return mapping[consensus] 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") prev_returning = bool(prev and prev.returning_item) prev_telechat = prev.telechat_date if prev else None prev_agenda = bool(prev_telechat) returning_item_changed = bool(new_returning_item != None and new_returning_item != prev_returning) if new_returning_item == None: returning = prev_returning else: returning = new_returning_item if returning == prev_returning and new_telechat_date == prev_telechat: # fully updated, nothing to do return # auto-update returning item if (not returning_item_changed and on_agenda and prev_agenda and new_telechat_date != prev_telechat): returning = True e = TelechatDocEvent() e.type = "scheduled_for_telechat" e.by = by e.doc = doc e.returning_item = returning e.telechat_date = new_telechat_date if on_agenda != prev_agenda: if on_agenda: e.desc = "Placed on agenda for telechat - %s" % (new_telechat_date) else: e.desc = "Removed from agenda for telechat" elif on_agenda and new_telechat_date != prev_telechat: e.desc = "Telechat date has been changed to %s from %s" % ( new_telechat_date, prev_telechat) else: # we didn't reschedule but flipped returning item bit - let's # just explain that if returning: e.desc = "Set telechat returning item indication" else: e.desc = "Removed telechat returning item indication" e.save() def rebuild_reference_relations(doc): if doc.type.slug != 'draft': return None if doc.get_state_slug() == 'rfc': filename=os.path.join(settings.RFC_PATH,doc.canonical_name()+".txt") else: filename=os.path.join(settings.INTERNET_DRAFT_PATH,doc.filename_with_rev()) try: refs = draft.Draft(draft._gettext(filename), filename).get_refs() except IOError as e: return { 'errors': ["%s :%s" % (e.strerror, filename)] } doc.relateddocument_set.filter(relationship__slug__in=['refnorm','refinfo','refold','refunk']).delete() warnings = [] errors = [] unfound = set() for ( ref, refType ) in refs.iteritems(): refdoc = DocAlias.objects.filter( name=ref ) count = refdoc.count() if count == 0: unfound.add( "%s" % ref ) continue elif count > 1: errors.append("Too many DocAlias objects found for %s"%ref) else: # Don't add references to ourself if doc != refdoc[0].document: RelatedDocument.objects.get_or_create( source=doc, target=refdoc[ 0 ], relationship=DocRelationshipName.objects.get( slug='ref%s' % refType ) ) if unfound: warnings.append('There were %d references with no matching DocAlias'%len(unfound)) ret = {} if errors: ret['errors']=errors if warnings: ret['warnings']=warnings if unfound: ret['unfound']=list(unfound) return ret