import os import re import datetime from django.conf import settings from ietf.doc.models import Document, State, DocAlias, DocEvent, DocumentAuthor from ietf.doc.models import NewRevisionDocEvent from ietf.doc.models import RelatedDocument, DocRelationshipName from ietf.doc.utils import add_state_change_event, rebuild_reference_relations from ietf.doc.utils import set_replaces_for_document from ietf.doc.mails import send_review_possibly_replaces_request from ietf.group.models import Group from ietf.ietfauth.utils import has_role from ietf.name.models import StreamName from ietf.person.models import Person, Email from ietf.submit.mail import announce_to_lists, announce_new_version, announce_to_authors from ietf.submit.models import Submission, SubmissionEvent, Preapproval, DraftSubmissionStateName from ietf.utils import unaccent from ietf.utils.log import log from ietf.utils.pipe import pipe def check_idnits(path): #p = subprocess.Popen([self.idnits, '--submitcheck', '--nitcount', path], stdout=subprocess.PIPE) cmd = "%s --submitcheck --nitcount %s" % (settings.IDSUBMIT_IDNITS_BINARY, path) code, out, err = pipe(cmd) if code != 0: log("idnits error: %s:\n Error %s: %s" %( cmd, code, err)) return out def found_idnits(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 validate_submission(submission): errors = {} if submission.state_id not in ("cancel", "posted"): for ext in submission.file_types.split(','): source = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s%s' % (submission.name, submission.rev, ext)) if not os.path.exists(source): errors['files'] = '"%s" was not found in the staging area. We recommend you that you cancel this submission and upload your files again.' % os.path.basename(source) break if not submission.title: errors['title'] = 'Title is empty or was not found' if submission.group and submission.group.state_id != "active": errors['group'] = 'Group exists but is not an active group' if not submission.abstract: errors['abstract'] = 'Abstract is empty or was not found' if not submission.authors_parsed(): errors['authors'] = 'No authors found' # revision if submission.state_id != "posted": error = validate_submission_rev(submission.name, submission.rev) if error: errors['rev'] = error # draft date error = validate_submission_document_date(submission.submission_date, submission.document_date) if error: errors['document_date'] = error return errors def has_been_replaced_by(name): docs=Document.objects.filter(name=name) if docs: doc=docs[0] return doc.related_that("replaces") return None def validate_submission_rev(name, rev): if not rev: return 'Revision not found' try: rev = int(rev) except ValueError: return 'Revision must be a number' else: if not (0 <= rev <= 99): return 'Revision must be between 00 and 99' expected = 0 existing_revs = [int(i.rev) for i in Document.objects.filter(name=name)] if existing_revs: expected = max(existing_revs) + 1 if rev != expected: return 'Invalid revision (revision %02d is expected)' % expected replaced_by=has_been_replaced_by(name) if replaced_by: return 'This document has been replaced by %s' % ",".join(rd.name for rd in replaced_by) return None def validate_submission_document_date(submission_date, document_date): if not document_date: return 'Document date is empty or not in a proper format' elif abs(submission_date - document_date) > datetime.timedelta(days=3): return 'Document date must be within 3 days of submission date' return None def create_submission_event(request, submission, desc): by = None if request and request.user.is_authenticated(): try: by = request.user.person except Person.DoesNotExist: pass SubmissionEvent.objects.create(submission=submission, by=by, desc=desc) def post_submission(request, submission): # find out who did it system = Person.objects.get(name="(System)") submitter_parsed = submission.submitter_parsed() if submitter_parsed["name"] and submitter_parsed["email"]: submitter = ensure_person_email_info_exists(submitter_parsed["name"], submitter_parsed["email"]).person else: submitter = system # update draft attributes try: draft = Document.objects.get(name=submission.name) except Document.DoesNotExist: draft = Document.objects.create(name=submission.name, type_id="draft") prev_rev = draft.rev draft.type_id = "draft" draft.title = submission.title group = submission.group or Group.objects.get(type="individ") if not (group.type_id == "individ" 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.pk draft.rev = submission.rev draft.pages = submission.pages 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) events = [] # new revision event e = NewRevisionDocEvent.objects.create( type="new_revision", doc=draft, rev=draft.rev, by=submitter, desc="New version available: %s-%s.txt" % (draft.name, draft.rev), ) events.append(e) # update related objects DocAlias.objects.get_or_create(name=submission.name, document=draft) draft.set_state(State.objects.get(used=True, type="draft", slug="active")) update_authors(draft, submission) trouble = rebuild_reference_relations(draft, filename=os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.txt' % (submission.name, submission.rev))) if trouble: log('Rebuild_reference_relations trouble: %s'%trouble) # automatic state changes if draft.get_state_slug("draft-iana-review") in ("ok-act", "ok-noact", "not-ok"): prev_state = draft.get_state("draft-iana-review") next_state = State.objects.get(used=True, type="draft-iana-review", slug="changed") draft.set_state(next_state) e = add_state_change_event(draft, submitter, prev_state, next_state) if e: events.append(e) 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 AD Followup from Revised ID Needed" e.by = system e.save() events.append(e) state_change_msg = e.desc 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(used=True, type="draft-stream-%s" % draft.stream_id, slug="wg-doc")) # save history now that we're done with changes to the draft itself draft.save_with_history(events) # clean up old files if prev_rev != draft.rev: from ietf.doc.expire import move_draft_files_to_archive move_draft_files_to_archive(draft, prev_rev) move_files_to_repository(submission) submission.state = DraftSubmissionStateName.objects.get(slug="posted") new_replaces, new_possibly_replaces = update_replaces_from_submission(request, submission, draft) announce_to_lists(request, submission) announce_new_version(request, submission, draft, state_change_msg) announce_to_authors(request, submission) if new_possibly_replaces: send_review_possibly_replaces_request(request, draft) submission.save() def update_replaces_from_submission(request, submission, draft): if not submission.replaces: return [], [] is_secretariat = has_role(request.user, "Secretariat") is_chair_of = [] if request.user.is_authenticated(): is_chair_of = list(Group.objects.filter(role__person__user=request.user, role__name="chair")) replaces = DocAlias.objects.filter(name__in=submission.replaces.split(",")).select_related("document", "document__group") existing_replaces = list(draft.related_that_doc("replaces")) existing_suggested = set(draft.related_that_doc("possibly-replaces")) submitter_email = submission.submitter_parsed()["email"] approved = [] suggested = [] for r in replaces: if r in existing_replaces: continue rdoc = r.document if rdoc == draft: continue # TODO - I think the .exists() is in the wrong place below.... if (is_secretariat or (draft.group in is_chair_of and (rdoc.group.type_id == "individ" or rdoc.group in is_chair_of)) or (submitter_email and rdoc.authors.filter(address__iexact=submitter_email)).exists()): approved.append(r) else: if r not in existing_suggested: suggested.append(r) by = request.user.person if request.user.is_authenticated() else Person.objects.get(name="(System)") set_replaces_for_document(request, draft, existing_replaces + approved, by, email_subject="%s replacement status set during submit by %s" % (draft.name, submission.submitter_parsed()["name"])) if suggested: possibly_replaces = DocRelationshipName.objects.get(slug="possibly-replaces") for r in suggested: RelatedDocument.objects.create(source=draft, target=r, relationship=possibly_replaces) DocEvent.objects.create(doc=draft, by=by, type="added_suggested_replaces", desc="Added suggested replacement relationships: %s" % ", ".join(d.name for d in suggested)) return approved, suggested def get_person_from_name_email(name, email): # try email if email: persons = Person.objects.filter(email__address=email).distinct() if len(persons) == 1: return persons[0] else: persons = Person.objects.none() if not persons: persons = Person.objects.all() # try full name p = persons.filter(alias__name=name).distinct() if p: return p[0] return None def ensure_person_email_info_exists(name, email): person = get_person_from_name_email(name, email) # make sure we have a person if not person: person = Person() person.name = name person.ascii = unaccent.asciify(person.name) person.save() # make sure we have an email address if email: addr = email.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 = True email.person = person email.save() return email def update_authors(draft, submission): authors = [] for order, author in enumerate(submission.authors_parsed()): email = ensure_person_email_info_exists(author["name"], author["email"]) a = DocumentAuthor.objects.filter(document=draft, author=email) if a: a = a[0] else: a = DocumentAuthor(document=draft, author=email) a.order = order a.save() authors.append(email) draft.documentauthor_set.exclude(author__in=authors).delete() def cancel_submission(submission): submission.state = DraftSubmissionStateName.objects.get(slug="cancel") submission.save() remove_submission_files(submission) def rename_submission_files(submission, prev_rev, new_rev): from ietf.submit.forms import SubmissionUploadForm for ext in SubmissionUploadForm.base_fields.keys(): source = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.%s' % (submission.name, prev_rev, ext)) dest = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.%s' % (submission.name, new_rev, ext)) if os.path.exists(source): os.rename(source, dest) def move_files_to_repository(submission): from ietf.submit.forms import SubmissionUploadForm for ext in SubmissionUploadForm.base_fields.keys(): source = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.%s' % (submission.name, submission.rev, ext)) dest = os.path.join(settings.IDSUBMIT_REPOSITORY_PATH, '%s-%s.%s' % (submission.name, submission.rev, 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.") elif ext in submission.file_types.split(','): raise ValueError("Intended to move '%s' to '%s', but found source and destination missing.") def remove_submission_files(submission): for ext in submission.file_types.split(','): source = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s%s' % (submission.name, submission.rev, ext)) if os.path.exists(source): os.unlink(source) def approvable_submissions_for_user(user): if not user.is_authenticated(): return [] res = Submission.objects.filter(state="grp-appr").order_by('-submission_date') if has_role(user, "Secretariat"): return res # those we can reach as chair return res.filter(group__role__name="chair", group__role__person__user=user) def preapprovals_for_user(user): if not user.is_authenticated(): return [] posted = Submission.objects.distinct().filter(state="posted").values_list('name', 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__in=("wg", "rg"))] res = res.filter(name__regex="draft-[^-]+-(%s)-.*" % "|".join(acronyms)) return res def recently_approved_by_user(user, since): if not user.is_authenticated(): return [] res = Submission.objects.distinct().filter(state="posted", submission_date__gte=since, rev="00").order_by('-submission_date') if has_role(user, "Secretariat"): return res # those we can reach as chair return res.filter(group__role__name="chair", group__role__person__user=user) def expirable_submissions(older_than_days): cutoff = datetime.date.today() - datetime.timedelta(days=older_than_days) return Submission.objects.exclude(state__in=("cancel", "posted")).filter(submission_date__lt=cutoff) def expire_submission(submission, by): submission.state_id = "cancel" submission.save() SubmissionEvent.objects.create(submission=submission, by=by, desc="Canceled expired submission")