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
Hide 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
...
@@ -7,6 +7,7 @@ import ddt
from
nose.plugins.attrib
import
attr
from
nose.plugins.attrib
import
attr
from
bok_choy.promise
import
EmptyPromise
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.tests.helpers
import
UniqueCourseTest
,
get_modal_alert
,
EventsTestMixin
from
common.test.acceptance.pages.common.logout
import
LogoutPage
from
common.test.acceptance.pages.common.logout
import
LogoutPage
...
@@ -377,6 +378,7 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
...
@@ -377,6 +378,7 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
# Then, the added record should be visible
# Then, the added record should be visible
self
.
assertTrue
(
allowance_section
.
is_allowance_record_visible
)
self
.
assertTrue
(
allowance_section
.
is_allowance_record_visible
)
@flaky
# TNL-5832
def
test_can_reset_attempts
(
self
):
def
test_can_reset_attempts
(
self
):
"""
"""
Make sure that Exam attempts are visible and can be reset.
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):
...
@@ -1831,7 +1831,7 @@ class TestXmoduleRuntimeEvent(TestSubmittingProblems):
self
.
assertIsNone
(
student_module
.
grade
)
self
.
assertIsNone
(
student_module
.
grade
)
self
.
assertIsNone
(
student_module
.
max_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
):
def
test_score_change_signal
(
self
,
send_mock
):
"""Test that a Django signal is generated when a score changes"""
"""Test that a Django signal is generated when a score changes"""
self
.
set_module_grade_using_publish
(
self
.
grade_dict
)
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
...
@@ -153,7 +153,7 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl
self
.
student_user
=
User
.
objects
.
get
(
email
=
self
.
student
)
self
.
student_user
=
User
.
objects
.
get
(
email
=
self
.
student
)
self
.
factory
=
RequestFactory
()
self
.
factory
=
RequestFactory
()
# Disable the score change signal to prevent other components from being pulled into tests.
# 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
()
self
.
score_changed_signal_patch
.
start
()
def
tearDown
(
self
):
def
tearDown
(
self
):
...
@@ -162,7 +162,7 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl
...
@@ -162,7 +162,7 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl
def
_stop_signal_patch
(
self
):
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
In case a test wants to test with the event actually
firing.
firing.
"""
"""
...
...
lms/djangoapps/gating/signals.py
View file @
a1499e28
...
@@ -4,15 +4,15 @@ Signal handlers for the gating djangoapp
...
@@ -4,15 +4,15 @@ Signal handlers for the gating djangoapp
from
django.dispatch
import
receiver
from
django.dispatch
import
receiver
from
gating
import
api
as
gating_api
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
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
@receiver
(
SCORE_CHANGED
)
@receiver
(
PROBLEM_
SCORE_CHANGED
)
def
handle_score_changed
(
**
kwargs
):
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
for a given component and triggers the evaluation of any milestone relationships
which are attached to the updated content.
which are attached to the updated content.
...
...
lms/djangoapps/grades/new/course_grade.py
View file @
a1499e28
...
@@ -260,7 +260,14 @@ class CourseGradeFactory(object):
...
@@ -260,7 +260,14 @@ class CourseGradeFactory(object):
self
.
_compute_and_update_grade
(
course
,
course_structure
,
read_only
)
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.
Freshly computes and updates the grade for the student and course.
...
...
lms/djangoapps/grades/signals/handlers.py
View file @
a1499e28
...
@@ -2,6 +2,7 @@
...
@@ -2,6 +2,7 @@
Grades related signals.
Grades related signals.
"""
"""
from
celery
import
Task
from
django.dispatch
import
receiver
from
django.dispatch
import
receiver
from
logging
import
getLogger
from
logging
import
getLogger
...
@@ -10,8 +11,8 @@ from openedx.core.lib.grade_utils import is_score_higher
...
@@ -10,8 +11,8 @@ from openedx.core.lib.grade_utils import is_score_higher
from
student.models
import
user_by_anonymous_id
from
student.models
import
user_by_anonymous_id
from
submissions.models
import
score_set
,
score_reset
from
submissions.models
import
score_set
,
score_reset
from
.signals
import
SCORE_CHANGED
,
SCORE_PUBLISHED
from
.signals
import
PROBLEM_SCORE_CHANGED
,
SUBSECTION_
SCORE_CHANGED
,
SCORE_PUBLISHED
from
..tasks
import
recalculate_subsection_grade
from
..tasks
import
recalculate_subsection_grade
,
recalculate_course_grade
log
=
getLogger
(
__name__
)
log
=
getLogger
(
__name__
)
...
@@ -21,9 +22,9 @@ log = getLogger(__name__)
...
@@ -21,9 +22,9 @@ log = getLogger(__name__)
def
submissions_score_set_handler
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
def
submissions_score_set_handler
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
"""
"""
Consume the score_set signal defined in the Submissions API, and convert it
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
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
This method expects that the kwargs dictionary will contain the following
entries (See the definition of score_set):
entries (See the definition of score_set):
...
@@ -41,7 +42,7 @@ def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-a
...
@@ -41,7 +42,7 @@ def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-a
if
user
is
None
:
if
user
is
None
:
return
return
SCORE_CHANGED
.
send
(
PROBLEM_
SCORE_CHANGED
.
send
(
sender
=
None
,
sender
=
None
,
points_earned
=
points_earned
,
points_earned
=
points_earned
,
points_possible
=
points_possible
,
points_possible
=
points_possible
,
...
@@ -55,9 +56,9 @@ def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-a
...
@@ -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
def
submissions_score_reset_handler
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
"""
"""
Consume the score_reset signal defined in the Submissions API, and convert
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
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
This method expects that the kwargs dictionary will contain the following
entries (See the definition of score_reset):
entries (See the definition of score_reset):
...
@@ -71,7 +72,7 @@ def submissions_score_reset_handler(sender, **kwargs): # pylint: disable=unused
...
@@ -71,7 +72,7 @@ def submissions_score_reset_handler(sender, **kwargs): # pylint: disable=unused
if
user
is
None
:
if
user
is
None
:
return
return
SCORE_CHANGED
.
send
(
PROBLEM_
SCORE_CHANGED
.
send
(
sender
=
None
,
sender
=
None
,
points_earned
=
0
,
points_earned
=
0
,
points_possible
=
0
,
points_possible
=
0
,
...
@@ -106,7 +107,7 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_
...
@@ -106,7 +107,7 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_
if
update_score
:
if
update_score
:
set_score
(
user
.
id
,
block
.
location
,
raw_earned
,
raw_possible
)
set_score
(
user
.
id
,
block
.
location
,
raw_earned
,
raw_possible
)
SCORE_CHANGED
.
send
(
PROBLEM_
SCORE_CHANGED
.
send
(
sender
=
None
,
sender
=
None
,
points_earned
=
raw_earned
,
points_earned
=
raw_earned
,
points_possible
=
raw_possible
,
points_possible
=
raw_possible
,
...
@@ -118,10 +119,10 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_
...
@@ -118,10 +119,10 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_
return
update_score
return
update_score
@receiver
(
SCORE_CHANGED
)
@receiver
(
PROBLEM_
SCORE_CHANGED
)
def
enqueue_
grade
_update
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
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
(
recalculate_subsection_grade
.
apply_async
(
args
=
(
args
=
(
...
@@ -131,3 +132,14 @@ def enqueue_grade_update(sender, **kwargs): # pylint: disable=unused-argument
...
@@ -131,3 +132,14 @@ def enqueue_grade_update(sender, **kwargs): # pylint: disable=unused-argument
kwargs
.
get
(
'only_if_higher'
),
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
...
@@ -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
# 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
# case that this signal is generated when a user re-attempts a problem but
# receives the same score).
# receives the same score).
SCORE_CHANGED
=
Signal
(
PROBLEM_
SCORE_CHANGED
=
Signal
(
providing_args
=
[
providing_args
=
[
'user_id'
,
# Integer User ID
'user_id'
,
# Integer User ID
'course_id'
,
# Unicode string representing the course
'course_id'
,
# Unicode string representing the course
...
@@ -25,7 +25,7 @@ SCORE_CHANGED = Signal(
...
@@ -25,7 +25,7 @@ SCORE_CHANGED = Signal(
# Signal that indicates that a user's score for a problem has been published
# Signal that indicates that a user's score for a problem has been published
# for possible persistence and update. Typically, most clients should listen
# 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.
# problem's score is changed.
SCORE_PUBLISHED
=
Signal
(
SCORE_PUBLISHED
=
Signal
(
providing_args
=
[
providing_args
=
[
...
@@ -40,7 +40,7 @@ SCORE_PUBLISHED = Signal(
...
@@ -40,7 +40,7 @@ SCORE_PUBLISHED = Signal(
# Signal that indicates that a user's score for a subsection has been updated.
# 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.
SUBSECTION_SCORE_CHANGED
=
Signal
(
SUBSECTION_SCORE_CHANGED
=
Signal
(
providing_args
=
[
providing_args
=
[
...
...
lms/djangoapps/grades/tasks.py
View file @
a1499e28
...
@@ -13,6 +13,7 @@ from opaque_keys.edx.locator import CourseLocator
...
@@ -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
openedx.core.djangoapps.content.block_structure.api
import
get_course_in_cache
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
.new.course_grade
import
CourseGradeFactory
from
.new.subsection_grade
import
SubsectionGradeFactory
from
.new.subsection_grade
import
SubsectionGradeFactory
from
.signals.signals
import
SUBSECTION_SCORE_CHANGED
from
.signals.signals
import
SUBSECTION_SCORE_CHANGED
from
.transformer
import
GradesTransformer
from
.transformer
import
GradesTransformer
...
@@ -57,7 +58,7 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher):
...
@@ -57,7 +58,7 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher):
only_if_higher
,
only_if_higher
,
)
)
SUBSECTION_SCORE_CHANGED
.
send
(
SUBSECTION_SCORE_CHANGED
.
send
(
sender
=
Non
e
,
sender
=
recalculate_subsection_grad
e
,
course
=
course
,
course
=
course
,
user
=
student
,
user
=
student
,
subsection_grade
=
subsection_grade
,
subsection_grade
=
subsection_grade
,
...
@@ -65,3 +66,21 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher):
...
@@ -65,3 +66,21 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher):
except
IntegrityError
as
exc
:
except
IntegrityError
as
exc
:
raise
recalculate_subsection_grade
.
retry
(
args
=
[
user_id
,
course_id
,
usage_id
],
exc
=
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):
...
@@ -574,6 +574,8 @@ class TestCourseGradeLogging(SharedModuleStoreTestCase):
enabled_for_course
=
True
enabled_for_course
=
True
):
):
with
patch
(
'lms.djangoapps.grades.new.course_grade.log'
)
as
log_mock
:
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
# the course grade has not been created, so we expect each grade to be created
self
.
_create_course_grade_and_check_logging
(
self
.
_create_course_grade_and_check_logging
(
grade_factory
,
grade_factory
,
...
...
lms/djangoapps/grades/tests/test_signals.py
View file @
a1499e28
...
@@ -42,7 +42,7 @@ class SubmissionSignalRelayTest(TestCase):
...
@@ -42,7 +42,7 @@ class SubmissionSignalRelayTest(TestCase):
Configure mocks for all the dependencies of the render method
Configure mocks for all the dependencies of the render method
"""
"""
super
(
SubmissionSignalRelayTest
,
self
)
.
setUp
()
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
=
MagicMock
()
self
.
user_mock
.
id
=
42
self
.
user_mock
.
id
=
42
self
.
get_user_mock
=
self
.
setup_patch
(
self
.
get_user_mock
=
self
.
setup_patch
(
...
@@ -68,7 +68,7 @@ class SubmissionSignalRelayTest(TestCase):
...
@@ -68,7 +68,7 @@ class SubmissionSignalRelayTest(TestCase):
def
test_score_set_signal_handler
(
self
,
handler
,
kwargs
,
earned
,
possible
):
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,
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.
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
...
@@ -7,17 +7,20 @@ import ddt
from
django.conf
import
settings
from
django.conf
import
settings
from
django.db.utils
import
IntegrityError
from
django.db.utils
import
IntegrityError
from
mock
import
patch
from
mock
import
patch
from
uuid
import
uuid4
from
unittest
import
skip
from
unittest
import
skip
from
opaque_keys.edx.locator
import
CourseLocator
from
student.models
import
anonymous_id_for_user
from
student.models
import
anonymous_id_for_user
from
student.tests.factories
import
UserFactory
from
student.tests.factories
import
UserFactory
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
,
check_mongo_calls
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
,
check_mongo_calls
from
lms.djangoapps.grades.config.models
import
PersistentGradesEnabledFlag
from
lms.djangoapps.grades.config.models
import
PersistentGradesEnabledFlag
from
lms.djangoapps.grades.signals.signals
import
SCORE_CHANGED
from
lms.djangoapps.grades.signals.signals
import
PROBLEM_SCORE_CHANGED
,
SUBSECTION_
SCORE_CHANGED
from
lms.djangoapps.grades.tasks
import
recalculate_subsection_grade
from
lms.djangoapps.grades.tasks
import
recalculate_
course_grade
,
recalculate_
subsection_grade
@patch.dict
(
settings
.
FEATURES
,
{
'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS'
:
False
})
@patch.dict
(
settings
.
FEATURES
,
{
'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS'
:
False
})
...
@@ -59,24 +62,91 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -59,24 +62,91 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
_
=
anonymous_id_for_user
(
self
.
user
,
self
.
course
.
id
)
_
=
anonymous_id_for_user
(
self
.
user
,
self
.
course
.
id
)
# pylint: enable=attribute-defined-outside-init,no-member
# 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
()
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
(
with
patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade.apply_async'
,
enqueue_op
,
return_value
=
None
return_value
=
None
)
as
mock_task_apply
:
)
as
mock_task_apply
:
SCORE_CHANGED
.
send
(
sender
=
None
,
**
self
.
score_changed_kw
args
)
test_signal
.
send
(
sender
=
None
,
**
send_
args
)
mock_task_apply
.
assert_called_once_with
(
args
=
tuple
(
self
.
score_changed_kwargs
.
values
())
)
mock_task_apply
.
assert_called_once_with
(
args
=
expected_args
)
@ddt.data
(
ModuleStoreEnum
.
Type
.
mongo
,
ModuleStoreEnum
.
Type
.
split
)
@patch
(
'lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send'
)
def
test_subsection_grade_updated
(
self
,
default_store
):
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
):
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
()
self
.
set_up_course
()
self
.
assertTrue
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
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
()))
recalculate_subsection_grade
.
apply
(
args
=
tuple
(
self
.
score_changed_kwargs
.
values
()))
def
test_single_call_to_create_block_structure
(
self
):
def
test_single_call_to_create_block_structure
(
self
):
...
@@ -87,16 +157,20 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -87,16 +157,20 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
return_value
=
None
,
return_value
=
None
,
)
as
mock_block_structure_create
:
)
as
mock_block_structure_create
:
recalculate_subsection_grade
.
apply
(
args
=
tuple
(
self
.
score_changed_kwargs
.
values
()))
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
)
@ddt.data
(
def
test_query_count_does_not_change_with_more_problems
(
self
,
default_store
):
(
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
):
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
()
self
.
set_up_course
()
self
.
assertTrue
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
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
=
'problem2'
)
ItemFactory
.
create
(
parent
=
self
.
sequential
,
category
=
'problem'
,
display_name
=
'problem3'
)
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
()))
recalculate_subsection_grade
.
apply
(
args
=
tuple
(
self
.
score_changed_kwargs
.
values
()))
@ddt.data
(
ModuleStoreEnum
.
Type
.
mongo
,
ModuleStoreEnum
.
Type
.
split
)
@ddt.data
(
ModuleStoreEnum
.
Type
.
mongo
,
ModuleStoreEnum
.
Type
.
split
)
...
@@ -104,7 +178,8 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -104,7 +178,8 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
with
self
.
store
.
default_store
(
default_store
):
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
(
enable_subsection_grades
=
False
)
self
.
set_up_course
(
enable_subsection_grades
=
False
)
self
.
assertFalse
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
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
()))
recalculate_subsection_grade
.
apply
(
args
=
tuple
(
self
.
score_changed_kwargs
.
values
()))
@skip
(
"Pending completion of TNL-5089"
)
@skip
(
"Pending completion of TNL-5089"
)
...
@@ -124,7 +199,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -124,7 +199,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade.retry'
)
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade.retry'
)
@patch
(
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update'
)
@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.
Ensures that tasks will be retried if IntegrityErrors are encountered.
"""
"""
...
@@ -132,3 +207,19 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -132,3 +207,19 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
mock_update
.
side_effect
=
IntegrityError
(
"WHAMMY"
)
mock_update
.
side_effect
=
IntegrityError
(
"WHAMMY"
)
recalculate_subsection_grade
.
apply
(
args
=
tuple
(
self
.
score_changed_kwargs
.
values
()))
recalculate_subsection_grade
.
apply
(
args
=
tuple
(
self
.
score_changed_kwargs
.
values
()))
self
.
assertTrue
(
mock_retry
.
called
)
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
...
@@ -19,7 +19,7 @@ from courseware.model_data import FieldDataCache
from
courseware.module_render
import
get_module_for_descriptor
from
courseware.module_render
import
get_module_for_descriptor
from
edxmako.shortcuts
import
render_to_string
from
edxmako.shortcuts
import
render_to_string
from
lms.djangoapps.grades.scores
import
weighted_score
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.lang_pref
import
LANGUAGE_KEY
from
openedx.core.djangoapps.site_configuration
import
helpers
as
configuration_helpers
from
openedx.core.djangoapps.site_configuration
import
helpers
as
configuration_helpers
from
openedx.core.djangoapps.user_api.models
import
UserPreference
from
openedx.core.djangoapps.user_api.models
import
UserPreference
...
@@ -293,7 +293,7 @@ def _reset_module_attempts(studentmodule):
...
@@ -293,7 +293,7 @@ def _reset_module_attempts(studentmodule):
def
_fire_score_changed_for_block
(
course_id
,
student
,
block
,
module_state_key
):
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
always zero. We must retrieve the possible points from the XModule, as
noted below.
noted below.
"""
"""
...
@@ -322,7 +322,7 @@ def _fire_score_changed_for_block(course_id, student, block, module_state_key):
...
@@ -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
))
points_earned
,
points_possible
=
weighted_score
(
0
,
max_score
,
getattr
(
module
,
'weight'
,
None
))
else
:
else
:
points_earned
,
points_possible
=
0
,
0
points_earned
,
points_possible
=
0
,
0
SCORE_CHANGED
.
send
(
PROBLEM_
SCORE_CHANGED
.
send
(
sender
=
None
,
sender
=
None
,
points_possible
=
points_possible
,
points_possible
=
points_possible
,
points_earned
=
points_earned
,
points_earned
=
points_earned
,
...
...
lms/djangoapps/instructor/tests/test_api.py
View file @
a1499e28
...
@@ -3190,7 +3190,7 @@ class TestInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginEnrollmentTes
...
@@ -3190,7 +3190,7 @@ class TestInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginEnrollmentTes
})
})
self
.
assertEqual
(
response
.
status_code
,
400
)
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
):
def
test_reset_student_attempts_delete
(
self
,
_mock_signal
):
""" Test delete single student state. """
""" Test delete single student state. """
url
=
reverse
(
'reset_student_attempts'
,
kwargs
=
{
'course_id'
:
self
.
course
.
id
.
to_deprecated_string
()})
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):
...
@@ -378,7 +378,7 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase):
reset_student_attempts
(
self
.
course_key
,
self
.
user
,
msk
,
requesting_user
=
self
.
user
)
reset_student_attempts
(
self
.
course_key
,
self
.
user
,
msk
,
requesting_user
=
self
.
user
)
self
.
assertEqual
(
json
.
loads
(
module
()
.
state
)[
'attempts'
],
0
)
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
):
def
test_delete_student_attempts
(
self
,
_mock_signal
):
msk
=
self
.
course_key
.
make_usage_key
(
'dummy'
,
'module'
)
msk
=
self
.
course_key
.
make_usage_key
(
'dummy'
,
'module'
)
original_state
=
json
.
dumps
({
'attempts'
:
32
,
'otherstuff'
:
'alsorobots'
})
original_state
=
json
.
dumps
({
'attempts'
:
32
,
'otherstuff'
:
'alsorobots'
})
...
@@ -404,7 +404,7 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase):
...
@@ -404,7 +404,7 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase):
# Disable the score change signal to prevent other components from being
# Disable the score change signal to prevent other components from being
# pulled into tests.
# 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
):
def
test_delete_submission_scores
(
self
,
_mock_signal
):
user
=
UserFactory
()
user
=
UserFactory
()
problem_location
=
self
.
course_key
.
make_usage_key
(
'dummy'
,
'module'
)
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):
...
@@ -49,7 +49,7 @@ class InstructorServiceTests(SharedModuleStoreTestCase):
state
=
json
.
dumps
({
'attempts'
:
2
}),
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
):
def
test_reset_student_attempts_delete
(
self
,
_mock_signal
):
"""
"""
Test delete student state.
Test delete student state.
...
...
lms/djangoapps/lti_provider/tasks.py
View file @
a1499e28
...
@@ -8,7 +8,7 @@ from django.dispatch import receiver
...
@@ -8,7 +8,7 @@ from django.dispatch import receiver
import
logging
import
logging
from
lms.djangoapps.grades
import
progress
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
lms
import
CELERY_APP
from
lti_provider.models
import
GradedAssignment
from
lti_provider.models
import
GradedAssignment
import
lti_provider.outcomes
as
outcomes
import
lti_provider.outcomes
as
outcomes
...
@@ -19,11 +19,11 @@ from xmodule.modulestore.django import modulestore
...
@@ -19,11 +19,11 @@ from xmodule.modulestore.django import modulestore
log
=
logging
.
getLogger
(
"edx.lti_provider"
)
log
=
logging
.
getLogger
(
"edx.lti_provider"
)
@receiver
(
SCORE_CHANGED
)
@receiver
(
PROBLEM_
SCORE_CHANGED
)
def
score_changed_handler
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
def
score_changed_handler
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
"""
"""
Consume signals that indicate score changes. See the definition of
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_possible
=
kwargs
.
get
(
'points_possible'
,
None
)
points_earned
=
kwargs
.
get
(
'points_earned'
,
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