Merged in ^/branch/scow/track-manual-id-posts-6.31.1.dev0, which provides secretariat support for managing draft submissions outside of the automated datatracker draft submission tool. In addition to making the secreatiat handling of draft submissions that come in by email or other paths easier, it also improves the datatracker submission and document history for these documents.

- Legacy-Id: 11959
This commit is contained in:
Henrik Levkowetz 2016-09-09 17:00:23 +00:00
commit 22b2953b18
42 changed files with 1962 additions and 188 deletions

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('message', '__first__'),
('doc', '0013_auto_20151027_1127'),
]
operations = [
migrations.CreateModel(
name='AddedMessageEvent',
fields=[
('docevent_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='doc.DocEvent')),
('msgtype', models.CharField(max_length=25)),
('in_reply_to', models.ForeignKey(related_name='doc_irtomanual', blank=True, to='message.Message', null=True)),
('message', models.ForeignKey(related_name='doc_manualevents', blank=True, to='message.Message', null=True)),
],
options={
},
bases=('doc.docevent',),
),
migrations.AlterField(
model_name='docevent',
name='type',
field=models.CharField(max_length=50, choices=[(b'new_revision', b'Added new revision'), (b'changed_document', b'Changed document metadata'), (b'added_comment', b'Added comment'), (b'added_message', b'Added message'), (b'deleted', b'Deleted document'), (b'changed_state', b'Changed state'), (b'changed_stream', b'Changed document stream'), (b'expired_document', b'Expired document'), (b'extended_expiry', b'Extended expiry of document'), (b'requested_resurrect', b'Requested resurrect'), (b'completed_resurrect', b'Completed resurrect'), (b'changed_consensus', b'Changed consensus'), (b'published_rfc', b'Published RFC'), (b'added_suggested_replaces', b'Added suggested replacement relationships'), (b'reviewed_suggested_replaces', b'Reviewed suggested replacement relationships'), (b'changed_group', b'Changed group'), (b'changed_protocol_writeup', b'Changed protocol writeup'), (b'changed_charter_milestone', b'Changed charter milestone'), (b'initial_review', b'Set initial review time'), (b'changed_review_announcement', b'Changed WG Review text'), (b'changed_action_announcement', b'Changed WG Action text'), (b'started_iesg_process', b'Started IESG process on document'), (b'created_ballot', b'Created ballot'), (b'closed_ballot', b'Closed ballot'), (b'sent_ballot_announcement', b'Sent ballot announcement'), (b'changed_ballot_position', b'Changed ballot position'), (b'changed_ballot_approval_text', b'Changed ballot approval text'), (b'changed_ballot_writeup_text', b'Changed ballot writeup text'), (b'changed_rfc_editor_note_text', b'Changed RFC Editor Note text'), (b'changed_last_call_text', b'Changed last call text'), (b'requested_last_call', b'Requested last call'), (b'sent_last_call', b'Sent last call'), (b'scheduled_for_telechat', b'Scheduled for telechat'), (b'iesg_approved', b'IESG approved document (no problem)'), (b'iesg_disapproved', b'IESG disapproved document (do not publish)'), (b'approved_in_minute', b'Approved in minute'), (b'iana_review', b'IANA review comment'), (b'rfc_in_iana_registry', b'RFC is in IANA registry'), (b'rfc_editor_received_announcement', b'Announcement was received by RFC Editor'), (b'requested_publication', b'Publication at RFC Editor requested')]),
preserve_default=True,
),
]

View file

@ -7,7 +7,7 @@ from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('doc', '0013_auto_20151027_1127'),
('doc', '0014_auto_20160524_2147'),
]
operations = [

View file

@ -631,6 +631,7 @@ EVENT_TYPES = [
("new_revision", "Added new revision"),
("changed_document", "Changed document metadata"),
("added_comment", "Added comment"),
("added_message", "Added message"),
("deleted", "Deleted document"),
@ -823,6 +824,13 @@ class InitialReviewDocEvent(DocEvent):
expires = models.DateTimeField(blank=True, null=True)
class AddedMessageEvent(DocEvent):
import ietf.message.models
message = models.ForeignKey(ietf.message.models.Message, null=True, blank=True,related_name='doc_manualevents')
msgtype = models.CharField(max_length=25)
in_reply_to = models.ForeignKey(ietf.message.models.Message, null=True, blank=True,related_name='doc_irtomanual')
# dumping store for removed events
class DeletedEvent(models.Model):
content_type = models.ForeignKey(ContentType)

View file

@ -11,7 +11,7 @@ from ietf.doc.models import (BallotType, DeletedEvent, StateType, State, Documen
DocumentAuthor, DocEvent, StateDocEvent, DocHistory, ConsensusDocEvent, DocAlias,
TelechatDocEvent, DocReminder, LastCallDocEvent, NewRevisionDocEvent, WriteupDocEvent,
InitialReviewDocEvent, DocHistoryAuthor, BallotDocEvent, RelatedDocument,
RelatedDocHistory, BallotPositionDocEvent)
RelatedDocHistory, BallotPositionDocEvent, AddedMessageEvent)
from ietf.name.resources import BallotPositionNameResource, DocTypeNameResource
@ -513,3 +513,32 @@ class BallotPositionDocEventResource(ModelResource):
}
api.doc.register(BallotPositionDocEventResource())
from ietf.person.resources import PersonResource
from ietf.message.resources import MessageResource
class AddedMessageEventResource(ModelResource):
by = ToOneField(PersonResource, 'by')
doc = ToOneField(DocumentResource, 'doc')
docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr')
message = ToOneField(MessageResource, 'message', null=True)
in_reply_to = ToOneField(MessageResource, 'in_reply_to', null=True)
class Meta:
queryset = AddedMessageEvent.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'addedmessageevent'
filtering = {
"id": ALL,
"time": ALL,
"type": ALL,
"desc": ALL,
"msgtype": ALL,
"by": ALL_WITH_RELATIONS,
"doc": ALL_WITH_RELATIONS,
"docevent_ptr": ALL_WITH_RELATIONS,
"message": ALL_WITH_RELATIONS,
"in_reply_to": ALL_WITH_RELATIONS,
}
api.doc.register(AddedMessageEventResource())

View file

@ -278,6 +278,16 @@ def add_links_in_new_revision_events(doc, events, diff_revisions):
prev = diff_url
def add_events_message_info(events):
for e in events:
if not e.type == "added_message":
continue
e.message = e.addedmessageevent.message
e.msgtype = e.addedmessageevent.msgtype
e.in_reply_to = e.addedmessageevent.in_reply_to
def get_document_content(key, filename, split=True, markup=True):
try:
with open(filename, 'rb') as f:

View file

@ -50,7 +50,8 @@ from ietf.doc.models import ( Document, DocAlias, DocHistory, DocEvent, BallotDo
from ietf.doc.utils import ( add_links_in_new_revision_events, augment_events_with_revision,
can_adopt_draft, get_chartering_type, get_document_content, get_tags_for_stream_id,
needed_ballot_positions, nice_consensus, prettify_std_name, update_telechat, has_same_ballot,
get_initial_notify, make_notify_changed_event, crawl_history, default_consensus)
get_initial_notify, make_notify_changed_event, crawl_history, default_consensus,
add_events_message_info)
from ietf.community.utils import augment_docs_with_tracking_info
from ietf.group.models import Role
from ietf.group.utils import can_manage_group, can_manage_materials
@ -657,6 +658,7 @@ def document_history(request, name):
augment_events_with_revision(doc, events)
add_links_in_new_revision_events(doc, events, diff_revisions)
add_events_message_info(events)
# figure out if the current user can add a comment to the history
if doc.type_id == "draft" and doc.group != None:

View file

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def forward(apps, schema_editor):
Recipient=apps.get_model('mailtrigger','Recipient')
rc = Recipient.objects.create
rc(slug='submission_manualpost_handling',
desc='IETF manual post handling',
template='<ietf-manualpost@ietf.org>')
def reverse(apps, schema_editor):
Recipient=apps.get_model('mailtrigger','Recipient')
Recipient.objects.filter(slug='submission_manualpost_handling').delete()
class Migration(migrations.Migration):
dependencies = [
('mailtrigger', '0005_interim_trigger'),
]
operations = [
migrations.RunPython(forward, reverse)
]

View file

@ -80,6 +80,9 @@ def gather_relevant_expansions(**kwargs):
rule_list.append((mailtrigger.slug,mailtrigger.desc,addrs.to,addrs.cc))
return sorted(rule_list)
def get_base_submission_message_address():
return Recipient.objects.get(slug='submission_manualpost_handling').gather()[0]
def get_base_ipr_request_address():
return Recipient.objects.get(slug='ipr_requests').gather()[0]

View file

@ -28,6 +28,19 @@ class Message(models.Model):
def __unicode__(self):
return "'%s' %s -> %s" % (self.subject, self.frm, self.to)
class MessageAttachment(models.Model):
message = models.ForeignKey(Message)
filename = models.CharField(max_length=255, db_index=True, blank=True)
content_type = models.CharField(max_length=255, blank=True)
encoding = models.CharField(max_length=255, blank=True)
removed = models.BooleanField(default=False)
body = models.TextField()
def __unicode__(self):
return self.filename
class SendQueue(models.Model):
time = models.DateTimeField(default=datetime.datetime.now)
by = models.ForeignKey(Person)

View file

@ -7,12 +7,11 @@ from tastypie.cache import SimpleCache
from ietf import api
from ietf.message.models import Message, SendQueue
from ietf.message.models import Message, SendQueue, MessageAttachment
from ietf.person.resources import PersonResource
from ietf.group.resources import GroupResource
from ietf.doc.resources import DocumentResource
class MessageResource(ModelResource):
by = ToOneField(PersonResource, 'by')
related_groups = ToManyField(GroupResource, 'related_groups', null=True)
@ -59,3 +58,21 @@ class SendQueueResource(ModelResource):
}
api.message.register(SendQueueResource())
class MessageAttachmentResource(ModelResource):
message = ToOneField(MessageResource, 'message')
class Meta:
queryset = MessageAttachment.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'messageattachment'
filtering = {
"id": ALL,
"filename": ALL,
"removed": ALL,
"body": ALL,
"message": ALL_WITH_RELATIONS,
}
api.message.register(MessageAttachmentResource())

View file

