Added an API for draft submission, at /api/submit. Added an urls.py file under api/ to hold api urls, and moved those from ietf/urls.py. Refactored out many parts of the regular submission forms and functions in submit/forms.py and submit/views.py in order to re-use the appropriate parts for the submission API. Moved support functions to submit/utils.py. Added a new validation errors for missing docName in xml-based submissions. Updated the submission test document templates to use insert additional values. Added failure and success test cases for automated API submissions, and refactored some test utility functions.

- Legacy-Id: 14125
This commit is contained in:
Henrik Levkowetz 2017-09-16 09:35:42 +00:00
parent da23da1e8e
commit 3af2554b2f
9 changed files with 521 additions and 281 deletions

24
ietf/api/urls.py Normal file
View file

@ -0,0 +1,24 @@
# Copyright The IETF Trust 2017, All Rights Reserved
from django.conf.urls import include
from ietf import api
from ietf.meeting import views as meeting_views
from ietf.submit import views as submit_views
from ietf.utils.urls import url
api.autodiscover()
urlpatterns = [
# Top endpoint for Tastypie's REST API (this isn't standard Tastypie):
url(r'^v1/?$', api.top_level),
# Custom API endpoints
url(r'^notify/meeting/import_recordings/(?P<number>[a-z0-9-]+)/?$', meeting_views.api_import_recordings),
url(r'^submit/?$', submit_views.api_submit),
]
# Additional (standard) Tastypie endpoints
for n,a in api._api_list:
urlpatterns += [
url(r'^v1/', include(a.urls)),
]

View file

@ -31,14 +31,11 @@ from ietf.submit.parsers.ps_parser import PSParser
from ietf.submit.parsers.xml_parser import XMLParser
from ietf.utils.draft import Draft
class SubmissionUploadForm(forms.Form):
txt = forms.FileField(label=u'.txt format', required=False)
xml = forms.FileField(label=u'.xml format', required=False)
pdf = forms.FileField(label=u'.pdf format', required=False)
ps = forms.FileField(label=u'.ps format', required=False)
class SubmissionBaseUploadForm(forms.Form):
xml = forms.FileField(label=u'.xml format', required=True)
def __init__(self, request, *args, **kwargs):
super(SubmissionUploadForm, self).__init__(*args, **kwargs)
super(SubmissionBaseUploadForm, self).__init__(*args, **kwargs)
self.remote_ip = request.META.get('REMOTE_ADDR', None)
@ -56,6 +53,14 @@ class SubmissionUploadForm(forms.Form):
self.authors = []
self.parsed_draft = None
self.file_types = []
# No code currently (14 Sep 2017) uses this class directly; it is
# only used through its subclasses. The two assignments below are
# set to trigger an exception if it is used directly only to make
# sure that adequate consideration is made if it is decided to use it
# directly in the future. Feel free to set these appropriately to
# avoid the exceptions in that case:
self.formats = None # None will raise an exception in clean() if this isn't changed in a subclass
self.base_formats = None # None will raise an exception in clean() if this isn't changed in a subclass
def set_cutoff_warnings(self):
now = datetime.datetime.now(pytz.utc)
@ -96,6 +101,7 @@ class SubmissionUploadForm(forms.Form):
'The last submission time for the I-D submission was %s.<br/><br>'
'The I-D submission tool will be reopened after %s (IETF-meeting local time).' % (cutoff_01_str, reopen_str))
self.shutdown = True
def clean_file(self, field_name, parser_class):
f = self.cleaned_data[field_name]
if not f:
@ -107,15 +113,6 @@ class SubmissionUploadForm(forms.Form):
return f
def clean_txt(self):
return self.clean_file("txt", PlainParser)
def clean_pdf(self):
return self.clean_file("pdf", PDFParser)
def clean_ps(self):
return self.clean_file("ps", PSParser)
def clean_xml(self):
return self.clean_file("xml", XMLParser)
@ -123,13 +120,13 @@ class SubmissionUploadForm(forms.Form):
if self.shutdown and not has_role(self.request.user, "Secretariat"):
raise forms.ValidationError('The submission tool is currently shut down')
for ext in ['txt', 'pdf', 'xml', 'ps']:
for ext in self.formats:
f = self.cleaned_data.get(ext, None)
if not f:
continue
self.file_types.append('.%s' % ext)
if not ('.txt' in self.file_types or '.xml' in self.file_types):
raise forms.ValidationError('You must submit at least a valid .txt or a valid .xml file; didn\'t find either.')
raise forms.ValidationError('Unexpected submission file types; found %s, but %s is required' % (', '.join(self.file_types), ' or '.join(self.base_formats)))
#debug.show('self.cleaned_data["xml"]')
if self.cleaned_data.get('xml'):
@ -168,6 +165,8 @@ class SubmissionUploadForm(forms.Form):
)
self.xmlroot = self.xmltree.getroot()
draftname = self.xmlroot.attrib.get('docName')
if draftname is None:
raise forms.ValidationError("No docName attribute found in the xml root element")
revmatch = re.search("-[0-9][0-9]$", draftname)
if revmatch:
self.revision = draftname[-2:]
@ -273,7 +272,7 @@ class SubmissionUploadForm(forms.Form):
settings.IDSUBMIT_MAX_DAILY_SUBMISSIONS, settings.IDSUBMIT_MAX_DAILY_SUBMISSIONS_SIZE,
)
return super(SubmissionUploadForm, self).clean()
return super(SubmissionBaseUploadForm, self).clean()
def check_submissions_tresholds(self, which, filter_kwargs, max_amount, max_size):
submissions = Submission.objects.filter(**filter_kwargs)
@ -332,6 +331,34 @@ class SubmissionUploadForm(forms.Form):
else:
return None
class SubmissionManualUploadForm(SubmissionBaseUploadForm):
xml = forms.FileField(label=u'.xml format', required=False) # xml field with required=False instead of True
txt = forms.FileField(label=u'.txt format', required=False)
pdf = forms.FileField(label=u'.pdf format', required=False)
ps = forms.FileField(label=u'.ps format', required=False)
def __init__(self, request, *args, **kwargs):
super(SubmissionManualUploadForm, self).__init__(request, *args, **kwargs)
self.formats = ['txt', 'pdf', 'xml', 'ps', ]
self.base_formats = ['txt', 'xml', ]
def clean_txt(self):
return self.clean_file("txt", PlainParser)
def clean_pdf(self):
return self.clean_file("pdf", PDFParser)
def clean_ps(self):
return self.clean_file("ps", PSParser)
class SubmissionAutoUploadForm(SubmissionBaseUploadForm):
user = forms.EmailField(required=True)
def __init__(self, request, *args, **kwargs):
super(SubmissionAutoUploadForm, self).__init__(request, *args, **kwargs)
self.formats = ['xml', ]
self.base_formats = ['xml', ]
class NameEmailForm(forms.Form):
name = forms.CharField(required=True)
email = forms.EmailField(label=u'Email address', required=True)

