datatracker/ietf/doc/views_material.py
Robert Sparks e6138ca126
feat: session apis (#7173)
* feat: Show bluesheets using Attended tables (#7094)

* feat: Show bluesheets using Attended tables (#6898)

* feat: Allow users to add themselves to session attendance (#6454)

* chore: Correct copyright year

* fix: Address review comments

* fix: Don't try to generate empty bluesheets

* refactor: Complete rewrite of bluesheet.html

* refactor: Fill in a few gaps, close a few holes

- Rename the live "bluesheet" to "attendance", add some explanatory text.
- Add attendance links in materials view and pre-finalized proceedings view.
- Don't allow users to add themselves after the corrections cutoff date.

* fix: Report file-save errors to caller

* fix: Address review comments

* fix: typo

* refactor: if instead of except; refactor gently

* refactor: Rearrange logic a little, add comment

* style: Black

* refactor: auto_now_add->default to allow override

* refactor: jsonschema to validate API payload

* feat: Handle new API data format

Not yet tested except that it falls back when the old
format is used.

* test: Split test into deprecated/new version

Have not yet touched the new version

* style: Black

* test: Test new add_session_attendees API

* fix: Fix bug uncovered by test

* refactor: Refactor affiliation lookup a bit

* fix: Order bluesheet by Attended.time

* refactor: Move helpers from views.py to utils.py

* test: Test that finalize calls generate_bluesheets

* test: test_bluesheet_data()

* fix: Clean up merge

* fix: Remove debug statement

* chore: comments

* refactor: Renumber migrations

---------

Co-authored-by: Paul Selkirk <paul@painless-security.com>

* chore: Remove unused import

* style: Black

* feat: Stub session update notify API

* feat: Add order & rev to slides JSON

* style: Black

* feat: Stub actual Meetecho slide deck mgmt API

* refactor: Limit reordering to type="slides"

* chore: Remove repository from meetecho API

(API changed on their end)

* feat: update Meetecho on slide reorder

* refactor: drop pytz from meetecho.py

* chore: Remove more repository refs

* refactor: Eliminate more pytz

* test: Test add_slide_deck api

* fix: Allow 202 status code / absent Content-Type

* test: Test delete_slide_deck api

* test: Test update_slide_decks api

* refactor: sessionpresentation_set -> presentations

* test: Test send_update()

* fix: Debug send_update()

* test: ajax_reorder_slides calls Meetecho API

* test: Test SldesManager.add()

* feat: Implement SlidesManager.add()

* test: Test that ajax_add_slides... calls API

* feat: Call Meetecho API when slides added to session

* test: Test SlidesManager.delete()

* feat: Implement SlidesManager.delete()

* test: ajax_remove_slides... calls Meetecho API

* feat: Call Meetecho API when slides removed

* chore: Update docstring

* feat: rudimentary debug mode for Meetecho API

* test: remove_sessionpresentation() calls Meetecho API

* feat: Call Meetecho API from remove_sessionpresentation()

* test: upload_slides() calls Meetecho API

* style: Black

* fix: Refactor/debug upload_session_slides

Avoids double-save of a SessionPresentation for the session
being updated and updates other sessions when apply_to_all
is set (previously it only created ones that did not exist,
so rev would never be updated).

* test: Fix test bug

* feat: Call Meetecho API when uploading session slides

* fix: Only replace slides actually linked to session

* fix: Delint

Removed some type checking rather than debugging it

* fix: Send get_versionless_href() as url for slides

* test: TZ-aware timestamps, please

* chore: Add comments

* feat: Call Meetecho API in edit_sessionpresentation

* feat: Call Meetecho API in remove_sessionpresentation

* feat: Call Meetecho API from add_sessionpresentation

* fix: Set order in add_sessionpresentation

* fix: Restrict API calls to "slides" docs

* feat: Call Meetecho API on title changes

* test: Check meetecho API calls in test_revise()

* fix: better Meetecho API "order" management

* fix: no PUT if there are no slides after DELETE

* feat: Catch exceptions from SlidesManager

Don't let errors in the MeetEcho slides API interfere with
the ability to modify slides for a session.

* feat: Limit which sessions we send notifications for

* fix: handle absence of request_timeout in api config

* test: always send slide notifications in tests

* fix: save slides before sending notification (#7172)

* fix: save slides before sending notification

* style: fix indentation

It's not a bug, it's a flourish!

---------

Co-authored-by: Jennifer Richards <jennifer@staff.ietf.org>
Co-authored-by: Paul Selkirk <paul@painless-security.com>
2024-03-12 10:22:24 -05:00

240 lines
9.6 KiB
Python

# Copyright The IETF Trust 2014-2020, All Rights Reserved
# -*- coding: utf-8 -*-
# views for managing group materials (slides, ...)
import io
import os
import re
from django import forms
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.http import Http404
from django.shortcuts import render, get_object_or_404, redirect
from django.utils.html import mark_safe # type:ignore
from django.urls import reverse as urlreverse
import debug # pyflakes:ignore
from ietf.doc.models import Document, DocTypeName, DocEvent, State
from ietf.doc.models import NewRevisionDocEvent
from ietf.doc.utils import add_state_change_event, check_common_doc_name_rules
from ietf.group.models import Group
from ietf.group.utils import can_manage_materials
from ietf.utils import log
from ietf.utils.decorators import ignore_view_kwargs
from ietf.utils.meetecho import MeetechoAPIError, SlidesManager
from ietf.utils.response import permission_denied
@login_required
@ignore_view_kwargs("group_type")
def choose_material_type(request, acronym):
group = get_object_or_404(Group, acronym=acronym)
if not group.features.has_nonsession_materials:
raise Http404
return render(request, 'doc/material/choose_material_type.html', {
'group': group,
'material_types': DocTypeName.objects.filter(slug__in=group.features.material_types),
})
class UploadMaterialForm(forms.Form):
title = forms.CharField(max_length=Document._meta.get_field("title").max_length)
name = forms.CharField(max_length=Document._meta.get_field("name").max_length)
abstract = forms.CharField(max_length=Document._meta.get_field("abstract").max_length,widget=forms.Textarea, strip=False)
state = forms.ModelChoiceField(State.objects.all(), empty_label=None)
material = forms.FileField(label='File')
def __init__(self, doc_type, action, group, doc, *args, **kwargs):
super(UploadMaterialForm, self).__init__(*args, **kwargs)
self.fields["state"].queryset = self.fields["state"].queryset.filter(type__slug=doc_type.slug)
self.doc_type = doc_type
self.action = action
self.group = group
if action == "new":
self.fields["state"].widget = forms.HiddenInput()
self.fields["state"].queryset = self.fields["state"].queryset.filter(slug="active")
self.fields["state"].initial = self.fields["state"].queryset[0].pk
self.fields["name"].initial = self._default_name()
else:
del self.fields["name"]
self.fields["title"].initial = doc.title
self.fields["abstract"].initial = doc.abstract
self.fields["state"].initial = doc.get_state().pk if doc.get_state() else None
if doc.get_state_slug() == "deleted":
self.fields["state"].help_text = "Note: If you wish to revise this document, you may wish to change the state so it's not deleted."
if action in ["title","state","abstract"]:
for fieldname in ["title","state","material","abstract"]:
if fieldname != action:
del self.fields[fieldname]
if doc_type.slug == 'procmaterials' and 'abstract' in self.fields:
del self.fields['abstract']
def _default_name(self):
return "%s-%s-" % (self.doc_type.slug, self.group.acronym)
def clean_name(self):
name = self.cleaned_data["name"].strip().rstrip("-")
check_common_doc_name_rules(name)
if not re.search("^%s-%s-[a-z0-9]+" % (self.doc_type.slug, self.group.acronym), name):
raise forms.ValidationError("The name must start with %s-%s- followed by descriptive dash-separated words." % (self.doc_type.slug, self.group.acronym))
existing = Document.objects.filter(type=self.doc_type, name=name)
if existing:
url = urlreverse('ietf.doc.views_material.edit_material', kwargs={ 'name': existing[0].name, 'action': 'revise' })
raise forms.ValidationError(mark_safe("Can't upload: %s with name %s already exists. Choose another title and name for what you're uploading or <a href=\"%s\">revise the existing %s</a>." % (self.doc_type.name, name, url, name)))
return name
@login_required
@ignore_view_kwargs("group_type")
def edit_material(request, name=None, acronym=None, action=None, doc_type=None):
# the materials process is not very developed, so at the moment we
# handle everything through the same view/form
if action == "new":
group = get_object_or_404(Group, acronym=acronym)
if not group.features.has_nonsession_materials:
raise Http404
doc = None
document_type = get_object_or_404(DocTypeName, slug=doc_type)
else:
doc = get_object_or_404(Document, name=name)
group = doc.group
document_type = doc.type
valid_doctypes = ['procmaterials']
if group is not None:
valid_doctypes.extend(['minutes','agenda','bluesheets'])
if group.acronym=="iesg":
valid_doctypes.append("narrativeminutes")
valid_doctypes.extend(group.features.material_types)
if document_type.slug not in valid_doctypes:
raise Http404
if not can_manage_materials(request.user, group):
permission_denied(request, "You don't have permission to access this view")
sessions_with_slide_title_updates = set()
if request.method == 'POST':
form = UploadMaterialForm(document_type, action, group, doc, request.POST, request.FILES)
if form.is_valid():
events = []
if action == "new":
doc = Document.objects.create(
type=document_type,
group=group,
rev="00",
name=form.cleaned_data["name"])
prev_rev = None
else:
prev_rev = doc.rev
prev_title = doc.title
prev_state = doc.get_state()
prev_abstract = doc.abstract
if "title" in form.cleaned_data:
doc.title = form.cleaned_data["title"]
if "abstract" in form.cleaned_data:
doc.abstract = form.cleaned_data["abstract"]
if "material" in form.fields:
if action != "new":
doc.rev = "%02d" % (int(doc.rev) + 1)
f = form.cleaned_data["material"]
file_ext = os.path.splitext(f.name)[1]
with io.open(os.path.join(doc.get_file_path(), doc.name + "-" + doc.rev + file_ext), 'wb+') as dest:
for chunk in f.chunks():
dest.write(chunk)
if prev_rev != doc.rev:
e = NewRevisionDocEvent(type="new_revision", doc=doc, rev=doc.rev)
e.by = request.user.person
e.desc = "New version available: <b>%s-%s</b>" % (doc.name, doc.rev)
e.save()
events.append(e)
if prev_title != doc.title:
e = DocEvent(doc=doc, rev=doc.rev, by=request.user.person, type='changed_document')
e.desc = "Changed title to <b>%s</b>" % doc.title
if prev_title:
e.desc += " from %s" % prev_title
e.save()
events.append(e)
if doc.type_id == "slides":
for sp in doc.presentations.all():
sessions_with_slide_title_updates.add(sp.session)
if prev_abstract != doc.abstract:
e = DocEvent(doc=doc, rev=doc.rev, by=request.user.person, type='changed_document')
e.desc = "Changed abstract to <b>%s</b>" % doc.abstract
if prev_abstract:
e.desc += " from %s" % prev_abstract
e.save()
events.append(e)
if "state" in form.cleaned_data and form.cleaned_data["state"] != prev_state:
doc.set_state(form.cleaned_data["state"])
e = add_state_change_event(doc, request.user.person, prev_state, form.cleaned_data["state"])
events.append(e)
if events:
doc.save_with_history(events)
# Call Meetecho API if any session slides titles changed
if sessions_with_slide_title_updates and hasattr(settings, "MEETECHO_API_CONFIG"):
sm = SlidesManager(api_config=settings.MEETECHO_API_CONFIG)
for session in sessions_with_slide_title_updates:
try:
# SessionPresentations are unique over (session, document) so there will be no duplicates
sm.send_update(session)
except MeetechoAPIError as err:
log.log(f"Error in SlidesManager.send_update(): {err}")
return redirect("ietf.doc.views_doc.document_main", name=doc.name)
else:
form = UploadMaterialForm(document_type, action, group, doc)
# decide where to go if upload is canceled
if doc:
back_href = urlreverse('ietf.doc.views_doc.document_main', kwargs={'name': doc.name})
else:
back_href = urlreverse('ietf.group.views.materials', kwargs={'acronym': group.acronym})
if document_type.slug == 'procmaterials':
name_prefix = 'proceedings-'
else:
name_prefix = f'{document_type.slug}-{group.acronym}-'
return render(request, 'doc/material/edit_material.html', {
'group': group,
'form': form,
'action': action,
'material_type': document_type,
'name_prefix': name_prefix,
'doc': doc,
'doc_name': doc.name if doc else "",
'back_href': back_href,
})