@ -188,7 +188,7 @@
"order": 0,
"revname": "Conflict reviewed by",
"used": true,
"name": "Conflict reviews",
"name": "conflict reviews",
"desc": ""
},
"model": "name.docrelationshipname",
@ -872,6 +872,20 @@
"model": "name.draftsubmissionstatename",
"pk": "posted"
},
{
"fields": {
"order": 8,
"next_states": [
"cancel",
"posted"
],
"used": true,
"name": "Manual Post Waiting for Draft",
"desc": ""
},
"model": "name.draftsubmissionstatename",
"pk": "waiting-for-draft"
},
{
"fields": {
"order": 0,
@ -4903,6 +4917,14 @@
"model": "mailtrigger.recipient",
"pk": "logged_in_person"
},
{
"fields": {
"template": "<ietf-manualpost@ietf.org>",
"desc": "IETF manual post handling"
},
"model": "mailtrigger.recipient",
"pk": "submission_manualpost_handling"
},
{
"fields": {
"template": "<new-work@ietf.org>",

View file

@ -2,7 +2,7 @@ from django.core.urlresolvers import reverse as urlreverse
from django.contrib import admin
from ietf.submit.models import Preapproval, Submission, SubmissionEvent, SubmissionCheck
from ietf.submit.models import Preapproval, Submission, SubmissionEvent, SubmissionCheck, SubmissionEmailEvent
class SubmissionAdmin(admin.ModelAdmin):
list_display = ['id', 'rev', 'draft_link', 'status_link', 'submission_date',]
@ -11,7 +11,7 @@ class SubmissionAdmin(admin.ModelAdmin):
raw_id_fields = ['group', 'draft']
def status_link(self, instance):
url = urlreverse('submit_submission_status_by_hash',
url = urlreverse('ietf.submit.views.submission_status',
kwargs=dict(submission_id=instance.pk,
access_token=instance.access_token()))
return '<a href="%s">%s</a>' % (url, instance.state)
@ -40,3 +40,6 @@ class PreapprovalAdmin(admin.ModelAdmin):
pass
admin.site.register(Preapproval, PreapprovalAdmin)
class SubmissionEmailEventAdmin(admin.ModelAdmin):
list_display = ['id', 'submission', 'time', 'by', 'message', 'desc', ]
admin.site.register(SubmissionEmailEvent, SubmissionEmailEventAdmin)

View file

@ -1,6 +1,7 @@
import os
import re
import datetime
import email
import pytz
import xml2rfc
import tempfile
@ -16,7 +17,9 @@ from ietf.doc.models import Document
from ietf.group.models import Group
from ietf.ietfauth.utils import has_role
from ietf.doc.fields import SearchableDocAliasesField
from ietf.ipr.mail import utc_from_string
from ietf.meeting.models import Meeting
from ietf.message.models import Message
from ietf.submit.models import Submission, Preapproval
from ietf.submit.utils import validate_submission_rev, validate_submission_document_date
from ietf.submit.parsers.pdf_parser import PDFParser
@ -223,9 +226,9 @@ class SubmissionUploadForm(forms.Form):
self.group = self.deduce_group()
# check existing
existing = Submission.objects.filter(name=self.filename, rev=self.revision).exclude(state__in=("posted", "cancel"))
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("submit_submission_status", kwargs={ 'submission_id': existing[0].pk })))
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:
@ -320,6 +323,9 @@ class NameEmailForm(forms.Form):
name = forms.CharField(required=True)
email = forms.EmailField(label=u'Email address')
#Fields for secretariat only
approvals_received = forms.BooleanField(label=u'Approvals received', required=False, initial=False)
def __init__(self, *args, **kwargs):
email_required = kwargs.pop("email_required", True)
super(NameEmailForm, self).__init__(*args, **kwargs)
@ -421,3 +427,86 @@ class PreapprovalForm(forms.Form):
raise forms.ValidationError("A draft with this name has already been submitted and accepted. A pre-approval would not make any difference.")
return n
class SubmissionEmailForm(forms.Form):
'''
Used to add a message to a submission or to create a new submission.
This message is NOT a reply to a previous message but has arrived out of band
if submission_pk is None we are startign a new submission and name
must be unique. Otehrwise the name must match the submission.name.
'''
name = forms.CharField(required=True, max_length=255, label="Draft name")
submission_pk = forms.IntegerField(required=False, widget=forms.HiddenInput())
direction = forms.ChoiceField(choices=(("incoming", "Incoming"), ("outgoing", "Outgoing")),
widget=forms.RadioSelect)
message = forms.CharField(required=True, widget=forms.Textarea,
help_text="Copy the entire message including headers. To do so, view the source, select all, copy then paste into the text area above")
#in_reply_to = MessageModelChoiceField(queryset=Message.objects,label="In Reply To",required=False)
def __init__(self, *args, **kwargs):
super(SubmissionEmailForm, self).__init__(*args, **kwargs)
def clean_message(self):
'''Returns a ietf.message.models.Message object'''
self.message_text = self.cleaned_data['message']
try:
message = email.message_from_string(self.message_text)
except Exception as e:
self.add_error('message', e)
return None
for field in ('to','from','subject','date'):
if not message[field]:
raise forms.ValidationError('Error parsing email: {} field not found.'.format(field))
date = utc_from_string(message['date'])
if not isinstance(date,datetime.datetime):
raise forms.ValidationError('Error parsing email date field')
return message
def clean(self):
if any(self.errors):
return self.cleaned_data
super(SubmissionEmailForm, self).clean()
name = self.cleaned_data['name']
match = re.search(r"(draft-[a-z0-9-]*)-(\d\d)", name)
if not match:
self.add_error('name',
"Submission name {} must start with 'draft-' and only contain digits, lowercase letters and dash characters and end with revision.".format(name))
else:
self.draft_name = match.group(1)
self.revision = match.group(2)
error = validate_submission_rev(self.draft_name, self.revision)
if error:
raise forms.ValidationError(error)
#in_reply_to = self.cleaned_data['in_reply_to']
#message = self.cleaned_data['message']
direction = self.cleaned_data['direction']
if direction != 'incoming' and direction != 'outgoing':
self.add_error('direction', "Must be one of 'outgoing' or 'incoming'")
#if in_reply_to:
# if direction != 'incoming':
# raise forms.ValidationError('Only incoming messages can have In Reply To selected')
# date = utc_from_string(message['date'])
# if date < in_reply_to.time:
# raise forms.ValidationError('The incoming message must have a date later than the message it is replying to')
return self.cleaned_data
class MessageModelForm(forms.ModelForm):
in_reply_to_id = forms.CharField(required=False, widget=forms.HiddenInput())
class Meta:
model = Message
fields = ['to','frm','cc','bcc','reply_to','subject','body']
exclude = ['time','by','content_type','related_groups','related_docs']
def __init__(self, *args, **kwargs):
super(MessageModelForm, self).__init__(*args, **kwargs)
self.fields['frm'].label='From'
self.fields['frm'].widget.attrs['readonly'] = 'True'
self.fields['reply_to'].widget.attrs['readonly'] = 'True'

View file

@ -1,22 +1,33 @@
import re
import email
import datetime
import base64
import os
import pyzmail
from django.conf import settings
from django.core.urlresolvers import reverse as urlreverse
from django.core.validators import ValidationError
from django.contrib.sites.models import Site
from django.template.loader import render_to_string
from ietf.utils.log import log
from ietf.utils.mail import send_mail, send_mail_message
from ietf.doc.models import Document
from ietf.ipr.mail import utc_from_string
from ietf.person.models import Person
from ietf.message.models import Message
from ietf.message.models import Message, MessageAttachment
from ietf.utils.accesstoken import generate_access_token
from ietf.mailtrigger.utils import gather_address_lists
from ietf.mailtrigger.utils import gather_address_lists, get_base_submission_message_address
from ietf.submit.models import SubmissionEmailEvent, Submission
def send_submission_confirmation(request, submission, chair_notice=False):
subject = 'Confirm submission of I-D %s' % submission.name
from_email = settings.IDSUBMIT_FROM_EMAIL
(to_email, cc) = gather_address_lists('sub_confirmation_requested',submission=submission)
confirm_url = settings.IDTRACKER_BASE_URL + urlreverse('submit_confirm_submission', kwargs=dict(submission_id=submission.pk, auth_token=generate_access_token(submission.auth_key)))
status_url = settings.IDTRACKER_BASE_URL + urlreverse('submit_submission_status_by_hash', kwargs=dict(submission_id=submission.pk, access_token=submission.access_token()))
confirm_url = settings.IDTRACKER_BASE_URL + urlreverse('ietf.submit.views.confirm_submission', kwargs=dict(submission_id=submission.pk, auth_token=generate_access_token(submission.auth_key)))
status_url = settings.IDTRACKER_BASE_URL + urlreverse('ietf.submit.views.submission_status', kwargs=dict(submission_id=submission.pk, access_token=submission.access_token()))
send_mail(request, to_email, from_email, subject, 'submit/confirm_submission.txt',
{
@ -35,7 +46,7 @@ def send_full_url(request, submission):
subject = 'Full URL for managing submission of draft %s' % submission.name
from_email = settings.IDSUBMIT_FROM_EMAIL
(to_email, cc) = gather_address_lists('sub_management_url_requested',submission=submission)
url = settings.IDTRACKER_BASE_URL + urlreverse('submit_submission_status_by_hash', kwargs=dict(submission_id=submission.pk, access_token=submission.access_token()))
url = settings.IDTRACKER_BASE_URL + urlreverse('ietf.submit.views.submission_status', kwargs=dict(submission_id=submission.pk, access_token=submission.access_token()))
send_mail(request, to_email, from_email, subject, 'submit/full_url.txt',
{
@ -71,7 +82,7 @@ def send_manual_post_request(request, submission, errors):
(to_email,cc) = gather_address_lists('sub_manual_post_requested',submission=submission)
send_mail(request, to_email, from_email, subject, 'submit/manual_post_request.txt', {
'submission': submission,
'url': settings.IDTRACKER_BASE_URL + urlreverse('submit_submission_status', kwargs=dict(submission_id=submission.pk)),
'url': settings.IDTRACKER_BASE_URL + urlreverse('ietf.submit.views.submission_status', kwargs=dict(submission_id=submission.pk)),
'errors': errors,
}, cc=cc)
@ -121,3 +132,183 @@ def announce_to_authors(request, submission):
{'submission': submission,
'group': group},
cc=cc)
def get_reply_to():
"""Returns a new reply-to address for use with an outgoing message. This is an
address with "plus addressing" using a random string. Guaranteed to be unique"""
local,domain = get_base_submission_message_address().split('@')
while True:
rand = base64.urlsafe_b64encode(os.urandom(12))
address = "{}+{}@{}".format(local,rand,domain)
q = Message.objects.filter(reply_to=address)
if not q:
return address
def process_response_email(msg):
"""Saves an incoming message. msg=string. Message "To" field is expected to
be in the format ietf-submit+[identifier]@ietf.org. Expect to find a message with
a matching value in the reply_to field, associated to a submission.
Create a Message object for the incoming message and associate it to
the original message via new SubmissionEvent"""
message = email.message_from_string(msg)
to = message.get('To')
# exit if this isn't a response we're interested in (with plus addressing)
local,domain = get_base_submission_message_address().split('@')
if not re.match(r'^{}\+[a-zA-Z0-9_\-]{}@{}'.format(local,'{16}',domain),to):
return None
try:
to_message = Message.objects.get(reply_to=to)
except Message.DoesNotExist:
log('Error finding matching message ({})'.format(to))
return None
try:
submission = to_message.manualevents.first().submission
except:
log('Error processing message ({})'.format(to))
return None
if not submission:
log('Error processing message - no submission ({})'.format(to))
return None
parts = pyzmail.parse.get_mail_parts(message)
body=''
for part in parts:
if part.is_body == 'text/plain' and part.disposition == None:
payload, used_charset = pyzmail.decode_text(part.get_payload(), part.charset, None)
body = body + payload + '\n'
by = Person.objects.get(name="(System)")
msg = submit_message_from_message(message, body, by)
desc = "Email: received message - manual post - {}-{}".format(
submission.name,
submission.rev)
submission_email_event = SubmissionEmailEvent.objects.create(
submission = submission,
desc = desc,
msgtype = 'msgin',
by = by,
message = msg,
in_reply_to = to_message
)
save_submission_email_attachments(submission_email_event, parts)
log(u"Received submission email from %s" % msg.frm)
return msg
def add_submission_email(request, remote_ip, name, rev, submission_pk, message, by, msgtype):
"""Add email to submission history"""
#in_reply_to = form.cleaned_data['in_reply_to']
# create Message
parts = pyzmail.parse.get_mail_parts(message)
body=''
for part in parts:
if part.is_body == 'text/plain' and part.disposition == None:
payload, used_charset = pyzmail.decode_text(part.get_payload(), part.charset, None)
body = body + payload + '\n'
msg = submit_message_from_message(message, body, by)
if (submission_pk != None):
# Must exist - we're adding a message to an existing submission
submission = Submission.objects.get(pk=submission_pk)
else:
# Must not exist
submissions = Submission.objects.filter(name=name,rev=rev).exclude(state_id='cancel')
if submissions.count() > 0:
raise ValidationError("Submission {} already exists".format(name))
# create Submission using the name
try:
submission = Submission.objects.create(
state_id="waiting-for-draft",
remote_ip=remote_ip,
name=name,
rev=rev,
title=name,
note="",
submission_date=datetime.date.today(),
replaces="",
)
from ietf.submit.utils import create_submission_event, docevent_from_submission
desc = "Submission created for rev {} in response to email".format(rev)
create_submission_event(request,
submission,
desc)
docevent_from_submission(request,
submission,
desc)
except Exception as e:
log("Exception: %s\n" % e)
raise
if msgtype == 'msgin':
rs = "Received"
else:
rs = "Sent"
desc = "{} message - manual post - {}-{}".format(rs, name, rev)
submission_email_event = SubmissionEmailEvent.objects.create(
desc = desc,
submission = submission,
msgtype = msgtype,
by = by,
message = msg)
#in_reply_to = in_reply_to
save_submission_email_attachments(submission_email_event, parts)
return submission, submission_email_event
def submit_message_from_message(message,body,by=None):
"""Returns a ietf.message.models.Message. msg=email.Message
A copy of mail.message_from_message with different body handling
"""
if not by:
by = Person.objects.get(name="(System)")
msg = Message.objects.create(
by = by,
subject = message.get('subject',''),
frm = message.get('from',''),
to = message.get('to',''),
cc = message.get('cc',''),
bcc = message.get('bcc',''),
reply_to = message.get('reply_to',''),
body = body,
time = utc_from_string(message.get('date', ''))
)
return msg
def save_submission_email_attachments(submission_email_event, parts):
for part in parts:
if part.disposition != 'attachment':
continue
if part.type == 'text/plain':
payload, used_charset = pyzmail.decode_text(part.get_payload(),
part.charset,
None)
encoding = ""
else:
# Need a better approach - for the moment we'll just handle these
# and encode as base64
payload = base64.b64encode(part.get_payload())
encoding = "base64"
#name = submission_email_event.submission.name
MessageAttachment.objects.create(message = submission_email_event.message,
content_type = part.type,
encoding = encoding,
filename=part.filename,
body=payload)

View file

View file

@ -0,0 +1,27 @@
import sys
from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
from ietf.submit.mail import process_response_email
import debug # pyflakes:ignore
class Command(BaseCommand):
help = (u"Process incoming manual post email responses")
option_list = BaseCommand.option_list + (
make_option('--email-file', dest='email', help='File containing email (default: stdin)'),)
def handle(self, *args, **options):
email = options.get('email', None)
msg = None
if not email:
msg = sys.stdin.read()
else:
msg = open(email, "r").read()
try:
process_response_email(msg)
except ValueError as e:
raise CommandError(e)

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('message', '__first__'),
('submit', '0010_data_set_submission_check_symbol'),
]
operations = [
migrations.CreateModel(
name='SubmissionEmailEvent',
fields=[
('submissionevent_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='submit.SubmissionEvent')),
('msgtype', models.CharField(max_length=25)),
('in_reply_to', models.ForeignKey(related_name='irtomanual', blank=True, to='message.Message', null=True)),
('message', models.ForeignKey(related_name='manualevents', blank=True, to='message.Message', null=True)),
],
options={
'ordering': ['-time', '-id'],
},
bases=('submit.submissionevent',),
),
]

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
def add_draft_submission_state_name(apps, schema_editor):
# We can't import the model directly as it may be a newer
# version than this migration expects. We use the historical version.
DraftSubmissionStateName = apps.get_model("name", "DraftSubmissionStateName")
DraftSubmissionStateName.objects.create(slug="waiting-for-draft",
name="Manual Post Waiting for Draft",
desc="",
used=True,
order=8)
dependencies = [
('submit', '0011_submissionemail'),
]
operations = [
migrations.RunPython(add_draft_submission_state_name),
]

View file

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from datetime import date
from django.db import migrations
from ietf.submit.utils import remove_submission_files
class Migration(migrations.Migration):
def remove_old_submissions(apps, schema_editor):
"""
We'll remove any submissions awaiting manual post that are older
than a date provided here.
These all showed up when we added the ability to list submissions
awaiting manual post and go back many years
"""
# We can't import the model directly as it may be a newer
# version than this migration expects. We use the historical version.
before=date(2016, 3, 1)
Submission = apps.get_model("submit", "Submission")
DraftSubmissionStateName = apps.get_model("name", "DraftSubmissionStateName")
cancelled = DraftSubmissionStateName.objects.get(slug="cancel")
for submission in Submission.objects.filter(state_id = "manual", submission_date__lt=before).distinct():
submission.state = cancelled
submission.save()
remove_submission_files(submission)
dependencies = [
('submit', '0012_auto_20160414_1902'),
]
operations = [
migrations.RunPython(remove_old_submissions),
]

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
def add_next_states(apps, schema_editor):
DraftSubmissionStateName = apps.get_model("name", "DraftSubmissionStateName")
cancelled = DraftSubmissionStateName.objects.get(slug="cancel")
posted = DraftSubmissionStateName.objects.get(slug="posted")
mad = DraftSubmissionStateName.objects.get(slug="waiting-for-draft")
mad.next_states.add(cancelled)
mad.next_states.add(posted)
mad.save()
dependencies = [
('submit', '0013_auto_20160415_2120'),
]
operations = [
migrations.RunPython(add_next_states),
]

View file

@ -7,6 +7,7 @@ import jsonfield
from ietf.doc.models import Document
from ietf.person.models import Person
from ietf.group.models import Group
from ietf.message.models import Message
from ietf.name.models import DraftSubmissionStateName
from ietf.utils.accesstoken import generate_random_key, generate_access_token
@ -106,3 +107,15 @@ class Preapproval(models.Model):
def __unicode__(self):
return self.name
class SubmissionEmailEvent(SubmissionEvent):
message = models.ForeignKey(Message, null=True, blank=True,related_name='manualevents')
msgtype = models.CharField(max_length=25)
in_reply_to = models.ForeignKey(Message, null=True, blank=True,related_name='irtomanual')
def __unicode__(self):
return u"%s %s by %s at %s" % (self.submission.name, self.desc, self.by.plain_name() if self.by else "(unknown)", self.time)
class Meta:
ordering = ['-time', '-id']

View file

@ -5,11 +5,11 @@ from tastypie.constants import ALL, ALL_WITH_RELATIONS
from tastypie.cache import SimpleCache
from ietf import api
from ietf.submit.models import Preapproval, SubmissionCheck, Submission, SubmissionEvent
from ietf.submit.models import ( Preapproval, SubmissionCheck, Submission,
SubmissionEmailEvent, SubmissionEvent )
from ietf.person.resources import PersonResource
class PreapprovalResource(ModelResource):
by = ToOneField(PersonResource, 'by')
class Meta:
@ -100,3 +100,31 @@ class SubmissionCheckResource(ModelResource):
}
api.submit.register(SubmissionCheckResource())
from ietf.person.resources import PersonResource
from ietf.message.resources import MessageResource
class SubmissionEmailEventResource(ModelResource):
submission = ToOneField(SubmissionResource, 'submission')
by = ToOneField(PersonResource, 'by', null=True)
submissionevent_ptr = ToOneField(SubmissionEventResource, 'submissionevent_ptr')
message = ToOneField(MessageResource, 'message', null=True)
in_reply_to = ToOneField(MessageResource, 'in_reply_to', null=True)
class Meta:
queryset = SubmissionEmailEvent.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'submissionemailevent'
filtering = {
"id": ALL,
"time": ALL,
"desc": ALL,
"msgtype": ALL,
"submission": ALL_WITH_RELATIONS,
"by": ALL_WITH_RELATIONS,
"submissionevent_ptr": ALL_WITH_RELATIONS,
"message": ALL_WITH_RELATIONS,
"in_reply_to": ALL_WITH_RELATIONS,
}
api.submit.register(SubmissionEmailEventResource())

View file

@ -1,6 +1,8 @@
import datetime
import os
import shutil
import email
from StringIO import StringIO
from pyquery import PyQuery
@ -9,17 +11,38 @@ from django.core.urlresolvers import reverse as urlreverse
import debug # pyflakes:ignore
from ietf.utils.test_utils import login_testing_unauthorized, unicontent
from ietf.utils.test_data import make_test_data
from ietf.utils.mail import outbox
from ietf.utils.test_utils import TestCase
from ietf.meeting.models import Meeting
from ietf.submit.utils import expirable_submissions, expire_submission, ensure_person_email_info_exists
from ietf.person.models import Person, Email
from ietf.group.models import Group
from ietf.doc.models import Document, DocAlias, DocEvent, State, BallotDocEvent, BallotPositionDocEvent, DocumentAuthor
from ietf.submit.models import Submission, Preapproval
from ietf.group.models import Group
from ietf.group.utils import setup_default_community_list_for_group
from ietf.meeting.models import Meeting
from ietf.message.models import Message
from ietf.person.models import Person, Email
from ietf.submit.models import Submission, Preapproval
from ietf.submit.mail import add_submission_email, process_response_email
from ietf.utils.mail import outbox, empty_outbox
from ietf.utils.test_data import make_test_data
from ietf.utils.test_utils import login_testing_unauthorized, unicontent, TestCase
def submission_file(name, rev, group, format, templatename):
# construct appropriate text draft
f = open(os.path.join(settings.BASE_DIR, "submit", templatename))
template = f.read()
f.close()
submission_text = template % dict(
date=datetime.date.today().strftime("%d %B %Y"),
expiration=(datetime.date.today() + datetime.timedelta(days=100)).strftime("%d %B, %Y"),
year=datetime.date.today().strftime("%Y"),
month=datetime.date.today().strftime("%B"),
name="%s-%s" % (name, rev),
group=group or "",
)
file = StringIO(str(submission_text))
file.name = "%s-%s.%s" % (name, rev, format)
return file
class SubmitTests(TestCase):
def setUp(self):
@ -70,31 +93,12 @@ class SubmitTests(TestCase):
settings.YANG_INVAL_MODEL_DIR = self.saved_yang_inval_model_dir
def submission_file(self, name, rev, group, format, templatename):
# construct appropriate text draft
f = open(os.path.join(settings.BASE_DIR, "submit", templatename))
template = f.read()
f.close()
submission_text = template % dict(
date=datetime.date.today().strftime("%d %B %Y"),
expiration=(datetime.date.today() + datetime.timedelta(days=100)).strftime("%d %B, %Y"),
year=datetime.date.today().strftime("%Y"),
month=datetime.date.today().strftime("%B"),
name="%s-%s" % (name, rev),
group=group or "",
)
file = StringIO(str(submission_text))
file.name = "%s-%s.%s" % (name, rev, format)
return file
def do_submission(self, name, rev, group=None, formats=["txt",]):
# break early in case of missing configuration
self.assertTrue(os.path.exists(settings.IDSUBMIT_IDNITS_BINARY))
# get
url = urlreverse('submit_upload_submission')
url = urlreverse('ietf.submit.views.upload_submission')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
@ -104,7 +108,7 @@ class SubmitTests(TestCase):
# submit
files = {}
for format in formats:
files[format] = self.submission_file(name, rev, group, format, "test_submission.%s" % format)
files[format] = submission_file(name, rev, group, format, "test_submission.%s" % format)
r = self.client.post(url, files)
if r.status_code != 302:
@ -346,6 +350,12 @@ class SubmitTests(TestCase):
r = self.client.post(confirm_url)
self.assertEqual(r.status_code, 302)
# check we have document events
doc_events = draft.docevent_set.filter(type="added_comment")
edescs = '::'.join([x.desc for x in doc_events])
self.assertTrue('New version approved' in edescs)
self.assertTrue('Uploaded new revision' in edescs)
draft = Document.objects.get(docalias__name=name)
self.assertEqual(draft.rev, rev)
self.assertEqual(draft.group.acronym, name.split("-")[2])
@ -635,12 +645,12 @@ class SubmitTests(TestCase):
self.do_submission(name, rev)
# search status page
r = self.client.get(urlreverse("submit_search_submission"))
r = self.client.get(urlreverse("ietf.submit.views.search_submission"))
self.assertEqual(r.status_code, 200)
self.assertTrue("submission status" in unicontent(r))
# search
r = self.client.post(urlreverse("submit_search_submission"), dict(name=name))
r = self.client.post(urlreverse("ietf.submit.views.search_submission"), dict(name=name))
self.assertEqual(r.status_code, 302)
unprivileged_status_url = r['Location']
@ -680,7 +690,7 @@ class SubmitTests(TestCase):
self.do_submission(name, rev)
submission = Submission.objects.get(name=name)
url = urlreverse('submit_submission_status', kwargs=dict(submission_id=submission.pk))
url = urlreverse('ietf.submit.views.submission_status', kwargs=dict(submission_id=submission.pk))
# check we got request full URL button
r = self.client.get(url)
@ -752,17 +762,17 @@ class SubmitTests(TestCase):
self.assertEqual(s.state_id, "cancel")
def test_help_pages(self):
r = self.client.get(urlreverse("submit_note_well"))
r = self.client.get(urlreverse("ietf.submit.views.note_well"))
self.assertEquals(r.status_code, 200)
r = self.client.get(urlreverse("submit_tool_instructions"))
r = self.client.get(urlreverse("ietf.submit.views.tool_instructions"))
self.assertEquals(r.status_code, 200)
def test_blackout_access(self):
make_test_data()
# get
url = urlreverse('submit_upload_submission')
url = urlreverse('ietf.submit.views.upload_submission')
# set meeting to today so we're in blackout period
meeting = Meeting.get_current_meeting()
meeting.date = datetime.datetime.utcnow()
@ -792,7 +802,7 @@ class SubmitTests(TestCase):
self.assertTrue(os.path.exists(settings.IDSUBMIT_IDNITS_BINARY))
# get
url = urlreverse('submit_upload_submission')
url = urlreverse('ietf.submit.views.upload_submission')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
@ -800,7 +810,7 @@ class SubmitTests(TestCase):
# submit
files = {}
for format in formats:
files[format] = self.submission_file(name, rev, group, "bad", "test_submission.bad")
files[format] = submission_file(name, rev, group, "bad", "test_submission.bad")
r = self.client.post(url, files)
@ -840,7 +850,7 @@ class ApprovalsTestCase(TestCase):
def test_approvals(self):
make_test_data()
url = urlreverse('submit_approvals')
url = urlreverse('ietf.submit.views.approvals')
self.client.login(username="marschairman", password="marschairman+password")
Preapproval.objects.create(name="draft-ietf-mars-foo", by=Person.objects.get(user__username="marschairman"))
@ -871,7 +881,7 @@ class ApprovalsTestCase(TestCase):
def test_add_preapproval(self):
make_test_data()
url = urlreverse('submit_add_preapproval')
url = urlreverse('ietf.submit.views.add_preapproval')
login_testing_unauthorized(self, "marschairman", url)
# get
@ -898,7 +908,7 @@ class ApprovalsTestCase(TestCase):
preapproval = Preapproval.objects.create(name="draft-ietf-mars-foo", by=Person.objects.get(user__username="marschairman"))
url = urlreverse('submit_cancel_preapproval', kwargs=dict(preapproval_id=preapproval.pk))
url = urlreverse('ietf.submit.views.cancel_preapproval', kwargs=dict(preapproval_id=preapproval.pk))
login_testing_unauthorized(self, "marschairman", url)
# get
@ -912,3 +922,450 @@ class ApprovalsTestCase(TestCase):
self.assertEqual(r.status_code, 302)
self.assertEqual(len(Preapproval.objects.filter(name=preapproval.name)), 0)
class ManualPostsTestCase(TestCase):
def test_manual_posts(self):
make_test_data()
url = urlreverse('ietf.submit.views.manualpost')
# Secretariat has access
self.client.login(username="secretary", password="secretary+password")
Submission.objects.create(name="draft-ietf-mars-foo",
group=Group.objects.get(acronym="mars"),
submission_date=datetime.date.today(),
state_id="manual")
Submission.objects.create(name="draft-ietf-mars-bar",
group=Group.objects.get(acronym="mars"),
submission_date=datetime.date.today(),
rev="00",
state_id="grp-appr")
# get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('.submissions a:contains("draft-ietf-mars-foo")')), 1)
self.assertEqual(len(q('.submissions a:contains("draft-ietf-mars-bar")')), 0)
def test_waiting_for_draft(self):
message_string = """To: somebody@ietf.org
From: joe@test.com
Date: {}
Subject: test submission via email
Please submit my draft at http://test.com/mydraft.txt
Thank you
""".format(datetime.datetime.now().ctime())
message = email.message_from_string(message_string)
submission, submission_email_event = (
add_submission_email(request=None,
remote_ip ="192.168.0.1",
name = "draft-my-new-draft",
rev='00',
submission_pk=None,
message = message,
by = Person.objects.get(name="(System)"),
msgtype = "msgin") )
url = urlreverse('ietf.submit.views.manualpost')
# Secretariat has access
self.client.login(username="secretary", password="secretary+password")
# get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('.waiting-for-draft a:contains("draft-my-new-draft")')), 1)
# Same name should raise an error
with self.assertRaises(Exception):
add_submission_email(request=None,
remote_ip ="192.168.0.1",
name = "draft-my-new-draft",
rev='00',
submission_pk=None,
message = message,
by = Person.objects.get(name="(System)"),
msgtype = "msgin")
# Cancel this one
r = self.client.post(urlreverse("ietf.submit.views.cancel_waiting_for_draft"), {
"submission_id": submission.pk,
"access_token": submission.access_token(),
})
self.assertEqual(r.status_code, 302)
url = r["Location"]
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('.waiting-for-draft a:contains("draft-my-new-draft")')), 0)
# Should now be able to add it again
submission, submission_email_event = (
add_submission_email(request=None,
remote_ip ="192.168.0.1",
name = "draft-my-new-draft",
rev='00',
submission_pk=None,
message = message,
by = Person.objects.get(name="(System)"),
msgtype = "msgin") )
def test_waiting_for_draft_with_attachment(self):
frm = "joe@test.com"
message_string = """To: somebody@ietf.org
From: {}
Date: {}
Subject: A very important message with a small attachment
Content-Type: multipart/mixed; boundary="------------090908050800030909090207"
This is a multi-part message in MIME format.
--------------090908050800030909090207
Content-Type: text/plain; charset=utf-8; format=flowed
Content-Transfer-Encoding: 7bit
The message body will probably say something about the attached document
--------------090908050800030909090207
Content-Type: text/plain; charset=UTF-8; name="attach.txt"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="attach.txt"
QW4gZXhhbXBsZSBhdHRhY2htZW50IHd0aG91dCB2ZXJ5IG11Y2ggaW4gaXQuCgpBIGNvdXBs
ZSBvZiBsaW5lcyAtIGJ1dCBpdCBjb3VsZCBiZSBhIGRyYWZ0Cg==
--------------090908050800030909090207--
""".format(frm, datetime.datetime.now().ctime())
message = email.message_from_string(message_string)
submission, submission_email_event = (
add_submission_email(request=None,
remote_ip ="192.168.0.1",
name = "draft-my-new-draft",
rev='00',
submission_pk=None,
message = message,
by = Person.objects.get(name="(System)"),
msgtype = "msgin") )
manualpost_page_url = urlreverse('ietf.submit.views.manualpost')
# Secretariat has access
self.client.login(username="secretary", password="secretary+password")
self.check_manualpost_page(submission=submission,
submission_email_event=submission_email_event,
the_url=manualpost_page_url,
submission_name_fragment='draft-my-new-draft',
frm=frm,
is_secretariat=True)
# Try the status page with no credentials
self.client.logout()
self.check_manualpost_page(submission=submission,
submission_email_event=submission_email_event,
the_url=manualpost_page_url,
submission_name_fragment='draft-my-new-draft',
frm=frm,
is_secretariat=False)
# Post another message to this submission using the link
message_string = """To: somebody@ietf.org
From: joe@test.com
Date: {}
Subject: A new submission message with a small attachment
Content-Type: multipart/mixed; boundary="------------090908050800030909090207"
This is a multi-part message in MIME format.
--------------090908050800030909090207
Content-Type: text/plain; charset=utf-8; format=flowed
Content-Transfer-Encoding: 7bit
The message body will probably say something more about the attached document
--------------090908050800030909090207
Content-Type: text/plain; charset=UTF-8; name="attach.txt"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="attachment.txt"
QW4gZXhhbXBsZSBhdHRhY2htZW50IHd0aG91dCB2ZXJ5IG11Y2ggaW4gaXQuCgpBIGNvdXBs
ZSBvZiBsaW5lcyAtIGJ1dCBpdCBjb3VsZCBiZSBhIGRyYWZ0Cg==
--------------090908050800030909090207--
""".format(datetime.datetime.now().ctime())
# Back to secretariat
self.client.login(username="secretary", password="secretary+password")
r, q = self.request_and_parse(manualpost_page_url)
url = self.get_href(q, "a#new-submission-email:contains('New submission from email')")
# Get the form
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
#self.assertEqual(len(q('input[name=edit-title]')), 1)
# Post the new message
r = self.client.post(url, {
"name": "draft-my-next-new-draft-00",
"direction": "incoming",
"message": message_string,
})
if r.status_code != 302:
q = PyQuery(r.content)
print q
self.assertEqual(r.status_code, 302)
#self.check_manualpost_page(submission, submission_email_event,
# url, 'draft-my-next-new-draft'
# 'Another very important message',
# true)
def check_manualpost_page(self, submission, submission_email_event,
the_url, submission_name_fragment,
frm,
is_secretariat):
# get the page listing manual posts
r, q = self.request_and_parse(the_url)
selector = "#waiting-for-draft a#add-submission-email%s:contains('Add email')" % submission.pk
if is_secretariat:
# Can add an email to the submission
add_email_url = self.get_href(q, selector)
else:
# No add email button button
self.assertEqual(len(q(selector)), 0)
# Find the link for our submission in those awaiting drafts
submission_url = self.get_href(q, "#waiting-for-draft a#aw{}:contains({})".
format(submission.pk, submission_name_fragment))
# Follow the link to the status page for this submission
r, q = self.request_and_parse(submission_url)
selector = "#history a#reply%s:contains('Reply')" % submission.pk
if is_secretariat:
# check that reply button is visible and get the form
reply_url = self.get_href(q, selector)
# Get the form
r = self.client.get(reply_url)
self.assertEqual(r.status_code, 200)
reply_q = PyQuery(r.content)
self.assertEqual(len(reply_q('input[name=to]')), 1)
else:
# No reply button
self.assertEqual(len(q(selector)), 0)
if is_secretariat:
# Now try to send an email using the send email link
selector = "a#send%s:contains('Send Email')" % submission.pk
send_url = self.get_href(q, selector)
self.do_submission_email(the_url = send_url,
to = frm,
body = "A new message")
# print q
# print submission.pk
# print submission_email_event.pk
# Find the link for our message in the list
url = self.get_href(q, "#aw{}-{}:contains('{}')".format(submission.pk,
submission_email_event.message.pk,
"Received message - manual post"))
# Page displaying message details
r, q = self.request_and_parse(url)
if is_secretariat:
# check that reply button is visible
reply_href = self.get_href(q, "#email-details a#reply%s:contains('Reply')" % submission.pk)
else:
# No reply button
self.assertEqual(len(q(selector)), 0)
reply_href = None
# check that attachment link is visible
url = self.get_href(q, "#email-details a#attach{}:contains('attach.txt')".format(submission.pk))
# Fetch the attachment
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# Attempt a reply if we can
if reply_href == None:
return
self.do_submission_email(the_url = reply_href,
to = frm,
body = "A reply to the message")
# try adding an email to the submission
# Use the add email link from the manual post listing page
if is_secretariat:
# Can add an email to the submission
# add_email_url set previously
r = self.client.get(add_email_url)
self.assertEqual(r.status_code, 200)
add_email_q = PyQuery(r.content)
self.assertEqual(len(add_email_q('input[name=submission_pk]')), 1)
# Add a simple email
new_message_string = """To: somebody@ietf.org
From: joe@test.com
Date: {}
Subject: Another message
About my submission
Thank you
""".format(datetime.datetime.now().ctime())
r = self.client.post(add_email_url, {
"name": "{}-{}".format(submission.name, submission.rev),
"direction": "incoming",
"submission_pk": submission.pk,
"message": new_message_string,
})
if r.status_code != 302:
q = PyQuery(r.content)
print q
self.assertEqual(r.status_code, 302)
def request_and_parse(self, url):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
return r, PyQuery(r.content)
def get_href(self, q, query):
link = q(query)
self.assertEqual(len(link), 1)
return PyQuery(link[0]).attr('href')
def do_submission_email(self, the_url, to, body):
# check the page
r = self.client.get(the_url)
q = PyQuery(r.content)
post_button = q('[type=submit]:contains("Send Email")')
self.assertEqual(len(post_button), 1)
action = post_button.parents("form").find('input[type=hidden][name="action"]').val()
subject = post_button.parents("form").find('input[name="subject"]').val()
frm = post_button.parents("form").find('input[name="frm"]').val()
cc = post_button.parents("form").find('input[name="cc"]').val()
reply_to = post_button.parents("form").find('input[name="reply_to"]').val()
empty_outbox()
# post submitter info
r = self.client.post(the_url, {
"action": action,
"subject": subject,
"frm": frm,
"to": to,
"cc": cc,
"reply_to": reply_to,
"body": body,
})
self.assertEqual(r.status_code, 302)
self.assertEqual(len(outbox), 1)
outmsg = outbox[0]
self.assertTrue(to in outmsg['To'])
reply_to = outmsg['Reply-to']
self.assertIsNotNone(reply_to, "Expected Reply-to")
# Build a reply
message_string = """To: {}
From: {}
Date: {}
Subject: test
""".format(reply_to, to, datetime.datetime.now().ctime())
result = process_response_email(message_string)
self.assertIsInstance(result, Message)
return r
def do_submission(self, name, rev, group=None, formats=["txt",]):
# We're not testing the submission process - just the submission status
# get
url = urlreverse('ietf.submit.views.upload_submission')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('input[type=file][name=txt]')), 1)
self.assertEqual(len(q('input[type=file][name=xml]')), 1)
# submit
files = {}
for format in formats:
files[format] = submission_file(name, rev, group, format, "test_submission.%s" % format)
r = self.client.post(url, files)
if r.status_code != 302:
q = PyQuery(r.content)
print(q('div.has-error span.help-block div').text)
self.assertEqual(r.status_code, 302)
status_url = r["Location"]
for format in formats:
self.assertTrue(os.path.exists(os.path.join(self.staging_dir, u"%s-%s.%s" % (name, rev, format))))
self.assertEqual(Submission.objects.filter(name=name).count(), 1)
submission = Submission.objects.get(name=name)
self.assertTrue(all([ c.passed!=False for c in submission.checks.all() ]))
self.assertEqual(len(submission.authors_parsed()), 1)
author = submission.authors_parsed()[0]
self.assertEqual(author["name"], "Author Name")
self.assertEqual(author["email"], "author@example.com")
return status_url
def supply_extra_metadata(self, name, status_url, submitter_name, submitter_email):
# check the page
r = self.client.get(status_url)
q = PyQuery(r.content)
post_button = q('[type=submit]:contains("Post")')
self.assertEqual(len(post_button), 1)
action = post_button.parents("form").find('input[type=hidden][name="action"]').val()
# post submitter info
r = self.client.post(status_url, {
"action": action,
"submitter-name": submitter_name,
"submitter-email": submitter_email,
"approvals_received": True,
})
if r.status_code == 302:
submission = Submission.objects.get(name=name)
self.assertEqual(submission.submitter, u"%s <%s>" % (submitter_name, submitter_email))
return r

