Improve proceedings display with new title block, configurable host logos, and additional PDF or URL materials. Fixes #3147. Commit ready for merge.
- Legacy-Id: 19306
This commit is contained in:
parent
ca78da6ff4
commit
2060173f3a
|
@ -32,6 +32,7 @@ INTERNET_ALL_DRAFTS_ARCHIVE_DIR = "test/archive/"
|
|||
RFC_PATH = "test/rfc/"
|
||||
|
||||
AGENDA_PATH = 'data/developers/www6s/proceedings/'
|
||||
MEETINGHOST_LOGO_PATH = AGENDA_PATH
|
||||
|
||||
USING_DEBUG_EMAIL_SERVER=True
|
||||
EMAIL_HOST='localhost'
|
||||
|
|
|
@ -439,3 +439,18 @@ class BofreqFactory(BaseDocumentFactory):
|
|||
else:
|
||||
obj.set_state(State.objects.get(type_id='bofreq',slug='proposed'))
|
||||
|
||||
|
||||
class ProceedingsMaterialDocFactory(BaseDocumentFactory):
|
||||
type_id = 'procmaterials'
|
||||
abstract = ''
|
||||
expires = None
|
||||
|
||||
@factory.post_generation
|
||||
def states(obj, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
if extracted:
|
||||
for (state_type_id,state_slug) in extracted:
|
||||
obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug))
|
||||
else:
|
||||
obj.set_state(State.objects.get(type_id='procmaterials', slug='active'))
|
||||
|
|
34
ietf/doc/migrations/0044_procmaterials_states.py
Normal file
34
ietf/doc/migrations/0044_procmaterials_states.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
# Copyright The IETF Trust 2021 All Rights Reserved
|
||||
|
||||
# Generated by Django 2.2.23 on 2021-05-21 13:29
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
def forward(apps, schema_editor):
|
||||
StateType = apps.get_model('doc', 'StateType')
|
||||
State = apps.get_model('doc', 'State')
|
||||
|
||||
StateType.objects.create(slug='procmaterials', label='Proceedings Materials State')
|
||||
active = State.objects.create(type_id='procmaterials', slug='active', name='Active', used=True, desc='The material is active', order=0)
|
||||
removed = State.objects.create(type_id='procmaterials', slug='removed', name='Removed', used=True, desc='The material is removed', order=1)
|
||||
|
||||
active.next_states.set([removed])
|
||||
removed.next_states.set([active])
|
||||
|
||||
def reverse(apps, schema_editor):
|
||||
StateType = apps.get_model('doc', 'StateType')
|
||||
State = apps.get_model('doc', 'State')
|
||||
State.objects.filter(type_id='procmaterials').delete()
|
||||
StateType.objects.filter(slug='procmaterials').delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('doc', '0043_bofreq_docevents'),
|
||||
('name', '0030_add_procmaterials'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forward, reverse)
|
||||
]
|
|
@ -9,6 +9,8 @@ import os
|
|||
import rfc2html
|
||||
import time
|
||||
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from django.db import models
|
||||
from django.core import checks
|
||||
from django.core.cache import caches
|
||||
|
@ -34,6 +36,9 @@ from ietf.utils.decorators import memoize
|
|||
from ietf.utils.validators import validate_no_control_chars
|
||||
from ietf.utils.mail import formataddr
|
||||
from ietf.utils.models import ForeignKey
|
||||
if TYPE_CHECKING:
|
||||
# importing other than for type checking causes errors due to cyclic imports
|
||||
from ietf.meeting.models import ProceedingsMaterial, Session
|
||||
|
||||
logger = logging.getLogger('django')
|
||||
|
||||
|
@ -129,10 +134,11 @@ class DocumentInfo(models.Model):
|
|||
self._cached_file_path = settings.INTERNET_DRAFT_PATH
|
||||
else:
|
||||
self._cached_file_path = settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR
|
||||
elif self.type_id in ("agenda", "minutes", "slides", "bluesheets") and self.meeting_related():
|
||||
doc = self.doc if isinstance(self, DocHistory) else self
|
||||
if doc.session_set.exists():
|
||||
meeting = doc.session_set.first().meeting
|
||||
elif self.meeting_related() and self.type_id in (
|
||||
"agenda", "minutes", "slides", "bluesheets", "procmaterials"
|
||||
):
|
||||
meeting = self.get_related_meeting()
|
||||
if meeting is not None:
|
||||
self._cached_file_path = os.path.join(meeting.get_materials_path(), self.type_id) + "/"
|
||||
else:
|
||||
self._cached_file_path = ""
|
||||
|
@ -160,8 +166,9 @@ class DocumentInfo(models.Model):
|
|||
self._cached_base_name = "%s.txt" % self.canonical_name()
|
||||
else:
|
||||
self._cached_base_name = "%s-%s.txt" % (self.name, self.rev)
|
||||
elif self.type_id in ["slides", "agenda", "minutes", "bluesheets", ] and self.meeting_related():
|
||||
self._cached_base_name = "%s-%s.txt" % (self.canonical_name(), self.rev)
|
||||
elif self.type_id in ["slides", "agenda", "minutes", "bluesheets", "procmaterials", ] and self.meeting_related():
|
||||
ext = 'pdf' if self.type_id == 'procmaterials' else 'txt'
|
||||
self._cached_base_name = f'{self.canonical_name()}-{self.rev}.{ext}'
|
||||
elif self.type_id == 'review':
|
||||
# TODO: This will be wrong if a review is updated on the same day it was created (or updated more than once on the same day)
|
||||
self._cached_base_name = "%s.txt" % self.name
|
||||
|
@ -218,7 +225,6 @@ class DocumentInfo(models.Model):
|
|||
log.unreachable('2018-12-28')
|
||||
pass
|
||||
|
||||
|
||||
if self.type_id in settings.DOC_HREFS and self.type_id in meeting_doc_refs:
|
||||
if self.meeting_related():
|
||||
self.is_meeting_related = True
|
||||
|
@ -239,16 +245,13 @@ class DocumentInfo(models.Model):
|
|||
|
||||
if self.is_meeting_related:
|
||||
if not meeting:
|
||||
# we need to do this because DocHistory items don't have
|
||||
# any session_set entry:
|
||||
doc = self.doc if isinstance(self, DocHistory) else self
|
||||
sess = doc.session_set.first()
|
||||
if not sess:
|
||||
return ""
|
||||
meeting = sess.meeting
|
||||
meeting = self.get_related_meeting()
|
||||
if meeting is None:
|
||||
return ''
|
||||
|
||||
# After IETF 96, meeting materials acquired revision
|
||||
# handling, and the document naming changed.
|
||||
if meeting.number.isdigit() and int(meeting.number) <= 96:
|
||||
if meeting.proceedings_format_version == 1:
|
||||
format = settings.MEETING_DOC_OLD_HREFS[self.type_id]
|
||||
else:
|
||||
# This branch includes interims
|
||||
|
@ -420,10 +423,44 @@ class DocumentInfo(models.Model):
|
|||
return e != None and (e.text != "")
|
||||
|
||||
def meeting_related(self):
|
||||
if self.type_id in ("agenda","minutes","bluesheets","slides","recording"):
|
||||
if self.type_id in ("agenda","minutes","bluesheets","slides","recording","procmaterials"):
|
||||
return self.type_id != "slides" or self.get_state_slug('reuse_policy')=='single'
|
||||
return False
|
||||
|
||||
def get_related_session(self) -> Optional['Session']:
|
||||
"""Get the meeting session related to this document
|
||||
|
||||
Return None if there is no related session.
|
||||
Must define this in DocumentInfo subclasses.
|
||||
"""
|
||||
raise NotImplementedError(f'Class {self.__class__} must define get_related_session()')
|
||||
|
||||
def get_related_proceedings_material(self) -> Optional['ProceedingsMaterial']:
|
||||
"""Get the proceedings material related to this document
|
||||
|
||||
Return None if there is no related proceedings material.
|
||||
Must define this in DocumentInfo subclasses.
|
||||
"""
|
||||
raise NotImplementedError(f'Class {self.__class__} must define get_related_proceedings_material()')
|
||||
|
||||
def get_related_meeting(self):
|
||||
"""Get the meeting this document relates to"""
|
||||
if not self.meeting_related():
|
||||
return None # no related meeting if not meeting_related!
|
||||
elif self.type_id in ("agenda", "minutes", "slides", "bluesheets",):
|
||||
# session-related
|
||||
session = self.get_related_session()
|
||||
if session is not None:
|
||||
return session.meeting
|
||||
elif self.type_id == "procmaterials":
|
||||
# proceedings-related
|
||||
material = self.get_related_proceedings_material()
|
||||
if material is not None:
|
||||
return material.meeting
|
||||
else:
|
||||
log.unreachable('2021-08-29') # if meeting_related, there must be a way to retrieve the meeting!
|
||||
return None
|
||||
|
||||
def relations_that(self, relationship):
|
||||
"""Return the related-document objects that describe a given relationship targeting self."""
|
||||
if isinstance(relationship, str):
|
||||
|
@ -713,6 +750,13 @@ class Document(DocumentInfo):
|
|||
self._cached_absolute_url = url
|
||||
return self._cached_absolute_url
|
||||
|
||||
def get_related_session(self):
|
||||
sessions = self.session_set.all()
|
||||
return sessions.first()
|
||||
|
||||
def get_related_proceedings_material(self):
|
||||
return self.proceedingsmaterial_set.first()
|
||||
|
||||
def file_tag(self):
|
||||
return "<%s>" % self.filename_with_rev()
|
||||
|
||||
|
@ -1003,6 +1047,12 @@ class DocHistory(DocumentInfo):
|
|||
def __str__(self):
|
||||
return force_text(self.doc.name)
|
||||
|
||||
def get_related_session(self):
|
||||
return self.doc.get_related_session()
|
||||
|
||||
def get_related_proceedings_material(self):
|
||||
return self.doc.get_related_proceedings_material()
|
||||
|
||||
def canonical_name(self):
|
||||
if hasattr(self, '_canonical_name'):
|
||||
return self._canonical_name
|
||||
|
|
|
@ -614,7 +614,7 @@ def document_main(request, name, rev=None):
|
|||
|
||||
# TODO : Add "recording", and "bluesheets" here when those documents are appropriately
|
||||
# created and content is made available on disk
|
||||
if doc.type_id in ("slides", "agenda", "minutes", "bluesheets",):
|
||||
if doc.type_id in ("slides", "agenda", "minutes", "bluesheets","procmaterials",):
|
||||
can_manage_material = can_manage_materials(request.user, doc.group)
|
||||
presentations = doc.future_presentations()
|
||||
if doc.uploaded_filename:
|
||||
|
@ -645,6 +645,16 @@ def document_main(request, name, rev=None):
|
|||
t = "markdown"
|
||||
other_types.append((t, url))
|
||||
|
||||
# determine whether uploads are allowed
|
||||
can_upload = can_manage_material and not snapshot
|
||||
if doc.group is None:
|
||||
can_upload = can_upload and (doc.type_id == 'procmaterials')
|
||||
else:
|
||||
can_upload = (
|
||||
can_upload
|
||||
and doc.group.features.has_nonsession_materials
|
||||
and doc.type_id in doc.group.features.material_types
|
||||
)
|
||||
return render(request, "doc/document_material.html",
|
||||
dict(doc=doc,
|
||||
top=top,
|
||||
|
@ -654,7 +664,7 @@ def document_main(request, name, rev=None):
|
|||
latest_rev=latest_rev,
|
||||
snapshot=snapshot,
|
||||
can_manage_material=can_manage_material,
|
||||
in_group_materials_types = doc.group and doc.group.features.has_nonsession_materials and doc.type_id in doc.group.features.material_types,
|
||||
can_upload = can_upload,
|
||||
other_types=other_types,
|
||||
presentations=presentations,
|
||||
))
|
||||
|
|
|
@ -18,6 +18,7 @@ def state_help(request, type):
|
|||
"conflict-review": ("conflrev", "Conflict Review States"),
|
||||
"status-change": ("statchg", "RFC Status Change States"),
|
||||
"bofreq": ("bofreq", "BOF Request States"),
|
||||
"procmaterials": ("procmaterials", "Proceedings Materials States"),
|
||||
}.get(type, (None, None))
|
||||
state_type = get_object_or_404(StateType, slug=slug)
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ class UploadMaterialForm(forms.Form):
|
|||
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 = "%s-%s-" % (doc_type.slug, group.acronym)
|
||||
self.fields["name"].initial = self._default_name()
|
||||
else:
|
||||
del self.fields["name"]
|
||||
|
||||
|
@ -69,6 +69,12 @@ class UploadMaterialForm(forms.Form):
|
|||
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("-")
|
||||
|
||||
|
@ -101,10 +107,13 @@ def edit_material(request, name=None, acronym=None, action=None, doc_type=None):
|
|||
group = doc.group
|
||||
document_type = doc.type
|
||||
|
||||
if (document_type not in DocTypeName.objects.filter(slug__in=group.features.material_types)
|
||||
and document_type.slug not in ['minutes','agenda','bluesheets',]):
|
||||
raise Http404
|
||||
valid_doctypes = ['procmaterials']
|
||||
if group is not None:
|
||||
valid_doctypes.extend(['minutes','agenda','bluesheets'])
|
||||
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")
|
||||
|
@ -186,10 +195,26 @@ def edit_material(request, name=None, acronym=None, action=None, doc_type=None):
|
|||
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,
|
||||
'document_type': document_type,
|
||||
'material_type': document_type,
|
||||
'name_prefix': name_prefix,
|
||||
'doc': doc,
|
||||
'doc_name': doc.name if doc else "",
|
||||
'back_href': back_href,
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -125,7 +125,7 @@ def milestone_reviewer_for_group_type(group_type):
|
|||
return "Area Director"
|
||||
|
||||
def can_manage_materials(user, group):
|
||||
return has_role(user, 'Secretariat') or group.has_role(user, group.features.matman_roles)
|
||||
return has_role(user, 'Secretariat') or (group is not None and group.has_role(user, group.features.matman_roles))
|
||||
|
||||
def can_manage_session_materials(user, group, session):
|
||||
return has_role(user, 'Secretariat') or (group.has_role(user, group.features.matman_roles) and not session.is_material_submission_cutoff())
|
||||
|
|
|
@ -6,7 +6,8 @@ from django.contrib import admin
|
|||
|
||||
from ietf.meeting.models import (Meeting, Room, Session, TimeSlot, Constraint, Schedule,
|
||||
SchedTimeSessAssignment, ResourceAssociation, FloorPlan, UrlResource,
|
||||
SessionPresentation, ImportantDate, SlideSubmission, SchedulingEvent, BusinessConstraint)
|
||||
SessionPresentation, ImportantDate, SlideSubmission, SchedulingEvent, BusinessConstraint,
|
||||
ProceedingsMaterial, MeetingHost)
|
||||
|
||||
|
||||
class UrlResourceAdmin(admin.ModelAdmin):
|
||||
|
@ -186,3 +187,17 @@ class SlideSubmissionAdmin(admin.ModelAdmin):
|
|||
raw_id_fields = ['submitter', 'session']
|
||||
|
||||
admin.site.register(SlideSubmission, SlideSubmissionAdmin)
|
||||
|
||||
|
||||
class ProceedingsMaterialAdmin(admin.ModelAdmin):
|
||||
model = ProceedingsMaterial
|
||||
list_display = ['meeting', 'type', 'document']
|
||||
raw_id_fields = ['meeting', 'document']
|
||||
admin.site.register(ProceedingsMaterial, ProceedingsMaterialAdmin)
|
||||
|
||||
|
||||
class MeetingHostAdmin(admin.ModelAdmin):
|
||||
model = MeetingHost
|
||||
list_display = ['name', 'meeting']
|
||||
raw_id_fields = ['meeting']
|
||||
admin.site.register(MeetingHost, MeetingHostAdmin)
|
||||
|
|
|
@ -8,10 +8,14 @@ import datetime
|
|||
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
from ietf.meeting.models import Meeting, Session, SchedulingEvent, Schedule, TimeSlot, SessionPresentation, FloorPlan, Room, SlideSubmission
|
||||
from ietf.name.models import ConstraintName, SessionStatusName
|
||||
from ietf.meeting.models import (Meeting, Session, SchedulingEvent, Schedule, TimeSlot, SessionPresentation,
|
||||
FloorPlan, Room, SlideSubmission, MeetingHost, ProceedingsMaterial)
|
||||
from ietf.name.models import ConstraintName, SessionStatusName, ProceedingsMaterialTypeName
|
||||
from ietf.doc.factories import ProceedingsMaterialDocFactory
|
||||
from ietf.group.factories import GroupFactory
|
||||
from ietf.person.factories import PersonFactory
|
||||
from ietf.utils.text import xslugify
|
||||
|
||||
|
||||
class MeetingFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
|
@ -220,3 +224,45 @@ class SlideSubmissionFactory(factory.django.DjangoModelFactory):
|
|||
make_file = factory.PostGeneration(
|
||||
lambda obj, create, extracted, **kwargs: open(obj.staged_filepath(),'a').close()
|
||||
)
|
||||
|
||||
class MeetingHostFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = MeetingHost
|
||||
|
||||
meeting = factory.SubFactory(MeetingFactory, type_id='ietf')
|
||||
name = factory.Faker('company')
|
||||
logo = factory.django.ImageField() # generates an image
|
||||
|
||||
|
||||
def _pmf_doc_name(doc):
|
||||
"""Helper to generate document name for a ProceedingsMaterialFactory LazyAttribute"""
|
||||
return 'proceedings-{number}-{slug}'.format(
|
||||
number=doc.factory_parent.meeting.number,
|
||||
slug=xslugify(doc.factory_parent.type.slug).replace("_", "-")[:128]
|
||||
)
|
||||
|
||||
class ProceedingsMaterialFactory(factory.django.DjangoModelFactory):
|
||||
"""Create a ProceedingsMaterial for testing
|
||||
|
||||
Note: if you want to specify a type, use type=ProceedingsMaterialTypeName.objects.get(slug='slug')
|
||||
rather than the type_id='slug' shortcut. The latter will advance the Iterator used to generate
|
||||
types. This value is then used by the document SubFactory to set the document's title. This will
|
||||
not match the type of material created.
|
||||
"""
|
||||
class Meta:
|
||||
model = ProceedingsMaterial
|
||||
|
||||
meeting = factory.SubFactory(MeetingFactory, type_id='ietf')
|
||||
type = factory.Iterator(ProceedingsMaterialTypeName.objects.filter(used=True))
|
||||
# The SelfAttribute atnd LazyAttribute allow the document to be a SubFactory instead
|
||||
# of a generic LazyAttribute. This allows other attributes on the document to be
|
||||
# specified as document__external_url, etc.
|
||||
document = factory.SubFactory(
|
||||
ProceedingsMaterialDocFactory,
|
||||
type_id='procmaterials',
|
||||
title=factory.SelfAttribute('..type.name'),
|
||||
name=factory.LazyAttribute(_pmf_doc_name),
|
||||
uploaded_filename=factory.LazyAttribute(
|
||||
lambda doc: f'{_pmf_doc_name(doc)}-{doc.rev}.pdf'
|
||||
))
|
||||
|
||||
|
|
30
ietf/meeting/migrations/0045_proceedingsmaterial.py
Normal file
30
ietf/meeting/migrations/0045_proceedingsmaterial.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Copyright The IETF Trust 2021 All Rights Reserved
|
||||
|
||||
# Generated by Django 2.2.24 on 2021-07-26 17:09
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import ietf.utils.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('name', '0028_proceedingsmaterialtypename'),
|
||||
('meeting', '0044_again_assign_correct_constraintnames'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ProceedingsMaterial',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('meeting', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='proceedings_materials', to='meeting.Meeting')),
|
||||
('type', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='name.ProceedingsMaterialTypeName')),
|
||||
('document', ietf.utils.models.ForeignKey(limit_choices_to={'type_id': 'procmaterials'}, on_delete=django.db.models.deletion.CASCADE, to='doc.Document', unique=True)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('meeting', 'type')},
|
||||
},
|
||||
),
|
||||
]
|
34
ietf/meeting/migrations/0046_meetinghost.py
Normal file
34
ietf/meeting/migrations/0046_meetinghost.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
# Generated by Django 2.2.24 on 2021-08-26 10:18
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import ietf.meeting.models
|
||||
import ietf.utils.fields
|
||||
import ietf.utils.models
|
||||
import ietf.utils.storage
|
||||
import ietf.utils.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('meeting', '0045_proceedingsmaterial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MeetingHost',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('logo', ietf.utils.fields.MissingOkImageField(height_field='logo_height', storage=ietf.utils.storage.NoLocationMigrationFileSystemStorage(location=None), upload_to=ietf.meeting.models._host_upload_path, validators=[ietf.utils.validators.MaxImageSizeValidator(1600, 1600), ietf.utils.validators.WrappedValidator(ietf.utils.validators.validate_file_size, True), ietf.utils.validators.WrappedValidator(ietf.utils.validators.validate_file_extension, ['.png', '.jpg', '.jpeg']), ietf.utils.validators.WrappedValidator(ietf.utils.validators.validate_mime_type, ['image/jpeg', 'image/png'], True)], width_field='logo_width')),
|
||||
('logo_width', models.PositiveIntegerField(null=True)),
|
||||
('logo_height', models.PositiveIntegerField(null=True)),
|
||||
('meeting', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meetinghosts', to='meeting.Meeting')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('pk',),
|
||||
'unique_together': {('meeting', 'name')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -8,9 +8,12 @@ import datetime
|
|||
import io
|
||||
import os
|
||||
import pytz
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
|
||||
from collections import namedtuple
|
||||
from pathlib import Path
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
@ -30,13 +33,22 @@ from ietf.dbtemplate.models import DBTemplate
|
|||
from ietf.doc.models import Document
|
||||
from ietf.group.models import Group
|
||||
from ietf.group.utils import can_manage_materials
|
||||
from ietf.name.models import MeetingTypeName, TimeSlotTypeName, SessionStatusName, ConstraintName, RoomResourceName, ImportantDateName, TimerangeName, SlideSubmissionStatusName
|
||||
from ietf.name.models import (
|
||||
MeetingTypeName, TimeSlotTypeName, SessionStatusName, ConstraintName, RoomResourceName,
|
||||
ImportantDateName, TimerangeName, SlideSubmissionStatusName, ProceedingsMaterialTypeName,
|
||||
)
|
||||
from ietf.person.models import Person
|
||||
from ietf.utils.decorators import memoize
|
||||
from ietf.utils.storage import NoLocationMigrationFileSystemStorage
|
||||
from ietf.utils.text import xslugify
|
||||
from ietf.utils.timezone import date2datetime
|
||||
from ietf.utils.models import ForeignKey
|
||||
from ietf.utils.validators import (
|
||||
MaxImageSizeValidator, WrappedValidator, validate_file_size, validate_mime_type,
|
||||
validate_file_extension,
|
||||
)
|
||||
from ietf.utils.fields import MissingOkImageField
|
||||
from ietf.utils.log import unreachable
|
||||
|
||||
countries = list(pytz.country_names.items())
|
||||
countries.sort(key=lambda x: x[1])
|
||||
|
@ -219,6 +231,63 @@ class Meeting(models.Model):
|
|||
else:
|
||||
return None
|
||||
|
||||
def get_proceedings_materials(self):
|
||||
"""Get proceedings materials"""
|
||||
return self.proceedings_materials.filter(
|
||||
document__states__slug='active', document__states__type_id='procmaterials'
|
||||
).order_by('type__order')
|
||||
|
||||
def get_attendance(self):
|
||||
"""Get the meeting attendance from the MeetingRegistrations
|
||||
|
||||
Returns a NamedTuple with onsite and online attributes. Returns None if the record is unavailable
|
||||
for this meeting.
|
||||
"""
|
||||
number = self.get_number()
|
||||
if number is None or number < 110:
|
||||
return None
|
||||
Attendance = namedtuple('Attendance', 'onsite online')
|
||||
return Attendance(
|
||||
onsite=Person.objects.filter(
|
||||
meetingregistration__meeting=self,
|
||||
meetingregistration__attended=True,
|
||||
meetingregistration__reg_type__contains='in_person',
|
||||
).distinct().count(),
|
||||
online=Person.objects.filter(
|
||||
meetingregistration__meeting=self,
|
||||
meetingregistration__attended=True,
|
||||
meetingregistration__reg_type__contains='remote',
|
||||
).distinct().count(),
|
||||
)
|
||||
|
||||
@property
|
||||
def proceedings_format_version(self):
|
||||
"""Indicate version of proceedings that should be used for this meeting
|
||||
|
||||
Only makes sense for IETF meeting. Returns None for any meeting without a purely numeric number.
|
||||
|
||||
Uses settings.PROCEEDINGS_VERSION_CHANGES. Versions start at 1. Entries
|
||||
in the array are the first meeting number using each version.
|
||||
"""
|
||||
if not hasattr(self, '_proceedings_format_version'):
|
||||
if not self.number.isdigit():
|
||||
version = None # no version for non-IETF meeting
|
||||
else:
|
||||
version = len(settings.PROCEEDINGS_VERSION_CHANGES) # start assuming latest version
|
||||
mtg_number = self.get_number()
|
||||
if mtg_number is None:
|
||||
unreachable('2021-08-10')
|
||||
else:
|
||||
# Find the index of the first entry in the version change array that
|
||||
# is >= this meeting's number. The first entry in the array is 0, so the
|
||||
# version is always >= 1 for positive meeting numbers.
|
||||
for vers, threshold in enumerate(settings.PROCEEDINGS_VERSION_CHANGES):
|
||||
if mtg_number < threshold:
|
||||
version = vers
|
||||
break
|
||||
self._proceedings_format_version = version # save this for later
|
||||
return self._proceedings_format_version
|
||||
|
||||
@property
|
||||
def session_constraintnames(self):
|
||||
"""Gets a list of the constraint names that should be used for this meeting
|
||||
|
@ -1407,3 +1476,78 @@ class SlideSubmission(models.Model):
|
|||
|
||||
def staged_url(self):
|
||||
return "".join([settings.SLIDE_STAGING_URL, self.filename])
|
||||
|
||||
|
||||
class ProceedingsMaterial(models.Model):
|
||||
meeting = ForeignKey(Meeting, related_name='proceedings_materials')
|
||||
document = ForeignKey(
|
||||
Document,
|
||||
limit_choices_to=dict(type_id='procmaterials'),
|
||||
unique=True,
|
||||
)
|
||||
type = ForeignKey(ProceedingsMaterialTypeName)
|
||||
|
||||
class Meta:
|
||||
unique_together = (('meeting', 'type'),)
|
||||
|
||||
def __str__(self):
|
||||
return self.document.title
|
||||
|
||||
def get_href(self):
|
||||
return f'{self.document.get_href(self.meeting)}'
|
||||
|
||||
def active(self):
|
||||
return self.document.get_state().slug == 'active'
|
||||
|
||||
def is_url(self):
|
||||
return len(self.document.external_url) > 0
|
||||
|
||||
def _host_upload_path(instance : 'MeetingHost', filename):
|
||||
"""Compute filename relative to the storage location
|
||||
|
||||
Must live outside a class to allow migrations to deconstruct fields that use it
|
||||
"""
|
||||
num = instance.meeting.number
|
||||
path = (
|
||||
Path(num) / 'meetinghosts' / f'logo-{"".join(random.choices(string.ascii_lowercase, k=10))}'
|
||||
).with_suffix(
|
||||
Path(filename).suffix
|
||||
)
|
||||
return str(path)
|
||||
|
||||
|
||||
class MeetingHost(models.Model):
|
||||
"""Meeting sponsor"""
|
||||
meeting = ForeignKey(Meeting, related_name='meetinghosts')
|
||||
name = models.CharField(max_length=255, blank=False)
|
||||
logo = MissingOkImageField(
|
||||
storage=NoLocationMigrationFileSystemStorage(location=settings.MEETINGHOST_LOGO_PATH),
|
||||
upload_to=_host_upload_path,
|
||||
width_field='logo_width',
|
||||
height_field='logo_height',
|
||||
blank=False,
|
||||
validators=[
|
||||
MaxImageSizeValidator(
|
||||
settings.MEETINGHOST_LOGO_MAX_UPLOAD_WIDTH,
|
||||
settings.MEETINGHOST_LOGO_MAX_UPLOAD_HEIGHT,
|
||||
),
|
||||
WrappedValidator(validate_file_size, True),
|
||||
WrappedValidator(
|
||||
validate_file_extension,
|
||||
settings.MEETING_VALID_UPLOAD_EXTENSIONS['meetinghostlogo'],
|
||||
),
|
||||
WrappedValidator(
|
||||
validate_mime_type,
|
||||
settings.MEETING_VALID_UPLOAD_MIME_TYPES['meetinghostlogo'],
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
# These are filled in by the ImageField allow retrieval of image dimensions
|
||||
# without processing the image each time it's loaded.
|
||||
logo_width = models.PositiveIntegerField(null=True)
|
||||
logo_height = models.PositiveIntegerField(null=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = (('meeting', 'name'),)
|
||||
ordering = ('pk',)
|
||||
|
|
|
@ -14,7 +14,7 @@ from ietf import api
|
|||
from ietf.meeting.models import ( Meeting, ResourceAssociation, Constraint, Room, Schedule, Session,
|
||||
TimeSlot, SchedTimeSessAssignment, SessionPresentation, FloorPlan,
|
||||
UrlResource, ImportantDate, SlideSubmission, SchedulingEvent,
|
||||
BusinessConstraint)
|
||||
BusinessConstraint, ProceedingsMaterial, MeetingHost)
|
||||
|
||||
from ietf.name.resources import MeetingTypeNameResource
|
||||
class MeetingResource(ModelResource):
|
||||
|
@ -373,3 +373,42 @@ class BusinessConstraintResource(ModelResource):
|
|||
"penalty": ALL,
|
||||
}
|
||||
api.meeting.register(BusinessConstraintResource())
|
||||
|
||||
|
||||
from ietf.doc.resources import DocumentResource
|
||||
from ietf.name.resources import ProceedingsMaterialTypeNameResource
|
||||
class ProceedingsMaterialResource(ModelResource):
|
||||
meeting = ToOneField(MeetingResource, 'meeting')
|
||||
document = ToOneField(DocumentResource, 'document')
|
||||
type = ToOneField(ProceedingsMaterialTypeNameResource, 'type')
|
||||
class Meta:
|
||||
queryset = ProceedingsMaterial.objects.all()
|
||||
serializer = api.Serializer()
|
||||
cache = SimpleCache()
|
||||
#resource_name = 'proceedingsmaterial'
|
||||
ordering = ['id', ]
|
||||
filtering = {
|
||||
"id": ALL,
|
||||
"meeting": ALL_WITH_RELATIONS,
|
||||
"document": ALL_WITH_RELATIONS,
|
||||
"type": ALL_WITH_RELATIONS,
|
||||
}
|
||||
api.meeting.register(ProceedingsMaterialResource())
|
||||
|
||||
class MeetingHostResource(ModelResource):
|
||||
meeting = ToOneField(MeetingResource, 'meeting')
|
||||
class Meta:
|
||||
queryset = MeetingHost.objects.all()
|
||||
serializer = api.Serializer()
|
||||
cache = SimpleCache()
|
||||
#resource_name = 'meetinghost'
|
||||
ordering = ['id', ]
|
||||
filtering = {
|
||||
"id": ALL,
|
||||
"name": ALL,
|
||||
"logo": ALL,
|
||||
"logo_width": ALL,
|
||||
"logo_height": ALL,
|
||||
"meeting": ALL_WITH_RELATIONS,
|
||||
}
|
||||
api.meeting.register(MeetingHostResource())
|
||||
|
|
BIN
ietf/meeting/test_procmat.pdf
Normal file
BIN
ietf/meeting/test_procmat.pdf
Normal file
Binary file not shown.
|
@ -25,7 +25,8 @@ from ietf.group import colors
|
|||
from ietf.person.models import Person
|
||||
from ietf.group.models import Group
|
||||
from ietf.group.factories import GroupFactory
|
||||
from ietf.meeting.factories import MeetingFactory, RoomFactory, SessionFactory, TimeSlotFactory
|
||||
from ietf.meeting.factories import ( MeetingFactory, RoomFactory, SessionFactory, TimeSlotFactory,
|
||||
ProceedingsMaterialFactory )
|
||||
from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting
|
||||
from ietf.meeting.models import (Schedule, SchedTimeSessAssignment, Session,
|
||||
Room, TimeSlot, Constraint, ConstraintName,
|
||||
|
@ -2430,6 +2431,105 @@ class InterimTests(IetfSeleniumTestCase):
|
|||
)
|
||||
|
||||
|
||||
@ifSeleniumEnabled
|
||||
class ProceedingsMaterialTests(IetfSeleniumTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.wait = WebDriverWait(self.driver, 2)
|
||||
self.meeting = MeetingFactory(type_id='ietf', number='123', date=datetime.date.today())
|
||||
|
||||
def test_add_proceedings_material(self):
|
||||
url = self.absreverse(
|
||||
'ietf.meeting.views_proceedings.upload_material',
|
||||
kwargs=dict(num=self.meeting.number, material_type='supporters'),
|
||||
)
|
||||
self.login('secretary')
|
||||
self.driver.get(url)
|
||||
|
||||
# get the UI elements
|
||||
use_url_checkbox = self.wait.until(
|
||||
expected_conditions.element_to_be_clickable((By.ID, 'id_use_url'))
|
||||
)
|
||||
choose_file_button = self.wait.until(
|
||||
expected_conditions.presence_of_element_located((By.ID, 'id_file'))
|
||||
)
|
||||
external_url_field = self.wait.until(
|
||||
expected_conditions.presence_of_element_located((By.ID, 'id_external_url'))
|
||||
)
|
||||
|
||||
# should start with use_url unchecked for a new material
|
||||
self.assertTrue(choose_file_button.is_displayed(),
|
||||
'File chooser should be shown by default')
|
||||
self.assertFalse(external_url_field.is_displayed(),
|
||||
'URL field should be hidden by default')
|
||||
|
||||
# enable URL
|
||||
use_url_checkbox.click()
|
||||
self.wait.until(expected_conditions.invisibility_of_element(choose_file_button),
|
||||
'File chooser should be hidden when URL option is checked')
|
||||
self.wait.until(expected_conditions.visibility_of(external_url_field),
|
||||
'URL field should appear when URL option is checked')
|
||||
|
||||
# disable URL
|
||||
use_url_checkbox.click()
|
||||
self.wait.until(expected_conditions.visibility_of(choose_file_button),
|
||||
'File chooser should appear when URL option is unchecked')
|
||||
self.wait.until(expected_conditions.invisibility_of_element(external_url_field),
|
||||
'URL field should be hidden when URL option is unchecked')
|
||||
|
||||
def test_replace_proceedings_material_shows_correct_default(self):
|
||||
doc_mat = ProceedingsMaterialFactory(meeting=self.meeting)
|
||||
url_mat = ProceedingsMaterialFactory(meeting=self.meeting, document__external_url='https://example.com')
|
||||
|
||||
# check the document material
|
||||
url = self.absreverse(
|
||||
'ietf.meeting.views_proceedings.upload_material',
|
||||
kwargs=dict(num=self.meeting.number, material_type=doc_mat.type.slug),
|
||||
)
|
||||
self.login('secretary')
|
||||
self.driver.get(url)
|
||||
use_url_checkbox = self.wait.until(
|
||||
expected_conditions.element_to_be_clickable((By.ID, 'id_use_url'))
|
||||
)
|
||||
choose_file_button = self.wait.until(
|
||||
expected_conditions.presence_of_element_located((By.ID, 'id_file'))
|
||||
)
|
||||
external_url_field = self.wait.until(
|
||||
expected_conditions.presence_of_element_located((By.ID, 'id_external_url'))
|
||||
)
|
||||
|
||||
# should start with use_url unchecked for a document material
|
||||
self.assertFalse(use_url_checkbox.is_selected(), 'URL option should be unchecked for a document material')
|
||||
self.assertTrue(choose_file_button.is_displayed(),
|
||||
'File chooser should be shown by default')
|
||||
self.assertFalse(external_url_field.is_displayed(),
|
||||
'URL field should be hidden by default')
|
||||
|
||||
# check the URL material
|
||||
url = self.absreverse(
|
||||
'ietf.meeting.views_proceedings.upload_material',
|
||||
kwargs=dict(num=self.meeting.number, material_type=url_mat.type.slug),
|
||||
)
|
||||
self.driver.get(url)
|
||||
|
||||
use_url_checkbox = self.wait.until(
|
||||
expected_conditions.element_to_be_clickable((By.ID, 'id_use_url'))
|
||||
)
|
||||
choose_file_button = self.wait.until(
|
||||
expected_conditions.presence_of_element_located((By.ID, 'id_file'))
|
||||
)
|
||||
external_url_field = self.wait.until(
|
||||
expected_conditions.presence_of_element_located((By.ID, 'id_external_url'))
|
||||
)
|
||||
|
||||
# should start with use_url unchecked for a document material
|
||||
self.assertTrue(use_url_checkbox.is_selected(), 'URL option should be checked for URL material')
|
||||
self.assertFalse(choose_file_button.is_displayed(),
|
||||
'File chooser should be hidden by default')
|
||||
self.assertTrue(external_url_field.is_displayed(),
|
||||
'URL field should be shown by default')
|
||||
|
||||
|
||||
# The following are useful debugging tools
|
||||
|
||||
# If you add this to a LiveServerTestCase and run just this test, you can browse
|
||||
|
|
51
ietf/meeting/tests_models.py
Normal file
51
ietf/meeting/tests_models.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
# Copyright The IETF Trust 2021, All Rights Reserved
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests of models in the Meeting application"""
|
||||
from ietf.meeting.factories import MeetingFactory
|
||||
from ietf.stats.factories import MeetingRegistrationFactory
|
||||
from ietf.utils.test_utils import TestCase
|
||||
|
||||
|
||||
class MeetingTests(TestCase):
|
||||
def test_get_attendance_pre110(self):
|
||||
"""Pre-110 meetings do not calculate attendance"""
|
||||
meeting = MeetingFactory(type_id='ietf', number='109')
|
||||
MeetingRegistrationFactory.create_batch(3, meeting=meeting, reg_type='')
|
||||
MeetingRegistrationFactory.create_batch(4, meeting=meeting, reg_type='remote')
|
||||
MeetingRegistrationFactory.create_batch(5, meeting=meeting, reg_type='in_person')
|
||||
self.assertIsNone(meeting.get_attendance())
|
||||
|
||||
def test_get_attendance(self):
|
||||
"""Post-110 meetings do calculate attendance"""
|
||||
meeting = MeetingFactory(type_id='ietf', number='110')
|
||||
|
||||
# start with attendees that should be ignored
|
||||
MeetingRegistrationFactory.create_batch(3, meeting=meeting, reg_type='')
|
||||
MeetingRegistrationFactory(meeting=meeting, reg_type='', attended=False)
|
||||
attendance = meeting.get_attendance()
|
||||
self.assertIsNotNone(attendance)
|
||||
self.assertEqual(attendance.online, 0)
|
||||
self.assertEqual(attendance.onsite, 0)
|
||||
|
||||
# add online attendees with at least one who registered but did not attend
|
||||
MeetingRegistrationFactory.create_batch(4, meeting=meeting, reg_type='remote')
|
||||
MeetingRegistrationFactory(meeting=meeting, reg_type='remote', attended=False)
|
||||
attendance = meeting.get_attendance()
|
||||
self.assertIsNotNone(attendance)
|
||||
self.assertEqual(attendance.online, 4)
|
||||
self.assertEqual(attendance.onsite, 0)
|
||||
|
||||
# and the same for onsite attendees
|
||||
MeetingRegistrationFactory.create_batch(5, meeting=meeting, reg_type='in_person')
|
||||
MeetingRegistrationFactory(meeting=meeting, reg_type='in_person', attended=False)
|
||||
attendance = meeting.get_attendance()
|
||||
self.assertIsNotNone(attendance)
|
||||
self.assertEqual(attendance.online, 4)
|
||||
self.assertEqual(attendance.onsite, 5)
|
||||
|
||||
# and once more after removing all the online attendees
|
||||
meeting.meetingregistration_set.filter(reg_type='remote').delete()
|
||||
attendance = meeting.get_attendance()
|
||||
self.assertIsNotNone(attendance)
|
||||
self.assertEqual(attendance.online, 0)
|
||||
self.assertEqual(attendance.onsite, 5)
|
File diff suppressed because it is too large
Load diff
|
@ -4,7 +4,7 @@ from django.conf.urls import include
|
|||
from django.views.generic import RedirectView
|
||||
from django.conf import settings
|
||||
|
||||
from ietf.meeting import views, ajax
|
||||
from ietf.meeting import views, ajax, views_proceedings
|
||||
from ietf.utils.urls import url
|
||||
|
||||
safe_for_all_meeting_types = [
|
||||
|
@ -104,8 +104,21 @@ type_ietf_only_patterns_id_optional = [
|
|||
url(r'^proceedings/attendees/$', views.proceedings_attendees),
|
||||
url(r'^proceedings/overview/$', views.proceedings_overview),
|
||||
url(r'^proceedings/progress-report/$', views.proceedings_progress_report),
|
||||
url(r'^proceedings/materials/$', views_proceedings.material_details),
|
||||
url(r'^proceedings/materials/(?P<material_type>[a-z_]+)/$', views_proceedings.edit_material),
|
||||
url(r'^proceedings/materials/(?P<material_type>[a-z_]+)/new/$', views_proceedings.upload_material),
|
||||
url(r'^proceedings/materials/(?P<material_type>[a-z_]+)/remove/$',
|
||||
views_proceedings.remove_restore_material,
|
||||
{'action': 'remove'},
|
||||
'ietf.meeting.views_proceedings.remove_material'),
|
||||
url(r'^proceedings/materials/(?P<material_type>[a-z_]+)/restore/$',
|
||||
views_proceedings.remove_restore_material,
|
||||
{'action': 'restore'},
|
||||
'ietf.meeting.views_proceedings.restore_material'),
|
||||
url(r'^important-dates/$', views.important_dates),
|
||||
url(r'^important-dates.(?P<output_format>ics)$', views.important_dates),
|
||||
url(r'^proceedings/meetinghosts/edit/', views_proceedings.edit_meetinghosts),
|
||||
url(r'^proceedings/meetinghosts/(?P<host_id>\d+)/logo/$', views_proceedings.meetinghost_logo),
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
|
|
|
@ -87,7 +87,7 @@ from ietf.meeting.utils import diff_meeting_schedules, prefetch_schedule_diff_ob
|
|||
from ietf.meeting.utils import swap_meeting_schedule_timeslot_assignments
|
||||
from ietf.meeting.utils import preprocess_meeting_important_dates
|
||||
from ietf.message.utils import infer_message
|
||||
from ietf.name.models import SlideSubmissionStatusName
|
||||
from ietf.name.models import SlideSubmissionStatusName, ProceedingsMaterialTypeName
|
||||
from ietf.secr.proceedings.utils import handle_upload_file
|
||||
from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files,
|
||||
create_recording)
|
||||
|
@ -174,8 +174,14 @@ def materials(request, num=None):
|
|||
for session in session_list:
|
||||
session.past_cutoff_date = past_cutoff_date
|
||||
|
||||
proceedings_materials = [
|
||||
(type_name, meeting.proceedings_materials.filter(type=type_name).first())
|
||||
for type_name in ProceedingsMaterialTypeName.objects.all()
|
||||
]
|
||||
|
||||
return render(request, "meeting/materials.html", {
|
||||
'meeting': meeting,
|
||||
'proceedings_materials': proceedings_materials,
|
||||
'plenaries': plenaries,
|
||||
'ietf': ietf,
|
||||
'training': training,
|
||||
|
@ -221,7 +227,7 @@ def materials_document(request, document, num=None, ext=None):
|
|||
|
||||
if not doc.meeting_related():
|
||||
raise Http404("Not a meeting related document")
|
||||
if not doc.session_set.filter(meeting__number=num).exists():
|
||||
if doc.get_related_meeting() != meeting:
|
||||
raise Http404("No such document for meeting %s" % num)
|
||||
if not rev:
|
||||
filename = doc.get_file_name()
|
||||
|
@ -1639,7 +1645,7 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""
|
|||
meeting = get_ietf_meeting(num)
|
||||
if not meeting or (meeting.number.isdigit() and int(meeting.number) <= 64 and (not meeting.schedule or not meeting.schedule.assignments.exists())):
|
||||
if ext == '.html' or (meeting and meeting.number.isdigit() and 0 < int(meeting.number) <= 64):
|
||||
return HttpResponseRedirect( 'https://www.ietf.org/proceedings/%s' % num )
|
||||
return HttpResponseRedirect(f'{settings.PROCEEDINGS_V1_BASE_URL.format(meeting=meeting)}')
|
||||
else:
|
||||
raise Http404("No such meeting")
|
||||
|
||||
|
@ -3790,8 +3796,9 @@ def proceedings(request, num=None):
|
|||
|
||||
meeting = get_meeting(num)
|
||||
|
||||
if (meeting.number.isdigit() and int(meeting.number) <= 96):
|
||||
return HttpResponseRedirect('https://www.ietf.org/proceedings/%s' % meeting.number)
|
||||
# Early proceedings were hosted on www.ietf.org rather than the datatracker
|
||||
if meeting.proceedings_format_version == 1:
|
||||
return HttpResponseRedirect(settings.PROCEEDINGS_V1_BASE_URL.format(meeting=meeting))
|
||||
|
||||
if not meeting.schedule or not meeting.schedule.assignments.exists():
|
||||
kwargs = dict()
|
||||
|
@ -3839,6 +3846,11 @@ def proceedings(request, num=None):
|
|||
'cor_cut_off_date': cor_cut_off_date,
|
||||
'submission_started': now > begin_date,
|
||||
'cache_version': cache_version,
|
||||
'attendance': meeting.get_attendance(),
|
||||
'meetinghost_logo': {
|
||||
'max_height': settings.MEETINGHOST_LOGO_MAX_DISPLAY_HEIGHT,
|
||||
'max_width': settings.MEETINGHOST_LOGO_MAX_DISPLAY_WIDTH,
|
||||
}
|
||||
})
|
||||
|
||||
@role_required('Secretariat')
|
||||
|
@ -3860,8 +3872,8 @@ def proceedings_acknowledgements(request, num=None):
|
|||
if not (num and num.isdigit()):
|
||||
raise Http404
|
||||
meeting = get_meeting(num)
|
||||
if int(meeting.number) < settings.NEW_PROCEEDINGS_START:
|
||||
return HttpResponseRedirect( 'https://www.ietf.org/proceedings/%s/acknowledgement.html' % num )
|
||||
if meeting.proceedings_format_version == 1:
|
||||
return HttpResponseRedirect( f'{settings.PROCEEDINGS_V1_BASE_URL.format(meeting=meeting)}/acknowledgement.html')
|
||||
return render(request, "meeting/proceedings_acknowledgements.html", {
|
||||
'meeting': meeting,
|
||||
})
|
||||
|
@ -3871,8 +3883,8 @@ def proceedings_attendees(request, num=None):
|
|||
if not (num and num.isdigit()):
|
||||
raise Http404
|
||||
meeting = get_meeting(num)
|
||||
if int(meeting.number) < settings.NEW_PROCEEDINGS_START:
|
||||
return HttpResponseRedirect( 'https://www.ietf.org/proceedings/%s/attendees.html' % num )
|
||||
if meeting.proceedings_format_version == 1:
|
||||
return HttpResponseRedirect(f'{settings.PROCEEDINGS_V1_BASE_URL.format(meeting=meeting)}/attendee.html')
|
||||
overview_template = '/meeting/proceedings/%s/attendees.html' % meeting.number
|
||||
try:
|
||||
template = render_to_string(overview_template, {})
|
||||
|
@ -3888,8 +3900,8 @@ def proceedings_overview(request, num=None):
|
|||
if not (num and num.isdigit()):
|
||||
raise Http404
|
||||
meeting = get_meeting(num)
|
||||
if int(meeting.number) < settings.NEW_PROCEEDINGS_START:
|
||||
return HttpResponseRedirect( 'https://www.ietf.org/proceedings/%s/overview.html' % num )
|
||||
if meeting.proceedings_format_version == 1:
|
||||
return HttpResponseRedirect(f'{settings.PROCEEDINGS_V1_BASE_URL.format(meeting=meeting)}/overview.html')
|
||||
overview_template = '/meeting/proceedings/%s/overview.rst' % meeting.number
|
||||
try:
|
||||
template = render_to_string(overview_template, {})
|
||||
|
@ -3906,8 +3918,8 @@ def proceedings_progress_report(request, num=None):
|
|||
if not (num and num.isdigit()):
|
||||
raise Http404
|
||||
meeting = get_meeting(num)
|
||||
if int(meeting.number) < settings.NEW_PROCEEDINGS_START:
|
||||
return HttpResponseRedirect( 'https://www.ietf.org/proceedings/%s/progress-report.html' % num )
|
||||
if meeting.proceedings_format_version == 1:
|
||||
return HttpResponseRedirect(f'{settings.PROCEEDINGS_V1_BASE_URL.format(meeting=meeting)}/progress-report.html')
|
||||
sdate = meeting.previous_meeting().date
|
||||
edate = meeting.date
|
||||
context = get_progress_stats(sdate,edate)
|
||||
|
|
312
ietf/meeting/views_proceedings.py
Normal file
312
ietf/meeting/views_proceedings.py
Normal file
|
@ -0,0 +1,312 @@
|
|||
# Copyright The IETF Trust 2021 All Rights Reserved
|
||||
from pathlib import Path
|
||||
|
||||
from django import forms
|
||||
from django.http import Http404, FileResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.doc.utils import add_state_change_event
|
||||
from ietf.doc.models import DocAlias, DocEvent, Document, NewRevisionDocEvent, State
|
||||
from ietf.ietfauth.utils import role_required
|
||||
from ietf.meeting.forms import FileUploadForm
|
||||
from ietf.meeting.models import Meeting, MeetingHost
|
||||
from ietf.meeting.helpers import get_meeting
|
||||
from ietf.name.models import ProceedingsMaterialTypeName
|
||||
from ietf.secr.proceedings.utils import handle_upload_file
|
||||
from ietf.utils.text import xslugify
|
||||
|
||||
class UploadProceedingsMaterialForm(FileUploadForm):
|
||||
use_url = forms.BooleanField(
|
||||
required=False,
|
||||
label='Use an external URL instead of uploading a document',
|
||||
)
|
||||
external_url = forms.URLField(
|
||||
required=False,
|
||||
help_text='External URL to link from the proceedings'
|
||||
)
|
||||
field_order = ['use_url', 'external_url'] # will precede superclass fields
|
||||
|
||||
class Media:
|
||||
js = (
|
||||
'ietf/js/upload-material.js',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(doc_type='procmaterials', *args, **kwargs)
|
||||
self.fields['file'].label = 'Select a file to upload. Allowed format{}: {}'.format(
|
||||
'' if len(self.mime_types) == 1 else 's',
|
||||
', '.join(self.mime_types),
|
||||
)
|
||||
self.fields['file'].required = False
|
||||
|
||||
def clean_file(self):
|
||||
if self.cleaned_data.get('file', None) is None:
|
||||
return None # bypass cleaning the file if none was provided
|
||||
return super().clean_file()
|
||||
|
||||
def clean(self):
|
||||
if self.cleaned_data['use_url']:
|
||||
if not self.cleaned_data['external_url']:
|
||||
self.add_error('external_url', 'This field is required')
|
||||
else:
|
||||
self.cleaned_data['external_url'] = None # make sure this is empty
|
||||
if self.cleaned_data['file'] is None:
|
||||
self.add_error('file', 'This field is required')
|
||||
|
||||
class EditProceedingsMaterialForm(forms.Form):
|
||||
"""Form to edit proceedings material properties"""
|
||||
# A note: we use Document._meta to get the max length of a model field.
|
||||
# The leading underscore makes this look like accessing a private member,
|
||||
# but it is in fact part of Django's API.
|
||||
# noinspection PyProtectedMember
|
||||
title = forms.CharField(
|
||||
help_text='Label that will appear on the proceedings page',
|
||||
max_length=Document._meta.get_field("title").max_length,
|
||||
required=True,
|
||||
)
|
||||
|
||||
|
||||
def save_proceedings_material_doc(meeting, material_type, title, request, file=None, external_url=None, state=None):
|
||||
events = []
|
||||
by = request.user.person
|
||||
|
||||
if not (file is None or external_url is None):
|
||||
raise ValueError('One of file or external_url must be None')
|
||||
|
||||
# doc naming duplicates naming of docs elsewhere - use dashes instead of underscores
|
||||
doc_name = '-'.join([
|
||||
'proceedings',
|
||||
meeting.number,
|
||||
xslugify(
|
||||
getattr(material_type, 'slug', material_type)
|
||||
).replace('_', '-')[:128],
|
||||
])
|
||||
|
||||
created = False
|
||||
doc = Document.objects.filter(type_id='procmaterials', name=doc_name).first()
|
||||
if doc is None:
|
||||
if file is None and external_url is None:
|
||||
raise ValueError('Cannot create a new document without a file or external URL')
|
||||
doc = Document.objects.create(
|
||||
type_id='procmaterials',
|
||||
name=doc_name,
|
||||
rev="00",
|
||||
)
|
||||
created = True
|
||||
|
||||
# do this even if we did not create the document, just to be sure the alias exists
|
||||
alias, _ = DocAlias.objects.get_or_create(name=doc.name)
|
||||
alias.docs.add(doc)
|
||||
|
||||
if file:
|
||||
if not created:
|
||||
doc.rev = '{:02}'.format(int(doc.rev) + 1)
|
||||
filename = f'{doc.name}-{doc.rev}{Path(file.name).suffix}'
|
||||
save_error = handle_upload_file(file, filename, meeting, 'procmaterials', )
|
||||
if save_error is not None:
|
||||
raise RuntimeError(save_error)
|
||||
|
||||
doc.uploaded_filename = filename
|
||||
doc.external_url = ''
|
||||
e = NewRevisionDocEvent.objects.create(
|
||||
type="new_revision",
|
||||
doc=doc,
|
||||
rev=doc.rev,
|
||||
by=by,
|
||||
desc="New version available: <b>%s-%s</b>" % (doc.name, doc.rev),
|
||||
)
|
||||
events.append(e)
|
||||
elif (external_url is not None) and external_url != doc.external_url:
|
||||
if not created:
|
||||
doc.rev = '{:02}'.format(int(doc.rev) + 1)
|
||||
doc.uploaded_filename = ''
|
||||
doc.external_url = external_url
|
||||
e = NewRevisionDocEvent.objects.create(
|
||||
type="new_revision",
|
||||
doc=doc,
|
||||
rev=doc.rev,
|
||||
by=by,
|
||||
desc="Set external URL to <b>{}</b>".format(external_url),
|
||||
)
|
||||
events.append(e)
|
||||
|
||||
if doc.title != title and title is not None:
|
||||
e = DocEvent(doc=doc, rev=doc.rev, by=by, type='changed_document')
|
||||
e.desc = f'Changed title to <b>{title}</b>'
|
||||
if doc.title:
|
||||
e.desc += f' from {doc.title}'
|
||||
e.save()
|
||||
events.append(e)
|
||||
doc.title = title
|
||||
|
||||
# Set the state and create a change event if necessary
|
||||
prev_state = doc.get_state('procmaterials')
|
||||
new_state = state if state is not None else State.objects.get(type_id='procmaterials', slug='active')
|
||||
if prev_state != new_state:
|
||||
if not created:
|
||||
e = add_state_change_event(doc, by, prev_state, new_state)
|
||||
events.append(e)
|
||||
doc.set_state(new_state)
|
||||
|
||||
if events:
|
||||
doc.save_with_history(events)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@role_required('Secretariat')
|
||||
def upload_material(request, num, material_type):
|
||||
meeting = get_meeting(num)
|
||||
|
||||
# turn the material_type slug into the actual instance
|
||||
material_type = get_object_or_404(ProceedingsMaterialTypeName, slug=material_type)
|
||||
material = meeting.proceedings_materials.filter(type=material_type).first()
|
||||
|
||||
if request.method == 'POST':
|
||||
form = UploadProceedingsMaterialForm(request.POST, request.FILES)
|
||||
|
||||
if form.is_valid():
|
||||
doc = save_proceedings_material_doc(
|
||||
meeting,
|
||||
material_type,
|
||||
request=request,
|
||||
file=form.cleaned_data.get('file', None),
|
||||
external_url=form.cleaned_data.get('external_url', None),
|
||||
title=str(material if material is not None else material_type),
|
||||
)
|
||||
if material is None:
|
||||
meeting.proceedings_materials.create(type=material_type, document=doc)
|
||||
return redirect('ietf.meeting.views_proceedings.material_details', num=num)
|
||||
else:
|
||||
initial = dict()
|
||||
if material is not None:
|
||||
ext_url = material.document.external_url
|
||||
if ext_url != '':
|
||||
initial['use_url'] = True
|
||||
initial['external_url'] = ext_url
|
||||
form = UploadProceedingsMaterialForm(initial=initial)
|
||||
|
||||
return render(request, 'meeting/proceedings/upload_material.html', {
|
||||
'form': form,
|
||||
'material': material,
|
||||
'material_type': material_type,
|
||||
'meeting': meeting,
|
||||
'submit_button_label': 'Upload',
|
||||
})
|
||||
|
||||
@role_required('Secretariat')
|
||||
def material_details(request, num):
|
||||
meeting = get_meeting(num)
|
||||
proceedings_materials = [
|
||||
(type_name, meeting.proceedings_materials.filter(type=type_name).first())
|
||||
for type_name in ProceedingsMaterialTypeName.objects.all()
|
||||
]
|
||||
return render(
|
||||
request,
|
||||
'meeting/proceedings/material_details.html',
|
||||
dict(
|
||||
meeting=meeting,
|
||||
proceedings_materials=proceedings_materials,
|
||||
)
|
||||
)
|
||||
|
||||
@role_required('Secretariat')
|
||||
def edit_material(request, num, material_type):
|
||||
meeting = get_meeting(num)
|
||||
material = meeting.proceedings_materials.filter(type_id=material_type).first()
|
||||
if material is None:
|
||||
raise Http404('No such material for this meeting')
|
||||
if request.method == 'POST':
|
||||
form = EditProceedingsMaterialForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
save_proceedings_material_doc(
|
||||
meeting,
|
||||
material_type,
|
||||
request=request,
|
||||
title=form.cleaned_data['title'],
|
||||
)
|
||||
return redirect("ietf.meeting.views_proceedings.material_details", num=meeting.number)
|
||||
else:
|
||||
form = EditProceedingsMaterialForm(
|
||||
initial=dict(
|
||||
title=material.document.title,
|
||||
),
|
||||
)
|
||||
|
||||
return render(request, 'meeting/proceedings/edit_material.html', {
|
||||
'form': form,
|
||||
'material': material,
|
||||
'material_type': material.type,
|
||||
'meeting': meeting,
|
||||
})
|
||||
|
||||
@role_required('Secretariat')
|
||||
def remove_restore_material(request, num, material_type, action):
|
||||
"""Remove or restore proceedings material"""
|
||||
if action not in ['remove', 'restore']:
|
||||
return HttpResponseBadRequest('Unsupported action')
|
||||
meeting = get_meeting(num)
|
||||
material = meeting.proceedings_materials.filter(type_id=material_type).first()
|
||||
if material is None:
|
||||
raise Http404('No such material for this meeting')
|
||||
if request.method == 'POST':
|
||||
prev_state = material.document.get_state('procmaterials')
|
||||
new_state = State.objects.get(
|
||||
type_id='procmaterials',
|
||||
slug='active' if action == 'restore' else 'removed',
|
||||
)
|
||||
if new_state != prev_state:
|
||||
material.document.set_state(new_state)
|
||||
add_state_change_event(material.document, request.user.person, prev_state, new_state)
|
||||
return redirect('ietf.meeting.views_proceedings.material_details', num=num)
|
||||
|
||||
return render(
|
||||
request,
|
||||
'meeting/proceedings/remove_restore_material.html',
|
||||
dict(material=material, action=action)
|
||||
)
|
||||
|
||||
@role_required('Secretariat')
|
||||
def edit_meetinghosts(request, num):
|
||||
meeting = get_meeting(num)
|
||||
|
||||
MeetingHostFormSet = forms.inlineformset_factory(
|
||||
Meeting,
|
||||
MeetingHost,
|
||||
fields=('name', 'logo',),
|
||||
extra=2,
|
||||
)
|
||||
|
||||
if request.method == 'POST':
|
||||
formset = MeetingHostFormSet(request.POST, request.FILES, instance=meeting)
|
||||
if formset.is_valid():
|
||||
# If we are removing a MeetingHost or replacing its logo, delete the
|
||||
# old logo file.
|
||||
for form in formset:
|
||||
if form.instance.pk:
|
||||
deleted = form.cleaned_data.get('DELETE', False)
|
||||
logo_replaced = 'logo' in form.changed_data
|
||||
if deleted or logo_replaced:
|
||||
orig_instance = meeting.meetinghosts.get(pk=form.instance.pk)
|
||||
orig_instance.logo.delete()
|
||||
|
||||
# this will update the DB and add any newly uploaded files
|
||||
formset.save()
|
||||
return redirect('ietf.meeting.views.materials', num=meeting.number)
|
||||
else:
|
||||
formset = MeetingHostFormSet(instance=meeting)
|
||||
|
||||
return render(request, 'meeting/proceedings/edit_meetinghosts.html', {
|
||||
'formset': formset,
|
||||
'meeting': meeting,
|
||||
})
|
||||
|
||||
|
||||
def meetinghost_logo(request, num, host_id):
|
||||
host = get_object_or_404(MeetingHost, pk=host_id)
|
||||
if host.meeting.number != num:
|
||||
raise Http404()
|
||||
|
||||
return FileResponse(host.logo.open())
|
|
@ -11,7 +11,7 @@ from ietf.name.models import (
|
|||
ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName,
|
||||
SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, TopicAudienceName,
|
||||
DocUrlTagName, ReviewAssignmentStateName, ReviewerQueuePolicyName, TimerangeName,
|
||||
ExtResourceName, ExtResourceTypeName, SlideSubmissionStatusName)
|
||||
ExtResourceName, ExtResourceTypeName, SlideSubmissionStatusName, ProceedingsMaterialTypeName)
|
||||
|
||||
|
||||
from ietf.stats.models import CountryAlias
|
||||
|
@ -52,6 +52,10 @@ class ExtResourceNameAdmin(NameAdmin):
|
|||
list_display = ["slug", "name", "type", "desc", "used",]
|
||||
admin.site.register(ExtResourceName,ExtResourceNameAdmin)
|
||||
|
||||
class ProceedingsMaterialTypeNameAdmin(NameAdmin):
|
||||
list_display = ["slug", "name", "desc", "used", "order",]
|
||||
admin.site.register(ProceedingsMaterialTypeName, ProceedingsMaterialTypeNameAdmin)
|
||||
|
||||
admin.site.register(AgendaTypeName, NameAdmin)
|
||||
admin.site.register(BallotPositionName, NameAdmin)
|
||||
admin.site.register(ConstraintName, NameAdmin)
|
||||
|
|
|
@ -2310,10 +2310,10 @@
|
|||
"desc": "The BOF request is proposed",
|
||||
"name": "Proposed",
|
||||
"next_states": [
|
||||
159,
|
||||
160,
|
||||
161,
|
||||
162
|
||||
178,
|
||||
179,
|
||||
180,
|
||||
181
|
||||
],
|
||||
"order": 0,
|
||||
"slug": "proposed",
|
||||
|
@ -2321,7 +2321,7 @@
|
|||
"used": true
|
||||
},
|
||||
"model": "doc.state",
|
||||
"pk": 158
|
||||
"pk": 177
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
|
@ -2334,7 +2334,7 @@
|
|||
"used": true
|
||||
},
|
||||
"model": "doc.state",
|
||||
"pk": 159
|
||||
"pk": 178
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
|
@ -2347,7 +2347,7 @@
|
|||
"used": true
|
||||
},
|
||||
"model": "doc.state",
|
||||
"pk": 160
|
||||
"pk": 179
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
|
@ -2360,7 +2360,7 @@
|
|||
"used": true
|
||||
},
|
||||
"model": "doc.state",
|
||||
"pk": 161
|
||||
"pk": 180
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
|
@ -2373,7 +2373,37 @@
|
|||
"used": true
|
||||
},
|
||||
"model": "doc.state",
|
||||
"pk": 162
|
||||
"pk": 181
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "The material is active",
|
||||
"name": "Active",
|
||||
"next_states": [
|
||||
185
|
||||
],
|
||||
"order": 0,
|
||||
"slug": "active",
|
||||
"type": "procmaterials",
|
||||
"used": true
|
||||
},
|
||||
"model": "doc.state",
|
||||
"pk": 184
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "The material is removed",
|
||||
"name": "Removed",
|
||||
"next_states": [
|
||||
184
|
||||
],
|
||||
"order": 1,
|
||||
"slug": "removed",
|
||||
"type": "procmaterials",
|
||||
"used": true
|
||||
},
|
||||
"model": "doc.state",
|
||||
"pk": 185
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
|
@ -2508,6 +2538,13 @@
|
|||
"model": "doc.statetype",
|
||||
"pk": "minutes"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"label": "Proceedings Materials State"
|
||||
},
|
||||
"model": "doc.statetype",
|
||||
"pk": "procmaterials"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"label": "State"
|
||||
|
@ -9993,6 +10030,17 @@
|
|||
"model": "name.doctypename",
|
||||
"pk": "minutes"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
"name": "Proceedings Materials",
|
||||
"order": 0,
|
||||
"prefix": "proc-materials",
|
||||
"used": true
|
||||
},
|
||||
"model": "name.doctypename",
|
||||
"pk": "procmaterials"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
|
@ -11768,6 +11816,56 @@
|
|||
"model": "name.nomineepositionstatename",
|
||||
"pk": "pending"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "Any other materials",
|
||||
"name": "Additional Information",
|
||||
"order": 4,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.proceedingsmaterialtypename",
|
||||
"pk": "additional_information"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "Host speaker series",
|
||||
"name": "Host Speaker Series",
|
||||
"order": 1,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.proceedingsmaterialtypename",
|
||||
"pk": "host_speaker_series"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "Social event",
|
||||
"name": "Social Event",
|
||||
"order": 2,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.proceedingsmaterialtypename",
|
||||
"pk": "social_event"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "Sponsors and Supporters",
|
||||
"name": "Sponsors and supporters",
|
||||
"order": 0,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.proceedingsmaterialtypename",
|
||||
"pk": "supporters"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "Meeting wiki",
|
||||
"name": "Meeting Wiki",
|
||||
"order": 3,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.proceedingsmaterialtypename",
|
||||
"pk": "wiki"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "The reviewer has accepted the assignment",
|
||||
|
|
29
ietf/name/migrations/0028_proceedingsmaterialtypename.py
Normal file
29
ietf/name/migrations/0028_proceedingsmaterialtypename.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
# Copyright The IETF Trust 2021 All Rights Reserved
|
||||
|
||||
# Generated by Django 2.2.24 on 2021-07-26 16:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('name', '0027_add_bofrequest'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ProceedingsMaterialTypeName',
|
||||
fields=[
|
||||
('slug', models.CharField(max_length=32, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('desc', models.TextField(blank=True)),
|
||||
('used', models.BooleanField(default=True)),
|
||||
('order', models.IntegerField(default=0)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['order', 'name'],
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,34 @@
|
|||
# Copyright The IETF Trust 2021 All Rights Reserved
|
||||
|
||||
# Generated by Django 2.2.24 on 2021-07-26 16:55
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def forward(apps, schema_editor):
|
||||
ProceedingsMaterialTypeName = apps.get_model('name', 'ProceedingsMaterialTypeName')
|
||||
names = [
|
||||
{'slug': 'supporters', 'name': 'Sponsors and Supporters', 'desc': 'Sponsors and supporters', 'order': 0},
|
||||
{'slug': 'host_speaker_series', 'name': 'Host Speaker Series', 'desc': 'Host speaker series', 'order': 1},
|
||||
{'slug': 'social_event', 'name': 'Social Event', 'desc': 'Social event', 'order': 2},
|
||||
{'slug': 'wiki', 'name': 'Meeting Wiki', 'desc': 'Meeting wiki', 'order': 3},
|
||||
{'slug': 'additional_information', 'name': 'Additional Information', 'desc': 'Any other materials', 'order': 4},
|
||||
]
|
||||
for name in names:
|
||||
ProceedingsMaterialTypeName.objects.create(used=True, **name)
|
||||
|
||||
|
||||
def reverse(apps, schema_editor):
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('name', '0028_proceedingsmaterialtypename'),
|
||||
('meeting', '0046_meetinghost'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forward, reverse),
|
||||
]
|
42
ietf/name/migrations/0030_add_procmaterials.py
Normal file
42
ietf/name/migrations/0030_add_procmaterials.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
# Copyright The IETF Trust 2021 All Rights Reserved
|
||||
|
||||
# Generated by Django 2.2.24 on 2021-07-27 08:04
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def forward(apps, schema_editor):
|
||||
DocTypeName = apps.get_model('name', 'DocTypeName')
|
||||
DocTypeName.objects.create(
|
||||
prefix='proc-materials',
|
||||
slug='procmaterials',
|
||||
name="Proceedings Materials",
|
||||
desc="",
|
||||
used=True,
|
||||
order=0
|
||||
)
|
||||
|
||||
|
||||
def reverse(apps, schema_editor):
|
||||
DocTypeName = apps.get_model('name', 'DocTypeName')
|
||||
DocTypeName.objects.filter(slug='procmaterials').delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
# Most of these dependencies are needed to permit the reverse migration
|
||||
# to work. Without them Django does not replay enough migrations and during
|
||||
# migration believes that there are foreign key references to the old
|
||||
# PK (name) on the Document model.
|
||||
dependencies = [
|
||||
('doc', '0043_bofreq_docevents'),
|
||||
('group', '0044_populate_groupfeatures_parent_type_fields'),
|
||||
('liaisons', '0006_document_primary_key_cleanup'),
|
||||
('meeting', '0018_document_primary_key_cleanup'),
|
||||
('name', '0029_populate_proceedingsmaterialtypename'),
|
||||
('review', '0014_document_primary_key_cleanup'),
|
||||
('submit', '0008_submissionextresource'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forward, reverse)
|
||||
]
|
|
@ -60,6 +60,8 @@ class BallotPositionName(NameModel):
|
|||
blocking = models.BooleanField(default=False)
|
||||
class MeetingTypeName(NameModel):
|
||||
"""IETF, Interim"""
|
||||
class ProceedingsMaterialTypeName(NameModel):
|
||||
"""social_event, host_speaker_series, supporters, wiki, additional_information"""
|
||||
class AgendaTypeName(NameModel):
|
||||
"""ietf, ad, side, workshop, ..."""
|
||||
class SessionStatusName(NameModel):
|
||||
|
|
|
@ -18,7 +18,7 @@ from ietf.name.models import ( AgendaTypeName, BallotPositionName, ConstraintNam
|
|||
ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName, ReviewTypeName,
|
||||
RoleName, RoomResourceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName,
|
||||
TopicAudienceName, ReviewerQueuePolicyName, TimerangeName, ExtResourceTypeName, ExtResourceName,
|
||||
SlideSubmissionStatusName)
|
||||
SlideSubmissionStatusName, ProceedingsMaterialTypeName)
|
||||
|
||||
class TimeSlotTypeNameResource(ModelResource):
|
||||
class Meta:
|
||||
|
@ -668,3 +668,19 @@ class SlideSubmissionStatusNameResource(ModelResource):
|
|||
}
|
||||
api.name.register(SlideSubmissionStatusNameResource())
|
||||
|
||||
|
||||
class ProceedingsMaterialTypeNameResource(ModelResource):
|
||||
class Meta:
|
||||
queryset = ProceedingsMaterialTypeName.objects.all()
|
||||
serializer = api.Serializer()
|
||||
cache = SimpleCache()
|
||||
#resource_name = 'proceedingsmaterialtypename'
|
||||
ordering = ['slug', ]
|
||||
filtering = {
|
||||
"slug": ALL,
|
||||
"name": ALL,
|
||||
"desc": ALL,
|
||||
"used": ALL,
|
||||
"order": ALL,
|
||||
}
|
||||
api.name.register(ProceedingsMaterialTypeNameResource())
|
||||
|
|
|
@ -669,6 +669,7 @@ BOFREQ_PATH = '/a/ietfdata/doc/bofreq/'
|
|||
CONFLICT_REVIEW_PATH = '/a/ietfdata/doc/conflict-review'
|
||||
STATUS_CHANGE_PATH = '/a/ietfdata/doc/status-change'
|
||||
AGENDA_PATH = '/a/www/www6s/proceedings/'
|
||||
MEETINGHOST_LOGO_PATH = AGENDA_PATH # put these in the same place as other proceedings files
|
||||
IPR_DOCUMENT_PATH = '/a/www/ietf-ftp/ietf/IPR/'
|
||||
IESG_TASK_FILE = '/a/www/www6/iesg/internal/task.txt'
|
||||
IESG_ROLL_CALL_FILE = '/a/www/www6/iesg/internal/rollcall.txt'
|
||||
|
@ -702,6 +703,7 @@ DOC_HREFS = {
|
|||
"draft": "https://www.ietf.org/archive/id/{doc.name}-{doc.rev}.txt",
|
||||
"rfc": "https://www.rfc-editor.org/rfc/rfc{doc.rfcnum}.txt",
|
||||
"slides": "https://www.ietf.org/slides/{doc.name}-{doc.rev}",
|
||||
"procmaterials": "https://www.ietf.org/procmaterials/{doc.name}-{doc.rev}",
|
||||
"conflrev": "https://www.ietf.org/cr/{doc.name}-{doc.rev}.txt",
|
||||
"statchg": "https://www.ietf.org/sc/{doc.name}-{doc.rev}.txt",
|
||||
"liaison": "%s{doc.uploaded_filename}" % LIAISON_ATTACH_URL,
|
||||
|
@ -885,6 +887,7 @@ MEETING_DOC_LOCAL_HREFS = {
|
|||
"slides": "/meeting/{meeting.number}/materials/{doc.name}-{doc.rev}",
|
||||
"recording": "{doc.external_url}",
|
||||
"bluesheets": "https://www.ietf.org/proceedings/{meeting.number}/bluesheets/{doc.uploaded_filename}",
|
||||
"procmaterials": "/meeting/{meeting.number}/materials/{doc.name}-{doc.rev}",
|
||||
}
|
||||
|
||||
MEETING_DOC_CDN_HREFS = {
|
||||
|
@ -893,6 +896,7 @@ MEETING_DOC_CDN_HREFS = {
|
|||
"slides": "https://www.ietf.org/proceedings/{meeting.number}/slides/{doc.name}-{doc.rev}",
|
||||
"recording": "{doc.external_url}",
|
||||
"bluesheets": "https://www.ietf.org/proceedings/{meeting.number}/bluesheets/{doc.uploaded_filename}",
|
||||
"procmaterials": "https://www.ietf.org/proceedings/{meeting.number}/procmaterials/{doc.name}-{doc.rev}",
|
||||
}
|
||||
|
||||
MEETING_DOC_HREFS = MEETING_DOC_LOCAL_HREFS if MEETING_MATERIALS_SERVE_LOCALLY else MEETING_DOC_CDN_HREFS
|
||||
|
@ -912,6 +916,7 @@ MEETING_DOC_GREFS = {
|
|||
"slides": "/meeting/{meeting.number}/materials/{doc.name}",
|
||||
"recording": "{doc.external_url}",
|
||||
"bluesheets": "https://www.ietf.org/proceedings/{meeting.number}/bluesheets/{doc.uploaded_filename}",
|
||||
"procmaterials": "/meeting/{meeting.number}/materials/{doc.name}",
|
||||
}
|
||||
|
||||
MEETING_MATERIALS_DEFAULT_SUBMISSION_START_DAYS = 90
|
||||
|
@ -923,6 +928,8 @@ MEETING_VALID_UPLOAD_EXTENSIONS = {
|
|||
'minutes': ['.txt','.html','.htm', '.md', '.pdf', ],
|
||||
'slides': ['.doc','.docx','.pdf','.ppt','.pptx','.txt', ], # Note the removal of .zip
|
||||
'bluesheets': ['.pdf', '.txt', ],
|
||||
'procmaterials':['.pdf', ],
|
||||
'meetinghostlogo': ['.png', '.jpg', '.jpeg'],
|
||||
}
|
||||
|
||||
MEETING_VALID_UPLOAD_MIME_TYPES = {
|
||||
|
@ -930,6 +937,8 @@ MEETING_VALID_UPLOAD_MIME_TYPES = {
|
|||
'minutes': ['text/plain', 'text/html', 'application/pdf', 'text/markdown', 'text/x-markdown', ],
|
||||
'slides': [],
|
||||
'bluesheets': ['application/pdf', 'text/plain', ],
|
||||
'procmaterials':['application/pdf', ],
|
||||
'meetinghostlogo': ['image/jpeg', 'image/png', ],
|
||||
}
|
||||
|
||||
MEETING_VALID_MIME_TYPE_EXTENSIONS = {
|
||||
|
@ -953,6 +962,14 @@ FLOORPLAN_DIR = os.path.join(MEDIA_ROOT, FLOORPLAN_MEDIA_DIR)
|
|||
|
||||
MEETING_USES_CODIMD_DATE = datetime.date(2020,7,6)
|
||||
|
||||
# Maximum dimensions to accept at all
|
||||
MEETINGHOST_LOGO_MAX_UPLOAD_WIDTH = 400
|
||||
MEETINGHOST_LOGO_MAX_UPLOAD_HEIGHT = 400
|
||||
|
||||
# Maximum dimensions to display
|
||||
MEETINGHOST_LOGO_MAX_DISPLAY_WIDTH = 120
|
||||
MEETINGHOST_LOGO_MAX_DISPLAY_HEIGHT = 120
|
||||
|
||||
# Session assignments on the official schedule lock this long before the timeslot starts
|
||||
MEETING_SESSION_LOCK_TIME = datetime.timedelta(minutes=10)
|
||||
|
||||
|
@ -994,7 +1011,12 @@ SECR_PROCEEDINGS_DIR = '/a/www/www6s/proceedings/'
|
|||
SECR_PPT2PDF_COMMAND = ['/usr/bin/soffice','--headless','--convert-to','pdf:writer_globaldocument_pdf_Export','--outdir']
|
||||
SECR_VIRTUAL_MEETINGS = ['108']
|
||||
STATS_REGISTRATION_ATTENDEES_JSON_URL = 'https://registration.ietf.org/{number}/attendees/'
|
||||
NEW_PROCEEDINGS_START = 95
|
||||
PROCEEDINGS_VERSION_CHANGES = [
|
||||
0, # version 1
|
||||
97, # version 2: meeting 97 and later (was number was NEW_PROCEEDINGS_START)
|
||||
111, # version 3: meeting 111 and later
|
||||
]
|
||||
PROCEEDINGS_V1_BASE_URL = 'https://www.ietf.org/proceedings/{meeting.number}'
|
||||
YOUTUBE_API_KEY = ''
|
||||
YOUTUBE_API_SERVICE_NAME = 'youtube'
|
||||
YOUTUBE_API_VERSION = 'v3'
|
||||
|
|
60
ietf/static/ietf/js/upload-material.js
Normal file
60
ietf/static/ietf/js/upload-material.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
// Copyright The IETF Trust 2021, All Rights Reserved
|
||||
(
|
||||
function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Hide the inactive input form-group
|
||||
* @param form form to process
|
||||
*/
|
||||
function showUrlOrFile(form) {
|
||||
const useUrlInput = form.elements.namedItem('use_url');
|
||||
const urlGroup = form.elements.namedItem('external_url').closest('.form-group');
|
||||
const fileGroup = form.elements.namedItem('file').closest('.form-group');
|
||||
|
||||
if (useUrlInput.checked) {
|
||||
urlGroup.hidden = false;
|
||||
fileGroup.hidden = true;
|
||||
} else {
|
||||
urlGroup.hidden = true;
|
||||
fileGroup.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch showUrlOrFile from a UI event on the enclosing form
|
||||
* @param evt change event instance
|
||||
*/
|
||||
function handleFormChange(evt) {
|
||||
showUrlOrFile(evt.currentTarget); // currentTarget is the form
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear hidden file input values before submitting form to avoid
|
||||
* needlessly sending a file when use_url is selected
|
||||
* @param evt submit event instance
|
||||
*/
|
||||
function handleFormSubmit(evt) {
|
||||
const form = evt.currentTarget;
|
||||
const fileInput = form.elements.namedItem('file');
|
||||
if (fileInput.hidden) {
|
||||
fileInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register event handlers and other initialization tasks.
|
||||
*/
|
||||
function initialize() {
|
||||
const forms = document.querySelectorAll('form.upload-material');
|
||||
for (let i = 0; i < forms.length; i++) {
|
||||
const form = forms[i];
|
||||
form.addEventListener('change', handleFormChange);
|
||||
form.addEventListener('submit', handleFormSubmit);
|
||||
showUrlOrFile(form);
|
||||
}
|
||||
}
|
||||
|
||||
initialize();
|
||||
}
|
||||
)();
|
|
@ -35,9 +35,10 @@
|
|||
<th class="col-md-1">{% if doc.meeting_related %}Meeting{% endif %} {{ doc.type.name }}</th>
|
||||
<td class="edit col-md-1"></td>
|
||||
<td class="col-md-10">
|
||||
{{ doc.group.name }}
|
||||
<a href="{{ doc.group.about_url }}">({{ doc.group.acronym }})</a> {{ doc.group.type.name }}
|
||||
|
||||
{% if doc.group %}
|
||||
{{ doc.group.name }}
|
||||
<a href="{{ doc.group.about_url }}">({{ doc.group.acronym }})</a> {{ doc.group.type.name }}
|
||||
{% endif %}
|
||||
{% if snapshot %}
|
||||
<span class="label label-warning">Snapshot</span>
|
||||
{% endif %}
|
||||
|
@ -88,25 +89,37 @@
|
|||
</tr>
|
||||
{% endif %}
|
||||
|
||||
{% if presentations or can_manage_material %}
|
||||
{% if doc.type_id == 'procmaterials' and doc.external_url|length > 0 %}
|
||||
<tr>
|
||||
<th>On agenda</th>
|
||||
<td class="edit">
|
||||
{% if not snapshot and can_manage_material %}
|
||||
{% doc_edit_button "ietf.doc.views_doc.all_presentations" name=doc.name %}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<th>External URL</th>
|
||||
<td class="edit"></td>
|
||||
<td>
|
||||
{% if presentations %}
|
||||
{% for pres in presentations %}{{ pres.session.short_name }} at {{ pres.session.meeting }} {% if pres.rev != doc.rev %}(version -{{ pres.rev }}){% endif %}{% if not forloop.last %}, {% endif %}{% endfor %}
|
||||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
<a href="{{ doc.external_url }}">{{ doc.external_url }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
{% if doc.type_id != 'procmaterials' %}
|
||||
{% if presentations or can_manage_material %}
|
||||
<tr>
|
||||
<th>On agenda</th>
|
||||
<td class="edit">
|
||||
{% if not snapshot and can_manage_material %}
|
||||
{% doc_edit_button "ietf.doc.views_doc.all_presentations" name=doc.name %}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{% if presentations %}
|
||||
{% for pres in presentations %}{{ pres.session.short_name }} at {{ pres.session.meeting }} {% if pres.rev != doc.rev %}(version -{{ pres.rev }}){% endif %}{% if not forloop.last %}, {% endif %}{% endfor %}
|
||||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
<th>Last updated</th>
|
||||
<td class="edit"></td>
|
||||
|
@ -116,7 +129,7 @@
|
|||
</table>
|
||||
|
||||
<p class="buttonlist">
|
||||
{% if not snapshot and can_manage_material and in_group_materials_types %}
|
||||
{% if can_upload %}
|
||||
<a class="btn btn-default" href="{% url 'ietf.doc.views_material.edit_material' name=doc.name action="revise" %}">Upload New Revision</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
|
|
|
@ -4,25 +4,54 @@
|
|||
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %}{% if action == "new" or action == "revise" %}Upload{% else %}Edit{% endif %} {{ document_type.name }} for group {{ group.name }} ({{ group.acronym }}){% endblock %}
|
||||
{% block title %}
|
||||
{% if action == "new" or action == "revise" %}
|
||||
Upload
|
||||
{% else %}
|
||||
Edit
|
||||
{% endif %}
|
||||
{{ doc.type.name }} for
|
||||
{% if group is not None %}group {{ group.name }} ({{ group.acronym }})
|
||||
{% elif doc.meeting_related %}{{ doc.get_related_meeting }} {% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
|
||||
<h1>{% if action == "new" or action == "revise" %}Upload{% else %}Edit{% endif %} {{ document_type.name }}<br><small>{{ group.name }} ({{ group.acronym }})</small></h1>
|
||||
<h1>
|
||||
{% if action == "new" or action == "revise" %}
|
||||
Upload
|
||||
{% else %}
|
||||
Edit
|
||||
{% endif %}
|
||||
{{ doc.type.name }}
|
||||
<br><small>
|
||||
{% if group is not None %}{{ group.name }} ({{ group.acronym }})
|
||||
{% elif doc.meeting_related %}{{ doc.get_related_meeting }}
|
||||
{% if doc.get_related_proceedings_material %} {{ doc.get_related_proceedings_material }}{% endif %}
|
||||
{% endif %}
|
||||
</small></h1>
|
||||
|
||||
{% if action == "new" %}
|
||||
<p class="help-block">
|
||||
Below you can upload a document for the group {{ group.name }}
|
||||
<a href="{% url "ietf.group.views.materials" acronym=group.acronym %}">({{ group.acronym }})</a>.
|
||||
The document will appear under the materials tab in the group pages.
|
||||
{% if group is not None %}
|
||||
Below you can upload a document for the group {{ group.name }}
|
||||
<a href="{% url "ietf.group.views.materials" acronym=group.acronym %}">({{ group.acronym }})</a>.
|
||||
The document will appear under the materials tab in the group pages.
|
||||
{% elif doc.meeting_related %}
|
||||
Below you can upload a document for the {{ doc.get_related_meeting }} meeting.
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<h2>Upload</h2>
|
||||
{% elif action == "revise" %}
|
||||
<p>
|
||||
Below you can upload a new revision of {{ doc_name }} for the group {{ group.name }}
|
||||
<a href="{% url "ietf.group.views.materials" acronym=group.acronym %}">({{ group.acronym }})</a>.
|
||||
{% if group is not None %}
|
||||
Below you can upload a new revision of {{ doc_name }} for the group {{ group.name }}
|
||||
<a href="{% url "ietf.group.views.materials" acronym=group.acronym %}">({{ group.acronym }})</a>.
|
||||
{% elif doc.meeting_related %}
|
||||
Below you can upload a new revision of {{ doc_name }} for the {{ doc.get_related_meeting }} meeting.
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<h2>Upload New Revision</h2>
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
|
||||
<p>
|
||||
{% if user|has_role:"Secretariat" %}
|
||||
<a class="btn btn-default" href="{% url 'ietf.meeting.views_proceedings.edit_meetinghosts' num=meeting.number %}">Edit meeting hosts</a>
|
||||
<a class="btn btn-default" href="{% url 'ietf.secr.proceedings.views.main' %}">Secretariat proceedings functions</a>
|
||||
{% if meeting.end_date.today > meeting.end_date %}
|
||||
<a class="btn btn-default" href="{% url 'ietf.meeting.views.request_minutes' num=meeting.number %}">Send request for minutes</a>
|
||||
|
@ -37,6 +38,8 @@
|
|||
<a class="btn btn-default" href="/meeting/{{meeting.number}}/requests">Meeting requests/conflicts</a>
|
||||
</p>
|
||||
|
||||
{% include 'meeting/proceedings/materials_table.html' with meeting=meeting proceedings_materials=proceedings_materials user=user only %}
|
||||
|
||||
{% with "True" as show_agenda %}
|
||||
<!-- Plenaries -->
|
||||
{% if plenaries %}
|
||||
|
|
|
@ -4,6 +4,45 @@
|
|||
|
||||
{% load ietf_filters static %}
|
||||
|
||||
{% block morecss %}
|
||||
.proceedings-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
.proceedings-title > h1 {font-size: xx-large; margin-bottom: 0;}
|
||||
.proceedings-date {font-size: x-large;}
|
||||
.proceedings-intro {
|
||||
font-size: large;
|
||||
display: flex;
|
||||
justify-content:
|
||||
space-around;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
.proceedings-intro.with-divider {border-bottom-style: solid; border-width: 1px;}
|
||||
.proceedings-intro > .proceedings-column {display: flex; flex-direction: column;}
|
||||
.proceedings-intro > .proceedings-column > .proceedings-row {display: flex;}
|
||||
.finalize-button {position: absolute; top: 0; right: 0;}
|
||||
.proceedings-intro .host-logo {
|
||||
max-height: {{ meetinghost_logo.max_height }}px;
|
||||
max-width: {{ meetinghost_logo.max_width }}px;
|
||||
overflow: hidden;
|
||||
margin: 0 0 0 1rem;
|
||||
}
|
||||
{# Resize logo so longest edge matches the display size, maintaining aspect ratio. #}
|
||||
{% widthratio meetinghost_logo.max_width meetinghost_logo.max_height 1 as displayed_aspect %}
|
||||
{% for host in meeting.meetinghosts.all %}
|
||||
{% widthratio host.logo.width host.logo.height 1 as logo_aspect %}
|
||||
.host-logo img.host{{ forloop.counter }} {
|
||||
{% if logo_aspect > displayed_aspect %}width: 100%; height: auto;{% else %}width: auto; height: 100%;{% endif %}
|
||||
}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block pagehead %}
|
||||
<link rel="stylesheet" href="{% static "jquery.tablesorter/css/theme.bootstrap.min.css" %}">
|
||||
{% endblock %}
|
||||
|
@ -17,26 +56,19 @@
|
|||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
|
||||
<h1>IETF {{ meeting.number }} {% if not meeting.proceedings_final %}Draft{% endif %} Proceedings
|
||||
{% if user|has_role:"Secretariat" and not meeting.proceedings_final %}
|
||||
<a class="btn btn-default" href="{% url 'ietf.meeting.views.finalize_proceedings' num=meeting.number %}">Finalize Proceedings</a>
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% if user|has_role:"Secretariat" and not meeting.proceedings_final %}
|
||||
<a class="btn btn-default finalize-button"
|
||||
href="{% url 'ietf.meeting.views.finalize_proceedings' num=meeting.number %}">
|
||||
Finalize Proceedings
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{# cache for 15 minutes, as long as there's no proceedings activity. takes 4-8 seconds to generate. #}
|
||||
{% load cache %}
|
||||
{% cache 900 ietf_meeting_proceedings meeting.number cache_version %}
|
||||
|
||||
{% if meeting.proceedings_final %}
|
||||
<h2 class="anchor-target" id="introduction">Introduction</h2>
|
||||
<div>
|
||||
<a href="{% url 'ietf.meeting.views.proceedings_acknowledgements' num=meeting.number %}">Acknowledgements</a><br>
|
||||
<a href="{% url 'ietf.meeting.views.proceedings_overview' num=meeting.number %}">IETF Overview</a><br>
|
||||
<a href="{% url 'ietf.meeting.views.proceedings_progress_report' num=meeting.number %}">Progress Report</a><br>
|
||||
<a href="{% url 'ietf.meeting.views.proceedings_attendees' num=meeting.number %}">Attendees</a><br>
|
||||
<a href="{% url 'ietf.meeting.views.important_dates' num=meeting.number %}">Important Dates</a><br>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include 'meeting/proceedings/title.html' with meeting=meeting attendance=attendance only %}
|
||||
{% include 'meeting/proceedings/introduction.html' with meeting=meeting only %}
|
||||
|
||||
{% with "True" as show_agenda %}
|
||||
<!-- Plenaries -->
|
||||
|
|
13
ietf/templates/meeting/proceedings/edit_material.html
Normal file
13
ietf/templates/meeting/proceedings/edit_material.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
{% extends "meeting/proceedings/edit_material_base.html" %}
|
||||
{# Copyright The IETF Trust 2021, All Rights Reserved #}
|
||||
{% load tz %}
|
||||
|
||||
{% block intro %}
|
||||
<p class="help-block">
|
||||
{% if material.active %}
|
||||
This item will be listed on the proceedings as "{{ material }}". To change this, set the title below.<br>
|
||||
{% else %}
|
||||
This item currently will not appear on the proceedings.<br>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endblock %}
|
59
ietf/templates/meeting/proceedings/edit_material_base.html
Normal file
59
ietf/templates/meeting/proceedings/edit_material_base.html
Normal file
|
@ -0,0 +1,59 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2021, All Rights Reserved #}
|
||||
{% load origin tz %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %}
|
||||
Edit {{ material_type.name }} for {{ meeting }} Proceedings
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
|
||||
<h1>
|
||||
{% block content_header %}
|
||||
Edit Proceedings Material<br>
|
||||
<small>
|
||||
{{ meeting }} {{ material_type.name }}
|
||||
</small>
|
||||
{% endblock %}
|
||||
</h1>
|
||||
|
||||
{% if meeting.proceedings_final %}
|
||||
<div class="alert alert-warning">
|
||||
The proceedings for this meeting have already been finalized.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if material is not None %}
|
||||
<p>
|
||||
{% if material.active %}
|
||||
{% if material.is_url %} An external URL for this material was set at
|
||||
{% else %} A file for this material type was uploaded at {% endif %}
|
||||
{% with tm=material.document.time|utc %}
|
||||
{{ tm|date:"H:i:s" }} UTC on {{ tm|date:"Y-m-d" }}.{% endwith %}
|
||||
{% else %}
|
||||
This material has been removed and will not appear in the proceedings.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% block intro %}{% endblock %}
|
||||
|
||||
{% block edit_form %}
|
||||
<form class="edit-proceedings-material" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% bootstrap_form form %}
|
||||
|
||||
{# To replace the form but keep default buttons, use block.super in the edit_form block #}
|
||||
{% block form_buttons %}
|
||||
{% buttons %}
|
||||
<a class="btn btn-default pull-right"
|
||||
href="{% url 'ietf.meeting.views_proceedings.material_details' num=meeting.number %}">Back</a>
|
||||
<button class="btn btn-primary" type="submit">{% firstof submit_button_label 'Save' %}</button>
|
||||
{% endbuttons %}
|
||||
{% endblock %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endblock content %}
|
59
ietf/templates/meeting/proceedings/edit_meetinghosts.html
Normal file
59
ietf/templates/meeting/proceedings/edit_meetinghosts.html
Normal file
|
@ -0,0 +1,59 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2021, All Rights Reserved #}
|
||||
{% load origin bootstrap3 %}
|
||||
|
||||
{% block morecss %}
|
||||
img.logo {max-width: 30rem; max-height: 30rem;}
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
Edit Hosts for {{ meeting }} Proceedings
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
|
||||
<h1>
|
||||
Edit Meeting Hosts<br>
|
||||
<small>
|
||||
{{ meeting }}
|
||||
</small>
|
||||
</h1>
|
||||
|
||||
{% if meeting.proceedings_final %}
|
||||
<div class="alert alert-warning">
|
||||
The proceedings for this meeting have already been finalized.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p class="help-block">
|
||||
The entries below will appear on the proceedings as meeting hosts.
|
||||
If you need to add more than there are slots, fill out the form below, save, and
|
||||
reopen this page.
|
||||
</p>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ formset.management_form }}
|
||||
{{ formset.non_form_errors }}
|
||||
<table class="table table-striped container-fluid">
|
||||
{% for form in formset %}
|
||||
<tr class="row">
|
||||
<td class="col-md-3">
|
||||
{% if form.instance.pk and form.instance.logo %}
|
||||
<img class="logo"
|
||||
src="{% url 'ietf.meeting.views_proceedings.meetinghost_logo' num=meeting.number host_id=form.instance.pk %} ">
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="col-md-9">
|
||||
{% bootstrap_form form %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% buttons %}
|
||||
<a class="btn btn-default pull-right" href="{% url 'ietf.meeting.views.materials' num=meeting.number %}">Back</a>
|
||||
<button class="btn btn-primary" type="submit">Save</button>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
{% endblock content %}
|
57
ietf/templates/meeting/proceedings/introduction.html
Normal file
57
ietf/templates/meeting/proceedings/introduction.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
{% comment %} Copyright The IETF Trust 2021, All Rights Reserved
|
||||
|
||||
This renders the list of links below the title on the meeting proceedings page.
|
||||
{% endcomment %}
|
||||
<div class="proceedings-intro">
|
||||
<div class="proceedings-column">
|
||||
<div class="proceedings-row">
|
||||
<a href="{% url 'ietf.meeting.views.proceedings_overview' num=meeting.number %}">
|
||||
IETF Overview
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="proceedings-row">
|
||||
<a href="{% url 'ietf.meeting.views.proceedings_attendees' num=meeting.number %}">
|
||||
Participants
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="proceedings-row">
|
||||
<a href="{% url 'ietf.meeting.views.agenda' num=meeting.number %}">
|
||||
Meeting Agenda
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="proceedings-row">
|
||||
<a href="{% url 'ietf.meeting.views.proceedings_progress_report' num=meeting.number %}">
|
||||
Activity Report
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="proceedings-row">
|
||||
<a href="{% url 'ietf.meeting.views.important_dates' num=meeting.number %}">
|
||||
Important Dates
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if meeting.proceedings_format_version < 3 %}
|
||||
<div class="proceedings-row">
|
||||
<a href="{% url 'ietf.meeting.views.proceedings_acknowledgements' num=meeting.number %}">
|
||||
Acknowledgements
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% with materials=meeting.get_proceedings_materials %}
|
||||
{% if materials|length > 0 %}
|
||||
<div class="proceedings-column">
|
||||
{% for mat in materials %}
|
||||
<div class="proceedings-row proceedings-material">
|
||||
<a href="{{ mat.get_href }}">{{ mat }}</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
152
ietf/templates/meeting/proceedings/material_details.html
Normal file
152
ietf/templates/meeting/proceedings/material_details.html
Normal file
|
@ -0,0 +1,152 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015-2021, All Rights Reserved #}
|
||||
{% load origin ietf_filters static tz %}
|
||||
|
||||
{% block title %}{{ meeting }} : Proceedings Materials{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
|
||||
|
||||
|
||||
<h1>{{ meeting }} : Proceedings Materials</h1>
|
||||
|
||||
{% if meeting.proceedings_final %}
|
||||
<div class="alert alert-warning">
|
||||
The proceedings have been finalized for this meeting.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Proceedings Materials</div>
|
||||
<div class="panel-body">
|
||||
<table class="table table-condensed table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Title</th>
|
||||
<th>Document</th>
|
||||
<th>Updated</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for mat_type, mat in proceedings_materials %}
|
||||
<tr>
|
||||
<td>{{ mat_type }}</td>
|
||||
{% if mat and mat.active %}
|
||||
{% url 'ietf.doc.views_doc.document_main' name=mat.document.name as url %}
|
||||
<td>
|
||||
<a href="{{ mat.document.get_href }}">{{ mat }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url }}">{{ mat.document }}</a>
|
||||
{% if mat.is_url %} (external URL) {% else %} (uploaded file) {% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% with timestamp=mat.document.time|utc %}
|
||||
{{ timestamp|date:"Y-m-d" }}<br><small>{{ timestamp|date:"H:i:s" }} UTC</small>
|
||||
{% endwith %}
|
||||
</td>
|
||||
{% else %}
|
||||
<td colspan="3"></td>
|
||||
{% endif %}
|
||||
|
||||
{% if user|has_role:"Secretariat" %}
|
||||
<td class="col-md-2">
|
||||
{% url 'ietf.meeting.views_proceedings.upload_material' num=meeting.number material_type=mat_type.slug as upload_url %}
|
||||
{% url 'ietf.meeting.views_proceedings.edit_material' num=meeting.number material_type=mat_type.slug as edit_url %}
|
||||
{% url 'ietf.meeting.views_proceedings.remove_material' num=meeting.number material_type=mat_type.slug as remove_url %}
|
||||
{% url 'ietf.meeting.views_proceedings.restore_material' num=meeting.number material_type=mat_type.slug as restore_url %}
|
||||
{% if mat is None %}
|
||||
<a class="btn btn-default btn-sm pull-right" href="{{ upload_url }}">Add Material</a>
|
||||
{% elif mat.active %}
|
||||
<a class="btn btn-default btn-sm pull-right" href="{{ upload_url }}">Replace Material</a>
|
||||
<a class="btn btn-default btn-sm pull-right" href="{{ edit_url }}">Change title</a>
|
||||
<a class="btn btn-default btn-sm pull-right" href="{{ remove_url }}">Remove</a>
|
||||
{% else %}
|
||||
<a class="btn btn-default btn-sm pull-right" href="{{ upload_url }}">Add Material</a>
|
||||
<a class="btn btn-default btn-sm pull-right" href="{{ restore_url }}">Restore</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="btn btn-default pull-right" href="{% url 'ietf.meeting.views.materials' num=meeting.number %}">Back</a>
|
||||
{% endblock %}
|
||||
|
||||
{% comment %}{% block js %}
|
||||
{% if can_manage_materials %}
|
||||
<script type="text/javascript" src="{% static 'jquery/jquery.min.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js-cookie/src/js.cookie.js' %}"></script>
|
||||
<script type="text/javascript" src={% static 'Sortable/Sortable.min.js' %}></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
|
||||
$.ajaxSetup({
|
||||
crossDomain: false,
|
||||
beforeSend: function (xhr, settings) {
|
||||
if (!csrfSafeMethod(settings.type)) {
|
||||
xhr.setRequestHeader('X-CSRFToken', Cookies.get('csrftoken'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var sortables = [];
|
||||
var options = {
|
||||
group: 'slides',
|
||||
animation: 150,
|
||||
onAdd: function (event) {onAdd(event)},
|
||||
onRemove: function (event) {onRemove(event)},
|
||||
onEnd: function (event) {onEnd(event)}
|
||||
};
|
||||
|
||||
function onAdd(event) {
|
||||
var old_session = event.from.getAttribute('session');
|
||||
var new_session = event.to.getAttribute('session');
|
||||
$.post(event.to.getAttribute('addToSession'), {
|
||||
'order': event.newIndex + 1,
|
||||
'name': event.item.getAttribute('name')
|
||||
});
|
||||
$(event.item).find('td:eq(1)').find('a').each(function () {
|
||||
$(this).attr('href', $(this).attr('href').replace(old_session, new_session));
|
||||
});
|
||||
}
|
||||
|
||||
function onRemove(event) {
|
||||
var old_session = event.from.getAttribute('session');
|
||||
$.post(event.from.getAttribute('removeFromSession'), {
|
||||
'oldIndex': event.oldIndex + 1,
|
||||
'name': event.item.getAttribute('name')
|
||||
});
|
||||
}
|
||||
|
||||
function onEnd(event) {
|
||||
if (event.to == event.from) {
|
||||
$.post(event.from.getAttribute('reorderInSession'), {
|
||||
'oldIndex': event.oldIndex + 1,
|
||||
'newIndex': event.newIndex + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
|
||||
$('.slides tbody').each(function () {
|
||||
sortables.push(Sortable.create(this, options));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endcomment %}
|
57
ietf/templates/meeting/proceedings/materials_table.html
Normal file
57
ietf/templates/meeting/proceedings/materials_table.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
{# Copyright The IETF Trust 2021, All Rights Reserved #}
|
||||
{% load ietf_filters misc_filters tz %}
|
||||
{# only show if user is secretariat or at least one material is active #}
|
||||
{% if proceedings_materials|list_extract:1|keep_only:'active' or user|has_role:'Secretariat' %}
|
||||
<!-- Proceedings materials not tied to groups -->
|
||||
<h2 class="anchor-target" id="proceedings-materials">Proceedings Materials</h2>
|
||||
<table class="table table-condensed table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if user|has_role:'Secretariat' %}
|
||||
<th class="col-md-2">Type</th>
|
||||
<th class="col-md-8">Title</th>
|
||||
<th class="col-md-1">Updated</th>
|
||||
<th class="col-md-1"> </th>
|
||||
{% else %}
|
||||
<th class="col-md-2">Type</th>
|
||||
<th class="col-md-9">Title</th>
|
||||
<th class="col-md-1">Updated</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for type_name, material in proceedings_materials %}
|
||||
{# secretariat sees empty slots, others do not #}
|
||||
{% if user|has_role:'Secretariat' or meeting and material.active %}
|
||||
<tr>
|
||||
<td>{{ type_name }}</td>
|
||||
{% if material and material.active %}
|
||||
<td>
|
||||
<a
|
||||
href="{{ material.get_href }}">
|
||||
{{ material.document.title }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{% with timestamp=material.document.time|utc %}
|
||||
{{ timestamp|date:"Y-m-d" }}<br><small>{{ timestamp|date:"H:i:s" }} UTC</small>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<td colspan="2"></td>
|
||||
{% endif %}
|
||||
{% if user|has_role:'Secretariat' %}
|
||||
{% if forloop.first %}
|
||||
<td rowspan="{{ proceedings_materials|length }}">
|
||||
<a href="{% url 'ietf.meeting.views_proceedings.material_details' num=meeting.number %}">
|
||||
Edit materials
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
|
@ -0,0 +1,23 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2021, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %}{{ action|capfirst }} {{material.type.name}} for {{ material.meeting }} proceedings{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>{{ action|capfirst }} material</h1>
|
||||
|
||||
<p>{{ action|capfirst }} <strong>{{material}}</strong> for the {{ material.meeting }} proceedings?</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-primary">{{ action|capfirst }} material</button>
|
||||
<a class="btn btn-default" href="{% url 'ietf.meeting.views_proceedings.material_details' num=material.meeting.number %}">Cancel</a>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
52
ietf/templates/meeting/proceedings/title.html
Normal file
52
ietf/templates/meeting/proceedings/title.html
Normal file
|
@ -0,0 +1,52 @@
|
|||
{% comment %} Copyright The IETF Trust 2021, All Rights Reserved
|
||||
|
||||
This renders the title block for the meeting proceedings page.
|
||||
{% endcomment %}
|
||||
{% load ietf_filters %}
|
||||
|
||||
<div class="proceedings-title">
|
||||
<h1>
|
||||
IETF {{ meeting.number }} {% if not meeting.proceedings_final %}Draft{% endif %}
|
||||
Proceedings
|
||||
</h1>
|
||||
|
||||
<div class="proceedings-date">
|
||||
{{ meeting.date|date:"d F Y" }} - {{ meeting.end_date|date:"d F Y" }}
|
||||
</div>
|
||||
{% if attendance is not None %}
|
||||
<div class="proceedings-info">
|
||||
{% if attendance.onsite > 0 %}{{ attendance.onsite }} onsite participant{{ attendance.onsite|pluralize }}{% if attendance.online > 0 %},{% endif %}{% endif %}
|
||||
{% if attendance.online > 0 %}{{ attendance.online }} online participant{{ attendance.online|pluralize }}{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="proceedings-intro with-divider">
|
||||
<div class="proceedings-column">
|
||||
<div class="proceedings-row">
|
||||
Location: {{ meeting.city|default:"TBD" }}
|
||||
</div>
|
||||
{% if meeting.venue_name %}
|
||||
<div class="proceedings-row">
|
||||
Venue: {{ meeting.venue_name }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% with hosts=meeting.meetinghosts.all %}
|
||||
{% if hosts.exists %}
|
||||
<div class="proceedings-column">
|
||||
<div class="proceedings-row">
|
||||
Hosted by:
|
||||
{% for host in hosts %}
|
||||
<div class="host-logo">
|
||||
<img class="host{{ forloop.counter }}"
|
||||
src="{% url 'ietf.meeting.views_proceedings.meetinghost_logo' num=meeting.number host_id=host.pk %}"
|
||||
height="{{ host.logo_height }}"
|
||||
width="{{ host.logo_width }}"
|
||||
title="{{ host.name }}">
|
||||
</div>{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
43
ietf/templates/meeting/proceedings/upload_material.html
Normal file
43
ietf/templates/meeting/proceedings/upload_material.html
Normal file
|
@ -0,0 +1,43 @@
|
|||
{% extends "meeting/proceedings/edit_material_base.html" %}
|
||||
{# Copyright The IETF Trust 2015-2021, All Rights Reserved #}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block morecss %}
|
||||
{{ form.media.css }}
|
||||
{% endblock %}
|
||||
{% block title %}
|
||||
Upload {{ material_type.name }} for {{ meeting }} Proceedings
|
||||
{% endblock %}
|
||||
|
||||
{% block content_header %}
|
||||
Upload Proceedings Material<br>
|
||||
<small>
|
||||
{{ meeting }} {{ material_type.name }}
|
||||
</small>
|
||||
{% endblock %}
|
||||
|
||||
{% block intro %}
|
||||
<p>
|
||||
{% if material is None %}
|
||||
This will be linked from the {{ meeting }} proceedings under
|
||||
the title "{{ material_type.name }}".
|
||||
{% else %}
|
||||
Select a file or external URL to replace the existing material. This will be linked from the
|
||||
{{ meeting }} proceedings under the title "{{ material.document.title }}".
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block edit_form %}
|
||||
<form class="upload-material" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
|
||||
{% block form_buttons %}{{ block.super }}{% endblock %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ form.media.js }}
|
||||
{% endblock %}
|
|
@ -264,3 +264,19 @@ class SearchableField(forms.CharField):
|
|||
))
|
||||
|
||||
return objs.first() if self.max_entries == 1 else objs
|
||||
|
||||
class MissingOkImageField(models.ImageField):
|
||||
"""Image field that can validate successfully if file goes missing
|
||||
|
||||
The default ImageField fails even to validate if its back-end file goes
|
||||
missing, at least when width_field and height_field are used. This ignores
|
||||
the exception that arises. Without this, even deleting a model instance
|
||||
through a form fails.
|
||||
"""
|
||||
def update_dimension_fields(self, *args, **kwargs):
|
||||
try:
|
||||
super().update_dimension_fields(*args, **kwargs)
|
||||
except FileNotFoundError:
|
||||
pass # don't do anything if the file has gone missing
|
||||
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
from django import template
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
@ -27,3 +28,35 @@ def merge_media(forms, arg=None):
|
|||
if arg is None:
|
||||
return str(combined)
|
||||
return str(combined[arg])
|
||||
|
||||
|
||||
@register.filter
|
||||
def list_extract(items, arg):
|
||||
"""Extract items from a list of containers
|
||||
|
||||
Uses Django template lookup rules: tries list index / dict key lookup first, then
|
||||
tries to getattr. If the result is callable, calls with no arguments and uses the return
|
||||
value..
|
||||
|
||||
Usage: {{ list_of_lists|list_extract:1 }} (gets elt 1 from each item in list)
|
||||
{{ list_of_dicts|list_extract:'key' }} (gets value of 'key' from each dict in list)
|
||||
"""
|
||||
def _extract(item):
|
||||
try:
|
||||
return item[arg]
|
||||
except TypeError:
|
||||
pass
|
||||
attr = getattr(item, arg, None)
|
||||
return attr() if callable(attr) else attr
|
||||
|
||||
return [_extract(item) for item in items]
|
||||
|
||||
@register.filter
|
||||
def keep_only(items, arg):
|
||||
"""Filter list of items based on an attribute
|
||||
|
||||
Usage: {{ item_list|keep_only:'attribute' }}
|
||||
Returns the list, keeping only those whose where item[attribute] or item.attribute is
|
||||
present and truthy. The attribute can be an int or a string.
|
||||
"""
|
||||
return [item for item, value in zip(items, list_extract(items, arg)) if value]
|
||||
|
|
|
@ -10,7 +10,7 @@ from urllib.parse import urlparse, urlsplit, urlunsplit
|
|||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator, URLValidator, EmailValidator, _lazy_re_compile
|
||||
from django.core.validators import RegexValidator, URLValidator, EmailValidator, _lazy_re_compile, BaseValidator
|
||||
from django.template.defaultfilters import filesizeformat
|
||||
from django.utils.deconstruct import deconstructible
|
||||
from django.utils.ipv6 import is_valid_ipv6_address
|
||||
|
@ -58,12 +58,26 @@ class RegexStringValidator(object):
|
|||
|
||||
validate_regular_expression_string = RegexStringValidator()
|
||||
|
||||
def validate_file_size(file):
|
||||
if file.size > settings.SECR_MAX_UPLOAD_SIZE:
|
||||
def validate_file_size(file, missing_ok=False):
|
||||
try:
|
||||
size = file.size
|
||||
except FileNotFoundError:
|
||||
if missing_ok:
|
||||
return
|
||||
else:
|
||||
raise
|
||||
|
||||
if size > settings.SECR_MAX_UPLOAD_SIZE:
|
||||
raise ValidationError('Please keep filesize under %s. Requested upload size was %s' % (filesizeformat(settings.SECR_MAX_UPLOAD_SIZE), filesizeformat(file.size)))
|
||||
|
||||
def validate_mime_type(file, valid):
|
||||
file.open()
|
||||
def validate_mime_type(file, valid, missing_ok=False):
|
||||
try:
|
||||
file.open()
|
||||
except FileNotFoundError:
|
||||
if missing_ok:
|
||||
return None, None
|
||||
else:
|
||||
raise
|
||||
raw = file.read()
|
||||
mime_type, encoding = get_mime_type(raw)
|
||||
# work around mis-identification of text where a line has 'virtual' as
|
||||
|
@ -76,6 +90,28 @@ def validate_mime_type(file, valid):
|
|||
(mime_type, ', '.join(valid) ))
|
||||
return mime_type, encoding
|
||||
|
||||
@deconstructible
|
||||
class WrappedValidator:
|
||||
"""Helper for attaching a validate function with parameters to a model
|
||||
|
||||
This captures extra arguments to migration functions in a way that is compatible
|
||||
with Django's migrations. E.g., WrappedValidator(validate_mime_type, valid_type_list)
|
||||
will arrange to call validate_mime_type.
|
||||
"""
|
||||
def __init__(self, validate_method, *args):
|
||||
self.validate_method = validate_method
|
||||
self.args = args
|
||||
|
||||
def __call__(self, inst):
|
||||
return self.validate_method(inst, *self.args)
|
||||
|
||||
def __eq__(self, other):
|
||||
return all([
|
||||
isinstance(other, WrappedValidator),
|
||||
(self.validate_method == other.validate_method),
|
||||
(self.kwargs == other.kwargs)
|
||||
])
|
||||
|
||||
def validate_file_extension(file, valid):
|
||||
name, ext = os.path.splitext(file.name)
|
||||
if ext.lower() not in valid:
|
||||
|
@ -194,3 +230,21 @@ def validate_external_resource_value(name, value):
|
|||
else:
|
||||
raise ValidationError('Unknown resource type '+name.type.name)
|
||||
|
||||
|
||||
@deconstructible
|
||||
class MaxImageSizeValidator(BaseValidator):
|
||||
"""Validate that an image is no longer than a given size"""
|
||||
message = 'Ensure this image is smaller than %(limit_value)s (it is %(show_value)s)'
|
||||
code = 'max_image_size'
|
||||
|
||||
def __init__(self, max_width, max_height):
|
||||
super().__init__(limit_value=(max_width, max_height))
|
||||
|
||||
def compare(self, a, b):
|
||||
return (a[0] > b[0]) or (a[1] > b[1])
|
||||
|
||||
def clean(self, x):
|
||||
try:
|
||||
return x.width, x.height
|
||||
except FileNotFoundError:
|
||||
return 0, 0 # don't fail if the image is missing
|
||||
|
|
Loading…
Reference in a new issue