637 lines
20 KiB
Python
637 lines
20 KiB
Python
# Copyright The IETF Trust 2013-2020, All Rights Reserved
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
import datetime
|
|
import glob
|
|
import io
|
|
import os
|
|
import shutil
|
|
from dateutil.parser import parse
|
|
from collections import OrderedDict
|
|
|
|
from django.conf import settings
|
|
from django.contrib import messages
|
|
from django.db.models import Max
|
|
from django.shortcuts import render, get_object_or_404, redirect
|
|
from django.urls import reverse
|
|
from django.utils.http import urlencode
|
|
|
|
from ietf.doc.models import Document, DocumentAuthor, State
|
|
from ietf.doc.models import DocEvent, NewRevisionDocEvent
|
|
from ietf.doc.utils import add_state_change_event
|
|
from ietf.ietfauth.utils import role_required
|
|
from ietf.meeting.helpers import get_meeting
|
|
from ietf.secr.drafts.email import announcement_from_form, get_email_initial
|
|
from ietf.secr.drafts.forms import AuthorForm, EditModelForm, EmailForm, ExtendForm, SearchForm, WithdrawForm
|
|
from ietf.secr.utils.document import get_rfc_num, get_start_date
|
|
from ietf.submit.models import Preapproval
|
|
from ietf.utils.log import log
|
|
|
|
# -------------------------------------------------
|
|
# Helper Functions
|
|
# -------------------------------------------------
|
|
|
|
def get_action_details(draft, request):
|
|
'''
|
|
This function takes a draft object and request object and returns a list of dictionaries
|
|
with keys: label, value to be used in displaying information on the confirmation
|
|
page.
|
|
'''
|
|
result = []
|
|
data = request.POST
|
|
|
|
if data['action'] == 'revision':
|
|
m = {'label':'New Revision','value':data['revision']}
|
|
result.append(m)
|
|
|
|
if data['action'] == 'replace':
|
|
m = {'label':'Replaced By:','value':data['replaced_by']}
|
|
result.append(m)
|
|
|
|
return result
|
|
|
|
def handle_uploaded_file(f):
|
|
'''
|
|
Save uploaded draft files to temporary directory
|
|
'''
|
|
destination = io.open(os.path.join(settings.IDSUBMIT_MANUAL_STAGING_DIR, f.name), 'wb+')
|
|
for chunk in f.chunks():
|
|
destination.write(chunk)
|
|
destination.close()
|
|
|
|
def file_types_for_draft(draft):
|
|
'''Returns list of file extensions that exist for this draft'''
|
|
basename, ext = os.path.splitext(draft.get_file_name())
|
|
files = glob.glob(basename + '.*')
|
|
file_types = []
|
|
for filename in files:
|
|
base, ext = os.path.splitext(filename)
|
|
if ext:
|
|
file_types.append(ext)
|
|
return file_types
|
|
|
|
# -------------------------------------------------
|
|
# Action Button Functions
|
|
# -------------------------------------------------
|
|
'''
|
|
These functions handle the real work of the action buttons: database updates,
|
|
moving files, etc. Generally speaking the action buttons trigger a multi-page
|
|
sequence where information may be gathered using a custom form, an email
|
|
may be produced and presented to the user to edit, and only then when confirmation
|
|
is given will the action work take place. That's when these functions are called.
|
|
'''
|
|
|
|
def do_extend(draft, request):
|
|
'''
|
|
Actions:
|
|
- update revision_date
|
|
- set extension_date
|
|
'''
|
|
|
|
e = DocEvent.objects.create(
|
|
type='changed_document',
|
|
by=request.user.person,
|
|
doc=draft,
|
|
rev=draft.rev,
|
|
time=draft.time,
|
|
desc='Extended expiry',
|
|
)
|
|
draft.expires = parse(request.POST.get('expiration_date'))
|
|
draft.save_with_history([e])
|
|
|
|
# save scheduled announcement
|
|
form = EmailForm(request.POST)
|
|
announcement_from_form(form.data,by=request.user.person)
|
|
|
|
return
|
|
|
|
def do_resurrect(draft, request):
|
|
'''
|
|
Actions
|
|
- restore last archived version
|
|
- change state to Active
|
|
- reset expires
|
|
- create DocEvent
|
|
'''
|
|
# restore latest revision documents file from archive
|
|
files = glob.glob(os.path.join(settings.INTERNET_DRAFT_ARCHIVE_DIR,draft.name) + '-??.*')
|
|
sorted_files = sorted(files)
|
|
latest,ext = os.path.splitext(sorted_files[-1])
|
|
files = glob.glob(os.path.join(settings.INTERNET_DRAFT_ARCHIVE_DIR,latest) + '.*')
|
|
log("Resurrecting %s. Moving files:" % draft.name)
|
|
for file in files:
|
|
try:
|
|
shutil.move(file, settings.INTERNET_DRAFT_PATH)
|
|
log(" Moved file %s to %s" % (file, settings.INTERNET_DRAFT_PATH))
|
|
except shutil.Error as ex:
|
|
log(" Exception %s when attempting to move %s" % (ex, file))
|
|
|
|
# Update draft record
|
|
draft.set_state(State.objects.get(type="draft", slug="active"))
|
|
|
|
# set expires
|
|
draft.expires = datetime.datetime.now() + datetime.timedelta(settings.INTERNET_DRAFT_DAYS_TO_EXPIRE)
|
|
|
|
# create DocEvent
|
|
e = NewRevisionDocEvent.objects.create(type='completed_resurrect',
|
|
by=request.user.person,
|
|
doc=draft,
|
|
rev=draft.rev,
|
|
time=draft.time)
|
|
|
|
draft.save_with_history([e])
|
|
|
|
# send announcement
|
|
form = EmailForm(request.POST)
|
|
announcement_from_form(form.data,by=request.user.person)
|
|
|
|
return
|
|
|
|
def do_withdraw(draft,request):
|
|
'''
|
|
Actions
|
|
- change state to withdrawn
|
|
- TODO move file to archive
|
|
'''
|
|
withdraw_type = request.POST.get('withdraw_type')
|
|
|
|
prev_state = draft.get_state("draft")
|
|
new_state = None
|
|
if withdraw_type == 'ietf':
|
|
new_state = State.objects.get(type="draft", slug="ietf-rm")
|
|
elif withdraw_type == 'author':
|
|
new_state = State.objects.get(type="draft", slug="auth-rm")
|
|
|
|
if not new_state:
|
|
return
|
|
|
|
draft.set_state(new_state)
|
|
|
|
e = add_state_change_event(draft, request.user.person, prev_state, new_state)
|
|
if e:
|
|
draft.save_with_history([e])
|
|
|
|
# send announcement
|
|
form = EmailForm(request.POST)
|
|
announcement_from_form(form.data,by=request.user.person)
|
|
|
|
return
|
|
|
|
# -------------------------------------------------
|
|
# Standard View Functions
|
|
# -------------------------------------------------
|
|
@role_required('Secretariat')
|
|
def abstract(request, id):
|
|
'''
|
|
View Internet Draft Abstract
|
|
|
|
**Templates:**
|
|
|
|
* ``drafts/abstract.html``
|
|
|
|
**Template Variables:**
|
|
|
|
* draft
|
|
'''
|
|
draft = get_object_or_404(Document, name=id)
|
|
|
|
return render(request, 'drafts/abstract.html', {
|
|
'draft': draft},
|
|
)
|
|
|
|
@role_required('Secretariat')
|
|
def approvals(request):
|
|
'''
|
|
This view handles setting Initial Approval for drafts
|
|
'''
|
|
|
|
approved = Preapproval.objects.all().order_by('name')
|
|
form = None
|
|
|
|
return render(request, 'drafts/approvals.html', {
|
|
'form': form,
|
|
'approved': approved},
|
|
)
|
|
|
|
@role_required('Secretariat')
|
|
def author_delete(request, id, oid):
|
|
'''
|
|
This view deletes the specified author from the draft
|
|
'''
|
|
author = DocumentAuthor.objects.get(id=oid)
|
|
|
|
if request.method == 'POST' and request.POST['post'] == 'yes':
|
|
author.delete()
|
|
messages.success(request, 'The author was deleted successfully')
|
|
return redirect('ietf.secr.drafts.views.authors', id=id)
|
|
|
|
return render(request, 'confirm_delete.html', {'object': author})
|
|
|
|
@role_required('Secretariat')
|
|
def authors(request, id):
|
|
'''
|
|
Edit Internet Draft Authors
|
|
|
|
**Templates:**
|
|
|
|
* ``drafts/authors.html``
|
|
|
|
**Template Variables:**
|
|
|
|
* form, draft
|
|
|
|
'''
|
|
draft = get_object_or_404(Document, name=id)
|
|
action = request.GET.get('action')
|
|
|
|
if request.method == 'POST':
|
|
form = AuthorForm(request.POST)
|
|
button_text = request.POST.get('submit', '')
|
|
if button_text == 'Done':
|
|
if action == 'add':
|
|
return redirect('ietf.secr.drafts.views.announce', id=id)
|
|
return redirect('ietf.secr.drafts.views.view', id=id)
|
|
|
|
if form.is_valid():
|
|
person = form.cleaned_data['person']
|
|
email = form.cleaned_data['email']
|
|
affiliation = form.cleaned_data.get('affiliation') or ""
|
|
country = form.cleaned_data.get('country') or ""
|
|
|
|
authors = draft.documentauthor_set.all()
|
|
if authors:
|
|
order = list(authors.aggregate(Max('order')).values())[0] + 1
|
|
else:
|
|
order = 1
|
|
DocumentAuthor.objects.create(document=draft, person=person, email=email, affiliation=affiliation, country=country, order=order)
|
|
|
|
messages.success(request, 'Author added successfully!')
|
|
return redirect('ietf.secr.drafts.views.authors', id=id)
|
|
|
|
else:
|
|
form = AuthorForm()
|
|
|
|
return render(request, 'drafts/authors.html', {
|
|
'draft': draft,
|
|
'form': form},
|
|
)
|
|
|
|
@role_required('Secretariat')
|
|
def confirm(request, id):
|
|
draft = get_object_or_404(Document, name=id)
|
|
|
|
if request.method == 'POST':
|
|
button_text = request.POST.get('submit', '')
|
|
if button_text == 'Cancel':
|
|
return redirect('ietf.secr.drafts.views.view', id=id)
|
|
|
|
action = request.POST.get('action','')
|
|
form = EmailForm(request.POST)
|
|
if form.is_valid():
|
|
email = form.data
|
|
details = get_action_details(draft, request)
|
|
hidden_form = EmailForm(request.POST, hidden=True)
|
|
|
|
return render(request, 'drafts/confirm.html', {
|
|
'details': details,
|
|
'email': email,
|
|
'action': action,
|
|
'draft': draft,
|
|
'form': hidden_form},
|
|
)
|
|
else:
|
|
return render(request, 'drafts/email.html', {
|
|
'form': form,
|
|
'draft': draft,
|
|
'action': action},
|
|
)
|
|
|
|
@role_required('Secretariat')
|
|
def do_action(request, id):
|
|
'''
|
|
This view displays changes that will be made and calls appropriate
|
|
function if the user elects to proceed. If the user cancels then
|
|
the view page is returned.
|
|
'''
|
|
draft = get_object_or_404(Document, name=id)
|
|
|
|
if request.method == 'POST':
|
|
button_text = request.POST.get('submit', '')
|
|
if button_text == 'Cancel':
|
|
return redirect('ietf.secr.drafts.views.view', id=id)
|
|
|
|
action = request.POST.get('action')
|
|
|
|
if action == 'resurrect':
|
|
func = do_resurrect
|
|
elif action == 'extend':
|
|
func = do_extend
|
|
elif action == 'withdraw':
|
|
func = do_withdraw
|
|
|
|
func(draft,request)
|
|
|
|
messages.success(request, '%s action performed successfully!' % action)
|
|
return redirect('ietf.secr.drafts.views.view', id=id)
|
|
|
|
@role_required('Secretariat')
|
|
def dates(request):
|
|
'''
|
|
Manage ID Submission Dates
|
|
|
|
**Templates:**
|
|
|
|
* none
|
|
|
|
**Template Variables:**
|
|
|
|
* none
|
|
'''
|
|
meeting = get_meeting()
|
|
|
|
return render(request, 'drafts/dates.html', {
|
|
'meeting':meeting},
|
|
)
|
|
|
|
@role_required('Secretariat')
|
|
def edit(request, id):
|
|
'''
|
|
Since there's a lot going on in this function we are summarizing in the docstring.
|
|
Also serves as a record of requirements.
|
|
|
|
if revision number increases add document_comments and send notify-revision
|
|
if revision date changed and not the number return error
|
|
check if using restricted words (?)
|
|
send notification based on check box
|
|
revision date = now if a new status box checked add_id5.cfm
|
|
(notify_[resurrection,revision,updated,extended])
|
|
if rfcnum="" rfcnum=0
|
|
if status != 2, expired_tombstone="0"
|
|
if new revision move current txt and ps files to archive directory (add_id5.cfm)
|
|
if status > 3 create tombstone, else send revision notification (EmailIDRevision.cfm)
|
|
'''
|
|
draft = get_object_or_404(Document, name=id)
|
|
|
|
if request.method == 'POST':
|
|
button_text = request.POST.get('submit', '')
|
|
if button_text == 'Cancel':
|
|
return redirect('ietf.secr.drafts.views.view', id=id)
|
|
|
|
form = EditModelForm(request.POST, instance=draft)
|
|
if form.is_valid():
|
|
if form.changed_data:
|
|
e = DocEvent.objects.create(type='changed_document',
|
|
by=request.user.person,
|
|
doc=draft,
|
|
rev=draft.rev,
|
|
desc='Changed field(s): %s' % ','.join(form.changed_data))
|
|
# see EditModelForm.save() for detailed logic
|
|
form.save(commit=False)
|
|
draft.save_with_history([e])
|
|
|
|
messages.success(request, 'Draft modified successfully!')
|
|
|
|
return redirect('ietf.secr.drafts.views.view', id=id)
|
|
else:
|
|
#assert False, form.errors
|
|
pass
|
|
else:
|
|
form = EditModelForm(instance=draft)
|
|
|
|
return render(request, 'drafts/edit.html', {
|
|
'form': form,
|
|
'draft': draft},
|
|
)
|
|
|
|
@role_required('Secretariat')
|
|
def email(request, id):
|
|
'''
|
|
This function displays the notification message and allows the
|
|
user to make changes before continuing to confirmation page.
|
|
'''
|
|
draft = get_object_or_404(Document, name=id)
|
|
action = request.GET.get('action')
|
|
data = request.GET
|
|
|
|
# the resurrect email body references the last revision number, handle
|
|
# exception if no last revision found
|
|
# if this exception was handled closer to the source it would be easier to debug
|
|
# other problems with get_email_initial
|
|
try:
|
|
form = EmailForm(initial=get_email_initial(draft,action=action,input=data))
|
|
except Exception as e:
|
|
return render(request, 'drafts/error.html', { 'error': e},)
|
|
|
|
return render(request, 'drafts/email.html', {
|
|
'form': form,
|
|
'draft': draft,
|
|
'action': action,
|
|
})
|
|
|
|
@role_required('Secretariat')
|
|
def extend(request, id):
|
|
'''
|
|
This view handles extending the expiration date for an Internet-Draft
|
|
Prerequisites: draft must be active
|
|
Input: new date
|
|
Actions
|
|
- revision_date = today
|
|
# - call handle_comment
|
|
'''
|
|
draft = get_object_or_404(Document, name=id)
|
|
|
|
if request.method == 'POST':
|
|
button_text = request.POST.get('submit', '')
|
|
if button_text == 'Cancel':
|
|
return redirect('ietf.secr.drafts.views.view', id=id)
|
|
|
|
form = ExtendForm(request.POST)
|
|
if form.is_valid():
|
|
params = form.cleaned_data
|
|
params['action'] = 'extend'
|
|
url = reverse('ietf.secr.drafts.views.email', kwargs={'id':id})
|
|
url = url + '?' + urlencode(params)
|
|
return redirect(url)
|
|
|
|
else:
|
|
form = ExtendForm(initial={'revision_date':datetime.date.today().isoformat()})
|
|
|
|
return render(request, 'drafts/extend.html', {
|
|
'form': form,
|
|
'draft': draft},
|
|
)
|
|
|
|
@role_required('Secretariat')
|
|
def nudge_report(request):
|
|
'''
|
|
This view produces the Nudge Report, basically a list of documents that are in the IESG
|
|
process but have not had activity in some time
|
|
'''
|
|
docs = Document.objects.filter(type='draft',states__slug='active')
|
|
docs = docs.filter(states=12,tags='need-rev')
|
|
|
|
return render(request, 'drafts/report_nudge.html', {
|
|
'docs': docs},
|
|
)
|
|
|
|
@role_required('Secretariat')
|
|
def search(request):
|
|
'''
|
|
Search Internet Drafts
|
|
|
|
**Templates:**
|
|
|
|
* ``drafts/search.html``
|
|
|
|
**Template Variables:**
|
|
|
|
* form, results
|
|
|
|
'''
|
|
results = []
|
|
|
|
if request.method == 'POST':
|
|
form = SearchForm(request.POST)
|
|
if request.POST['submit'] == 'Add':
|
|
return redirect('sec.drafts.views.add')
|
|
|
|
if form.is_valid():
|
|
kwargs = {}
|
|
intended_std_level = form.cleaned_data['intended_std_level']
|
|
title = form.cleaned_data['document_title']
|
|
group = form.cleaned_data['group']
|
|
name = form.cleaned_data['filename']
|
|
state = form.cleaned_data['state']
|
|
revision_date_start = form.cleaned_data['revision_date_start']
|
|
revision_date_end = form.cleaned_data['revision_date_end']
|
|
# construct seach query
|
|
if intended_std_level:
|
|
kwargs['intended_std_level'] = intended_std_level
|
|
if title:
|
|
kwargs['title__istartswith'] = title
|
|
if state:
|
|
kwargs['states__type'] = 'draft'
|
|
kwargs['states'] = state
|
|
if name:
|
|
kwargs['name__istartswith'] = name
|
|
if group:
|
|
kwargs['group__acronym__istartswith'] = group
|
|
if revision_date_start:
|
|
kwargs['docevent__type'] = 'new_revision'
|
|
kwargs['docevent__time__gte'] = revision_date_start
|
|
if revision_date_end:
|
|
kwargs['docevent__type'] = 'new_revision'
|
|
kwargs['docevent__time__lte'] = revision_date_end
|
|
|
|
# perform query
|
|
if kwargs:
|
|
qs = Document.objects.filter(**kwargs)
|
|
else:
|
|
qs = Document.objects.all()
|
|
#results = qs.order_by('group__name')
|
|
results = qs.order_by('name')
|
|
|
|
# if there's just one result go straight to view
|
|
if len(results) == 1:
|
|
return redirect('ietf.secr.drafts.views.view', id=results[0].name)
|
|
else:
|
|
active_state = State.objects.get(type='draft',slug='active')
|
|
form = SearchForm(initial={'state':active_state.pk})
|
|
|
|
return render(request, 'drafts/search.html', {
|
|
'results': results,
|
|
'form': form},
|
|
)
|
|
|
|
@role_required('Secretariat')
|
|
def view(request, id):
|
|
'''
|
|
View Internet Draft
|
|
|
|
**Templates:**
|
|
|
|
* ``drafts/view.html``
|
|
|
|
**Template Variables:**
|
|
|
|
* draft, area, id_tracker_state
|
|
'''
|
|
draft = get_object_or_404(Document, name=id)
|
|
|
|
# TODO fix in Django 1.2
|
|
# some boolean state variables for use in the view.html template to manage display
|
|
# of action buttons. NOTE: Django 1.2 support new smart if tag in templates which
|
|
# will remove the need for these variables
|
|
state = draft.get_state_slug()
|
|
is_active = True if state == 'active' else False
|
|
is_expired = True if state == 'expired' else False
|
|
is_withdrawn = True if (state in ('auth-rm','ietf-rm')) else False
|
|
|
|
# TODO should I rewrite all these or just use proxy.InternetDraft?
|
|
# add legacy fields
|
|
draft.iesg_state = draft.get_state('draft-iesg')
|
|
draft.review_by_rfc_editor = bool(draft.tags.filter(slug='rfc-rev'))
|
|
|
|
# can't assume there will be a new_revision record
|
|
r_event = draft.latest_event(type__in=('new_revision','completed_resurrect'))
|
|
draft.revision_date = r_event.time.date() if r_event else None
|
|
|
|
draft.start_date = get_start_date(draft)
|
|
|
|
e = draft.latest_event(type__in=('expired_document', 'new_revision', "completed_resurrect"))
|
|
draft.expiration_date = e.time.date() if e and e.type == "expired_document" else None
|
|
draft.rfc_number = get_rfc_num(draft)
|
|
|
|
# check for replaced bys
|
|
qs = Document.objects.filter(relateddocument__target__docs=draft, relateddocument__relationship='replaces')
|
|
if qs:
|
|
draft.replaced_by = qs[0]
|
|
|
|
# check for DEVELOPMENT setting and pass to template
|
|
is_development = False
|
|
try:
|
|
is_development = settings.DEVELOPMENT
|
|
except AttributeError:
|
|
pass
|
|
|
|
return render(request, 'drafts/view.html', {
|
|
'is_active': is_active,
|
|
'is_expired': is_expired,
|
|
'is_withdrawn': is_withdrawn,
|
|
'is_development': is_development,
|
|
'draft': draft},
|
|
)
|
|
|
|
@role_required('Secretariat')
|
|
def withdraw(request, id):
|
|
'''
|
|
This view handles withdrawing an Internet-Draft
|
|
Prerequisites: draft must be active
|
|
Input: by IETF or Author
|
|
'''
|
|
|
|
draft = get_object_or_404(Document, name=id)
|
|
|
|
if request.method == 'POST':
|
|
button_text = request.POST.get('submit', '')
|
|
if button_text == 'Cancel':
|
|
return redirect('ietf.secr.drafts.views.view', id=id)
|
|
|
|
form = WithdrawForm(request.POST)
|
|
if form.is_valid():
|
|
params = OrderedDict([('action', 'withdraw')])
|
|
params['withdraw_type'] = form.cleaned_data['withdraw_type']
|
|
url = reverse('ietf.secr.drafts.views.email', kwargs={'id':id})
|
|
url = url + '?' + urlencode(params)
|
|
return redirect(url)
|
|
|
|
else:
|
|
form = WithdrawForm()
|
|
|
|
return render(request, 'drafts/withdraw.html', {
|
|
'draft': draft,
|
|
'form': form},
|
|
)
|
|
|