feat: remove deprecated /api/submit/ endpoint (#8017)
* feat: remove deprecated /api/submit endpoint * chore: remove unused imports * test: test api_submit_tombstone view
This commit is contained in:
parent
364dec3e33
commit
6aff818b72
|
@ -59,7 +59,7 @@ urlpatterns = [
|
|||
# Email alias listing
|
||||
url(r'^person/email/$', api_views.active_email_list),
|
||||
# Draft submission API
|
||||
url(r'^submit/?$', submit_views.api_submit),
|
||||
url(r'^submit/?$', submit_views.api_submit_tombstone),
|
||||
# Draft upload API
|
||||
url(r'^submission/?$', submit_views.api_submission),
|
||||
# Draft submission state API
|
||||
|
|
|
@ -2,19 +2,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import datetime
|
||||
import email
|
||||
import sys
|
||||
import tempfile
|
||||
import xml2rfc
|
||||
from contextlib import ExitStack
|
||||
|
||||
from email.utils import formataddr
|
||||
from typing import Tuple
|
||||
from unidecode import unidecode
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
|
@ -37,10 +34,8 @@ from ietf.submit.models import Submission, Preapproval
|
|||
from ietf.submit.utils import validate_submission_name, validate_submission_rev, validate_submission_document_date, remote_ip
|
||||
from ietf.submit.parsers.plain_parser import PlainParser
|
||||
from ietf.submit.parsers.xml_parser import XMLParser
|
||||
from ietf.utils import log
|
||||
from ietf.utils.draft import PlaintextDraft
|
||||
from ietf.utils.fields import ModelMultipleChoiceField
|
||||
from ietf.utils.text import normalize_text
|
||||
from ietf.utils.timezone import date_today
|
||||
from ietf.utils.xmldraft import InvalidXMLError, XMLDraft, XMLParseError
|
||||
|
||||
|
@ -371,273 +366,6 @@ class SubmissionBaseUploadForm(forms.Form):
|
|||
return None
|
||||
|
||||
|
||||
class DeprecatedSubmissionBaseUploadForm(SubmissionBaseUploadForm):
|
||||
def clean(self):
|
||||
def format_messages(where, e, log):
|
||||
out = log.write_out.getvalue().splitlines()
|
||||
err = log.write_err.getvalue().splitlines()
|
||||
m = str(e)
|
||||
if m:
|
||||
m = [ m ]
|
||||
else:
|
||||
import traceback
|
||||
typ, val, tb = sys.exc_info()
|
||||
m = traceback.format_exception(typ, val, tb)
|
||||
m = [ l.replace('\n ', ':\n ') for l in m ]
|
||||
msgs = [s for s in (["Error from xml2rfc (%s):" % (where,)] + m + out + err) if s]
|
||||
return msgs
|
||||
|
||||
if self.shutdown and not has_role(self.request.user, "Secretariat"):
|
||||
raise forms.ValidationError(self.cutoff_warning)
|
||||
|
||||
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):
|
||||
if not self.errors:
|
||||
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'):
|
||||
#if not self.cleaned_data.get('txt'):
|
||||
xml_file = self.cleaned_data.get('xml')
|
||||
file_name = {}
|
||||
xml2rfc.log.write_out = io.StringIO() # open(os.devnull, "w")
|
||||
xml2rfc.log.write_err = io.StringIO() # open(os.devnull, "w")
|
||||
tfn = None
|
||||
with ExitStack() as stack:
|
||||
@stack.callback
|
||||
def cleanup(): # called when context exited, even in case of exception
|
||||
if tfn is not None:
|
||||
os.unlink(tfn)
|
||||
|
||||
# We need to write the xml file to disk in order to hand it
|
||||
# over to the xml parser. XXX FIXME: investigate updating
|
||||
# xml2rfc to be able to work with file handles to in-memory
|
||||
# files.
|
||||
name, ext = os.path.splitext(os.path.basename(xml_file.name))
|
||||
with tempfile.NamedTemporaryFile(prefix=name+'-',
|
||||
suffix='.xml',
|
||||
mode='wb+',
|
||||
delete=False) as tf:
|
||||
tfn = tf.name
|
||||
for chunk in xml_file.chunks():
|
||||
tf.write(chunk)
|
||||
|
||||
parser = xml2rfc.XmlRfcParser(str(tfn), quiet=True)
|
||||
# --- Parse the xml ---
|
||||
try:
|
||||
self.xmltree = parser.parse(remove_comments=False)
|
||||
# If we have v2, run it through v2v3. Keep track of the submitted version, though.
|
||||
self.xmlroot = self.xmltree.getroot()
|
||||
self.xml_version = self.xmlroot.get('version', '2')
|
||||
if self.xml_version == '2':
|
||||
v2v3 = xml2rfc.V2v3XmlWriter(self.xmltree)
|
||||
self.xmltree.tree = v2v3.convert2to3()
|
||||
self.xmlroot = self.xmltree.getroot() # update to the new root
|
||||
|
||||
draftname = self.xmlroot.attrib.get('docName')
|
||||
if draftname is None:
|
||||
self.add_error('xml', "No docName attribute found in the xml root element")
|
||||
name_error = validate_submission_name(draftname)
|
||||
if name_error:
|
||||
self.add_error('xml', name_error) # This is a critical and immediate failure - do not proceed with other validation.
|
||||
else:
|
||||
revmatch = re.search("-[0-9][0-9]$", draftname)
|
||||
if revmatch:
|
||||
self.revision = draftname[-2:]
|
||||
self.filename = draftname[:-3]
|
||||
else:
|
||||
self.revision = None
|
||||
self.filename = draftname
|
||||
self.title = self.xmlroot.findtext('front/title').strip()
|
||||
if type(self.title) is str:
|
||||
self.title = unidecode(self.title)
|
||||
self.title = normalize_text(self.title)
|
||||
self.abstract = (self.xmlroot.findtext('front/abstract') or '').strip()
|
||||
if type(self.abstract) is str:
|
||||
self.abstract = unidecode(self.abstract)
|
||||
author_info = self.xmlroot.findall('front/author')
|
||||
for author in author_info:
|
||||
info = {
|
||||
"name": author.attrib.get('fullname'),
|
||||
"email": author.findtext('address/email'),
|
||||
"affiliation": author.findtext('organization'),
|
||||
}
|
||||
elem = author.find('address/postal/country')
|
||||
if elem != None:
|
||||
ascii_country = elem.get('ascii', None)
|
||||
info['country'] = ascii_country if ascii_country else elem.text
|
||||
|
||||
for item in info:
|
||||
if info[item]:
|
||||
info[item] = info[item].strip()
|
||||
self.authors.append(info)
|
||||
|
||||
# --- Prep the xml ---
|
||||
file_name['xml'] = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s%s' % (self.filename, self.revision, ext))
|
||||
try:
|
||||
prep = xml2rfc.PrepToolWriter(self.xmltree, quiet=True, liberal=True, keep_pis=[xml2rfc.V3_PI_TARGET])
|
||||
prep.options.accept_prepped = True
|
||||
self.xmltree.tree = prep.prep()
|
||||
if self.xmltree.tree == None:
|
||||
self.add_error('xml', "Error from xml2rfc (prep): %s" % prep.errors)
|
||||
except Exception as e:
|
||||
msgs = format_messages('prep', e, xml2rfc.log)
|
||||
self.add_error('xml', msgs)
|
||||
|
||||
# --- Convert to txt ---
|
||||
if not ('txt' in self.cleaned_data and self.cleaned_data['txt']):
|
||||
file_name['txt'] = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.txt' % (self.filename, self.revision))
|
||||
try:
|
||||
writer = xml2rfc.TextWriter(self.xmltree, quiet=True)
|
||||
writer.options.accept_prepped = True
|
||||
writer.write(file_name['txt'])
|
||||
log.log("In %s: xml2rfc %s generated %s from %s (version %s)" %
|
||||
( os.path.dirname(file_name['xml']),
|
||||
xml2rfc.__version__,
|
||||
os.path.basename(file_name['txt']),
|
||||
os.path.basename(file_name['xml']),
|
||||
self.xml_version))
|
||||
except Exception as e:
|
||||
msgs = format_messages('txt', e, xml2rfc.log)
|
||||
log.log('\n'.join(msgs))
|
||||
self.add_error('xml', msgs)
|
||||
|
||||
# --- Convert to html ---
|
||||
try:
|
||||
file_name['html'] = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.html' % (self.filename, self.revision))
|
||||
writer = xml2rfc.HtmlWriter(self.xmltree, quiet=True)
|
||||
writer.write(file_name['html'])
|
||||
self.file_types.append('.html')
|
||||
log.log("In %s: xml2rfc %s generated %s from %s (version %s)" %
|
||||
( os.path.dirname(file_name['xml']),
|
||||
xml2rfc.__version__,
|
||||
os.path.basename(file_name['html']),
|
||||
os.path.basename(file_name['xml']),
|
||||
self.xml_version))
|
||||
except Exception as e:
|
||||
msgs = format_messages('html', e, xml2rfc.log)
|
||||
self.add_error('xml', msgs)
|
||||
|
||||
except Exception as e:
|
||||
try:
|
||||
msgs = format_messages('txt', e, xml2rfc.log)
|
||||
log.log('\n'.join(msgs))
|
||||
self.add_error('xml', msgs)
|
||||
except Exception:
|
||||
self.add_error('xml', "An exception occurred when trying to process the XML file: %s" % e)
|
||||
|
||||
# The following errors are likely noise if we have previous field
|
||||
# errors:
|
||||
if self.errors:
|
||||
raise forms.ValidationError('')
|
||||
|
||||
if self.cleaned_data.get('txt'):
|
||||
# try to parse it
|
||||
txt_file = self.cleaned_data['txt']
|
||||
txt_file.seek(0)
|
||||
bytes = txt_file.read()
|
||||
txt_file.seek(0)
|
||||
try:
|
||||
text = bytes.decode(PlainParser.encoding)
|
||||
self.parsed_draft = PlaintextDraft(text, txt_file.name)
|
||||
if self.filename == None:
|
||||
self.filename = self.parsed_draft.filename
|
||||
elif self.filename != self.parsed_draft.filename:
|
||||
self.add_error('txt', "Inconsistent name information: xml:%s, txt:%s" % (self.filename, self.parsed_draft.filename))
|
||||
if self.revision == None:
|
||||
self.revision = self.parsed_draft.revision
|
||||
elif self.revision != self.parsed_draft.revision:
|
||||
self.add_error('txt', "Inconsistent revision information: xml:%s, txt:%s" % (self.revision, self.parsed_draft.revision))
|
||||
if self.title == None:
|
||||
self.title = self.parsed_draft.get_title()
|
||||
elif self.title != self.parsed_draft.get_title():
|
||||
self.add_error('txt', "Inconsistent title information: xml:%s, txt:%s" % (self.title, self.parsed_draft.get_title()))
|
||||
except (UnicodeDecodeError, LookupError) as e:
|
||||
self.add_error('txt', 'Failed decoding the uploaded file: "%s"' % str(e))
|
||||
|
||||
rev_error = validate_submission_rev(self.filename, self.revision)
|
||||
if rev_error:
|
||||
raise forms.ValidationError(rev_error)
|
||||
|
||||
# The following errors are likely noise if we have previous field
|
||||
# errors:
|
||||
if self.errors:
|
||||
raise forms.ValidationError('')
|
||||
|
||||
if not self.filename:
|
||||
raise forms.ValidationError("Could not extract a valid Internet-Draft name from the upload. "
|
||||
"To fix this in a text upload, please make sure that the full Internet-Draft name including "
|
||||
"revision number appears centered on its own line below the document title on the "
|
||||
"first page. In an xml upload, please make sure that the top-level <rfc/> "
|
||||
"element has a docName attribute which provides the full Internet-Draft name including "
|
||||
"revision number.")
|
||||
|
||||
if not self.revision:
|
||||
raise forms.ValidationError("Could not extract a valid Internet-Draft revision from the upload. "
|
||||
"To fix this in a text upload, please make sure that the full Internet-Draft name including "
|
||||
"revision number appears centered on its own line below the document title on the "
|
||||
"first page. In an xml upload, please make sure that the top-level <rfc/> "
|
||||
"element has a docName attribute which provides the full Internet-Draft name including "
|
||||
"revision number.")
|
||||
|
||||
if not self.title:
|
||||
raise forms.ValidationError("Could not extract a valid title from the upload")
|
||||
|
||||
if self.cleaned_data.get('txt') or self.cleaned_data.get('xml'):
|
||||
# check group
|
||||
self.group = self.deduce_group(self.filename)
|
||||
|
||||
# check existing
|
||||
existing = Submission.objects.filter(name=self.filename, rev=self.revision).exclude(state__in=("posted", "cancel", "waiting-for-draft"))
|
||||
if existing:
|
||||
raise forms.ValidationError(mark_safe('A submission with same name and revision is currently being processed. <a href="%s">Check the status here.</a>' % urlreverse("ietf.submit.views.submission_status", kwargs={ 'submission_id': existing[0].pk })))
|
||||
|
||||
# cut-off
|
||||
if self.revision == '00' and self.in_first_cut_off:
|
||||
raise forms.ValidationError(mark_safe(self.cutoff_warning))
|
||||
|
||||
# check thresholds
|
||||
today = date_today()
|
||||
|
||||
self.check_submissions_thresholds(
|
||||
"for the Internet-Draft %s" % self.filename,
|
||||
dict(name=self.filename, rev=self.revision, submission_date=today),
|
||||
settings.IDSUBMIT_MAX_DAILY_SAME_DRAFT_NAME, settings.IDSUBMIT_MAX_DAILY_SAME_DRAFT_NAME_SIZE,
|
||||
)
|
||||
self.check_submissions_thresholds(
|
||||
"for the same submitter",
|
||||
dict(remote_ip=self.remote_ip, submission_date=today),
|
||||
settings.IDSUBMIT_MAX_DAILY_SAME_SUBMITTER, settings.IDSUBMIT_MAX_DAILY_SAME_SUBMITTER_SIZE,
|
||||
)
|
||||
if self.group:
|
||||
self.check_submissions_thresholds(
|
||||
"for the group \"%s\"" % (self.group.acronym),
|
||||
dict(group=self.group, submission_date=today),
|
||||
settings.IDSUBMIT_MAX_DAILY_SAME_GROUP, settings.IDSUBMIT_MAX_DAILY_SAME_GROUP_SIZE,
|
||||
)
|
||||
self.check_submissions_thresholds(
|
||||
"across all submitters",
|
||||
dict(submission_date=today),
|
||||
settings.IDSUBMIT_MAX_DAILY_SUBMISSIONS, settings.IDSUBMIT_MAX_DAILY_SUBMISSIONS_SIZE,
|
||||
)
|
||||
|
||||
return super().clean()
|
||||
|
||||
|
||||
class DeprecatedSubmissionAutoUploadForm(DeprecatedSubmissionBaseUploadForm):
|
||||
"""Full-service upload form, replaced by the asynchronous version"""
|
||||
user = forms.EmailField(required=True)
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(DeprecatedSubmissionAutoUploadForm, self).__init__(request, *args, **kwargs)
|
||||
self.formats = ['xml', ]
|
||||
self.base_formats = ['xml', ]
|
||||
|
||||
|
||||
class SubmissionManualUploadForm(SubmissionBaseUploadForm):
|
||||
txt = forms.FileField(label='.txt format', required=False)
|
||||
formats = SubmissionBaseUploadForm.formats + ('txt',)
|
||||
|
@ -676,6 +404,7 @@ class SubmissionManualUploadForm(SubmissionBaseUploadForm):
|
|||
)
|
||||
return txt_file
|
||||
|
||||
|
||||
class SubmissionAutoUploadForm(SubmissionBaseUploadForm):
|
||||
user = forms.EmailField(required=True)
|
||||
replaces = forms.CharField(required=False, max_length=1000, strip=True)
|
||||
|
|
|
@ -44,7 +44,7 @@ from ietf.meeting.models import Meeting
|
|||
from ietf.meeting.factories import MeetingFactory
|
||||
from ietf.name.models import DraftSubmissionStateName, FormalLanguageName
|
||||
from ietf.person.models import Person
|
||||
from ietf.person.factories import UserFactory, PersonFactory, EmailFactory
|
||||
from ietf.person.factories import UserFactory, PersonFactory
|
||||
from ietf.submit.factories import SubmissionFactory, SubmissionExtResourceFactory
|
||||
from ietf.submit.forms import SubmissionBaseUploadForm, SubmissionAutoUploadForm
|
||||
from ietf.submit.models import Submission, Preapproval, SubmissionExtResource
|
||||
|
@ -2345,6 +2345,12 @@ class ApiSubmissionTests(BaseSubmitTestCase):
|
|||
super().setUp()
|
||||
MeetingFactory(type_id='ietf', date=date_today()+datetime.timedelta(days=60))
|
||||
|
||||
def test_api_submit_tombstone(self):
|
||||
"""Tombstone for obsolete API endpoint should return 410 Gone"""
|
||||
url = urlreverse("ietf.submit.views.api_submit_tombstone")
|
||||
self.assertEqual(self.client.get(url).status_code, 410)
|
||||
self.assertEqual(self.client.post(url).status_code, 410)
|
||||
|
||||
def test_upload_draft(self):
|
||||
"""api_submission accepts a submission and queues it for processing"""
|
||||
url = urlreverse('ietf.submit.views.api_submission')
|
||||
|
@ -3191,141 +3197,6 @@ class AsyncSubmissionTests(BaseSubmitTestCase):
|
|||
self.assertEqual(subm.state_id, "cancel")
|
||||
self.assertEqual(subm.submissionevent_set.count(), 2)
|
||||
|
||||
|
||||
class ApiSubmitTests(BaseSubmitTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# break early in case of missing configuration
|
||||
self.assertTrue(os.path.exists(settings.IDSUBMIT_IDNITS_BINARY))
|
||||
MeetingFactory(type_id='ietf', date=date_today()+datetime.timedelta(days=60))
|
||||
|
||||
def do_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(f'{name}-{rev}', f'{name}-{rev}.xml', group, "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_info(self):
|
||||
url = urlreverse('ietf.submit.views.api_submit')
|
||||
r = self.client.get(url)
|
||||
expected = "A simplified Internet-Draft submission interface, intended for automation"
|
||||
self.assertContains(r, expected, status_code=200)
|
||||
|
||||
def test_api_submit_bad_method(self):
|
||||
url = urlreverse('ietf.submit.views.api_submit')
|
||||
r = self.client.put(url)
|
||||
self.assertEqual(r.status_code, 405)
|
||||
|
||||
def test_api_submit_ok(self):
|
||||
r, author, name = self.do_post_submission('00')
|
||||
expected = "Upload of %s OK, confirmation requests sent to:\n %s" % (name, author.formatted_email().replace('\n',''))
|
||||
self.assertContains(r, expected, status_code=200)
|
||||
|
||||
def test_api_submit_secondary_email_active(self):
|
||||
person = PersonFactory()
|
||||
email = EmailFactory(person=person)
|
||||
r, author, name = self.do_post_submission('00', author=person, email=email.address)
|
||||
for expected in [
|
||||
"Upload of %s OK, confirmation requests sent to:" % (name, ),
|
||||
author.formatted_email().replace('\n',''),
|
||||
]:
|
||||
self.assertContains(r, expected, status_code=200)
|
||||
|
||||
def test_api_submit_secondary_email_inactive(self):
|
||||
person = PersonFactory()
|
||||
prim = person.email()
|
||||
prim.primary = True
|
||||
prim.save()
|
||||
email = EmailFactory(person=person, active=False)
|
||||
r, author, name = self.do_post_submission('00', author=person, email=email.address)
|
||||
expected = "No such user: %s" % email.address
|
||||
self.assertContains(r, expected, status_code=400)
|
||||
|
||||
def test_api_submit_no_user(self):
|
||||
email='nonexistant.user@example.org'
|
||||
r, author, name = self.do_post_submission('00', email=email)
|
||||
expected = "No such user: %s" % email
|
||||
self.assertContains(r, expected, status_code=400)
|
||||
|
||||
def test_api_submit_no_person(self):
|
||||
user = UserFactory()
|
||||
email = user.username
|
||||
r, author, name = self.do_post_submission('00', email=email)
|
||||
expected = "No person with username %s" % email
|
||||
self.assertContains(r, expected, status_code=400)
|
||||
|
||||
def test_api_submit_wrong_revision(self):
|
||||
r, author, name = self.do_post_submission('01')
|
||||
expected = "Invalid revision (revision 00 is expected)"
|
||||
self.assertContains(r, expected, status_code=400)
|
||||
|
||||
def test_api_submit_update_existing_submissiondocevent_rev(self):
|
||||
draft, _ = create_draft_submission_with_rev_mismatch(rev='01')
|
||||
r, _, __ = self.do_post_submission(rev='01', name=draft.name)
|
||||
expected = "Submission failed"
|
||||
self.assertContains(r, expected, status_code=409)
|
||||
|
||||
def test_api_submit_update_later_submissiondocevent_rev(self):
|
||||
draft, _ = create_draft_submission_with_rev_mismatch(rev='02')
|
||||
r, _, __ = self.do_post_submission(rev='01', name=draft.name)
|
||||
expected = "Submission failed"
|
||||
self.assertContains(r, expected, status_code=409)
|
||||
|
||||
def test_api_submit_pending_submission(self):
|
||||
r, author, name = self.do_post_submission('00')
|
||||
expected = "Upload of"
|
||||
self.assertContains(r, expected, status_code=200)
|
||||
r, author, name = self.do_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.do_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):
|
||||
# `year` on the next line must be leap year or this test will fail every Feb 29
|
||||
r, author, name = self.do_post_submission('00', year="2012")
|
||||
expected = "Document date must be within 3 days of submission date"
|
||||
self.assertContains(r, expected, status_code=400)
|
||||
|
||||
def test_api_submit_keeps_extresources(self):
|
||||
"""API submit should not disturb doc external resources
|
||||
|
||||
Tests that the submission inherits the existing doc's docextresource_set.
|
||||
Relies on separate testing that Submission external_resources will be
|
||||
handled appropriately.
|
||||
"""
|
||||
draft = WgDraftFactory()
|
||||
|
||||
# add an external resource
|
||||
self.assertEqual(draft.docextresource_set.count(), 0)
|
||||
extres = draft.docextresource_set.create(
|
||||
name_id='faq',
|
||||
display_name='this is a display name',
|
||||
value='https://example.com/faq-for-test.html',
|
||||
)
|
||||
|
||||
r, _, __ = self.do_post_submission('01', name=draft.name)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
# draft = Document.objects.get(pk=draft.pk) # update the draft
|
||||
sub = Submission.objects.get(name=draft.name)
|
||||
self.assertEqual(
|
||||
[str(r) for r in sub.external_resources.all()],
|
||||
[str(extres)],
|
||||
)
|
||||
|
||||
|
||||
class RefsTests(BaseSubmitTestCase):
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
# Copyright The IETF Trust 2011-2020, All Rights Reserved
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import re
|
||||
import datetime
|
||||
|
||||
|
@ -29,17 +27,15 @@ from ietf.ietfauth.utils import has_role, role_required
|
|||
from ietf.mailtrigger.utils import gather_address_lists
|
||||
from ietf.person.models import Email
|
||||
from ietf.submit.forms import (SubmissionAutoUploadForm, AuthorForm, SubmitterForm, EditSubmissionForm,
|
||||
PreapprovalForm, ReplacesForm,
|
||||
DeprecatedSubmissionAutoUploadForm, SubmissionManualUploadForm)
|
||||
PreapprovalForm, ReplacesForm, SubmissionManualUploadForm)
|
||||
from ietf.submit.mail import send_full_url, send_manual_post_request
|
||||
from ietf.submit.models import (Submission, Preapproval, SubmissionExtResource,
|
||||
DraftSubmissionStateName )
|
||||
from ietf.submit.tasks import process_uploaded_submission_task, process_and_accept_uploaded_submission_task, poke
|
||||
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, remove_submission_files, get_draft_meta,
|
||||
get_submission, fill_in_submission, apply_checkers, save_files, clear_existing_files,
|
||||
check_submission_revision_consistency, accept_submission, accept_submission_requires_group_approval,
|
||||
post_submission, cancel_submission, rename_submission_files, remove_submission_files,
|
||||
get_submission, save_files, clear_existing_files, accept_submission, accept_submission_requires_group_approval,
|
||||
accept_submission_requires_prev_auth_approval, update_submission_external_resources)
|
||||
from ietf.stats.utils import clean_country_name
|
||||
from ietf.utils.accesstoken import generate_access_token
|
||||
|
@ -187,97 +183,14 @@ def api_submission_status(request, submission_id):
|
|||
|
||||
|
||||
@csrf_exempt
|
||||
def api_submit(request):
|
||||
"Automated submission entrypoint"
|
||||
submission = None
|
||||
def err(code, text):
|
||||
return HttpResponse(text, status=code, content_type='text/plain')
|
||||
def api_submit_tombstone(request):
|
||||
"""Tombstone for removed automated submission entrypoint"""
|
||||
return render(
|
||||
request,
|
||||
'submit/api_submit_info.html',
|
||||
status=410, # Gone
|
||||
)
|
||||
|
||||
if request.method == 'GET':
|
||||
return render(request, 'submit/api_submit_info.html')
|
||||
elif request.method == 'POST':
|
||||
exception = None
|
||||
try:
|
||||
form = DeprecatedSubmissionAutoUploadForm(request, data=request.POST, files=request.FILES)
|
||||
if form.is_valid():
|
||||
log('got valid submission form for %s' % form.filename)
|
||||
username = form.cleaned_data['user']
|
||||
user = User.objects.filter(username__iexact=username)
|
||||
if user.count() == 0:
|
||||
# See if a secondary login was being used
|
||||
email = Email.objects.filter(address=username, active=True)
|
||||
# The error messages don't talk about 'email', as the field we're
|
||||
# looking at is still the 'username' field.
|
||||
if email.count() == 0:
|
||||
return err(400, "No such user: %s" % username)
|
||||
elif email.count() > 1:
|
||||
return err(500, "Multiple matching accounts for %s" % username)
|
||||
email = email.first()
|
||||
if not hasattr(email, 'person'):
|
||||
return err(400, "No person matches %s" % username)
|
||||
person = email.person
|
||||
if not hasattr(person, 'user'):
|
||||
return err(400, "No user matches: %s" % username)
|
||||
user = person.user
|
||||
elif user.count() > 1:
|
||||
return err(500, "Multiple matching accounts for %s" % username)
|
||||
else:
|
||||
user = user.first()
|
||||
if not hasattr(user, 'person'):
|
||||
return err(400, "No person with username %s" % username)
|
||||
|
||||
saved_files = save_files(form)
|
||||
authors, abstract, file_name, file_size = get_draft_meta(form, saved_files)
|
||||
for a in authors:
|
||||
if not a['email']:
|
||||
raise ValidationError("Missing email address for author %s" % a)
|
||||
|
||||
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 via api_submit")
|
||||
|
||||
errors = validate_submission(submission)
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
# must do this after validate_submission() or data needed for check may be invalid
|
||||
if check_submission_revision_consistency(submission):
|
||||
return err( 409, "Submission failed due to a document revision inconsistency error "
|
||||
"in the database. Please contact the secretariat for assistance.")
|
||||
|
||||
errors = [ c.message for c in submission.checks.all() if c.passed==False ]
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
if not username.lower() in [ a['email'].lower() for a in authors ]:
|
||||
raise ValidationError('Submitter %s is not one of the document authors' % user.username)
|
||||
|
||||
submission.submitter = user.person.formatted_email()
|
||||
sent_to = accept_submission(submission, request)
|
||||
|
||||
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:
|
||||
exception = e
|
||||
return err(500, "IO Error: %s" % str(e))
|
||||
except ValidationError as e:
|
||||
exception = e
|
||||
return err(400, "Validation Error: %s" % str(e))
|
||||
except Exception as e:
|
||||
exception = e
|
||||
raise
|
||||
return err(500, "Exception: %s" % str(e))
|
||||
finally:
|
||||
if exception and submission:
|
||||
remove_submission_files(submission)
|
||||
submission.delete()
|
||||
else:
|
||||
return err(405, "Method not allowed")
|
||||
|
||||
def tool_instructions(request):
|
||||
return render(request, 'submit/tool_instructions.html', {'selected': 'instructions'})
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<p>
|
||||
This section describes the autogenerated read-only API towards the database tables. See also
|
||||
the
|
||||
<a href="{% url 'ietf.submit.views.api_submit' %}">Internet-Draft submission API description</a>
|
||||
<a href="{% url 'ietf.submit.views.api_submission' %}">Internet-Draft submission API description</a>
|
||||
and the
|
||||
<a href="#iesg-position-api">IESG ballot position API description</a>
|
||||
</p>
|
||||
|
|
|
@ -1,56 +1,13 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015-2022, All Rights Reserved #}
|
||||
{# Copyright The IETF Trust 2015-2024, All Rights Reserved #}
|
||||
{% load origin ietf_filters %}
|
||||
{% block title %}I-D submission API instructions{% endblock %}
|
||||
{% block title %}Obsolete I-D submission API notice{% endblock %}
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1 class="mb-3">Internet-Draft submission API instructions</h1>
|
||||
<h1 class="mb-3">Obsolete Internet-Draft submission API notice</h1>
|
||||
<p>
|
||||
Note: API endpoint described here is known to have a slow response time or to fail
|
||||
due to timeout for some Internet-Draft submissions, particularly those with large file sizes.
|
||||
It is recommended to use the <a href="{% url 'ietf.submit.views.api_submission' %}">new API endpoint</a>
|
||||
instead for increased reliability.
|
||||
The API endpoint previously available here is obsolete and is no longer supported.
|
||||
Please use the <a href="{% url 'ietf.submit.views.api_submission' %}">new API endpoint</a>
|
||||
instead.
|
||||
</p>
|
||||
<p>
|
||||
A simplified Internet-Draft submission interface, intended for automation,
|
||||
is available at <code>{% absurl 'ietf.submit.views.api_submit' %}</code>.
|
||||
</p>
|
||||
<p>
|
||||
The interface accepts only XML uploads that can be processed on the server, and
|
||||
requires the user to have a datatracker account. A successful submit still requires
|
||||
the same email confirmation round-trip as submissions done through the regular
|
||||
<a href="{% url 'ietf.submit.views.upload_submission' %}">submission tool</a>.
|
||||
</p>
|
||||
<p>
|
||||
This interface does not provide all the options which the regular submission tool does.
|
||||
Some limitations:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Only XML-only uploads are supported, not text or combined.</li>
|
||||
<li>Document replacement information cannot be supplied.</li>
|
||||
<li>
|
||||
The server expects <code>multipart/form-data</code>, supported by <code>curl</code> but <b>not</b> by <code>wget</code>.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
It takes two parameters:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<code>user</code> which is the user login
|
||||
</li>
|
||||
<li>
|
||||
<code>xml</code>, which is the submitted file
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
It returns an appropriate http result code, and a brief explanatory text message.
|
||||
</p>
|
||||
<p>
|
||||
Here is an example:
|
||||
</p>
|
||||
<pre class="border p-3">
|
||||
$ curl -S -F "user=user.name@example.com" -F "xml=@~/draft-user-example.xml" {% absurl 'ietf.submit.views.api_submit' %}
|
||||
Upload of draft-user-example OK, confirmation requests sent to:
|
||||
User Name <user.name@example.com></pre>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
Loading…
Reference in a new issue