From 6f10d2f3f77fbfb572bf6ae425bcfe910f42610e Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 21 Sep 2011 19:29:50 +0000 Subject: [PATCH] Port submit to new schema, adding a set of tests and fixing some bugs - Legacy-Id: 3436 --- ietf/submit/admin.py | 16 +- ietf/submit/fixtures/idsubmissionstatus.xml | 102 ++++++ ietf/submit/forms.py | 175 +++++++--- ietf/submit/generate_fixtures.py | 34 ++ ietf/submit/models.py | 15 +- ietf/submit/parsers/plain_parser.py | 20 -- ietf/submit/templatetags/submit_tags.py | 3 - ietf/submit/test_submission.txt | 112 ++++--- ietf/submit/tests.py | 305 ++++++++++++++++-- ietf/submit/utils.py | 193 ++++++++++- ietf/submit/views.py | 11 +- ietf/templates/submit/confirm_autopost.txt | 7 +- ietf/templates/submit/draft_edit.html | 19 +- ietf/templates/submit/draft_status.html | 7 +- ietf/templates/submit/manual_post_mail.txt | 8 +- ietf/templates/submit/request_full_url.txt | 5 +- ietf/templates/submit/submission_approval.txt | 8 +- ietf/utils/test_data.py | 2 +- redesign/doc/utils.py | 49 +++ redesign/person/models.py | 13 +- static/js/draft-submit.js | 12 + 21 files changed, 924 insertions(+), 192 deletions(-) create mode 100644 ietf/submit/fixtures/idsubmissionstatus.xml create mode 100755 ietf/submit/generate_fixtures.py create mode 100644 redesign/doc/utils.py create mode 100644 static/js/draft-submit.js diff --git a/ietf/submit/admin.py b/ietf/submit/admin.py index f5537199a..2856727f0 100644 --- a/ietf/submit/admin.py +++ b/ietf/submit/admin.py @@ -1,3 +1,4 @@ +from django.core.urlresolvers import reverse as urlreverse from django.contrib import admin from ietf.submit.models import * @@ -9,7 +10,16 @@ class IdSubmissionDetailAdmin(admin.ModelAdmin): list_display = ['submission_id', 'filename', 'status_link', 'submission_date', 'last_updated_date',] ordering = [ '-submission_date' ] search_fields = ['filename', ] -admin.site.register(IdSubmissionDetail, IdSubmissionDetailAdmin) + raw_id_fields = ['group_acronym'] + + def status_link(self, instance): + url = urlreverse('draft_status_by_hash', + kwargs=dict(submission_id=instance.submission_id, + submission_hash=instance.get_hash())) + return '%s' % (url, instance.status) + status_link.allow_tags = True + +admin.site.register(IdSubmissionDetail, IdSubmissionDetailAdmin) class IdApprovedDetailAdmin(admin.ModelAdmin): pass @@ -17,5 +27,5 @@ admin.site.register(IdApprovedDetail, IdApprovedDetailAdmin) class TempIdAuthorsAdmin(admin.ModelAdmin): ordering = ["-id"] - pass -admin.site.register(TempIdAuthors, TempIdAuthorsAdmin) +admin.site.register(TempIdAuthors, TempIdAuthorsAdmin) + diff --git a/ietf/submit/fixtures/idsubmissionstatus.xml b/ietf/submit/fixtures/idsubmissionstatus.xml new file mode 100644 index 000000000..280d52693 --- /dev/null +++ b/ietf/submit/fixtures/idsubmissionstatus.xml @@ -0,0 +1,102 @@ + + + + Cancelled + + + Dead + + + Posted by the Secretariat + + + Posted + + + Ready To Post + + + Uploaded + + + ID NITS Passed + + + Initial Version Approval Required + + + Submitter Authentication Required + + + Manual Post Requested + + + External Meta-Data Required + + + Internal Database Has Been Updated + + + ID Announcement Scheduled + + + ID Tracker Notification Scheduled + + + Initial Version Approval Requested + + + Error - Plain text version does not exist + + + File size is larger than 20 MB + + + Duplicate Internet-Draft submission is currently in process. + + + Error - Simultaneous submission from the same IP address + + + Error - Auth key does not match + + + Error - No such Internet-Draft is currently in process + + + Error - Draft is not in an appropriate status for the requested page + + + Error - Unknown Request + + + Error - Invalid Email Address + + + Error - Direct Access is prohibited + + + Error - Invalid version number + + + Error - Invalid filename + + + Error - The document failed idnits verification + + + Creation Date must be within 3 days of the submission date. + + + Error – Not a valid submitter + + + Incorrect Meta-Data + + + The document does not contain a legitimate filename that start with draft-*. + + + Initial Version Approved + + \ No newline at end of file diff --git a/ietf/submit/forms.py b/ietf/submit/forms.py index d5627bc15..cdcb9c2be 100644 --- a/ietf/submit/forms.py +++ b/ietf/submit/forms.py @@ -1,4 +1,4 @@ -import sha +import hashlib import random import os import subprocess @@ -10,6 +10,7 @@ from django.conf import settings from django.contrib.sites.models import Site from django.template.loader import render_to_string from django.utils.html import mark_safe +from django.core.urlresolvers import reverse as urlreverse from ietf.idtracker.models import InternetDraft, IETFWG from ietf.proceedings.models import Meeting @@ -51,11 +52,8 @@ class UploadForm(forms.Form): def read_dates(self): now = datetime.datetime.utcnow() first_cut_off = Meeting.get_first_cut_off() - print "first_cut_off:", first_cut_off second_cut_off = Meeting.get_second_cut_off() - print "second_cut_off:", second_cut_off ietf_monday = Meeting.get_ietf_monday() - print "ietf_monday:", ietf_monday if now.date() >= first_cut_off and now.date() < second_cut_off: # We are in the first_cut_off if now.date() == first_cut_off and now.hour < settings.CUTOFF_HOUR: @@ -252,7 +250,10 @@ class UploadForm(forms.Form): document_id = 0 existing_draft = InternetDraft.objects.filter(filename=draft.filename) if existing_draft: - document_id = existing_draft[0].id_document_tag + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + document_id = -1 + else: + document_id = existing_draft[0].id_document_tag detail = IdSubmissionDetail.objects.create( id_document_name=draft.get_title(), filename=draft.filename, @@ -274,22 +275,34 @@ class UploadForm(forms.Form): for author in draft.get_author_info(): full_name, first_name, middle_initial, last_name, name_suffix, email = author order += 1 - TempIdAuthors.objects.create( - id_document_tag=document_id, - first_name=first_name, - middle_initial=middle_initial, - last_name=last_name, - name_suffix=name_suffix, - email_address=email, - author_order=order, - submission=detail) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + # save full name + TempIdAuthors.objects.create( + id_document_tag=document_id, + first_name=full_name.strip(), + email_address=email, + author_order=order, + submission=detail) + else: + TempIdAuthors.objects.create( + id_document_tag=document_id, + first_name=first_name, + middle_initial=middle_initial, + last_name=last_name, + name_suffix=name_suffix, + email_address=email, + author_order=order, + submission=detail) return detail class AutoPostForm(forms.Form): - first_name = forms.CharField(label=u'Given name', required=True) - last_name = forms.CharField(label=u'Last name', required=True) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + name = forms.CharField(required=True) + else: + first_name = forms.CharField(label=u'Given name', required=True) + last_name = forms.CharField(label=u'Last name', required=True) email = forms.EmailField(label=u'Email address', required=True) def __init__(self, *args, **kwargs): @@ -298,10 +311,21 @@ class AutoPostForm(forms.Form): super(AutoPostForm, self).__init__(*args, **kwargs) def get_author_buttons(self): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + buttons = [] + for i in self.validation.authors: + buttons.append('' + % dict(name=i.get_full_name(), + email=i.email()[1] or '')) + return "".join(buttons) + + + # this should be moved to a Javascript file and attributes like data-first-name ... button_template = '' + buttons = [] for i in self.validation.authors: - full_name = '%s. %s' % (i.first_name[0], i.last_name) + full_name = u'%s. %s' % (i.first_name[0], i.last_name) buttons.append(button_template % {'first_name': i.first_name, 'last_name': i.last_name, 'email': i.email()[1] or '', @@ -317,10 +341,22 @@ class AutoPostForm(forms.Form): subject = 'Confirmation for Auto-Post of I-D %s' % self.draft.filename from_email = settings.IDSUBMIT_FROM_EMAIL to_email = self.cleaned_data['email'] + + confirm_url = settings.IDTRACKER_BASE_URL + urlreverse('draft_confirm', kwargs=dict(submission_id=self.draft.submission_id, auth_key=self.draft.auth_key)) + status_url = settings.IDTRACKER_BASE_URL + urlreverse('draft_status_by_hash', kwargs=dict(submission_id=self.draft.submission_id, submission_hash=self.draft.get_hash())) + send_mail(request, to_email, from_email, subject, 'submit/confirm_autopost.txt', - {'draft': self.draft, 'domain': Site.objects.get_current().domain }) + { 'draft': self.draft, 'confirm_url': confirm_url, 'status_url': status_url }) def save_submitter_info(self): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + return TempIdAuthors.objects.create( + id_document_tag=self.draft.temp_id_document_tag, + first_name=self.cleaned_data['name'], + email_address=self.cleaned_data['email'], + author_order=0, + submission=self.draft) + return TempIdAuthors.objects.create( id_document_tag=self.draft.temp_id_document_tag, first_name=self.cleaned_data['first_name'], @@ -330,8 +366,8 @@ class AutoPostForm(forms.Form): submission=self.draft) def save_new_draft_info(self): - salt = sha.new(str(random.random())).hexdigest()[:5] - self.draft.auth_key = sha.new(salt+self.cleaned_data['email']).hexdigest() + salt = hashlib.sha1(str(random.random())).hexdigest()[:5] + self.draft.auth_key = hashlib.sha1(salt+self.cleaned_data['email']).hexdigest() self.draft.status_id = WAITING_AUTHENTICATION self.draft.save() @@ -343,11 +379,18 @@ class MetaDataForm(AutoPostForm): creation_date = forms.DateField(label=u'Creation date', required=True) pages = forms.IntegerField(label=u'Pages', required=True) abstract = forms.CharField(label=u'Abstract', widget=forms.Textarea, required=True) - first_name = forms.CharField(label=u'Given name', required=True) - last_name = forms.CharField(label=u'Last name', required=True) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + name = forms.CharField(required=True) + else: + first_name = forms.CharField(label=u'Given name', required=True) + last_name = forms.CharField(label=u'Last name', required=True) email = forms.EmailField(label=u'Email address', required=True) comments = forms.CharField(label=u'Comments to the secretariat', widget=forms.Textarea, required=False) - fields = ['title', 'version', 'creation_date', 'pages', 'abstract', 'first_name', 'last_name', 'email', 'comments'] + + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + fields = ['title', 'version', 'creation_date', 'pages', 'abstract', 'name', 'email', 'comments'] + else: + fields = ['title', 'version', 'creation_date', 'pages', 'abstract', 'first_name', 'last_name', 'email', 'comments'] def __init__(self, *args, **kwargs): super(MetaDataForm, self).__init__(*args, **kwargs) @@ -358,26 +401,44 @@ class MetaDataForm(AutoPostForm): authors=[] if self.is_bound: for key, value in self.data.items(): - if key.startswith('first_name_'): - author = {'errors': {}} - index = key.replace('first_name_', '') - first_name = value.strip() - if not first_name: - author['errors']['first_name'] = 'This field is required' - last_name = self.data.get('last_name_%s' % index, '').strip() - if not last_name: - author['errors']['last_name'] = 'This field is required' - email = self.data.get('email_%s' % index, '').strip() - if email and not email_re.search(email): - author['errors']['email'] = 'Enter a valid e-mail address' - if first_name or last_name or email: - author.update({'first_name': first_name, - 'last_name': last_name, - 'email': ('%s %s' % (first_name, last_name), email), - 'index': index, - }) - authors.append(author) - authors.sort(lambda x,y: cmp(int(x['index']), int(y['index']))) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + if key.startswith('name_'): + author = {'errors': {}} + index = key.replace('name_', '') + name = value.strip() + if not name: + author['errors']['name'] = 'This field is required' + email = self.data.get('email_%s' % index, '').strip() + if email and not email_re.search(email): + author['errors']['email'] = 'Enter a valid e-mail address' + if name or email: + author.update({'get_full_name': name, + 'email': (name, email), + 'index': index, + }) + authors.append(author) + + else: + if key.startswith('first_name_'): + author = {'errors': {}} + index = key.replace('first_name_', '') + first_name = value.strip() + if not first_name: + author['errors']['first_name'] = 'This field is required' + last_name = self.data.get('last_name_%s' % index, '').strip() + if not last_name: + author['errors']['last_name'] = 'This field is required' + email = self.data.get('email_%s' % index, '').strip() + if email and not email_re.search(email): + author['errors']['email'] = 'Enter a valid e-mail address' + if first_name or last_name or email: + author.update({'first_name': first_name, + 'last_name': last_name, + 'email': ('%s %s' % (first_name, last_name), email), + 'index': index, + }) + authors.append(author) + authors.sort(key=lambda x: x['index']) return authors def set_initials(self): @@ -437,7 +498,7 @@ class MetaDataForm(AutoPostForm): def save_new_draft_info(self): draft = self.draft - draft.id_documen_name = self.cleaned_data['title'] + draft.id_document_name = self.cleaned_data['title'] if draft.revision != self.cleaned_data['version']: self.move_docs(draft, self.cleaned_data['version']) draft.revision = self.cleaned_data['version'] @@ -447,7 +508,21 @@ class MetaDataForm(AutoPostForm): draft.comment_to_sec = self.cleaned_data['comments'] draft.status_id = MANUAL_POST_REQUESTED draft.save() - self.save_submitter_info() + + # sync authors + draft.tempidauthors_set.all().delete() + + self.save_submitter_info() # submitter is author 0 + + for i, author in enumerate(self.authors): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + # save full name + TempIdAuthors.objects.create( + id_document_tag=draft.temp_id_document_tag, + first_name=author["get_full_name"], + email_address=author["email"][1], + author_order=i + 1, + submission=draft) def save(self, request): self.save_new_draft_info() @@ -462,5 +537,11 @@ class MetaDataForm(AutoPostForm): if self.draft.group_acronym: cc += [i.person.email()[1] for i in self.draft.group_acronym.wgchair_set.all()] cc = list(set(cc)) - send_mail(request, to_email, from_email, subject, 'submit/manual_post_mail.txt', - {'form': self, 'draft': self.draft, 'domain': Site.objects.get_current().domain }, cc=cc) + submitter = self.draft.tempidauthors_set.get(author_order=0) + send_mail(request, to_email, from_email, subject, 'submit/manual_post_mail.txt', { + 'form': self, + 'draft': self.draft, + 'url': settings.IDTRACKER_BASE_URL + urlreverse('draft_status', kwargs=dict(submission_id=self.draft.submission_id)), + 'submitter': submitter + }, + cc=cc) diff --git a/ietf/submit/generate_fixtures.py b/ietf/submit/generate_fixtures.py new file mode 100755 index 000000000..cbda2392d --- /dev/null +++ b/ietf/submit/generate_fixtures.py @@ -0,0 +1,34 @@ +#!/usr/bin/python + +# boiler plate +import os, sys + +ietf_path = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../ietf')) + +sys.path.insert(0, ietf_path) + +from django.core.management import setup_environ +import settings +setup_environ(settings) + +# script +from django.core.serializers import serialize +from django.db.models import Q + +def output(name, qs): + try: + f = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures/%s.xml" % name), 'w') + f.write(serialize("xml", qs, indent=4)) + f.close() + except: + from django.db import connection + from pprint import pprint + pprint(connection.queries) + raise + +# pick all name models directly out of the module +names = [] + +from ietf.submit.models import IdSubmissionStatus + +output("idsubmissionstatus", IdSubmissionStatus.objects.all()) diff --git a/ietf/submit/models.py b/ietf/submit/models.py index 4ebc076ce..8a9406e2d 100644 --- a/ietf/submit/models.py +++ b/ietf/submit/models.py @@ -60,9 +60,6 @@ class IdSubmissionDetail(models.Model): self.create_hash() self.save() return self.submission_hash - def status_link(self): - return '%s' % (self.submission_id, self.submission_hash, self.status) - status_link.allow_tags = True def create_submission_hash(sender, instance, **kwargs): @@ -86,7 +83,7 @@ class IdApprovedDetail(models.Model): class TempIdAuthors(models.Model): id_document_tag = models.IntegerField() - first_name = models.CharField(blank=True, max_length=255) + first_name = models.CharField(blank=True, max_length=255) # with new schema, this contains the full name while the other name fields are empty to avoid loss of information last_name = models.CharField(blank=True, max_length=255) email_address = models.CharField(blank=True, max_length=255) last_modified_date = models.DateField(null=True, blank=True) @@ -100,13 +97,11 @@ class TempIdAuthors(models.Model): db_table = 'temp_id_authors' def email(self): - return ('%s %s' % (self.first_name, self.last_name), self.email_address) + return (self.get_full_name(), self.email_address) def get_full_name(self): - full_name = ('%s %s %s %s') % (self.first_name, self.middle_initial or '', - self.last_name, self.name_suffix or '') - full_name = re.sub(' +', ' ', full_name).strip() - return full_name + parts = (self.first_name or '', self.middle_initial or '', self.last_name or '', self.name_suffix or '') + return u" ".join(x.strip() for x in parts if x.strip()) def __unicode__(self): - return u"%s <%s>" % self.email() \ No newline at end of file + return u"%s <%s>" % self.email() diff --git a/ietf/submit/parsers/plain_parser.py b/ietf/submit/parsers/plain_parser.py index b49dee9a0..4aa9438d5 100644 --- a/ietf/submit/parsers/plain_parser.py +++ b/ietf/submit/parsers/plain_parser.py @@ -65,23 +65,3 @@ class PlainParser(FileParser): self.parsed_info.metadraft.filename = filename return self.parsed_info.add_error('The first page of the document does not contain a legitimate filename that start with draft-*') - - def parse_wg(self): - filename = self.parsed_info.metadraft.filename - try: - existing_draft = InternetDraft.objects.get(filename=filename) - self.parsed_info.metadraft.wg = existing_draft.group - except InternetDraft.DoesNotExist: - if filename.startswith('draft-ietf-'): - # Extra check for WG that contains dashes - for group in IETFWG.objects.filter(group_acronym__acronym__contains='-'): - if filename.startswith('draft-ietf-%s-' % group.group_acronym.acronym): - self.parsed_info.metadraft.wg = group - return - group_acronym = filename.split('-')[2] - try: - self.parsed_info.metadraft.wg = IETFWG.objects.get(group_acronym__acronym=group_acronym) - except IETFWG.DoesNotExist: - self.parsed_info.add_error('Invalid WG ID: %s' % group_acronym) - else: - self.parsed_info.metadraft.wg = IETFWG.objects.get(pk=NONE_WG_PK) diff --git a/ietf/submit/templatetags/submit_tags.py b/ietf/submit/templatetags/submit_tags.py index 9f8314976..efd308018 100644 --- a/ietf/submit/templatetags/submit_tags.py +++ b/ietf/submit/templatetags/submit_tags.py @@ -17,9 +17,6 @@ def show_submission_files(context, submission): 'url': '%s%s-%s%s' % (settings.IDSUBMIT_STAGING_URL, submission.filename, submission.revision, ext)}) return {'files': result} -def show_two_pages(context, two_pages, validation): - result - @register.filter def two_pages_decorated_with_validation(value, validation): diff --git a/ietf/submit/test_submission.txt b/ietf/submit/test_submission.txt index 8cab388a9..eaa2780b0 100644 --- a/ietf/submit/test_submission.txt +++ b/ietf/submit/test_submission.txt @@ -1,7 +1,7 @@ Informational Test Name Internet-Draft Test Center Inc. -Intended status: Informational -Expires: %(expire)s %(date)s +Intended status: Informational %(date)s +Expires: %(expire)s Testing tests @@ -15,9 +15,7 @@ Abstract Status of this Memo This Internet-Draft is submitted in full conformance with the - provisions of BCP 78 and BCP 79. This document may not be modified, - and derivative works of it may not be created, and it may not be - published except as an Internet-Draft. + provisions of BCP 78 and BCP 79. Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute @@ -41,73 +39,73 @@ Copyright Notice (http://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect - to this document. - - This Informational Internet Draft is submitted as an RFC Editor - + to this document. Code Components extracted from this document must Name Expires %(expire)s [Page 1] - + Internet-Draft Testing tests %(month_year)s - Contribution and/or non-IETF Document (not as a Contribution, IETF - Contribution, nor IETF Document) in accordance with BCP 78 and BCP - 79. + include Simplified BSD License text as described in Section 4.e of + the Trust Legal Provisions and are provided without warranty as + described in the Simplified BSD License. + + This document may contain material from IETF Documents or IETF + Contributions published or made publicly available before November + 10, 2008. The person(s) controlling the copyright in some of this + material may not have granted the IETF Trust the right to allow + modifications of such material outside the IETF Standards Process. + Without obtaining an adequate license from the person(s) controlling + the copyright in such materials, this document may not be modified + outside the IETF Standards Process, and derivative works of it may + not be created outside the IETF Standards Process, except to format + it for publication as an RFC or to translate it into languages other + than English. Table of Contents - 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 4 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 3 + 2. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 4 + 3. Security Considerations . . . . . . . . . . . . . . . . . . . 4 Name Expires %(expire)s [Page 2] - + Internet-Draft Testing tests %(month_year)s 1. Introduction This document describes a protocol for testing tests. +Name Expires %(expire)s [Page 3] + +Internet-Draft Testing tests %(month_year)s + +2. Security Considerations + + There are none. + + +3. IANA Considerations + + No new registrations for IANA. + + +Authors' Addresses + + Test Name + Test Center Inc. + Some way 42 + Some Where, NY + + Email: testname@example.com + + + + + + + + +Name Expires %(expire)s [Page 4] diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py index a79feba75..4411d8f34 100644 --- a/ietf/submit/tests.py +++ b/ietf/submit/tests.py @@ -13,56 +13,325 @@ from ietf.utils.test_data import make_test_data from redesign.person.models import Person, Email from redesign.group.models import Group, Role -from redesign.doc.models import Document - +from redesign.doc.models import Document, BallotPositionDocEvent +from ietf.submit.models import IdSubmissionDetail + class SubmitTestCase(django.test.TestCase): - fixtures = ['names'] + fixtures = ['names', 'idsubmissionstatus'] def setUp(self): self.staging_dir = os.path.abspath("tmp-submit-staging-dir") os.mkdir(self.staging_dir) - settings.IDSUBMIT_STAGING_PATH = self.staging_dir + self.repository_dir = os.path.abspath("tmp-submit-repository-dir") + os.mkdir(self.repository_dir) + settings.IDSUBMIT_REPOSITORY_PATH = self.repository_dir + def tearDown(self): shutil.rmtree(self.staging_dir) + shutil.rmtree(self.repository_dir) - def test_submit(self): + def do_submission(self, name, rev): # break early in case of missing configuration self.assertTrue(os.path.exists(settings.IDSUBMIT_IDNITS_BINARY)) - draft = make_test_data() - - url = urlreverse('submit_index') - # get + url = urlreverse('submit_index') r = self.client.get(url) self.assertEquals(r.status_code, 200) q = PyQuery(r.content) self.assertEquals(len(q('input[type=file][name=txt]')), 1) - # submit text draft - filename = "draft-mars-testing-tests-00" - - # construct appropriate text file + # construct appropriate text draft f = open(os.path.join(settings.BASE_DIR, "submit", "test_submission.txt")) template = f.read() f.close() + submission_text = template % dict( - date=datetime.date.today().strftime("%Y-%m-%d"), + date=datetime.date.today().strftime("%d %B %Y"), expire=(datetime.date.today() + datetime.timedelta(days=100)).strftime("%Y-%m-%d"), year=datetime.date.today().strftime("%Y"), month_year=datetime.date.today().strftime("%B, %Y"), - filename=filename, + filename="%s-%s" % (name, rev), ) - - test_file = StringIO(submission_text) + + test_file = StringIO(str(submission_text)) test_file.name = "somename.txt" + # submit r = self.client.post(url, dict(txt=test_file)) self.assertEquals(r.status_code, 302) - self.assertTrue(os.path.exists(os.path.join(self.staging_dir, filename + ".txt"))) + supply_submitter_url = r["Location"] + self.assertTrue(os.path.exists(os.path.join(self.staging_dir, u"%s-%s.txt" % (name, rev)))) + self.assertEquals(IdSubmissionDetail.objects.filter(filename=name).count(), 1) + submission = IdSubmissionDetail.objects.get(filename=name) + self.assertEquals(submission.group_acronym.acronym, "mars") + self.assertEquals(submission.tempidauthors_set.count(), 1) + author = submission.tempidauthors_set.all()[0] + self.assertEquals(author.first_name, "Test Name") + + return supply_submitter_url + + def supply_submitter(self, name, supply_submitter_url): + # check the page + r = self.client.get(supply_submitter_url) + q = PyQuery(r.content) + self.assertEquals(len(q('input[type=submit][name=autopost]')), 1) + + # post submitter info + r = self.client.post(supply_submitter_url, + dict(autopost="1", + name="Test Name", + email="testname@example.com", + )) + # submitter is saved as author order 0 + submission = IdSubmissionDetail.objects.get(filename=name) + self.assertEquals(submission.tempidauthors_set.count(), 2) + self.assertEquals(submission.tempidauthors_set.get(author_order=0).first_name, "Test Name") + + return r + + def test_submit_new(self): + # submit new -> supply submitter info -> approve + draft = make_test_data() + + name = "draft-ietf-mars-testing-tests" + rev = "00" + + supply_submitter_url = self.do_submission(name, rev) + + # supply submitter info, then draft should be in and ready for approval + mailbox_before = len(mail_outbox) + r = self.supply_submitter(name, supply_submitter_url) + + self.assertEquals(r.status_code, 302) + status_url = r["Location"] + self.assertEquals(len(mail_outbox), mailbox_before + 1) + self.assertTrue("New draft waiting for approval" in mail_outbox[-1]["Subject"]) + self.assertTrue(name in mail_outbox[-1]["Subject"]) + + # as chair of WG, we should see approval button + self.client.login(remote_user="marschairman") + + r = self.client.get(status_url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + approve_submit = q('input[type=submit][value*="Approve"]') + self.assertEquals(len(approve_submit), 1) + + # approve submission + mailbox_before = len(mail_outbox) + approve_url = approve_submit.parents("form").attr("action") + r = self.client.post(approve_url, dict()) + self.assertEquals(r.status_code, 302) + + draft = Document.objects.get(docalias__name=name) + self.assertEquals(draft.rev, rev) + new_revision = draft.latest_event() + self.assertEquals(new_revision.type, "new_revision") + self.assertEquals(new_revision.by.name, "Test Name") + self.assertTrue(not os.path.exists(os.path.join(self.staging_dir, u"%s-%s.txt" % (name, rev)))) + self.assertTrue(os.path.exists(os.path.join(self.repository_dir, u"%s-%s.txt" % (name, rev)))) + self.assertEquals(draft.authors.count(), 1) + self.assertEquals(draft.authors.all()[0].get_name(), "Test Name") + self.assertEquals(draft.authors.all()[0].address, "testname@example.com") + self.assertEquals(len(mail_outbox), mailbox_before + 2) + self.assertTrue((u"I-D Action: %s" % name) in mail_outbox[-2]["Subject"]) + self.assertTrue("Test Name" in unicode(mail_outbox[-2])) + self.assertTrue("New Version Notification" in mail_outbox[-1]["Subject"]) + self.assertTrue(name in unicode(mail_outbox[-1])) + self.assertTrue("mars" in unicode(mail_outbox[-1])) + + def test_submit_existing(self): + # submit new revision of existing -> supply submitter info -> confirm + draft = make_test_data() + + # make a discuss to see if the AD gets an email + ballot_position = BallotPositionDocEvent() + ballot_position.pos_id = "discuss" + ballot_position.type = "changed_ballot_position" + ballot_position.doc = draft + ballot_position.ad = ballot_position.by = Person.objects.get(user__username="ad2") + ballot_position.save() + + name = draft.name + rev = "%02d" % (int(draft.rev) + 1) + + supply_submitter_url = self.do_submission(name, rev) + + # supply submitter info, then we get a confirmation email + mailbox_before = len(mail_outbox) + r = self.supply_submitter(name, supply_submitter_url) + + self.assertEquals(r.status_code, 200) + self.assertTrue("Your submission is pending email authentication" in r.content) + + self.assertEquals(len(mail_outbox), mailbox_before + 1) + confirmation = mail_outbox[-1] + self.assertTrue("Confirmation for" in confirmation["Subject"]) + self.assertTrue(name in confirmation["Subject"]) + + # dig out confirmation link + msg = confirmation.get_payload(i=1).get_payload(i=0).get_payload(decode=True) + line_start = "I-D Submission Tool URL:" + self.assertTrue(line_start in msg) + confirm_url = None + for line in msg.split("\n"): + if line.startswith(line_start): + confirm_url = line[len(line_start):].strip() + + # go to confirm page + r = self.client.get(confirm_url) + q = PyQuery(r.content) + self.assertEquals(len(q('input[type=submit][value=Auto-Post]')), 1) + + # confirm + mailbox_before = len(mail_outbox) + r = self.client.post(confirm_url) + self.assertEquals(r.status_code, 200) + self.assertTrue('Authorization key accepted' in r.content) + + draft = Document.objects.get(docalias__name=name) + self.assertEquals(draft.rev, rev) + new_revision = draft.latest_event() + self.assertEquals(new_revision.type, "new_revision") + self.assertEquals(new_revision.by.name, "Test Name") + self.assertTrue(not os.path.exists(os.path.join(self.staging_dir, u"%s-%s.txt" % (name, rev)))) + self.assertTrue(os.path.exists(os.path.join(self.repository_dir, u"%s-%s.txt" % (name, rev)))) + self.assertEquals(draft.authors.count(), 1) + self.assertEquals(draft.authors.all()[0].get_name(), "Test Name") + self.assertEquals(draft.authors.all()[0].address, "testname@example.com") + self.assertEquals(len(mail_outbox), mailbox_before + 3) + self.assertTrue((u"I-D Action: %s" % name) in mail_outbox[-3]["Subject"]) + self.assertTrue("Test Name" in unicode(mail_outbox[-3])) + self.assertTrue("New Version Notification" in mail_outbox[-2]["Subject"]) + self.assertTrue(name in unicode(mail_outbox[-2])) + self.assertTrue("mars" in unicode(mail_outbox[-2])) + self.assertTrue(draft.ad.email_address().address in unicode(mail_outbox[-2])) + self.assertTrue(ballot_position.ad.email_address().address in unicode(mail_outbox[-2])) + self.assertTrue("New Version Notification" in mail_outbox[-1]["Subject"]) + self.assertTrue(name in unicode(mail_outbox[-1])) + self.assertTrue("mars" in unicode(mail_outbox[-1])) + + def test_cancel_submission(self): + # submit -> cancel + draft = make_test_data() + + name = "draft-ietf-mars-testing-tests" + rev = "00" + + supply_submitter_url = self.do_submission(name, rev) + + # check we got cancel button + r = self.client.get(supply_submitter_url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + cancel_submission = q('input[type=submit][value*="Cancel"]') + self.assertEquals(len(cancel_submission), 1) + + cancel_url = cancel_submission.parents("form").attr("action") + + # cancel + r = self.client.post(cancel_url) + self.assertTrue(not os.path.exists(os.path.join(self.staging_dir, u"%s-%s.txt" % (name, rev)))) + + def test_edit_submission(self): + # submit -> edit + draft = make_test_data() + + name = "draft-ietf-mars-testing-tests" + rev = "00" + + supply_submitter_url = self.do_submission(name, rev) + + # check we got edit button + r = self.client.get(supply_submitter_url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('input[type=submit][value*="Adjust"]')), 1) + + # go to edit, we do this by posting, slightly weird + r = self.client.post(supply_submitter_url) + self.assertEquals(r.status_code, 302) + edit_url = r['Location'] + + # check page + r = self.client.get(edit_url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('input[name=title]')), 1) + + # edit + mailbox_before = len(mail_outbox) + creation_date = datetime.date.today() - datetime.timedelta(days=-3) + r = self.client.post(edit_url, + dict(title="some title", + version="00", + creation_date=creation_date.strftime("%Y-%m-%d"), + abstract="some abstract", + pages="123", + name="Some Random Test Person", + email="random@example.com", + comments="no comments", + name_0="Person 1", + email_0="person1@example.com", + name_1="Person 2", + email_1="person2@example.com", + )) + self.assertEquals(r.status_code, 302) + + submission = IdSubmissionDetail.objects.get(filename=name) + self.assertEquals(submission.id_document_name, "some title") + self.assertEquals(submission.creation_date, creation_date) + self.assertEquals(submission.abstract, "some abstract") + self.assertEquals(submission.txt_page_count, 123) + self.assertEquals(submission.comment_to_sec, "no comments") + + authors = submission.tempidauthors_set + self.assertEquals(authors.count(), 3) + # first one is submitter + self.assertEquals(authors.get(author_order=0).first_name, "Some Random Test Person") + self.assertEquals(authors.get(author_order=0).email_address, "random@example.com") + self.assertEquals(authors.get(author_order=1).first_name, "Person 1") + self.assertEquals(authors.get(author_order=1).email_address, "person1@example.com") + self.assertEquals(authors.get(author_order=2).first_name, "Person 2") + self.assertEquals(authors.get(author_order=2).email_address, "person2@example.com") + + self.assertEquals(len(mail_outbox), mailbox_before + 1) + self.assertTrue("Manual Post Requested" in mail_outbox[-1]["Subject"]) + self.assertTrue(name in mail_outbox[-1]["Subject"]) + + def test_request_full_url(self): + # submit -> request full URL to be sent + draft = make_test_data() + + name = "draft-ietf-mars-testing-tests" + rev = "00" + + self.do_submission(name, rev) + + submission = IdSubmissionDetail.objects.get(filename=name) + url = urlreverse('draft_status', kwargs=dict(submission_id=submission.submission_id)) + + # check we got request full URL button + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + request_button = q('input[type=submit][value*="Request full access"]') + self.assertEquals(len(request_button), 1) + + request_url = request_button.parents("form").attr("action") + + # request URL to be sent + mailbox_before = len(mail_outbox) + r = self.client.post(request_url) + self.assertEquals(r.status_code, 200) + + self.assertEquals(len(mail_outbox), mailbox_before + 1) + self.assertTrue("Full URL for managing submission" in mail_outbox[-1]["Subject"]) + self.assertTrue(name in mail_outbox[-1]["Subject"]) if not settings.USE_DB_REDESIGN_PROXY_CLASSES: diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index 42b7d9063..56a9ea47a 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -4,14 +4,20 @@ import datetime from django.conf import settings from django.contrib.sites.models import Site +from django.core.urlresolvers import reverse as urlreverse from ietf.idtracker.models import (InternetDraft, PersonOrOrgInfo, IETFWG, IDAuthor, EmailAddress, IESGLogin, BallotInfo) +from ietf.submit.models import TempIdAuthors from ietf.utils.mail import send_mail from ietf.idrfc.utils import add_document_comment +from ietf.utils import unaccent +from redesign.doc.models import * +from redesign.person.models import Person, Alias, Email +from redesign.doc.utils import active_ballot_positions -# Some usefull states +# Some useful states UPLOADED = 1 WAITING_AUTHENTICATION = 4 MANUAL_POST_REQUESTED = 5 @@ -26,12 +32,15 @@ NONE_WG = 1027 def request_full_url(request, submission): - subject = 'Full url for managing submission of draft %s' % submission.filename + subject = 'Full URL for managing submission of draft %s' % submission.filename from_email = settings.IDSUBMIT_FROM_EMAIL - to_email = ['%s <%s>' % i.email() for i in submission.tempidauthors_set.all()] + 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, - 'domain': Site.objects.get_current().domain}) + {'submission': submission, + 'url': url}) def perform_post(submission): @@ -80,6 +89,72 @@ def perform_post(submission): send_announcements(submission, draft, state_change_msg) submission.save() +def perform_postREDESIGN(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) + draft.tags.remove(DocInfoTagName.objects.get(slug="exp-tomb")) + except Document.DoesNotExist: + draft = Document(name=submission.filename) + draft.intended_std_level = None + + draft.time = datetime.datetime.now() + draft.title = submission.id_document_name + draft.group_id = group_id + draft.rev = submission.revision + draft.pages = submission.txt_page_count + draft.abstract = submission.abstract + was_rfc = draft.state_id == "rfc" + draft.state_id = "active" + draft.save() + + DocAlias.objects.get_or_create(name=submission.filename, document=draft) + + update_authors(draft, submission) + + # new revision event + try: + a = submission.tempidauthors_set.get(author_order=0) + submitter = ensure_person_email_info_exists(a).person + except TempIdAuthors.DoesNotExist: + 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() + + + # 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 AD Followup from Revised ID Needed" + e.by = system + e.save() + + state_change_msg = e.desc + + move_docs(submission) + submission.status_id = POSTED + + announce_to_lists(submission) + if draft.iesg_state != None and not was_rfc: + announce_new_version(submission, draft, state_change_msg) + announce_to_authors(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(submission) @@ -126,9 +201,29 @@ def announce_new_version(submission, draft, state_change_msg): 'msg': state_change_msg}) +def announce_new_versionREDESIGN(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(None, 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(submission): authors = submission.tempidauthors_set.order_by('author_order') - cc = list(set([i.email()[1] for i in authors])) + 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) @@ -202,6 +297,85 @@ def update_authors(draft, submission): 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) + + try: + a = DocumentAuthor.objects.get(document=draft, author=email) + except DocumentAuthor.DoesNotExist: + 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: @@ -215,6 +389,8 @@ def is_secretariat(user): 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(','): @@ -274,7 +450,7 @@ class DraftValidation(object): self.validate_wg() def validate_wg(self): - if self.wg and not self.wg.status.pk == IETFWG.ACTIVE: + 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): @@ -314,8 +490,7 @@ class DraftValidation(object): self.add_warning('creation_date', 'Creation Date must be within 3 days of submission date') def get_authors(self): - tmpauthors = self.draft.tempidauthors_set.exclude(author_order=0).order_by('author_order') - return tmpauthors + 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) diff --git a/ietf/submit/views.py b/ietf/submit/views.py index 852b4b771..f9ed743cd 100644 --- a/ietf/submit/views.py +++ b/ietf/submit/views.py @@ -127,9 +127,9 @@ def draft_status(request, submission_id, submission_hash=None, message=None): to_email += [i.person.email()[1] for i in detail.group_acronym.wgchair_set.all()] to_email = list(set(to_email)) if to_email: - metadata_form = MetaDataForm(draft=detail, validation=validation) + authors = detail.tempidauthors_set.exclude(author_order=0).order_by('author_order') send_mail(request, to_email, from_email, subject, 'submit/submission_approval.txt', - {'submitter': submitter, 'form': metadata_form, + {'submitter': submitter, 'authors': authors, 'draft': detail, 'domain': Site.objects.get_current().domain}) return HttpResponseRedirect(reverse(draft_status, None, kwargs={'submission_id': detail.submission_id})) else: @@ -144,7 +144,11 @@ def draft_status(request, submission_id, submission_hash=None, message=None): allow_edit = None message = ('success', 'Your submission is pending email authentication. An email has been sent you with instructions.') else: - return HttpResponseRedirect(reverse(draft_edit, None, kwargs={'submission_id': detail.submission_id, 'submission_hash': submission_hash})) + submission_hash = detail.get_hash() + if submission_hash: + return HttpResponseRedirect(reverse('draft_edit_by_hash', None, kwargs={'submission_id': detail.submission_id, 'submission_hash': submission_hash})) + else: + return HttpResponseRedirect(reverse(draft_edit, None, kwargs={'submission_id': detail.submission_id })) else: auto_post_form = AutoPostForm(draft=detail, validation=validation) @@ -207,6 +211,7 @@ def draft_edit(request, submission_id, submission_hash=None): 'detail': detail, 'validation': validation, 'form': form, + 'settings': settings }, context_instance=RequestContext(request)) diff --git a/ietf/templates/submit/confirm_autopost.txt b/ietf/templates/submit/confirm_autopost.txt index 29243043d..302ded0cc 100644 --- a/ietf/templates/submit/confirm_autopost.txt +++ b/ietf/templates/submit/confirm_autopost.txt @@ -1,6 +1,7 @@ -Follow this link to confirm you Auto-Post of I-D {{ draft.filename }}-{{ draft.revision }} +Follow this link to confirm your Auto-Post of +I-D {{ draft.filename }}-{{ draft.revision }}: -I-D Submission Tool URL: http://{{ domain }}/submit/status/{{ draft.submission_id }}/confirm/{{ draft.auth_key }}/ +I-D Submission Tool URL: {{ confirm_url|safe }} Remember that you can cancel the submission from: -http://{{ domain }}/submit/status/{{ draft.submission_id }}/{{ draft.get_hash }}/ +{{ status_url|safe }} diff --git a/ietf/templates/submit/draft_edit.html b/ietf/templates/submit/draft_edit.html index 2893654a1..3426ec3f2 100644 --- a/ietf/templates/submit/draft_edit.html +++ b/ietf/templates/submit/draft_edit.html @@ -14,6 +14,7 @@ table.ietf-table span.field-error { display: block; color: red; } {% block pagehead %} {{ block.super }} + + {% if can_cancel %}