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
|
# Email alias listing
|
||||||
url(r'^person/email/$', api_views.active_email_list),
|
url(r'^person/email/$', api_views.active_email_list),
|
||||||
# Draft submission API
|
# Draft submission API
|
||||||
url(r'^submit/?$', submit_views.api_submit),
|
url(r'^submit/?$', submit_views.api_submit_tombstone),
|
||||||
# Draft upload API
|
# Draft upload API
|
||||||
url(r'^submission/?$', submit_views.api_submission),
|
url(r'^submission/?$', submit_views.api_submission),
|
||||||
# Draft submission state API
|
# Draft submission state API
|
||||||
|
|
|
@ -2,19 +2,16 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
import io
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import datetime
|
import datetime
|
||||||
import email
|
import email
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import xml2rfc
|
|
||||||
from contextlib import ExitStack
|
from contextlib import ExitStack
|
||||||
|
|
||||||
from email.utils import formataddr
|
from email.utils import formataddr
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from unidecode import unidecode
|
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
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.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.plain_parser import PlainParser
|
||||||
from ietf.submit.parsers.xml_parser import XMLParser
|
from ietf.submit.parsers.xml_parser import XMLParser
|
||||||
from ietf.utils import log
|
|
||||||
from ietf.utils.draft import PlaintextDraft
|
from ietf.utils.draft import PlaintextDraft
|
||||||
from ietf.utils.fields import ModelMultipleChoiceField
|
from ietf.utils.fields import ModelMultipleChoiceField
|
||||||
from ietf.utils.text import normalize_text
|
|
||||||
from ietf.utils.timezone import date_today
|
from ietf.utils.timezone import date_today
|
||||||
from ietf.utils.xmldraft import InvalidXMLError, XMLDraft, XMLParseError
|
from ietf.utils.xmldraft import InvalidXMLError, XMLDraft, XMLParseError
|
||||||
|
|
||||||
|
@ -371,273 +366,6 @@ class SubmissionBaseUploadForm(forms.Form):
|
||||||
return None
|
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):
|
class SubmissionManualUploadForm(SubmissionBaseUploadForm):
|
||||||
txt = forms.FileField(label='.txt format', required=False)
|
txt = forms.FileField(label='.txt format', required=False)
|
||||||
formats = SubmissionBaseUploadForm.formats + ('txt',)
|
formats = SubmissionBaseUploadForm.formats + ('txt',)
|
||||||
|
@ -676,6 +404,7 @@ class SubmissionManualUploadForm(SubmissionBaseUploadForm):
|
||||||
)
|
)
|
||||||
return txt_file
|
return txt_file
|
||||||
|
|
||||||
|
|
||||||
class SubmissionAutoUploadForm(SubmissionBaseUploadForm):
|
class SubmissionAutoUploadForm(SubmissionBaseUploadForm):
|
||||||
user = forms.EmailField(required=True)
|
user = forms.EmailField(required=True)
|
||||||
replaces = forms.CharField(required=False, max_length=1000, strip=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.meeting.factories import MeetingFactory
|
||||||
from ietf.name.models import DraftSubmissionStateName, FormalLanguageName
|
from ietf.name.models import DraftSubmissionStateName, FormalLanguageName
|
||||||
from ietf.person.models import Person
|
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.factories import SubmissionFactory, SubmissionExtResourceFactory
|
||||||
from ietf.submit.forms import SubmissionBaseUploadForm, SubmissionAutoUploadForm
|
from ietf.submit.forms import SubmissionBaseUploadForm, SubmissionAutoUploadForm
|
||||||
from ietf.submit.models import Submission, Preapproval, SubmissionExtResource
|
from ietf.submit.models import Submission, Preapproval, SubmissionExtResource
|
||||||
|
@ -2345,6 +2345,12 @@ class ApiSubmissionTests(BaseSubmitTestCase):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
MeetingFactory(type_id='ietf', date=date_today()+datetime.timedelta(days=60))
|
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):
|
def test_upload_draft(self):
|
||||||
"""api_submission accepts a submission and queues it for processing"""
|
"""api_submission accepts a submission and queues it for processing"""
|
||||||
url = urlreverse('ietf.submit.views.api_submission')
|
url = urlreverse('ietf.submit.views.api_submission')
|
||||||
|
@ -3191,141 +3197,6 @@ class AsyncSubmissionTests(BaseSubmitTestCase):
|
||||||
self.assertEqual(subm.state_id, "cancel")
|
self.assertEqual(subm.state_id, "cancel")
|
||||||
self.assertEqual(subm.submissionevent_set.count(), 2)
|
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):
|
class RefsTests(BaseSubmitTestCase):
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
# Copyright The IETF Trust 2011-2020, All Rights Reserved
|
# Copyright The IETF Trust 2011-2020, All Rights Reserved
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import datetime
|
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.mailtrigger.utils import gather_address_lists
|
||||||
from ietf.person.models import Email
|
from ietf.person.models import Email
|
||||||
from ietf.submit.forms import (SubmissionAutoUploadForm, AuthorForm, SubmitterForm, EditSubmissionForm,
|
from ietf.submit.forms import (SubmissionAutoUploadForm, AuthorForm, SubmitterForm, EditSubmissionForm,
|
||||||
PreapprovalForm, ReplacesForm,
|
PreapprovalForm, ReplacesForm, SubmissionManualUploadForm)
|
||||||
DeprecatedSubmissionAutoUploadForm, SubmissionManualUploadForm)
|
|
||||||
from ietf.submit.mail import send_full_url, send_manual_post_request
|
from ietf.submit.mail import send_full_url, send_manual_post_request
|
||||||
from ietf.submit.models import (Submission, Preapproval, SubmissionExtResource,
|
from ietf.submit.models import (Submission, Preapproval, SubmissionExtResource,
|
||||||
DraftSubmissionStateName )
|
DraftSubmissionStateName )
|
||||||
from ietf.submit.tasks import process_uploaded_submission_task, process_and_accept_uploaded_submission_task, poke
|
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,
|
from ietf.submit.utils import ( approvable_submissions_for_user, preapprovals_for_user,
|
||||||
recently_approved_by_user, validate_submission, create_submission_event, docevent_from_submission,
|
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,
|
post_submission, cancel_submission, rename_submission_files, remove_submission_files,
|
||||||
get_submission, fill_in_submission, apply_checkers, save_files, clear_existing_files,
|
get_submission, save_files, clear_existing_files, accept_submission, accept_submission_requires_group_approval,
|
||||||
check_submission_revision_consistency, accept_submission, accept_submission_requires_group_approval,
|
|
||||||
accept_submission_requires_prev_auth_approval, update_submission_external_resources)
|
accept_submission_requires_prev_auth_approval, update_submission_external_resources)
|
||||||
from ietf.stats.utils import clean_country_name
|
from ietf.stats.utils import clean_country_name
|
||||||
from ietf.utils.accesstoken import generate_access_token
|
from ietf.utils.accesstoken import generate_access_token
|
||||||
|
@ -187,97 +183,14 @@ def api_submission_status(request, submission_id):
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def api_submit(request):
|
def api_submit_tombstone(request):
|
||||||
"Automated submission entrypoint"
|
"""Tombstone for removed automated submission entrypoint"""
|
||||||
submission = None
|
return render(
|
||||||
def err(code, text):
|
request,
|
||||||
return HttpResponse(text, status=code, content_type='text/plain')
|
'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):
|
def tool_instructions(request):
|
||||||
return render(request, 'submit/tool_instructions.html', {'selected': 'instructions'})
|
return render(request, 'submit/tool_instructions.html', {'selected': 'instructions'})
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<p>
|
<p>
|
||||||
This section describes the autogenerated read-only API towards the database tables. See also
|
This section describes the autogenerated read-only API towards the database tables. See also
|
||||||
the
|
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
|
and the
|
||||||
<a href="#iesg-position-api">IESG ballot position API description</a>
|
<a href="#iesg-position-api">IESG ballot position API description</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -1,56 +1,13 @@
|
||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% load origin ietf_filters %}
|
||||||
{% block title %}I-D submission API instructions{% endblock %}
|
{% block title %}Obsolete I-D submission API notice{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% origin %}
|
{% origin %}
|
||||||
<h1 class="mb-3">Internet-Draft submission API instructions</h1>
|
<h1 class="mb-3">Obsolete Internet-Draft submission API notice</h1>
|
||||||
<p>
|
<p>
|
||||||
Note: API endpoint described here is known to have a slow response time or to fail
|
The API endpoint previously available here is obsolete and is no longer supported.
|
||||||
due to timeout for some Internet-Draft submissions, particularly those with large file sizes.
|
Please use the <a href="{% url 'ietf.submit.views.api_submission' %}">new API endpoint</a>
|
||||||
It is recommended to use the <a href="{% url 'ietf.submit.views.api_submission' %}">new API endpoint</a>
|
instead.
|
||||||
instead for increased reliability.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
{% endblock %}
|
||||||
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 %}
|
|
||||||
|
|
Loading…
Reference in a new issue