View file

@ -1,18 +1,29 @@
from django.conf.urls import patterns, url
from django.conf.urls import patterns
from ietf.submit import views
urlpatterns = patterns('ietf.submit.views',
url(r'^$', 'upload_submission', name='submit_upload_submission'),
url(r'^status/$', 'search_submission', name='submit_search_submission'),
url(r'^status/(?P<submission_id>\d+)/$', 'submission_status', name='submit_submission_status'),
url(r'^status/(?P<submission_id>\d+)/edit/$', 'edit_submission', name='submit_edit_submission'),
url(r'^status/(?P<submission_id>\d+)/confirm/(?P<auth_token>[a-f\d]+)/$', 'confirm_submission', name='submit_confirm_submission'),
url(r'^status/(?P<submission_id>\d+)/(?P<access_token>[a-f\d]*)/$', 'submission_status', name='submit_submission_status_by_hash'),
url(r'^status/(?P<submission_id>\d+)/(?P<access_token>[a-f\d]+)/edit/$', 'edit_submission', name='submit_edit_submission_by_hash'),
url(r'^note-well/$', 'note_well', name='submit_note_well'),
url(r'^tool-instructions/$', 'tool_instructions', name='submit_tool_instructions'),
(r'^$', views.upload_submission),
(r'^status/$', views.search_submission),
(r'^status/(?P<submission_id>\d+)/$', views.submission_status),
(r'^status/(?P<submission_id>\d+)/(?P<access_token>[a-f\d]*)/$', views.submission_status),
(r'^status/(?P<submission_id>\d+)/confirm/(?P<auth_token>[a-f\d]+)/$', views.confirm_submission),
(r'^status/(?P<submission_id>\d+)/edit/$', views.edit_submission),
(r'^status/(?P<submission_id>\d+)/(?P<access_token>[a-f\d]+)/edit/$', views.edit_submission),
(r'^note-well/$', views.note_well),
(r'^tool-instructions/$', views.tool_instructions),
url(r'^approvals/$', 'approvals', name='submit_approvals'),
url(r'^approvals/addpreapproval/$', 'add_preapproval', name='submit_add_preapproval'),
url(r'^approvals/cancelpreapproval/(?P<preapproval_id>[a-f\d]+)/$', 'cancel_preapproval', name='submit_cancel_preapproval'),
(r'^approvals/$', views.approvals),
(r'^approvals/addpreapproval/$', views.add_preapproval),
(r'^approvals/cancelpreapproval/(?P<preapproval_id>[a-f\d]+)/$', views.cancel_preapproval),
(r'^manualpost/$', views.manualpost),
(r'^manualpost/addemail$', views.add_manualpost_email),
(r'^manualpost/addemail/(?P<submission_id>\d+)/(?P<access_token>[a-f\d]*)/$', views.add_manualpost_email),
(r'^manualpost/attachment/(?P<submission_id>\d+)/(?P<message_id>\d+)/(?P<filename>.*)$', views.show_submission_email_attachment),
(r'^manualpost/cancel$', views.cancel_waiting_for_draft),
(r'^manualpost/email/(?P<submission_id>\d+)/(?P<message_id>\d+)/$', views.show_submission_email_message),
(r'^manualpost/email/(?P<submission_id>\d+)/(?P<message_id>\d+)/(?P<access_token>[a-f\d]*)/$', views.show_submission_email_message),
(r'^manualpost/replyemail/(?P<submission_id>\d+)/(?P<message_id>\d+)/$', views.send_submission_email),
(r'^manualpost/sendemail/(?P<submission_id>\d+)/$', views.send_submission_email),
)