View file

@ -2,7 +2,7 @@
Network Working Group A. Name
Network Working Group %(initials)s %(surname)s
Internet-Draft Test Centre Inc.
Intended status: Informational %(month)s %(year)s
Expires: %(expiration)s
@ -180,13 +180,13 @@ Table of Contents
Author's Address
Author Name
%(author)s
Test Centre Inc.
42 Some Road
Some Where 12345
UK
Email: author@example.com
Email: %(email)s

View file

@ -3,8 +3,8 @@
<?rfc toc="yes"?>
<rfc category="info" docName="%(name)s" ipr="trust200902">
<front>
<title>Testing&nbsp;Tests</title>
<author fullname="Author Name" initials="A." surname="Name">
<title>%(title)s</title>
<author fullname="%(author)s" initials="%(initials)s" surname="%(surname)s">
<organization>Test Centre Inc.</organization>
<address>
@ -13,7 +13,7 @@
<city>Some Where 12345</city>
<country>UK</country>
</postal>
<email>author@example.com</email>
<email>%(email)s</email>
</address>
</author>
<date month="%(month)s" year="%(year)s" />

View file

@ -180,13 +180,13 @@ Table of Contents
Author's Address
Author Name
%(author)s
Test Centre Inc.
42 Some Road
Some Where 12345
UK
Email: author@example.com
Email: %(email)s

View file

