Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
aa3d6e87
Commit
aa3d6e87
authored
Dec 19, 2016
by
Cliff Dyer
Committed by
GitHub
Dec 19, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #14092 from edx/cdyer/validate-capa-xml
Use new XMLString field type for CAPA data field.
parents
12d8cc50
22ce3003
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
55 additions
and
124 deletions
+55
-124
cms/envs/common.py
+3
-0
common/lib/xmodule/xmodule/capa_base.py
+39
-24
common/lib/xmodule/xmodule/tests/test_capa_module.py
+9
-18
common/test/acceptance/tests/studio/test_import_export.py
+0
-81
lms/envs/common.py
+3
-0
requirements/edx/github.txt
+1
-1
No files found.
cms/envs/common.py
View file @
aa3d6e87
...
...
@@ -215,6 +215,9 @@ FEATURES = {
# Show Language selector
'SHOW_LANGUAGE_SELECTOR'
:
False
,
# Set this to False to facilitate cleaning up invalid xml from your modulestore.
'ENABLE_XBLOCK_XML_VALIDATION'
:
True
,
}
ENABLE_JASMINE
=
False
...
...
common/lib/xmodule/xmodule/capa_base.py
View file @
aa3d6e87
...
...
@@ -6,28 +6,27 @@ import hashlib
import
json
import
logging
import
os
import
traceback
import
re
import
struct
import
sys
import
re
import
traceback
from
django.conf
import
settings
# We don't want to force a dependency on datadog, so make the import conditional
try
:
import
dogstats_wrapper
as
dog_stats_api
except
ImportError
:
dog_stats_api
=
None
from
pytz
import
utc
from
capa.capa_problem
import
LoncapaProblem
,
LoncapaSystem
from
capa.responsetypes
import
StudentInputError
,
\
ResponseError
,
LoncapaProblemError
from
capa.responsetypes
import
StudentInputError
,
ResponseError
,
LoncapaProblemError
from
capa.util
import
convert_files_to_filenames
,
get_inner_html_from_xpath
from
.progress
import
Progress
from
xmodule.exceptions
import
NotFoundError
from
xblock.fields
import
Scope
,
String
,
Boolean
,
Dict
,
Integer
,
Float
from
.fields
import
Timedelta
,
Date
from
django.utils.timezone
import
UTC
from
xblock.fields
import
Boolean
,
Dict
,
Float
,
Integer
,
Scope
,
String
,
XMLString
from
xmodule.capa_base_constants
import
RANDOMIZATION
,
SHOWANSWER
from
django.conf
import
settings
from
xmodule.exceptions
import
NotFoundError
from
.fields
import
Date
,
Timedelta
from
.progress
import
Progress
from
openedx.core.djangolib.markup
import
HTML
,
Text
...
...
@@ -42,6 +41,8 @@ NUM_RANDOMIZATION_BINS = 20
# Never produce more than this many different seeds, no matter what.
MAX_RANDOMIZATION_BINS
=
1000
FEATURES
=
getattr
(
settings
,
'FEATURES'
,
{})
def
randomization_bin
(
seed
,
problem_id
):
"""
...
...
@@ -76,7 +77,7 @@ class ComplexEncoder(json.JSONEncoder):
"""
Extend the JSON encoder to correctly handle complex numbers
"""
def
default
(
self
,
obj
):
def
default
(
self
,
obj
):
# pylint: disable=method-hidden
"""
Print a nicely formatted complex number, or default to the JSON encoder
"""
...
...
@@ -157,7 +158,12 @@ class CapaFields(object):
{
"display_name"
:
_
(
"Per Student"
),
"value"
:
RANDOMIZATION
.
PER_STUDENT
}
]
)
data
=
String
(
help
=
_
(
"XML data for the problem"
),
scope
=
Scope
.
content
,
default
=
"<problem></problem>"
)
data
=
XMLString
(
help
=
_
(
"XML data for the problem"
),
scope
=
Scope
.
content
,
enforce_type
=
FEATURES
.
get
(
'ENABLE_XBLOCK_XML_VALIDATION'
,
True
),
default
=
"<problem></problem>"
)
correct_map
=
Dict
(
help
=
_
(
"Dictionary with the correctness of current student answers"
),
scope
=
Scope
.
user_state
,
default
=
{})
input_state
=
Dict
(
help
=
_
(
"Dictionary for maintaining the state of inputtypes"
),
scope
=
Scope
.
user_state
)
...
...
@@ -257,11 +263,13 @@ class CapaMixin(CapaFields):
)
)
# create a dummy problem with error message instead of failing
problem_text
=
(
u'<problem><text><span class="inline-error">'
u'Problem {url} has an error:</span>{msg}</text></problem>'
.
format
(
url
=
self
.
location
.
to_deprecated_string
(),
msg
=
msg
)
)
problem_text
=
(
u'<problem><text><span class="inline-error">'
u'Problem {url} has an error:</span>{msg}</text></problem>'
.
format
(
url
=
self
.
location
.
to_deprecated_string
(),
msg
=
msg
,
)
)
self
.
lcp
=
self
.
new_lcp
(
self
.
get_state_for_lcp
(),
text
=
problem_text
)
else
:
# add extra info and raise
...
...
@@ -349,7 +357,7 @@ class CapaMixin(CapaFields):
"""
Set the module's last submission time (when the problem was submitted)
"""
self
.
last_submission_time
=
datetime
.
datetime
.
now
(
UTC
()
)
self
.
last_submission_time
=
datetime
.
datetime
.
now
(
utc
)
def
get_score
(
self
):
"""
...
...
@@ -803,7 +811,7 @@ class CapaMixin(CapaFields):
Is it now past this problem's due date, including grace period?
"""
return
(
self
.
close_date
is
not
None
and
datetime
.
datetime
.
now
(
UTC
()
)
>
self
.
close_date
)
datetime
.
datetime
.
now
(
utc
)
>
self
.
close_date
)
def
closed
(
self
):
"""
...
...
@@ -1093,7 +1101,7 @@ class CapaMixin(CapaFields):
metric_name
=
u'capa.check_problem.{}'
.
format
# Can override current time
current_time
=
datetime
.
datetime
.
now
(
UTC
()
)
current_time
=
datetime
.
datetime
.
now
(
utc
)
if
override_time
is
not
False
:
current_time
=
override_time
...
...
@@ -1128,8 +1136,9 @@ class CapaMixin(CapaFields):
# Wait time between resets: check if is too soon for submission.
if
self
.
last_submission_time
is
not
None
and
self
.
submission_wait_seconds
!=
0
:
if
(
current_time
-
self
.
last_submission_time
)
.
total_seconds
()
<
self
.
submission_wait_seconds
:
remaining_secs
=
int
(
self
.
submission_wait_seconds
-
(
current_time
-
self
.
last_submission_time
)
.
total_seconds
())
seconds_since_submission
=
(
current_time
-
self
.
last_submission_time
)
.
total_seconds
()
if
seconds_since_submission
<
self
.
submission_wait_seconds
:
remaining_secs
=
int
(
self
.
submission_wait_seconds
-
seconds_since_submission
)
msg
=
_
(
u'You must wait at least {wait_secs} between submissions. {remaining_secs} remaining.'
)
.
format
(
wait_secs
=
self
.
pretty_print_seconds
(
self
.
submission_wait_seconds
),
remaining_secs
=
self
.
pretty_print_seconds
(
remaining_secs
))
...
...
@@ -1343,7 +1352,7 @@ class CapaMixin(CapaFields):
log
.
warning
(
'Input id
%
s is not mapped to an input type.'
,
input_id
)
answer_response
=
None
for
respon
se
,
responder
in
self
.
lcp
.
responders
.
iteritem
s
():
for
respon
der
in
self
.
lcp
.
responders
.
itervalue
s
():
if
input_id
in
responder
.
answer_ids
:
answer_response
=
responder
...
...
@@ -1406,8 +1415,10 @@ class CapaMixin(CapaFields):
if
not
self
.
lcp
.
supports_rescoring
():
event_info
[
'failure'
]
=
'unsupported'
self
.
track_function_unmask
(
'problem_rescore_fail'
,
event_info
)
# pylint: disable=line-too-long
# Translators: 'rescoring' refers to the act of re-submitting a student's solution so it can get a new score.
raise
NotImplementedError
(
_
(
"Problem's definition does not support rescoring."
))
# pylint: enable=line-too-long
if
not
self
.
done
:
event_info
[
'failure'
]
=
'unanswered'
...
...
@@ -1485,8 +1496,10 @@ class CapaMixin(CapaFields):
self
.
track_function_unmask
(
'save_problem_fail'
,
event_info
)
return
{
'success'
:
False
,
# pylint: disable=line-too-long
# Translators: 'closed' means the problem's due date has passed. You may no longer attempt to solve the problem.
'msg'
:
_
(
"Problem is closed."
)
'msg'
:
_
(
"Problem is closed."
),
# pylint: enable=line-too-long
}
# Problem submitted. Student should reset before saving
...
...
@@ -1538,8 +1551,10 @@ class CapaMixin(CapaFields):
self
.
track_function_unmask
(
'reset_problem_fail'
,
event_info
)
return
{
'success'
:
False
,
# pylint: disable=line-too-long
# Translators: 'closed' means the problem's due date has passed. You may no longer attempt to solve the problem.
'msg'
:
_
(
"You cannot select Reset for a problem that is closed."
),
# pylint: enable=line-too-long
}
if
not
self
.
is_submitted
():
...
...
common/lib/xmodule/xmodule/tests/test_capa_module.py
View file @
aa3d6e87
...
...
@@ -81,14 +81,7 @@ class CapaFactory(object):
)
@classmethod
def
create
(
cls
,
attempts
=
None
,
problem_state
=
None
,
correct
=
False
,
xml
=
None
,
override_get_score
=
True
,
**
kwargs
):
def
create
(
cls
,
attempts
=
None
,
problem_state
=
None
,
correct
=
False
,
xml
=
None
,
override_get_score
=
True
,
**
kwargs
):
"""
All parameters are optional, and are added to the created problem if specified.
...
...
@@ -228,7 +221,6 @@ class CapaModuleTest(unittest.TestCase):
useful as unit-code coverage for this current implementation. I don't see a layer where LoncapaProblem
is tested directly
"""
from
capa.correctmap
import
CorrectMap
student_answers
=
{
'1_2_1'
:
'abcd'
}
correct_map
=
CorrectMap
(
answer_id
=
'1_2_1'
,
correctness
=
"correct"
,
npoints
=
0.9
)
module
=
CapaFactory
.
create
(
correct
=
True
,
override_get_score
=
False
)
...
...
@@ -623,6 +615,7 @@ class CapaModuleTest(unittest.TestCase):
module
.
submit_problem
(
get_request_dict
)
# pylint: disable=line-too-long
# _http_post is called like this:
# _http_post(
# 'http://example.com/xqueue/xqueue/submit/',
...
...
@@ -639,9 +632,10 @@ class CapaModuleTest(unittest.TestCase):
# <open file u'/home/ned/edx/edx-platform/common/test/data/uploads/textbook.pdf', mode 'r' at 0x49c5a50>,
# },
# )
# pylint: enable=line-too-long
self
.
assertEqual
(
xqueue_interface
.
_http_post
.
call_count
,
1
)
_
,
kwargs
=
xqueue_interface
.
_http_post
.
call_args
_
,
kwargs
=
xqueue_interface
.
_http_post
.
call_args
# pylint: disable=unpacking-non-sequence
self
.
assertItemsEqual
(
fpaths
,
kwargs
[
'files'
]
.
keys
())
for
fpath
,
fileobj
in
kwargs
[
'files'
]
.
iteritems
():
self
.
assertEqual
(
fpath
,
fileobj
.
name
)
...
...
@@ -674,7 +668,7 @@ class CapaModuleTest(unittest.TestCase):
module
.
handle
(
'xmodule_handler'
,
request
,
'problem_check'
)
self
.
assertEqual
(
xqueue_interface
.
_http_post
.
call_count
,
1
)
_
,
kwargs
=
xqueue_interface
.
_http_post
.
call_args
_
,
kwargs
=
xqueue_interface
.
_http_post
.
call_args
# pylint: disable=unpacking-non-sequence
self
.
assertItemsEqual
(
fnames
,
kwargs
[
'files'
]
.
keys
())
for
fpath
,
fileobj
in
kwargs
[
'files'
]
.
iteritems
():
self
.
assertEqual
(
fpath
,
fileobj
.
name
)
...
...
@@ -2487,18 +2481,15 @@ class CapaDescriptorTest(unittest.TestCase):
def
test_invalid_xml_handling
(
self
):
"""
Tests to confirm that invalid XML
does not throw a wake-up-ops level error.
See TNL-5057 for quick fix, TNL-5245 for full resolution
.
Tests to confirm that invalid XML
throws errors during xblock creation,
so as not to allow bad data into modulestore
.
"""
sample_invalid_xml
=
textwrap
.
dedent
(
"""
<problem>
</proble-oh no my finger broke and I can't close the problem tag properly...
"""
)
descriptor
=
self
.
_create_descriptor
(
sample_invalid_xml
,
name
=
"Invalid XML"
)
try
:
descriptor
.
has_support
(
None
,
"multi_device"
)
except
etree
.
XMLSyntaxError
:
self
.
fail
(
"Exception raised during XML parsing, this method should be resilient to such errors"
)
with
self
.
assertRaises
(
etree
.
XMLSyntaxError
):
self
.
_create_descriptor
(
sample_invalid_xml
,
name
=
"Invalid XML"
)
class
ComplexEncoderTest
(
unittest
.
TestCase
):
...
...
common/test/acceptance/tests/studio/test_import_export.py
View file @
aa3d6e87
...
...
@@ -7,17 +7,14 @@ from datetime import datetime
from
flaky
import
flaky
from
abc
import
abstractmethod
from
bok_choy.promise
import
EmptyPromise
from
common.test.acceptance.tests.studio.base_studio_test
import
StudioLibraryTest
,
StudioCourseTest
from
common.test.acceptance.fixtures.course
import
XBlockFixtureDesc
from
common.test.acceptance.pages.studio.import_export
import
(
ExportLibraryPage
,
ExportCoursePage
,
ImportLibraryPage
,
ImportCoursePage
)
from
common.test.acceptance.pages.studio.library
import
LibraryEditPage
from
common.test.acceptance.pages.studio.container
import
ContainerPage
from
common.test.acceptance.pages.studio.overview
import
CourseOutlinePage
from
common.test.acceptance.pages.lms.courseware
import
CoursewarePage
from
common.test.acceptance.pages.lms.staff_view
import
StaffPage
...
...
@@ -86,84 +83,6 @@ class TestLibraryExport(ExportTestMixin, StudioLibraryTest):
self
.
assertEqual
(
self
.
export_page
.
header_text
,
'Library Export'
)
class
BadExportMixin
(
object
):
"""
Test mixin for bad exports.
"""
def
test_bad_export
(
self
):
"""
Scenario: I should receive an error when attempting to export a broken course or library.
Given that I have a course or library
No error modal should be showing
When I click the export button
An error modal should be shown
When I click the modal's action button
I should arrive at the edit page for the broken component
"""
# No error should be there to start.
self
.
assertFalse
(
self
.
export_page
.
is_error_modal_showing
())
self
.
export_page
.
click_export
()
self
.
export_page
.
wait_for_error_modal
()
self
.
export_page
.
click_modal_button
()
self
.
edit_page
.
wait_for_page
()
@attr
(
shard
=
7
)
class
TestLibraryBadExport
(
BadExportMixin
,
StudioLibraryTest
):
"""
Verify exporting a bad library causes an error.
"""
def
setUp
(
self
):
"""
Set up the pages and start the tests.
"""
super
(
TestLibraryBadExport
,
self
)
.
setUp
()
self
.
export_page
=
ExportLibraryPage
(
self
.
browser
,
self
.
library_key
)
self
.
edit_page
=
LibraryEditPage
(
self
.
browser
,
self
.
library_key
)
self
.
export_page
.
visit
()
def
populate_library_fixture
(
self
,
library_fixture
):
"""
Create a library with a bad component.
"""
library_fixture
.
add_children
(
XBlockFixtureDesc
(
"problem"
,
"Bad Problem"
,
data
=
'<'
),
)
@attr
(
shard
=
7
)
class
TestCourseBadExport
(
BadExportMixin
,
StudioCourseTest
):
"""
Verify exporting a bad course causes an error.
"""
ready_method
=
'wait_for_component_menu'
def
setUp
(
self
):
# pylint: disable=arguments-differ
super
(
TestCourseBadExport
,
self
)
.
setUp
()
self
.
export_page
=
ExportCoursePage
(
self
.
browser
,
self
.
course_info
[
'org'
],
self
.
course_info
[
'number'
],
self
.
course_info
[
'run'
],
)
self
.
edit_page
=
ContainerPage
(
self
.
browser
,
self
.
unit
.
locator
)
self
.
export_page
.
visit
()
def
populate_course_fixture
(
self
,
course_fixture
):
"""
Populate the course with a unit that has a bad problem.
"""
self
.
unit
=
XBlockFixtureDesc
(
'vertical'
,
'Unit'
)
course_fixture
.
add_children
(
XBlockFixtureDesc
(
'chapter'
,
'Main Section'
)
.
add_children
(
XBlockFixtureDesc
(
'sequential'
,
'Subsection'
)
.
add_children
(
self
.
unit
.
add_children
(
XBlockFixtureDesc
(
"problem"
,
"Bad Problem"
,
data
=
'<'
)
)
)
)
)
@attr
(
shard
=
7
)
class
ImportTestMixin
(
object
):
"""
...
...
lms/envs/common.py
View file @
aa3d6e87
...
...
@@ -365,6 +365,9 @@ FEATURES = {
# Note: This has no effect unless ANALYTICS_DASHBOARD_URL is already set,
# because without that setting, the tab does not show up for any courses.
'ENABLE_CCX_ANALYTICS_DASHBOARD_URL'
:
False
,
# Set this to False to facilitate cleaning up invalid xml from your modulestore.
'ENABLE_XBLOCK_XML_VALIDATION'
:
True
,
}
# Ignore static asset files on import which match this pattern
...
...
requirements/edx/github.txt
View file @
aa3d6e87
...
...
@@ -71,7 +71,7 @@ git+https://github.com/edx/rfc6266.git@v0.0.5-edx#egg=rfc6266==0.0.5-edx
git+https://github.com/edx/lettuce.git@0.2.20.002#egg=lettuce==0.2.20.002
# Our libraries:
git+https://github.com/edx/XBlock.git@xblock-0.4.1
2#egg=XBlock==0.4.12
git+https://github.com/edx/XBlock.git@xblock-0.4.1
3#egg=XBlock==0.4.13
-e git+https://github.com/edx/codejail.git@6b17c33a89bef0ac510926b1d7fea2748b73aadd#egg=codejail
-e git+https://github.com/edx/event-tracking.git@0.2.1#egg=event-tracking==0.2.1
-e git+https://github.com/edx/django-splash.git@v0.2#egg=django-splash==0.2
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment