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
e4a9bef8
Commit
e4a9bef8
authored
7 years ago
by
Tyler Hallada
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Trigger recalculate subsection, undo override
parent
cf39bef7
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
215 additions
and
42 deletions
+215
-42
lms/djangoapps/grades/constants.py
+1
-0
lms/djangoapps/grades/models.py
+4
-0
lms/djangoapps/grades/services.py
+69
-18
lms/djangoapps/grades/tasks.py
+10
-2
lms/djangoapps/grades/tests/test_services.py
+97
-11
lms/djangoapps/grades/tests/test_tasks.py
+34
-11
No files found.
lms/djangoapps/grades/constants.py
View file @
e4a9bef8
...
...
@@ -9,3 +9,4 @@ class ScoreDatabaseTableEnum(object):
"""
courseware_student_module
=
'csm'
submissions
=
'submissions'
overrides
=
'overrides'
This diff is collapsed.
Click to expand it.
lms/djangoapps/grades/models.py
View file @
e4a9bef8
...
...
@@ -695,6 +695,10 @@ class PersistentSubsectionGradeOverride(models.Model):
grade
=
models
.
OneToOneField
(
PersistentSubsectionGrade
,
related_name
=
'override'
)
# Created/modified timestamps prevent race-conditions when using with async rescoring tasks
created
=
models
.
DateTimeField
(
auto_now_add
=
True
,
db_index
=
True
)
modified
=
models
.
DateTimeField
(
auto_now
=
True
,
db_index
=
True
)
# earned/possible refers to the number of points achieved and available to achieve.
# graded refers to the subset of all problems that are marked as being graded.
earned_all_override
=
models
.
FloatField
(
null
=
True
,
blank
=
True
)
...
...
This diff is collapsed.
Click to expand it.
lms/djangoapps/grades/services.py
View file @
e4a9bef8
from
datetime
import
datetime
import
pytz
from
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
from
lms.djangoapps.grades.models
import
PersistentSubsectionGrade
,
PersistentSubsectionGradeOverride
from
.constants
import
ScoreDatabaseTableEnum
from
.models
import
PersistentSubsectionGrade
,
PersistentSubsectionGradeOverride
def
_get_key
(
key_or_id
,
key_cls
):
...
...
@@ -24,48 +30,93 @@ class GradesService(object):
def
get_subsection_grade
(
self
,
user_id
,
course_key_or_id
,
usage_key_or_id
):
"""
Finds and returns the earned subsection grade for user
Result is a dict of two key value pairs with keys: earned_all and earned_graded.
"""
course_key
=
_get_key
(
course_key_or_id
,
CourseKey
)
usage_key
=
_get_key
(
usage_key_or_id
,
UsageKey
)
grade
=
PersistentSubsectionGrade
.
objects
.
get
(
return
PersistentSubsectionGrade
.
objects
.
get
(
user_id
=
user_id
,
course_id
=
course_key
,
usage_key
=
usage_key
)
return
{
'earned_all'
:
grade
.
earned_all
,
'earned_graded'
:
grade
.
earned_graded
}
def
get_subsection_grade_override
(
self
,
user_id
,
course_key_or_id
,
usage_key_or_id
):
"""
Finds the subsection grade for user and returns the override for that grade if it exists
If override does not exist, returns None. If subsection grade does not exist, will raise an exception.
"""
course_key
=
_get_key
(
course_key_or_id
,
CourseKey
)
usage_key
=
_get_key
(
usage_key_or_id
,
UsageKey
)
grade
=
self
.
get_subsection_grade
(
user_id
,
course_key
,
usage_key
)
try
:
return
PersistentSubsectionGradeOverride
.
objects
.
get
(
grade
=
grade
)
except
PersistentSubsectionGradeOverride
.
DoesNotExist
:
return
None
def
override_subsection_grade
(
self
,
user_id
,
course_key_or_id
,
usage_key_or_id
,
earned_all
=
None
,
earned_graded
=
None
):
"""
Override subsection grade (the PersistentSubsectionGrade model must already exist)
Will not override earned_all or earned_graded value if they are None. Both default to None.
Fires off a recalculate_subsection_grade async task to update the PersistentSubsectionGrade table. Will not
override earned_all or earned_graded value if they are None. Both default to None.
"""
from
.tasks
import
recalculate_subsection_grade_v3
# prevent circular import
course_key
=
_get_key
(
course_key_or_id
,
CourseKey
)
subsection
_key
=
_get_key
(
usage_key_or_id
,
UsageKey
)
usage
_key
=
_get_key
(
usage_key_or_id
,
UsageKey
)
grade
=
PersistentSubsectionGrade
.
objects
.
get
(
user_id
=
user_id
,
course_id
=
course_key
,
usage_key
=
subsection
_key
usage_key
=
usage
_key
)
# Create override that will prevent any future updates to grade
PersistentSubsectionGradeOverride
.
objects
.
create
(
override
,
_
=
PersistentSubsectionGradeOverride
.
objects
.
update_or_
create
(
grade
=
grade
,
earned_all_override
=
earned_all
,
earned_graded_override
=
earned_graded
)
# Change the grade as it is now
if
earned_all
is
not
None
:
grade
.
earned_all
=
earned_all
if
earned_graded
is
not
None
:
grade
.
earned_graded
=
earned_graded
grade
.
save
()
# Recalculation will call PersistentSubsectionGrade.update_or_create_grade which will use the above override
# to update the grade before writing to the table.
recalculate_subsection_grade_v3
.
apply_async
(
sender
=
None
,
user_id
=
user_id
,
course_id
=
unicode
(
course_key
),
usage_id
=
unicode
(
usage_key
),
only_if_higher
=
False
,
expeected_modified
=
override
.
modified
,
score_db_table
=
ScoreDatabaseTableEnum
.
overrides
)
def
undo_override_subsection_grade
(
self
,
user_id
,
course_key_or_id
,
usage_key_or_id
):
"""
Delete the override subsection grade row (the PersistentSubsectionGrade model must already exist)
Fires off a recalculate_subsection_grade async task to update the PersistentSubsectionGrade table.
"""
from
.tasks
import
recalculate_subsection_grade_v3
# prevent circular import
course_key
=
_get_key
(
course_key_or_id
,
CourseKey
)
usage_key
=
_get_key
(
usage_key_or_id
,
UsageKey
)
override
=
self
.
get_subsection_grade_override
(
user_id
,
course_key
,
usage_key
)
override
.
delete
()
recalculate_subsection_grade_v3
.
apply_async
(
sender
=
None
,
user_id
=
user_id
,
course_id
=
unicode
(
course_key
),
usage_id
=
unicode
(
usage_key
),
only_if_higher
=
False
,
expected_modified
=
datetime
.
now
()
.
replace
(
tzinfo
=
pytz
.
UTC
),
# Not used when score_deleted=True
score_deleted
=
True
,
score_db_table
=
ScoreDatabaseTableEnum
.
overrides
)
This diff is collapsed.
Click to expand it.
lms/djangoapps/grades/tasks.py
View file @
e4a9bef8
...
...
@@ -31,6 +31,7 @@ from .constants import ScoreDatabaseTableEnum
from
.exceptions
import
DatabaseNotReadyError
from
.new.course_grade_factory
import
CourseGradeFactory
from
.new.subsection_grade_factory
import
SubsectionGradeFactory
from
.services
import
GradesService
from
.signals.signals
import
SUBSECTION_SCORE_CHANGED
from
.transformer
import
GradesTransformer
...
...
@@ -201,8 +202,7 @@ def _has_db_updated_with_new_score(self, scored_block_usage_key, **kwargs):
score
=
get_score
(
kwargs
[
'user_id'
],
scored_block_usage_key
)
found_modified_time
=
score
.
modified
if
score
is
not
None
else
None
else
:
assert
kwargs
[
'score_db_table'
]
==
ScoreDatabaseTableEnum
.
submissions
elif
kwargs
[
'score_db_table'
]
==
ScoreDatabaseTableEnum
.
submissions
:
score
=
sub_api
.
get_score
(
{
"student_id"
:
kwargs
[
'anonymous_user_id'
],
...
...
@@ -212,6 +212,14 @@ def _has_db_updated_with_new_score(self, scored_block_usage_key, **kwargs):
}
)
found_modified_time
=
score
[
'created_at'
]
if
score
is
not
None
else
None
else
:
assert
kwargs
[
'score_db_table'
]
==
ScoreDatabaseTableEnum
.
overrides
score
=
GradesService
()
.
get_subsection_grade_override
(
user_id
=
kwargs
[
'user_id'
],
course_key_or_id
=
kwargs
[
'course_id'
],
usage_key_or_id
=
kwargs
[
'usage_id'
]
)
found_modified_time
=
score
.
modified
if
score
is
not
None
else
None
if
score
is
None
:
# score should be None only if it was deleted.
...
...
This diff is collapsed.
Click to expand it.
lms/djangoapps/grades/tests/test_services.py
View file @
e4a9bef8
import
ddt
import
pytz
from
datetime
import
datetime
from
freezegun
import
freeze_time
from
lms.djangoapps.grades.models
import
PersistentSubsectionGrade
,
PersistentSubsectionGradeOverride
from
lms.djangoapps.grades.services
import
GradesService
,
_get_key
from
mock
import
patch
from
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
from
student.tests.factories
import
UserFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
from
..constants
import
ScoreDatabaseTableEnum
@ddt.ddt
class
GradesServiceTests
(
ModuleStoreTestCase
):
...
...
@@ -29,27 +35,73 @@ class GradesServiceTests(ModuleStoreTestCase):
earned_graded
=
5.0
,
possible_graded
=
5.0
)
self
.
patcher
=
patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v3.apply_async'
)
self
.
mock_recalculate
=
self
.
patcher
.
start
()
def
tearDown
(
self
):
self
.
patcher
.
stop
()
def
subsection_grade_to_dict
(
self
,
grade
):
return
{
'earned_all'
:
grade
.
earned_all
,
'earned_graded'
:
grade
.
earned_graded
}
def
subsection_grade_override_to_dict
(
self
,
grade
):
return
{
'earned_all_override'
:
grade
.
earned_all_override
,
'earned_graded_override'
:
grade
.
earned_graded_override
}
def
test_get_subsection_grade
(
self
):
self
.
assertDictEqual
(
self
.
service
.
get_subsection_grade
(
self
.
assertDictEqual
(
self
.
s
ubsection_grade_to_dict
(
self
.
s
ervice
.
get_subsection_grade
(
user_id
=
self
.
user
.
id
,
course_key_or_id
=
self
.
course
.
id
,
usage_key_or_id
=
self
.
subsection
.
location
),
{
)
)
,
{
'earned_all'
:
6.0
,
'earned_graded'
:
5.0
})
# test with id strings as parameters instead
self
.
assertDictEqual
(
self
.
service
.
get_subsection_grade
(
self
.
assertDictEqual
(
self
.
s
ubsection_grade_to_dict
(
self
.
s
ervice
.
get_subsection_grade
(
user_id
=
self
.
user
.
id
,
course_key_or_id
=
str
(
self
.
course
.
id
),
usage_key_or_id
=
str
(
self
.
subsection
.
location
)
),
{
course_key_or_id
=
unicode
(
self
.
course
.
id
),
usage_key_or_id
=
unicode
(
self
.
subsection
.
location
)
)
)
,
{
'earned_all'
:
6.0
,
'earned_graded'
:
5.0
})
def
test_get_subsection_grade_override
(
self
):
override
,
_
=
PersistentSubsectionGradeOverride
.
objects
.
update_or_create
(
grade
=
self
.
grade
)
self
.
assertDictEqual
(
self
.
subsection_grade_override_to_dict
(
self
.
service
.
get_subsection_grade_override
(
user_id
=
self
.
user
.
id
,
course_key_or_id
=
self
.
course
.
id
,
usage_key_or_id
=
self
.
subsection
.
location
)),
{
'earned_all_override'
:
override
.
earned_all_override
,
'earned_graded_override'
:
override
.
earned_graded_override
})
override
,
_
=
PersistentSubsectionGradeOverride
.
objects
.
update_or_create
(
grade
=
self
.
grade
,
defaults
=
{
'earned_all_override'
:
9.0
}
)
# test with id strings as parameters instead
self
.
assertDictEqual
(
self
.
subsection_grade_override_to_dict
(
self
.
service
.
get_subsection_grade_override
(
user_id
=
self
.
user
.
id
,
course_key_or_id
=
unicode
(
self
.
course
.
id
),
usage_key_or_id
=
unicode
(
self
.
subsection
.
location
)
)),
{
'earned_all_override'
:
override
.
earned_all_override
,
'earned_graded_override'
:
override
.
earned_graded_override
})
@ddt.data
(
[{
'earned_all'
:
0.0
,
...
...
@@ -92,14 +144,48 @@ class GradesServiceTests(ModuleStoreTestCase):
earned_graded
=
override
[
'earned_graded'
]
)
grade
=
PersistentSubsectionGrade
.
objects
.
get
(
override_obj
=
self
.
service
.
get_subsection_grade_override
(
self
.
user
.
id
,
self
.
course
.
id
,
self
.
subsection
.
location
)
self
.
assertIsNotNone
(
override_obj
)
self
.
assertEqual
(
override_obj
.
earned_all_override
,
override
[
'earned_all'
])
self
.
assertEqual
(
override_obj
.
earned_graded_override
,
override
[
'earned_graded'
])
self
.
mock_recalculate
.
called_with
(
sender
=
None
,
user_id
=
self
.
user
.
id
,
course_id
=
self
.
course
.
id
,
usage_key
=
self
.
subsection
.
location
course_id
=
unicode
(
self
.
course
.
id
),
usage_id
=
unicode
(
self
.
subsection
.
location
),
only_if_higher
=
False
,
expected_modified
=
override_obj
.
modified
,
score_db_table
=
ScoreDatabaseTableEnum
.
overrides
)
@freeze_time
(
'2017-01-01'
)
def
test_undo_override_subsection_grade
(
self
):
override
,
_
=
PersistentSubsectionGradeOverride
.
objects
.
update_or_create
(
grade
=
self
.
grade
)
self
.
service
.
undo_override_subsection_grade
(
user_id
=
self
.
user
.
id
,
course_key_or_id
=
self
.
course
.
id
,
usage_key_or_id
=
self
.
subsection
.
location
,
)
self
.
assertEqual
(
grade
.
earned_all
,
expected
[
'earned_all'
])
self
.
assertEqual
(
grade
.
earned_graded
,
expected
[
'earned_graded'
])
override
=
self
.
service
.
get_subsection_grade_override
(
self
.
user
.
id
,
self
.
course
.
id
,
self
.
subsection
.
location
)
self
.
assertIsNone
(
override
)
self
.
mock_recalculate
.
called_with
(
sender
=
None
,
user_id
=
self
.
user
.
id
,
course_id
=
unicode
(
self
.
course
.
id
),
usage_id
=
unicode
(
self
.
subsection
.
location
),
only_if_higher
=
False
,
expected_modified
=
datetime
.
now
()
.
replace
(
tzinfo
=
pytz
.
UTC
),
score_deleted
=
True
,
score_db_table
=
ScoreDatabaseTableEnum
.
overrides
)
@ddt.data
(
[
'edX/DemoX/Demo_Course'
,
CourseKey
.
from_string
(
'edX/DemoX/Demo_Course'
),
CourseKey
],
...
...
This diff is collapsed.
Click to expand it.
lms/djangoapps/grades/tests/test_tasks.py
View file @
e4a9bef8
...
...
@@ -17,6 +17,7 @@ from mock import MagicMock, patch
from
lms.djangoapps.grades.config.models
import
PersistentGradesEnabledFlag
from
lms.djangoapps.grades.constants
import
ScoreDatabaseTableEnum
from
lms.djangoapps.grades.models
import
PersistentCourseGrade
,
PersistentSubsectionGrade
from
lms.djangoapps.grades.services
import
GradesService
from
lms.djangoapps.grades.signals.signals
import
PROBLEM_WEIGHTED_SCORE_CHANGED
from
lms.djangoapps.grades.tasks
import
(
RECALCULATE_GRADE_DELAY
,
...
...
@@ -36,6 +37,15 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
,
check_mongo_calls
class
MockGradesService
(
GradesService
):
def
__init__
(
self
,
mocked_return_value
=
None
):
super
(
MockGradesService
,
self
)
.
__init__
()
self
.
mocked_return_value
=
mocked_return_value
def
get_subsection_grade_override
(
self
,
user_id
,
course_key_or_id
,
usage_key_or_id
):
return
self
.
mocked_return_value
class
HasCourseWithProblemsMixin
(
object
):
"""
Mixin to provide tests with a sample course with graded subsections
...
...
@@ -153,10 +163,10 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self
.
assertEquals
(
mock_block_structure_create
.
call_count
,
1
)
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
8
,
True
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
4
,
False
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
8
,
True
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
4
,
False
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
9
,
True
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
5
,
False
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
9
,
True
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
5
,
False
),
)
@ddt.unpack
def
test_query_counts
(
self
,
default_store
,
num_mongo_calls
,
num_sql_calls
,
create_multiple_subsections
):
...
...
@@ -168,8 +178,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self
.
_apply_recalculate_subsection_grade
()
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
8
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
8
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
9
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
9
),
)
@ddt.unpack
def
test_query_counts_dont_change_with_more_content
(
self
,
default_store
,
num_mongo_calls
,
num_sql_calls
):
...
...
@@ -229,8 +239,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self
.
assertEqual
(
len
(
PersistentSubsectionGrade
.
bulk_read_grades
(
self
.
user
.
id
,
self
.
course
.
id
)),
0
)
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
5
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
5
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
6
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
6
),
)
@ddt.unpack
def
test_persistent_grades_enabled_on_course
(
self
,
default_store
,
num_mongo_queries
,
num_sql_queries
):
...
...
@@ -264,7 +274,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self
.
_apply_recalculate_subsection_grade
()
self
.
_assert_retry_called
(
mock_retry
)
@ddt.data
(
ScoreDatabaseTableEnum
.
courseware_student_module
,
ScoreDatabaseTableEnum
.
submissions
)
@ddt.data
(
ScoreDatabaseTableEnum
.
courseware_student_module
,
ScoreDatabaseTableEnum
.
submissions
,
ScoreDatabaseTableEnum
.
overrides
)
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v3.retry'
)
@patch
(
'lms.djangoapps.grades.tasks.log'
)
def
test_retry_when_db_not_updated
(
self
,
score_db_table
,
mock_log
,
mock_retry
):
...
...
@@ -279,10 +290,16 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self
.
_apply_recalculate_subsection_grade
(
mock_score
=
MagicMock
(
module_type
=
'any_block_type'
)
)
el
s
e
:
el
if
score_db_table
==
ScoreDatabaseTableEnum
.
courseware_student_modul
e
:
self
.
_apply_recalculate_subsection_grade
(
mock_score
=
MagicMock
(
modified
=
modified_datetime
)
)
else
:
with
patch
(
'lms.djangoapps.grades.tasks.GradesService'
,
return_value
=
MockGradesService
(
mocked_return_value
=
MagicMock
(
modified
=
modified_datetime
))
):
recalculate_subsection_grade_v3
.
apply
(
kwargs
=
self
.
recalculate_subsection_grade_kwargs
)
self
.
_assert_retry_called
(
mock_retry
)
self
.
assertIn
(
...
...
@@ -293,7 +310,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
@ddt.data
(
*
itertools
.
product
(
(
True
,
False
),
(
ScoreDatabaseTableEnum
.
courseware_student_module
,
ScoreDatabaseTableEnum
.
submissions
),
(
ScoreDatabaseTableEnum
.
courseware_student_module
,
ScoreDatabaseTableEnum
.
submissions
,
ScoreDatabaseTableEnum
.
overrides
),
)
)
@ddt.unpack
...
...
@@ -310,6 +328,11 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self
.
_apply_recalculate_subsection_grade
(
mock_score
=
MagicMock
(
module_type
=
'any_block_type'
)
)
elif
score_db_table
==
ScoreDatabaseTableEnum
.
overrides
:
with
patch
(
'lms.djangoapps.grades.tasks.GradesService'
,
return_value
=
MockGradesService
(
mocked_return_value
=
None
))
as
mock_service
:
mock_service
.
get_subsection_grade_override
.
return_value
=
None
recalculate_subsection_grade_v3
.
apply
(
kwargs
=
self
.
recalculate_subsection_grade_kwargs
)
else
:
self
.
_apply_recalculate_subsection_grade
(
mock_score
=
None
)
...
...
This diff is collapsed.
Click to expand it.
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