@ -1,10 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright The IETF Trust 2011, All Rights Reserved
from __future__ import unicode_literals, print_function
import datetime
import os
import shutil
import email
import os
import re
import shutil
from StringIO import StringIO
from pyquery import PyQuery
@ -19,6 +22,7 @@ from ietf.doc.models import Document, DocAlias, DocEvent, State, BallotDocEvent,
from ietf.group.models import Group
from ietf.group.utils import setup_default_community_list_for_group
from ietf.meeting.models import Meeting
from ietf.meeting.factories import MeetingFactory
from ietf.message.models import Message
from ietf.name.models import FormalLanguageName
from ietf.person.models import Person
@ -31,30 +35,37 @@ from ietf.utils.test_data import make_test_data
from ietf.utils.test_utils import login_testing_unauthorized, unicontent, TestCase
def submission_file(name, rev, group, format, templatename, author=None):
def submission_file(name, rev, group, format, templatename, author=None, email=None, title=None, year=None, ascii=True):
# construct appropriate text draft
f = open(os.path.join(settings.BASE_DIR, "submit", templatename))
template = f.read()
f.close()
if not author:
if author is None:
author = PersonFactory()
if email is None:
email = author.email().address.lower()
if title is None:
title = "Test Document"
if year is None:
year = datetime.date.today().strftime("%Y")
submission_text = template % dict(
date=datetime.date.today().strftime("%d %B %Y"),
expiration=(datetime.date.today() + datetime.timedelta(days=100)).strftime("%d %B, %Y"),
year=datetime.date.today().strftime("%Y"),
year=year,
month=datetime.date.today().strftime("%B"),
name="%s-%s" % (name, rev),
group=group or "",
author=author.name,
author=author.ascii if ascii else author.name,
initials=author.initials(),
surname=author.last_name(),
email=author.email().address.lower(),
surname=author.ascii_parts()[3] if ascii else author.name_parts()[3],
email=email,
title=title,
)
file = StringIO(submission_text)
file.name = "%s-%s.%s" % (name, rev, format)
return file
return file, author
class SubmitTests(TestCase):
def setUp(self):
@ -99,7 +110,7 @@ class SubmitTests(TestCase):
settings.SUBMIT_YANG_INVAL_MODEL_DIR = self.saved_yang_inval_model_dir
def do_submission(self, name, rev, group=None, formats=["txt",]):
def do_submission(self, name, rev, group=None, formats=["txt",], author=None):
# break early in case of missing configuration
self.assertTrue(os.path.exists(settings.IDSUBMIT_IDNITS_BINARY))
@ -113,8 +124,10 @@ class SubmitTests(TestCase):
# submit
files = {}
if author is None:
author = PersonFactory()
for format in formats:
files[format] = submission_file(name, rev, group, format, "test_submission.%s" % format)
files[format], __ = submission_file(name, rev, group, format, "test_submission.%s" % format, author=author)
r = self.client.post(url, files)
if r.status_code != 302:
@ -128,15 +141,17 @@ class SubmitTests(TestCase):
self.assertTrue(os.path.exists(os.path.join(self.staging_dir, u"%s-%s.%s" % (name, rev, format))))
self.assertEqual(Submission.objects.filter(name=name).count(), 1)
submission = Submission.objects.get(name=name)
self.assertTrue(all([ c.passed!=False for c in submission.checks.all() ]))
if len(submission.authors) != 1:
debug.show('submission')
debug.pprint('submission.__dict__')
self.assertEqual(len(submission.authors), 1)
author = submission.authors[0]
self.assertEqual(author["name"], "Author Name")
self.assertEqual(author["email"], "author@example.com")
self.assertEqual(author["affiliation"], "Test Centre Inc.")
self.assertEqual(author["country"], "UK")
a = submission.authors[0]
self.assertEqual(a["name"], author.ascii)
self.assertEqual(a["email"], author.email().address.lower())
self.assertEqual(a["affiliation"], "Test Centre Inc.")
self.assertEqual(a["country"], "UK")
return status_url
return status_url, author
def supply_extra_metadata(self, name, status_url, submitter_name, submitter_email, replaces):
# check the page
@ -204,12 +219,12 @@ class SubmitTests(TestCase):
rev = "00"
group = "mars"
status_url = self.do_submission(name, rev, group, formats)
status_url, author = self.do_submission(name, rev, group, formats)
# supply submitter info, then draft should be in and ready for approval
mailbox_before = len(outbox)
replaced_alias = draft.docalias_set.first()
r = self.supply_extra_metadata(name, status_url, "Author Name", "author@example.com",
r = self.supply_extra_metadata(name, status_url, author.ascii, author.email().address.lower(),
replaces=str(replaced_alias.pk) + "," + str(sug_replaced_alias.pk))
self.assertEqual(r.status_code, 302)
@ -245,7 +260,7 @@ class SubmitTests(TestCase):
new_revision = draft.latest_event(type="new_revision")
self.assertEqual(draft.group.acronym, "mars")
self.assertEqual(new_revision.type, "new_revision")
self.assertEqual(new_revision.by.name, "Author Name")
self.assertEqual(new_revision.by.name, author.name)
self.assertTrue(draft.latest_event(type="added_suggested_replaces"))
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))))
@ -255,8 +270,7 @@ class SubmitTests(TestCase):
self.assertEqual(draft.get_state("draft-stream-%s" % draft.stream_id).slug, "wg-doc")
authors = draft.documentauthor_set.all()
self.assertEqual(len(authors), 1)
self.assertEqual(authors[0].person.plain_name(), "Author Name")
self.assertEqual(authors[0].email.address, "author@example.com")
self.assertEqual(authors[0].person, author)
self.assertEqual(set(draft.formal_languages.all()), set(FormalLanguageName.objects.filter(slug="json")))
self.assertEqual(draft.relations_that_doc("replaces").count(), 1)
self.assertTrue(draft.relations_that_doc("replaces").first().target, replaced_alias)
@ -264,7 +278,7 @@ class SubmitTests(TestCase):
self.assertTrue(draft.relations_that_doc("possibly-replaces").first().target, sug_replaced_alias)
self.assertEqual(len(outbox), mailbox_before + 4)
self.assertTrue((u"I-D Action: %s" % name) in outbox[-3]["Subject"])
self.assertTrue("Author Name" in unicode(outbox[-3]))
self.assertTrue(author.ascii in unicode(outbox[-3]))
self.assertTrue("New Version Notification" in outbox[-2]["Subject"])
self.assertTrue(name in unicode(outbox[-2]))
self.assertTrue("mars" in unicode(outbox[-2]))
@ -302,17 +316,12 @@ class SubmitTests(TestCase):
if not stream_type=='ietf':
draft.stream_id=stream_type
draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")])
if not change_authors:
draft.documentauthor_set.all().delete()
author_person, author_email = ensure_person_email_info_exists(u'Author Name',u'author@example.com')
draft.documentauthor_set.create(person=author_person, email=author_email)
else:
prev_author = draft.documentauthor_set.all()[0]
if change_authors:
# Make it such that one of the previous authors has an invalid email address
bogus_person, bogus_email = ensure_person_email_info_exists(u'Bogus Person',None)
DocumentAuthor.objects.create(document=draft, person=bogus_person, email=bogus_email, order=draft.documentauthor_set.latest('order').order+1)
prev_author = draft.documentauthor_set.all()[0]
# pretend IANA reviewed it
draft.set_state(State.objects.get(used=True, type="draft-iana-review", slug="not-ok"))
@ -342,7 +351,7 @@ class SubmitTests(TestCase):
with open(os.path.join(self.repository_dir, "%s-%s.txt" % (name, old_rev)), 'w') as f:
f.write("a" * 2000)
status_url = self.do_submission(name, rev, group, formats)
status_url, author = self.do_submission(name, rev, group, formats, author=prev_author.person)
# supply submitter info, then previous authors get a confirmation email
mailbox_before = len(outbox)
@ -440,12 +449,11 @@ class SubmitTests(TestCase):
self.assertEqual(draft.get_state_slug("draft-iana-review"), "changed")
authors = draft.documentauthor_set.all()
self.assertEqual(len(authors), 1)
self.assertEqual(authors[0].person.plain_name(), "Author Name")
self.assertEqual(authors[0].email.address, "author@example.com")
self.assertIn(author, [ a.person for a in authors ])
self.assertEqual(len(outbox), mailbox_before + 3)
self.assertTrue((u"I-D Action: %s" % name) in outbox[-3]["Subject"])
self.assertTrue((u"I-D Action: %s" % name) in draft.message_set.order_by("-time")[0].subject)
self.assertTrue("Author Name" in unicode(outbox[-3]))
self.assertTrue(author.ascii in unicode(outbox[-3]))
self.assertTrue("i-d-announce@" in outbox[-3]['To'])
self.assertTrue("New Version Notification" in outbox[-2]["Subject"])
self.assertTrue(name in unicode(outbox[-2]))
@ -466,7 +474,7 @@ class SubmitTests(TestCase):
self.submit_existing(["txt", "xml"])
def test_submit_existing_txt_preserve_authors(self):
self.submit_existing(["txt"],change_authors=False)
self.submit_existing(["txt"], change_authors=False)
def test_submit_existing_rg(self):
self.submit_existing(["txt"],group_type='rg')
@ -488,7 +496,7 @@ class SubmitTests(TestCase):
rev = "00"
group = None
status_url = self.do_submission(name, rev, group, formats)
status_url, author = self.do_submission(name, rev, group, formats)
# supply submitter info, then draft should be be ready for email auth
mailbox_before = len(outbox)
@ -505,7 +513,7 @@ class SubmitTests(TestCase):
self.assertTrue("Confirm submission" in confirm_email["Subject"])
self.assertTrue(name in confirm_email["Subject"])
# both submitter and author get email
self.assertTrue("author@example.com" in confirm_email["To"])
self.assertTrue(author.email().address.lower() in confirm_email["To"])
self.assertTrue("submitter@example.com" in confirm_email["To"])
self.assertFalse("chairs have been copied" in unicode(confirm_email))
@ -543,7 +551,7 @@ class SubmitTests(TestCase):
replaces_count = draft.relateddocument_set.filter(relationship_id='replaces').count()
name = draft.name
rev = '%02d'%(int(draft.rev)+1)
status_url = self.do_submission(name,rev)
status_url, author = self.do_submission(name,rev)
mailbox_before = len(outbox)
replaced_alias = draft.docalias_set.first()
r = self.supply_extra_metadata(name, status_url, "Submitter Name", "author@example.com", replaces=str(replaced_alias.pk))
@ -611,7 +619,7 @@ class SubmitTests(TestCase):
name = "draft-ietf-mars-testing-tests"
rev = "00"
status_url = self.do_submission(name, rev)
status_url, author = self.do_submission(name, rev)
# check we got cancel button
r = self.client.get(status_url)
@ -633,7 +641,7 @@ class SubmitTests(TestCase):
name = "draft-ietf-mars-testing-tests"
rev = "00"
status_url = self.do_submission(name, rev)
status_url, author = self.do_submission(name, rev)
# check we have edit button
r = self.client.get(status_url)
@ -897,7 +905,7 @@ class SubmitTests(TestCase):
# submit
files = {}
for format in formats:
files[format] = submission_file(name, rev, group, "bad", "test_submission.bad")
files[format], author = submission_file(name, rev, group, "bad", "test_submission.bad")
r = self.client.post(url, files)
@ -950,7 +958,9 @@ class SubmitTests(TestCase):
#author = PersonFactory(name=u"Jörgen Nilsson".encode('latin1'))
user = UserFactory(first_name=u"Jörgen", last_name=u"Nilsson")
author = PersonFactory(user=user)
files = {"txt": submission_file(name, rev, group, "txt", "test_submission.nonascii", author=author) }
file, __ = submission_file(name, rev, group, "txt", "test_submission.nonascii", author=author, ascii=False)
files = {"txt": file }
r = self.client.post(url, files)
self.assertEqual(r.status_code, 302)
@ -1270,7 +1280,7 @@ ZSBvZiBsaW5lcyAtIGJ1dCBpdCBjb3VsZCBiZSBhIGRyYWZ0Cg==
if r.status_code != 302:
q = PyQuery(r.content)
print q
print(q)
self.assertEqual(r.status_code, 302)
@ -1396,7 +1406,7 @@ Thank you
if r.status_code != 302:
q = PyQuery(r.content)
print q
print(q)
self.assertEqual(r.status_code, 302)
@ -1475,7 +1485,7 @@ Subject: test
# submit
files = {}
for format in formats:
files[format] = submission_file(name, rev, group, format, "test_submission.%s" % format)
files[format], author = submission_file(name, rev, group, format, "test_submission.%s" % format)
r = self.client.post(url, files)
if r.status_code != 302:
@ -1519,3 +1529,99 @@ Subject: test
self.assertEqual(submission.submitter, email.utils.formataddr((submitter_name, submitter_email)))
return r
class ApiSubmitTests(TestCase):
def setUp(self):
# break early in case of missing configuration
self.assertTrue(os.path.exists(settings.IDSUBMIT_IDNITS_BINARY))
self.saved_idsubmit_staging_path = settings.IDSUBMIT_STAGING_PATH
self.staging_dir = self.tempdir('submit-staging')
settings.IDSUBMIT_STAGING_PATH = self.staging_dir
self.saved_internet_draft_path = settings.INTERNET_DRAFT_PATH
self.saved_idsubmit_repository_path = settings.IDSUBMIT_REPOSITORY_PATH
self.repository_dir = self.tempdir('submit-repository')
settings.INTERNET_DRAFT_PATH = settings.IDSUBMIT_REPOSITORY_PATH = self.repository_dir
self.saved_archive_dir = settings.INTERNET_DRAFT_ARCHIVE_DIR
self.archive_dir = self.tempdir('submit-archive')
settings.INTERNET_DRAFT_ARCHIVE_DIR = self.archive_dir
MeetingFactory(type_id='ietf', date=datetime.date.today()+datetime.timedelta(days=60))
def tearDown(self):
shutil.rmtree(self.staging_dir)
shutil.rmtree(self.repository_dir)
shutil.rmtree(self.archive_dir)
settings.IDSUBMIT_STAGING_PATH = self.saved_idsubmit_staging_path
settings.INTERNET_DRAFT_PATH = self.saved_internet_draft_path
settings.IDSUBMIT_REPOSITORY_PATH = self.saved_idsubmit_repository_path
settings.INTERNET_DRAFT_ARCHIVE_DIR = self.saved_archive_dir
def post_submission(self, rev, author=None, name=None, group=None, email=None, title=None, year=None):
url = urlreverse('ietf.submit.views.api_submit')
if author is None:
author = PersonFactory()
if name is None:
slug = re.sub('[^a-z0-9-]+', '', author.ascii_parts()[3].lower())
name = 'draft-%s-foo' % slug
if email is None:
email = author.user.username
# submit
data = {}
data['xml'], author = submission_file(name, rev, group, 'xml', "test_submission.xml", author=author, email=email, title=title, year=year)
data['user'] = email
r = self.client.post(url, data)
return r, author, name
def test_api_submit_bad_method(self):
url = urlreverse('ietf.submit.views.api_submit')
r = self.client.get(url)
self.assertEqual(r.status_code, 405)
def test_api_submit_ok(self):
r, author, name = self.post_submission('00')
expected = "Upload of %s OK, confirmation requests sent to:\n %s" % (name, author.formatted_email())
self.assertContains(r, expected, status_code=200)
def test_api_submit_no_user(self):
email='nonexistant.user@example.org'
r, author, name = self.post_submission('00', email=email)
expected = "No such user: %s" % email
self.assertContains(r, expected, status_code=404)
def test_api_submit_no_person(self):
user = UserFactory()
email = user.username
r, author, name = self.post_submission('00', email=email)
expected = "No person with username %s" % email
self.assertContains(r, expected, status_code=404)
def test_api_submit_wrong_revision(self):
r, author, name = self.post_submission('01')
expected = "Invalid revision (revision 00 is expected)"
self.assertContains(r, expected, status_code=400)
def test_api_submit_pending_submission(self):
r, author, name = self.post_submission('00')
expected = "Upload of"
self.assertContains(r, expected, status_code=200)
r, author, name = self.post_submission('00', author=author, name=name)
expected = "A submission with same name and revision is currently being processed"
self.assertContains(r, expected, status_code=400)
def test_api_submit_no_title(self):
r, author, name = self.post_submission('00', title="")
expected = "Could not extract a valid title from the upload"
self.assertContains(r, expected, status_code=400)
def test_api_submit_failed_idnits(self):
r, author, name = self.post_submission('00', year="1900")
expected = "Document date must be within 3 days of submission date"
self.assertContains(r, expected, status_code=400)

