diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 3a2e40f896..cc67389da9 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1119,11 +1119,6 @@ class CodeResponse(LoncapaResponse): (err, self.answer_id, convert_files_to_filenames(student_answers))) raise Exception(err) - if is_file(submission): - self.context.update({'submission': submission.name}) - else: - self.context.update({'submission': submission}) - # Prepare xqueue request #------------------------------------------------------------ qinterface = self.system.xqueue['interface'] @@ -1135,14 +1130,19 @@ class CodeResponse(LoncapaResponse): queue_name=self.queue_name) # Generate body + if is_list_of_files(submission): + self.context.update({'submission': queuekey}) # For tracking. TODO: May want to record something else here + else: + self.context.update({'submission': submission}) + contents = self.payload.copy() # Submit request. When successful, 'msg' is the prior length of the queue - if is_file(submission): - contents.update({'student_response': submission.name}) + if is_list_of_files(submission): + contents.update({'student_response': ''}) # TODO: Is there any information we want to send here? (error, msg) = qinterface.send_to_queue(header=xheader, body=json.dumps(contents), - file_to_upload=submission) + files_to_upload=submission) else: contents.update({'student_response': submission}) (error, msg) = qinterface.send_to_queue(header=xheader, diff --git a/common/lib/capa/capa/templates/filesubmission.html b/common/lib/capa/capa/templates/filesubmission.html index d3d57ee318..fccf469015 100644 --- a/common/lib/capa/capa/templates/filesubmission.html +++ b/common/lib/capa/capa/templates/filesubmission.html @@ -1,5 +1,5 @@
-
+
% if state == 'unsubmitted': % elif state == 'correct': diff --git a/common/lib/capa/capa/util.py b/common/lib/capa/capa/util.py index 005494e8c0..d12499ee40 100644 --- a/common/lib/capa/capa/util.py +++ b/common/lib/capa/capa/util.py @@ -39,12 +39,16 @@ def convert_files_to_filenames(answers): ''' new_answers = dict() for answer_id in answers.keys(): - if is_file(answers[answer_id]): - new_answers[answer_id] = answers[answer_id].name + answer = answers[answer_id] + if is_list_of_files(answer): # Files are stored as a list, even if one file + new_answers[answer_id] = [f.name for f in answer] else: new_answers[answer_id] = answers[answer_id] return new_answers +def is_list_of_files(files): + return isinstance(files, list) and all(is_file(f) for f in files) + def is_file(file_to_test): ''' Duck typing to check if 'file_to_test' is a File object diff --git a/common/lib/capa/capa/xqueue_interface.py b/common/lib/capa/capa/xqueue_interface.py index 2847968a89..2930eb682d 100644 --- a/common/lib/capa/capa/xqueue_interface.py +++ b/common/lib/capa/capa/xqueue_interface.py @@ -65,7 +65,7 @@ class XQueueInterface(object): self.auth = django_auth self.session = requests.session(auth=requests_auth) - def send_to_queue(self, header, body, file_to_upload=None): + def send_to_queue(self, header, body, files_to_upload=None): ''' Submit a request to xqueue. @@ -74,16 +74,19 @@ class XQueueInterface(object): body: Serialized data for the receipient behind the queueing service. The operation of xqueue is agnostic to the contents of 'body' - file_to_upload: File object to be uploaded to xqueue along with queue request + files_to_upload: List of file objects to be uploaded to xqueue along with queue request Returns (error_code, msg) where error_code != 0 indicates an error ''' # Attempt to send to queue - (error, msg) = self._send_to_queue(header, body, file_to_upload) + (error, msg) = self._send_to_queue(header, body, files_to_upload) if error and (msg == 'login_required'): # Log in, then try again self._login() - (error, msg) = self._send_to_queue(header, body, file_to_upload) + if files_to_upload is not None: + for f in files_to_upload: # Need to rewind file pointers + f.seek(0) + (error, msg) = self._send_to_queue(header, body, files_to_upload) return (error, msg) @@ -94,13 +97,15 @@ class XQueueInterface(object): return self._http_post(self.url+'/xqueue/login/', payload) - def _send_to_queue(self, header, body, file_to_upload=None): + def _send_to_queue(self, header, body, files_to_upload): payload = {'xqueue_header': header, 'xqueue_body' : body} - files = None - if file_to_upload is not None: - files = { file_to_upload.name: file_to_upload } - return self._http_post(self.url+'/xqueue/submit/', payload, files) + files = {} + if files_to_upload is not None: + for f in files_to_upload: + files.update({ f.name: f }) + + return self._http_post(self.url+'/xqueue/submit/', payload, files=files) def _http_post(self, url, data, files=None): diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index c00b680eba..a242757357 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -151,26 +151,33 @@ class @Problem return if not window.FormData - alert "Sorry, your browser does not support file uploads. Your submit request could not be fulfilled. If you can, please use Chrome or Safari which have been verified to support file uploads." + alert "Submission aborted! Sorry, your browser does not support file uploads. If you can, please use Chrome or Safari which have been verified to support file uploads." return fd = new FormData() - # Sanity check of file size - file_too_large = false + # Sanity checks on submission max_filesize = 4*1000*1000 # 4 MB + file_too_large = false + file_not_selected = false @inputs.each (index, element) -> if element.type is 'file' - if element.files[0] instanceof File - if element.files[0].size > max_filesize + for file in element.files + if file.size > max_filesize file_too_large = true - alert 'Submission aborted! Your file "' + element.files[0].name + '" is too large (max size: ' + max_filesize/(1000*1000) + ' MB)' - fd.append(element.id, element.files[0]) - else - fd.append(element.id, '') + alert 'Submission aborted! Your file "' + file.name '" is too large (max size: ' + max_filesize/(1000*1000) + ' MB)' + fd.append(element.id, file) + if element.files.length == 0 + file_not_selected = true + fd.append(element.id, '') # In case we want to allow submissions with no file else fd.append(element.id, element.value) + + if file_not_selected + alert 'Submission aborted! You did not select any files to submit' + + abort_submission = file_too_large or file_not_selected settings = type: "POST" @@ -184,8 +191,8 @@ class @Problem @updateProgress response else alert(response.success) - - if not file_too_large + + if not abort_submission $.ajaxWithPrefix("#{@url}/problem_check", settings) check: => diff --git a/common/lib/xmodule/xmodule/js/src/capa/schematic.js b/common/lib/xmodule/xmodule/js/src/capa/schematic.js index 56c4bc8195..92bc32441b 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/schematic.js +++ b/common/lib/xmodule/xmodule/js/src/capa/schematic.js @@ -172,11 +172,13 @@ schematic = (function() { this.tools = new Array(); this.toolbar = []; + /* DISABLE HELP BUTTON (target URL not consistent with multicourse hierarchy) -- SJSU if (!this.diagram_only) { this.tools['help'] = this.add_tool(help_icon,'Help: display help page',this.help); this.enable_tool('help',true); this.toolbar.push(null); // spacer } + END DISABLE HELP BUTTON -- SJSU */ if (this.edits_allowed) { this.tools['grid'] = this.add_tool(grid_icon,'Grid: toggle grid display',this.toggle_grid); diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index f08b0f8d6e..a3eac0258e 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -341,11 +341,11 @@ class CodeResponseTest(unittest.TestCase): fp = open(problem_file) answers_with_file = {'1_2_1': 'String-based answer', '1_3_1': ['answer1', 'answer2', 'answer3'], - '1_4_1': fp} + '1_4_1': [fp, fp]} answers_converted = convert_files_to_filenames(answers_with_file) self.assertEquals(answers_converted['1_2_1'], 'String-based answer') self.assertEquals(answers_converted['1_3_1'], ['answer1', 'answer2', 'answer3']) - self.assertEquals(answers_converted['1_4_1'], fp.name) + self.assertEquals(answers_converted['1_4_1'], [fp.name, fp.name]) class ChoiceResponseTest(unittest.TestCase): diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 558f6deeb2..7a23927504 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -375,15 +375,22 @@ def modx_dispatch(request, dispatch=None, id=None, course_id=None): # ''' (fix emacs broken parsing) # Check for submitted files and basic file size checks - p = request.POST.copy() + p = request.POST.dict() if request.FILES: - for inputfile_id in request.FILES.keys(): - inputfile = request.FILES[inputfile_id] - if inputfile.size > settings.STUDENT_FILEUPLOAD_MAX_SIZE: # Bytes - file_too_big_msg = 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %\ - (inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE/(1000**2)) - return HttpResponse(json.dumps({'success': file_too_big_msg})) - p[inputfile_id] = inputfile + for fileinput_id in request.FILES.keys(): + inputfiles = request.FILES.getlist(fileinput_id) + + if len(inputfiles) > settings.MAX_FILEUPLOADS_PER_INPUT: + too_many_files_msg = 'Submission aborted! Maximum %d files may be submitted at once' %\ + settings.MAX_FILEUPLOADS_PER_INPUT + return HttpResponse(json.dumps({'success': too_many_files_msg})) + + for inputfile in inputfiles: + if inputfile.size > settings.STUDENT_FILEUPLOAD_MAX_SIZE: # Bytes + file_too_big_msg = 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %\ + (inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE/(1000**2)) + return HttpResponse(json.dumps({'success': file_too_big_msg})) + p[fileinput_id] = inputfiles student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, modulestore().get_item(id)) instance = get_module(request.user, request, id, student_module_cache, course_id=course_id) diff --git a/lms/envs/common.py b/lms/envs/common.py index cda3a5a8b1..31067333c0 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -144,6 +144,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( ) STUDENT_FILEUPLOAD_MAX_SIZE = 4*1000*1000 # 4 MB +MAX_FILEUPLOADS_PER_INPUT = 10 # FIXME: # We should have separate S3 staged URLs in case we need to make changes to