diff --git a/ietf/meeting/forms.py b/ietf/meeting/forms.py
index d260793f1..e8a50dd38 100644
--- a/ietf/meeting/forms.py
+++ b/ietf/meeting/forms.py
@@ -6,6 +6,9 @@ import io
import os
import datetime
import json
+import re
+
+from pathlib import Path
from django import forms
from django.conf import settings
@@ -324,14 +327,19 @@ class InterimCancelForm(forms.Form):
self.fields['date'].widget.attrs['disabled'] = True
class FileUploadForm(forms.Form):
+ """Base class for FileUploadForms
+
+ Abstract base class - subclasses must fill in the doc_type value with
+ the type of document they handle.
+ """
file = forms.FileField(label='File to upload')
+ doc_type = '' # subclasses must set this
+
def __init__(self, *args, **kwargs):
- doc_type = kwargs.pop('doc_type')
- assert doc_type in settings.MEETING_VALID_UPLOAD_EXTENSIONS
- self.doc_type = doc_type
- self.extensions = settings.MEETING_VALID_UPLOAD_EXTENSIONS[doc_type]
- self.mime_types = settings.MEETING_VALID_UPLOAD_MIME_TYPES[doc_type]
+ assert self.doc_type in settings.MEETING_VALID_UPLOAD_EXTENSIONS
+ self.extensions = settings.MEETING_VALID_UPLOAD_EXTENSIONS[self.doc_type]
+ self.mime_types = settings.MEETING_VALID_UPLOAD_MIME_TYPES[self.doc_type]
super(FileUploadForm, self).__init__(*args, **kwargs)
label = '%s file to upload. ' % (self.doc_type.capitalize(), )
if self.doc_type == "slides":
@@ -344,6 +352,15 @@ class FileUploadForm(forms.Form):
file = self.cleaned_data['file']
validate_file_size(file)
ext = validate_file_extension(file, self.extensions)
+
+ # override the Content-Type if needed
+ if file.content_type in 'application/octet-stream':
+ content_type_map = settings.MEETING_APPLICATION_OCTET_STREAM_OVERRIDES
+ filename = Path(file.name)
+ if filename.suffix in content_type_map:
+ file.content_type = content_type_map[filename.suffix]
+ self.cleaned_data['file'] = file
+
mime_type, encoding = validate_mime_type(file, self.mime_types)
if not hasattr(self, 'file_encoding'):
self.file_encoding = {}
@@ -351,15 +368,72 @@ class FileUploadForm(forms.Form):
if self.mime_types:
if not file.content_type in settings.MEETING_VALID_UPLOAD_MIME_FOR_OBSERVED_MIME[mime_type]:
raise ValidationError('Upload Content-Type (%s) is different from the observed mime-type (%s)' % (file.content_type, mime_type))
- if mime_type in settings.MEETING_VALID_MIME_TYPE_EXTENSIONS:
- if not ext in settings.MEETING_VALID_MIME_TYPE_EXTENSIONS[mime_type]:
+ # We just validated that file.content_type is safe to accept despite being identified
+ # as a different MIME type by the validator. Check extension based on file.content_type
+ # because that better reflects the intention of the upload client.
+ if file.content_type in settings.MEETING_VALID_MIME_TYPE_EXTENSIONS:
+ if not ext in settings.MEETING_VALID_MIME_TYPE_EXTENSIONS[file.content_type]:
raise ValidationError('Upload Content-Type (%s) does not match the extension (%s)' % (file.content_type, ext))
- if mime_type in ['text/html', ] or ext in settings.MEETING_VALID_MIME_TYPE_EXTENSIONS['text/html']:
+ if (file.content_type in ['text/html', ]
+ or ext in settings.MEETING_VALID_MIME_TYPE_EXTENSIONS.get('text/html', [])):
# We'll do html sanitization later, but for frames, we fail here,
# as the sanitized version will most likely be useless.
validate_no_html_frame(file)
return file
+
+class UploadBlueSheetForm(FileUploadForm):
+ doc_type = 'bluesheets'
+
+
+class ApplyToAllFileUploadForm(FileUploadForm):
+ """FileUploadField that adds an apply_to_all checkbox
+
+ Checkbox can be disabled by passing show_apply_to_all_checkbox=False to the constructor.
+ This entirely removes the field from the form.
+ """
+ # Note: subclasses must set doc_type for FileUploadForm
+ apply_to_all = forms.BooleanField(label='Apply to all group sessions at this meeting',initial=True,required=False)
+
+ def __init__(self, show_apply_to_all_checkbox, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ if not show_apply_to_all_checkbox:
+ self.fields.pop('apply_to_all')
+ else:
+ self.order_fields(
+ sorted(
+ self.fields.keys(),
+ key=lambda f: 'zzzzzz' if f == 'apply_to_all' else f
+ )
+ )
+
+class UploadMinutesForm(ApplyToAllFileUploadForm):
+ doc_type = 'minutes'
+
+
+class UploadAgendaForm(ApplyToAllFileUploadForm):
+ doc_type = 'agenda'
+
+
+class UploadSlidesForm(ApplyToAllFileUploadForm):
+ doc_type = 'slides'
+ title = forms.CharField(max_length=255)
+
+ def __init__(self, session, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.session = session
+
+ def clean_title(self):
+ title = self.cleaned_data['title']
+ # The current tables only handles Unicode BMP:
+ if ord(max(title)) > 0xffff:
+ raise forms.ValidationError("The title contains characters outside the Unicode BMP, which is not currently supported")
+ if self.session.meeting.type_id=='interim':
+ if re.search(r'-\d{2}$', title):
+ raise forms.ValidationError("Interim slides currently may not have a title that ends with something that looks like a revision number (-nn)")
+ return title
+
+
class RequestMinutesForm(forms.Form):
to = MultiEmailField()
cc = MultiEmailField(required=False)
diff --git a/ietf/meeting/tests_forms.py b/ietf/meeting/tests_forms.py
index 416498235..871ad1ea3 100644
--- a/ietf/meeting/tests_forms.py
+++ b/ietf/meeting/tests_forms.py
@@ -1,476 +1,104 @@
# Copyright The IETF Trust 2021, All Rights Reserved
# -*- coding: utf-8 -*-
+"""Tests of forms in the Meeting application"""
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.test import override_settings
-import json
-
-from datetime import date, timedelta
-from unittest.mock import patch
-
-from django import forms
-
-import debug # pyflakes: ignore
-from ietf.group.factories import GroupFactory
-
-from ietf.meeting.factories import MeetingFactory, TimeSlotFactory, RoomFactory, SessionFactory
-from ietf.meeting.forms import (CsvModelPkInput, CustomDurationField, SwapTimeslotsForm, duration_string,
- TimeSlotDurationField, TimeSlotEditForm, TimeSlotCreateForm, DurationChoiceField,
- SessionDetailsForm, sessiondetailsformset_factory, SessionEditForm)
-from ietf.name.models import SessionPurposeName
+from ietf.meeting.forms import FileUploadForm, ApplyToAllFileUploadForm
from ietf.utils.test_utils import TestCase
-class CsvModelPkInputTests(TestCase):
- widget = CsvModelPkInput()
+@override_settings(
+ MEETING_APPLICATION_OCTET_STREAM_OVERRIDES={'.md': 'text/markdown'}, # test relies on .txt not mapping
+ MEETING_VALID_UPLOAD_EXTENSIONS={'minutes': ['.txt', '.md']}, # test relies on .exe being absent
+ MEETING_VALID_UPLOAD_MIME_TYPES={'minutes': ['text/plain', 'text/markdown']},
+ MEETING_VALID_MIME_TYPE_EXTENSIONS={'text/plain': ['.txt'], 'text/markdown': ['.md']},
+ MEETING_VALID_UPLOAD_MIME_FOR_OBSERVED_MIME={'text/plain': ['text/plain', 'text/markdown']},
+)
+class FileUploadFormTests(TestCase):
+ class TestClass(FileUploadForm):
+ doc_type = 'minutes'
- def test_render_none(self):
- result = self.widget.render('csv_model', value=None)
- self.assertHTMLEqual(result, '')
-
- def test_render_value(self):
- result = self.widget.render('csv_model', value=[1, 2, 3])
- self.assertHTMLEqual(result, '')
-
- def test_value_from_datadict(self):
- result = self.widget.value_from_datadict({'csv_model': '11,23,47'}, {}, 'csv_model')
- self.assertEqual(result, ['11', '23', '47'])
-
-
-class SwapTimeslotsFormTests(TestCase):
- def setUp(self):
- super().setUp()
- self.meeting = MeetingFactory(type_id='ietf', populate_schedule=False)
- self.timeslots = TimeSlotFactory.create_batch(2, meeting=self.meeting)
- self.other_meeting_timeslot = TimeSlotFactory()
-
- def test_valid(self):
- form = SwapTimeslotsForm(
- meeting=self.meeting,
- data={
- 'origin_timeslot': str(self.timeslots[0].pk),
- 'target_timeslot': str(self.timeslots[1].pk),
- 'rooms': ','.join(str(rm.pk) for rm in self.meeting.room_set.all()),
- }
- )
- self.assertTrue(form.is_valid())
-
- def test_invalid(self):
- # the magic numbers are (very likely) non-existent pks
- form = SwapTimeslotsForm(
- meeting=self.meeting,
- data={
- 'origin_timeslot': '25',
- 'target_timeslot': str(self.timeslots[1].pk),
- 'rooms': ','.join(str(rm.pk) for rm in self.meeting.room_set.all()),
- }
- )
- self.assertFalse(form.is_valid())
- form = SwapTimeslotsForm(
- meeting=self.meeting,
- data={
- 'origin_timeslot': str(self.timeslots[0].pk),
- 'target_timeslot': str(self.other_meeting_timeslot.pk),
- 'rooms': ','.join(str(rm.pk) for rm in self.meeting.room_set.all()),
- }
- )
- self.assertFalse(form.is_valid())
- form = SwapTimeslotsForm(
- meeting=self.meeting,
- data={
- 'origin_timeslot': str(self.timeslots[0].pk),
- 'target_timeslot': str(self.timeslots[1].pk),
- 'rooms': '1034',
- }
- )
- self.assertFalse(form.is_valid())
-
-
-class CustomDurationFieldTests(TestCase):
- def test_duration_string(self):
- self.assertEqual(duration_string(timedelta(hours=3, minutes=17)), '03:17')
- self.assertEqual(duration_string(timedelta(hours=3, minutes=17, seconds=43)), '03:17')
- self.assertEqual(duration_string(timedelta(days=1, hours=3, minutes=17, seconds=43)), '1 03:17')
- self.assertEqual(duration_string(timedelta(hours=3, minutes=17, seconds=43, microseconds=37438)), '03:17')
-
- def _render_field(self, field):
- """Helper to render a form containing a field"""
-
- class Form(forms.Form):
- f = field
-
- return str(Form()['f'])
-
- @patch('ietf.meeting.forms.duration_string', return_value='12:34')
- def test_render(self, mock_duration_string):
- self.assertHTMLEqual(
- self._render_field(CustomDurationField()),
- ''
- )
- self.assertHTMLEqual(
- self._render_field(CustomDurationField(initial=timedelta(hours=1))),
- '',
- 'Rendered value should come from duration_string when initial value is a timedelta'
- )
- self.assertHTMLEqual(
- self._render_field(CustomDurationField(initial="01:02")),
- '',
- 'Rendered value should come from initial when it is not a timedelta'
+ def test_accepts_valid_data(self):
+ test_file = SimpleUploadedFile(
+ name='file.txt',
+ content=b'plain text',
+ content_type='text/plain',
)
+ form = FileUploadFormTests.TestClass(files={'file': test_file})
+ self.assertTrue(form.is_valid(), 'Test data are valid input')
+ cleaned_file = form.cleaned_data['file']
+ self.assertEqual(cleaned_file.name, 'file.txt', 'Uploaded filename should not be changed')
+ with cleaned_file.open('rb') as f:
+ self.assertEqual(f.read(), b'plain text', 'Uploaded file contents should not be changed')
+ self.assertEqual(cleaned_file.content_type, 'text/plain', 'Content-Type should be overridden')
-class TimeSlotDurationFieldTests(TestCase):
- def test_validation(self):
- field = TimeSlotDurationField()
- with self.assertRaises(forms.ValidationError):
- field.clean('-01:00')
- with self.assertRaises(forms.ValidationError):
- field.clean('12:01')
- self.assertEqual(field.clean('00:00'), timedelta(seconds=0))
- self.assertEqual(field.clean('01:00'), timedelta(hours=1))
- self.assertEqual(field.clean('12:00'), timedelta(hours=12))
-
-
-class TimeSlotEditFormTests(TestCase):
- def test_location_options(self):
- meeting = MeetingFactory(type_id='ietf', populate_schedule=False)
- rooms = [
- RoomFactory(meeting=meeting, capacity=3),
- RoomFactory(meeting=meeting, capacity=123),
- ]
- ts = TimeSlotFactory(meeting=meeting)
- rendered = str(TimeSlotEditForm(instance=ts)['location'])
- # noinspection PyTypeChecker
- self.assertInHTML(
- f'',
- rendered,
- )
- for room in rooms:
- # noinspection PyTypeChecker
- self.assertInHTML(
- f'',
- rendered,
- )
-
-
-class TimeSlotCreateFormTests(TestCase):
- def setUp(self):
- super().setUp()
- self.meeting = MeetingFactory(type_id='ietf', date=date(2021, 11, 16), days=3, populate_schedule=False)
-
- def test_other_date(self):
- room = RoomFactory(meeting=self.meeting)
-
- # no other_date, no day selected
- form = TimeSlotCreateForm(
- self.meeting,
- data={
- 'name': 'time slot',
- 'type': 'regular',
- 'time': '12:00',
- 'duration': '01:00',
- 'locations': [str(room.pk)],
- })
- self.assertFalse(form.is_valid())
-
- # no other_date, day is selected
- form = TimeSlotCreateForm(
- self.meeting,
- data={
- 'name': 'time slot',
- 'type': 'regular',
- 'days': ['738111'], # date(2021,11,17).toordinal()
- 'time': '12:00',
- 'duration': '01:00',
- 'locations': [str(room.pk)],
- })
- self.assertTrue(form.is_valid())
- self.assertNotIn('other_date', form.cleaned_data)
- self.assertEqual(form.cleaned_data['days'], [date(2021, 11, 17)])
-
- # other_date given, no day is selected
- form = TimeSlotCreateForm(
- self.meeting,
- data={
- 'name': 'time slot',
- 'type': 'regular',
- 'time': '12:00',
- 'duration': '01:00',
- 'locations': [str(room.pk)],
- 'other_date': '2021-11-15',
- })
- self.assertTrue(form.is_valid())
- self.assertNotIn('other_date', form.cleaned_data)
- self.assertEqual(form.cleaned_data['days'], [date(2021, 11, 15)])
-
- # day is selected and other_date is given
- form = TimeSlotCreateForm(
- self.meeting,
- data={
- 'name': 'time slot',
- 'type': 'regular',
- 'days': ['738111'], # date(2021,11,17).toordinal()
- 'time': '12:00',
- 'duration': '01:00',
- 'locations': [str(room.pk)],
- 'other_date': '2021-11-15',
- })
- self.assertTrue(form.is_valid())
- self.assertNotIn('other_date', form.cleaned_data)
- self.assertCountEqual(form.cleaned_data['days'], [date(2021, 11, 17), date(2021, 11, 15)])
-
- # invalid other_date, no day selected
- form = TimeSlotCreateForm(
- self.meeting,
- data={
- 'name': 'time slot',
- 'type': 'regular',
- 'time': '12:00',
- 'duration': '01:00',
- 'locations': [str(room.pk)],
- 'other_date': 'invalid',
- })
- self.assertFalse(form.is_valid())
-
- # invalid other_date, day selected
- form = TimeSlotCreateForm(
- self.meeting,
- data={
- 'name': 'time slot',
- 'type': 'regular',
- 'days': ['738111'], # date(2021,11,17).toordinal()
- 'time': '12:00',
- 'duration': '01:00',
- 'locations': [str(room.pk)],
- 'other_date': 'invalid',
- })
- self.assertFalse(form.is_valid())
-
- def test_meeting_days(self):
- form = TimeSlotCreateForm(self.meeting)
- self.assertEqual(
- form.fields['days'].choices,
- [
- ('738110', 'Tuesday (2021-11-16)'),
- ('738111', 'Wednesday (2021-11-17)'),
- ('738112', 'Thursday (2021-11-18)'),
- ],
+ def test_overrides_content_type_application_octet_stream(self):
+ test_file = SimpleUploadedFile(
+ name='file.md',
+ content=b'plain text',
+ content_type='application/octet-stream',
)
- def test_locations(self):
- rooms = RoomFactory.create_batch(5, meeting=self.meeting)
- form = TimeSlotCreateForm(self.meeting)
- self.assertCountEqual(form.fields['locations'].queryset.all(), rooms)
+ form = FileUploadFormTests.TestClass(files={'file': test_file})
+ self.assertTrue(form.is_valid(), 'Test data are valid input')
+ cleaned_file = form.cleaned_data['file']
+ # Test that the test_file is what actually came out of the cleaning process.
+ # This is not technically required here, but the other tests check that test_file's
+ # content_type has not been changed. If cleaning does not modify the content_type
+ # when it succeeds, then those other tests are not actually testing anything.
+ self.assertEqual(cleaned_file, test_file, 'Cleaning should return the file object that was passed in')
+ self.assertEqual(cleaned_file.name, 'file.md', 'Uploaded filename should not be changed')
+ with cleaned_file.open('rb') as f:
+ self.assertEqual(f.read(), b'plain text', 'Uploaded file contents should not be changed')
+ self.assertEqual(cleaned_file.content_type, 'text/markdown', 'Content-Type should be overridden')
-
-class DurationChoiceFieldTests(TestCase):
- def test_choices_default(self):
- f = DurationChoiceField()
- self.assertEqual(f.choices, [('', '--Please select'), ('3600', '1 hour'), ('7200', '2 hours')])
-
- def test_choices(self):
- f = DurationChoiceField([60, 1800, 3600, 5400, 7260, 7261])
- self.assertEqual(
- f.choices,
- [
- ('', '--Please select'),
- ('60', '1 minute'),
- ('1800', '30 minutes'),
- ('3600', '1 hour'),
- ('5400', '1 hour 30 minutes'),
- ('7260', '2 hours 1 minute'),
- ('7261', '2 hours 1 minute'),
- ]
+ def test_overrides_only_application_octet_stream(self):
+ test_file = SimpleUploadedFile(
+ name='file.md',
+ content=b'plain text',
+ content_type='application/json'
)
- def test_bound_value(self):
- class Form(forms.Form):
- f = DurationChoiceField()
- form = Form(data={'f': '3600'})
- self.assertTrue(form.is_valid())
- self.assertEqual(form.cleaned_data['f'], timedelta(hours=1))
- form = Form(data={'f': '7200'})
- self.assertTrue(form.is_valid())
- self.assertEqual(form.cleaned_data['f'], timedelta(hours=2))
- self.assertFalse(Form(data={'f': '3601'}).is_valid())
- self.assertFalse(Form(data={'f': ''}).is_valid())
- self.assertFalse(Form(data={'f': 'bob'}).is_valid())
+ form = FileUploadFormTests.TestClass(files={'file': test_file})
+ self.assertFalse(form.is_valid(), 'Test data are invalid input')
+ self.assertEqual(test_file.name, 'file.md', 'Uploaded filename should not be changed')
+ self.assertEqual(test_file.content_type, 'application/json', 'Uploaded Content-Type should not be changed')
-
-class SessionDetailsFormTests(TestCase):
- def setUp(self):
- super().setUp()
- self.meeting = MeetingFactory(type_id='ietf', populate_schedule=False)
- self.group = GroupFactory()
-
- def test_initial_purpose(self):
- """First session purpose for group should be default"""
- # change the session_purposes GroupFeature to check that it's being used
- self.group.features.session_purposes = ['coding', 'admin', 'closed_meeting']
- self.group.features.save()
- self.assertEqual(SessionDetailsForm(group=self.group).initial['purpose'], 'coding')
- self.group.features.session_purposes = ['admin', 'coding', 'closed_meeting']
- self.group.features.save()
- self.assertEqual(SessionDetailsForm(group=self.group).initial['purpose'], 'admin')
-
- def test_session_purposes(self):
- # change the session_purposes GroupFeature to check that it's being used
- self.group.features.session_purposes = ['coding', 'admin', 'closed_meeting']
- self.group.features.save()
- self.assertCountEqual(
- SessionDetailsForm(group=self.group).fields['purpose'].queryset.values_list('slug', flat=True),
- ['coding', 'admin', 'closed_meeting'],
- )
- self.group.features.session_purposes = ['admin', 'closed_meeting']
- self.group.features.save()
- self.assertCountEqual(
- SessionDetailsForm(group=self.group).fields['purpose'].queryset.values_list('slug', flat=True),
- ['admin', 'closed_meeting'],
+ def test_overrides_only_requested_extensions_when_valid_ext(self):
+ test_file = SimpleUploadedFile(
+ name='file.txt',
+ content=b'plain text',
+ content_type='application/octet-stream',
)
- def test_allowed_types(self):
- """Correct map from SessionPurposeName to allowed TimeSlotTypeName should be sent to JS"""
- # change the allowed map to a known and non-standard arrangement
- SessionPurposeName.objects.filter(slug='regular').update(timeslot_types=['other'])
- SessionPurposeName.objects.filter(slug='admin').update(timeslot_types=['break', 'regular'])
- SessionPurposeName.objects.exclude(slug__in=['regular', 'admin']).update(timeslot_types=[])
- # check that the map we just installed is actually passed along to the JS through a widget attr
- allowed = json.loads(SessionDetailsForm(group=self.group).fields['type'].widget.attrs['data-allowed-options'])
- self.assertEqual(allowed['regular'], ['other'])
- self.assertEqual(allowed['admin'], ['break', 'regular'])
- for purpose in SessionPurposeName.objects.exclude(slug__in=['regular', 'admin']):
- self.assertEqual(allowed[purpose.slug], [])
+ form = FileUploadFormTests.TestClass(files={'file': test_file})
+ self.assertFalse(form.is_valid(), 'Test data are invalid input')
+ self.assertEqual(test_file.name, 'file.txt', 'Uploaded filename should not be changed')
+ self.assertEqual(test_file.content_type, 'application/octet-stream', 'Uploaded Content-Type should not be changed')
- def test_duration_options(self):
- self.assertTrue(self.group.features.acts_like_wg)
- self.assertEqual(
- SessionDetailsForm(group=self.group).fields['requested_duration'].choices,
- [('', '--Please select'), ('3600', '1 hour'), ('7200', '2 hours')],
- )
- self.group.features.acts_like_wg = False
- self.group.features.save()
- self.assertEqual(
- SessionDetailsForm(group=self.group).fields['requested_duration'].choices,
- [('', '--Please select'), ('1800', '30 minutes'),
- ('3600', '1 hour'), ('5400', '1 hour 30 minutes'),
- ('7200', '2 hours'), ('9000', '2 hours 30 minutes'),
- ('10800', '3 hours'), ('12600', '3 hours 30 minutes'),
- ('14400', '4 hours')],
+ def test_overrides_only_requested_extensions_when_invalid_ext(self):
+ test_file = SimpleUploadedFile(
+ name='file.exe',
+ content=b'plain text',
+ content_type='application/octet-stream'
)
- def test_on_agenda(self):
- # new session gets its purpose's on_agenda value when True
- self.assertTrue(SessionPurposeName.objects.get(slug='regular').on_agenda)
- form = SessionDetailsForm(group=self.group, data={
- 'name': 'blah',
- 'purpose': 'regular',
- 'type': 'regular',
- 'requested_duration': '3600',
- })
- self.assertTrue(form.is_valid())
- self.assertTrue(form.cleaned_data['on_agenda'])
-
- # new session gets its purpose's on_agenda value when False
- SessionPurposeName.objects.filter(slug='regular').update(on_agenda=False)
- form = SessionDetailsForm(group=self.group, data={
- 'name': 'blah',
- 'purpose': 'regular',
- 'type': 'regular',
- 'requested_duration': '3600',
- })
- self.assertTrue(form.is_valid())
- self.assertFalse(form.cleaned_data['on_agenda'])
-
- # updated session keeps its on_agenda value, even if it differs from its purpose
- session = SessionFactory(meeting=self.meeting, add_to_schedule=False, on_agenda=True)
- form = SessionDetailsForm(
- group=self.group,
- instance=session,
- data={
- 'name': 'blah',
- 'purpose': 'regular',
- 'type': 'regular',
- 'requested_duration': '3600',
- },
- )
- self.assertTrue(form.is_valid())
- self.assertTrue(form.cleaned_data['on_agenda'])
-
- # session gets purpose's on_agenda value if its purpose changes (changing the
- # purpose away from 'regular' so we can use the 'wg' type group that only allows
- # regular sessions)
- session.purpose_id = 'admin'
- session.save()
- form = SessionDetailsForm(
- group=self.group,
- instance=session,
- data={
- 'name': 'blah',
- 'purpose': 'regular',
- 'type': 'regular',
- 'requested_duration': '3600',
- },
- )
- self.assertTrue(form.is_valid())
- self.assertFalse(form.cleaned_data['on_agenda'])
-
-class SessionEditFormTests(TestCase):
- def test_rejects_group_mismatch(self):
- session = SessionFactory(meeting__type_id='ietf', meeting__populate_schedule=False, add_to_schedule=False)
- other_group = GroupFactory()
- with self.assertRaisesMessage(ValueError, 'Session group does not match group keyword'):
- SessionEditForm(instance=session, group=other_group)
+ form = FileUploadFormTests.TestClass(files={'file': test_file})
+ self.assertFalse(form.is_valid(), 'Test data are invalid input')
+ self.assertEqual(test_file.name, 'file.exe', 'Uploaded filename should not be changed')
+ self.assertEqual(test_file.content_type, 'application/octet-stream', 'Uploaded Content-Type should not be changed')
-class SessionDetailsInlineFormset(TestCase):
- def setUp(self):
- super().setUp()
- self.meeting = MeetingFactory(type_id='ietf', populate_schedule=False)
- self.group = GroupFactory()
+class ApplyToAllFileUploadFormTests(TestCase):
+ class TestClass(ApplyToAllFileUploadForm):
+ doc_type = 'minutes'
- def test_initial_sessions(self):
- """Sessions for the correct meeting and group should be included"""
- sessions = SessionFactory.create_batch(2, meeting=self.meeting, group=self.group, add_to_schedule=False)
- SessionFactory(meeting=self.meeting, add_to_schedule=False) # should be ignored
- SessionFactory(group=self.group, add_to_schedule=False) # should be ignored
- formset_class = sessiondetailsformset_factory()
- formset = formset_class(group=self.group, meeting=self.meeting)
- self.assertCountEqual(formset.queryset.all(), sessions)
+ def test_has_apply_to_all_field_by_default(self):
+ form = ApplyToAllFileUploadFormTests.TestClass(show_apply_to_all_checkbox=True)
+ self.assertIn('apply_to_all', form.fields)
- def test_forms_created_with_group_kwarg(self):
- class MockFormClass(SessionDetailsForm):
- """Mock class to track the group that was passed to the init method"""
- def __init__(self, group, *args, **kwargs):
- self.init_group_argument = group
- super().__init__(group, *args, **kwargs)
-
- with patch('ietf.meeting.forms.SessionDetailsForm', MockFormClass):
- formset_class = sessiondetailsformset_factory()
- formset = formset_class(meeting=self.meeting, group=self.group)
- str(formset) # triggers instantiation of forms
- self.assertGreaterEqual(len(formset), 1)
- for form in formset:
- self.assertEqual(form.init_group_argument, self.group)
-
- def test_add_instance(self):
- session = SessionFactory(meeting=self.meeting, group=self.group, add_to_schedule=False)
- formset_class = sessiondetailsformset_factory()
- formset = formset_class(group=self.group, meeting=self.meeting, data={
- 'session_set-TOTAL_FORMS': '2',
- 'session_set-INITIAL_FORMS': '1',
- 'session_set-0-id': str(session.pk),
- 'session_set-0-name': 'existing',
- 'session_set-0-purpose': 'regular',
- 'session_set-0-type': 'regular',
- 'session_set-0-requested_duration': '3600',
- 'session_set-1-name': 'new',
- 'session_set-1-purpose': 'regular',
- 'session_set-1-type': 'regular',
- 'session_set-1-requested_duration': '3600',
- })
- formset.save()
- # make sure session created
- self.assertEqual(self.meeting.session_set.count(), 2)
- self.assertIn(session, self.meeting.session_set.all())
- self.assertEqual(len(formset.new_objects), 1)
- self.assertEqual(formset.new_objects[0].name, 'new')
- self.assertEqual(formset.new_objects[0].meeting, self.meeting)
- self.assertEqual(formset.new_objects[0].group, self.group)
+ def test_no_show_apply_to_all_field(self):
+ form = ApplyToAllFileUploadFormTests.TestClass(show_apply_to_all_checkbox=False)
+ self.assertNotIn('apply_to_all', form.fields)
diff --git a/ietf/meeting/tests_schedule_forms.py b/ietf/meeting/tests_schedule_forms.py
new file mode 100644
index 000000000..416498235
--- /dev/null
+++ b/ietf/meeting/tests_schedule_forms.py
@@ -0,0 +1,476 @@
+# Copyright The IETF Trust 2021, All Rights Reserved
+# -*- coding: utf-8 -*-
+
+import json
+
+from datetime import date, timedelta
+from unittest.mock import patch
+
+from django import forms
+
+import debug # pyflakes: ignore
+from ietf.group.factories import GroupFactory
+
+from ietf.meeting.factories import MeetingFactory, TimeSlotFactory, RoomFactory, SessionFactory
+from ietf.meeting.forms import (CsvModelPkInput, CustomDurationField, SwapTimeslotsForm, duration_string,
+ TimeSlotDurationField, TimeSlotEditForm, TimeSlotCreateForm, DurationChoiceField,
+ SessionDetailsForm, sessiondetailsformset_factory, SessionEditForm)
+from ietf.name.models import SessionPurposeName
+from ietf.utils.test_utils import TestCase
+
+
+class CsvModelPkInputTests(TestCase):
+ widget = CsvModelPkInput()
+
+ def test_render_none(self):
+ result = self.widget.render('csv_model', value=None)
+ self.assertHTMLEqual(result, '')
+
+ def test_render_value(self):
+ result = self.widget.render('csv_model', value=[1, 2, 3])
+ self.assertHTMLEqual(result, '')
+
+ def test_value_from_datadict(self):
+ result = self.widget.value_from_datadict({'csv_model': '11,23,47'}, {}, 'csv_model')
+ self.assertEqual(result, ['11', '23', '47'])
+
+
+class SwapTimeslotsFormTests(TestCase):
+ def setUp(self):
+ super().setUp()
+ self.meeting = MeetingFactory(type_id='ietf', populate_schedule=False)
+ self.timeslots = TimeSlotFactory.create_batch(2, meeting=self.meeting)
+ self.other_meeting_timeslot = TimeSlotFactory()
+
+ def test_valid(self):
+ form = SwapTimeslotsForm(
+ meeting=self.meeting,
+ data={
+ 'origin_timeslot': str(self.timeslots[0].pk),
+ 'target_timeslot': str(self.timeslots[1].pk),
+ 'rooms': ','.join(str(rm.pk) for rm in self.meeting.room_set.all()),
+ }
+ )
+ self.assertTrue(form.is_valid())
+
+ def test_invalid(self):
+ # the magic numbers are (very likely) non-existent pks
+ form = SwapTimeslotsForm(
+ meeting=self.meeting,
+ data={
+ 'origin_timeslot': '25',
+ 'target_timeslot': str(self.timeslots[1].pk),
+ 'rooms': ','.join(str(rm.pk) for rm in self.meeting.room_set.all()),
+ }
+ )
+ self.assertFalse(form.is_valid())
+ form = SwapTimeslotsForm(
+ meeting=self.meeting,
+ data={
+ 'origin_timeslot': str(self.timeslots[0].pk),
+ 'target_timeslot': str(self.other_meeting_timeslot.pk),
+ 'rooms': ','.join(str(rm.pk) for rm in self.meeting.room_set.all()),
+ }
+ )
+ self.assertFalse(form.is_valid())
+ form = SwapTimeslotsForm(
+ meeting=self.meeting,
+ data={
+ 'origin_timeslot': str(self.timeslots[0].pk),
+ 'target_timeslot': str(self.timeslots[1].pk),
+ 'rooms': '1034',
+ }
+ )
+ self.assertFalse(form.is_valid())
+
+
+class CustomDurationFieldTests(TestCase):
+ def test_duration_string(self):
+ self.assertEqual(duration_string(timedelta(hours=3, minutes=17)), '03:17')
+ self.assertEqual(duration_string(timedelta(hours=3, minutes=17, seconds=43)), '03:17')
+ self.assertEqual(duration_string(timedelta(days=1, hours=3, minutes=17, seconds=43)), '1 03:17')
+ self.assertEqual(duration_string(timedelta(hours=3, minutes=17, seconds=43, microseconds=37438)), '03:17')
+
+ def _render_field(self, field):
+ """Helper to render a form containing a field"""
+
+ class Form(forms.Form):
+ f = field
+
+ return str(Form()['f'])
+
+ @patch('ietf.meeting.forms.duration_string', return_value='12:34')
+ def test_render(self, mock_duration_string):
+ self.assertHTMLEqual(
+ self._render_field(CustomDurationField()),
+ ''
+ )
+ self.assertHTMLEqual(
+ self._render_field(CustomDurationField(initial=timedelta(hours=1))),
+ '',
+ 'Rendered value should come from duration_string when initial value is a timedelta'
+ )
+ self.assertHTMLEqual(
+ self._render_field(CustomDurationField(initial="01:02")),
+ '',
+ 'Rendered value should come from initial when it is not a timedelta'
+ )
+
+
+class TimeSlotDurationFieldTests(TestCase):
+ def test_validation(self):
+ field = TimeSlotDurationField()
+ with self.assertRaises(forms.ValidationError):
+ field.clean('-01:00')
+ with self.assertRaises(forms.ValidationError):
+ field.clean('12:01')
+ self.assertEqual(field.clean('00:00'), timedelta(seconds=0))
+ self.assertEqual(field.clean('01:00'), timedelta(hours=1))
+ self.assertEqual(field.clean('12:00'), timedelta(hours=12))
+
+
+class TimeSlotEditFormTests(TestCase):
+ def test_location_options(self):
+ meeting = MeetingFactory(type_id='ietf', populate_schedule=False)
+ rooms = [
+ RoomFactory(meeting=meeting, capacity=3),
+ RoomFactory(meeting=meeting, capacity=123),
+ ]
+ ts = TimeSlotFactory(meeting=meeting)
+ rendered = str(TimeSlotEditForm(instance=ts)['location'])
+ # noinspection PyTypeChecker
+ self.assertInHTML(
+ f'',
+ rendered,
+ )
+ for room in rooms:
+ # noinspection PyTypeChecker
+ self.assertInHTML(
+ f'',
+ rendered,
+ )
+
+
+class TimeSlotCreateFormTests(TestCase):
+ def setUp(self):
+ super().setUp()
+ self.meeting = MeetingFactory(type_id='ietf', date=date(2021, 11, 16), days=3, populate_schedule=False)
+
+ def test_other_date(self):
+ room = RoomFactory(meeting=self.meeting)
+
+ # no other_date, no day selected
+ form = TimeSlotCreateForm(
+ self.meeting,
+ data={
+ 'name': 'time slot',
+ 'type': 'regular',
+ 'time': '12:00',
+ 'duration': '01:00',
+ 'locations': [str(room.pk)],
+ })
+ self.assertFalse(form.is_valid())
+
+ # no other_date, day is selected
+ form = TimeSlotCreateForm(
+ self.meeting,
+ data={
+ 'name': 'time slot',
+ 'type': 'regular',
+ 'days': ['738111'], # date(2021,11,17).toordinal()
+ 'time': '12:00',
+ 'duration': '01:00',
+ 'locations': [str(room.pk)],
+ })
+ self.assertTrue(form.is_valid())
+ self.assertNotIn('other_date', form.cleaned_data)
+ self.assertEqual(form.cleaned_data['days'], [date(2021, 11, 17)])
+
+ # other_date given, no day is selected
+ form = TimeSlotCreateForm(
+ self.meeting,
+ data={
+ 'name': 'time slot',
+ 'type': 'regular',
+ 'time': '12:00',
+ 'duration': '01:00',
+ 'locations': [str(room.pk)],
+ 'other_date': '2021-11-15',
+ })
+ self.assertTrue(form.is_valid())
+ self.assertNotIn('other_date', form.cleaned_data)
+ self.assertEqual(form.cleaned_data['days'], [date(2021, 11, 15)])
+
+ # day is selected and other_date is given
+ form = TimeSlotCreateForm(
+ self.meeting,
+ data={
+ 'name': 'time slot',
+ 'type': 'regular',
+ 'days': ['738111'], # date(2021,11,17).toordinal()
+ 'time': '12:00',
+ 'duration': '01:00',
+ 'locations': [str(room.pk)],
+ 'other_date': '2021-11-15',
+ })
+ self.assertTrue(form.is_valid())
+ self.assertNotIn('other_date', form.cleaned_data)
+ self.assertCountEqual(form.cleaned_data['days'], [date(2021, 11, 17), date(2021, 11, 15)])
+
+ # invalid other_date, no day selected
+ form = TimeSlotCreateForm(
+ self.meeting,
+ data={
+ 'name': 'time slot',
+ 'type': 'regular',
+ 'time': '12:00',
+ 'duration': '01:00',
+ 'locations': [str(room.pk)],
+ 'other_date': 'invalid',
+ })
+ self.assertFalse(form.is_valid())
+
+ # invalid other_date, day selected
+ form = TimeSlotCreateForm(
+ self.meeting,
+ data={
+ 'name': 'time slot',
+ 'type': 'regular',
+ 'days': ['738111'], # date(2021,11,17).toordinal()
+ 'time': '12:00',
+ 'duration': '01:00',
+ 'locations': [str(room.pk)],
+ 'other_date': 'invalid',
+ })
+ self.assertFalse(form.is_valid())
+
+ def test_meeting_days(self):
+ form = TimeSlotCreateForm(self.meeting)
+ self.assertEqual(
+ form.fields['days'].choices,
+ [
+ ('738110', 'Tuesday (2021-11-16)'),
+ ('738111', 'Wednesday (2021-11-17)'),
+ ('738112', 'Thursday (2021-11-18)'),
+ ],
+ )
+
+ def test_locations(self):
+ rooms = RoomFactory.create_batch(5, meeting=self.meeting)
+ form = TimeSlotCreateForm(self.meeting)
+ self.assertCountEqual(form.fields['locations'].queryset.all(), rooms)
+
+
+class DurationChoiceFieldTests(TestCase):
+ def test_choices_default(self):
+ f = DurationChoiceField()
+ self.assertEqual(f.choices, [('', '--Please select'), ('3600', '1 hour'), ('7200', '2 hours')])
+
+ def test_choices(self):
+ f = DurationChoiceField([60, 1800, 3600, 5400, 7260, 7261])
+ self.assertEqual(
+ f.choices,
+ [
+ ('', '--Please select'),
+ ('60', '1 minute'),
+ ('1800', '30 minutes'),
+ ('3600', '1 hour'),
+ ('5400', '1 hour 30 minutes'),
+ ('7260', '2 hours 1 minute'),
+ ('7261', '2 hours 1 minute'),
+ ]
+ )
+
+ def test_bound_value(self):
+ class Form(forms.Form):
+ f = DurationChoiceField()
+ form = Form(data={'f': '3600'})
+ self.assertTrue(form.is_valid())
+ self.assertEqual(form.cleaned_data['f'], timedelta(hours=1))
+ form = Form(data={'f': '7200'})
+ self.assertTrue(form.is_valid())
+ self.assertEqual(form.cleaned_data['f'], timedelta(hours=2))
+ self.assertFalse(Form(data={'f': '3601'}).is_valid())
+ self.assertFalse(Form(data={'f': ''}).is_valid())
+ self.assertFalse(Form(data={'f': 'bob'}).is_valid())
+
+
+class SessionDetailsFormTests(TestCase):
+ def setUp(self):
+ super().setUp()
+ self.meeting = MeetingFactory(type_id='ietf', populate_schedule=False)
+ self.group = GroupFactory()
+
+ def test_initial_purpose(self):
+ """First session purpose for group should be default"""
+ # change the session_purposes GroupFeature to check that it's being used
+ self.group.features.session_purposes = ['coding', 'admin', 'closed_meeting']
+ self.group.features.save()
+ self.assertEqual(SessionDetailsForm(group=self.group).initial['purpose'], 'coding')
+ self.group.features.session_purposes = ['admin', 'coding', 'closed_meeting']
+ self.group.features.save()
+ self.assertEqual(SessionDetailsForm(group=self.group).initial['purpose'], 'admin')
+
+ def test_session_purposes(self):
+ # change the session_purposes GroupFeature to check that it's being used
+ self.group.features.session_purposes = ['coding', 'admin', 'closed_meeting']
+ self.group.features.save()
+ self.assertCountEqual(
+ SessionDetailsForm(group=self.group).fields['purpose'].queryset.values_list('slug', flat=True),
+ ['coding', 'admin', 'closed_meeting'],
+ )
+ self.group.features.session_purposes = ['admin', 'closed_meeting']
+ self.group.features.save()
+ self.assertCountEqual(
+ SessionDetailsForm(group=self.group).fields['purpose'].queryset.values_list('slug', flat=True),
+ ['admin', 'closed_meeting'],
+ )
+
+ def test_allowed_types(self):
+ """Correct map from SessionPurposeName to allowed TimeSlotTypeName should be sent to JS"""
+ # change the allowed map to a known and non-standard arrangement
+ SessionPurposeName.objects.filter(slug='regular').update(timeslot_types=['other'])
+ SessionPurposeName.objects.filter(slug='admin').update(timeslot_types=['break', 'regular'])
+ SessionPurposeName.objects.exclude(slug__in=['regular', 'admin']).update(timeslot_types=[])
+ # check that the map we just installed is actually passed along to the JS through a widget attr
+ allowed = json.loads(SessionDetailsForm(group=self.group).fields['type'].widget.attrs['data-allowed-options'])
+ self.assertEqual(allowed['regular'], ['other'])
+ self.assertEqual(allowed['admin'], ['break', 'regular'])
+ for purpose in SessionPurposeName.objects.exclude(slug__in=['regular', 'admin']):
+ self.assertEqual(allowed[purpose.slug], [])
+
+ def test_duration_options(self):
+ self.assertTrue(self.group.features.acts_like_wg)
+ self.assertEqual(
+ SessionDetailsForm(group=self.group).fields['requested_duration'].choices,
+ [('', '--Please select'), ('3600', '1 hour'), ('7200', '2 hours')],
+ )
+ self.group.features.acts_like_wg = False
+ self.group.features.save()
+ self.assertEqual(
+ SessionDetailsForm(group=self.group).fields['requested_duration'].choices,
+ [('', '--Please select'), ('1800', '30 minutes'),
+ ('3600', '1 hour'), ('5400', '1 hour 30 minutes'),
+ ('7200', '2 hours'), ('9000', '2 hours 30 minutes'),
+ ('10800', '3 hours'), ('12600', '3 hours 30 minutes'),
+ ('14400', '4 hours')],
+ )
+
+ def test_on_agenda(self):
+ # new session gets its purpose's on_agenda value when True
+ self.assertTrue(SessionPurposeName.objects.get(slug='regular').on_agenda)
+ form = SessionDetailsForm(group=self.group, data={
+ 'name': 'blah',
+ 'purpose': 'regular',
+ 'type': 'regular',
+ 'requested_duration': '3600',
+ })
+ self.assertTrue(form.is_valid())
+ self.assertTrue(form.cleaned_data['on_agenda'])
+
+ # new session gets its purpose's on_agenda value when False
+ SessionPurposeName.objects.filter(slug='regular').update(on_agenda=False)
+ form = SessionDetailsForm(group=self.group, data={
+ 'name': 'blah',
+ 'purpose': 'regular',
+ 'type': 'regular',
+ 'requested_duration': '3600',
+ })
+ self.assertTrue(form.is_valid())
+ self.assertFalse(form.cleaned_data['on_agenda'])
+
+ # updated session keeps its on_agenda value, even if it differs from its purpose
+ session = SessionFactory(meeting=self.meeting, add_to_schedule=False, on_agenda=True)
+ form = SessionDetailsForm(
+ group=self.group,
+ instance=session,
+ data={
+ 'name': 'blah',
+ 'purpose': 'regular',
+ 'type': 'regular',
+ 'requested_duration': '3600',
+ },
+ )
+ self.assertTrue(form.is_valid())
+ self.assertTrue(form.cleaned_data['on_agenda'])
+
+ # session gets purpose's on_agenda value if its purpose changes (changing the
+ # purpose away from 'regular' so we can use the 'wg' type group that only allows
+ # regular sessions)
+ session.purpose_id = 'admin'
+ session.save()
+ form = SessionDetailsForm(
+ group=self.group,
+ instance=session,
+ data={
+ 'name': 'blah',
+ 'purpose': 'regular',
+ 'type': 'regular',
+ 'requested_duration': '3600',
+ },
+ )
+ self.assertTrue(form.is_valid())
+ self.assertFalse(form.cleaned_data['on_agenda'])
+
+class SessionEditFormTests(TestCase):
+ def test_rejects_group_mismatch(self):
+ session = SessionFactory(meeting__type_id='ietf', meeting__populate_schedule=False, add_to_schedule=False)
+ other_group = GroupFactory()
+ with self.assertRaisesMessage(ValueError, 'Session group does not match group keyword'):
+ SessionEditForm(instance=session, group=other_group)
+
+
+class SessionDetailsInlineFormset(TestCase):
+ def setUp(self):
+ super().setUp()
+ self.meeting = MeetingFactory(type_id='ietf', populate_schedule=False)
+ self.group = GroupFactory()
+
+ def test_initial_sessions(self):
+ """Sessions for the correct meeting and group should be included"""
+ sessions = SessionFactory.create_batch(2, meeting=self.meeting, group=self.group, add_to_schedule=False)
+ SessionFactory(meeting=self.meeting, add_to_schedule=False) # should be ignored
+ SessionFactory(group=self.group, add_to_schedule=False) # should be ignored
+ formset_class = sessiondetailsformset_factory()
+ formset = formset_class(group=self.group, meeting=self.meeting)
+ self.assertCountEqual(formset.queryset.all(), sessions)
+
+ def test_forms_created_with_group_kwarg(self):
+ class MockFormClass(SessionDetailsForm):
+ """Mock class to track the group that was passed to the init method"""
+ def __init__(self, group, *args, **kwargs):
+ self.init_group_argument = group
+ super().__init__(group, *args, **kwargs)
+
+ with patch('ietf.meeting.forms.SessionDetailsForm', MockFormClass):
+ formset_class = sessiondetailsformset_factory()
+ formset = formset_class(meeting=self.meeting, group=self.group)
+ str(formset) # triggers instantiation of forms
+ self.assertGreaterEqual(len(formset), 1)
+ for form in formset:
+ self.assertEqual(form.init_group_argument, self.group)
+
+ def test_add_instance(self):
+ session = SessionFactory(meeting=self.meeting, group=self.group, add_to_schedule=False)
+ formset_class = sessiondetailsformset_factory()
+ formset = formset_class(group=self.group, meeting=self.meeting, data={
+ 'session_set-TOTAL_FORMS': '2',
+ 'session_set-INITIAL_FORMS': '1',
+ 'session_set-0-id': str(session.pk),
+ 'session_set-0-name': 'existing',
+ 'session_set-0-purpose': 'regular',
+ 'session_set-0-type': 'regular',
+ 'session_set-0-requested_duration': '3600',
+ 'session_set-1-name': 'new',
+ 'session_set-1-purpose': 'regular',
+ 'session_set-1-type': 'regular',
+ 'session_set-1-requested_duration': '3600',
+ })
+ formset.save()
+ # make sure session created
+ self.assertEqual(self.meeting.session_set.count(), 2)
+ self.assertIn(session, self.meeting.session_set.all())
+ self.assertEqual(len(formset.new_objects), 1)
+ self.assertEqual(formset.new_objects[0].name, 'new')
+ self.assertEqual(formset.new_objects[0].meeting, self.meeting)
+ self.assertEqual(formset.new_objects[0].group, self.group)
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
index 35a09be2d..6ef70c096 100644
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -99,7 +99,8 @@ from ietf.utils.response import permission_denied
from ietf.utils.text import xslugify
from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm,
- InterimCancelForm, InterimSessionInlineFormSet, FileUploadForm, RequestMinutesForm,)
+ InterimCancelForm, InterimSessionInlineFormSet, RequestMinutesForm,
+ UploadAgendaForm, UploadBlueSheetForm, UploadMinutesForm, UploadSlidesForm)
def get_interim_menu_entries(request):
@@ -2341,13 +2342,6 @@ def add_session_drafts(request, session_id, num):
})
-class UploadBlueSheetForm(FileUploadForm):
-
- def __init__(self, *args, **kwargs):
- kwargs['doc_type'] = 'bluesheets'
- super(UploadBlueSheetForm, self).__init__(*args, **kwargs )
-
-
def upload_session_bluesheets(request, session_id, num):
# num is redundant, but we're dragging it along an artifact of where we are in the current URL structure
session = get_object_or_404(Session,pk=session_id)
@@ -2435,15 +2429,6 @@ def save_bluesheet(request, session, file, encoding='utf-8'):
doc.save_with_history([e])
return save_error
-class UploadMinutesForm(FileUploadForm):
- apply_to_all = forms.BooleanField(label='Apply to all group sessions at this meeting',initial=True,required=False)
-
- def __init__(self, show_apply_to_all_checkbox, *args, **kwargs):
- kwargs['doc_type'] = 'minutes'
- super(UploadMinutesForm, self).__init__(*args, **kwargs )
- if not show_apply_to_all_checkbox:
- self.fields.pop('apply_to_all')
-
def upload_session_minutes(request, session_id, num):
# num is redundant, but we're dragging it along an artifact of where we are in the current URL structure
@@ -2536,15 +2521,6 @@ def upload_session_minutes(request, session_id, num):
})
-class UploadAgendaForm(FileUploadForm):
- apply_to_all = forms.BooleanField(label='Apply to all group sessions at this meeting',initial=True,required=False)
-
- def __init__(self, show_apply_to_all_checkbox, *args, **kwargs):
- kwargs['doc_type'] = 'agenda'
- super(UploadAgendaForm, self).__init__(*args, **kwargs )
- if not show_apply_to_all_checkbox:
- self.fields.pop('apply_to_all')
-
def upload_session_agenda(request, session_id, num):
# num is redundant, but we're dragging it along an artifact of where we are in the current URL structure
session = get_object_or_404(Session,pk=session_id)
@@ -2639,27 +2615,6 @@ def upload_session_agenda(request, session_id, num):
})
-class UploadSlidesForm(FileUploadForm):
- title = forms.CharField(max_length=255)
- apply_to_all = forms.BooleanField(label='Apply to all group sessions at this meeting',initial=False,required=False)
-
- def __init__(self, session, show_apply_to_all_checkbox, *args, **kwargs):
- self.session = session
- kwargs['doc_type'] = 'slides'
- super(UploadSlidesForm, self).__init__(*args, **kwargs )
- if not show_apply_to_all_checkbox:
- self.fields.pop('apply_to_all')
-
- def clean_title(self):
- title = self.cleaned_data['title']
- # The current tables only handles Unicode BMP:
- if ord(max(title)) > 0xffff:
- raise forms.ValidationError("The title contains characters outside the Unicode BMP, which is not currently supported")
- if self.session.meeting.type_id=='interim':
- if re.search(r'-\d{2}$', title):
- raise forms.ValidationError("Interim slides currently may not have a title that ends with something that looks like a revision number (-nn)")
- return title
-
def upload_session_slides(request, session_id, num, name):
# num is redundant, but we're dragging it along an artifact of where we are in the current URL structure
session = get_object_or_404(Session,pk=session_id)
diff --git a/ietf/meeting/views_proceedings.py b/ietf/meeting/views_proceedings.py
index 9b2bd276b..cfb38e5ab 100644
--- a/ietf/meeting/views_proceedings.py
+++ b/ietf/meeting/views_proceedings.py
@@ -18,6 +18,8 @@ from ietf.secr.proceedings.utils import handle_upload_file
from ietf.utils.text import xslugify
class UploadProceedingsMaterialForm(FileUploadForm):
+ doc_type = 'procmaterials'
+
use_url = forms.BooleanField(
required=False,
label='Use an external URL instead of uploading a document',
@@ -34,7 +36,7 @@ class UploadProceedingsMaterialForm(FileUploadForm):
)
def __init__(self, *args, **kwargs):
- super().__init__(doc_type='procmaterials', *args, **kwargs)
+ super().__init__(*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),
diff --git a/ietf/settings.py b/ietf/settings.py
index f836ceb59..a6c4c8188 100644
--- a/ietf/settings.py
+++ b/ietf/settings.py
@@ -949,6 +949,12 @@ MEETING_VALID_MIME_TYPE_EXTENSIONS = {
'application/pdf': ['.pdf'],
}
+# Files uploaded with Content-Type application/octet-stream and an extension in this map will
+# be treated as if they had been uploaded with the mapped Content-Type value.
+MEETING_APPLICATION_OCTET_STREAM_OVERRIDES = {
+ '.md': 'text/markdown',
+}
+
MEETING_VALID_UPLOAD_MIME_FOR_OBSERVED_MIME = {
'text/plain': ['text/plain', 'text/markdown', 'text/x-markdown', ],
'text/html': ['text/html', ],