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