From 3af2554b2fbfcfae78a374cc569f23df61e30596 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz Date: Sat, 16 Sep 2017 09:35:42 +0000 Subject: [PATCH] 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 --- ietf/api/urls.py | 24 ++ ietf/submit/forms.py | 63 +++-- ietf/submit/test_submission.txt | 6 +- ietf/submit/test_submission.xml | 6 +- ietf/submit/test_submission_invalid_yang.txt | 4 +- ietf/submit/tests.py | 200 +++++++++---- ietf/submit/utils.py | 201 ++++++++++++- ietf/submit/views.py | 280 +++++++------------ ietf/urls.py | 18 +- 9 files changed, 521 insertions(+), 281 deletions(-) create mode 100644 ietf/api/urls.py diff --git a/ietf/api/urls.py b/ietf/api/urls.py new file mode 100644 index 000000000..7cb85e8f4 --- /dev/null +++ b/ietf/api/urls.py @@ -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[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)), + ] + diff --git a/ietf/submit/forms.py b/ietf/submit/forms.py index 260fe6db2..b38351598 100644 --- a/ietf/submit/forms.py +++ b/ietf/submit/forms.py @@ -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.

' '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) diff --git a/ietf/submit/test_submission.txt b/ietf/submit/test_submission.txt index 30e063c47..17c206a75 100644 --- a/ietf/submit/test_submission.txt +++ b/ietf/submit/test_submission.txt @@ -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 diff --git a/ietf/submit/test_submission.xml b/ietf/submit/test_submission.xml index 4dbe41382..c147589b6 100644 --- a/ietf/submit/test_submission.xml +++ b/ietf/submit/test_submission.xml @@ -3,8 +3,8 @@ - Testing Tests - + %(title)s + Test Centre Inc.
@@ -13,7 +13,7 @@ Some Where 12345 UK - author@example.com + %(email)s
diff --git a/ietf/submit/test_submission_invalid_yang.txt b/ietf/submit/test_submission_invalid_yang.txt index d3b9a9d3d..ef90b3fcb 100644 --- a/ietf/submit/test_submission_invalid_yang.txt +++ b/ietf/submit/test_submission_invalid_yang.txt @@ -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 diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py index ec77c6ef6..e1688df53 100644 --- a/ietf/submit/tests.py +++ b/ietf/submit/tests.py @@ -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) diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index 194017afd..66c9fec57 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -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 + + diff --git a/ietf/submit/views.py b/ietf/submit/views.py index 575165497..77d4e12ad 100644 --- a/ietf/submit/views.py +++ b/ietf/submit/views.py @@ -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 - - 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)) - 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") + authors, abstract, file_name, file_size = get_draft_meta(form) + submission = get_submission(form) 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)", diff --git a/ietf/urls.py b/ietf/urls.py index bbbead1b0..1bdbedbd7 100644 --- a/ietf/urls.py +++ b/ietf/urls.py @@ -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[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