View file

@ -3,7 +3,8 @@ import datetime
from django.conf import settings
from ietf.doc.models import Document, State, DocAlias, DocEvent, DocumentAuthor
from ietf.doc.models import ( Document, State, DocAlias, DocEvent,
DocumentAuthor, AddedMessageEvent )
from ietf.doc.models import NewRevisionDocEvent
from ietf.doc.models import RelatedDocument, DocRelationshipName
from ietf.doc.utils import add_state_change_event, rebuild_reference_relations
@ -19,6 +20,7 @@ from ietf.submit.models import Submission, SubmissionEvent, Preapproval, DraftSu
from ietf.utils import unaccent
from ietf.utils.log import log
def validate_submission(submission):
errors = {}
@ -108,8 +110,62 @@ def create_submission_event(request, submission, desc):
SubmissionEvent.objects.create(submission=submission, by=by, desc=desc)
def post_submission(request, submission):
# find out who did it
def docevent_from_submission(request, submission, desc):
system = Person.objects.get(name="(System)")
try:
draft = Document.objects.get(name=submission.name)
except Document.DoesNotExist:
# Assume this is revision 00 - we'll do this later
return
submitter_parsed = submission.submitter_parsed()
if submitter_parsed["name"] and submitter_parsed["email"]:
submitter = ensure_person_email_info_exists(submitter_parsed["name"], submitter_parsed["email"]).person
else:
submitter = system
e = DocEvent(doc=draft)
e.by = submitter
e.type = "added_comment"
e.desc = desc
e.save()
def post_rev00_submission_events(draft, submission, submitter):
# Add previous submission events as docevents
# For now we'll filter based on the description
for subevent in submission.submissionevent_set.all():
desc = subevent.desc
if desc.startswith("Uploaded submission"):
desc = "Uploaded new revision"
e = DocEvent(type="added_comment", doc=draft)
elif desc.startswith("Submission created"):
e = DocEvent(type="added_comment", doc=draft)
elif desc.startswith("Set submitter to"):
pos = subevent.desc.find("sent confirmation email")
e = DocEvent(type="added_comment", doc=draft)
if pos > 0:
desc = "Request for posting confirmation emailed %s" % (subevent.desc[pos + 23:])
else:
pos = subevent.desc.find("sent appproval email")
if pos > 0:
desc = "Request for posting approval emailed %s" % (subevent.desc[pos + 19:])
elif desc.startswith("Received message") or desc.startswith("Sent message"):
e = AddedMessageEvent(type="added_message", doc=draft)
e.message = subevent.submissionemailevent.message
e.msgtype = subevent.submissionemailevent.msgtype
e.in_reply_to = subevent.submissionemailevent.in_reply_to
else:
continue
e.time = subevent.time #submission.submission_date
e.by = submitter
e.desc = desc
e.save()
def post_submission(request, submission, approvedDesc):
system = Person.objects.get(name="(System)")
submitter_parsed = submission.submitter_parsed()
if submitter_parsed["name"] and submitter_parsed["email"]:
@ -175,8 +231,29 @@ def post_submission(request, submission):
trouble = rebuild_reference_relations(draft, filename=os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.txt' % (submission.name, submission.rev)))
if trouble:
log('Rebuild_reference_relations trouble: %s'%trouble)
if draft.rev == '00':
# Add all the previous submission events as docevents
post_rev00_submission_events(draft, submission, submitter)
# Add an approval docevent
e = DocEvent(type="added_comment", doc=draft)
e.time = draft.time #submission.submission_date
e.by = submitter
e.desc = approvedDesc
e.save()
# new revision event
e = NewRevisionDocEvent(type="new_revision", doc=draft, rev=draft.rev)
e.time = draft.time #submission.submission_date
e.by = submitter
e.desc = "New version available: <b>%s-%s.txt</b>" % (draft.name, draft.rev)
e.save()
if draft.stream_id == "ietf" and draft.group.type_id == "wg" and draft.rev == "00":
# automatically set state "WG Document"
draft.set_state(State.objects.get(used=True, type="draft-stream-%s" % draft.stream_id, slug="wg-doc"))
# automatic state changes
if draft.get_state_slug("draft-iana-review") in ("ok-act", "ok-noact", "not-ok"):
prev_state = draft.get_state("draft-iana-review")
next_state = State.objects.get(used=True, type="draft-iana-review", slug="changed")
@ -435,4 +512,4 @@ def expire_submission(submission, by):
submission.state_id = "cancel"
submission.save()
SubmissionEvent.objects.create(submission=submission, by=by, desc="Canceled expired submission")
SubmissionEvent.objects.create(submission=submission, by=by, desc="Cancelled expired submission")

View file

@ -1,31 +1,40 @@
# Copyright The IETF Trust 2007, All Rights Reserved
import base64
import datetime
import os
import xml2rfc
from django.conf import settings
from django.contrib import messages
from django.core.urlresolvers import reverse as urlreverse
from django.core.validators import validate_email, ValidationError
from django.http import HttpResponseRedirect, Http404, HttpResponseForbidden
from django.http import HttpResponseRedirect, Http404, HttpResponseForbidden, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.module_loading import import_string
import debug # pyflakes:ignore
from ietf.doc.models import Document, DocAlias
from ietf.doc.models import Document, DocAlias, AddedMessageEvent
from ietf.doc.utils import prettify_std_name
from ietf.group.models import Group
from ietf.ietfauth.utils import has_role, role_required
from ietf.submit.forms import SubmissionUploadForm, NameEmailForm, EditSubmissionForm, PreapprovalForm, ReplacesForm
from ietf.submit.mail import send_full_url, send_approval_request_to_group, send_submission_confirmation, send_manual_post_request
from ietf.submit.models import Submission, SubmissionCheck, Preapproval, DraftSubmissionStateName
from ietf.submit.utils import approvable_submissions_for_user, preapprovals_for_user, recently_approved_by_user
from ietf.mailtrigger.utils import gather_address_lists
from ietf.message.models import Message, MessageAttachment
from ietf.submit.forms import ( SubmissionUploadForm, NameEmailForm, EditSubmissionForm,
PreapprovalForm, ReplacesForm, SubmissionEmailForm, MessageModelForm )
from ietf.submit.mail import ( send_full_url, send_approval_request_to_group,
send_submission_confirmation, send_manual_post_request, add_submission_email, get_reply_to )
from ietf.submit.models import (Submission, SubmissionCheck, Preapproval,
DraftSubmissionStateName, SubmissionEmailEvent )
from ietf.submit.utils import ( approvable_submissions_for_user, preapprovals_for_user,
recently_approved_by_user )
from ietf.submit.utils import validate_submission, create_submission_event
from ietf.submit.utils import docevent_from_submission
from ietf.submit.utils import post_submission, cancel_submission, rename_submission_files
from ietf.utils.accesstoken import generate_random_key, generate_access_token
from ietf.utils.draft import Draft
from ietf.utils.log import log
from ietf.mailtrigger.utils import gather_address_lists
from ietf.utils.mail import send_mail_message
def upload_submission(request):
@ -93,29 +102,59 @@ def upload_submission(request):
else:
abstract = form.parsed_draft.get_abstract()
# save submission
try:
submission = Submission.objects.create(
state=DraftSubmissionStateName.objects.get(slug="uploaded"),
remote_ip=form.remote_ip,
name=form.filename,
group=form.group,
title=form.title,
abstract=abstract,
rev=form.revision,
pages=form.parsed_draft.get_pagecount(),
authors="\n".join(authors),
note="",
first_two_pages=''.join(form.parsed_draft.pages[:2]),
file_size=file_size,
file_types=','.join(form.file_types),
submission_date=datetime.date.today(),
document_date=form.parsed_draft.get_creation_date(),
replaces="",
# See if there is a Submission in state waiting-for-draft
# for this revision.
# If so - we're going to update it otherwise we create a new object
submission = Submission.objects.filter(name=form.filename,
rev=form.revision,
state_id = "waiting-for-draft").distinct()
if (len(submission) == 0):
submission = None
elif (len(submission) == 1):
submission = submission[0]
submission.state = DraftSubmissionStateName.objects.get(slug="uploaded")
submission.remote_ip=form.remote_ip
submission.title=form.title
submission.abstract=abstract
submission.rev=form.revision
submission.pages=form.parsed_draft.get_pagecount()
submission.authors="\n".join(authors)
submission.first_two_pages=''.join(form.parsed_draft.pages[:2])
submission.file_size=file_size
submission.file_types=','.join(form.file_types)
submission.submission_date=datetime.date.today()
submission.document_date=form.parsed_draft.get_creation_date()
submission.replaces=""
submission.save()
else:
raise Exception("Multiple submissions found waiting for upload")
if (submission == None):
try:
submission = Submission.objects.create(
state=DraftSubmissionStateName.objects.get(slug="uploaded"),
remote_ip=form.remote_ip,
name=form.filename,
group=form.group,
title=form.title,
abstract=abstract,
rev=form.revision,
pages=form.parsed_draft.get_pagecount(),
authors="\n".join(authors),
note="",
first_two_pages=''.join(form.parsed_draft.pages[:2]),
file_size=file_size,
file_types=','.join(form.file_types),
submission_date=datetime.date.today(),
document_date=form.parsed_draft.get_creation_date(),
replaces="",
)
except Exception as e:
log("Exception: %s\n" % e)
raise
except Exception as e:
log("Exception: %s\n" % e)
raise
# run submission checkers
def apply_check(submission, checker, method, fn):
@ -135,8 +174,9 @@ def upload_submission(request):
break
create_submission_event(request, submission, desc="Uploaded submission")
docevent_from_submission(request, submission, desc="Uploaded new revision")
return redirect("submit_submission_status_by_hash", submission_id=submission.pk, access_token=submission.access_token())
return redirect("ietf.submit.views.submission_status", submission_id=submission.pk, access_token=submission.access_token())
except IOError as e:
if "read error" in str(e): # The server got an IOError when trying to read POST data
form = SubmissionUploadForm(request=request)
@ -197,9 +237,14 @@ def submission_status(request, submission_id, access_token=None):
can_edit = can_edit_submission(request.user, submission, access_token) and submission.state_id == "uploaded"
can_cancel = (key_matched or is_secretariat) and submission.state.next_states.filter(slug="cancel")
can_group_approve = (is_secretariat or is_chair) and submission.state_id == "grp-appr"
can_force_post = is_secretariat and submission.state.next_states.filter(slug="posted")
show_send_full_url = not key_matched and not is_secretariat and submission.state_id not in ("cancel", "posted")
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") )
addrs = gather_address_lists('sub_confirmation_requested',submission=submission)
confirmation_list = addrs.to
confirmation_list.extend(addrs.cc)
@ -218,7 +263,7 @@ def submission_status(request, submission_id, access_token=None):
message = None
if submission.state_id == "cancel":
message = ('error', 'This submission has been canceled, modification is no longer possible.')
message = ('error', 'This submission has been cancelled, modification is no longer possible.')
elif submission.state_id == "auth":
message = ('success', u'The submission is pending email authentication. An email has been sent to: %s' % ", ".join(confirmation_list))
elif submission.state_id == "grp-appr":
@ -244,45 +289,60 @@ def submission_status(request, submission_id, access_token=None):
replaces = replaces_form.cleaned_data.get("replaces", [])
submission.replaces = ",".join(o.name for o in replaces)
if requires_group_approval:
submission.state = DraftSubmissionStateName.objects.get(slug="grp-appr")
submission.save()
sent_to = send_approval_request_to_group(request, submission)
desc = "sent approval email to group chairs: %s" % u", ".join(sent_to)
approvals_received = submitter_form.cleaned_data['approvals_received']
if approvals_received:
if not is_secretariat:
return HttpResponseForbidden('You do not have permission to perform this action')
# go directly to posting submission
desc = u"Secretariat manually posting. Approvals already received"
post_submission(request, submission, desc)
create_submission_event(request, submission, desc)
else:
submission.auth_key = generate_random_key()
if requires_prev_authors_approval:
submission.state = DraftSubmissionStateName.objects.get(slug="aut-appr")
if requires_group_approval:
submission.state = DraftSubmissionStateName.objects.get(slug="grp-appr")
submission.save()
sent_to = send_approval_request_to_group(request, submission)
desc = "sent approval email to group chairs: %s" % u", ".join(sent_to)
docDesc = u"Request for posting approval emailed to group chairs: %s" % u", ".join(sent_to)
else:
submission.state = DraftSubmissionStateName.objects.get(slug="auth")
submission.save()
submission.auth_key = generate_random_key()
if requires_prev_authors_approval:
submission.state = DraftSubmissionStateName.objects.get(slug="aut-appr")
else:
submission.state = DraftSubmissionStateName.objects.get(slug="auth")
submission.save()
sent_to = send_submission_confirmation(request, submission, chair_notice=group_authors_changed)
if submission.state_id == "aut-appr":
desc = u"sent confirmation email to previous authors: %s" % u", ".join(sent_to)
else:
desc = u"sent confirmation email to submitter and authors: %s" % u", ".join(sent_to)
msg = u"Set submitter to \"%s\", replaces to %s and %s" % (
submission.submitter,
", ".join(prettify_std_name(r.name) for r in replaces) if replaces else "(none)",
desc)
create_submission_event(request, submission, msg)
sent_to = send_submission_confirmation(request, submission, chair_notice=group_authors_changed)
if submission.state_id == "aut-appr":
desc = u"sent confirmation email to previous authors: %s" % u", ".join(sent_to)
docDesc = "Request for posting confirmation emailed to previous authors: %s" % u", ".join(sent_to)
else:
desc = u"sent confirmation email to submitter and authors: %s" % u", ".join(sent_to)
docDesc = "Request for posting confirmation emailed to submitter and authors: %s" % u", ".join(sent_to)
msg = u"Set submitter to \"%s\", replaces to %s and %s" % (
submission.submitter,
", ".join(prettify_std_name(r.name) for r in replaces) if replaces else "(none)",
desc)
create_submission_event(request, submission, msg)
docevent_from_submission(request, submission, docDesc)
if access_token:
return redirect("submit_submission_status_by_hash", submission_id=submission.pk, access_token=access_token)
return redirect("ietf.submit.views.submission_status", submission_id=submission.pk, access_token=access_token)
else:
return redirect("submit_submission_status", submission_id=submission.pk)
return redirect("ietf.submit.views.submission_status", submission_id=submission.pk)
elif action == "edit" and submission.state_id == "uploaded":
if access_token:
return redirect("submit_edit_submission_by_hash", submission_id=submission.pk, access_token=access_token)
return redirect("ietf.submit.views.edit_submission", submission_id=submission.pk, access_token=access_token)
else:
return redirect("submit_edit_submission", submission_id=submission.pk)
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)
@ -297,16 +357,16 @@ def submission_status(request, submission_id, access_token=None):
cancel_submission(submission)
create_submission_event(request, submission, "Canceled submission")
create_submission_event(request, submission, "Cancelled submission")
return redirect("submit_submission_status", submission_id=submission_id)
return redirect("ietf.submit.views.submission_status", submission_id=submission_id)
elif action == "approve" and submission.state_id == "grp-appr":
if not can_group_approve:
return HttpResponseForbidden('You do not have permission to perform this action')
post_submission(request, submission)
post_submission(request, submission, "WG -00 approved")
create_submission_event(request, submission, "Approved and posted submission")
@ -317,13 +377,13 @@ def submission_status(request, submission_id, access_token=None):
if not can_force_post:
return HttpResponseForbidden('You do not have permission to perform this action')
post_submission(request, submission)
if submission.state_id == "manual":
desc = "Posted submission manually"
else:
desc = "Forced post of submission"
post_submission(request, submission, desc)
create_submission_event(request, submission, desc)
return redirect("doc_view", name=submission.name)
@ -409,7 +469,7 @@ def edit_submission(request, submission_id, access_token=None):
create_submission_event(request, submission, desc)
return redirect("submit_submission_status", submission_id=submission.pk)
return redirect("ietf.submit.views.submission_status", submission_id=submission.pk)
else:
form_errors = True
else:
@ -439,7 +499,16 @@ def confirm_submission(request, submission_id, auth_token):
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:
post_submission(request, submission)
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)
create_submission_event(request, submission, "Confirmed and posted submission")
@ -482,7 +551,7 @@ def add_preapproval(request):
p.by = request.user.person
p.save()
return HttpResponseRedirect(urlreverse("submit_approvals") + "#preapprovals")
return HttpResponseRedirect(urlreverse("ietf.submit.views.approvals") + "#preapprovals")
else:
form = PreapprovalForm()
@ -501,8 +570,271 @@ def cancel_preapproval(request, preapproval_id):
if request.method == "POST" and request.POST.get("action", "") == "cancel":
preapproval.delete()
return HttpResponseRedirect(urlreverse("submit_approvals") + "#preapprovals")
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:
return HttpResponseForbidden('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(request,
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=request.META.get('REMOTE_ADDR', None),
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

View file

@ -43,12 +43,12 @@
</a>
<ul class="dropdown-menu" role="menu">
{% endif %}
<li><a href="{% url "submit_upload_submission" %}">Submit a draft</a></li>
<li><a href="{% url "ietf.submit.views.upload_submission" %}">Draft submissions</a></li>
{% if user|has_role:"WG Chair" %}
{% if flavor == "top" %}<li class="divider hidden-xs"></li>{% endif %}
<li {%if flavor == "top" %}class="dropdown-header hidden-xs"{% else %}class="nav-header"{% endif %}>WG chair</li>
<li><a href="{% url "submit_approvals" %}">Approve a draft</a></li>
<li><a href="{% url "ietf.submit.views.approvals" %}">Approve a draft</a></li>
{% endif %}
{% if user and user.is_authenticated %}

View file

@ -46,7 +46,7 @@
{% buttons %}
<button type="submit" class="btn btn-primary">Save</button>
<a class="btn btn-default pull-right" href="{% url "submit_approvals" %}#preapprovals">Back</a>
<a class="btn btn-default pull-right" href="{% url "ietf.submit.views.approvals" %}#preapprovals">Back</a>
{% endbuttons %}
</form>

View file

@ -0,0 +1,37 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load bootstrap3 %}
{% block title %}{% if submission == None %}Add new submission request email{% else %}Add submission request email to {{ submission.name }}{% endif %}{% endblock %}
{% block content %}
{% origin %}
<h1>Add email</h1>
{% if submission == None %}
<p>
A new submission request will be created for the given name and revision. The
name must take the form "draft-xxx-nn" where xxx is lowercase letters, digits or dashes
and nn is the revision number - 00 for the initial revision. For example<br/>
&nbsp;&nbsp;&nbsp;draft-my-spec-00
</p>
{% else %}
<p>
The email will be added to the submission history for {{ submission.name }}
</p>
{% endif %}
<form class="add-email" method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-primary">Add Email</button>
<a class="btn btn-default pull-right" href="{% url "ietf.submit.views.manualpost" %}">Back</a>
{% endbuttons %}
</form>
{% endblock %}

