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
+
+
+
+
+
+
+