View file

@ -3,9 +3,12 @@
import os
import datetime
import six # pyflakes:ignore
import xml2rfc
from unidecode import unidecode
from django.conf import settings
from django.core.validators import validate_email, ValidationError
from django.utils.module_loading import import_string
import debug # pyflakes:ignore
@ -18,12 +21,15 @@ 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.name.models import StreamName, FormalLanguageName
from ietf.person.models import Person, Email
from ietf.community.utils import update_name_contains_indexes_with_new_doc
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.submit.mail import ( announce_to_lists, announce_new_version, announce_to_authors,
send_approval_request_to_group, send_submission_confirmation )
from ietf.submit.models import Submission, SubmissionEvent, Preapproval, DraftSubmissionStateName, SubmissionCheck
from ietf.utils import log
from ietf.utils.accesstoken import generate_random_key
from ietf.utils.draft import Draft
from ietf.utils.mail import is_valid_email
@ -461,16 +467,16 @@ def cancel_submission(submission):
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():
from ietf.submit.forms import SubmissionManualUploadForm
for ext in SubmissionManualUploadForm.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():
from ietf.submit.forms import SubmissionManualUploadForm
for ext in SubmissionManualUploadForm.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):
@ -533,3 +539,184 @@ def expire_submission(submission, by):
submission.save()
SubmissionEvent.objects.create(submission=submission, by=by, desc="Cancelled expired submission")
def get_draft_meta(form):
authors = []
file_name = {}
abstract = None
file_size = None
for ext in form.fields.keys():
if not ext in form.formats:
continue
f = form.cleaned_data[ext]
if not f:
continue
name = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.%s' % (form.filename, form.revision, ext))
file_name[ext] = name
with open(name, 'wb+') as destination:
for chunk in f.chunks():
destination.write(chunk)
if form.cleaned_data['xml']:
if not ('txt' in form.cleaned_data and form.cleaned_data['txt']):
file_name['txt'] = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.txt' % (form.filename, form.revision))
try:
pagedwriter = xml2rfc.PaginatedTextRfcWriter(form.xmltree, quiet=True)
pagedwriter.write(file_name['txt'])
except Exception as e:
raise ValidationError("Error from xml2rfc: %s" % e)
file_size = os.stat(file_name['txt']).st_size
# Some meta-information, such as the page-count, can only
# be retrieved from the generated text file. Provide a
# parsed draft object to get at that kind of information.
with open(file_name['txt']) as txt_file:
form.parsed_draft = Draft(txt_file.read().decode('utf8'), txt_file.name)
else:
file_size = form.cleaned_data['txt'].size
if form.authors:
authors = form.authors
else:
# If we don't have an xml file, try to extract the
# relevant information from the text file
for author in form.parsed_draft.get_author_list():
full_name, first_name, middle_initial, last_name, name_suffix, email, country, company = author
name = full_name.replace("\n", "").replace("\r", "").replace("<", "").replace(">", "").strip()
if email:
try:
validate_email(email)
except ValidationError:
email = ""
def turn_into_unicode(s):
if s is None:
return u""
if isinstance(s, unicode):
return s
else:
try:
return s.decode("utf-8")
except UnicodeDecodeError:
try:
return s.decode("latin-1")
except UnicodeDecodeError:
return ""
name = turn_into_unicode(name)
email = turn_into_unicode(email)
company = turn_into_unicode(company)
authors.append({
"name": name,
"email": email,
"affiliation": company,
"country": country
})
if form.abstract:
abstract = form.abstract
else:
abstract = form.parsed_draft.get_abstract()
return authors, abstract, file_name, file_size
def get_submission(form):
submissions = Submission.objects.filter(name=form.filename,
rev=form.revision,
state_id = "waiting-for-draft").distinct()
if not submissions:
submission = Submission(name=form.filename, rev=form.revision, group=form.group)
elif len(submissions) == 1:
submission = submissions.first()
else:
raise Exception("Multiple submissions found waiting for upload")
return submission
def fill_in_submission(form, submission, authors, abstract, file_size):
# See if there is a Submission in state waiting-for-draft
# for this revision.
# If so - we're going to update it otherwise we create a new object
submission.state = DraftSubmissionStateName.objects.get(slug="uploaded")
submission.remote_ip = form.remote_ip
submission.title = form.title
submission.abstract = abstract
submission.pages = form.parsed_draft.get_pagecount()
submission.words = form.parsed_draft.get_wordcount()
submission.authors = authors
submission.first_two_pages = ''.join(form.parsed_draft.pages[:2])
submission.file_size = file_size
submission.file_types = ','.join(form.file_types)
submission.submission_date = datetime.date.today()
submission.document_date = form.parsed_draft.get_creation_date()
submission.replaces = ""
submission.save()
submission.formal_languages = FormalLanguageName.objects.filter(slug__in=form.parsed_draft.get_formal_languages())
def apply_checkers(submission, file_name):
# run submission checkers
def apply_check(submission, checker, method, fn):
func = getattr(checker, method)
passed, message, errors, warnings, items = func(fn)
check = SubmissionCheck(submission=submission, checker=checker.name, passed=passed,
message=message, errors=errors, warnings=warnings, items=items,
symbol=checker.symbol)
check.save()
for checker_path in settings.IDSUBMIT_CHECKER_CLASSES:
checker_class = import_string(checker_path)
checker = checker_class()
# ordered list of methods to try
for method in ("check_fragment_xml", "check_file_xml", "check_fragment_txt", "check_file_txt", ):
ext = method[-3:]
if hasattr(checker, method) and ext in file_name:
apply_check(submission, checker, method, file_name[ext])
break
def send_confirmation_emails(request, submission, requires_group_approval, requires_prev_authors_approval):
docevent_from_submission(request, submission, desc="Uploaded new revision")
if requires_group_approval:
submission.state = DraftSubmissionStateName.objects.get(slug="grp-appr")
submission.save()
sent_to = send_approval_request_to_group(request, submission)
desc = "sent approval email to group chairs: %s" % u", ".join(sent_to)
docDesc = u"Request for posting approval emailed to group chairs: %s" % u", ".join(sent_to)
else:
group_authors_changed = False
doc = submission.existing_document()
if doc and doc.group:
old_authors = [ author.person for author in doc.documentauthor_set.all() ]
new_authors = [ get_person_from_name_email(author["name"], author.get("email")) for author in submission.authors ]
group_authors_changed = set(old_authors)!=set(new_authors)
submission.auth_key = generate_random_key()
if requires_prev_authors_approval:
submission.state = DraftSubmissionStateName.objects.get(slug="aut-appr")
else:
submission.state = DraftSubmissionStateName.objects.get(slug="auth")
submission.save()
sent_to = send_submission_confirmation(request, submission, chair_notice=group_authors_changed)
if submission.state_id == "aut-appr":
desc = u"sent confirmation email to previous authors: %s" % u", ".join(sent_to)
docDesc = "Request for posting confirmation emailed to previous authors: %s" % u", ".join(sent_to)
else:
desc = u"sent confirmation email to submitter and authors: %s" % u", ".join(sent_to)
docDesc = "Request for posting confirmation emailed to submitter and authors: %s" % u", ".join(sent_to)
return sent_to, desc, docDesc

