From 19fb2017be6eddf0887b212e81b5459eee6b8a55 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 27 Sep 2013 15:05:05 -0400 Subject: [PATCH] Streamline and test uploading ... remove requirement that uploads be images, can now upload any file. --- .../xmodule/combined_open_ended_module.py | 2 +- .../js/src/combinedopenended/display.coffee | 17 +- .../open_ended_image_submission.py | 270 ------------------ .../open_ended_module.py | 5 +- .../openendedchild.py | 228 +++++++++------ .../self_assessment_module.py | 5 +- .../xmodule/tests/test_combined_open_ended.py | 83 +++++- .../xmodule/tests/test_self_assessment.py | 2 +- .../xmodule/tests/test_util_open_ended.py | 58 +++- .../SampleQuestionImageUpload.xml | 24 ++ .../test/data/open_ended/course/2012_Fall.xml | 1 + 11 files changed, 315 insertions(+), 380 deletions(-) delete mode 100644 common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py create mode 100644 common/test/data/open_ended/combinedopenended/SampleQuestionImageUpload.xml diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 5b1a57f20b..0bc79a4a1c 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -306,7 +306,7 @@ class CombinedOpenEndedFields(object): ) peer_grade_finished_submissions_when_none_pending = Boolean( display_name='Allow "overgrading" of peer submissions', - help=("Allow students to peer grade submissions that already have the requisite number of graders, " + help=("EXPERIMENTAL FEATURE. Allow students to peer grade submissions that already have the requisite number of graders, " "but ONLY WHEN all submissions they are eligible to grade already have enough graders. " "This is intended for use when settings for `Required Peer Grading` > `Peer Graders per Response`"), default=False, diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 7103d2e841..030d93e9b5 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -372,7 +372,8 @@ class @CombinedOpenEnded answer_area_div = @$(@answer_area_div_sel) answer_area_div.html(response.student_response) else - @can_upload_files = pre_can_upload_files + @submit_button.show() + @submit_button.attr('disabled', false) @gentle_alert response.error confirm_save_answer: (event) => @@ -385,23 +386,27 @@ class @CombinedOpenEnded event.preventDefault() @answer_area.attr("disabled", true) max_filesize = 2*1000*1000 #2MB - pre_can_upload_files = @can_upload_files if @child_state == 'initial' files = "" + valid_files_attached = false if @can_upload_files == true files = @$(@file_upload_box_sel)[0].files[0] if files != undefined + valid_files_attached = true if files.size > max_filesize - @can_upload_files = false files = "" - else - @can_upload_files = false + # Don't submit the file in the case of it being too large, deal with the error locally. + @submit_button.show() + @submit_button.attr('disabled', false) + @gentle_alert "You are trying to upload a file that is too large for our system. Please choose a file under 2MB or paste a link to it into the answer box." + return fd = new FormData() fd.append('student_answer', @answer_area.val()) fd.append('student_file', files) - fd.append('can_upload_files', @can_upload_files) + fd.append('valid_files_attached', valid_files_attached) + that=this settings = type: "POST" data: fd diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py deleted file mode 100644 index ea5c3b3527..0000000000 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py +++ /dev/null @@ -1,270 +0,0 @@ -""" -This contains functions and classes used to evaluate if images are acceptable (do not show improper content, etc), and -to send them to S3. -""" - -try: - from PIL import Image - - ENABLE_PIL = True -except: - ENABLE_PIL = False - -from urlparse import urlparse -import requests -from boto.s3.connection import S3Connection -from boto.s3.key import Key -import logging - -log = logging.getLogger(__name__) - -#Domains where any image linked to can be trusted to have acceptable content. -TRUSTED_IMAGE_DOMAINS = [ - 'wikipedia', - 'edxuploads.s3.amazonaws.com', - 'wikimedia', -] - -#Suffixes that are allowed in image urls -ALLOWABLE_IMAGE_SUFFIXES = [ - 'jpg', - 'png', - 'gif', - 'jpeg' -] - -#Maximum allowed dimensions (x and y) for an uploaded image -MAX_ALLOWED_IMAGE_DIM = 2000 - -#Dimensions to which image is resized before it is evaluated for color count, etc -MAX_IMAGE_DIM = 150 - -#Maximum number of colors that should be counted in ImageProperties -MAX_COLORS_TO_COUNT = 16 - -#Maximum number of colors allowed in an uploaded image -MAX_COLORS = 400 - - -class ImageProperties(object): - """ - Class to check properties of an image and to validate if they are allowed. - """ - - def __init__(self, image_data): - """ - Initializes class variables - @param image: Image object (from PIL) - @return: None - """ - self.image = Image.open(image_data) - image_size = self.image.size - self.image_too_large = False - if image_size[0] > MAX_ALLOWED_IMAGE_DIM or image_size[1] > MAX_ALLOWED_IMAGE_DIM: - self.image_too_large = True - if image_size[0] > MAX_IMAGE_DIM or image_size[1] > MAX_IMAGE_DIM: - self.image = self.image.resize((MAX_IMAGE_DIM, MAX_IMAGE_DIM)) - self.image_size = self.image.size - - def count_colors(self): - """ - Counts the number of colors in an image, and matches them to the max allowed - @return: boolean true if color count is acceptable, false otherwise - """ - colors = self.image.getcolors(MAX_COLORS_TO_COUNT) - if colors is None: - color_count = MAX_COLORS_TO_COUNT - else: - color_count = len(colors) - - too_many_colors = (color_count <= MAX_COLORS) - return too_many_colors - - def check_if_rgb_is_skin(self, rgb): - """ - Checks if a given input rgb tuple/list is a skin tone - @param rgb: RGB tuple - @return: Boolean true false - """ - colors_okay = False - try: - r = rgb[0] - g = rgb[1] - b = rgb[2] - check_r = (r > 60) - check_g = (r * 0.4) < g < (r * 0.85) - check_b = (r * 0.2) < b < (r * 0.7) - colors_okay = check_r and check_b and check_g - except: - pass - - return colors_okay - - def get_skin_ratio(self): - """ - Gets the ratio of skin tone colors in an image - @return: True if the ratio is low enough to be acceptable, false otherwise - """ - colors = self.image.getcolors(MAX_COLORS_TO_COUNT) - is_okay = True - if colors is not None: - skin = sum([count for count, rgb in colors if self.check_if_rgb_is_skin(rgb)]) - total_colored_pixels = sum([count for count, rgb in colors]) - bad_color_val = float(skin) / total_colored_pixels - if bad_color_val > .4: - is_okay = False - - return is_okay - - def run_tests(self): - """ - Does all available checks on an image to ensure that it is okay (size, skin ratio, colors) - @return: Boolean indicating whether or not image passes all checks - """ - image_is_okay = False - try: - #image_is_okay = self.count_colors() and self.get_skin_ratio() and not self.image_too_large - image_is_okay = not self.image_too_large - except: - log.exception("Could not run image tests.") - - if not ENABLE_PIL: - image_is_okay = True - - #log.debug("Image OK: {0}".format(image_is_okay)) - - return image_is_okay - - -class URLProperties(object): - """ - Checks to see if a URL points to acceptable content. Added to check if students are submitting reasonable - links to the peer grading image functionality of the external grading service. - """ - - def __init__(self, url_string): - self.url_string = url_string - - def check_if_parses(self): - """ - Check to see if a URL parses properly - @return: success (True if parses, false if not) - """ - success = False - try: - self.parsed_url = urlparse(self.url_string) - success = True - except: - pass - - return success - - def check_suffix(self): - """ - Checks the suffix of a url to make sure that it is allowed - @return: True if suffix is okay, false if not - """ - good_suffix = False - for suffix in ALLOWABLE_IMAGE_SUFFIXES: - if self.url_string.endswith(suffix): - good_suffix = True - break - return good_suffix - - def run_tests(self): - """ - Runs all available url tests - @return: True if URL passes tests, false if not. - """ - url_is_okay = self.check_suffix() and self.check_if_parses() - return url_is_okay - - def check_domain(self): - """ - Checks to see if url is from a trusted domain - """ - success = False - for domain in TRUSTED_IMAGE_DOMAINS: - if domain in self.url_string: - success = True - return success - return success - - -def run_url_tests(url_string): - """ - Creates a URLProperties object and runs all tests - @param url_string: A URL in string format - @return: Boolean indicating whether or not URL has passed all tests - """ - url_properties = URLProperties(url_string) - return url_properties.run_tests() - - -def run_image_tests(image): - """ - Runs all available image tests - @param image: PIL Image object - @return: Boolean indicating whether or not all tests have been passed - """ - success = False - try: - image_properties = ImageProperties(image) - success = image_properties.run_tests() - except: - log.exception("Cannot run image tests in combined open ended xmodule. May be an issue with a particular image," - "or an issue with the deployment configuration of PIL/Pillow") - return success - - -def upload_to_s3(file_to_upload, keyname, s3_interface): - ''' - Upload file to S3 using provided keyname. - - Returns: - public_url: URL to access uploaded file - ''' - - #This commented out code is kept here in case we change the uploading method and require images to be - #converted before they are sent to S3. - #TODO: determine if commented code is needed and remove - #im = Image.open(file_to_upload) - #out_im = cStringIO.StringIO() - #im.save(out_im, 'PNG') - - try: - conn = S3Connection(s3_interface['access_key'], s3_interface['secret_access_key']) - bucketname = str(s3_interface['storage_bucket_name']) - bucket = conn.create_bucket(bucketname.lower()) - - k = Key(bucket) - k.key = keyname - k.set_metadata('filename', file_to_upload.name) - k.set_contents_from_file(file_to_upload) - - #This commented out code is kept here in case we change the uploading method and require images to be - #converted before they are sent to S3. - #k.set_contents_from_string(out_im.getvalue()) - #k.set_metadata("Content-Type", 'images/png') - - k.set_acl("public-read") - public_url = k.generate_url(60 * 60 * 24 * 365) # URL timeout in seconds. - - return True, public_url - except: - #This is a dev_facing_error - error_message = "Could not connect to S3 to upload peer grading image. Trying to utilize bucket: {0}".format( - bucketname.lower()) - log.error(error_message) - return False, error_message - - -def get_from_s3(s3_public_url): - """ - Gets an image from a given S3 url - @param s3_public_url: The URL where an image is located - @return: The image data - """ - r = requests.get(s3_public_url, timeout=2) - data = r.text - return data diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py index d7fe8c0d26..2d973aa9ed 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py @@ -651,15 +651,12 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return self.out_of_sync_error(data) # add new history element with answer and empty score and hint. - success, data = self.append_image_to_student_answer(data) + success, error_message, data = self.append_file_link_to_student_answer(data) if success: data['student_answer'] = OpenEndedModule.sanitize_html(data['student_answer']) self.new_history_entry(data['student_answer']) self.send_to_grader(data['student_answer'], system) self.change_state(self.ASSESSING) - else: - # This is a student_facing_error - error_message = "There was a problem saving the image in your submission. Please try a different image, or try pasting a link to an image into the answer box." return { 'success': success, diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py index d7555ce77e..67a058f478 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py @@ -2,8 +2,6 @@ import json import logging from lxml.html.clean import Cleaner, autolink_html import re - -import open_ended_image_submission from xmodule.progress import Progress import capa.xqueue_interface as xqueue_interface from capa.util import * @@ -12,6 +10,9 @@ import controller_query_service from datetime import datetime from pytz import UTC +import requests +from boto.s3.connection import S3Connection +from boto.s3.key import Key log = logging.getLogger("mitx.courseware") @@ -24,6 +25,50 @@ MAX_ATTEMPTS = 1 # Overriden by max_score specified in xml. MAX_SCORE = 1 +FILE_NOT_FOUND_IN_RESPONSE_MESSAGE = "We could not find a file in your submission. Please try choosing a file or pasting a link to your file into the answer box." +ERROR_SAVING_FILE_MESSAGE = "We are having trouble saving your file. Please try another file or paste a link to your file into the answer box." + +def upload_to_s3(file_to_upload, keyname, s3_interface): + ''' + Upload file to S3 using provided keyname. + + Returns: + public_url: URL to access uploaded file + ''' + + conn = S3Connection(s3_interface['access_key'], s3_interface['secret_access_key']) + bucketname = str(s3_interface['storage_bucket_name']) + bucket = conn.create_bucket(bucketname.lower()) + + k = Key(bucket) + k.key = keyname + k.set_metadata('filename', file_to_upload.name) + k.set_contents_from_file(file_to_upload) + + k.set_acl("public-read") + public_url = k.generate_url(60 * 60 * 24 * 365) # URL timeout in seconds. + + return public_url + +class WhiteListCleaner(Cleaner): + """ + By default, lxml cleaner strips out all links that are not in a defined whitelist. + We want to allow all links, and rely on the peer grading flagging mechanic to catch + the "bad" ones. So, don't define a whitelist at all. + """ + def allow_embedded_url(self, el, url): + """ + Override the Cleaner allow_embedded_url method to remove the whitelist url requirement. + Ensure that any tags not in the whitelist are stripped beforehand. + """ + + # Tell cleaner to strip any element with a tag that isn't whitelisted. + if self.whitelist_tags is not None and el.tag not in self.whitelist_tags: + return False + + # Tell cleaner to allow all urls. + return True + class OpenEndedChild(object): """ @@ -70,6 +115,7 @@ class OpenEndedChild(object): except: log.error( "Could not load instance state for open ended. Setting it to nothing.: {0}".format(instance_state)) + instance_state = {} else: instance_state = {} @@ -176,11 +222,22 @@ class OpenEndedChild(object): @staticmethod def sanitize_html(answer): + """ + Take a student response and sanitize the HTML to prevent malicious script injection + or other unwanted content. + answer - any string + return - a cleaned version of the string + """ try: answer = autolink_html(answer) - cleaner = Cleaner(style=True, links=True, add_nofollow=False, page_structure=True, safe_attrs_only=True, - host_whitelist=open_ended_image_submission.TRUSTED_IMAGE_DOMAINS, - whitelist_tags=set(['embed', 'iframe', 'a', 'img', 'br'])) + cleaner = WhiteListCleaner( + style=True, + links=True, + add_nofollow=False, + page_structure=True, + safe_attrs_only=True, + whitelist_tags=('embed', 'iframe', 'a', 'img', 'br',) + ) clean_html = cleaner.clean_html(answer) clean_html = re.sub(r'

$', '', re.sub(r'^

', '', clean_html)) clean_html = re.sub("\n","
", clean_html) @@ -351,119 +408,116 @@ class OpenEndedChild(object): correctness = 'correct' if self.is_submission_correct(score) else 'incorrect' return correctness - def upload_image_to_s3(self, image_data): + def upload_file_to_s3(self, file_data): """ - Uploads an image to S3 - Image_data: InMemoryUploadedFileObject that responds to read() and seek() - @return:Success and a URL corresponding to the uploaded object + Uploads a file to S3. + file_data: InMemoryUploadedFileObject that responds to read() and seek(). + @return: A URL corresponding to the uploaded object. """ - success = False - s3_public_url = "" - image_ok = False - try: - image_data.seek(0) - image_ok = open_ended_image_submission.run_image_tests(image_data) - except Exception: - log.exception("Could not create image and check it.") - if image_ok: - image_key = image_data.name + datetime.now(UTC).strftime( - xqueue_interface.dateformat - ) + file_key = file_data.name + datetime.now(UTC).strftime( + xqueue_interface.dateformat + ) - try: - image_data.seek(0) - success, s3_public_url = open_ended_image_submission.upload_to_s3( - image_data, image_key, self.s3_interface - ) - except Exception: - log.exception("Could not upload image to S3.") + file_data.seek(0) + s3_public_url = upload_to_s3( + file_data, file_key, self.s3_interface + ) - return success, image_ok, s3_public_url + return s3_public_url - def check_for_image_and_upload(self, data): + def check_for_file_and_upload(self, data): """ - Checks to see if an image was passed back in the AJAX query. If so, it will upload it to S3 - @param data: AJAX data - @return: Success, whether or not a file was in the data dictionary, - and the html corresponding to the uploaded image + Checks to see if a file was passed back by the student. If so, it will be uploaded to S3. + @param data: AJAX post dictionary containing keys student_file and valid_files_attached. + @return: has_file_to_upload, whether or not a file was in the data dictionary, + and image_tag, the html needed to create a link to the uploaded file. """ has_file_to_upload = False - uploaded_to_s3 = False image_tag = "" - image_ok = False - if 'can_upload_files' in data: - if data['can_upload_files'] in ['true', '1']: + + # Ensure that a valid file was uploaded. + if ('valid_files_attached' in data + and data['valid_files_attached'] in ['true', '1', True] + and data['student_file'] is not None + and len(data['student_file']) > 0): has_file_to_upload = True student_file = data['student_file'][0] - uploaded_to_s3, image_ok, s3_public_url = self.upload_image_to_s3(student_file) - if uploaded_to_s3: - image_tag = self.generate_image_tag_from_url(s3_public_url, student_file.name) - return has_file_to_upload, uploaded_to_s3, image_ok, image_tag + # Upload the file to S3 and generate html to embed a link. + s3_public_url = self.upload_file_to_s3(student_file) + image_tag = self.generate_file_link_html_from_url(s3_public_url, student_file.name) - def generate_image_tag_from_url(self, s3_public_url, image_name): + return has_file_to_upload, image_tag + + def generate_file_link_html_from_url(self, s3_public_url, file_name): """ - Makes an image tag from a given URL - @param s3_public_url: URL of the image - @param image_name: Name of the image - @return: Boolean success, updated AJAX data + Create an html link to a given URL. + @param s3_public_url: URL of the file. + @param file_name: Name of the file. + @return: Boolean success, updated AJAX data. """ - image_template = """ + image_link = """ {1} - """.format(s3_public_url, image_name) - return image_template + """.format(s3_public_url, file_name) + return image_link - def append_image_to_student_answer(self, data): + def append_file_link_to_student_answer(self, data): """ - Adds an image to a student answer after uploading it to S3 - @param data: AJAx data - @return: Boolean success, updated AJAX data + Adds a file to a student answer after uploading it to S3. + @param data: AJAX data containing keys student_answer, valid_files_attached, and student_file. + @return: Boolean success, and updated AJAX data dictionary. """ - overall_success = False + + error_message = "" + if not self.accept_file_upload: # If the question does not accept file uploads, do not do anything - return True, data + return True, error_message, data - has_file_to_upload, uploaded_to_s3, image_ok, image_tag = self.check_for_image_and_upload(data) - if uploaded_to_s3 and has_file_to_upload and image_ok: + try: + # Try to upload the file to S3. + has_file_to_upload, image_tag = self.check_for_file_and_upload(data) data['student_answer'] += image_tag - overall_success = True - elif has_file_to_upload and not uploaded_to_s3 and image_ok: + success = True + if not has_file_to_upload: + # If there is no file to upload, probably the student has embedded the link in the answer text + success, data['student_answer'] = self.check_for_url_in_text(data['student_answer']) + + # If success is False, we have not found a link, and no file was attached. + # Show error to student. + if success is False: + error_message = FILE_NOT_FOUND_IN_RESPONSE_MESSAGE + + except Exception: # In this case, an image was submitted by the student, but the image could not be uploaded to S3. Likely - # a config issue (development vs deployment). For now, just treat this as a "success" - log.exception("Student AJAX post to combined open ended xmodule indicated that it contained an image, " - "but the image was not able to be uploaded to S3. This could indicate a config" - "issue with this deployment, but it could also indicate a problem with S3 or with the" - "student image itself.") - overall_success = True - elif not has_file_to_upload: - # If there is no file to upload, probably the student has embedded the link in the answer text - success, data['student_answer'] = self.check_for_url_in_text(data['student_answer']) - overall_success = success + # a config issue (development vs deployment). + log.exception("Student AJAX post to combined open ended xmodule indicated that it contained a file, " + "but the image was not able to be uploaded to S3. This could indicate a configuration " + "issue with this deployment and the S3_INTERFACE setting.") + success = False + error_message = ERROR_SAVING_FILE_MESSAGE - # log.debug("Has file: {0} Uploaded: {1} Image Ok: {2}".format(has_file_to_upload, uploaded_to_s3, image_ok)) - - return overall_success, data + return success, error_message, data def check_for_url_in_text(self, string): """ - Checks for urls in a string - @param string: Arbitrary string - @return: Boolean success, the edited string + Checks for urls in a string. + @param string: Arbitrary string. + @return: Boolean success, and the edited string. """ - success = False - links = re.findall(r'(https?://\S+)', string) - if len(links) > 0: - for link in links: - success = open_ended_image_submission.run_url_tests(link) - if not success: - string = re.sub(link, '', string) - else: - string = re.sub(link, self.generate_image_tag_from_url(link, link), string) - success = True + has_link = False - return success, string + # Find all links in the string. + links = re.findall(r'(https?://\S+)', string) + if len(links)>0: + has_link = True + + # Autolink by wrapping links in anchor tags. + for link in links: + string = re.sub(link, self.generate_file_link_html_from_url(link, link), string) + + return has_link, string def get_eta(self): if self.controller_qs: diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py index cc830f88c8..6c0d1bbf08 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py @@ -179,14 +179,11 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): error_message = "" # add new history element with answer and empty score and hint. - success, data = self.append_image_to_student_answer(data) + success, error_message, data = self.append_file_link_to_student_answer(data) if success: data['student_answer'] = SelfAssessmentModule.sanitize_html(data['student_answer']) self.new_history_entry(data['student_answer']) self.change_state(self.ASSESSING) - else: - # This is a student_facing_error - error_message = "There was a problem saving the image in your submission. Please try a different image, or try pasting a link to an image into the answer box." return { 'success': success, diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py index 5d11a4924f..65fc2bb608 100644 --- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py +++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py @@ -12,7 +12,7 @@ import logging import unittest from lxml import etree -from mock import Mock, MagicMock, ANY +from mock import Mock, MagicMock, ANY, patch from pytz import UTC from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild @@ -26,7 +26,7 @@ from xmodule.progress import Progress from xmodule.tests.test_util_open_ended import ( MockQueryDict, DummyModulestore, TEST_STATE_SA_IN, MOCK_INSTANCE_STATE, TEST_STATE_SA, TEST_STATE_AI, TEST_STATE_AI2, TEST_STATE_AI2_INVALID, - TEST_STATE_SINGLE, TEST_STATE_PE_SINGLE + TEST_STATE_SINGLE, TEST_STATE_PE_SINGLE, MockUploadedFile ) from xblock.field_data import DictFieldData @@ -374,7 +374,7 @@ class OpenEndedModuleTest(unittest.TestCase): # Submit a student response to the question. test_module.handle_ajax( "save_answer", - {"student_answer": submitted_response, "can_upload_files": False, "student_file": None}, + {"student_answer": submitted_response}, get_test_system() ) # Submitting an answer should clear the stored answer. @@ -753,7 +753,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore): #Simulate a student saving an answer html = module.handle_ajax("get_html", {}) - module.handle_ajax("save_answer", {"student_answer": self.answer, "can_upload_files": False, "student_file": None}) + module.handle_ajax("save_answer", {"student_answer": self.answer}) html = module.handle_ajax("get_html", {}) #Mock a student submitting an assessment @@ -902,3 +902,78 @@ class OpenEndedModuleXmlAttemptTest(unittest.TestCase, DummyModulestore): #Try to reset, should fail because only 1 attempt is allowed reset_data = json.loads(module.handle_ajax("reset", {})) self.assertEqual(reset_data['success'], False) + +class OpenEndedModuleXmlImageUploadTest(unittest.TestCase, DummyModulestore): + """ + Test if student is able to upload images properly. + """ + problem_location = Location(["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestionImageUpload"]) + answer_text = "Hello, this is my amazing answer." + file_text = "Hello, this is my amazing file." + file_name = "Student file 1" + answer_link = "http://www.edx.org" + autolink_tag = " + + + + Writing Applications + + + + + Language Conventions + + + + + + +

Censorship in the Libraries

+

"All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us." --Katherine Paterson, Author

+

Write a persuasive essay to a newspaper reflecting your views on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading.

+ + + + + \ No newline at end of file diff --git a/common/test/data/open_ended/course/2012_Fall.xml b/common/test/data/open_ended/course/2012_Fall.xml index 232de855cc..57bcc6ddb6 100644 --- a/common/test/data/open_ended/course/2012_Fall.xml +++ b/common/test/data/open_ended/course/2012_Fall.xml @@ -2,6 +2,7 @@ +