From 7c79a4d7079acbcd21488d2942253b334f5f5700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20A=2E=20S=C3=A1nchez=20L=C3=B3pez?= Date: Thu, 10 Feb 2011 18:29:37 +0000 Subject: [PATCH] Edit of metadata and request manual post. See #595 - Legacy-Id: 2845 --- ietf/settings.py | 3 + ietf/submit/forms.py | 181 ++++++++++++++++++++- ietf/submit/urls.py | 1 + ietf/submit/utils.py | 102 +++++++++--- ietf/submit/views.py | 53 +++++- ietf/templates/submit/confirm_autopost.txt | 3 + ietf/templates/submit/draft_edit.html | 145 +++++++++++++++++ ietf/templates/submit/draft_status.html | 31 +++- ietf/templates/submit/manual_post_mail.txt | 21 +++ 9 files changed, 505 insertions(+), 35 deletions(-) create mode 100644 ietf/templates/submit/confirm_autopost.txt create mode 100644 ietf/templates/submit/draft_edit.html create mode 100644 ietf/templates/submit/manual_post_mail.txt diff --git a/ietf/settings.py b/ietf/settings.py index bab6b4b0d..86e859869 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -189,6 +189,9 @@ LIAISON_ATTACH_PATH = '/a/www/ietf-datatracker/documents/LIAISON/' LIAISON_ATTACH_URL = '/documents/LIAISON/' # ID Submission Tool settings +IDST_FROM_EMAIL = 'IETF I-D Submission Tool ' +IDST_TO_EMAIL = 'internet-drafts@ietf.org' + # Days from meeting to cut off dates on submit FIRST_CUTOFF_DAYS = 5 SECOND_CUTOFF_DAYS = 3 diff --git a/ietf/submit/forms.py b/ietf/submit/forms.py index 712834e1c..ecbf772c6 100644 --- a/ietf/submit/forms.py +++ b/ietf/submit/forms.py @@ -1,8 +1,11 @@ +import sha +import random import os import subprocess import datetime from django import forms +from django.forms.fields import email_re from django.conf import settings from django.template.loader import render_to_string from django.utils.html import mark_safe @@ -10,10 +13,12 @@ from django.utils.html import mark_safe from ietf.idtracker.models import InternetDraft, IETFWG from ietf.proceedings.models import Meeting from ietf.submit.models import IdSubmissionDetail, TempIdAuthors +from ietf.submit.utils import MANUAL_POST_REQUESTED, NONE_WG, UPLOADED, WAITING_AUTHENTICATION from ietf.submit.parsers.pdf_parser import PDFParser from ietf.submit.parsers.plain_parser import PlainParser from ietf.submit.parsers.ps_parser import PSParser from ietf.submit.parsers.xml_parser import XMLParser +from ietf.utils.mail import send_mail from ietf.utils.draft import Draft @@ -42,6 +47,7 @@ class UploadForm(forms.Form): self.draft = None self.filesize = None self.group = None + self.file_type = [] self.read_dates() def read_dates(self): @@ -200,15 +206,16 @@ class UploadForm(forms.Form): return self.draft def save(self): - for fd in [self.cleaned_data['txt'], self.cleaned_data['pdf'], - self.cleaned_data['xml'], self.cleaned_data['ps']]: + for ext in ['txt', 'pdf', 'xml', 'ps']: + fd = self.cleaned_data[ext] if not fd: continue - filename = os.path.join(self.staging_path, fd.name) + self.file_type.append('.%s' % ext) + filename = os.path.join(self.staging_path, '%s-%s.%s' % (self.draft.filename, self.draft.revision, ext)) destination = open(filename, 'wb+') for chunk in fd.chunks(): destination.write(chunk) - destination.close() + destination.close() self.check_idnits() return self.save_draft_info(self.draft) @@ -222,7 +229,7 @@ class UploadForm(forms.Form): existing_draft = InternetDraft.objects.filter(filename=filename) if existing_draft: group = existing_draft[0].group and existing_draft[0].group.ietfwg or None - if group and group.pk != 1027: + if group and group.pk != NONE_WG: return group else: return None @@ -258,7 +265,9 @@ class UploadForm(forms.Form): group_acronym=self.group, remote_ip=self.remote_ip, first_two_pages=''.join(draft.pages[:2]), - status_id=1, # Status 1 - upload + status_id=UPLOADED, + abstract=draft.get_abstract(), + file_type=','.join(self.file_type), ) order = 0 for author in draft.get_authors(): @@ -297,3 +306,163 @@ class AutoPostForm(forms.Form): 'email': i.email()[1], 'full_name': full_name}) return ''.join(buttons) + + def save(self, request): + self.save_submitter_info() + self.save_new_draft_info() + self.send_confirmation_mail(request) + + def send_confirmation_mail(self, request): + subject = 'Confirmation for Auto-Post of I-D %s' % self.draft.filename + from_email = settings.IDST_FROM_EMAIL + to_email = self.cleaned_data['email'] + send_mail(request, from_email, to_email, subject, 'submit/confirm_autopost.txt', + {'draft': self.draft }) + + def save_submitter_info(self): + TempIdAuthors.objects.create( + id_document_tag=self.draft.temp_id_document_tag, + first_name=self.cleaned_data['first_name'], + last_name=self.cleaned_data['last_name'], + email_address=self.cleaned_data['email'], + author_order=0, + submission=self.draft) + + def save_new_draft_info(self): + salt = sha.new(str(random.random())).hexdigest()[:5] + self.draft.auth_key = sha.new(salt+self.cleaned_data['email']).hexdigest() + self.draft.status_id = WAITING_AUTHENTICATION + self.draft.save() + + +class MetaDataForm(AutoPostForm): + + title = forms.CharField(label=u'Title', required=True) + version = forms.CharField(label=u'Version', required=True) + creation_date = forms.DateField(label=u'Creation date', required=True) + pages = forms.IntegerField(label=u'Pages', required=True) + abstract = forms.CharField(label=u'Abstract', widget=forms.Textarea, required=True) + first_name = forms.CharField(label=u'Given name', required=True) + last_name = forms.CharField(label=u'Last name', required=True) + email = forms.EmailField(label=u'Email address', required=True) + comments = forms.CharField(label=u'Comments to the secretariat', widget=forms.Textarea, required=False) + fields = ['title', 'version', 'creation_date', 'pages', 'abstract', 'first_name', 'last_name', 'email', 'comments'] + + def __init__(self, *args, **kwargs): + super(MetaDataForm, self).__init__(*args, **kwargs) + self.set_initials() + self.authors = self.get_initial_authors() + + def get_initial_authors(self): + authors=[] + if self.is_bound: + for key, value in self.data.items(): + if key.startswith('first_name_'): + author = {'errors': {}} + index = key.replace('first_name_', '') + first_name = value.strip() + if not first_name: + author['errors']['first_name'] = 'This field is required' + last_name = self.data.get('last_name_%s' % index, '').strip() + if not last_name: + author['errors']['last_name'] = 'This field is required' + email = self.data.get('email_%s' % index, '').strip() + if not email: + author['errors']['email'] = 'This field is required' + elif not email_re.search(email): + author['errors']['email'] = 'Enter a valid e-mail address' + if first_name or last_name or email: + author.update({'first_name': first_name, + 'last_name': last_name, + 'email': ('%s %s' % (first_name, last_name), email), + 'index': index, + }) + authors.append(author) + authors.sort(lambda x,y: cmp(int(x['index']), int(y['index']))) + return authors + + def set_initials(self): + self.fields['pages'].initial=self.draft.txt_page_count + self.fields['creation_date'].initial=self.draft.creation_date + self.fields['version'].initial=self.draft.revision + self.fields['abstract'].initial=self.draft.abstract + self.fields['title'].initial=self.draft.id_document_name + + def clean_creation_date(self): + creation_date = self.cleaned_data.get('creation_date', None) + if not creation_date: + return None + submit_date = self.draft.submission_date + if creation_date > submit_date: + raise forms.ValidationError('Creation Date must not be set after submission date') + if creation_date + datetime.timedelta(days=3) < submit_date: + raise forms.ValidationError('Creation Date must be within 3 days of submission date') + return creation_date + + def clean_version(self): + version = self.cleaned_data.get('version', None) + if not version: + return None + if len(version) > 2: + raise forms.ValidationError('Version field is not in NN format') + try: + version_int = int(version) + except ValueError: + raise forms.ValidationError('Version field is not in NN format') + if version_int > 99 or version_int < 0: + raise forms.ValidationError('Version must be set between 00 and 99') + existing_revisions = [int(i.revision) for i in InternetDraft.objects.filter(filename=self.draft.filename)] + expected = 0 + if existing_revisions: + expected = max(existing_revisions) + 1 + if version_int != expected: + raise forms.ValidationError('Invalid Version Number (Version %00d is expected)' % expected) + return version + + def clean(self): + if bool([i for i in self.authors if i['errors']]): + raise forms.ValidationError('Please fix errors in author list') + return super(MetaDataForm, self).clean() + + def get_authors(self): + if not self.is_bound: + return self.validation.get_authors() + else: + return self.authors + + def move_docs(self, draft, revision): + old_revision = draft.revision + for ext in draft.file_type.split(','): + source = os.path.join(settings.STAGING_PATH, '%s-%s%s' % (draft.filename, old_revision, ext)) + dest = os.path.join(settings.STAGING_PATH, '%s-%s%s' % (draft.filename, revision, ext)) + os.rename(source, dest) + + def save_new_draft_info(self): + draft = self.draft + draft.id_documen_name = self.cleaned_data['title'] + if draft.revision != self.cleaned_data['version']: + self.move_docs(draft, self.cleaned_data['version']) + draft.revision = self.cleaned_data['version'] + draft.creation_date = self.cleaned_data['creation_date'] + draft.txt_page_count = self.cleaned_data['pages'] + draft.abstract = self.cleaned_data['abstract'] + draft.comment_to_sec = self.cleaned_data['comments'] + draft.status_id = MANUAL_POST_REQUESTED + draft.save() + self.save_submitter_info() + + def save(self, request): + self.save_new_draft_info() + self.send_mail_to_secretariat(request) + + def send_mail_to_secretariat(self, request): + subject = 'Manual Post Requested for %s' % self.draft.filename + from_email = settings.IDST_FROM_EMAIL + to_email = settings.IDST_TO_EMAIL + cc = [self.cleaned_data['email']] + cc += [i['email'][1] for i in self.authors] + if self.draft.group_acronym: + cc += [i.person.email()[1] for i in self.draft.group_acronym.wgchair_set.all()] + cc = list(set(cc)) + send_mail(request, from_email, to_email, subject, 'submit/manual_post_mail.txt', + {'form': self, 'draft': self.draft }, cc=cc) diff --git a/ietf/submit/urls.py b/ietf/submit/urls.py index 9f5ae1ed8..5b1e75a2b 100644 --- a/ietf/submit/urls.py +++ b/ietf/submit/urls.py @@ -6,6 +6,7 @@ urlpatterns = patterns('ietf.submit.views', url(r'^status/$', 'submit_status', name='submit_status'), url(r'^status/(?P\d+)/$', 'draft_status', name='draft_status'), url(r'^status/(?P\d+)/edit/$', 'draft_edit', name='draft_edit'), + url(r'^status/(?P\d+)/confirm/(?P[a-f\d]+)/$', 'draft_confirm', name='draft_confirm'), ) urlpatterns += patterns('django.views.generic.simple', diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index acadd30f2..c50ba8275 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -1,7 +1,62 @@ +import os import re import datetime -from ietf.idtracker.models import InternetDraft, EmailAddress +from ietf.idtracker.models import InternetDraft, EmailAddress, PersonOrOrgInfo + + +# Some usefull states +UPLOADED = 1 +WAITING_AUTHENTICATION = 4 +MANUAL_POST_REQUESTED = 5 +POSTED = -1 +POSTED_BY_SECRETARIAT = -2 + + +# Not a real WG +NONE_WG = 1027 + + +def perform_post(submission): + group_id = submission.group_acronym and submission.group_acronym.pk or NONE_WG + updated = False + try: + draft = InternetDraft.objects.get(filename=submission.filename) + draft.title = submission.id_document_name + draft.group_id = group_id + draft.filename = submission.filename + draft.revision = submission.revision + draft.revision_date = submission.creation_date + draft.file_type = submission.file_type + draft.txt_page_count = submission.txt_page_count + draft.last_modified_date = datetime.date.today() + draft.abstract = submission.abstract + draft.save() + updated = True + except InternetDraft.DoesNotExist: + draft = InternetDraft.objects.create( + title = submission.id_document_name, + group_id = group_id, + filename = submission.filename, + revision = submission.revision, + revision_date = submission.creation_date, + file_type = submission.file_type, + txt_page_count = submission.txt_page_count, + start_date = datetime.date.today(), + last_modified_date = datetime.date.today(), + abstract = submission.abstract, + status_id = 1, # Active + intended_status_id = 8, # None + ) + move_docs(submission) + submission.status_id = POSTED + submission.save() + +def move_docs(submission): + for ext in submission.file_type.split(','): + source = os.path.join(settings.STAGING_PATH, '%s-%s%s' % (submission.filename, submission.revision, ext)) + dest = os.path.join(settings.INTERNET_DRAFT_PATH, '%s-%s%s' % (submission.filename, submission.revision, ext)) + os.rename(source, dest) class DraftValidation(object): @@ -12,13 +67,14 @@ class DraftValidation(object): self.passes_idnits = self.passes_idnits() self.wg = self.get_working_group() self.authors = self.get_authors() + self.submitter = self.get_submitter() def passes_idnits(self): passes_idnits = self.check_idnits_success(self.draft.idnits_message) return passes_idnits def get_working_group(self): - if self.draft.group_acronym and self.draft.group_acronym.pk == 1027: + if self.draft.group_acronym and self.draft.group_acronym.pk == NONE_WG: return None return self.draft.group_acronym @@ -40,12 +96,19 @@ class DraftValidation(object): def validate_metadata(self): self.validate_revision() self.validate_authors() + self.validate_abstract() self.validate_creation_date() + def validate_abstract(self): + if not self.draft.abstract: + self.add_warning('abstract', 'Abstract is empty or was not found') + def add_warning(self, key, value): self.warnings.update({key: value}) def validate_revision(self): + if self.draft.status_id in [POSTED, POSTED_BY_SECRETARIAT]: + return revision = self.draft.revision existing_revisions = [int(i.revision) for i in InternetDraft.objects.filter(filename=self.draft.filename)] expected = 0 @@ -61,24 +124,25 @@ class DraftValidation(object): def validate_creation_date(self): date = self.draft.creation_date if not date: - self.add_warning('creation_date', 'Creation Date field is empty or the creation date is not in a proper format.') + self.add_warning('creation_date', 'Creation Date field is empty or the creation date is not in a proper format') return submit_date = self.draft.submission_date - if date + datetime.timedelta(days=3) > submit_date: - self.add_warning('creation_date', 'Creation Date must be within 3 days of submission date.') + if date > submit_date: + self.add_warning('creation_date', 'Creation Date must not be set after submission date') + if date + datetime.timedelta(days=3) < submit_date: + self.add_warning('creation_date', 'Creation Date must be within 3 days of submission date') def get_authors(self): - tmpauthors = self.draft.tempidauthors_set.all().order_by('author_order') - authors = [] - for i in tmpauthors: - person = None - for existing in EmailAddress.objects.filter(address=i.email_address): - try: - person = existing.person_or_org - except PersonOrOrgInfo.DoesNotExist: - pass - if not person: - authors.append(i) - else: - authors.append(person) - return authors + tmpauthors = self.draft.tempidauthors_set.exclude(author_order=0).order_by('author_order') + return tmpauthors + + def get_submitter(self): + submitter = self.draft.tempidauthors_set.filter(author_order=0) + if submitter: + return submitter[0] + elif self.draft.submitter_tag: + try: + return PersonOrOrgInfo.objects.get(pk=self.draft.submitter_tag) + except PersonOrOrgInfo.DoesNotExist: + return False + return None diff --git a/ietf/submit/views.py b/ietf/submit/views.py index 6166659dd..740b38de7 100644 --- a/ietf/submit/views.py +++ b/ietf/submit/views.py @@ -1,13 +1,14 @@ # Copyright The IETF Trust 2007, All Rights Reserved from django.core.urlresolvers import reverse -from django.http import HttpResponseRedirect +from django.http import HttpResponseRedirect, Http404 from django.shortcuts import get_object_or_404 from django.shortcuts import render_to_response from django.template import RequestContext from ietf.submit.models import IdSubmissionDetail -from ietf.submit.forms import UploadForm, AutoPostForm -from ietf.submit.utils import DraftValidation +from ietf.submit.forms import UploadForm, AutoPostForm, MetaDataForm +from ietf.submit.utils import (DraftValidation, UPLOADED, WAITING_AUTHENTICATION, + perform_post) def submit_index(request): @@ -41,13 +42,21 @@ def submit_status(request): -def draft_status(request, submission_id): +def draft_status(request, submission_id, message=None): detail = get_object_or_404(IdSubmissionDetail, submission_id=submission_id) validation = DraftValidation(detail) is_valid = validation.is_valid() - if request.method=='POST': + status = None + allow_edit = True + if detail.status_id != UPLOADED: + status = detail.status + allow_edit = None + if request.method=='POST' and allow_edit: if request.POST.get('autopost', False): auto_post_form = AutoPostForm(draft=detail, validation=validation, data=request.POST) + if auto_post_form.is_valid(): + auto_post_form.save(request) + return HttpResponseRedirect(reverse(draft_status, None, kwargs={'submission_id': detail.submission_id})) else: return HttpResponseRedirect(reverse(draft_edit, None, kwargs={'submission_id': detail.submission_id})) else: @@ -58,9 +67,41 @@ def draft_status(request, submission_id): 'validation': validation, 'auto_post_form': auto_post_form, 'is_valid': is_valid, + 'status': status, + 'allow_edit': allow_edit, + 'message': message, }, context_instance=RequestContext(request)) def draft_edit(request, submission_id): - pass + detail = get_object_or_404(IdSubmissionDetail, submission_id=submission_id) + if detail.status_id != UPLOADED: + raise Http404 + validation = DraftValidation(detail) + if request.method=='POST': + form = MetaDataForm(draft=detail, validation=validation, data=request.POST) + if form.is_valid(): + form.save(request) + else: + form = MetaDataForm(draft=detail, validation=validation) + return render_to_response('submit/draft_edit.html', + {'selected': 'status', + 'detail': detail, + 'validation': validation, + 'form': form, + }, + context_instance=RequestContext(request)) + + +def draft_confirm(request, submission_id, auth_key): + detail = get_object_or_404(IdSubmissionDetail, submission_id=submission_id) + message = None + if auth_key != detail.auth_key: + message = ('error', 'Incorrect authorization key') + elif detail.status_id != WAITING_AUTHENTICATION: + message = ('error', 'The submission can not be autoposted because it is in state: %s' % detail.status.status_value) + else: + message = ('success', 'Authorization key accepted. Auto-Post complete') + perform_post(detail) + return draft_status(request, submission_id, message) diff --git a/ietf/templates/submit/confirm_autopost.txt b/ietf/templates/submit/confirm_autopost.txt new file mode 100644 index 000000000..517951445 --- /dev/null +++ b/ietf/templates/submit/confirm_autopost.txt @@ -0,0 +1,3 @@ +Follow this link to confirm you Auto-Post of I-D {{ draft.filename }}-{{ draft.revision }} + +I-D Submission Tool URL: /submit/status/{{ draft.submission_id }}/confirm/{{ draft.auth_key }}/ diff --git a/ietf/templates/submit/draft_edit.html b/ietf/templates/submit/draft_edit.html new file mode 100644 index 000000000..e99fcf09c --- /dev/null +++ b/ietf/templates/submit/draft_edit.html @@ -0,0 +1,145 @@ +{% extends "submit/draft_status.html" %} +{% block title %}Adjust Meta-Data{% endblock %} + +{% block morecss %} +{{ block.super }} +table.metadata-table #id_title, table.metadata-table #id_abstract, table.metadata-table #id_comments { width: 500px; } +table.metadata-table tr.warning th, table.metadata-table tr.warning td { background-color: #ffeebb; } +table.ietf-table tr { vertical-align: top; } +table.ietf-table tr.error { background-color: #ffeebb; border-top: 1px dashed red; border-bottom: 1px dashed red;} +table.ietf-table span.field-error { display: block; color: red; } +{% endblock %} + +{% block pagehead %} +{{ block.super }} + + +{% endblock %} + +{% block submit_content %} +

