66import os
77import datetime
88import json
9+ import re
10+
11+ from pathlib import Path
912
1013from django import forms
1114from django .conf import settings
@@ -324,14 +327,19 @@ def __init__(self, *args, **kwargs):
324327 self .fields ['date' ].widget .attrs ['disabled' ] = True
325328
326329class FileUploadForm (forms .Form ):
330+ """Base class for FileUploadForms
331+
332+ Abstract base class - subclasses must fill in the doc_type value with
333+ the type of document they handle.
334+ """
327335 file = forms .FileField (label = 'File to upload' )
328336
337+ doc_type = '' # subclasses must set this
338+
329339 def __init__ (self , * args , ** kwargs ):
330- doc_type = kwargs .pop ('doc_type' )
331- assert doc_type in settings .MEETING_VALID_UPLOAD_EXTENSIONS
332- self .doc_type = doc_type
333- self .extensions = settings .MEETING_VALID_UPLOAD_EXTENSIONS [doc_type ]
334- self .mime_types = settings .MEETING_VALID_UPLOAD_MIME_TYPES [doc_type ]
340+ assert self .doc_type in settings .MEETING_VALID_UPLOAD_EXTENSIONS
341+ self .extensions = settings .MEETING_VALID_UPLOAD_EXTENSIONS [self .doc_type ]
342+ self .mime_types = settings .MEETING_VALID_UPLOAD_MIME_TYPES [self .doc_type ]
335343 super (FileUploadForm , self ).__init__ (* args , ** kwargs )
336344 label = '%s file to upload. ' % (self .doc_type .capitalize (), )
337345 if self .doc_type == "slides" :
@@ -344,22 +352,88 @@ def clean_file(self):
344352 file = self .cleaned_data ['file' ]
345353 validate_file_size (file )
346354 ext = validate_file_extension (file , self .extensions )
355+
356+ # override the Content-Type if needed
357+ if file .content_type in 'application/octet-stream' :
358+ content_type_map = settings .MEETING_APPLICATION_OCTET_STREAM_OVERRIDES
359+ filename = Path (file .name )
360+ if filename .suffix in content_type_map :
361+ file .content_type = content_type_map [filename .suffix ]
362+ self .cleaned_data ['file' ] = file
363+
347364 mime_type , encoding = validate_mime_type (file , self .mime_types )
348365 if not hasattr (self , 'file_encoding' ):
349366 self .file_encoding = {}
350367 self .file_encoding [file .name ] = encoding or None
351368 if self .mime_types :
352369 if not file .content_type in settings .MEETING_VALID_UPLOAD_MIME_FOR_OBSERVED_MIME [mime_type ]:
353370 raise ValidationError ('Upload Content-Type (%s) is different from the observed mime-type (%s)' % (file .content_type , mime_type ))
354- if mime_type in settings .MEETING_VALID_MIME_TYPE_EXTENSIONS :
355- if not ext in settings .MEETING_VALID_MIME_TYPE_EXTENSIONS [mime_type ]:
371+ # We just validated that file.content_type is safe to accept despite being identified
372+ # as a different MIME type by the validator. Check extension based on file.content_type
373+ # because that better reflects the intention of the upload client.
374+ if file .content_type in settings .MEETING_VALID_MIME_TYPE_EXTENSIONS :
375+ if not ext in settings .MEETING_VALID_MIME_TYPE_EXTENSIONS [file .content_type ]:
356376 raise ValidationError ('Upload Content-Type (%s) does not match the extension (%s)' % (file .content_type , ext ))
357- if mime_type in ['text/html' , ] or ext in settings .MEETING_VALID_MIME_TYPE_EXTENSIONS ['text/html' ]:
377+ if (file .content_type in ['text/html' , ]
378+ or ext in settings .MEETING_VALID_MIME_TYPE_EXTENSIONS .get ('text/html' , [])):
358379 # We'll do html sanitization later, but for frames, we fail here,
359380 # as the sanitized version will most likely be useless.
360381 validate_no_html_frame (file )
361382 return file
362383
384+
385+ class UploadBlueSheetForm (FileUploadForm ):
386+ doc_type = 'bluesheets'
387+
388+
389+ class ApplyToAllFileUploadForm (FileUploadForm ):
390+ """FileUploadField that adds an apply_to_all checkbox
391+
392+ Checkbox can be disabled by passing show_apply_to_all_checkbox=False to the constructor.
393+ This entirely removes the field from the form.
394+ """
395+ # Note: subclasses must set doc_type for FileUploadForm
396+ apply_to_all = forms .BooleanField (label = 'Apply to all group sessions at this meeting' ,initial = True ,required = False )
397+
398+ def __init__ (self , show_apply_to_all_checkbox , * args , ** kwargs ):
399+ super ().__init__ (* args , ** kwargs )
400+ if not show_apply_to_all_checkbox :
401+ self .fields .pop ('apply_to_all' )
402+ else :
403+ self .order_fields (
404+ sorted (
405+ self .fields .keys (),
406+ key = lambda f : 'zzzzzz' if f == 'apply_to_all' else f
407+ )
408+ )
409+
410+ class UploadMinutesForm (ApplyToAllFileUploadForm ):
411+ doc_type = 'minutes'
412+
413+
414+ class UploadAgendaForm (ApplyToAllFileUploadForm ):
415+ doc_type = 'agenda'
416+
417+
418+ class UploadSlidesForm (ApplyToAllFileUploadForm ):
419+ doc_type = 'slides'
420+ title = forms .CharField (max_length = 255 )
421+
422+ def __init__ (self , session , * args , ** kwargs ):
423+ super ().__init__ (* args , ** kwargs )
424+ self .session = session
425+
426+ def clean_title (self ):
427+ title = self .cleaned_data ['title' ]
428+ # The current tables only handles Unicode BMP:
429+ if ord (max (title )) > 0xffff :
430+ raise forms .ValidationError ("The title contains characters outside the Unicode BMP, which is not currently supported" )
431+ if self .session .meeting .type_id == 'interim' :
432+ if re .search (r'-\d{2}$' , title ):
433+ raise forms .ValidationError ("Interim slides currently may not have a title that ends with something that looks like a revision number (-nn)" )
434+ return title
435+
436+
363437class RequestMinutesForm (forms .Form ):
364438 to = MultiEmailField ()
365439 cc = MultiEmailField (required = False )
0 commit comments