Merged in [19744] from jennifer@painless-security.com:
Treat application/octet-stream as text/markdown for '.md' materials uploads. Refactor FileUploadForm hierarchy to reduce boilerplate. Fixes #3163.
- Legacy-Id: 19746
Note: SVN reference [19744] has been migrated to Git commit b04254a293
This commit is contained in:
commit
640855f786
|
@ -6,6 +6,9 @@ import io
|
||||||
import os
|
import os
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -324,14 +327,19 @@ class InterimCancelForm(forms.Form):
|
||||||
self.fields['date'].widget.attrs['disabled'] = True
|
self.fields['date'].widget.attrs['disabled'] = True
|
||||||
|
|
||||||
class FileUploadForm(forms.Form):
|
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')
|
file = forms.FileField(label='File to upload')
|
||||||
|
|
||||||
|
doc_type = '' # subclasses must set this
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
doc_type = kwargs.pop('doc_type')
|
assert self.doc_type in settings.MEETING_VALID_UPLOAD_EXTENSIONS
|
||||||
assert doc_type in settings.MEETING_VALID_UPLOAD_EXTENSIONS
|
self.extensions = settings.MEETING_VALID_UPLOAD_EXTENSIONS[self.doc_type]
|
||||||
self.doc_type = doc_type
|
self.mime_types = settings.MEETING_VALID_UPLOAD_MIME_TYPES[self.doc_type]
|
||||||
self.extensions = settings.MEETING_VALID_UPLOAD_EXTENSIONS[doc_type]
|
|
||||||
self.mime_types = settings.MEETING_VALID_UPLOAD_MIME_TYPES[doc_type]
|
|
||||||
super(FileUploadForm, self).__init__(*args, **kwargs)
|
super(FileUploadForm, self).__init__(*args, **kwargs)
|
||||||
label = '%s file to upload. ' % (self.doc_type.capitalize(), )
|
label = '%s file to upload. ' % (self.doc_type.capitalize(), )
|
||||||
if self.doc_type == "slides":
|
if self.doc_type == "slides":
|
||||||
|
@ -344,6 +352,15 @@ class FileUploadForm(forms.Form):
|
||||||
file = self.cleaned_data['file']
|
file = self.cleaned_data['file']
|
||||||
validate_file_size(file)
|
validate_file_size(file)
|
||||||
ext = validate_file_extension(file, self.extensions)
|
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)
|
mime_type, encoding = validate_mime_type(file, self.mime_types)
|
||||||
if not hasattr(self, 'file_encoding'):
|
if not hasattr(self, 'file_encoding'):
|
||||||
self.file_encoding = {}
|
self.file_encoding = {}
|
||||||
|
@ -351,15 +368,72 @@ class FileUploadForm(forms.Form):
|
||||||
if self.mime_types:
|
if self.mime_types:
|
||||||
if not file.content_type in settings.MEETING_VALID_UPLOAD_MIME_FOR_OBSERVED_MIME[mime_type]:
|
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))
|
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:
|
# We just validated that file.content_type is safe to accept despite being identified
|
||||||
if not ext in settings.MEETING_VALID_MIME_TYPE_EXTENSIONS[mime_type]:
|
# 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))
|
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,
|
# We'll do html sanitization later, but for frames, we fail here,
|
||||||
# as the sanitized version will most likely be useless.
|
# as the sanitized version will most likely be useless.
|
||||||
validate_no_html_frame(file)
|
validate_no_html_frame(file)
|
||||||
return 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):
|
class RequestMinutesForm(forms.Form):
|
||||||
to = MultiEmailField()
|
to = MultiEmailField()
|
||||||
cc = MultiEmailField(required=False)
|
cc = MultiEmailField(required=False)
|
||||||
|
|
|
@ -1,476 +1,104 @@
|
||||||
# Copyright The IETF Trust 2021, All Rights Reserved
|
# Copyright The IETF Trust 2021, All Rights Reserved
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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 ietf.meeting.forms import FileUploadForm, ApplyToAllFileUploadForm
|
||||||
|
|
||||||
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
|
from ietf.utils.test_utils import TestCase
|
||||||
|
|
||||||
|
|
||||||
class CsvModelPkInputTests(TestCase):
|
@override_settings(
|
||||||
widget = CsvModelPkInput()
|
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):
|
def test_accepts_valid_data(self):
|
||||||
result = self.widget.render('csv_model', value=None)
|
test_file = SimpleUploadedFile(
|
||||||
self.assertHTMLEqual(result, '<input type="text" name="csv_model" value="">')
|
name='file.txt',
|
||||||
|
content=b'plain text',
|
||||||
def test_render_value(self):
|
content_type='text/plain',
|
||||||
result = self.widget.render('csv_model', value=[1, 2, 3])
|
|
||||||
self.assertHTMLEqual(result, '<input type="text" name="csv_model" value="1,2,3">')
|
|
||||||
|
|
||||||
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()),
|
|
||||||
'<input id="id_f" name="f" type="text" placeholder="HH:MM" required>'
|
|
||||||
)
|
|
||||||
self.assertHTMLEqual(
|
|
||||||
self._render_field(CustomDurationField(initial=timedelta(hours=1))),
|
|
||||||
'<input id="id_f" name="f" type="text" placeholder="HH:MM" required value="12:34">',
|
|
||||||
'Rendered value should come from duration_string when initial value is a timedelta'
|
|
||||||
)
|
|
||||||
self.assertHTMLEqual(
|
|
||||||
self._render_field(CustomDurationField(initial="01:02")),
|
|
||||||
'<input id="id_f" name="f" type="text" placeholder="HH:MM" required value="01:02">',
|
|
||||||
'Rendered value should come from initial when it is not a timedelta'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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_overrides_content_type_application_octet_stream(self):
|
||||||
def test_validation(self):
|
test_file = SimpleUploadedFile(
|
||||||
field = TimeSlotDurationField()
|
name='file.md',
|
||||||
with self.assertRaises(forms.ValidationError):
|
content=b'plain text',
|
||||||
field.clean('-01:00')
|
content_type='application/octet-stream',
|
||||||
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'<option value="{ts.location.pk}" selected>{ts.location.name} size: None</option>',
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
for room in rooms:
|
|
||||||
# noinspection PyTypeChecker
|
|
||||||
self.assertInHTML(
|
|
||||||
f'<option value="{room.pk}">{room.name} size: {room.capacity}</option>',
|
|
||||||
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):
|
form = FileUploadFormTests.TestClass(files={'file': test_file})
|
||||||
rooms = RoomFactory.create_batch(5, meeting=self.meeting)
|
self.assertTrue(form.is_valid(), 'Test data are valid input')
|
||||||
form = TimeSlotCreateForm(self.meeting)
|
cleaned_file = form.cleaned_data['file']
|
||||||
self.assertCountEqual(form.fields['locations'].queryset.all(), rooms)
|
# 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')
|
||||||
|
|
||||||
|
def test_overrides_only_application_octet_stream(self):
|
||||||
class DurationChoiceFieldTests(TestCase):
|
test_file = SimpleUploadedFile(
|
||||||
def test_choices_default(self):
|
name='file.md',
|
||||||
f = DurationChoiceField()
|
content=b'plain text',
|
||||||
self.assertEqual(f.choices, [('', '--Please select'), ('3600', '1 hour'), ('7200', '2 hours')])
|
content_type='application/json'
|
||||||
|
|
||||||
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):
|
form = FileUploadFormTests.TestClass(files={'file': test_file})
|
||||||
class Form(forms.Form):
|
self.assertFalse(form.is_valid(), 'Test data are invalid input')
|
||||||
f = DurationChoiceField()
|
self.assertEqual(test_file.name, 'file.md', 'Uploaded filename should not be changed')
|
||||||
form = Form(data={'f': '3600'})
|
self.assertEqual(test_file.content_type, 'application/json', 'Uploaded Content-Type should not be changed')
|
||||||
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())
|
|
||||||
|
|
||||||
|
def test_overrides_only_requested_extensions_when_valid_ext(self):
|
||||||
class SessionDetailsFormTests(TestCase):
|
test_file = SimpleUploadedFile(
|
||||||
def setUp(self):
|
name='file.txt',
|
||||||
super().setUp()
|
content=b'plain text',
|
||||||
self.meeting = MeetingFactory(type_id='ietf', populate_schedule=False)
|
content_type='application/octet-stream',
|
||||||
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):
|
form = FileUploadFormTests.TestClass(files={'file': test_file})
|
||||||
"""Correct map from SessionPurposeName to allowed TimeSlotTypeName should be sent to JS"""
|
self.assertFalse(form.is_valid(), 'Test data are invalid input')
|
||||||
# change the allowed map to a known and non-standard arrangement
|
self.assertEqual(test_file.name, 'file.txt', 'Uploaded filename should not be changed')
|
||||||
SessionPurposeName.objects.filter(slug='regular').update(timeslot_types=['other'])
|
self.assertEqual(test_file.content_type, 'application/octet-stream', 'Uploaded Content-Type should not be changed')
|
||||||
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):
|
def test_overrides_only_requested_extensions_when_invalid_ext(self):
|
||||||
self.assertTrue(self.group.features.acts_like_wg)
|
test_file = SimpleUploadedFile(
|
||||||
self.assertEqual(
|
name='file.exe',
|
||||||
SessionDetailsForm(group=self.group).fields['requested_duration'].choices,
|
content=b'plain text',
|
||||||
[('', '--Please select'), ('3600', '1 hour'), ('7200', '2 hours')],
|
content_type='application/octet-stream'
|
||||||
)
|
|
||||||
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):
|
form = FileUploadFormTests.TestClass(files={'file': test_file})
|
||||||
# new session gets its purpose's on_agenda value when True
|
self.assertFalse(form.is_valid(), 'Test data are invalid input')
|
||||||
self.assertTrue(SessionPurposeName.objects.get(slug='regular').on_agenda)
|
self.assertEqual(test_file.name, 'file.exe', 'Uploaded filename should not be changed')
|
||||||
form = SessionDetailsForm(group=self.group, data={
|
self.assertEqual(test_file.content_type, 'application/octet-stream', 'Uploaded Content-Type should not be changed')
|
||||||
'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):
|
class ApplyToAllFileUploadFormTests(TestCase):
|
||||||
def setUp(self):
|
class TestClass(ApplyToAllFileUploadForm):
|
||||||
super().setUp()
|
doc_type = 'minutes'
|
||||||
self.meeting = MeetingFactory(type_id='ietf', populate_schedule=False)
|
|
||||||
self.group = GroupFactory()
|
|
||||||
|
|
||||||
def test_initial_sessions(self):
|
def test_has_apply_to_all_field_by_default(self):
|
||||||
"""Sessions for the correct meeting and group should be included"""
|
form = ApplyToAllFileUploadFormTests.TestClass(show_apply_to_all_checkbox=True)
|
||||||
sessions = SessionFactory.create_batch(2, meeting=self.meeting, group=self.group, add_to_schedule=False)
|
self.assertIn('apply_to_all', form.fields)
|
||||||
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):
|
def test_no_show_apply_to_all_field(self):
|
||||||
class MockFormClass(SessionDetailsForm):
|
form = ApplyToAllFileUploadFormTests.TestClass(show_apply_to_all_checkbox=False)
|
||||||
"""Mock class to track the group that was passed to the init method"""
|
self.assertNotIn('apply_to_all', form.fields)
|
||||||
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)
|
|
||||||
|
|
476
ietf/meeting/tests_schedule_forms.py
Normal file
476
ietf/meeting/tests_schedule_forms.py
Normal file
|
@ -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, '<input type="text" name="csv_model" value="">')
|
||||||
|
|
||||||
|
def test_render_value(self):
|
||||||
|
result = self.widget.render('csv_model', value=[1, 2, 3])
|
||||||
|
self.assertHTMLEqual(result, '<input type="text" name="csv_model" value="1,2,3">')
|
||||||
|
|
||||||
|
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()),
|
||||||
|
'<input id="id_f" name="f" type="text" placeholder="HH:MM" required>'
|
||||||
|
)
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
self._render_field(CustomDurationField(initial=timedelta(hours=1))),
|
||||||
|
'<input id="id_f" name="f" type="text" placeholder="HH:MM" required value="12:34">',
|
||||||
|
'Rendered value should come from duration_string when initial value is a timedelta'
|
||||||
|
)
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
self._render_field(CustomDurationField(initial="01:02")),
|
||||||
|
'<input id="id_f" name="f" type="text" placeholder="HH:MM" required value="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'<option value="{ts.location.pk}" selected>{ts.location.name} size: None</option>',
|
||||||
|
rendered,
|
||||||
|
)
|
||||||
|
for room in rooms:
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
self.assertInHTML(
|
||||||
|
f'<option value="{room.pk}">{room.name} size: {room.capacity}</option>',
|
||||||
|
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)
|
|
@ -99,7 +99,8 @@ from ietf.utils.response import permission_denied
|
||||||
from ietf.utils.text import xslugify
|
from ietf.utils.text import xslugify
|
||||||
|
|
||||||
from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm,
|
from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm,
|
||||||
InterimCancelForm, InterimSessionInlineFormSet, FileUploadForm, RequestMinutesForm,)
|
InterimCancelForm, InterimSessionInlineFormSet, RequestMinutesForm,
|
||||||
|
UploadAgendaForm, UploadBlueSheetForm, UploadMinutesForm, UploadSlidesForm)
|
||||||
|
|
||||||
|
|
||||||
def get_interim_menu_entries(request):
|
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):
|
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
|
# 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)
|
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])
|
doc.save_with_history([e])
|
||||||
return save_error
|
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):
|
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
|
# 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):
|
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
|
# 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)
|
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):
|
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
|
# 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)
|
session = get_object_or_404(Session,pk=session_id)
|
||||||
|
|
|
@ -18,6 +18,8 @@ from ietf.secr.proceedings.utils import handle_upload_file
|
||||||
from ietf.utils.text import xslugify
|
from ietf.utils.text import xslugify
|
||||||
|
|
||||||
class UploadProceedingsMaterialForm(FileUploadForm):
|
class UploadProceedingsMaterialForm(FileUploadForm):
|
||||||
|
doc_type = 'procmaterials'
|
||||||
|
|
||||||
use_url = forms.BooleanField(
|
use_url = forms.BooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
label='Use an external URL instead of uploading a document',
|
label='Use an external URL instead of uploading a document',
|
||||||
|
@ -34,7 +36,7 @@ class UploadProceedingsMaterialForm(FileUploadForm):
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
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(
|
self.fields['file'].label = 'Select a file to upload. Allowed format{}: {}'.format(
|
||||||
'' if len(self.mime_types) == 1 else 's',
|
'' if len(self.mime_types) == 1 else 's',
|
||||||
', '.join(self.mime_types),
|
', '.join(self.mime_types),
|
||||||
|
|
|
@ -949,6 +949,12 @@ MEETING_VALID_MIME_TYPE_EXTENSIONS = {
|
||||||
'application/pdf': ['.pdf'],
|
'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 = {
|
MEETING_VALID_UPLOAD_MIME_FOR_OBSERVED_MIME = {
|
||||||
'text/plain': ['text/plain', 'text/markdown', 'text/x-markdown', ],
|
'text/plain': ['text/plain', 'text/markdown', 'text/x-markdown', ],
|
||||||
'text/html': ['text/html', ],
|
'text/html': ['text/html', ],
|
||||||
|
|
Loading…
Reference in a new issue