* chore: Remove unused "rendertest" stuff (#6015) * fix: restore ability to create status change documents (#5963) * fix: restore ability to create status change documents Fixes #5962 * chore: address review comment * fix: Provide human-friendly status in submission status API response (#6011) Co-authored-by: nectostr <bastinda96@gmail.com> * fix: Make name/email lookups case-insensitive (#5972) (#6007) * fix: Make name/email lookups case-insensitive (#5972) Use icontains so that looking up name or email is case insensitive Added a test Fixes: 5972 * fix: Use __iexact not __icontains * fix: Clarify no-action-needed (#5918) (#6020) When a draft is submitted for manual processing, clarify that no action is needed; the Secretariat has the next steps. Fixes: #5918 * fix: Fix menu hover issue (#6019) * fix: Fix menu hover issue Fixes #5702 * Fix leftmenu hover issue * fix: Server error from api_get_session_materials() (#6025) Fixes #5877 * fix: Clarify Questionnaire label (#4688) (#6017) When filtering nominees, `Questionnaire` implies `Accepted == yes` so fix the dropdown test tosay that. Fixes: #4688 * chore: Merge from @martinthomson's rfc-txt-html (#6023) * fix:no history entry when changing RFC Editor note for doc (#6021) * fix:no history entry when changing RFC Editor note for doc * fix:no history entry when changing RFC Editor note for doc --------- Co-authored-by: Priyanka Narkar <priyankanarkar@dhcp-91f8.meeting.ietf.org> * fix: avoid deprecation warning on view_list() for objs without CommunityList Fixes #5942 * fix: return 404 for non-existing revisions (#6014) * fix: return 404 for non-existing revisions Links to non-existing revisions to docs should return 404 * fix: change rfc/rev and search behaviour * refactor: fix tab level * fix: return 404 for rfc revision for bibtex * fix: provide date for revisions in bibtex output (#6029) * fix: provide date for revisions in bibtex output * refactor: change walrus to if's * fix: specify particular revision for events * fix: review refactoring issue fixes #5447 * fix: Remove automatically suggested document for document that is already has review request (fixes #3211) (#5425) * Added check that if there is already review request for the document in question, ignore the automatic suggestion for that document. Fixes #3211. * fix: dont block on open requests for a previous version. Add tests --------- Co-authored-by: Nicolas Giard <github@ngpixel.com> Co-authored-by: Robert Sparks <rjsparks@nostrum.com> * feat: IAB statements (#5940) * feat: support iab and iesg statements. Import iab statements. (#5895) * feat: infrastructure for statements doctype * chore: basic test framework * feat: basic statement document view * feat: show replaced statements * chore: black * fix: state help for statements * fix: cleanout non-relevant email expansions * feat: import iab statements, provide group statements tab * fix: guard against running import twice * feat: build redirect csv for iab statements * fix: set document state on import * feat: show published date on main doc view * feat: handle pdf statements * feat: create new and update statements * chore: copyright block updates * chore: remove flakes * chore: black * feat: add edit/new buttons for the secretariat * fix: address PR #5895 review comments * fix: pin pydantic until inflect catches up (#5901) (#5902) * chore: re-un-pin pydantic * feat: include submitter in email about submitted slides (#6033) * feat: include submitter in email about submitted slides fixes #6031 * chore: remove unintended whitespace change * chore(dev): update .vscode/settings.json with new taskExplorer settings * fix: Add editorial stream to proceedings (#6027) * fix: Add editorial stream to proceedings Fixes #5717 * fix: Move editorial stream after the irtf in proceedings * fix: Add editorial stream to meeting materials (#6047) Fixes #6042 * fix: Shows requested reviews for doc fixes (#6022) * Fix: Shows requested reviews for doc * Changed template includes to only give required variables to them. * feat: allow openId to choose an unactive email if there are none active (#6041) * feat: allow openId to choose an unactive email if there are no active ones * chore: correct typo * chore: rename unactive to inactive * fix: Make review table more responsive (#6053) * fix: Improve layout of review table * Progress * Progress * Final changes * Fix tests * Remove fluff * Undo commits * ci: add --validate-html-harder to tests * ci: add --validate-html-harder to build.yml workflow * fix: Set colspan to actual number of columns (#6069) * fix: Clean up view_feedback_pending (#6070) - Remove "Unclassified" column header, which caused misalignment in the table body. - Show the message author - previously displayed as `(None)`. * docs: Update LICENSE year * fix: Remove IESG state edit button when state is 'dead' (#6051) (#6065) * fix: Correctly order "last call requested" column in the IESG dashboard (#6079) * ci: update dev sandbox init script to start memcached * feat: Reclassify nomcom feedback (#6002) * fix: Clean up view_feedback_pending - Remove "Unclassified" column header, which caused misalignment in the table body. - Show the message author - previously displayed as `(None)`. * feat: Reclassify nomcom feedback (#4669) - There's a new `Chair/Advisor Tasks` menu item `Reclassify feedback`. - I overloaded `view_feedback*` URLs with a `?reclassify` parameter. - This adds a checkbox to each feedback message, and a `Reclassify` button at the bottom of each feedback page. - "Reclassifying" basically de-classifies the feedback, and punts it back to the "Pending emails" view for reclassification. - If a feedback has been applied to multiple nominees, declassifying it from one nominee removes it from all. * fix: Remove unused local variables * fix: Fix some missing and mis-nested html * test: Add tests for reclassifying feedback * refactor: Substantial redesign of feedback reclassification - Break out reclassify_feedback* as their own URLs and views, and revert changes to view_feedback*.html. - Replace checkboxes with a Reclassify button on each message. * fix: Remember to clear the feedback associations when reclassifying * feat: Add an 'Overcome by events' feedback type * refactor: When invoking reclassification from a view-feedback page, load the corresponding reclassify-feedback page * fix: De-conflict migration with 0004_statements Also change the coding style to match, and add a reverse migration. * fix: Fix a test case to account for new feedback type * fix: 842e730 broke the Back button * refactor: Reclassify feedback directly instead of putting it back in the work queue * fix: Adjust tests to new workflow * refactor: Further refine reclassification to avoid redirects * refactor: Impose a FeedbackTypeName ordering Also add FeedbackTypeName.legend field, rather than synthesizing it every time we classify or reclassify feedback. In the reclassification forms, only show the relevant feedback types. * refactor: Merge reclassify_feedback_* back into view_feedback_* This means the "Reclassify" button is always present, but eliminates some complexity. * refactor: Add filter(used=True) on FeedbackTypeName querysets * refactor: Add the new FeedbackTypeName to the reclassification success message * fix: Secure reclassification against rogue nomcom members * fix: Print decoded key and fully clean up test nomcom (#6094) * fix: Delete Person records when deleting a test nomcom * fix: Decode test nomcom private key before printing * test: Use correct time zone for test_statement_doc_view (#6064) * chore(deps): update all npm dependencies for playwright (#6061) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> * chore(deps): update all npm dependencies for dev/diff (#6062) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> * chore(deps): update all npm dependencies for dev/coverage-action (#6063) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> * fix: Hash cache key for default memcached cache (#6089) * feat: Show docs that an AD hasn't balloted on that need ballots to progress (#6075) * fix(doc): Unify help texts for document states (#6060) * Fix IESG State help text link (only) * Intermediate checkpoint * Correct URL filtering of state descriptions * Unify help texts for document states * Remove redundant load static from template --------- Co-authored-by: Robert Sparks <rjsparks@nostrum.com> * ci: fix sandbox start.sh memcached user * fix: refactor how settings handles cache definitions (#6099) * fix: refactor how settings handles cache definitions * chore: more english-speaker readable expression * fix: Cast cache key to str before calling encode (#6100) --------- Co-authored-by: Robert Sparks <rjsparks@nostrum.com> Co-authored-by: Liubov Kurafeeva <liubov.kurafeeva@gmail.com> Co-authored-by: nectostr <bastinda96@gmail.com> Co-authored-by: Rich Salz <rsalz@akamai.com> Co-authored-by: PriyankaN <priyanka@amsl.com> Co-authored-by: Priyanka Narkar <priyankanarkar@dhcp-91f8.meeting.ietf.org> Co-authored-by: Ali <alireza83@gmail.com> Co-authored-by: Roman Beltiukov <maybe.hello.world@gmail.com> Co-authored-by: Tero Kivinen <kivinen@iki.fi> Co-authored-by: Nicolas Giard <github@ngpixel.com> Co-authored-by: Kesara Rathnayake <kesara@fq.nz> Co-authored-by: Jennifer Richards <jennifer@staff.ietf.org> Co-authored-by: Paul Selkirk <paul@painless-security.com> Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> Co-authored-by: Jim Fenton <fenton@bluepopcorn.net>
1016 lines
44 KiB
Python
1016 lines
44 KiB
Python
# Copyright The IETF Trust 2011-2020, All Rights Reserved
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
import re
|
|
import base64
|
|
import datetime
|
|
|
|
from typing import Optional, cast # pyflakes:ignore
|
|
from urllib.parse import urljoin
|
|
|
|
from django.conf import settings
|
|
from django.contrib import messages
|
|
from django.contrib.auth.models import User
|
|
from django.db import transaction
|
|
from django.urls import reverse as urlreverse
|
|
from django.core.exceptions import ValidationError
|
|
from django.http import HttpResponseRedirect, Http404, HttpResponseForbidden, HttpResponse, JsonResponse
|
|
from django.http import HttpRequest # pyflakes:ignore
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
|
|
import debug # pyflakes:ignore
|
|
|
|
from ietf.doc.models import Document, DocAlias, AddedMessageEvent
|
|
from ietf.doc.forms import ExtResourceForm
|
|
from ietf.group.models import Group
|
|
from ietf.group.utils import group_features_group_filter
|
|
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.person.models import Email
|
|
from ietf.submit.forms import (SubmissionAutoUploadForm, AuthorForm, SubmitterForm, EditSubmissionForm,
|
|
PreapprovalForm, ReplacesForm, SubmissionEmailForm, MessageModelForm,
|
|
DeprecatedSubmissionAutoUploadForm, SubmissionManualUploadForm)
|
|
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, SubmissionExtResource,
|
|
DraftSubmissionStateName, SubmissionEmailEvent )
|
|
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,
|
|
accept_submission_requires_prev_auth_approval, update_submission_external_resources, remote_ip )
|
|
from ietf.stats.utils import clean_country_name
|
|
from ietf.utils.accesstoken import generate_access_token
|
|
from ietf.utils.log import log
|
|
from ietf.utils.mail import parseaddr, send_mail_message
|
|
from ietf.utils.response import permission_denied
|
|
from ietf.utils.timezone import date_today
|
|
|
|
|
|
def upload_submission(request):
|
|
if request.method == "POST":
|
|
form = SubmissionManualUploadForm(
|
|
request, data=request.POST, files=request.FILES
|
|
)
|
|
if form.is_valid():
|
|
submission = get_submission(form)
|
|
submission.state = DraftSubmissionStateName.objects.get(slug="validating")
|
|
submission.remote_ip = form.remote_ip
|
|
submission.file_types = ",".join(form.file_types)
|
|
submission.submission_date = date_today()
|
|
submission.save()
|
|
clear_existing_files(form)
|
|
save_files(form)
|
|
create_submission_event(request, submission, desc="Uploaded submission")
|
|
# Wrap in on_commit so the delayed task cannot start until the view is done with the DB
|
|
transaction.on_commit(
|
|
lambda: process_uploaded_submission_task.delay(submission.pk)
|
|
)
|
|
return redirect(
|
|
"ietf.submit.views.submission_status",
|
|
submission_id=submission.pk,
|
|
access_token=submission.access_token(),
|
|
)
|
|
else:
|
|
form = SubmissionManualUploadForm(request=request)
|
|
|
|
return render(
|
|
request, "submit/upload_submission.html", {"selected": "index", "form": form}
|
|
)
|
|
|
|
@csrf_exempt
|
|
def api_submission(request):
|
|
def err(code, error, messages=None):
|
|
data = {'error': error}
|
|
if messages is not None:
|
|
data['messages'] = [messages] if isinstance(messages, str) else messages
|
|
return JsonResponse(data, status=code)
|
|
|
|
if request.method == 'GET':
|
|
return render(request, 'submit/api_submission_info.html')
|
|
elif request.method == 'POST':
|
|
exception = None
|
|
submission = None
|
|
try:
|
|
form = SubmissionAutoUploadForm(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)
|
|
|
|
# There is a race condition here: creating the Submission with the name/rev
|
|
# of this draft is meant to prevent another submission from occurring. However,
|
|
# if two submissions occur at the same time, both may decide that they are the
|
|
# only submission in progress. This may result in a Submission being posted with
|
|
# the wrong files. The window for this is short, though, so it's probably
|
|
# tolerable risk.
|
|
submission = get_submission(form)
|
|
submission.state = DraftSubmissionStateName.objects.get(slug="validating")
|
|
submission.remote_ip = form.remote_ip
|
|
submission.file_types = ','.join(form.file_types)
|
|
submission.submission_date = date_today()
|
|
submission.submitter = user.person.formatted_email()
|
|
submission.replaces = form.cleaned_data['replaces']
|
|
submission.save()
|
|
clear_existing_files(form)
|
|
save_files(form)
|
|
create_submission_event(request, submission, desc="Uploaded submission through API")
|
|
|
|
# Wrap in on_commit so the delayed task cannot start until the view is done with the DB
|
|
transaction.on_commit(
|
|
lambda: process_and_accept_uploaded_submission_task.delay(submission.pk)
|
|
)
|
|
return JsonResponse(
|
|
{
|
|
'id': str(submission.pk),
|
|
'name': submission.name,
|
|
'rev': submission.rev,
|
|
'status_url': urljoin(
|
|
settings.IDTRACKER_BASE_URL,
|
|
urlreverse(api_submission_status, kwargs={'submission_id': submission.pk}),
|
|
),
|
|
}
|
|
)
|
|
else:
|
|
raise ValidationError(form.errors)
|
|
except IOError as e:
|
|
exception = e
|
|
return err(500, 'IO Error', str(e))
|
|
except ValidationError as e:
|
|
exception = e
|
|
return err(400, 'Validation Error', e.messages)
|
|
except Exception as e:
|
|
exception = e
|
|
raise
|
|
finally:
|
|
if exception and submission:
|
|
remove_submission_files(submission)
|
|
submission.delete()
|
|
else:
|
|
return err(405, "Method not allowed")
|
|
|
|
|
|
@csrf_exempt
|
|
def api_submission_status(request, submission_id):
|
|
submission = get_submission_or_404(submission_id)
|
|
return JsonResponse(
|
|
{
|
|
'id': str(submission.pk),
|
|
'state': submission.state.slug,
|
|
'state_desc': submission.state.name,
|
|
}
|
|
)
|
|
|
|
|
|
@csrf_exempt
|
|
def api_submit(request):
|
|
"Automated submission entrypoint"
|
|
submission = None
|
|
def err(code, text):
|
|
return HttpResponse(text, status=code, content_type='text/plain')
|
|
|
|
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'})
|
|
|
|
def search_submission(request):
|
|
error = None
|
|
name = None
|
|
if request.method == 'POST':
|
|
name = request.POST.get('name', '')
|
|
submission = Submission.objects.filter(name=name).order_by('-pk').first()
|
|
if submission:
|
|
return redirect(submission_status, submission_id=submission.pk)
|
|
else:
|
|
if re.search(r'-\d\d$', name):
|
|
submission = Submission.objects.filter(name=name[:-3]).order_by('-pk').first()
|
|
if submission:
|
|
return redirect(submission_status, submission_id=submission.pk)
|
|
error = 'No valid submission found for %s' % name
|
|
return render(request, 'submit/search_submission.html',
|
|
{'selected': 'status',
|
|
'error': error,
|
|
'name': name})
|
|
|
|
def can_edit_submission(user, submission, access_token):
|
|
key_matched = access_token and submission.access_token() == access_token
|
|
if not key_matched: key_matched = submission.access_key == access_token # backwards-compat
|
|
return key_matched or has_role(user, "Secretariat")
|
|
|
|
def submission_status(request, submission_id, access_token=None):
|
|
# type: (HttpRequest, str, Optional[str]) -> HttpResponse
|
|
submission = get_object_or_404(Submission, pk=submission_id)
|
|
|
|
key_matched = access_token and submission.access_token() == access_token
|
|
if not key_matched: key_matched = submission.access_key == access_token # backwards-compat
|
|
if access_token and not key_matched:
|
|
raise Http404
|
|
|
|
errors = validate_submission(submission)
|
|
passes_checks = all([ c.passed!=False for c in submission.checks.all() ])
|
|
|
|
is_secretariat = has_role(request.user, "Secretariat")
|
|
is_chair = submission.group and submission.group.has_role(request.user, "chair")
|
|
area = submission.area
|
|
is_ad = area and area.has_role(request.user, "ad")
|
|
|
|
can_edit = can_edit_submission(request.user, submission, access_token) and submission.state_id == "uploaded"
|
|
# disallow cancellation of 'validating' submissions except by secretariat until async process is safely abortable
|
|
can_cancel = (
|
|
(is_secretariat or (key_matched and submission.state_id != 'validating'))
|
|
and submission.state.next_states.filter(slug="cancel")
|
|
)
|
|
can_group_approve = (is_secretariat or is_ad or is_chair) and submission.state_id == "grp-appr"
|
|
can_ad_approve = (is_secretariat or is_ad) and submission.state_id == "ad-appr"
|
|
|
|
can_force_post = (
|
|
is_secretariat
|
|
and submission.state.next_states.filter(slug="posted").exists()
|
|
and submission.state_id != "waiting-for-draft")
|
|
show_send_full_url = (
|
|
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)
|
|
addresses = addrs.to
|
|
addresses.extend(addrs.cc)
|
|
# Convert from RFC 2822 format if needed
|
|
confirmation_list = [ "%s <%s>" % parseaddr(a) for a in addresses ]
|
|
|
|
message = None
|
|
|
|
if submission.state_id == "cancel":
|
|
message = ('error', 'This submission has been cancelled, modification is no longer possible.')
|
|
elif submission.state_id == "auth":
|
|
message = ('success', 'The submission is pending email authentication. An email has been sent to: %s' % ", ".join(confirmation_list))
|
|
elif submission.state_id == "grp-appr":
|
|
message = ('success', 'The submission is pending approval by the group chairs.')
|
|
elif submission.state_id == "ad-appr":
|
|
message = ('success', 'The submission is pending approval by the area director.')
|
|
elif submission.state_id == "aut-appr":
|
|
message = ('success', 'The submission is pending approval by the authors of the previous version. An email has been sent to: %s' % ", ".join(confirmation_list))
|
|
|
|
existing_doc = submission.existing_document()
|
|
|
|
# Sort out external resources
|
|
external_resources = [
|
|
dict(res=r, added=False)
|
|
for r in submission.external_resources.order_by('name__slug', 'value', 'display_name')
|
|
]
|
|
|
|
# Show comparison of resources with current doc resources. If not posted or canceled,
|
|
# determine which resources were added / removed. In the output, submission resources
|
|
# will be marked as "new" if they were not present on the existing document. Document
|
|
# resources will be marked as "removed" if they are not present in the submission.
|
|
#
|
|
# To classify the resources, start by assuming that every submission resource already
|
|
# existed (the "added=False" above) and that every existing document resource was
|
|
# removed (the "removed=True" below). Then check every submission resource for a
|
|
# matching resource on the existing document that is still marked as "removed". If one
|
|
# exists, change the existing resource to "not removed" and leave the submission resource
|
|
# as "not added." If there is no matching removed resource, then mark the submission
|
|
# resource as "added."
|
|
#
|
|
show_resource_changes = submission.state_id not in ['posted', 'cancel']
|
|
doc_external_resources = [dict(res=r, removed=True)
|
|
for r in existing_doc.docextresource_set.all()] if existing_doc else []
|
|
if show_resource_changes:
|
|
for item in external_resources:
|
|
er = cast(SubmissionExtResource, item['res']) # cast to help type checker with the dict typing
|
|
# get first matching resource still marked as 'removed' from previous rev resources
|
|
existing_item = next(
|
|
filter(
|
|
lambda r: (r['removed']
|
|
and er.name == r['res'].name
|
|
and er.value == r['res'].value
|
|
and er.display_name == r['res'].display_name),
|
|
doc_external_resources
|
|
),
|
|
None
|
|
) # type: ignore
|
|
if existing_item is None:
|
|
item['added'] = True
|
|
else:
|
|
existing_item['removed'] = False
|
|
doc_external_resources.sort(
|
|
key=lambda d: (d['res'].name.slug, d['res'].value, d['res'].display_name)
|
|
)
|
|
|
|
submitter_form = SubmitterForm(initial=submission.submitter_parsed(), prefix="submitter")
|
|
replaces_form = ReplacesForm(name=submission.name,initial=DocAlias.objects.filter(name__in=submission.replaces.split(",")))
|
|
extresources_form = ExtResourceForm(
|
|
initial=dict(resources=[er['res'] for er in external_resources]),
|
|
extresource_model=SubmissionExtResource,
|
|
)
|
|
|
|
if request.method == 'POST':
|
|
action = request.POST.get('action')
|
|
if action == "autopost" and submission.state_id == "uploaded":
|
|
if not can_edit:
|
|
permission_denied(request, "You do not have permission to perform this action")
|
|
|
|
submitter_form = SubmitterForm(request.POST, prefix="submitter")
|
|
replaces_form = ReplacesForm(request.POST, name=submission.name)
|
|
extresources_form = ExtResourceForm(
|
|
request.POST, extresource_model=SubmissionExtResource
|
|
)
|
|
validations = [
|
|
submitter_form.is_valid(),
|
|
replaces_form.is_valid(),
|
|
extresources_form.is_valid(),
|
|
]
|
|
|
|
if all(validations):
|
|
submission.submitter = submitter_form.cleaned_line()
|
|
replaces = replaces_form.cleaned_data.get("replaces", [])
|
|
submission.replaces = ",".join(o.name for o in replaces)
|
|
|
|
extresources = extresources_form.cleaned_data.get('resources', [])
|
|
update_submission_external_resources(submission, extresources)
|
|
|
|
approvals_received = submitter_form.cleaned_data['approvals_received']
|
|
|
|
if submission.rev == '00' and submission.group and not submission.group.is_active:
|
|
permission_denied(request, 'Posting a new Internet-Draft for an inactive group is not permitted.')
|
|
|
|
if approvals_received:
|
|
if not is_secretariat:
|
|
permission_denied(request, 'You do not have permission to perform this action')
|
|
|
|
# go directly to posting submission
|
|
docevent_from_submission(submission, desc="Uploaded new revision")
|
|
|
|
desc = "Secretariat manually posting. Approvals already received"
|
|
post_submission(request, submission, desc, desc)
|
|
|
|
else:
|
|
accept_submission(submission, request, autopost=True)
|
|
|
|
if access_token:
|
|
return redirect("ietf.submit.views.submission_status", submission_id=submission.pk, access_token=access_token)
|
|
else:
|
|
return redirect("ietf.submit.views.submission_status", submission_id=submission.pk)
|
|
|
|
elif action == "edit" and submission.state_id == "uploaded":
|
|
if access_token:
|
|
return redirect("ietf.submit.views.edit_submission", submission_id=submission.pk, access_token=access_token)
|
|
else:
|
|
return redirect("ietf.submit.views.edit_submission", submission_id=submission.pk)
|
|
|
|
elif action == "sendfullurl" and submission.state_id not in ("cancel", "posted"):
|
|
sent_to = send_full_url(request, submission)
|
|
|
|
message = ('success', 'An email has been sent with the full access URL to: %s' % ",".join(confirmation_list))
|
|
|
|
create_submission_event(request, submission, "Sent full access URL to: %s" % ", ".join(sent_to))
|
|
|
|
elif action == "cancel" and submission.state.next_states.filter(slug="cancel"):
|
|
if not can_cancel:
|
|
permission_denied(request, 'You do not have permission to perform this action.')
|
|
|
|
cancel_submission(submission)
|
|
|
|
create_submission_event(request, submission, "Cancelled submission")
|
|
|
|
return redirect("ietf.submit.views.submission_status", submission_id=submission_id)
|
|
|
|
elif action == "approve" and submission.state_id == "ad-appr":
|
|
if not can_ad_approve:
|
|
permission_denied(request, 'You do not have permission to perform this action.')
|
|
|
|
post_submission(request, submission, "WG -00 approved", "Approved and posted submission")
|
|
|
|
return redirect("ietf.doc.views_doc.document_main", name=submission.name)
|
|
|
|
elif action == "approve" and submission.state_id == "grp-appr":
|
|
if not can_group_approve:
|
|
permission_denied(request, 'You do not have permission to perform this action.')
|
|
|
|
post_submission(request, submission, "WG -00 approved", "Approved and posted submission")
|
|
|
|
return redirect("ietf.doc.views_doc.document_main", name=submission.name)
|
|
|
|
elif action == "forcepost" and submission.state.next_states.filter(slug="posted"):
|
|
if not can_force_post:
|
|
permission_denied(request, 'You do not have permission to perform this action.')
|
|
|
|
if submission.state_id == "manual":
|
|
desc = "Posted submission manually"
|
|
else:
|
|
desc = "Forced post of submission"
|
|
|
|
post_submission(request, submission, desc, desc)
|
|
|
|
return redirect("ietf.doc.views_doc.document_main", name=submission.name)
|
|
|
|
|
|
else:
|
|
# something went wrong, turn this into a GET and let the user deal with it
|
|
return HttpResponseRedirect("")
|
|
|
|
for author in submission.authors:
|
|
author["cleaned_country"] = clean_country_name(author.get("country"))
|
|
|
|
all_forms = [submitter_form, replaces_form]
|
|
|
|
return render(request, 'submit/submission_status.html', {
|
|
'selected': 'status',
|
|
'submission': submission,
|
|
'errors': errors,
|
|
'passes_checks': passes_checks,
|
|
'submitter_form': submitter_form,
|
|
'replaces_form': replaces_form,
|
|
'extresources_form': extresources_form,
|
|
'external_resources': {
|
|
'current': external_resources, # dict with 'res' and 'added' as keys
|
|
'previous': doc_external_resources, # dict with 'res' and 'removed' as keys
|
|
'show_changes': show_resource_changes,
|
|
},
|
|
'message': message,
|
|
'can_edit': can_edit,
|
|
'can_force_post': can_force_post,
|
|
'can_group_approve': can_group_approve,
|
|
'can_cancel': can_cancel,
|
|
'show_send_full_url': show_send_full_url,
|
|
'requires_group_approval': accept_submission_requires_group_approval(submission),
|
|
'requires_prev_authors_approval': accept_submission_requires_prev_auth_approval(submission),
|
|
'confirmation_list': confirmation_list,
|
|
'all_forms': all_forms,
|
|
})
|
|
|
|
|
|
def edit_submission(request, submission_id, access_token=None):
|
|
submission = get_object_or_404(Submission, pk=submission_id, state="uploaded")
|
|
|
|
if not can_edit_submission(request.user, submission, access_token):
|
|
permission_denied(request, 'You do not have permission to access this page.')
|
|
|
|
errors = validate_submission(submission)
|
|
form_errors = False
|
|
|
|
# we split the form handling into multiple forms, one for the
|
|
# submission itself, one for the submitter, and a list of forms
|
|
# for the authors
|
|
|
|
empty_author_form = AuthorForm()
|
|
|
|
if request.method == 'POST':
|
|
# get a backup submission now, the model form may change some
|
|
# fields during validation
|
|
prev_submission = Submission.objects.get(pk=submission.pk)
|
|
|
|
edit_form = EditSubmissionForm(request.POST, instance=submission, prefix="edit")
|
|
submitter_form = SubmitterForm(request.POST, prefix="submitter")
|
|
replaces_form = ReplacesForm(request.POST,name=submission.name)
|
|
author_forms = [ AuthorForm(request.POST, prefix=prefix)
|
|
for prefix in request.POST.getlist("authors-prefix")
|
|
if prefix != "authors-" ]
|
|
|
|
# trigger validation of all forms
|
|
validations = [edit_form.is_valid(), submitter_form.is_valid(), replaces_form.is_valid()] + [ f.is_valid() for f in author_forms ]
|
|
if all(validations):
|
|
changed_fields = []
|
|
|
|
submission.submitter = submitter_form.cleaned_line()
|
|
replaces = replaces_form.cleaned_data.get("replaces", [])
|
|
submission.replaces = ",".join(o.name for o in replaces)
|
|
submission.authors = [ { attr: f.cleaned_data.get(attr) or ""
|
|
for attr in ["name", "email", "affiliation", "country"] }
|
|
for f in author_forms ]
|
|
edit_form.save(commit=False) # transfer changes
|
|
|
|
if submission.rev != prev_submission.rev:
|
|
rename_submission_files(submission, prev_submission.rev, submission.rev)
|
|
|
|
submission.state = DraftSubmissionStateName.objects.get(slug="manual")
|
|
submission.save()
|
|
|
|
formal_languages_changed = False
|
|
if set(submission.formal_languages.all()) != set(edit_form.cleaned_data["formal_languages"]):
|
|
submission.formal_languages.clear()
|
|
submission.formal_languages.set(edit_form.cleaned_data["formal_languages"])
|
|
formal_languages_changed = True
|
|
|
|
send_manual_post_request(request, submission, errors)
|
|
|
|
changed_fields += [
|
|
submission._meta.get_field(f).verbose_name
|
|
for f in list(edit_form.fields.keys()) + ["submitter", "authors"]
|
|
if (f == "formal_languages" and formal_languages_changed)
|
|
or getattr(submission, f) != getattr(prev_submission, f)
|
|
]
|
|
|
|
if changed_fields:
|
|
desc = "Edited %s and sent request for manual post" % ", ".join(changed_fields)
|
|
else:
|
|
desc = "Sent request for manual post"
|
|
|
|
create_submission_event(request, submission, desc)
|
|
|
|
return redirect("ietf.submit.views.submission_status", submission_id=submission.pk)
|
|
else:
|
|
form_errors = True
|
|
else:
|
|
edit_form = EditSubmissionForm(instance=submission, prefix="edit")
|
|
submitter_form = SubmitterForm(initial=submission.submitter_parsed(), prefix="submitter")
|
|
replaces_form = ReplacesForm(name=submission.name,initial=DocAlias.objects.filter(name__in=submission.replaces.split(",")))
|
|
author_forms = [ AuthorForm(initial=author, prefix="authors-%s" % i)
|
|
for i, author in enumerate(submission.authors) ]
|
|
|
|
all_forms = [edit_form, submitter_form, replaces_form, *author_forms, empty_author_form]
|
|
|
|
return render(request, 'submit/edit_submission.html',
|
|
{'selected': 'status',
|
|
'submission': submission,
|
|
'edit_form': edit_form,
|
|
'submitter_form': submitter_form,
|
|
'replaces_form': replaces_form,
|
|
'author_forms': author_forms,
|
|
'empty_author_form': empty_author_form,
|
|
'errors': errors,
|
|
'form_errors': form_errors,
|
|
'all_forms': all_forms,
|
|
})
|
|
|
|
|
|
def confirm_submission(request, submission_id, auth_token):
|
|
submission = get_object_or_404(Submission, pk=submission_id)
|
|
|
|
key_matched = submission.auth_key and auth_token == generate_access_token(submission.auth_key)
|
|
if not key_matched: key_matched = auth_token == submission.auth_key # backwards-compat
|
|
|
|
if request.method == 'POST' and submission.state_id in ("auth", "aut-appr") and key_matched:
|
|
# Set a temporary state 'confirmed' to avoid entering this code
|
|
# multiple times to confirm.
|
|
submission.state = DraftSubmissionStateName.objects.get(slug="confirmed")
|
|
submission.save()
|
|
|
|
action = request.POST.get('action')
|
|
if action == 'confirm':
|
|
submitter_parsed = submission.submitter_parsed()
|
|
if submitter_parsed["name"] and submitter_parsed["email"]:
|
|
# We know who approved it
|
|
desc = "New version approved"
|
|
elif submission.state_id == "auth":
|
|
desc = "New version approved by author"
|
|
else:
|
|
desc = "New version approved by previous author"
|
|
|
|
post_submission(request, submission, desc, "Confirmed and posted submission")
|
|
|
|
return redirect("ietf.doc.views_doc.document_main", name=submission.name)
|
|
|
|
elif action == "cancel":
|
|
if submission.state.next_states.filter(slug="cancel"):
|
|
cancel_submission(submission)
|
|
create_submission_event(request, submission, "Cancelled submission")
|
|
messages.success(request, 'The submission was cancelled.')
|
|
else:
|
|
messages.error(request, 'The submission is not in a state where it can be cancelled.')
|
|
|
|
return redirect("ietf.submit.views.submission_status", submission_id=submission_id)
|
|
|
|
else:
|
|
raise RuntimeError("Unexpected state in confirm_submission()")
|
|
|
|
return render(request, 'submit/confirm_submission.html', {
|
|
'submission': submission,
|
|
'key_matched': key_matched,
|
|
})
|
|
|
|
|
|
def approvals(request):
|
|
approvals = approvable_submissions_for_user(request.user)
|
|
preapprovals = preapprovals_for_user(request.user)
|
|
|
|
days = 30
|
|
recently_approved = recently_approved_by_user(request.user, date_today() - datetime.timedelta(days=days))
|
|
|
|
return render(request, 'submit/approvals.html',
|
|
{'selected': 'approvals',
|
|
'approvals': approvals,
|
|
'preapprovals': preapprovals,
|
|
'recently_approved': recently_approved,
|
|
'days': days })
|
|
|
|
|
|
@role_required("Secretariat", "Area Director", "WG Chair", "RG Chair")
|
|
def add_preapproval(request):
|
|
groups = Group.objects.filter(type__features__req_subm_approval=True).exclude(state__in=["conclude","bof-conc"]).order_by("acronym").distinct()
|
|
|
|
if not has_role(request.user, "Secretariat"):
|
|
groups = group_features_group_filter(groups, request.user.person, 'docman_roles')
|
|
|
|
if request.method == "POST":
|
|
form = PreapprovalForm(request.POST)
|
|
form.groups = groups
|
|
if form.is_valid():
|
|
p = Preapproval()
|
|
p.name = form.cleaned_data["name"]
|
|
p.by = request.user.person
|
|
p.save()
|
|
|
|
return HttpResponseRedirect(urlreverse("ietf.submit.views.approvals") + "#preapprovals")
|
|
else:
|
|
form = PreapprovalForm()
|
|
|
|
return render(request, 'submit/add_preapproval.html',
|
|
{'selected': 'approvals',
|
|
'groups': groups,
|
|
'form': form })
|
|
|
|
@role_required("Secretariat", "WG Chair", "RG Chair")
|
|
def cancel_preapproval(request, preapproval_id):
|
|
preapproval = get_object_or_404(Preapproval, pk=preapproval_id)
|
|
|
|
if preapproval not in preapprovals_for_user(request.user):
|
|
raise HttpResponseForbidden("You do not have permission to cancel this preapproval.")
|
|
|
|
if request.method == "POST" and request.POST.get("action", "") == "cancel":
|
|
preapproval.delete()
|
|
|
|
return HttpResponseRedirect(urlreverse("ietf.submit.views.approvals") + "#preapprovals")
|
|
|
|
return render(request, 'submit/cancel_preapproval.html',
|
|
{'selected': 'approvals',
|
|
'preapproval': preapproval })
|
|
|
|
|
|
def manualpost(request):
|
|
'''
|
|
Main view for manual post requests
|
|
'''
|
|
|
|
manual = Submission.objects.filter(state_id = "manual").distinct()
|
|
|
|
for s in manual:
|
|
s.passes_checks = all([ c.passed!=False for c in s.checks.all() ])
|
|
s.errors = validate_submission(s)
|
|
|
|
waiting_for_draft = Submission.objects.filter(state_id = "waiting-for-draft").distinct()
|
|
|
|
return render(request, 'submit/manual_post.html',
|
|
{'manual': manual,
|
|
'selected': 'manual_posts',
|
|
'waiting_for_draft': waiting_for_draft})
|
|
|
|
|
|
def cancel_waiting_for_draft(request):
|
|
if request.method == 'POST':
|
|
can_cancel = has_role(request.user, "Secretariat")
|
|
|
|
if not can_cancel:
|
|
permission_denied(request, 'You do not have permission to perform this action.')
|
|
|
|
submission_id = request.POST.get('submission_id', '')
|
|
access_token = request.POST.get('access_token', '')
|
|
|
|
submission = get_submission_or_404(submission_id, access_token = access_token)
|
|
cancel_submission(submission)
|
|
|
|
create_submission_event(request, submission, "Cancelled submission")
|
|
if (submission.rev != "00"):
|
|
# Add a doc event
|
|
docevent_from_submission(submission, "Cancelled submission for rev {}".format(submission.rev))
|
|
|
|
return redirect("ietf.submit.views.manualpost")
|
|
|
|
|
|
@role_required('Secretariat',)
|
|
def add_manualpost_email(request, submission_id=None, access_token=None):
|
|
"""Add email to submission history"""
|
|
|
|
if request.method == 'POST':
|
|
try:
|
|
button_text = request.POST.get('submit', '')
|
|
if button_text == 'Cancel':
|
|
return redirect("submit/manual_post.html")
|
|
|
|
form = SubmissionEmailForm(request.POST)
|
|
if form.is_valid():
|
|
submission_pk = form.cleaned_data['submission_pk']
|
|
message = form.cleaned_data['message']
|
|
#in_reply_to = form.cleaned_data['in_reply_to']
|
|
# create Message
|
|
|
|
if form.cleaned_data['direction'] == 'incoming':
|
|
msgtype = 'msgin'
|
|
else:
|
|
msgtype = 'msgout'
|
|
|
|
submission, submission_email_event = (
|
|
add_submission_email(request=request,
|
|
remote_ip=remote_ip(request),
|
|
name = form.draft_name,
|
|
rev=form.revision,
|
|
submission_pk = submission_pk,
|
|
message = message,
|
|
by = request.user.person,
|
|
msgtype = msgtype) )
|
|
|
|
messages.success(request, 'Email added.')
|
|
|
|
try:
|
|
draft = Document.objects.get(name=submission.name)
|
|
except Document.DoesNotExist:
|
|
# Assume this is revision 00 - we'll do this later
|
|
draft = None
|
|
|
|
if (draft != None):
|
|
e = AddedMessageEvent(type="added_message", doc=draft)
|
|
e.message = submission_email_event.submissionemailevent.message
|
|
e.msgtype = submission_email_event.submissionemailevent.msgtype
|
|
e.in_reply_to = submission_email_event.submissionemailevent.in_reply_to
|
|
e.by = request.user.person
|
|
e.desc = submission_email_event.desc
|
|
e.time = submission_email_event.time
|
|
e.save()
|
|
|
|
return redirect("ietf.submit.views.manualpost")
|
|
except ValidationError as e:
|
|
form = SubmissionEmailForm(request.POST)
|
|
form._errors = {}
|
|
form._errors["__all__"] = form.error_class(["There was a failure uploading your message. (%s)" % e.message])
|
|
else:
|
|
initial = {
|
|
}
|
|
|
|
if (submission_id != None):
|
|
submission = get_submission_or_404(submission_id, access_token)
|
|
initial['name'] = "{}-{}".format(submission.name, submission.rev)
|
|
initial['direction'] = 'incoming'
|
|
initial['submission_pk'] = submission.pk
|
|
else:
|
|
initial['direction'] = 'incoming'
|
|
|
|
form = SubmissionEmailForm(initial=initial)
|
|
|
|
return render(request, 'submit/add_submit_email.html',dict(form=form))
|
|
|
|
|
|
@role_required('Secretariat',)
|
|
def send_submission_email(request, submission_id, message_id=None):
|
|
"""Send an email related to a submission"""
|
|
submission = get_submission_or_404(submission_id, access_token = None)
|
|
|
|
if request.method == 'POST':
|
|
button_text = request.POST.get('submit', '')
|
|
if button_text == 'Cancel':
|
|
return redirect('ietf.submit.views.submission_status',
|
|
submission_id=submission.id,
|
|
access_token=submission.access_token())
|
|
|
|
form = MessageModelForm(request.POST)
|
|
if form.is_valid():
|
|
# create Message
|
|
msg = Message.objects.create(
|
|
by = request.user.person,
|
|
subject = form.cleaned_data['subject'],
|
|
frm = form.cleaned_data['frm'],
|
|
to = form.cleaned_data['to'],
|
|
cc = form.cleaned_data['cc'],
|
|
bcc = form.cleaned_data['bcc'],
|
|
reply_to = form.cleaned_data['reply_to'],
|
|
body = form.cleaned_data['body']
|
|
)
|
|
|
|
in_reply_to_id = form.cleaned_data['in_reply_to_id']
|
|
in_reply_to = None
|
|
rp = ""
|
|
|
|
if in_reply_to_id:
|
|
rp = " reply"
|
|
try:
|
|
in_reply_to = Message.objects.get(id=in_reply_to_id)
|
|
except Message.DoesNotExist:
|
|
log("Unable to retrieve in_reply_to message: %s" % in_reply_to_id)
|
|
|
|
desc = "Sent message {} - manual post - {}-{}".format(rp,
|
|
submission.name,
|
|
submission.rev)
|
|
SubmissionEmailEvent.objects.create(
|
|
submission = submission,
|
|
desc = desc,
|
|
msgtype = 'msgout',
|
|
by = request.user.person,
|
|
message = msg,
|
|
in_reply_to = in_reply_to)
|
|
|
|
# send email
|
|
send_mail_message(None,msg)
|
|
|
|
messages.success(request, 'Email sent.')
|
|
return redirect('ietf.submit.views.submission_status',
|
|
submission_id=submission.id,
|
|
access_token=submission.access_token())
|
|
|
|
else:
|
|
reply_to = get_reply_to()
|
|
msg = None
|
|
|
|
if not message_id:
|
|
addrs = gather_address_lists('sub_confirmation_requested',submission=submission).as_strings(compact=False)
|
|
to_email = addrs.to
|
|
cc = addrs.cc
|
|
subject = 'Regarding {}'.format(submission.name)
|
|
else:
|
|
try:
|
|
submitEmail = SubmissionEmailEvent.objects.get(id=message_id)
|
|
msg = submitEmail.message
|
|
|
|
if msg:
|
|
to_email = msg.frm
|
|
cc = msg.cc
|
|
subject = 'Re:{}'.format(msg.subject)
|
|
else:
|
|
to_email = None
|
|
cc = None
|
|
subject = 'Regarding {}'.format(submission.name)
|
|
except Message.DoesNotExist:
|
|
to_email = None
|
|
cc = None
|
|
subject = 'Regarding {}'.format(submission.name)
|
|
|
|
initial = {
|
|
'to': to_email,
|
|
'cc': cc,
|
|
'frm': settings.IDSUBMIT_FROM_EMAIL,
|
|
'subject': subject,
|
|
'reply_to': reply_to,
|
|
}
|
|
|
|
if msg:
|
|
initial['in_reply_to_id'] = msg.id
|
|
|
|
form = MessageModelForm(initial=initial)
|
|
|
|
return render(request, "submit/email.html", {
|
|
'submission': submission,
|
|
'access_token': submission.access_token(),
|
|
'form':form})
|
|
|
|
|
|
def show_submission_email_message(request, submission_id, message_id, access_token=None):
|
|
submission = get_submission_or_404(submission_id, access_token)
|
|
|
|
submitEmail = get_object_or_404(SubmissionEmailEvent, pk=message_id)
|
|
attachments = submitEmail.message.messageattachment_set.all()
|
|
|
|
return render(request, 'submit/submission_email.html',
|
|
{'submission': submission,
|
|
'message': submitEmail,
|
|
'attachments': attachments})
|
|
|
|
def show_submission_email_attachment(request, submission_id, message_id, filename, access_token=None):
|
|
get_submission_or_404(submission_id, access_token)
|
|
|
|
message = get_object_or_404(SubmissionEmailEvent, pk=message_id)
|
|
|
|
attach = get_object_or_404(MessageAttachment,
|
|
message=message.message,
|
|
filename=filename)
|
|
|
|
if attach.encoding == "base64":
|
|
body = base64.b64decode(attach.body)
|
|
else:
|
|
body = attach.body.encode('utf-8')
|
|
|
|
if attach.content_type is None:
|
|
content_type='text/plain'
|
|
else:
|
|
content_type=attach.content_type
|
|
|
|
response = HttpResponse(body, content_type=content_type)
|
|
response['Content-Disposition'] = 'attachment; filename=%s' % attach.filename
|
|
response['Content-Length'] = len(body)
|
|
return response
|
|
|
|
|
|
def get_submission_or_404(submission_id, access_token=None):
|
|
submission = get_object_or_404(Submission, pk=submission_id)
|
|
|
|
key_matched = access_token and submission.access_token() == access_token
|
|
if not key_matched: key_matched = submission.access_key == access_token # backwards-compat
|
|
if access_token and not key_matched:
|
|
raise Http404
|
|
|
|
return submission
|
|
|
|
|
|
def async_poke_test(request):
|
|
result = poke.delay()
|
|
return HttpResponse(f'Poked {result}', content_type='text/plain')
|