View file

@ -4,7 +4,7 @@ Hi,
Chair approval is needed for posting of {{ submission.name }}-{{ submission.rev }}.
To approve the draft, go to this URL (note: you need to login to be able to approve):
https://{{ domain }}{% url "submit_submission_status_by_hash" submission_id=submission.pk access_token=submission.access_token %}
https://{{ domain }}{% url "ietf.submit.views.submission_status" submission_id=submission.pk access_token=submission.access_token %}
File name : {{ submission.name }}
Revision : {{ submission.rev }}

View file

@ -28,7 +28,7 @@
<tbody>
{% for s in approvals %}
<tr>
<td><a href="{% url "submit_submission_status_by_hash" submission_id=s.pk access_token=s.access_token %}">{{ s.name }}-{{ s.rev }}</a></td>
<td><a href="{% url "ietf.submit.views.submission_status" submission_id=s.pk access_token=s.access_token %}">{{ s.name }}-{{ s.rev }}</a></td>
<td>{{ s.submission_date }}</td>
</tr>
{% endfor %}
@ -39,7 +39,7 @@
<h2 class="anchor-target" id="preapprovals">Pre-approved drafts not yet submitted</h2>
{% if user|has_role:"Secretariat,WG Chair,RG Chair" %}
<p><a class="btn btn-default" href="{% url "submit_add_preapproval" %}">Add pre-approval</a></p>
<p><a class="btn btn-default" href="{% url "ietf.submit.views.add_preapproval" %}">Add pre-approval</a></p>
{% endif %}
{% if not preapprovals %}
@ -60,7 +60,7 @@
<td>{{ p.name }}</td>
<td>{{ p.time|date:"Y-m-d" }}</td>
<td>{{ p.by }}</td>
<td><a class="btn btn-danger btn-xs" href="{% url "submit_cancel_preapproval" preapproval_id=p.id %}">Cancel</a></td>
<td><a class="btn btn-danger btn-xs" href="{% url "ietf.submit.views.cancel_preapproval" preapproval_id=p.id %}">Cancel</a></td>
</tr>
{% endfor %}
</tbody>
@ -104,4 +104,4 @@
{% block js %}
<script src="{% static "jquery.tablesorter/js/jquery.tablesorter.combined.min.js" %}"></script>
{% endblock %}
{% endblock %}

