datatracker/ietf/submit/utils.py
2012-07-28 20:19:36 +00:00

616 lines
23 KiB
Python

import os
import re
import datetime
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse as urlreverse
from django.template.loader import render_to_string
from ietf.idtracker.models import (InternetDraft, PersonOrOrgInfo, IETFWG,
IDAuthor, EmailAddress, IESGLogin, BallotInfo)
from ietf.submit.models import TempIdAuthors, IdSubmissionDetail, Preapproval
from ietf.utils.mail import send_mail, send_mail_message
from ietf.utils.log import log
from ietf.utils import unaccent
from ietf.ietfauth.decorators import has_role
from ietf.doc.models import *
from ietf.person.models import Person, Alias, Email
from ietf.doc.utils import active_ballot_positions
from ietf.message.models import Message
# Some useful states
UPLOADED = 1
WAITING_AUTHENTICATION = 4
MANUAL_POST_REQUESTED = 5
POSTED = -1
POSTED_BY_SECRETARIAT = -2
CANCELED = -4
INITIAL_VERSION_APPROVAL_REQUESTED = 10
# Not a real WG
NONE_WG = 1027
def request_full_url(request, submission):
subject = 'Full URL for managing submission of draft %s' % submission.filename
from_email = settings.IDSUBMIT_FROM_EMAIL
to_email = list(set(u'%s <%s>' % i.email() for i in submission.tempidauthors_set.all()))
url = settings.IDTRACKER_BASE_URL + urlreverse('draft_status_by_hash',
kwargs=dict(submission_id=submission.submission_id,
submission_hash=submission.get_hash()))
send_mail(request, to_email, from_email, subject, 'submit/request_full_url.txt',
{'submission': submission,
'url': url})
def perform_post(request, submission):
group_id = submission.group_acronym and submission.group_acronym.pk or NONE_WG
state_change_msg = ''
try:
draft = InternetDraft.objects.get(filename=submission.filename)
draft.title = submission.id_document_name
draft.group_id = group_id
draft.filename = submission.filename
draft.revision = submission.revision
draft.revision_date = submission.submission_date
draft.file_type = submission.file_type
draft.txt_page_count = submission.txt_page_count
draft.last_modified_date = datetime.date.today()
draft.abstract = submission.abstract
draft.status_id = 1 # Active
draft.expired_tombstone = 0
draft.save()
except InternetDraft.DoesNotExist:
draft = InternetDraft.objects.create(
title=submission.id_document_name,
group_id=group_id,
filename=submission.filename,
revision=submission.revision,
revision_date=submission.submission_date,
file_type=submission.file_type,
txt_page_count=submission.txt_page_count,
start_date=datetime.date.today(),
last_modified_date=datetime.date.today(),
abstract=submission.abstract,
status_id=1, # Active
intended_status_id=8, # None
)
update_authors(draft, submission)
if draft.idinternal:
from ietf.idrfc.utils import add_document_comment
add_document_comment(None, draft, "New version available")
if draft.idinternal.cur_sub_state_id == 5 and draft.idinternal.rfc_flag == 0: # Substate 5 Revised ID Needed
draft.idinternal.prev_sub_state_id = draft.idinternal.cur_sub_state_id
draft.idinternal.cur_sub_state_id = 2 # Substate 2 AD Followup
draft.idinternal.save()
state_change_msg = "Sub state has been changed to AD Follow up from New Id Needed"
add_document_comment(None, draft, state_change_msg)
move_docs(submission)
submission.status_id = POSTED
send_announcements(submission, draft, state_change_msg)
submission.save()
def perform_postREDESIGN(request, submission):
system = Person.objects.get(name="(System)")
group_id = submission.group_acronym_id or NONE_WG
try:
draft = Document.objects.get(name=submission.filename)
save_document_in_history(draft)
except Document.DoesNotExist:
draft = Document(name=submission.filename)
draft.intended_std_level = None
prev_rev = draft.rev
draft.type_id = "draft"
draft.time = datetime.datetime.now()
draft.title = submission.id_document_name
if not (group_id == NONE_WG and draft.group and draft.group.type_id == "area"):
# don't overwrite an assigned area if it's still an individual
# submission
draft.group_id = group_id
draft.rev = submission.revision
draft.pages = submission.txt_page_count
draft.abstract = submission.abstract
was_rfc = draft.get_state_slug() == "rfc"
if not draft.stream:
stream_slug = None
if draft.name.startswith("draft-iab-"):
stream_slug = "iab"
elif draft.name.startswith("draft-irtf-"):
stream_slug = "irtf"
elif draft.name.startswith("draft-ietf-") and (draft.group.type_id != "individ" or was_rfc):
stream_slug = "ietf"
if stream_slug:
draft.stream = StreamName.objects.get(slug=stream_slug)
draft.expires = datetime.datetime.now() + datetime.timedelta(settings.INTERNET_DRAFT_DAYS_TO_EXPIRE)
draft.save()
draft.set_state(State.objects.get(type="draft", slug="active"))
if draft.stream_id == "ietf" and draft.group.type_id == "wg" and draft.rev == "00":
# automatically set state "WG Document"
draft.set_state(State.objects.get(type="draft-stream-%s" % draft.stream_id, slug="wg-doc"))
DocAlias.objects.get_or_create(name=submission.filename, document=draft)
update_authors(draft, submission)
# new revision event
a = submission.tempidauthors_set.filter(author_order=0)
if a:
submitter = ensure_person_email_info_exists(a[0]).person
else:
submitter = system
e = NewRevisionDocEvent(type="new_revision", doc=draft, rev=draft.rev)
e.time = draft.time #submission.submission_date
e.by = submitter
e.desc = "New revision available"
e.save()
# clean up old files
if prev_rev != draft.rev:
from ietf.idrfc.expire import move_draft_files_to_archive
move_draft_files_to_archive(draft, prev_rev)
# automatic state changes
state_change_msg = ""
if not was_rfc and draft.tags.filter(slug="need-rev"):
draft.tags.remove("need-rev")
draft.tags.add("ad-f-up")
e = DocEvent(type="changed_document", doc=draft)
e.desc = "Sub state has been changed to <b>AD Followup</b> from <b>Revised ID Needed</b>"
e.by = system
e.save()
state_change_msg = e.desc
move_docs(submission)
submission.status_id = POSTED
announce_to_lists(request, submission)
if draft.get_state("draft-iesg") != None and not was_rfc:
announce_new_version(request, submission, draft, state_change_msg)
announce_to_authors(request, submission)
submission.save()
if settings.USE_DB_REDESIGN_PROXY_CLASSES:
perform_post = perform_postREDESIGN
def send_announcements(submission, draft, state_change_msg):
announce_to_lists(request, submission)
if draft.idinternal and not draft.idinternal.rfc_flag:
announce_new_version(request, submission, draft, state_change_msg)
announce_to_authors(request, submission)
def announce_to_lists(request, submission):
authors = []
for i in submission.tempidauthors_set.order_by('author_order'):
if not i.author_order:
continue
authors.append(i.get_full_name())
if settings.USE_DB_REDESIGN_PROXY_CLASSES:
m = Message()
m.by = Person.objects.get(name="(System)")
if request.user.is_authenticated():
try:
m.by = request.user.get_profile()
except Person.DoesNotExist:
pass
m.subject = 'I-D Action: %s-%s.txt' % (submission.filename, submission.revision)
m.frm = settings.IDSUBMIT_ANNOUNCE_FROM_EMAIL
m.to = settings.IDSUBMIT_ANNOUNCE_LIST_EMAIL
if submission.group_acronym:
m.cc = submission.group_acronym.email_address
m.body = render_to_string('submit/announce_to_lists.txt', dict(submission=submission,
authors=authors,
settings=settings,))
m.save()
m.related_docs.add(Document.objects.get(name=submission.filename))
send_mail_message(request, m)
else:
subject = 'I-D Action: %s-%s.txt' % (submission.filename, submission.revision)
from_email = settings.IDSUBMIT_ANNOUNCE_FROM_EMAIL
to_email = [settings.IDSUBMIT_ANNOUNCE_LIST_EMAIL]
if submission.group_acronym:
cc = [submission.group_acronym.email_address]
else:
cc = None
send_mail(request, to_email, from_email, subject, 'submit/announce_to_lists.txt',
{'submission': submission,
'authors': authors}, cc=cc, save_message=True)
def announce_new_version(request, submission, draft, state_change_msg):
to_email = []
if draft.idinternal.state_change_notice_to:
to_email.append(draft.idinternal.state_change_notice_to)
if draft.idinternal.job_owner:
to_email.append(draft.idinternal.job_owner.person.email()[1])
try:
if draft.idinternal.ballot:
for p in draft.idinternal.ballot.positions.all():
if p.discuss == 1 and p.ad.user_level == IESGLogin.AD_LEVEL:
to_email.append(p.ad.person.email()[1])
except BallotInfo.DoesNotExist:
pass
subject = 'New Version Notification - %s-%s.txt' % (submission.filename, submission.revision)
from_email = settings.IDSUBMIT_ANNOUNCE_FROM_EMAIL
send_mail(request, to_email, from_email, subject, 'submit/announce_new_version.txt',
{'submission': submission,
'msg': state_change_msg})
def announce_new_versionREDESIGN(request, submission, draft, state_change_msg):
to_email = []
if draft.notify:
to_email.append(draft.notify)
if draft.ad:
to_email.append(draft.ad.role_email("ad").address)
for ad, pos in active_ballot_positions(draft).iteritems():
if pos and pos.pos_id == "discuss":
to_email.append(ad.role_email("ad").address)
subject = 'New Version Notification - %s-%s.txt' % (submission.filename, submission.revision)
from_email = settings.IDSUBMIT_ANNOUNCE_FROM_EMAIL
send_mail(request, to_email, from_email, subject, 'submit/announce_new_version.txt',
{'submission': submission,
'msg': state_change_msg})
if settings.USE_DB_REDESIGN_PROXY_CLASSES:
announce_new_version = announce_new_versionREDESIGN
def announce_to_authors(request, submission):
authors = submission.tempidauthors_set.order_by('author_order')
cc = list(set(i.email()[1] for i in authors if i.email() != authors[0].email()))
to_email = [authors[0].email()[1]] # First TempIdAuthor is submitter
from_email = settings.IDSUBMIT_ANNOUNCE_FROM_EMAIL
subject = 'New Version Notification for %s-%s.txt' % (submission.filename, submission.revision)
if submission.group_acronym:
wg = submission.group_acronym.group_acronym.acronym
elif submission.filename.startswith('draft-iesg'):
wg = 'IESG'
else:
wg = 'Individual Submission'
send_mail(request, to_email, from_email, subject, 'submit/announce_to_authors.txt',
{'submission': submission,
'submitter': authors[0].get_full_name(),
'wg': wg}, cc=cc)
def find_person(first_name, last_name, middle_initial, name_suffix, email):
person_list = None
if email:
person_list = PersonOrOrgInfo.objects.filter(emailaddress__address=email).distinct()
if person_list and len(person_list) == 1:
return person_list[0]
if not person_list:
person_list = PersonOrOrgInfo.objects.all()
person_list = person_list.filter(first_name=first_name,
last_name=last_name)
if middle_initial:
person_list = person_list.filter(middle_initial=middle_initial)
if name_suffix:
person_list = person_list.filter(name_suffix=name_suffix)
if person_list:
return person_list[0]
return None
def update_authors(draft, submission):
# TempAuthor of order 0 is submitter
new_authors = list(submission.tempidauthors_set.filter(author_order__gt=0))
person_pks = []
for author in new_authors:
person = find_person(author.first_name, author.last_name,
author.middle_initial, author.name_suffix,
author.email_address)
if not person:
person = PersonOrOrgInfo(
first_name=author.first_name,
last_name=author.last_name,
middle_initial=author.middle_initial or '',
name_suffix=author.name_suffix or '',
)
person.save()
if author.email_address:
EmailAddress.objects.create(
address=author.email_address,
priority=1,
type='INET',
person_or_org=person,
)
person_pks.append(person.pk)
try:
idauthor = IDAuthor.objects.get(
document=draft,
person=person,
)
idauthor.author_order = author.author_order
except IDAuthor.DoesNotExist:
idauthor = IDAuthor(
document=draft,
person=person,
author_order=author.author_order,
)
idauthor.save()
draft.authors.exclude(person__pk__in=person_pks).delete()
def get_person_from_author(author):
persons = None
# try email
if author.email_address:
persons = Person.objects.filter(email__address=author.email_address).distinct()
if len(persons) == 1:
return persons[0]
if not persons:
persons = Person.objects.all()
# try full name
p = persons.filter(alias__name=author.get_full_name()).distinct()
if p:
return p[0]
return None
def ensure_person_email_info_exists(author):
person = get_person_from_author(author)
# make sure we got a person
if not person:
person = Person()
person.name = author.get_full_name()
person.ascii = unaccent.asciify(person.name)
person.save()
Alias.objects.create(name=person.name, person=person)
if person.name != person.ascii:
Alias.objects.create(name=ascii, person=person)
# make sure we got an email address
if author.email_address:
addr = author.email_address.lower()
else:
# we're in trouble, use a fake one
addr = u"unknown-email-%s" % person.name.replace(" ", "-")
try:
email = person.email_set.get(address=addr)
except Email.DoesNotExist:
try:
# maybe it's pointing to someone else
email = Email.objects.get(address=addr)
except Email.DoesNotExist:
# most likely we just need to create it
email = Email(address=addr)
email.active = False
email.person = person
email.save()
return email
def update_authorsREDESIGN(draft, submission):
# order 0 is submitter
authors = []
for author in submission.tempidauthors_set.exclude(author_order=0).order_by('author_order'):
email = ensure_person_email_info_exists(author)
a = DocumentAuthor.objects.filter(document=draft, author=email)
if a:
a = a[0]
else:
a = DocumentAuthor(document=draft, author=email)
a.order = author.author_order
a.save()
authors.append(email)
draft.documentauthor_set.exclude(author__in=authors).delete()
if settings.USE_DB_REDESIGN_PROXY_CLASSES:
update_authors = update_authorsREDESIGN
def get_person_for_user(user):
try:
return user.get_profile().person()
except:
return None
def is_secretariat(user):
if not user or not user.is_authenticated():
return False
return bool(user.groups.filter(name='Secretariat'))
if settings.USE_DB_REDESIGN_PROXY_CLASSES:
from ietf.liaisons.accounts import is_secretariat, get_person_for_user
def move_docs(submission):
for ext in submission.file_type.split(','):
source = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s%s' % (submission.filename, submission.revision, ext))
dest = os.path.join(settings.IDSUBMIT_REPOSITORY_PATH, '%s-%s%s' % (submission.filename, submission.revision, ext))
if os.path.exists(source):
os.rename(source, dest)
else:
if os.path.exists(dest):
log("Intended to move '%s' to '%s', but found source missing while destination exists.", send_mail=True)
else:
raise ValueError("Intended to move '%s' to '%s', but found source and destination missing.")
def remove_docs(submission):
for ext in submission.file_type.split(','):
source = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s%s' % (submission.filename, submission.revision, ext))
if os.path.exists(source):
os.unlink(source)
def get_approvable_submissions(user):
if not user.is_authenticated():
return []
res = IdSubmissionDetail.objects.filter(status=INITIAL_VERSION_APPROVAL_REQUESTED).order_by('-submission_date')
if has_role(user, "Secretariat"):
return res
# those we can reach as chair
return res.filter(group_acronym__role__name="chair", group_acronym__role__person__user=user)
def get_preapprovals(user):
if not user.is_authenticated():
return []
posted = IdSubmissionDetail.objects.distinct().filter(status__in=[POSTED, POSTED_BY_SECRETARIAT]).values_list('filename', flat=True)
res = Preapproval.objects.exclude(name__in=posted).order_by("-time").select_related('by')
if has_role(user, "Secretariat"):
return res
acronyms = [g.acronym for g in Group.objects.filter(role__person__user=user, type="wg")]
res = res.filter(name__regex="draft-[^-]+-(%s)-.*" % "|".join(acronyms))
return res
def get_recently_approved(user, since):
if not user.is_authenticated():
return []
res = IdSubmissionDetail.objects.distinct().filter(status__in=[POSTED, POSTED_BY_SECRETARIAT], submission_date__gte=since, revision="00").order_by('-submission_date')
if has_role(user, "Secretariat"):
return res
# those we can reach as chair
return res.filter(group_acronym__role__name="chair", group_acronym__role__person__user=user)
class DraftValidation(object):
def __init__(self, draft):
self.draft = draft
self.warnings = {}
self.passes_idnits = self.passes_idnits()
self.wg = self.get_working_group()
self.authors = self.get_authors()
self.submitter = self.get_submitter()
def passes_idnits(self):
passes_idnits = self.check_idnits_success(self.draft.idnits_message)
return passes_idnits
def get_working_group(self):
if self.draft.group_acronym and self.draft.group_acronym.pk == NONE_WG:
return None
return self.draft.group_acronym
def check_idnits_success(self, idnits_message):
if not idnits_message:
return False
success_re = re.compile('\s+Summary:\s+0\s+|No nits found')
if success_re.search(idnits_message):
return True
return False
def is_valid_attr(self, key):
if key in self.warnings.keys():
return False
return True
def is_valid(self):
self.validate_metadata()
return not bool(self.warnings.keys()) and self.passes_idnits
def validate_metadata(self):
self.validate_revision()
self.validate_title()
self.validate_authors()
self.validate_abstract()
self.validate_creation_date()
self.validate_wg()
self.validate_files()
def validate_files(self):
if self.draft.status_id in [POSTED, POSTED_BY_SECRETARIAT]:
return
for ext in self.draft.file_type.split(','):
source = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s%s' % (self.draft.filename, self.draft.revision, ext))
if not os.path.exists(source):
self.add_warning('document_files', '"%s" were not found in the staging area.<br />We recommend you that you cancel this submission and upload your files again.' % os.path.basename(source))
break
def validate_title(self):
if not self.draft.id_document_name:
self.add_warning('title', 'Title is empty or was not found')
def validate_wg(self):
if self.wg and not self.wg.status_id == IETFWG.ACTIVE:
self.add_warning('group', 'Working Group exists but is not an active WG')
def validate_abstract(self):
if not self.draft.abstract:
self.add_warning('abstract', 'Abstract is empty or was not found')
def add_warning(self, key, value):
self.warnings.update({key: value})
def validate_revision(self):
if self.draft.status_id in [POSTED, POSTED_BY_SECRETARIAT]:
return
revision = self.draft.revision
existing_revisions = [int(i.revision_display()) for i in InternetDraft.objects.filter(filename=self.draft.filename)]
expected = 0
if existing_revisions:
expected = max(existing_revisions) + 1
try:
if int(revision) != expected:
self.add_warning('revision', 'Invalid Version Number (Version %02d is expected)' % expected)
except ValueError:
self.add_warning('revision', 'Revision not found')
def validate_authors(self):
if not self.authors:
self.add_warning('authors', 'No authors found')
return
def validate_creation_date(self):
date = self.draft.creation_date
if not date:
self.add_warning('creation_date', 'Creation Date field is empty or the creation date is not in a proper format')
return
submit_date = self.draft.submission_date
if (date + datetime.timedelta(days=3) < submit_date or
date - datetime.timedelta(days=3) > submit_date):
self.add_warning('creation_date', 'Creation Date must be within 3 days of submission date')
def get_authors(self):
return self.draft.tempidauthors_set.exclude(author_order=0).order_by('author_order')
def get_submitter(self):
submitter = self.draft.tempidauthors_set.filter(author_order=0)
if submitter:
return submitter[0]
elif self.draft.submitter_tag:
try:
return PersonOrOrgInfo.objects.get(pk=self.draft.submitter_tag)
except PersonOrOrgInfo.DoesNotExist:
return False
return None