Adjust External Meta-Data

+ + + + + + + + + + +

Adjust data

+{% if form.errors %} + +{% endif %} +
+ + + + + + + + + + + + + + +

Authors

+ + + + + +{% for author in form.get_authors %} + + + + + +{% endfor %} + +
First nameLast nameEmail address
{{ author.first_name }}{{ author.errors.first_name }}{{ author.last_name }}{{ author.errors.last_name }}{{ author.email.1 }}{{ author.errors.email }}
+
+ +
+ +
+ +

+The IETF is an organized activity of the Internet Society +
Please send problem reports to ietf-action@ietf.org. +

+{% endblock %} diff --git a/ietf/templates/submit/draft_status.html b/ietf/templates/submit/draft_status.html index 7196a28fd..cb6b567ec 100644 --- a/ietf/templates/submit/draft_status.html +++ b/ietf/templates/submit/draft_status.html @@ -4,6 +4,8 @@ {% block morecss %} {{ block.super }} div.metadata-errors { border: 1px solid red; background-color: #ffeebb; padding: 5px 10px; margin: 1em 0px; } +div.info-message-error { border: 1px solid red; background-color: #ffeebb; padding: 5px 10px; margin: 1em 0px; color: red; } +div.info-message-success { border: 1px solid green; background-color: #eeffbb; padding: 5px 10px; margin: 1em 0px; color: green; } table.metadata-table th { white-space: nowrap; font-weight: bold; } table.metadata-table #id_first_name, table.metadata-table #id_last_name { width: 200px; } table.metadata-table #id_email { width: 400px; } @@ -60,6 +62,14 @@ table.metadata-table ul.errorlist { color: red; padding: 0px; margin: 0px; list- {% endblock %} {% block submit_content %} +{% if status %} +

Status of the submission: {{ status.status_value }}

+{% endif %} + +{% if message %} +
{{ message.1 }}
+{% endif %} +

Check Page

{% if validation.passes_idnits %} @@ -114,9 +124,9 @@ returned to the submitter. Revision{{ detail.revision }}

{{ validation.warnings.revision }}
Submission date{{ detail.submission_date }} Title{{ detail.id_document_name }} -WG{{ validation.wg|default:"Individual Submission" }}
{{ validation.warnings.group }} +WG{{ validation.wg|default:"Individual Submission" }}
{{ validation.warnings.group }}
File size{{ detail.filesize|filesizeformat }} -Creation date{{ detail.creation_date }}
{{ validation.warnings.creation_date }} +Creation date{{ detail.creation_date }}
{{ validation.warnings.creation_date }}
Author(s) information {% if not validation.authors %}
{{ validation.warning.authors }}
@@ -125,9 +135,20 @@ returned to the submitter. Author {{ forloop.counter }}{{ author.email.0 }} <{{ author.email.1 }}> {% endfor %} {% endif %} -Pages{{ detail.txt_page_count }}
{{ validation.warnings.pages }} -Abstract{{ detail.abstract|linebreaksbr }} +Pages{{ detail.txt_page_count }}
{{ validation.warnings.pages }}
+Abstract{{ detail.abstract|linebreaksbr }}
{{ validation.warnings.abstract }}
+ +{% if validation.submitter %} +

Submitter information

+ + + + + +{% endif %} + +{% if allow_edit %}
(Leads to manual post by the Secretariat)
@@ -144,6 +165,8 @@ If you are one of the authors of this document, then please click the button wit {% endif %} +{% endif %} +

The IETF is an organized activity of the Internet Society
Please send problem reports to ietf-action@ietf.org. diff --git a/ietf/templates/submit/manual_post_mail.txt b/ietf/templates/submit/manual_post_mail.txt new file mode 100644 index 000000000..03c2c11e6 --- /dev/null +++ b/ietf/templates/submit/manual_post_mail.txt @@ -0,0 +1,21 @@ +Manual Posting Requested for following Internet-Draft: + +I-D Submission Tool URL: /submit/status/{{ draft.submission_id }}/ + +File name: {{ draft.filename }} +Submission date: {{ draft.submission_date }} +WG: {{ draft.wg|default:"Individual Submission" }} +File size: {{ draft.filesize }} + +Title: {{ draft.id_document_name }} +Version: {{ draft.revision }} +Creation date: {{ draft.id_document_name }} +Pages: {{ draft.txt_page_count }} +Abstract: {{ draft.abstract }} +Submitter: {{ form.cleaned_data.first_name }} {{ form.cleaned_data.last_name }} <{{ form.cleaned_data.email }}> + +Author(s): +{% for author in form.get_authors %}{{ author.first_name }} {{ author.last_name }} <{{ author.email.1 }}> +{% endfor %} + +{{ draft.comment_to_sec }}