View file

@ -21,7 +21,7 @@
{% buttons %}
<button type="submit" class="btn btn-danger">Cancel pre-approval</button>
<a class="btn btn-default pull-right" href="{% url "submit_approvals" %}#preapprovals">Back</a>
<a class="btn btn-default pull-right" href="{% url "ietf.submit.views.approvals" %}#preapprovals">Back</a>
{% endbuttons %}
</form>

View file

@ -14,7 +14,7 @@
{% else %}
<p>The submission is not in a state where it can be confirmed.</p>
<p>Go to the <a href="{% url "submit_submission_status" submission_id=submission.pk %}">status page</a>
<p>Go to the <a href="{% url "ietf.submit.views.submission_status" submission_id=submission.pk %}">status page</a>
to see what has happened to it.</p>
{% endif %}
{% else %}
@ -23,7 +23,7 @@
<p class="error">Incorrect authorization key.</p>
<p>Double-check the link you followed. If everything fails, you can go to
the <a href="{% url "submit_submission_status" submission_id=submission.pk %}">status page</a>,
the <a href="{% url "ietf.submit.views.submission_status" submission_id=submission.pk %}">status page</a>,
cancel the submission and try again.</p>
{% else %}
<p>Authorization key accepted.</p>

View file

@ -0,0 +1,30 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load staticfiles %}
{% load bootstrap3 %}
{% block title %}Email related to{{ submission.name }}{% endblock %}
{% block pagehead %}
<link rel="stylesheet" href="{% static 'bootstrap-datepicker/css/bootstrap-datepicker3.min.css' %}">
{% endblock %}
{% block content %}
{% origin %}
<h1>Email related to<br><small>{{ submission.name }}</small></h1>
<form method="post" class="show-required">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-primary">Send Email</button>
{% endbuttons %}
</form>
{% endblock %}
{% block js %}
<script src="{% static 'bootstrap-datepicker/js/bootstrap-datepicker.min.js' %}"></script>
{% endblock %}

