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
2cadbbad
Commit
2cadbbad
authored
Oct 27, 2017
by
Cliff Dyer
Committed by
GitHub
Oct 27, 2017
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #16186 from open-craft/cliff/complete-scored-blocks
Complete scored blocks
parents
a9448876
69271d04
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
188 additions
and
57 deletions
+188
-57
lms/djangoapps/completion/apps.py
+3
-0
lms/djangoapps/completion/handlers.py
+36
-0
lms/djangoapps/completion/tests/test_handlers.py
+120
-0
lms/djangoapps/completion/tests/test_models.py
+3
-3
lms/djangoapps/courseware/tests/test_module_render.py
+22
-54
lms/djangoapps/grades/signals/signals.py
+4
-0
No files found.
lms/djangoapps/completion/apps.py
View file @
2cadbbad
...
@@ -12,3 +12,6 @@ class CompletionAppConfig(AppConfig):
...
@@ -12,3 +12,6 @@ class CompletionAppConfig(AppConfig):
"""
"""
name
=
'lms.djangoapps.completion'
name
=
'lms.djangoapps.completion'
verbose_name
=
'Completion'
verbose_name
=
'Completion'
def
ready
(
self
):
from
.
import
handlers
# pylint: disable=unused-variable
lms/djangoapps/completion/handlers.py
0 → 100644
View file @
2cadbbad
"""
Signal handlers to trigger completion updates.
"""
from
__future__
import
absolute_import
,
division
,
print_function
,
unicode_literals
from
django.contrib.auth.models
import
User
from
django.dispatch
import
receiver
from
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
from
lms.djangoapps.grades.signals.signals
import
PROBLEM_WEIGHTED_SCORE_CHANGED
from
.models
import
BlockCompletion
from
.
import
waffle
@receiver
(
PROBLEM_WEIGHTED_SCORE_CHANGED
)
def
scorable_block_completion
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
"""
When a problem is scored, submit a new BlockCompletion for that block.
"""
if
not
waffle
.
waffle
()
.
is_enabled
(
waffle
.
ENABLE_COMPLETION_TRACKING
):
return
user
=
User
.
objects
.
get
(
id
=
kwargs
[
'user_id'
])
course_key
=
CourseKey
.
from_string
(
kwargs
[
'course_id'
])
block_key
=
UsageKey
.
from_string
(
kwargs
[
'usage_id'
])
if
kwargs
.
get
(
'score_deleted'
):
completion
=
0.0
else
:
completion
=
1.0
BlockCompletion
.
objects
.
submit_completion
(
user
=
user
,
course_key
=
course_key
,
block_key
=
block_key
,
completion
=
completion
,
)
lms/djangoapps/completion/tests/test_handlers.py
0 → 100644
View file @
2cadbbad
"""
Test signal handlers.
"""
from
datetime
import
datetime
import
ddt
from
django.test
import
TestCase
from
mock
import
patch
from
opaque_keys.edx.keys
import
CourseKey
from
pytz
import
utc
import
six
from
lms.djangoapps.grades.signals.signals
import
PROBLEM_WEIGHTED_SCORE_CHANGED
from
student.tests.factories
import
UserFactory
from
..
import
handlers
from
..models
import
BlockCompletion
from
..
import
waffle
class
CompletionHandlerMixin
(
object
):
"""
Common functionality for completion handler tests.
"""
def
override_waffle_switch
(
self
,
override
):
"""
Override the setting of the ENABLE_COMPLETION_TRACKING waffle switch
for the course of the test.
Parameters:
override (bool): True if tracking should be enabled.
"""
_waffle_overrider
=
waffle
.
waffle
()
.
override
(
waffle
.
ENABLE_COMPLETION_TRACKING
,
override
)
_waffle_overrider
.
__enter__
()
self
.
addCleanup
(
_waffle_overrider
.
__exit__
,
None
,
None
,
None
)
@ddt.ddt
class
ScorableCompletionHandlerTestCase
(
CompletionHandlerMixin
,
TestCase
):
"""
Test the signal handler
"""
def
setUp
(
self
):
super
(
ScorableCompletionHandlerTestCase
,
self
)
.
setUp
()
self
.
user
=
UserFactory
.
create
()
self
.
course_key
=
CourseKey
.
from_string
(
"course-v1:a+valid+course"
)
self
.
block_key
=
self
.
course_key
.
make_usage_key
(
block_type
=
"video"
,
block_id
=
"mah-video"
)
self
.
override_waffle_switch
(
True
)
@ddt.data
(
({
'score_deleted'
:
True
},
0.0
),
({
'score_deleted'
:
False
},
1.0
),
({},
1.0
),
)
@ddt.unpack
def
test_handler_submits_completion
(
self
,
params
,
expected_completion
):
handlers
.
scorable_block_completion
(
sender
=
self
,
user_id
=
self
.
user
.
id
,
course_id
=
six
.
text_type
(
self
.
course_key
),
usage_id
=
six
.
text_type
(
self
.
block_key
),
weighted_earned
=
0.0
,
weighted_possible
=
3.0
,
modified
=
datetime
.
utcnow
()
.
replace
(
tzinfo
=
utc
),
score_db_table
=
'submissions'
,
**
params
)
completion
=
BlockCompletion
.
objects
.
get
(
user
=
self
.
user
,
course_key
=
self
.
course_key
,
block_key
=
self
.
block_key
)
self
.
assertEqual
(
completion
.
completion
,
expected_completion
)
def
test_signal_calls_handler
(
self
):
user
=
UserFactory
.
create
()
course_key
=
CourseKey
.
from_string
(
"course-v1:a+valid+course"
)
block_key
=
course_key
.
make_usage_key
(
block_type
=
"video"
,
block_id
=
"mah-video"
)
with
patch
(
'lms.djangoapps.completion.handlers.scorable_block_completion'
)
as
mock_handler
:
PROBLEM_WEIGHTED_SCORE_CHANGED
.
send_robust
(
sender
=
self
,
user_id
=
user
.
id
,
course_id
=
six
.
text_type
(
course_key
),
usage_id
=
six
.
text_type
(
block_key
),
weighted_earned
=
0.0
,
weighted_possible
=
3.0
,
modified
=
datetime
.
utcnow
()
.
replace
(
tzinfo
=
utc
),
score_db_table
=
'submissions'
,
)
mock_handler
.
assert_called
()
class
DisabledCompletionHandlerTestCase
(
CompletionHandlerMixin
,
TestCase
):
"""
Test that disabling the ENABLE_COMPLETION_TRACKING waffle switch prevents
the signal handler from submitting a completion.
"""
def
setUp
(
self
):
super
(
DisabledCompletionHandlerTestCase
,
self
)
.
setUp
()
self
.
user
=
UserFactory
.
create
()
self
.
course_key
=
CourseKey
.
from_string
(
"course-v1:a+valid+course"
)
self
.
block_key
=
self
.
course_key
.
make_usage_key
(
block_type
=
"video"
,
block_id
=
"mah-video"
)
self
.
override_waffle_switch
(
False
)
def
test_disabled_handler_does_not_submit_completion
(
self
):
handlers
.
scorable_block_completion
(
sender
=
self
,
user_id
=
self
.
user
.
id
,
course_id
=
six
.
text_type
(
self
.
course_key
),
usage_id
=
six
.
text_type
(
self
.
block_key
),
weighted_earned
=
0.0
,
weighted_possible
=
3.0
,
modified
=
datetime
.
utcnow
()
.
replace
(
tzinfo
=
utc
),
score_db_table
=
'submissions'
,
)
with
self
.
assertRaises
(
BlockCompletion
.
DoesNotExist
):
BlockCompletion
.
objects
.
get
(
user
=
self
.
user
,
course_key
=
self
.
course_key
,
block_key
=
self
.
block_key
)
lms/djangoapps/completion/tests/test_models.py
View file @
2cadbbad
...
@@ -47,9 +47,9 @@ class SubmitCompletionTestCase(CompletionSetUpMixin, TestCase):
...
@@ -47,9 +47,9 @@ class SubmitCompletionTestCase(CompletionSetUpMixin, TestCase):
"""
"""
def
setUp
(
self
):
def
setUp
(
self
):
super
(
SubmitCompletionTestCase
,
self
)
.
setUp
()
super
(
SubmitCompletionTestCase
,
self
)
.
setUp
()
self
.
_overrider
=
waffle
.
waffle
()
.
override
(
waffle
.
ENABLE_COMPLETION_TRACKING
,
True
)
_overrider
=
waffle
.
waffle
()
.
override
(
waffle
.
ENABLE_COMPLETION_TRACKING
,
True
)
self
.
_overrider
.
__enter__
()
_overrider
.
__enter__
()
self
.
addCleanup
(
self
.
_overrider
.
__exit__
,
None
,
None
,
None
)
self
.
addCleanup
(
_overrider
.
__exit__
,
None
,
None
,
None
)
self
.
set_up_completion
()
self
.
set_up_completion
()
def
test_changed_value
(
self
):
def
test_changed_value
(
self
):
...
...
lms/djangoapps/courseware/tests/test_module_render.py
View file @
2cadbbad
...
@@ -136,6 +136,7 @@ class StubCompletableXBlock(XBlock):
...
@@ -136,6 +136,7 @@ class StubCompletableXBlock(XBlock):
def
progress
(
self
,
json_data
,
suffix
):
# pylint: disable=unused-argument
def
progress
(
self
,
json_data
,
suffix
):
# pylint: disable=unused-argument
"""
"""
Mark the block as complete using the deprecated progress interface.
Mark the block as complete using the deprecated progress interface.
New code should use the completion event instead.
New code should use the completion event instead.
"""
"""
return
self
.
runtime
.
publish
(
self
,
'progress'
,
{})
return
self
.
runtime
.
publish
(
self
,
'progress'
,
{})
...
@@ -425,6 +426,7 @@ class ModuleRenderTestCase(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
...
@@ -425,6 +426,7 @@ class ModuleRenderTestCase(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
@attr
(
shard
=
1
)
@attr
(
shard
=
1
)
@ddt.ddt
class
TestHandleXBlockCallback
(
SharedModuleStoreTestCase
,
LoginEnrollmentTestCase
):
class
TestHandleXBlockCallback
(
SharedModuleStoreTestCase
,
LoginEnrollmentTestCase
):
"""
"""
Test the handle_xblock_callback function
Test the handle_xblock_callback function
...
@@ -606,78 +608,44 @@ class TestHandleXBlockCallback(SharedModuleStoreTestCase, LoginEnrollmentTestCas
...
@@ -606,78 +608,44 @@ class TestHandleXBlockCallback(SharedModuleStoreTestCase, LoginEnrollmentTestCas
self
.
assertEquals
(
student_module
.
grade
,
0.75
)
self
.
assertEquals
(
student_module
.
grade
,
0.75
)
self
.
assertEquals
(
student_module
.
max_grade
,
1
)
self
.
assertEquals
(
student_module
.
max_grade
,
1
)
@ddt.data
(
(
'complete'
,
{
'completion'
:
0.625
}),
(
'progress'
,
{}),
)
@ddt.unpack
@XBlock.register_temp_plugin
(
StubCompletableXBlock
,
identifier
=
'comp'
)
@XBlock.register_temp_plugin
(
StubCompletableXBlock
,
identifier
=
'comp'
)
def
test_completion_event_with_completion_disabled
(
self
):
def
test_completion_events_with_completion_disabled
(
self
,
signal
,
data
):
with
completion_waffle
.
waffle
()
.
override
(
completion_waffle
.
ENABLE_COMPLETION_TRACKING
,
False
):
course
=
CourseFactory
.
create
()
block
=
ItemFactory
.
create
(
category
=
'comp'
,
parent
=
course
)
request
=
self
.
request_factory
.
post
(
'/'
,
data
=
json
.
dumps
({
'completion'
:
0.625
}),
content_type
=
'application/json'
,
)
request
.
user
=
self
.
mock_user
with
self
.
assertRaises
(
Http404
):
result
=
render
.
handle_xblock_callback
(
request
,
unicode
(
course
.
id
),
quote_slashes
(
unicode
(
block
.
scope_ids
.
usage_id
)),
'complete'
,
''
,
)
@XBlock.register_temp_plugin
(
StubCompletableXBlock
,
identifier
=
'comp'
)
def
test_completion_event
(
self
):
with
completion_waffle
.
waffle
()
.
override
(
completion_waffle
.
ENABLE_COMPLETION_TRACKING
,
True
):
course
=
CourseFactory
.
create
()
block
=
ItemFactory
.
create
(
category
=
'comp'
,
parent
=
course
)
request
=
self
.
request_factory
.
post
(
'/'
,
data
=
json
.
dumps
({
'completion'
:
0.625
}),
content_type
=
'application/json'
,
)
request
.
user
=
self
.
mock_user
response
=
render
.
handle_xblock_callback
(
request
,
unicode
(
course
.
id
),
quote_slashes
(
unicode
(
block
.
scope_ids
.
usage_id
)),
'complete'
,
''
,
)
self
.
assertEqual
(
response
.
status_code
,
200
)
completion
=
BlockCompletion
.
objects
.
get
(
block_key
=
block
.
scope_ids
.
usage_id
)
self
.
assertEqual
(
completion
.
completion
,
0.625
)
@XBlock.register_temp_plugin
(
StubCompletableXBlock
,
identifier
=
'comp'
)
def
test_progress_event_with_completion_disabled
(
self
):
with
completion_waffle
.
waffle
()
.
override
(
completion_waffle
.
ENABLE_COMPLETION_TRACKING
,
False
):
with
completion_waffle
.
waffle
()
.
override
(
completion_waffle
.
ENABLE_COMPLETION_TRACKING
,
False
):
course
=
CourseFactory
.
create
()
course
=
CourseFactory
.
create
()
block
=
ItemFactory
.
create
(
category
=
'comp'
,
parent
=
course
)
block
=
ItemFactory
.
create
(
category
=
'comp'
,
parent
=
course
)
request
=
self
.
request_factory
.
post
(
request
=
self
.
request_factory
.
post
(
'/'
,
'/'
,
data
=
json
.
dumps
(
{}
),
data
=
json
.
dumps
(
data
),
content_type
=
'application/json'
,
content_type
=
'application/json'
,
)
)
request
.
user
=
self
.
mock_user
request
.
user
=
self
.
mock_user
with
self
.
assertRaises
(
Http404
):
with
self
.
assertRaises
(
Http404
):
re
sponse
=
re
nder
.
handle_xblock_callback
(
render
.
handle_xblock_callback
(
request
,
request
,
unicode
(
course
.
id
),
unicode
(
course
.
id
),
quote_slashes
(
unicode
(
block
.
scope_ids
.
usage_id
)),
quote_slashes
(
unicode
(
block
.
scope_ids
.
usage_id
)),
'progress'
,
signal
,
''
,
''
,
)
)
self
.
assertEqual
(
response
.
status_code
,
404
)
raise
Http404
@ddt.data
(
(
'complete'
,
{
'completion'
:
0.625
},
0.625
),
(
'progress'
,
{},
1.0
),
)
@ddt.unpack
@XBlock.register_temp_plugin
(
StubCompletableXBlock
,
identifier
=
'comp'
)
@XBlock.register_temp_plugin
(
StubCompletableXBlock
,
identifier
=
'comp'
)
def
test_
progress_event
(
self
):
def
test_
completion_events
(
self
,
signal
,
data
,
expected_completion
):
with
completion_waffle
.
waffle
()
.
override
(
completion_waffle
.
ENABLE_COMPLETION_TRACKING
,
True
):
with
completion_waffle
.
waffle
()
.
override
(
completion_waffle
.
ENABLE_COMPLETION_TRACKING
,
True
):
course
=
CourseFactory
.
create
()
course
=
CourseFactory
.
create
()
block
=
ItemFactory
.
create
(
category
=
'comp'
,
parent
=
course
)
block
=
ItemFactory
.
create
(
category
=
'comp'
,
parent
=
course
)
request
=
self
.
request_factory
.
post
(
request
=
self
.
request_factory
.
post
(
'/'
,
'/'
,
data
=
json
.
dumps
(
{}
),
data
=
json
.
dumps
(
data
),
content_type
=
'application/json'
,
content_type
=
'application/json'
,
)
)
request
.
user
=
self
.
mock_user
request
.
user
=
self
.
mock_user
...
@@ -685,12 +653,12 @@ class TestHandleXBlockCallback(SharedModuleStoreTestCase, LoginEnrollmentTestCas
...
@@ -685,12 +653,12 @@ class TestHandleXBlockCallback(SharedModuleStoreTestCase, LoginEnrollmentTestCas
request
,
request
,
unicode
(
course
.
id
),
unicode
(
course
.
id
),
quote_slashes
(
unicode
(
block
.
scope_ids
.
usage_id
)),
quote_slashes
(
unicode
(
block
.
scope_ids
.
usage_id
)),
'progress'
,
signal
,
''
,
''
,
)
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
completion
=
BlockCompletion
.
objects
.
get
(
block_key
=
block
.
scope_ids
.
usage_id
)
completion
=
BlockCompletion
.
objects
.
get
(
block_key
=
block
.
scope_ids
.
usage_id
)
self
.
assertEqual
(
completion
.
completion
,
1.0
)
self
.
assertEqual
(
completion
.
completion
,
expected_completion
)
@XBlock.register_temp_plugin
(
StubCompletableXBlock
,
identifier
=
'comp'
)
@XBlock.register_temp_plugin
(
StubCompletableXBlock
,
identifier
=
'comp'
)
def
test_skip_handlers_for_masquerading_staff
(
self
):
def
test_skip_handlers_for_masquerading_staff
(
self
):
...
...
lms/djangoapps/grades/signals/signals.py
View file @
2cadbbad
...
@@ -26,6 +26,8 @@ PROBLEM_RAW_SCORE_CHANGED = Signal(
...
@@ -26,6 +26,8 @@ PROBLEM_RAW_SCORE_CHANGED = Signal(
'modified'
,
# A datetime indicating when the database representation of
'modified'
,
# A datetime indicating when the database representation of
# this the problem score was saved.
# this the problem score was saved.
'score_db_table'
,
# The database table that houses the score that changed.
'score_db_table'
,
# The database table that houses the score that changed.
'score_deleted'
,
# Boolean indicating whether the score changed due to
# the user state being deleted.
]
]
)
)
...
@@ -49,6 +51,8 @@ PROBLEM_WEIGHTED_SCORE_CHANGED = Signal(
...
@@ -49,6 +51,8 @@ PROBLEM_WEIGHTED_SCORE_CHANGED = Signal(
'modified'
,
# A datetime indicating when the database representation of
'modified'
,
# A datetime indicating when the database representation of
# this the problem score was saved.
# this the problem score was saved.
'score_db_table'
,
# The database table that houses the score that changed.
'score_db_table'
,
# The database table that houses the score that changed.
'score_deleted'
,
# Boolean indicating whether the score changed due to
# the user state being deleted.
]
]
)
)
...
...
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