datatracker/ietf/secr/drafts/views.py
Henrik Levkowetz 726fcbf27d Removed all __future__ imports.
- Legacy-Id: 17391
2020-03-05 23:53:42 +00:00

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},
)