View file

@ -0,0 +1,98 @@
{% extends "submit/submit_base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin staticfiles %}
{% block pagehead %}
<link rel="stylesheet" href="{% static "jquery.tablesorter/css/theme.bootstrap.min.css" %}">
{% endblock %}
{% block title %}Draft submissions awaiting manual posting{% endblock %}
{% load ietf_filters %}
{% block submit_content %}
{% origin %}
<h2 class="anchor-target" id="approvals">Submissions needing manual posting</h2>
{% if not manual %}
<p id="no-manual">There are no submissions needing manual posting.</p>
{% else %}
<table id="manual" class="submissions table table-condensed table-striped tablesorter">
<thead>
<tr>
<th>Draft</th>
<th>Submitted</th>
<th>Passes Checks</th>
<th>Metadata</th>
</tr>
</thead>
<tbody>
{% for s in manual %}
<tr>
{% if user.is_authenticated %}
<td><a href="{% url "ietf.submit.views.submission_status" submission_id=s.pk access_token=s.access_token %}">{{ s.name }}-{{ s.rev }}</a></td>
{% else %}
<td><a href="{% url "ietf.submit.views.submission_status" submission_id=s.pk %}">{{ s.name }}-{{ s.rev }}</a></td>
{% endif %}
<td>{{ s.submission_date }}</td>
<td>{% if s.passes_checks %}Ok{% else %}Fails{% endif %}</td>
<td>{% if s.errors %}Errors{% else %}Ok{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<h2 class="anchor-target" id="approvals">Submissions awaiting draft upload</h2>
{% if not waiting_for_draft %}
<p id="no-waiting-for-draft">There are no submissions awaiting draft upload.</p>
{% else %}
<table id="waiting-for-draft" class="waiting-for-draft table table-condensed table-striped tablesorter">
<thead>
<tr>
<th>Name</th>
<th>Rev</th>
<th>Submitted</th>
</tr>
</thead>
<tbody>
{% for s in waiting_for_draft %}
<tr>
{% if user.is_authenticated %}
<td><a id="aw{{ s.pk }}" href="{% url "ietf.submit.views.submission_status" submission_id=s.pk access_token=s.access_token %}">{{ s.name }}</a></td>
{% else %}
<td><a id="aw{{ s.pk }}" href="{% url "ietf.submit.views.submission_status" submission_id=s.pk %}">{{ s.name }}</a></td>
{% endif %}
<td>{{ s.rev }}</td>
<td>{{ s.submission_date }}</td>
{% if user|has_role:"Secretariat" %}
<td>
<form id="cancel-submission" action="/submit/awaitingdraft/cancel" method="post">
{% csrf_token %}
<input type="hidden" name="submission_id" value="{{ s.pk }}">
<input type="hidden" name="access_token" value="{{ s.access_token }}">
<button class="btn btn-danger btn-xs" type="submit" data-toggle="tooltip" title="Cancels the submission permanently.">Cancel submission</button>
</form>
</td>
{% endif %}
{% if user|has_role:"Secretariat" %}
<td><a id="add-submission-email{{ s.pk }}" class="btn btn-default btn-xs" href="{% url "ietf.submit.views.add_manualpost_email" submission_id=s.pk access_token=s.access_token %}">Add email</a></td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if user|has_role:"Secretariat" %}
<a id="new-submission-email" class="btn btn-default btn-sm" href="{% url "ietf.submit.views.add_manualpost_email" %}">New submission from email</a>
{% endif %}
{% endblock %}
{% block js %}
<script src="{% static "jquery.tablesorter/js/jquery.tablesorter.combined.min.js" %}"></script>
{% endblock %}

View file

@ -0,0 +1,60 @@
{% extends "submit/submit_base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin staticfiles %}
{% block pagehead %}
<link rel="stylesheet" href="{% static "jquery.tablesorter/css/theme.bootstrap.min.css" %}">
{% endblock %}
{% block title %}Submission email{% endblock %}
{% load ietf_filters %}
{% block submit_content %}
{% origin %}
<h2 class="anchor-target" id="approvals">Email for {{ submission.name }}</h2>
<table id="email-details" class="emails table table-condensed table-striped tablesorter">
<tr>
<th>Uploaded</th>
<td>{{ message.time }}</td>
</tr>
<tr>
<th>Date</th>
<td>{{ message.message.time }}</td>
</tr>
<tr>
<th>From</th>
<td>{{ message.message.frm }}</td>
</tr>
<tr>
<th>Subject</th>
<td>{{ message.message.subject }}</td>
</tr>
<tr>
<th>Message</th>
<td>{{ message.message.body|linebreaksbr }}</td>
</tr>
{% for a in attachments %}
<tr>
<th>Attachment</th>
<td><a id="attach{{ submission.pk }}" href="{% url "ietf.submit.views.show_submission_email_attachment" submission_id=submission.pk message_id=message.pk filename=a.filename %}">{{ a.filename }}</a></td>
</tr>
{% endfor %}
{% if user|has_role:"Secretariat" %}
<tr>
<td><a id="reply{{ submission.pk }}" class="btn btn-default" href="{% url "ietf.submit.views.send_submission_email" submission_id=submission.pk message_id=message.pk %}" title="Reply"><span class="glyphicon glyphicon-envelope" aria-hidden="true"></span> Reply</a></td>
</tr>
{% endif %}
</table>
{% endblock %}
{% block js %}
<script src="{% static "jquery.tablesorter/js/jquery.tablesorter.combined.min.js" %}"></script>
{% endblock %}

View file

@ -101,28 +101,33 @@
</div>
</div>
{% if submission.state_id == "waiting-for-draft" %}
<p class="alert alert-warning">
This submission is awaiting the first draft upload.
</p>
{% else %}
<h2>Meta-data from the submission</h2>
<h2>Meta-data from the submission</h2>
{% if errors %}
<div class="alert alert-danger">
<p><b>Meta-Data errors found!</b></p>
<p >Please make sure that your Internet-Draft includes all of the required meta-data in the proper format.</p>
{% if errors %}
<div class="alert alert-danger">
<p><b>Meta-Data errors found!</b></p>
<p>Please make sure that your Internet-Draft includes all of the required meta-data in the proper format.</p>
<p>If your Internet-Draft <b>does</b> include all of the required meta-data in the proper format, and if
the error(s) identified below are due to the failure of the tool to extract the meta-data correctly,
then please use the "Adjust meta-data" button below, which will take you to the "Adjust screen" where
you can correct the improperly extracted meta-data. You will then be able to submit your Internet-Draft
to the Secretariat for manual posting.</p>
<p>If your Internet-Draft <b>does</b> include all of the required meta-data in the proper format, and if
the error(s) identified below are due to the failure of the tool to extract the meta-data correctly,
then please use the "Adjust meta-data" button below, which will take you to the "Adjust screen" where
you can correct the improperly extracted meta-data. You will then be able to submit your Internet-Draft
to the Secretariat for manual posting.</p>
<p>If your Internet-Draft <b>does not</b> include all of the required meta-data in the proper format, then
please cancel this submission, update your Internet-Draft, and resubmit it.</p>
<p>If your Internet-Draft <b>does not</b> include all of the required meta-data in the proper format, then
please cancel this submission, update your Internet-Draft, and resubmit it.</p>
<p><b>Note:</b> The secretariat will <b>not</b> add any
meta-data to your Internet-Draft or edit the meta-data. An
Internet-Draft that does not include all of the required meta-data in
the proper format <b>will</b> be returned to the submitter.</p>
</div>
<p><b>Note:</b> The secretariat will <b>not</b> add any
meta-data to your Internet-Draft or edit the meta-data. An
Internet-Draft that does not include all of the required meta-data in
the proper format <b>will</b> be returned to the submitter.</p>
</div>
{% endif %}
{% endif %}
<table class="table table-condensed table-striped">
@ -305,6 +310,11 @@
</form>
{% endif %}
{% if user|has_role:"Secretariat" %}
<p></p>
<a id="send{{ submission.pk }}" class="btn btn-default" href="{% url "ietf.submit.views.send_submission_email" submission_id=submission.pk %}" title="Email submitter"><span class="glyphicon glyphicon-envelope" aria-hidden="true"></span>Send Email</a>
{% endif %}
{% if show_send_full_url %}
<div class="alert alert-danger">
<p>You are not allowed to modify or cancel this submission. You can
@ -329,7 +339,7 @@
<h2>History</h2>
<table class="table table-condensed table-striped">
<table id="history" class="table table-condensed table-striped">
<thead>
<tr><th>Date</th><th>By</th><th>Event</th></tr>
</thead>
@ -338,7 +348,21 @@
<tr>
<td class="text-nowrap">{{ e.time|date:"Y-m-d" }}</td>
<td>{{ e.by|default:"" }}</td>
<td>{{ e.desc }}</td>
{% if e.desc|startswith:"Received message" or e.desc|startswith:"Sent message" %}
{% with m=e.submissionemailevent.message %}
{% if user.is_authenticated %}
<td>
{% if e.desc|startswith:"Received message" and user|has_role:"Secretariat" %}
<a id="reply{{ submission.pk }}" class="btn btn-default btn-xs" href="{% url "ietf.submit.views.send_submission_email" submission_id=submission.pk message_id=e.submissionemailevent.pk %}" title="Reply"><span class="glyphicon glyphicon-envelope" aria-hidden="true"></span> Reply</a>
{% endif %}
Email: <a id="aw{{ submission.pk }}-{{ m.pk }}" href="{% url "ietf.submit.views.show_submission_email_message" submission_id=submission.pk message_id=e.submissionemailevent.pk access_token=submission.access_token %}">{{ e.desc }}</a></td>
{% else %}
<td>Email: <a id="aw{{ submission.pk }}-{{ m.pk }}" href="{% url "ietf.submit.views.show_submission_email_message" submission_id=submission.pk message_id=e.submissionemailevent.pk %}">{{ e.desc }}</a></td>
{% endif %}
{% endwith %}
{% else %}
<td>{{ e.desc }}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>

View file

@ -12,19 +12,22 @@
<ul class="nav nav-tabs" role="tablist">
<li {% if selected == "index" %}class="active"{% endif %}>
<a href="{% url "submit_upload_submission" %}">Upload</a>
<a href="{% url "ietf.submit.views.upload_submission" %}">Upload</a>
</li>
<li {% if selected == "status" %}class="active"{% endif %}>
<a href="{% url "submit_search_submission" %}">Status</a>
<a href="{% url "ietf.submit.views.search_submission" %}">Status</a>
</li>
<li {% if selected == "instructions" %}class="active"{% endif %}>
<a href="{% url "submit_tool_instructions" %}">Instructions</a>
<a href="{% url "ietf.submit.views.tool_instructions" %}">Instructions</a>
</li>
<li {% if selected == "notewell" %}class="active"{% endif %}>
<a href="{% url "submit_note_well" %}">Note Well</a>
<a href="{% url "ietf.submit.views.note_well" %}">Note Well</a>
</li>
<li {% if selected == "approvals" %}class="active"{% endif %}>
<a href="{% url "submit_approvals" %}">Approvals</a>
<a href="{% url "ietf.submit.views.approvals" %}">Approvals</a>
</li>
<li {% if selected == "manual_posts" %}class="active"{% endif %}>
<a href="{% url "ietf.submit.views.manualpost" %}">Manual Post Requests</a>
</li>
</ul>

View file

@ -8,10 +8,17 @@
please manually enter your name and email address.
</p>
{% load ietf_filters %}
{% buttons %}
{% for author in submission.authors_parsed %}
<input type="button" class="author btn btn-default" data-name="{{ author.name }}" data-email="{{ author.email }}" value="{{ author.name }}">
{% endfor %}
{% endbuttons %}
{% bootstrap_form submitter_form %}
{% bootstrap_form_errors submitter_form %}
{% bootstrap_field submitter_form.name %}
{% bootstrap_field submitter_form.email %}
{% if user|has_role:"Secretariat" %}
{% bootstrap_field submitter_form.approvals_received %}
{% endif %}

View file

@ -8,7 +8,7 @@
{% origin %}
<h2>I-D submission tool instructions</h2>
<p><b>Tool URL:</b> <a href="{% url "submit_upload_submission" %}">https://datatracker.ietf.org{% url "submit_upload_submission" %}</a></p>
<p><b>Tool URL:</b> <a href="{% url "ietf.submit.views.upload_submission" %}">https://datatracker.ietf.org{% url "ietf.submit.views.upload_submission" %}</a></p>
<p>
This page will explain the purpose and content of each screen in the I-D Submission Tool, and the actions that result by clicking the form buttons on each screen.

View file

@ -29,6 +29,7 @@ python-dateutil>=2.2
python-magic>=0.4.6
python-memcached>=1.48 # for django.core.cache.backends.memcached
pytz>=2014.7
pyzmail>=1.0.3
selenium>=2.42
six>=1.8.0
tqdm>=3.7.0