View file

@ -1,16 +1,15 @@
# Copyright The IETF Trust 2007, All Rights Reserved
import base64
import datetime
import os
import xml2rfc
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.models import User
from django.urls import reverse as urlreverse
from django.core.validators import validate_email, ValidationError
from django.core.validators import ValidationError
from django.http import HttpResponseRedirect, Http404, HttpResponseForbidden, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.module_loading import import_string
from django.views.decorators.csrf import csrf_exempt
import debug # pyflakes:ignore
@ -20,20 +19,17 @@ from ietf.group.models import Group
from ietf.ietfauth.utils import has_role, role_required
from ietf.mailtrigger.utils import gather_address_lists
from ietf.message.models import Message, MessageAttachment
from ietf.name.models import FormalLanguageName
from ietf.submit.forms import ( SubmissionUploadForm, AuthorForm, SubmitterForm, EditSubmissionForm,
PreapprovalForm, ReplacesForm, SubmissionEmailForm, MessageModelForm )
from ietf.submit.mail import ( send_full_url, send_approval_request_to_group,
send_submission_confirmation, send_manual_post_request, add_submission_email, get_reply_to )
from ietf.submit.models import (Submission, SubmissionCheck, Preapproval,
from ietf.submit.forms import ( SubmissionManualUploadForm, SubmissionAutoUploadForm, AuthorForm,
SubmitterForm, EditSubmissionForm, PreapprovalForm, ReplacesForm, SubmissionEmailForm, MessageModelForm )
from ietf.submit.mail import ( send_full_url, send_manual_post_request, add_submission_email, get_reply_to )
from ietf.submit.models import (Submission, Preapproval,
DraftSubmissionStateName, SubmissionEmailEvent )
from ietf.submit.utils import ( approvable_submissions_for_user, preapprovals_for_user,
recently_approved_by_user, validate_submission, create_submission_event,
docevent_from_submission, post_submission, cancel_submission, rename_submission_files,
get_person_from_name_email )
recently_approved_by_user, validate_submission, create_submission_event, docevent_from_submission,
post_submission, cancel_submission, rename_submission_files, remove_submission_files, get_draft_meta,
get_submission, fill_in_submission, apply_checkers, send_confirmation_emails )
from ietf.stats.utils import clean_country_name
from ietf.utils.accesstoken import generate_random_key, generate_access_token
from ietf.utils.draft import Draft
from ietf.utils.accesstoken import generate_access_token
from ietf.utils.log import log
from ietf.utils.mail import send_mail_message
@ -41,144 +37,20 @@ from ietf.utils.mail import send_mail_message
def upload_submission(request):
if request.method == 'POST':
try:
form = SubmissionUploadForm(request, data=request.POST, files=request.FILES)
form = SubmissionManualUploadForm(request, data=request.POST, files=request.FILES)
if form.is_valid():
authors = []
file_name = {}
abstract = None
file_size = None
for ext in form.fields.keys():
f = form.cleaned_data[ext]
if not f:
continue
authors, abstract, file_name, file_size = get_draft_meta(form)
name = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.%s' % (form.filename, form.revision, ext))
file_name[ext] = name
with open(name, 'wb+') as destination:
for chunk in f.chunks():
destination.write(chunk)
if form.cleaned_data['xml']:
if not form.cleaned_data['txt']:
file_name['txt'] = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.txt' % (form.filename, form.revision))
submission = get_submission(form)
try:
pagedwriter = xml2rfc.PaginatedTextRfcWriter(form.xmltree, quiet=True)
pagedwriter.write(file_name['txt'])
except Exception as e:
raise ValidationError("Error from xml2rfc: %s" % e)
file_size = os.stat(file_name['txt']).st_size
# Some meta-information, such as the page-count, can only
# be retrieved from the generated text file. Provide a
# parsed draft object to get at that kind of information.
with open(file_name['txt']) as txt_file:
form.parsed_draft = Draft(txt_file.read().decode('utf8'), txt_file.name)
else:
file_size = form.cleaned_data['txt'].size
if form.authors:
authors = form.authors
else:
# If we don't have an xml file, try to extract the
# relevant information from the text file
for author in form.parsed_draft.get_author_list():
full_name, first_name, middle_initial, last_name, name_suffix, email, country, company = author
name = full_name.replace("\n", "").replace("\r", "").replace("<", "").replace(">", "").strip()
if email:
try:
validate_email(email)
except ValidationError:
email = ""
def turn_into_unicode(s):
if s is None:
return u""
if isinstance(s, unicode):
return s
else:
try:
return s.decode("utf-8")
except UnicodeDecodeError:
try:
return s.decode("latin-1")
except UnicodeDecodeError:
return ""
name = turn_into_unicode(name)
email = turn_into_unicode(email)
company = turn_into_unicode(company)
authors.append({
"name": name,
"email": email,
"affiliation": company,
"country": country
})
if form.abstract:
abstract = form.abstract
else:
abstract = form.parsed_draft.get_abstract()
# See if there is a Submission in state waiting-for-draft
# for this revision.
# If so - we're going to update it otherwise we create a new object
submissions = Submission.objects.filter(name=form.filename,
rev=form.revision,
state_id = "waiting-for-draft").distinct()
if not submissions:
submission = Submission(name=form.filename, rev=form.revision, group=form.group)
elif len(submissions) == 1:
submission = submissions[0]
else:
raise Exception("Multiple submissions found waiting for upload")
try:
submission.state = DraftSubmissionStateName.objects.get(slug="uploaded")
submission.remote_ip = form.remote_ip
submission.title = form.title
submission.abstract = abstract
submission.pages = form.parsed_draft.get_pagecount()
submission.words = form.parsed_draft.get_wordcount()
submission.authors = authors
submission.first_two_pages = ''.join(form.parsed_draft.pages[:2])
submission.file_size = file_size
submission.file_types = ','.join(form.file_types)
submission.submission_date = datetime.date.today()
submission.document_date = form.parsed_draft.get_creation_date()
submission.replaces = ""
submission.save()
submission.formal_languages = FormalLanguageName.objects.filter(slug__in=form.parsed_draft.get_formal_languages())
fill_in_submission(form, submission, authors, abstract, file_size)
except Exception as e:
if submission:
submission.delete()
log("Exception: %s\n" % e)
raise
# run submission checkers
def apply_check(submission, checker, method, fn):
func = getattr(checker, method)
passed, message, errors, warnings, items = func(fn)
check = SubmissionCheck(submission=submission, checker=checker.name, passed=passed,
message=message, errors=errors, warnings=warnings, items=items,
symbol=checker.symbol)
check.save()
for checker_path in settings.IDSUBMIT_CHECKER_CLASSES:
checker_class = import_string(checker_path)
checker = checker_class()
# ordered list of methods to try
for method in ("check_fragment_xml", "check_file_xml", "check_fragment_txt", "check_file_txt", ):
ext = method[-3:]
if hasattr(checker, method) and ext in file_name:
apply_check(submission, checker, method, file_name[ext])
break
apply_checkers(submission, file_name)
create_submission_event(request, submission, desc="Uploaded submission")
# Don't add an "Uploaded new revision doevent yet, in case of cancellation
@ -186,22 +58,92 @@ def upload_submission(request):
return redirect("ietf.submit.views.submission_status", submission_id=submission.pk, access_token=submission.access_token())
except IOError as e:
if "read error" in str(e): # The server got an IOError when trying to read POST data
form = SubmissionUploadForm(request=request)
form = SubmissionManualUploadForm(request=request)
form._errors = {}
form._errors["__all__"] = form.error_class(["There was a failure receiving the complete form data -- please try again."])
else:
raise
except ValidationError as e:
form = SubmissionUploadForm(request=request)
form = SubmissionManualUploadForm(request=request)
form._errors = {}
form._errors["__all__"] = form.error_class(["There was a failure converting the xml file to text -- please verify that your xml file is valid. (%s)" % e.message])
else:
form = SubmissionUploadForm(request=request)
form = SubmissionManualUploadForm(request=request)
return render(request, 'submit/upload_submission.html',
{'selected': 'index',
'form': form})
@csrf_exempt
def api_submit(request):
"Automated submission entrypoint"
submission = None
def err(code, text):
return HttpResponse(text, status=code, reason=text, content_type='text/plain')
if request.method == 'POST':
e = None
try:
form = SubmissionAutoUploadForm(request, data=request.POST, files=request.FILES)
if form.is_valid():
username = form.cleaned_data['user']
user = User.objects.filter(username=username)
if user.count() == 0:
return err(404, "No such user: %s" % username)
if user.count() > 1:
return err(500, "Multiple matching accounts for %s" % username)
user = user.first()
if not hasattr(user, 'person'):
return err(404, "No person with username %s" % username)
authors, abstract, file_name, file_size = get_draft_meta(form)
submission = get_submission(form)
fill_in_submission(form, submission, authors, abstract, file_size)
apply_checkers(submission, file_name)
create_submission_event(request, submission, desc="Uploaded submission")
errors = validate_submission(submission)
if errors:
raise ValidationError(errors)
errors = [ c.message for c in submission.checks.all() if not c.passed ]
if errors:
raise ValidationError(errors)
if not user.username in [ a['email'] for a in authors ]:
raise ValidationError('Submitter %s is not one of the document authors' % user.username)
submission.submitter = user.person.formatted_email()
docevent_from_submission(request, submission, desc="Uploaded new revision")
requires_group_approval = (submission.rev == '00' and submission.group and submission.group.type_id in ("wg", "rg", "ietf", "irtf", "iab", "iana", "rfcedtyp") and not Preapproval.objects.filter(name=submission.name).exists())
requires_prev_authors_approval = Document.objects.filter(name=submission.name)
sent_to, desc, docDesc = send_confirmation_emails(request, submission, requires_group_approval, requires_prev_authors_approval)
msg = u"Set submitter to \"%s\" and %s" % (submission.submitter, desc)
create_submission_event(request, submission, msg)
docevent_from_submission(request, submission, docDesc, who="(System)")
return HttpResponse(
"Upload of %s OK, confirmation requests sent to:\n %s" % (submission.name, ',\n '.join(sent_to)),
content_type="text/plain")
else:
raise ValidationError(form.errors)
except IOError as e:
return err(500, "IO Error: %s" % str(e))
except ValidationError as e:
return err(400, "Validation Error: %s" % str(e))
except Exception as e:
raise
return err(500, "Exception: %s" % str(e))
finally:
if e and submission:
remove_submission_files(submission)
submission.delete()
else:
return err(405, "Method not allowed")
def note_well(request):
return render(request, 'submit/note_well.html', {'selected': 'notewell'})
@ -252,6 +194,8 @@ def submission_status(request, submission_id, access_token=None):
not key_matched
and not is_secretariat
and not submission.state_id in ("cancel", "posted") )
# Begin common code chunk
addrs = gather_address_lists('sub_confirmation_requested',submission=submission)
confirmation_list = addrs.to
confirmation_list.extend(addrs.cc)
@ -260,15 +204,10 @@ def submission_status(request, submission_id, access_token=None):
requires_prev_authors_approval = Document.objects.filter(name=submission.name)
group_authors_changed = False
doc = submission.existing_document()
if doc and doc.group:
old_authors = [ author.person for author in doc.documentauthor_set.all() ]
new_authors = [ get_person_from_name_email(author["name"], author.get("email")) for author in submission.authors ]
group_authors_changed = set(old_authors)!=set(new_authors)
message = None
if submission.state_id == "cancel":
message = ('error', 'This submission has been cancelled, modification is no longer possible.')
elif submission.state_id == "auth":
@ -309,34 +248,7 @@ def submission_status(request, submission_id, access_token=None):
post_submission(request, submission, desc)
create_submission_event(request, submission, desc)
else:
docevent_from_submission(request, submission, desc="Uploaded new revision")
if requires_group_approval:
submission.state = DraftSubmissionStateName.objects.get(slug="grp-appr")
submission.save()
sent_to = send_approval_request_to_group(request, submission)
desc = "sent approval email to group chairs: %s" % u", ".join(sent_to)
docDesc = u"Request for posting approval emailed to group chairs: %s" % u", ".join(sent_to)
else:
submission.auth_key = generate_random_key()
if requires_prev_authors_approval:
submission.state = DraftSubmissionStateName.objects.get(slug="aut-appr")
else:
submission.state = DraftSubmissionStateName.objects.get(slug="auth")
submission.save()
sent_to = send_submission_confirmation(request, submission, chair_notice=group_authors_changed)
if submission.state_id == "aut-appr":
desc = u"sent confirmation email to previous authors: %s" % u", ".join(sent_to)
docDesc = "Request for posting confirmation emailed to previous authors: %s" % u", ".join(sent_to)
else:
desc = u"sent confirmation email to submitter and authors: %s" % u", ".join(sent_to)
docDesc = "Request for posting confirmation emailed to submitter and authors: %s" % u", ".join(sent_to)
sent_to, desc, docDesc = send_confirmation_emails(request, submission, requires_group_approval, requires_prev_authors_approval)
msg = u"Set submitter to \"%s\", replaces to %s and %s" % (
submission.submitter,
", ".join(prettify_std_name(r.name) for r in replaces) if replaces else "(none)",

View file

@ -12,18 +12,15 @@ from django.views.defaults import server_error
import debug # pyflakes:ignore
from ietf import api
from ietf.doc import views_search
from ietf.group.urls import group_urls, grouptype_urls, stream_urls
from ietf.help import views as help_views
from ietf.ipr.sitemaps import IPRMap
from ietf.liaisons.sitemaps import LiaisonMap
from ietf.meeting import views as meeting_views
from ietf.utils.urls import url
admin.autodiscover()
api.autodiscover()
# sometimes, this code gets called more than once, which is an
# that seems impossible to work around.
@ -43,6 +40,7 @@ urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^admin/docs/', include('django.contrib.admindocs.urls')),
url(r'^ann/', include('ietf.nomcom.redirect_ann_urls')),
url(r'^api/', include('ietf.api.urls')),
url(r'^community/', include('ietf.community.urls')),
url(r'^accounts/settings/', include('ietf.cookies.urls')),
url(r'^doc/', include('ietf.doc.urls')),
@ -77,20 +75,6 @@ urlpatterns = [
url(r'^googlea30ad1dacffb5e5b.html', TemplateView.as_view(template_name='googlea30ad1dacffb5e5b.html')),
]
# Endpoints for Tastypie's REST API
urlpatterns += [
url(r'^api/v1/?$', api.top_level),
]
for n,a in api._api_list:
urlpatterns += [
url(r'^api/v1/', include(a.urls)),
]
# Custom API endpoints
urlpatterns += [
url(r'^api/notify/meeting/import_recordings/(?P<number>[a-z0-9-]+)/?$', meeting_views.api_import_recordings),
]
# This is needed to serve files during testing
if settings.SERVER_MODE in ('development', 'test'):
save_debug = settings.DEBUG