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
a1499e28
Commit
a1499e28
authored
Oct 25, 2016
by
Eric Fischer
Committed by
GitHub
Oct 25, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #13766 from edx/efischer/course_grade_update
Course Grade Update Signal/Task
parents
ba491a64
563122fe
Show whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
184 additions
and
51 deletions
+184
-51
common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py
+2
-0
lms/djangoapps/courseware/tests/test_module_render.py
+1
-1
lms/djangoapps/courseware/tests/test_submitting_problems.py
+2
-2
lms/djangoapps/gating/signals.py
+3
-3
lms/djangoapps/grades/new/course_grade.py
+8
-1
lms/djangoapps/grades/signals/handlers.py
+24
-12
lms/djangoapps/grades/signals/signals.py
+3
-3
lms/djangoapps/grades/tasks.py
+20
-1
lms/djangoapps/grades/tests/test_new.py
+2
-0
lms/djangoapps/grades/tests/test_signals.py
+2
-2
lms/djangoapps/grades/tests/test_tasks.py
+107
-16
lms/djangoapps/instructor/enrollment.py
+3
-3
lms/djangoapps/instructor/tests/test_api.py
+1
-1
lms/djangoapps/instructor/tests/test_enrollment.py
+2
-2
lms/djangoapps/instructor/tests/test_services.py
+1
-1
lms/djangoapps/lti_provider/tasks.py
+3
-3
No files found.
common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py
View file @
a1499e28
...
...
@@ -7,6 +7,7 @@ import ddt
from
nose.plugins.attrib
import
attr
from
bok_choy.promise
import
EmptyPromise
from
flaky
import
flaky
from
common.test.acceptance.tests.helpers
import
UniqueCourseTest
,
get_modal_alert
,
EventsTestMixin
from
common.test.acceptance.pages.common.logout
import
LogoutPage
...
...
@@ -377,6 +378,7 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
# Then, the added record should be visible
self
.
assertTrue
(
allowance_section
.
is_allowance_record_visible
)
@flaky
# TNL-5832
def
test_can_reset_attempts
(
self
):
"""
Make sure that Exam attempts are visible and can be reset.
...
...
lms/djangoapps/courseware/tests/test_module_render.py
View file @
a1499e28
...
...
@@ -1831,7 +1831,7 @@ class TestXmoduleRuntimeEvent(TestSubmittingProblems):
self
.
assertIsNone
(
student_module
.
grade
)
self
.
assertIsNone
(
student_module
.
max_grade
)
@patch
(
'lms.djangoapps.grades.signals.handlers.SCORE_CHANGED.send'
)
@patch
(
'lms.djangoapps.grades.signals.handlers.
PROBLEM_
SCORE_CHANGED.send'
)
def
test_score_change_signal
(
self
,
send_mock
):
"""Test that a Django signal is generated when a score changes"""
self
.
set_module_grade_using_publish
(
self
.
grade_dict
)
...
...
lms/djangoapps/courseware/tests/test_submitting_problems.py
View file @
a1499e28
...
...
@@ -153,7 +153,7 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl
self
.
student_user
=
User
.
objects
.
get
(
email
=
self
.
student
)
self
.
factory
=
RequestFactory
()
# Disable the score change signal to prevent other components from being pulled into tests.
self
.
score_changed_signal_patch
=
patch
(
'lms.djangoapps.grades.signals.handlers.SCORE_CHANGED.send'
)
self
.
score_changed_signal_patch
=
patch
(
'lms.djangoapps.grades.signals.handlers.
PROBLEM_
SCORE_CHANGED.send'
)
self
.
score_changed_signal_patch
.
start
()
def
tearDown
(
self
):
...
...
@@ -162,7 +162,7 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl
def
_stop_signal_patch
(
self
):
"""
Stops the signal patch for the SCORE_CHANGED event.
Stops the signal patch for the
PROBLEM_
SCORE_CHANGED event.
In case a test wants to test with the event actually
firing.
"""
...
...
lms/djangoapps/gating/signals.py
View file @
a1499e28
...
...
@@ -4,15 +4,15 @@ Signal handlers for the gating djangoapp
from
django.dispatch
import
receiver
from
gating
import
api
as
gating_api
from
lms.djangoapps.grades.signals.signals
import
SCORE_CHANGED
,
SUBSECTION_SCORE_CHANGED
from
lms.djangoapps.grades.signals.signals
import
PROBLEM_
SCORE_CHANGED
,
SUBSECTION_SCORE_CHANGED
from
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
from
xmodule.modulestore.django
import
modulestore
@receiver
(
SCORE_CHANGED
)
@receiver
(
PROBLEM_
SCORE_CHANGED
)
def
handle_score_changed
(
**
kwargs
):
"""
Receives the SCORE_CHANGED signal sent by LMS when a student's score has changed
Receives the
PROBLEM_
SCORE_CHANGED signal sent by LMS when a student's score has changed
for a given component and triggers the evaluation of any milestone relationships
which are attached to the updated content.
...
...
lms/djangoapps/grades/new/course_grade.py
View file @
a1499e28
...
...
@@ -260,7 +260,14 @@ class CourseGradeFactory(object):
self
.
_compute_and_update_grade
(
course
,
course_structure
,
read_only
)
)
def
_compute_and_update_grade
(
self
,
course
,
course_structure
,
read_only
):
def
update
(
self
,
course
):
"""
Updates the CourseGrade for this Factory's student.
"""
course_structure
=
get_course_blocks
(
self
.
student
,
course
.
location
)
self
.
_compute_and_update_grade
(
course
,
course_structure
)
def
_compute_and_update_grade
(
self
,
course
,
course_structure
,
read_only
=
False
):
"""
Freshly computes and updates the grade for the student and course.
...
...
lms/djangoapps/grades/signals/handlers.py
View file @
a1499e28
...
...
@@ -2,6 +2,7 @@
Grades related signals.
"""
from
celery
import
Task
from
django.dispatch
import
receiver
from
logging
import
getLogger
...
...
@@ -10,8 +11,8 @@ from openedx.core.lib.grade_utils import is_score_higher
from
student.models
import
user_by_anonymous_id
from
submissions.models
import
score_set
,
score_reset
from
.signals
import
SCORE_CHANGED
,
SCORE_PUBLISHED
from
..tasks
import
recalculate_subsection_grade
from
.signals
import
PROBLEM_SCORE_CHANGED
,
SUBSECTION_
SCORE_CHANGED
,
SCORE_PUBLISHED
from
..tasks
import
recalculate_subsection_grade
,
recalculate_course_grade
log
=
getLogger
(
__name__
)
...
...
@@ -21,9 +22,9 @@ log = getLogger(__name__)
def
submissions_score_set_handler
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Consume the score_set signal defined in the Submissions API, and convert it
to a SCORE_CHANGED signal defined in this module. Converts the unicode keys
to a
PROBLEM_
SCORE_CHANGED signal defined in this module. Converts the unicode keys
for user, course and item into the standard representation for the
SCORE_CHANGED signal.
PROBLEM_
SCORE_CHANGED signal.
This method expects that the kwargs dictionary will contain the following
entries (See the definition of score_set):
...
...
@@ -41,7 +42,7 @@ def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-a
if
user
is
None
:
return
SCORE_CHANGED
.
send
(
PROBLEM_
SCORE_CHANGED
.
send
(
sender
=
None
,
points_earned
=
points_earned
,
points_possible
=
points_possible
,
...
...
@@ -55,9 +56,9 @@ def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-a
def
submissions_score_reset_handler
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Consume the score_reset signal defined in the Submissions API, and convert
it to a SCORE_CHANGED signal indicating that the score has been set to 0/0.
it to a
PROBLEM_
SCORE_CHANGED signal indicating that the score has been set to 0/0.
Converts the unicode keys for user, course and item into the standard
representation for the SCORE_CHANGED signal.
representation for the
PROBLEM_
SCORE_CHANGED signal.
This method expects that the kwargs dictionary will contain the following
entries (See the definition of score_reset):
...
...
@@ -71,7 +72,7 @@ def submissions_score_reset_handler(sender, **kwargs): # pylint: disable=unused
if
user
is
None
:
return
SCORE_CHANGED
.
send
(
PROBLEM_
SCORE_CHANGED
.
send
(
sender
=
None
,
points_earned
=
0
,
points_possible
=
0
,
...
...
@@ -106,7 +107,7 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_
if
update_score
:
set_score
(
user
.
id
,
block
.
location
,
raw_earned
,
raw_possible
)
SCORE_CHANGED
.
send
(
PROBLEM_
SCORE_CHANGED
.
send
(
sender
=
None
,
points_earned
=
raw_earned
,
points_possible
=
raw_possible
,
...
...
@@ -118,10 +119,10 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_
return
update_score
@receiver
(
SCORE_CHANGED
)
def
enqueue_
grade
_update
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
@receiver
(
PROBLEM_
SCORE_CHANGED
)
def
enqueue_
subsection
_update
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Handles the
SCORE_CHANGED signal by enqueueing a
n update operation to occur asynchronously.
Handles the
PROBLEM_SCORE_CHANGED signal by enqueueing a subsectio
n update operation to occur asynchronously.
"""
recalculate_subsection_grade
.
apply_async
(
args
=
(
...
...
@@ -131,3 +132,14 @@ def enqueue_grade_update(sender, **kwargs): # pylint: disable=unused-argument
kwargs
.
get
(
'only_if_higher'
),
)
)
@receiver
(
SUBSECTION_SCORE_CHANGED
)
def
enqueue_course_update
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Handles the SUBSECTION_SCORE_CHANGED signal by enqueueing a course update operation to occur asynchronously.
"""
if
isinstance
(
sender
,
Task
):
# We're already in a async worker, just do the task
recalculate_course_grade
.
apply
(
args
=
(
kwargs
[
'user'
]
.
id
,
unicode
(
kwargs
[
'course'
]
.
id
)))
else
:
# Otherwise, queue the work to be done asynchronously
recalculate_course_grade
.
apply_async
(
args
=
(
kwargs
[
'user'
]
.
id
,
unicode
(
kwargs
[
'course'
]
.
id
)))
lms/djangoapps/grades/signals/signals.py
View file @
a1499e28
...
...
@@ -10,7 +10,7 @@ from django.dispatch import Signal
# regardless of the new and previous values of the score (i.e. it may be the
# case that this signal is generated when a user re-attempts a problem but
# receives the same score).
SCORE_CHANGED
=
Signal
(
PROBLEM_
SCORE_CHANGED
=
Signal
(
providing_args
=
[
'user_id'
,
# Integer User ID
'course_id'
,
# Unicode string representing the course
...
...
@@ -25,7 +25,7 @@ SCORE_CHANGED = Signal(
# Signal that indicates that a user's score for a problem has been published
# for possible persistence and update. Typically, most clients should listen
# to the SCORE_CHANGED signal instead, since that is signalled only after the
# to the
PROBLEM_
SCORE_CHANGED signal instead, since that is signalled only after the
# problem's score is changed.
SCORE_PUBLISHED
=
Signal
(
providing_args
=
[
...
...
@@ -40,7 +40,7 @@ SCORE_PUBLISHED = Signal(
# Signal that indicates that a user's score for a subsection has been updated.
# This is a downstream signal of SCORE_CHANGED sent for each affected containing
# This is a downstream signal of
PROBLEM_
SCORE_CHANGED sent for each affected containing
# subsection.
SUBSECTION_SCORE_CHANGED
=
Signal
(
providing_args
=
[
...
...
lms/djangoapps/grades/tasks.py
View file @
a1499e28
...
...
@@ -13,6 +13,7 @@ from opaque_keys.edx.locator import CourseLocator
from
openedx.core.djangoapps.content.block_structure.api
import
get_course_in_cache
from
xmodule.modulestore.django
import
modulestore
from
.new.course_grade
import
CourseGradeFactory
from
.new.subsection_grade
import
SubsectionGradeFactory
from
.signals.signals
import
SUBSECTION_SCORE_CHANGED
from
.transformer
import
GradesTransformer
...
...
@@ -57,7 +58,7 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher):
only_if_higher
,
)
SUBSECTION_SCORE_CHANGED
.
send
(
sender
=
Non
e
,
sender
=
recalculate_subsection_grad
e
,
course
=
course
,
user
=
student
,
subsection_grade
=
subsection_grade
,
...
...
@@ -65,3 +66,21 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher):
except
IntegrityError
as
exc
:
raise
recalculate_subsection_grade
.
retry
(
args
=
[
user_id
,
course_id
,
usage_id
],
exc
=
exc
)
@task
(
default_retry_delay
=
30
,
routing_key
=
settings
.
RECALCULATE_GRADES_ROUTING_KEY
)
def
recalculate_course_grade
(
user_id
,
course_id
):
"""
Updates a saved course grade.
This method expects the following parameters:
- user_id: serialized id of applicable User object
- course_id: Unicode string representing the course
"""
student
=
User
.
objects
.
get
(
id
=
user_id
)
course_key
=
CourseLocator
.
from_string
(
course_id
)
course
=
modulestore
()
.
get_course
(
course_key
,
depth
=
0
)
try
:
CourseGradeFactory
(
student
)
.
update
(
course
)
except
IntegrityError
as
exc
:
raise
recalculate_course_grade
.
retry
(
args
=
[
user_id
,
course_id
],
exc
=
exc
)
lms/djangoapps/grades/tests/test_new.py
View file @
a1499e28
...
...
@@ -574,6 +574,8 @@ class TestCourseGradeLogging(SharedModuleStoreTestCase):
enabled_for_course
=
True
):
with
patch
(
'lms.djangoapps.grades.new.course_grade.log'
)
as
log_mock
:
# TODO: once merged with the "glue code" PR, update expected logging to include the relevant new info
# the course grade has not been created, so we expect each grade to be created
self
.
_create_course_grade_and_check_logging
(
grade_factory
,
...
...
lms/djangoapps/grades/tests/test_signals.py
View file @
a1499e28
...
...
@@ -42,7 +42,7 @@ class SubmissionSignalRelayTest(TestCase):
Configure mocks for all the dependencies of the render method
"""
super
(
SubmissionSignalRelayTest
,
self
)
.
setUp
()
self
.
signal_mock
=
self
.
setup_patch
(
'lms.djangoapps.grades.signals.signals.SCORE_CHANGED.send'
,
None
)
self
.
signal_mock
=
self
.
setup_patch
(
'lms.djangoapps.grades.signals.signals.
PROBLEM_
SCORE_CHANGED.send'
,
None
)
self
.
user_mock
=
MagicMock
()
self
.
user_mock
.
id
=
42
self
.
get_user_mock
=
self
.
setup_patch
(
...
...
@@ -68,7 +68,7 @@ class SubmissionSignalRelayTest(TestCase):
def
test_score_set_signal_handler
(
self
,
handler
,
kwargs
,
earned
,
possible
):
"""
Ensure that on receipt of a score_(re)set signal from the Submissions API,
the signal handler correctly converts it to a SCORE_CHANGED signal.
the signal handler correctly converts it to a
PROBLEM_
SCORE_CHANGED signal.
Also ensures that the handler calls user_by_anonymous_id correctly.
"""
...
...
lms/djangoapps/grades/tests/test_tasks.py
View file @
a1499e28
...
...
@@ -7,17 +7,20 @@ import ddt
from
django.conf
import
settings
from
django.db.utils
import
IntegrityError
from
mock
import
patch
from
uuid
import
uuid4
from
unittest
import
skip
from
opaque_keys.edx.locator
import
CourseLocator
from
student.models
import
anonymous_id_for_user
from
student.tests.factories
import
UserFactory
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
,
check_mongo_calls
from
lms.djangoapps.grades.config.models
import
PersistentGradesEnabledFlag
from
lms.djangoapps.grades.signals.signals
import
SCORE_CHANGED
from
lms.djangoapps.grades.tasks
import
recalculate_subsection_grade
from
lms.djangoapps.grades.signals.signals
import
PROBLEM_SCORE_CHANGED
,
SUBSECTION_
SCORE_CHANGED
from
lms.djangoapps.grades.tasks
import
recalculate_
course_grade
,
recalculate_
subsection_grade
@patch.dict
(
settings
.
FEATURES
,
{
'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS'
:
False
})
...
...
@@ -59,24 +62,91 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
_
=
anonymous_id_for_user
(
self
.
user
,
self
.
course
.
id
)
# pylint: enable=attribute-defined-outside-init,no-member
def
test_score_changed_signal_queues_task
(
self
):
@ddt.data
(
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade.apply_async'
,
PROBLEM_SCORE_CHANGED
),
(
'lms.djangoapps.grades.tasks.recalculate_course_grade.apply_async'
,
SUBSECTION_SCORE_CHANGED
)
)
@ddt.unpack
def
test_signal_queues_task
(
self
,
enqueue_op
,
test_signal
):
"""
Ensures that the
SCORE_CHANGED signal enqueues a recalculate subsection grade task
.
Ensures that the
PROBLEM_SCORE_CHANGED and SUBSECTION_SCORE_CHANGED signals enqueue the correct tasks
.
"""
self
.
set_up_course
()
if
test_signal
==
PROBLEM_SCORE_CHANGED
:
send_args
=
self
.
score_changed_kwargs
expected_args
=
tuple
(
self
.
score_changed_kwargs
.
values
())
else
:
send_args
=
{
'user'
:
self
.
user
,
'course'
:
self
.
course
}
expected_args
=
(
self
.
score_changed_kwargs
[
'user_id'
],
self
.
score_changed_kwargs
[
'course_id'
])
with
patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade.apply_async'
,
enqueue_op
,
return_value
=
None
)
as
mock_task_apply
:
SCORE_CHANGED
.
send
(
sender
=
None
,
**
self
.
score_changed_kw
args
)
mock_task_apply
.
assert_called_once_with
(
args
=
tuple
(
self
.
score_changed_kwargs
.
values
())
)
test_signal
.
send
(
sender
=
None
,
**
send_
args
)
mock_task_apply
.
assert_called_once_with
(
args
=
expected_args
)
@ddt.data
(
ModuleStoreEnum
.
Type
.
mongo
,
ModuleStoreEnum
.
Type
.
split
)
def
test_subsection_grade_updated
(
self
,
default_store
):
@patch
(
'lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send'
)
def
test_subsection_update_triggers_course_update
(
self
,
mock_course_signal
):
"""
Ensures that the subsection update operation also updates the course grade.
"""
self
.
set_up_course
()
mock_return
=
uuid4
()
course_key
=
CourseLocator
.
from_string
(
unicode
(
self
.
course
.
id
))
course
=
modulestore
()
.
get_course
(
course_key
,
depth
=
0
)
with
patch
(
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update'
,
return_value
=
mock_return
):
recalculate_subsection_grade
.
apply
(
args
=
tuple
(
self
.
score_changed_kwargs
.
values
()))
mock_course_signal
.
assert_called_once_with
(
sender
=
recalculate_subsection_grade
,
course
=
course
,
user
=
self
.
user
,
subsection_grade
=
mock_return
,
)
@ddt.data
(
True
,
False
)
def
test_course_update_enqueuing
(
self
,
should_be_async
):
"""
Ensures that the course update operation is enqueued on an async queue (or not) as expected.
"""
base
=
'lms.djangoapps.grades.tasks.recalculate_course_grade'
if
should_be_async
:
executed
=
base
+
'.apply_async'
other
=
base
+
'.apply'
sender
=
None
else
:
executed
=
base
+
'.apply'
other
=
base
+
'.apply_async'
sender
=
recalculate_subsection_grade
self
.
set_up_course
()
with
patch
(
executed
)
as
executed_task
:
with
patch
(
other
)
as
other_task
:
SUBSECTION_SCORE_CHANGED
.
send
(
sender
=
sender
,
course
=
self
.
course
,
user
=
self
.
user
,
)
other_task
.
assert_not_called
()
executed_task
.
assert_called_once_with
(
args
=
(
self
.
score_changed_kwargs
[
'user_id'
],
self
.
score_changed_kwargs
[
'course_id'
],
)
)
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
),
(
ModuleStoreEnum
.
Type
.
split
,
0
),
)
@ddt.unpack
def
test_subsection_grade_updated
(
self
,
default_store
,
added_queries
):
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
()
self
.
assertTrue
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
with
check_mongo_calls
(
2
)
and
self
.
assertNumQueries
(
13
):
with
check_mongo_calls
(
2
)
and
self
.
assertNumQueries
(
21
+
added_queries
):
recalculate_subsection_grade
.
apply
(
args
=
tuple
(
self
.
score_changed_kwargs
.
values
()))
def
test_single_call_to_create_block_structure
(
self
):
...
...
@@ -87,16 +157,20 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
return_value
=
None
,
)
as
mock_block_structure_create
:
recalculate_subsection_grade
.
apply
(
args
=
tuple
(
self
.
score_changed_kwargs
.
values
()))
self
.
assertEquals
(
mock_block_structure_create
.
call_count
,
1
)
self
.
assertEquals
(
mock_block_structure_create
.
call_count
,
2
)
@ddt.data
(
ModuleStoreEnum
.
Type
.
mongo
,
ModuleStoreEnum
.
Type
.
split
)
def
test_query_count_does_not_change_with_more_problems
(
self
,
default_store
):
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
),
(
ModuleStoreEnum
.
Type
.
split
,
0
),
)
@ddt.unpack
def
test_query_count_does_not_change_with_more_problems
(
self
,
default_store
,
added_queries
):
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
()
self
.
assertTrue
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
ItemFactory
.
create
(
parent
=
self
.
sequential
,
category
=
'problem'
,
display_name
=
'problem2'
)
ItemFactory
.
create
(
parent
=
self
.
sequential
,
category
=
'problem'
,
display_name
=
'problem3'
)
with
check_mongo_calls
(
2
)
and
self
.
assertNumQueries
(
13
):
with
check_mongo_calls
(
2
)
and
self
.
assertNumQueries
(
21
+
added_queries
):
recalculate_subsection_grade
.
apply
(
args
=
tuple
(
self
.
score_changed_kwargs
.
values
()))
@ddt.data
(
ModuleStoreEnum
.
Type
.
mongo
,
ModuleStoreEnum
.
Type
.
split
)
...
...
@@ -104,7 +178,8 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
(
enable_subsection_grades
=
False
)
self
.
assertFalse
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
with
check_mongo_calls
(
2
)
and
self
.
assertNumQueries
(
5
):
additional_queries
=
1
if
default_store
==
ModuleStoreEnum
.
Type
.
mongo
else
0
with
check_mongo_calls
(
2
)
and
self
.
assertNumQueries
(
12
+
additional_queries
):
recalculate_subsection_grade
.
apply
(
args
=
tuple
(
self
.
score_changed_kwargs
.
values
()))
@skip
(
"Pending completion of TNL-5089"
)
...
...
@@ -124,7 +199,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade.retry'
)
@patch
(
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update'
)
def
test_retry_on_integrity_error
(
self
,
mock_update
,
mock_retry
):
def
test_retry_
subsection_update_
on_integrity_error
(
self
,
mock_update
,
mock_retry
):
"""
Ensures that tasks will be retried if IntegrityErrors are encountered.
"""
...
...
@@ -132,3 +207,19 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
mock_update
.
side_effect
=
IntegrityError
(
"WHAMMY"
)
recalculate_subsection_grade
.
apply
(
args
=
tuple
(
self
.
score_changed_kwargs
.
values
()))
self
.
assertTrue
(
mock_retry
.
called
)
@patch
(
'lms.djangoapps.grades.tasks.recalculate_course_grade.retry'
)
@patch
(
'lms.djangoapps.grades.new.course_grade.CourseGradeFactory.update'
)
def
test_retry_course_update_on_integrity_error
(
self
,
mock_update
,
mock_retry
):
"""
Ensures that tasks will be retried if IntegrityErrors are encountered.
"""
self
.
set_up_course
()
mock_update
.
side_effect
=
IntegrityError
(
"WHAMMY"
)
recalculate_course_grade
.
apply
(
args
=
(
self
.
score_changed_kwargs
[
'user_id'
],
self
.
score_changed_kwargs
[
'course_id'
],
)
)
self
.
assertTrue
(
mock_retry
.
called
)
lms/djangoapps/instructor/enrollment.py
View file @
a1499e28
...
...
@@ -19,7 +19,7 @@ from courseware.model_data import FieldDataCache
from
courseware.module_render
import
get_module_for_descriptor
from
edxmako.shortcuts
import
render_to_string
from
lms.djangoapps.grades.scores
import
weighted_score
from
lms.djangoapps.grades.signals.signals
import
SCORE_CHANGED
from
lms.djangoapps.grades.signals.signals
import
PROBLEM_
SCORE_CHANGED
from
openedx.core.djangoapps.lang_pref
import
LANGUAGE_KEY
from
openedx.core.djangoapps.site_configuration
import
helpers
as
configuration_helpers
from
openedx.core.djangoapps.user_api.models
import
UserPreference
...
...
@@ -293,7 +293,7 @@ def _reset_module_attempts(studentmodule):
def
_fire_score_changed_for_block
(
course_id
,
student
,
block
,
module_state_key
):
"""
Fires a SCORE_CHANGED event for the given module. The earned points are
Fires a
PROBLEM_
SCORE_CHANGED event for the given module. The earned points are
always zero. We must retrieve the possible points from the XModule, as
noted below.
"""
...
...
@@ -322,7 +322,7 @@ def _fire_score_changed_for_block(course_id, student, block, module_state_key):
points_earned
,
points_possible
=
weighted_score
(
0
,
max_score
,
getattr
(
module
,
'weight'
,
None
))
else
:
points_earned
,
points_possible
=
0
,
0
SCORE_CHANGED
.
send
(
PROBLEM_
SCORE_CHANGED
.
send
(
sender
=
None
,
points_possible
=
points_possible
,
points_earned
=
points_earned
,
...
...
lms/djangoapps/instructor/tests/test_api.py
View file @
a1499e28
...
...
@@ -3190,7 +3190,7 @@ class TestInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginEnrollmentTes
})
self
.
assertEqual
(
response
.
status_code
,
400
)
@patch
(
'lms.djangoapps.grades.signals.handlers.SCORE_CHANGED.send'
)
@patch
(
'lms.djangoapps.grades.signals.handlers.
PROBLEM_
SCORE_CHANGED.send'
)
def
test_reset_student_attempts_delete
(
self
,
_mock_signal
):
""" Test delete single student state. """
url
=
reverse
(
'reset_student_attempts'
,
kwargs
=
{
'course_id'
:
self
.
course
.
id
.
to_deprecated_string
()})
...
...
lms/djangoapps/instructor/tests/test_enrollment.py
View file @
a1499e28
...
...
@@ -378,7 +378,7 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase):
reset_student_attempts
(
self
.
course_key
,
self
.
user
,
msk
,
requesting_user
=
self
.
user
)
self
.
assertEqual
(
json
.
loads
(
module
()
.
state
)[
'attempts'
],
0
)
@mock.patch
(
'lms.djangoapps.grades.signals.handlers.SCORE_CHANGED.send'
)
@mock.patch
(
'lms.djangoapps.grades.signals.handlers.
PROBLEM_
SCORE_CHANGED.send'
)
def
test_delete_student_attempts
(
self
,
_mock_signal
):
msk
=
self
.
course_key
.
make_usage_key
(
'dummy'
,
'module'
)
original_state
=
json
.
dumps
({
'attempts'
:
32
,
'otherstuff'
:
'alsorobots'
})
...
...
@@ -404,7 +404,7 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase):
# Disable the score change signal to prevent other components from being
# pulled into tests.
@mock.patch
(
'lms.djangoapps.grades.signals.handlers.SCORE_CHANGED.send'
)
@mock.patch
(
'lms.djangoapps.grades.signals.handlers.
PROBLEM_
SCORE_CHANGED.send'
)
def
test_delete_submission_scores
(
self
,
_mock_signal
):
user
=
UserFactory
()
problem_location
=
self
.
course_key
.
make_usage_key
(
'dummy'
,
'module'
)
...
...
lms/djangoapps/instructor/tests/test_services.py
View file @
a1499e28
...
...
@@ -49,7 +49,7 @@ class InstructorServiceTests(SharedModuleStoreTestCase):
state
=
json
.
dumps
({
'attempts'
:
2
}),
)
@mock.patch
(
'lms.djangoapps.grades.signals.handlers.SCORE_CHANGED.send'
)
@mock.patch
(
'lms.djangoapps.grades.signals.handlers.
PROBLEM_
SCORE_CHANGED.send'
)
def
test_reset_student_attempts_delete
(
self
,
_mock_signal
):
"""
Test delete student state.
...
...
lms/djangoapps/lti_provider/tasks.py
View file @
a1499e28
...
...
@@ -8,7 +8,7 @@ from django.dispatch import receiver
import
logging
from
lms.djangoapps.grades
import
progress
from
lms.djangoapps.grades.signals.signals
import
SCORE_CHANGED
from
lms.djangoapps.grades.signals.signals
import
PROBLEM_
SCORE_CHANGED
from
lms
import
CELERY_APP
from
lti_provider.models
import
GradedAssignment
import
lti_provider.outcomes
as
outcomes
...
...
@@ -19,11 +19,11 @@ from xmodule.modulestore.django import modulestore
log
=
logging
.
getLogger
(
"edx.lti_provider"
)
@receiver
(
SCORE_CHANGED
)
@receiver
(
PROBLEM_
SCORE_CHANGED
)
def
score_changed_handler
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Consume signals that indicate score changes. See the definition of
SCORE_CHANGED for a description of the signal.
PROBLEM_
SCORE_CHANGED for a description of the signal.
"""
points_possible
=
kwargs
.
get
(
'points_possible'
,
None
)
points_earned
=
kwargs
.
get
(
'points_earned'
,
None
)
...
...
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