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
22ce3003
Commit
22ce3003
authored
Dec 02, 2016
by
J. Cliff Dyer
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Use new XMLString type for CAPA data field.
TNL-5245
parent
c1cf4e78
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 @
22ce3003
...
...
@@ -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 @
22ce3003
...
...
@@ -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 @
22ce3003
...
...
@@ -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 @
22ce3003
...
...
@@ -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 @
22ce3003
...
...
@@ -361,6 +361,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 @
22ce3003
...
...
@@ -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