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
f8e3a3c4
Commit
f8e3a3c4
authored
Oct 02, 2017
by
Nimisha Asthagiri
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Grades Cleanup
EDUCATOR-1405 EDUCATOR-1451
parent
0de55de2
Hide whitespace changes
Inline
Side-by-side
Showing
22 changed files
with
490 additions
and
623 deletions
+490
-623
lms/djangoapps/grades/config/waffle.py
+0
-1
lms/djangoapps/grades/course_grade.py
+10
-5
lms/djangoapps/grades/course_grade_factory.py
+10
-18
lms/djangoapps/grades/events.py
+134
-0
lms/djangoapps/grades/management/commands/recalculate_subsection_grades.py
+1
-1
lms/djangoapps/grades/models.py
+95
-148
lms/djangoapps/grades/services.py
+1
-7
lms/djangoapps/grades/signals/handlers.py
+6
-81
lms/djangoapps/grades/subsection_grade.py
+38
-37
lms/djangoapps/grades/subsection_grade_factory.py
+11
-11
lms/djangoapps/grades/tasks.py
+1
-12
lms/djangoapps/grades/tests/base.py
+2
-2
lms/djangoapps/grades/tests/integration/test_events.py
+93
-113
lms/djangoapps/grades/tests/integration/test_problems.py
+2
-2
lms/djangoapps/grades/tests/test_course_grade_factory.py
+42
-105
lms/djangoapps/grades/tests/test_models.py
+21
-42
lms/djangoapps/grades/tests/test_services.py
+0
-1
lms/djangoapps/grades/tests/test_signals.py
+0
-5
lms/djangoapps/grades/tests/test_subsection_grade.py
+11
-13
lms/djangoapps/grades/tests/test_tasks.py
+8
-8
lms/djangoapps/instructor/enrollment.py
+2
-1
lms/djangoapps/instructor_task/tasks_helper/module_state.py
+2
-10
No files found.
lms/djangoapps/grades/config/waffle.py
View file @
f8e3a3c4
...
...
@@ -9,7 +9,6 @@ WAFFLE_NAMESPACE = u'grades'
# Switches
ASSUME_ZERO_GRADE_IF_ABSENT
=
u'assume_zero_grade_if_absent'
ESTIMATE_FIRST_ATTEMPTED
=
u'estimate_first_attempted'
DISABLE_REGRADE_ON_POLICY_CHANGE
=
u'disable_regrade_on_policy_change'
# Course Flags
...
...
lms/djangoapps/grades/course_grade.py
View file @
f8e3a3c4
...
...
@@ -14,10 +14,6 @@ from .subsection_grade import ZeroSubsectionGrade
from
.subsection_grade_factory
import
SubsectionGradeFactory
def
uniqueify
(
iterable
):
return
OrderedDict
([(
item
,
None
)
for
item
in
iterable
])
.
keys
()
class
CourseGradeBase
(
object
):
"""
Base class for Course Grades.
...
...
@@ -194,7 +190,7 @@ class CourseGradeBase(object):
"""
return
[
self
.
_get_subsection_grade
(
course_structure
[
subsection_key
])
for
subsection_key
in
uniqueify
(
course_structure
.
get_children
(
chapter_key
))
for
subsection_key
in
_uniqueify_and_keep_order
(
course_structure
.
get_children
(
chapter_key
))
]
@abstractmethod
...
...
@@ -229,6 +225,11 @@ class CourseGrade(CourseGradeBase):
if self.force_update_subsections is true, via the lazy call
to self.grader_result.
"""
# TODO update this code to be more functional and readable.
# Currently, it is hard to follow since there are plenty of
# side-effects. Once functional, force_update_subsections
# can be passed through and not confusingly stored and used
# at a later time.
grade_cutoffs
=
self
.
course_data
.
course
.
grade_cutoffs
self
.
percent
=
self
.
_compute_percent
(
self
.
grader_result
)
self
.
letter_grade
=
self
.
_compute_letter_grade
(
grade_cutoffs
,
self
.
percent
)
...
...
@@ -289,3 +290,7 @@ class CourseGrade(CourseGradeBase):
nonzero_cutoffs
=
[
cutoff
for
cutoff
in
grade_cutoffs
.
values
()
if
cutoff
>
0
]
success_cutoff
=
min
(
nonzero_cutoffs
)
if
nonzero_cutoffs
else
None
return
success_cutoff
and
percent
>=
success_cutoff
def
_uniqueify_and_keep_order
(
iterable
):
return
OrderedDict
([(
item
,
None
)
for
item
in
iterable
])
.
keys
()
lms/djangoapps/grades/course_grade_factory.py
View file @
f8e3a3c4
from
collections
import
namedtuple
from
contextlib
import
contextmanager
from
logging
import
getLogger
import
dogstats_wrapper
as
dog_stats_api
...
...
@@ -9,7 +8,7 @@ from openedx.core.djangoapps.signals.signals import COURSE_GRADE_CHANGED, COURSE
from
.config
import
assume_zero_if_absent
,
should_persist_grades
from
.course_data
import
CourseData
from
.course_grade
import
CourseGrade
,
ZeroCourseGrade
from
.models
import
PersistentCourseGrade
,
VisibleBlocks
from
.models
import
PersistentCourseGrade
,
prefetch
log
=
getLogger
(
__name__
)
...
...
@@ -95,18 +94,9 @@ class CourseGradeFactory(object):
user
=
None
,
course
=
course
,
collected_block_structure
=
collected_block_structure
,
course_key
=
course_key
,
)
stats_tags
=
[
u'action:{}'
.
format
(
course_data
.
course_key
)]
with
self
.
_course_transaction
(
course_data
.
course_key
):
for
user
in
users
:
with
dog_stats_api
.
timer
(
'lms.grades.CourseGradeFactory.iter'
,
tags
=
stats_tags
):
yield
self
.
_iter_grade_result
(
user
,
course_data
,
force_update
)
@contextmanager
def
_course_transaction
(
self
,
course_key
):
"""
Provides a transaction context in which GradeResults are created.
"""
yield
VisibleBlocks
.
clear_cache
(
course_key
)
for
user
in
users
:
with
dog_stats_api
.
timer
(
'lms.grades.CourseGradeFactory.iter'
,
tags
=
stats_tags
):
yield
self
.
_iter_grade_result
(
user
,
course_data
,
force_update
)
def
_iter_grade_result
(
self
,
user
,
course_data
,
force_update
):
try
:
...
...
@@ -169,13 +159,15 @@ class CourseGradeFactory(object):
Sends a COURSE_GRADE_CHANGED signal to listeners and a
COURSE_GRADE_NOW_PASSED if learner has passed course.
"""
should_persist
=
should_persist_grades
(
course_data
.
course_key
)
if
should_persist
and
force_update_subsections
:
prefetch
(
user
,
course_data
.
course_key
)
course_grade
=
CourseGrade
(
user
,
course_data
,
force_update_subsections
=
force_update_subsections
)
course_grade
=
course_grade
.
update
()
should_persist
=
(
should_persist_grades
(
course_data
.
course_key
)
and
course_grade
.
attempted
)
should_persist
=
should_persist
and
course_grade
.
attempted
if
should_persist
:
course_grade
.
_subsection_grade_factory
.
bulk_create_unsaved
()
PersistentCourseGrade
.
update_or_create
(
...
...
lms/djangoapps/grades/events.py
0 → 100644
View file @
f8e3a3c4
from
crum
import
get_current_user
from
eventtracking
import
tracker
from
track
import
contexts
from
track.event_transaction_utils
import
(
create_new_event_transaction_id
,
get_event_transaction_id
,
get_event_transaction_type
,
set_event_transaction_type
)
COURSE_GRADE_CALCULATED
=
u'edx.grades.course.grade_calculated'
GRADES_OVERRIDE_EVENT_TYPE
=
u'edx.grades.problem.score_overridden'
GRADES_RESCORE_EVENT_TYPE
=
u'edx.grades.problem.rescored'
PROBLEM_SUBMITTED_EVENT_TYPE
=
u'edx.grades.problem.submitted'
STATE_DELETED_EVENT_TYPE
=
u'edx.grades.problem.state_deleted'
SUBSECTION_OVERRIDE_EVENT_TYPE
=
u'edx.grades.subsection.score_overridden'
SUBSECTION_GRADE_CALCULATED
=
u'edx.grades.subsection.grade_calculated'
def
grade_updated
(
**
kwargs
):
"""
Emits the appropriate grade-related event after checking for which
event-transaction is active.
Emits a problem.submitted event only if there is no current event
transaction type, i.e. we have not reached this point in the code via
an outer event type (such as problem.rescored or score_overridden).
"""
root_type
=
get_event_transaction_type
()
if
not
root_type
:
root_id
=
get_event_transaction_id
()
if
not
root_id
:
root_id
=
create_new_event_transaction_id
()
set_event_transaction_type
(
PROBLEM_SUBMITTED_EVENT_TYPE
)
tracker
.
emit
(
unicode
(
PROBLEM_SUBMITTED_EVENT_TYPE
),
{
'user_id'
:
unicode
(
kwargs
[
'user_id'
]),
'course_id'
:
unicode
(
kwargs
[
'course_id'
]),
'problem_id'
:
unicode
(
kwargs
[
'usage_id'
]),
'event_transaction_id'
:
unicode
(
root_id
),
'event_transaction_type'
:
unicode
(
PROBLEM_SUBMITTED_EVENT_TYPE
),
'weighted_earned'
:
kwargs
.
get
(
'weighted_earned'
),
'weighted_possible'
:
kwargs
.
get
(
'weighted_possible'
),
}
)
elif
root_type
in
[
GRADES_RESCORE_EVENT_TYPE
,
GRADES_OVERRIDE_EVENT_TYPE
]:
current_user
=
get_current_user
()
instructor_id
=
getattr
(
current_user
,
'id'
,
None
)
tracker
.
emit
(
unicode
(
root_type
),
{
'course_id'
:
unicode
(
kwargs
[
'course_id'
]),
'user_id'
:
unicode
(
kwargs
[
'user_id'
]),
'problem_id'
:
unicode
(
kwargs
[
'usage_id'
]),
'new_weighted_earned'
:
kwargs
.
get
(
'weighted_earned'
),
'new_weighted_possible'
:
kwargs
.
get
(
'weighted_possible'
),
'only_if_higher'
:
kwargs
.
get
(
'only_if_higher'
),
'instructor_id'
:
unicode
(
instructor_id
),
'event_transaction_id'
:
unicode
(
get_event_transaction_id
()),
'event_transaction_type'
:
unicode
(
root_type
),
}
)
elif
root_type
in
[
SUBSECTION_OVERRIDE_EVENT_TYPE
]:
tracker
.
emit
(
unicode
(
root_type
),
{
'course_id'
:
unicode
(
kwargs
[
'course_id'
]),
'user_id'
:
unicode
(
kwargs
[
'user_id'
]),
'problem_id'
:
unicode
(
kwargs
[
'usage_id'
]),
'only_if_higher'
:
kwargs
.
get
(
'only_if_higher'
),
'override_deleted'
:
kwargs
.
get
(
'score_deleted'
,
False
),
'event_transaction_id'
:
unicode
(
get_event_transaction_id
()),
'event_transaction_type'
:
unicode
(
root_type
),
}
)
def
subsection_grade_calculated
(
subsection_grade
):
"""
Emits an edx.grades.subsection.grade_calculated event
with data from the passed subsection_grade.
"""
event_name
=
SUBSECTION_GRADE_CALCULATED
context
=
contexts
.
course_context_from_course_id
(
subsection_grade
.
course_id
)
# TODO (AN-6134): remove this context manager
with
tracker
.
get_tracker
()
.
context
(
event_name
,
context
):
tracker
.
emit
(
event_name
,
{
'user_id'
:
unicode
(
subsection_grade
.
user_id
),
'course_id'
:
unicode
(
subsection_grade
.
course_id
),
'block_id'
:
unicode
(
subsection_grade
.
usage_key
),
'course_version'
:
unicode
(
subsection_grade
.
course_version
),
'weighted_total_earned'
:
subsection_grade
.
earned_all
,
'weighted_total_possible'
:
subsection_grade
.
possible_all
,
'weighted_graded_earned'
:
subsection_grade
.
earned_graded
,
'weighted_graded_possible'
:
subsection_grade
.
possible_graded
,
'first_attempted'
:
unicode
(
subsection_grade
.
first_attempted
),
'subtree_edited_timestamp'
:
unicode
(
subsection_grade
.
subtree_edited_timestamp
),
'event_transaction_id'
:
unicode
(
get_event_transaction_id
()),
'event_transaction_type'
:
unicode
(
get_event_transaction_type
()),
'visible_blocks_hash'
:
unicode
(
subsection_grade
.
visible_blocks_id
),
}
)
def
course_grade_calculated
(
course_grade
):
"""
Emits an edx.grades.course.grade_calculated event
with data from the passed course_grade.
"""
event_name
=
COURSE_GRADE_CALCULATED
context
=
contexts
.
course_context_from_course_id
(
course_grade
.
course_id
)
# TODO (AN-6134): remove this context manager
with
tracker
.
get_tracker
()
.
context
(
event_name
,
context
):
tracker
.
emit
(
event_name
,
{
'user_id'
:
unicode
(
course_grade
.
user_id
),
'course_id'
:
unicode
(
course_grade
.
course_id
),
'course_version'
:
unicode
(
course_grade
.
course_version
),
'percent_grade'
:
course_grade
.
percent_grade
,
'letter_grade'
:
unicode
(
course_grade
.
letter_grade
),
'course_edited_timestamp'
:
unicode
(
course_grade
.
course_edited_timestamp
),
'event_transaction_id'
:
unicode
(
get_event_transaction_id
()),
'event_transaction_type'
:
unicode
(
get_event_transaction_type
()),
'grading_policy_hash'
:
unicode
(
course_grade
.
grading_policy_hash
),
}
)
lms/djangoapps/grades/management/commands/recalculate_subsection_grades.py
View file @
f8e3a3c4
...
...
@@ -13,7 +13,7 @@ from pytz import utc
from
courseware.models
import
StudentModule
from
lms.djangoapps.grades.constants
import
ScoreDatabaseTableEnum
from
lms.djangoapps.grades.
signals.handler
s
import
PROBLEM_SUBMITTED_EVENT_TYPE
from
lms.djangoapps.grades.
event
s
import
PROBLEM_SUBMITTED_EVENT_TYPE
from
lms.djangoapps.grades.tasks
import
recalculate_subsection_grade_v3
from
student.models
import
user_by_anonymous_id
from
submissions.models
import
Submission
...
...
lms/djangoapps/grades/models.py
View file @
f8e3a3c4
...
...
@@ -21,13 +21,11 @@ from model_utils.models import TimeStampedModel
from
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
from
coursewarehistoryextended.fields
import
UnsignedBigIntAutoField
,
UnsignedBigIntOneToOneField
from
eventtracking
import
tracker
from
openedx.core.djangoapps.xmodule_django.models
import
CourseKeyField
,
UsageKeyField
from
request_cache
import
get_cache
from
track
import
contexts
from
track.event_transaction_utils
import
get_event_transaction_id
,
get_event_transaction_type
from
.config
import
waffle
import
events
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -122,24 +120,6 @@ class BlockRecordList(tuple):
return
cls
(
blocks
,
course_key
)
class
VisibleBlocksQuerySet
(
models
.
QuerySet
):
"""
A custom QuerySet representing VisibleBlocks.
"""
def
create_from_blockrecords
(
self
,
blocks
):
"""
Creates a new VisibleBlocks model object.
Argument 'blocks' should be a BlockRecordList.
"""
model
,
_
=
self
.
get_or_create
(
hashed
=
blocks
.
hash_value
,
defaults
=
{
u'blocks_json'
:
blocks
.
json_value
,
u'course_id'
:
blocks
.
course_key
},
)
return
model
class
VisibleBlocks
(
models
.
Model
):
"""
A django model used to track the state of a set of visible blocks under a
...
...
@@ -149,12 +129,11 @@ class VisibleBlocks(models.Model):
in the blocks_json field. A hash of this json array is used for lookup
purposes.
"""
CACHE_NAMESPACE
=
u"grades.models.VisibleBlocks"
blocks_json
=
models
.
TextField
()
hashed
=
models
.
CharField
(
max_length
=
100
,
unique
=
True
)
course_id
=
CourseKeyField
(
blank
=
False
,
max_length
=
255
,
db_index
=
True
)
objects
=
VisibleBlocksQuerySet
.
as_manager
()
_CACHE_NAMESPACE
=
u"grades.models.VisibleBlocks"
class
Meta
(
object
):
app_label
=
"grades"
...
...
@@ -184,12 +163,29 @@ class VisibleBlocks(models.Model):
Arguments:
course_key: The course identifier for the desired records
"""
prefetched
=
get_cache
(
cls
.
CACHE_NAMESPACE
)
.
get
(
cls
.
_cache_key
(
course_key
)
)
if
not
prefetched
:
prefetched
=
get_cache
(
cls
.
_CACHE_NAMESPACE
)
.
get
(
cls
.
_cache_key
(
course_key
),
None
)
if
prefetched
is
None
:
prefetched
=
cls
.
_initialize_cache
(
course_key
)
return
prefetched
@classmethod
def
cached_get_or_create
(
cls
,
blocks
):
prefetched
=
get_cache
(
cls
.
_CACHE_NAMESPACE
)
.
get
(
cls
.
_cache_key
(
blocks
.
course_key
))
if
prefetched
is
not
None
:
model
=
prefetched
.
get
(
blocks
.
hash_value
)
if
not
model
:
model
=
cls
.
objects
.
create
(
hashed
=
blocks
.
hash_value
,
blocks_json
=
blocks
.
json_value
,
course_id
=
blocks
.
course_key
,
)
cls
.
_update_cache
(
blocks
.
course_key
,
[
model
])
else
:
model
,
_
=
cls
.
objects
.
get_or_create
(
hashed
=
blocks
.
hash_value
,
defaults
=
{
u'blocks_json'
:
blocks
.
json_value
,
u'course_id'
:
blocks
.
course_key
},
)
return
model
@classmethod
def
bulk_create
(
cls
,
course_key
,
block_record_lists
):
"""
Bulk creates VisibleBlocks for the given iterator of
...
...
@@ -227,7 +223,7 @@ class VisibleBlocks(models.Model):
block record objects.
"""
prefetched
=
{
record
.
hashed
:
record
for
record
in
cls
.
objects
.
filter
(
course_id
=
course_key
)}
get_cache
(
cls
.
CACHE_NAMESPACE
)[
cls
.
_cache_key
(
course_key
)]
=
prefetched
get_cache
(
cls
.
_
CACHE_NAMESPACE
)[
cls
.
_cache_key
(
course_key
)]
=
prefetched
return
prefetched
@classmethod
...
...
@@ -236,19 +232,11 @@ class VisibleBlocks(models.Model):
Adds a specific set of visible blocks to the request cache.
This assumes that prefetch has already been called.
"""
get_cache
(
cls
.
CACHE_NAMESPACE
)[
cls
.
_cache_key
(
course_key
)]
.
update
(
get_cache
(
cls
.
_
CACHE_NAMESPACE
)[
cls
.
_cache_key
(
course_key
)]
.
update
(
{
visible_block
.
hashed
:
visible_block
for
visible_block
in
visible_blocks
}
)
@classmethod
def
clear_cache
(
cls
,
course_key
):
"""
Clears the cache of all contents for a given course.
"""
cache
=
get_cache
(
cls
.
CACHE_NAMESPACE
)
cache
.
pop
(
cls
.
_cache_key
(
course_key
),
None
)
@classmethod
def
_cache_key
(
cls
,
course_key
):
return
u"visible_blocks_cache.{}"
.
format
(
course_key
)
...
...
@@ -348,7 +336,7 @@ class PersistentSubsectionGrade(TimeStampedModel):
Raises PersistentSubsectionGrade.DoesNotExist if applicable
"""
return
cls
.
objects
.
select_related
(
'visible_blocks'
)
.
get
(
return
cls
.
objects
.
select_related
(
'visible_blocks'
,
'override'
)
.
get
(
user_id
=
user_id
,
course_id
=
usage_key
.
course_key
,
# course_id is included to take advantage of db indexes
usage_key
=
usage_key
,
...
...
@@ -363,7 +351,7 @@ class PersistentSubsectionGrade(TimeStampedModel):
user_id: The user associated with the desired grades
course_key: The course identifier for the desired grades
"""
return
cls
.
objects
.
select_related
(
'visible_blocks'
)
.
filter
(
return
cls
.
objects
.
select_related
(
'visible_blocks'
,
'override'
)
.
filter
(
user_id
=
user_id
,
course_id
=
course_key
,
)
...
...
@@ -373,30 +361,15 @@ class PersistentSubsectionGrade(TimeStampedModel):
"""
Wrapper for objects.update_or_create.
"""
cls
.
_prepare_params_and_visible_blocks
(
params
)
cls
.
_prepare_params
(
params
)
VisibleBlocks
.
cached_get_or_create
(
params
[
'visible_blocks'
])
cls
.
_prepare_params_visible_blocks_id
(
params
)
cls
.
_prepare_params_override
(
params
)
first_attempted
=
params
.
pop
(
'first_attempted'
)
user_id
=
params
.
pop
(
'user_id'
)
usage_key
=
params
.
pop
(
'usage_key'
)
# apply grade override if one exists before saving model
try
:
override
=
PersistentSubsectionGradeOverride
.
objects
.
get
(
grade__user_id
=
user_id
,
grade__course_id
=
usage_key
.
course_key
,
grade__usage_key
=
usage_key
,
)
if
override
.
earned_all_override
is
not
None
:
params
[
'earned_all'
]
=
override
.
earned_all_override
if
override
.
possible_all_override
is
not
None
:
params
[
'possible_all'
]
=
override
.
possible_all_override
if
override
.
earned_graded_override
is
not
None
:
params
[
'earned_graded'
]
=
override
.
earned_graded_override
if
override
.
possible_graded_override
is
not
None
:
params
[
'possible_graded'
]
=
override
.
possible_graded_override
except
PersistentSubsectionGradeOverride
.
DoesNotExist
:
pass
grade
,
_
=
cls
.
objects
.
update_or_create
(
user_id
=
user_id
,
course_id
=
usage_key
.
course_key
,
...
...
@@ -404,48 +377,27 @@ class PersistentSubsectionGrade(TimeStampedModel):
defaults
=
params
,
)
if
first_attempted
is
not
None
and
grade
.
first_attempted
is
None
:
if
waffle
.
waffle
()
.
is_enabled
(
waffle
.
ESTIMATE_FIRST_ATTEMPTED
):
grade
.
first_attempted
=
first_attempted
else
:
grade
.
first_attempted
=
now
()
grade
.
first_attempted
=
first_attempted
grade
.
save
()
cls
.
_emit_grade_calculated_event
(
grade
)
return
grade
@classmethod
def
_prepare_first_attempted_for_create
(
cls
,
params
):
"""
Update the value of 'first_attempted' to now() if we aren't
using score-based estimates.
"""
if
params
[
'first_attempted'
]
is
not
None
and
not
waffle
.
waffle
()
.
is_enabled
(
waffle
.
ESTIMATE_FIRST_ATTEMPTED
):
params
[
'first_attempted'
]
=
now
()
@classmethod
def
create_grade
(
cls
,
**
params
):
"""
Wrapper for objects.create.
"""
cls
.
_prepare_params_and_visible_blocks
(
params
)
cls
.
_prepare_first_attempted_for_create
(
params
)
grade
=
cls
.
objects
.
create
(
**
params
)
cls
.
_emit_grade_calculated_event
(
grade
)
return
grade
@classmethod
def
bulk_create_grades
(
cls
,
grade_params_iter
,
course_key
):
def
bulk_create_grades
(
cls
,
grade_params_iter
,
user_id
,
course_key
):
"""
Bulk creation of grades.
"""
if
not
grade_params_iter
:
return
PersistentSubsectionGradeOverride
.
prefetch
(
user_id
,
course_key
)
map
(
cls
.
_prepare_params
,
grade_params_iter
)
VisibleBlocks
.
bulk_get_or_create
([
params
[
'visible_blocks'
]
for
params
in
grade_params_iter
],
course_key
)
map
(
cls
.
_prepare_params_visible_blocks_id
,
grade_params_iter
)
map
(
cls
.
_prepare_first_attempted_for_create
,
grade_params_iter
)
map
(
cls
.
_prepare_params_override
,
grade_params_iter
)
grades
=
[
PersistentSubsectionGrade
(
**
params
)
for
params
in
grade_params_iter
]
grades
=
cls
.
objects
.
bulk_create
(
grades
)
for
grade
in
grades
:
...
...
@@ -453,15 +405,6 @@ class PersistentSubsectionGrade(TimeStampedModel):
return
grades
@classmethod
def
_prepare_params_and_visible_blocks
(
cls
,
params
):
"""
Prepares the fields for the grade record, while
creating the related VisibleBlocks, if needed.
"""
cls
.
_prepare_params
(
params
)
params
[
'visible_blocks'
]
=
VisibleBlocks
.
objects
.
create_from_blockrecords
(
params
[
'visible_blocks'
])
@classmethod
def
_prepare_params
(
cls
,
params
):
"""
Prepares the fields for the grade record.
...
...
@@ -484,34 +427,22 @@ class PersistentSubsectionGrade(TimeStampedModel):
params
[
'visible_blocks_id'
]
=
params
[
'visible_blocks'
]
.
hash_value
del
params
[
'visible_blocks'
]
@classmethod
def
_prepare_params_override
(
cls
,
params
):
override
=
PersistentSubsectionGradeOverride
.
get_override
(
params
[
'user_id'
],
params
[
'usage_key'
])
if
override
:
if
override
.
earned_all_override
is
not
None
:
params
[
'earned_all'
]
=
override
.
earned_all_override
if
override
.
possible_all_override
is
not
None
:
params
[
'possible_all'
]
=
override
.
possible_all_override
if
override
.
earned_graded_override
is
not
None
:
params
[
'earned_graded'
]
=
override
.
earned_graded_override
if
override
.
possible_graded_override
is
not
None
:
params
[
'possible_graded'
]
=
override
.
possible_graded_override
@staticmethod
def
_emit_grade_calculated_event
(
grade
):
"""
Emits an edx.grades.subsection.grade_calculated event
with data from the passed grade.
"""
# TODO: remove this context manager after completion of AN-6134
event_name
=
u'edx.grades.subsection.grade_calculated'
context
=
contexts
.
course_context_from_course_id
(
grade
.
course_id
)
with
tracker
.
get_tracker
()
.
context
(
event_name
,
context
):
tracker
.
emit
(
event_name
,
{
'user_id'
:
unicode
(
grade
.
user_id
),
'course_id'
:
unicode
(
grade
.
course_id
),
'block_id'
:
unicode
(
grade
.
usage_key
),
'course_version'
:
unicode
(
grade
.
course_version
),
'weighted_total_earned'
:
grade
.
earned_all
,
'weighted_total_possible'
:
grade
.
possible_all
,
'weighted_graded_earned'
:
grade
.
earned_graded
,
'weighted_graded_possible'
:
grade
.
possible_graded
,
'first_attempted'
:
unicode
(
grade
.
first_attempted
),
'subtree_edited_timestamp'
:
unicode
(
grade
.
subtree_edited_timestamp
),
'event_transaction_id'
:
unicode
(
get_event_transaction_id
()),
'event_transaction_type'
:
unicode
(
get_event_transaction_type
()),
'visible_blocks_hash'
:
unicode
(
grade
.
visible_blocks_id
),
}
)
events
.
subsection_grade_calculated
(
grade
)
class
PersistentCourseGrade
(
TimeStampedModel
):
...
...
@@ -553,7 +484,7 @@ class PersistentCourseGrade(TimeStampedModel):
# Information related to course completion
passed_timestamp
=
models
.
DateTimeField
(
u'Date learner earned a passing grade'
,
blank
=
True
,
null
=
True
)
CACHE_NAMESPACE
=
u"grades.models.PersistentCourseGrade"
_
CACHE_NAMESPACE
=
u"grades.models.PersistentCourseGrade"
def
__unicode__
(
self
):
"""
...
...
@@ -569,15 +500,11 @@ class PersistentCourseGrade(TimeStampedModel):
])
@classmethod
def
_cache_key
(
cls
,
course_id
):
return
u"grades_cache.{}"
.
format
(
course_id
)
@classmethod
def
prefetch
(
cls
,
course_id
,
users
):
"""
Prefetches grades for the given users for the given course.
"""
get_cache
(
cls
.
CACHE_NAMESPACE
)[
cls
.
_cache_key
(
course_id
)]
=
{
get_cache
(
cls
.
_
CACHE_NAMESPACE
)[
cls
.
_cache_key
(
course_id
)]
=
{
grade
.
user_id
:
grade
for
grade
in
cls
.
objects
.
filter
(
user_id__in
=
[
user
.
id
for
user
in
users
],
course_id
=
course_id
)
...
...
@@ -595,7 +522,7 @@ class PersistentCourseGrade(TimeStampedModel):
Raises PersistentCourseGrade.DoesNotExist if applicable
"""
try
:
prefetched_grades
=
get_cache
(
cls
.
CACHE_NAMESPACE
)[
cls
.
_cache_key
(
course_id
)]
prefetched_grades
=
get_cache
(
cls
.
_
CACHE_NAMESPACE
)[
cls
.
_cache_key
(
course_id
)]
try
:
return
prefetched_grades
[
user_id
]
except
KeyError
:
...
...
@@ -625,33 +552,24 @@ class PersistentCourseGrade(TimeStampedModel):
if
passed
and
not
grade
.
passed_timestamp
:
grade
.
passed_timestamp
=
now
()
grade
.
save
()
cls
.
_emit_grade_calculated_event
(
grade
)
cls
.
_update_cache
(
course_id
,
user_id
,
grade
)
return
grade
@classmethod
def
_update_cache
(
cls
,
course_id
,
user_id
,
grade
):
course_cache
=
get_cache
(
cls
.
_CACHE_NAMESPACE
)
.
get
(
cls
.
_cache_key
(
course_id
))
if
course_cache
is
not
None
:
course_cache
[
user_id
]
=
grade
@classmethod
def
_cache_key
(
cls
,
course_id
):
return
u"grades_cache.{}"
.
format
(
course_id
)
@staticmethod
def
_emit_grade_calculated_event
(
grade
):
"""
Emits an edx.grades.course.grade_calculated event
with data from the passed grade.
"""
# TODO: remove this context manager after completion of AN-6134
event_name
=
u'edx.grades.course.grade_calculated'
context
=
contexts
.
course_context_from_course_id
(
grade
.
course_id
)
with
tracker
.
get_tracker
()
.
context
(
event_name
,
context
):
tracker
.
emit
(
event_name
,
{
'user_id'
:
unicode
(
grade
.
user_id
),
'course_id'
:
unicode
(
grade
.
course_id
),
'course_version'
:
unicode
(
grade
.
course_version
),
'percent_grade'
:
grade
.
percent_grade
,
'letter_grade'
:
unicode
(
grade
.
letter_grade
),
'course_edited_timestamp'
:
unicode
(
grade
.
course_edited_timestamp
),
'event_transaction_id'
:
unicode
(
get_event_transaction_id
()),
'event_transaction_type'
:
unicode
(
get_event_transaction_type
()),
'grading_policy_hash'
:
unicode
(
grade
.
grading_policy_hash
),
}
)
events
.
course_grade_calculated
(
grade
)
class
PersistentSubsectionGradeOverride
(
models
.
Model
):
...
...
@@ -673,3 +591,32 @@ class PersistentSubsectionGradeOverride(models.Model):
possible_all_override
=
models
.
FloatField
(
null
=
True
,
blank
=
True
)
earned_graded_override
=
models
.
FloatField
(
null
=
True
,
blank
=
True
)
possible_graded_override
=
models
.
FloatField
(
null
=
True
,
blank
=
True
)
_CACHE_NAMESPACE
=
u"grades.models.PersistentSubsectionGradeOverride"
@classmethod
def
prefetch
(
cls
,
user_id
,
course_key
):
get_cache
(
cls
.
_CACHE_NAMESPACE
)[(
user_id
,
str
(
course_key
))]
=
{
override
.
grade
.
usage_key
:
override
for
override
in
cls
.
objects
.
filter
(
grade__user_id
=
user_id
,
grade__course_id
=
course_key
)
}
@classmethod
def
get_override
(
cls
,
user_id
,
usage_key
):
prefetch_values
=
get_cache
(
cls
.
_CACHE_NAMESPACE
)
.
get
((
user_id
,
str
(
usage_key
.
course_key
)),
None
)
if
prefetch_values
is
not
None
:
return
prefetch_values
.
get
(
usage_key
)
try
:
return
cls
.
objects
.
get
(
grade__user_id
=
user_id
,
grade__course_id
=
usage_key
.
course_key
,
grade__usage_key
=
usage_key
,
)
except
PersistentSubsectionGradeOverride
.
DoesNotExist
:
pass
def
prefetch
(
user
,
course_key
):
PersistentSubsectionGradeOverride
.
prefetch
(
user
.
id
,
course_key
)
VisibleBlocks
.
bulk_read
(
course_key
)
lms/djangoapps/grades/services.py
View file @
f8e3a3c4
...
...
@@ -4,10 +4,10 @@ import pytz
from
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
from
track.event_transaction_utils
import
create_new_event_transaction_id
,
set_event_transaction_type
from
util.date_utils
import
to_timestamp
from
.config.waffle
import
waffle_flags
,
REJECTED_EXAM_OVERRIDES_GRADE
from
.constants
import
ScoreDatabaseTableEnum
from
.events
import
SUBSECTION_OVERRIDE_EVENT_TYPE
from
.models
import
PersistentSubsectionGrade
,
PersistentSubsectionGradeOverride
from
.signals.signals
import
SUBSECTION_OVERRIDE_CHANGED
...
...
@@ -70,9 +70,6 @@ class GradesService(object):
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.
"""
# prevent circular imports:
from
.signals.handlers
import
SUBSECTION_OVERRIDE_EVENT_TYPE
course_key
=
_get_key
(
course_key_or_id
,
CourseKey
)
usage_key
=
_get_key
(
usage_key_or_id
,
UsageKey
)
...
...
@@ -113,9 +110,6 @@ class GradesService(object):
Fires off a recalculate_subsection_grade async task to update the PersistentSubsectionGrade table. If the
override does not exist, no error is raised, it just triggers the recalculation.
"""
# prevent circular imports:
from
.signals.handlers
import
SUBSECTION_OVERRIDE_EVENT_TYPE
course_key
=
_get_key
(
course_key_or_id
,
CourseKey
)
usage_key
=
_get_key
(
usage_key_or_id
,
UsageKey
)
...
...
lms/djangoapps/grades/signals/handlers.py
View file @
f8e3a3c4
...
...
@@ -5,21 +5,13 @@ from contextlib import contextmanager
from
logging
import
getLogger
from
courseware.model_data
import
get_score
,
set_score
from
crum
import
get_current_user
from
django.dispatch
import
receiver
from
eventtracking
import
tracker
from
lms.djangoapps.instructor_task.tasks_helper.module_state
import
GRADES_OVERRIDE_EVENT_TYPE
from
openedx.core.djangoapps.course_groups.signals.signals
import
COHORT_MEMBERSHIP_UPDATED
from
openedx.core.lib.grade_utils
import
is_score_higher_or_equal
from
student.models
import
user_by_anonymous_id
from
student.signals.signals
import
ENROLLMENT_TRACK_UPDATED
from
submissions.models
import
score_reset
,
score_set
from
track.event_transaction_utils
import
(
create_new_event_transaction_id
,
get_event_transaction_id
,
get_event_transaction_type
,
set_event_transaction_type
)
from
track.event_transaction_utils
import
get_event_transaction_id
,
get_event_transaction_type
from
util.date_utils
import
to_timestamp
from
xblock.scorable
import
ScorableXBlockMixin
,
Score
...
...
@@ -32,17 +24,12 @@ from .signals import (
)
from
..constants
import
ScoreDatabaseTableEnum
from
..course_grade_factory
import
CourseGradeFactory
from
..
import
events
from
..scores
import
weighted_score
from
..tasks
import
RECALCULATE_GRADE_DELAY
,
recalculate_subsection_grade_v3
log
=
getLogger
(
__name__
)
# define values to be used in grading events
GRADES_RESCORE_EVENT_TYPE
=
'edx.grades.problem.rescored'
PROBLEM_SUBMITTED_EVENT_TYPE
=
'edx.grades.problem.submitted'
SUBSECTION_OVERRIDE_EVENT_TYPE
=
'edx.grades.subsection.score_overridden'
STATE_DELETED_EVENT_TYPE
=
'edx.grades.problem.state_deleted'
@receiver
(
score_set
)
def
submissions_score_set_handler
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
...
...
@@ -127,7 +114,7 @@ def disconnect_submissions_signal_receiver(signal):
handler
=
submissions_score_set_handler
else
:
if
signal
!=
score_reset
:
raise
ValueError
(
"This context manager only
deal with
score_set and score_reset signals."
)
raise
ValueError
(
"This context manager only
handles
score_set and score_reset signals."
)
handler
=
submissions_score_reset_handler
signal
.
disconnect
(
handler
)
...
...
@@ -220,8 +207,8 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
Handles the PROBLEM_WEIGHTED_SCORE_CHANGED or SUBSECTION_OVERRIDE_CHANGED signals by
enqueueing a subsection update operation to occur asynchronously.
"""
_emit_event
(
kwargs
)
re
sult
=
re
calculate_subsection_grade_v3
.
apply_async
(
events
.
grade_updated
(
**
kwargs
)
recalculate_subsection_grade_v3
.
apply_async
(
kwargs
=
dict
(
user_id
=
kwargs
[
'user_id'
],
anonymous_user_id
=
kwargs
.
get
(
'anonymous_user_id'
),
...
...
@@ -249,7 +236,7 @@ def recalculate_course_grade_only(sender, course, course_structure, user, **kwar
@receiver
(
ENROLLMENT_TRACK_UPDATED
)
@receiver
(
COHORT_MEMBERSHIP_UPDATED
)
def
force_
recalculate_course_and_subsection_grades
(
sender
,
user
,
course_key
,
**
kwargs
):
def
recalculate_course_and_subsection_grades
(
sender
,
user
,
course_key
,
**
kwargs
):
"""
Updates a saved course grade, forcing the subsection grades
from which it is calculated to update along the way.
...
...
@@ -257,65 +244,3 @@ def force_recalculate_course_and_subsection_grades(sender, user, course_key, **k
previous_course_grade
=
CourseGradeFactory
()
.
read
(
user
,
course_key
=
course_key
)
if
previous_course_grade
and
previous_course_grade
.
attempted
:
CourseGradeFactory
()
.
update
(
user
=
user
,
course_key
=
course_key
,
force_update_subsections
=
True
)
def
_emit_event
(
kwargs
):
"""
Emits a problem submitted event only if there is no current event
transaction type, i.e. we have not reached this point in the code via a
rescore or student state deletion.
If the event transaction type has already been set and the transacation is
a rescore, emits a problem rescored event.
"""
root_type
=
get_event_transaction_type
()
if
not
root_type
:
root_id
=
get_event_transaction_id
()
if
not
root_id
:
root_id
=
create_new_event_transaction_id
()
set_event_transaction_type
(
PROBLEM_SUBMITTED_EVENT_TYPE
)
tracker
.
emit
(
unicode
(
PROBLEM_SUBMITTED_EVENT_TYPE
),
{
'user_id'
:
unicode
(
kwargs
[
'user_id'
]),
'course_id'
:
unicode
(
kwargs
[
'course_id'
]),
'problem_id'
:
unicode
(
kwargs
[
'usage_id'
]),
'event_transaction_id'
:
unicode
(
root_id
),
'event_transaction_type'
:
unicode
(
PROBLEM_SUBMITTED_EVENT_TYPE
),
'weighted_earned'
:
kwargs
.
get
(
'weighted_earned'
),
'weighted_possible'
:
kwargs
.
get
(
'weighted_possible'
),
}
)
if
root_type
in
[
GRADES_RESCORE_EVENT_TYPE
,
GRADES_OVERRIDE_EVENT_TYPE
]:
current_user
=
get_current_user
()
instructor_id
=
getattr
(
current_user
,
'id'
,
None
)
tracker
.
emit
(
unicode
(
GRADES_RESCORE_EVENT_TYPE
),
{
'course_id'
:
unicode
(
kwargs
[
'course_id'
]),
'user_id'
:
unicode
(
kwargs
[
'user_id'
]),
'problem_id'
:
unicode
(
kwargs
[
'usage_id'
]),
'new_weighted_earned'
:
kwargs
.
get
(
'weighted_earned'
),
'new_weighted_possible'
:
kwargs
.
get
(
'weighted_possible'
),
'only_if_higher'
:
kwargs
.
get
(
'only_if_higher'
),
'instructor_id'
:
unicode
(
instructor_id
),
'event_transaction_id'
:
unicode
(
get_event_transaction_id
()),
'event_transaction_type'
:
unicode
(
root_type
),
}
)
if
root_type
in
[
SUBSECTION_OVERRIDE_EVENT_TYPE
]:
tracker
.
emit
(
unicode
(
SUBSECTION_OVERRIDE_EVENT_TYPE
),
{
'course_id'
:
unicode
(
kwargs
[
'course_id'
]),
'user_id'
:
unicode
(
kwargs
[
'user_id'
]),
'problem_id'
:
unicode
(
kwargs
[
'usage_id'
]),
'only_if_higher'
:
kwargs
.
get
(
'only_if_higher'
),
'override_deleted'
:
kwargs
.
get
(
'score_deleted'
,
False
),
'event_transaction_id'
:
unicode
(
get_event_transaction_id
()),
'event_transaction_type'
:
unicode
(
root_type
),
}
)
lms/djangoapps/grades/subsection_grade.py
View file @
f8e3a3c4
...
...
@@ -101,46 +101,57 @@ class SubsectionGrade(SubsectionGradeBase):
"""
Class for Subsection Grades.
"""
def
__init__
(
self
,
subsection
):
def
__init__
(
self
,
subsection
,
problem_scores
,
all_total
,
graded_total
,
override
=
None
):
super
(
SubsectionGrade
,
self
)
.
__init__
(
subsection
)
self
.
problem_scores
=
OrderedDict
()
# dict of problem locations to ProblemScore
self
.
problem_scores
=
problem_scores
self
.
all_total
=
all_total
self
.
graded_total
=
graded_total
self
.
override
=
override
def
init_from_structure
(
self
,
student
,
course_structure
,
submissions_scores
,
csm_scores
):
@classmethod
def
create
(
cls
,
subsection
,
course_structure
,
submissions_scores
,
csm_scores
):
"""
Compute
the grade of this subsection for the given student and cours
e.
Compute
and create the subsection grad
e.
"""
for
descendant_key
in
course_structure
.
post_order_traversal
(
problem_scores
=
OrderedDict
()
for
block_key
in
course_structure
.
post_order_traversal
(
filter_func
=
possibly_scored
,
start_node
=
s
elf
.
location
,
start_node
=
s
ubsection
.
location
,
):
self
.
_compute_block_score
(
descendant_key
,
course_structure
,
submissions_scores
,
csm_scores
)
problem_score
=
cls
.
_compute_block_score
(
block_key
,
course_structure
,
submissions_scores
,
csm_scores
)
if
problem_score
:
problem_scores
[
block_key
]
=
problem_score
all_total
,
graded_total
=
graders
.
aggregate_scores
(
problem_scores
.
values
())
self
.
all_total
,
self
.
graded_total
=
graders
.
aggregate_scores
(
self
.
problem_scores
.
values
())
self
.
_log_event
(
log
.
debug
,
u"init_from_structure"
,
student
)
return
self
return
cls
(
subsection
,
problem_scores
,
all_total
,
graded_total
)
def
init_from_model
(
self
,
student
,
model
,
course_structure
,
submissions_scores
,
csm_scores
):
@classmethod
def
read
(
cls
,
subsection
,
model
,
course_structure
,
submissions_scores
,
csm_scores
):
"""
Lo
ad the subsection grade from the persisted model.
Re
ad the subsection grade from the persisted model.
"""
problem_scores
=
OrderedDict
()
for
block
in
model
.
visible_blocks
.
blocks
:
self
.
_compute_block_score
(
block
.
locator
,
course_structure
,
submissions_scores
,
csm_scores
,
block
)
problem_score
=
cls
.
_compute_block_score
(
block
.
locator
,
course_structure
,
submissions_scores
,
csm_scores
,
block
,
)
if
problem_score
:
problem_scores
[
block
.
locator
]
=
problem_score
self
.
graded_total
=
AggregatedScore
(
tw_earned
=
model
.
earned_graded
,
tw_possible
=
model
.
possible_graded
,
graded
=
True
,
first_attempted
=
model
.
first_attempted
,
)
self
.
all_total
=
AggregatedScore
(
all_total
=
AggregatedScore
(
tw_earned
=
model
.
earned_all
,
tw_possible
=
model
.
possible_all
,
graded
=
False
,
first_attempted
=
model
.
first_attempted
,
)
self
.
override
=
model
.
override
if
hasattr
(
model
,
'override'
)
else
None
self
.
_log_event
(
log
.
debug
,
u"init_from_model"
,
student
)
return
self
graded_total
=
AggregatedScore
(
tw_earned
=
model
.
earned_graded
,
tw_possible
=
model
.
possible_graded
,
graded
=
True
,
first_attempted
=
model
.
first_attempted
,
)
override
=
model
.
override
if
hasattr
(
model
,
'override'
)
else
None
return
cls
(
subsection
,
problem_scores
,
all_total
,
graded_total
,
override
)
@classmethod
def
bulk_create_models
(
cls
,
student
,
subsection_grades
,
course_key
):
...
...
@@ -153,17 +164,9 @@ class SubsectionGrade(SubsectionGradeBase):
if
subsection_grade
if
subsection_grade
.
_should_persist_per_attempted
()
# pylint: disable=protected-access
]
return
PersistentSubsectionGrade
.
bulk_create_grades
(
params
,
course_key
)
return
PersistentSubsectionGrade
.
bulk_create_grades
(
params
,
student
.
id
,
course_key
)
def
create_model
(
self
,
student
):
"""
Saves the subsection grade in a persisted model.
"""
if
self
.
_should_persist_per_attempted
():
self
.
_log_event
(
log
.
debug
,
u"create_model"
,
student
)
return
PersistentSubsectionGrade
.
create_grade
(
**
self
.
_persisted_model_params
(
student
))
def
update_or_create_model
(
self
,
student
,
score_deleted
):
def
update_or_create_model
(
self
,
student
,
score_deleted
=
False
):
"""
Saves or updates the subsection grade in a persisted model.
"""
...
...
@@ -184,8 +187,8 @@ class SubsectionGrade(SubsectionGradeBase):
score_deleted
)
@staticmethod
def
_compute_block_score
(
self
,
block_key
,
course_structure
,
submissions_scores
,
...
...
@@ -205,14 +208,12 @@ class SubsectionGrade(SubsectionGradeBase):
pass
else
:
if
getattr
(
block
,
'has_score'
,
False
):
problem_score
=
get_score
(
return
get_score
(
submissions_scores
,
csm_scores
,
persisted_block
,
block
,
)
if
problem_score
:
self
.
problem_scores
[
block_key
]
=
problem_score
def
_persisted_model_params
(
self
,
student
):
"""
...
...
lms/djangoapps/grades/subsection_grade_factory.py
View file @
f8e3a3c4
...
...
@@ -43,14 +43,14 @@ class SubsectionGradeFactory(object):
if
assume_zero_if_absent
(
self
.
course_data
.
course_key
):
subsection_grade
=
ZeroSubsectionGrade
(
subsection
,
self
.
course_data
)
else
:
subsection_grade
=
SubsectionGrade
(
subsection
)
.
init_from_structur
e
(
s
elf
.
student
,
self
.
course_data
.
structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
subsection_grade
=
SubsectionGrade
.
creat
e
(
s
ubsection
,
self
.
course_data
.
structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
)
if
should_persist_grades
(
self
.
course_data
.
course_key
):
if
read_only
:
self
.
_unsaved_subsection_grades
[
subsection_grade
.
location
]
=
subsection_grade
else
:
grade_model
=
subsection_grade
.
create_model
(
self
.
student
)
grade_model
=
subsection_grade
.
update_or_
create_model
(
self
.
student
)
self
.
_update_saved_subsection_grade
(
subsection
.
location
,
grade_model
)
return
subsection_grade
...
...
@@ -69,8 +69,8 @@ class SubsectionGradeFactory(object):
"""
self
.
_log_event
(
log
.
debug
,
u"update, subsection: {}"
.
format
(
subsection
.
location
),
subsection
)
calculated_grade
=
SubsectionGrade
(
subsection
)
.
init_from_structur
e
(
s
elf
.
student
,
self
.
course_data
.
structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
calculated_grade
=
SubsectionGrade
.
creat
e
(
s
ubsection
,
self
.
course_data
.
structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
)
if
should_persist_grades
(
self
.
course_data
.
course_key
):
...
...
@@ -80,8 +80,8 @@ class SubsectionGradeFactory(object):
except
PersistentSubsectionGrade
.
DoesNotExist
:
pass
else
:
orig_subsection_grade
=
SubsectionGrade
(
subsection
)
.
init_from_model
(
s
elf
.
student
,
grade_model
,
self
.
course_data
.
structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
orig_subsection_grade
=
SubsectionGrade
.
read
(
s
ubsection
,
grade_model
,
self
.
course_data
.
structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
)
if
not
is_score_higher_or_equal
(
orig_subsection_grade
.
graded_total
.
earned
,
...
...
@@ -123,10 +123,10 @@ class SubsectionGradeFactory(object):
"""
if
should_persist_grades
(
self
.
course_data
.
course_key
):
saved_subsection_grades
=
self
.
_get_bulk_cached_subsection_grades
()
subsection_
grade
=
saved_subsection_grades
.
get
(
subsection
.
location
)
if
subsection_
grade
:
return
SubsectionGrade
(
subsection
)
.
init_from_model
(
s
elf
.
student
,
subsection_
grade
,
self
.
course_data
.
structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
grade
=
saved_subsection_grades
.
get
(
subsection
.
location
)
if
grade
:
return
SubsectionGrade
.
read
(
s
ubsection
,
grade
,
self
.
course_data
.
structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
)
def
_get_bulk_cached_subsection_grades
(
self
):
...
...
lms/djangoapps/grades/tasks.py
View file @
f8e3a3c4
...
...
@@ -24,7 +24,7 @@ from track.event_transaction_utils import set_event_transaction_id, set_event_tr
from
util.date_utils
import
from_timestamp
from
xmodule.modulestore.django
import
modulestore
from
.config.waffle
import
ESTIMATE_FIRST_ATTEMPTED
,
DISABLE_REGRADE_ON_POLICY_CHANGE
,
waffle
from
.config.waffle
import
DISABLE_REGRADE_ON_POLICY_CHANGE
,
waffle
from
.constants
import
ScoreDatabaseTableEnum
from
.course_grade_factory
import
CourseGradeFactory
from
.exceptions
import
DatabaseNotReadyError
...
...
@@ -83,14 +83,6 @@ def compute_grades_for_course_v2(self, **kwargs):
TODO: Roll this back into compute_grades_for_course once all workers have
the version with **kwargs.
Sets the ESTIMATE_FIRST_ATTEMPTED flag, then calls the original task as a
synchronous function.
estimate_first_attempted:
controls whether to unconditionally set the ESTIMATE_FIRST_ATTEMPTED
waffle switch. If false or not provided, use the global value of
the ESTIMATE_FIRST_ATTEMPTED waffle switch.
"""
if
'event_transaction_id'
in
kwargs
:
set_event_transaction_id
(
kwargs
[
'event_transaction_id'
])
...
...
@@ -98,9 +90,6 @@ def compute_grades_for_course_v2(self, **kwargs):
if
'event_transaction_type'
in
kwargs
:
set_event_transaction_type
(
kwargs
[
'event_transaction_type'
])
if
kwargs
.
get
(
'estimate_first_attempted'
):
waffle
()
.
override_for_request
(
ESTIMATE_FIRST_ATTEMPTED
,
True
)
try
:
return
compute_grades_for_course
(
kwargs
[
'course_key'
],
kwargs
[
'offset'
],
kwargs
[
'batch_size'
])
except
Exception
as
exc
:
# pylint: disable=broad-except
...
...
lms/djangoapps/grades/tests/base.py
View file @
f8e3a3c4
...
...
@@ -26,7 +26,7 @@ class GradeTestBase(SharedModuleStoreTestCase):
cls
.
sequence
=
ItemFactory
.
create
(
parent
=
cls
.
chapter
,
category
=
'sequential'
,
display_name
=
"Test Sequential
1
"
,
display_name
=
"Test Sequential
X
"
,
graded
=
True
,
format
=
"Homework"
)
...
...
@@ -49,7 +49,7 @@ class GradeTestBase(SharedModuleStoreTestCase):
cls
.
sequence2
=
ItemFactory
.
create
(
parent
=
cls
.
chapter
,
category
=
'sequential'
,
display_name
=
"Test Sequential
2
"
,
display_name
=
"Test Sequential
A
"
,
graded
=
True
,
format
=
"Homework"
)
...
...
lms/djangoapps/grades/tests/integration/test_events.py
View file @
f8e3a3c4
...
...
@@ -3,7 +3,7 @@ Test grading events across apps.
"""
# pylint: disable=protected-access
from
mock
import
patch
from
mock
import
call
as
mock_call
,
patch
from
capa.tests.response_xml_factory
import
MultipleChoiceResponseXMLFactory
from
courseware.tests.test_submitting_problems
import
ProblemSubmissionTestMixin
...
...
@@ -16,9 +16,7 @@ from xmodule.modulestore import ModuleStoreEnum
from
xmodule.modulestore.tests.django_utils
import
SharedModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
STATE_DELETED_TYPE
=
'edx.grades.problem.state_deleted'
RESCORE_TYPE
=
'edx.grades.problem.rescored'
SUBMITTED_TYPE
=
'edx.grades.problem.submitted'
from
...
import
events
class
GradesEventIntegrationTest
(
ProblemSubmissionTestMixin
,
SharedModuleStoreTestCase
):
...
...
@@ -75,99 +73,84 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe
self
.
instructor
=
UserFactory
.
create
(
is_staff
=
True
,
username
=
u'test_instructor'
,
password
=
u'test'
)
self
.
refresh_course
()
@patch
(
'lms.djangoapps.instructor.enrollment.tracker'
)
@patch
(
'lms.djangoapps.grades.signals.handlers.tracker'
)
@patch
(
'lms.djangoapps.grades.models.tracker'
)
def
test_delete_student_state_events
(
self
,
models_tracker
,
handlers_tracker
,
enrollment_tracker
):
# submit answer
@patch
(
'lms.djangoapps.grades.events.tracker'
)
def
test_submit_answer
(
self
,
events_tracker
):
self
.
submit_question_answer
(
'p1'
,
{
'2_1'
:
'choice_choice_2'
})
course
=
self
.
store
.
get_course
(
self
.
course
.
id
,
depth
=
0
)
# check logging to make sure id's are tracked correctly across events
event_transaction_id
=
handlers_tracker
.
emit
.
mock_calls
[
0
][
1
][
1
][
'event_transaction_id'
]
for
call
in
models_tracker
.
emit
.
mock_calls
:
self
.
assertEqual
(
event_transaction_id
,
call
[
1
][
1
][
'event_transaction_id'
])
self
.
assertEqual
(
unicode
(
SUBMITTED_TYPE
),
call
[
1
][
1
][
'event_transaction_type'
])
handlers_tracker
.
emit
.
assert_called_with
(
unicode
(
SUBMITTED_TYPE
),
{
'user_id'
:
unicode
(
self
.
student
.
id
),
'event_transaction_id'
:
event_transaction_id
,
'event_transaction_type'
:
unicode
(
SUBMITTED_TYPE
),
'course_id'
:
unicode
(
self
.
course
.
id
),
'problem_id'
:
unicode
(
self
.
problem
.
location
),
'weighted_earned'
:
2.0
,
'weighted_possible'
:
2.0
,
}
event_transaction_id
=
events_tracker
.
emit
.
mock_calls
[
0
][
1
][
1
][
'event_transaction_id'
]
events_tracker
.
emit
.
assert_has_calls
(
[
mock_call
(
events
.
PROBLEM_SUBMITTED_EVENT_TYPE
,
{
'user_id'
:
unicode
(
self
.
student
.
id
),
'event_transaction_id'
:
event_transaction_id
,
'event_transaction_type'
:
events
.
PROBLEM_SUBMITTED_EVENT_TYPE
,
'course_id'
:
unicode
(
self
.
course
.
id
),
'problem_id'
:
unicode
(
self
.
problem
.
location
),
'weighted_earned'
:
2.0
,
'weighted_possible'
:
2.0
,
},
),
mock_call
(
events
.
COURSE_GRADE_CALCULATED
,
{
'course_version'
:
unicode
(
course
.
course_version
),
'percent_grade'
:
0.02
,
'grading_policy_hash'
:
u'ChVp0lHGQGCevD0t4njna/C44zQ='
,
'user_id'
:
unicode
(
self
.
student
.
id
),
'letter_grade'
:
u''
,
'event_transaction_id'
:
event_transaction_id
,
'event_transaction_type'
:
events
.
PROBLEM_SUBMITTED_EVENT_TYPE
,
'course_id'
:
unicode
(
self
.
course
.
id
),
'course_edited_timestamp'
:
unicode
(
course
.
subtree_edited_on
),
}
),
],
any_order
=
True
,
)
course
=
self
.
store
.
get_course
(
self
.
course
.
id
,
depth
=
0
)
models_tracker
.
emit
.
assert_called_with
(
u'edx.grades.course.grade_calculated'
,
{
'course_version'
:
unicode
(
course
.
course_version
),
'percent_grade'
:
0.02
,
'grading_policy_hash'
:
u'ChVp0lHGQGCevD0t4njna/C44zQ='
,
'user_id'
:
unicode
(
self
.
student
.
id
),
'letter_grade'
:
u''
,
'event_transaction_id'
:
event_transaction_id
,
'event_transaction_type'
:
unicode
(
SUBMITTED_TYPE
),
'course_id'
:
unicode
(
self
.
course
.
id
),
'course_edited_timestamp'
:
unicode
(
course
.
subtree_edited_on
),
}
)
models_tracker
.
reset_mock
()
handlers_tracker
.
reset_mock
()
def
test_delete_student_state
(
self
):
self
.
submit_question_answer
(
'p1'
,
{
'2_1'
:
'choice_choice_2'
})
# delete state
reset_student_attempts
(
self
.
course
.
id
,
self
.
student
,
self
.
problem
.
location
,
self
.
instructor
,
delete_module
=
True
)
with
patch
(
'lms.djangoapps.instructor.enrollment.tracker'
)
as
enrollment_tracker
:
with
patch
(
'lms.djangoapps.grades.events.tracker'
)
as
events_tracker
:
reset_student_attempts
(
self
.
course
.
id
,
self
.
student
,
self
.
problem
.
location
,
self
.
instructor
,
delete_module
=
True
,
)
course
=
self
.
store
.
get_course
(
self
.
course
.
id
,
depth
=
0
)
# check logging to make sure id's are tracked correctly across events
event_transaction_id
=
enrollment_tracker
.
method_calls
[
0
][
1
][
1
][
'event_transaction_id'
]
# make sure the id is propagated throughout the event flow
for
call
in
models_tracker
.
emit
.
mock_calls
:
self
.
assertEqual
(
event_transaction_id
,
call
[
1
][
1
][
'event_transaction_id'
])
self
.
assertEqual
(
unicode
(
STATE_DELETED_TYPE
),
call
[
1
][
1
][
'event_transaction_type'
])
# ensure we do not log a problem submitted event when state is deleted
handlers_tracker
.
assert_not_called
()
enrollment_tracker
.
emit
.
assert_called_with
(
unicode
(
STATE_DELETED_TYPE
)
,
events
.
STATE_DELETED_EVENT_TYPE
,
{
'user_id'
:
unicode
(
self
.
student
.
id
),
'course_id'
:
unicode
(
self
.
course
.
id
),
'problem_id'
:
unicode
(
self
.
problem
.
location
),
'instructor_id'
:
unicode
(
self
.
instructor
.
id
),
'event_transaction_id'
:
event_transaction_id
,
'event_transaction_type'
:
unicode
(
STATE_DELETED_TYPE
)
,
'event_transaction_type'
:
events
.
STATE_DELETED_EVENT_TYPE
,
}
)
course
=
self
.
store
.
get_course
(
self
.
course
.
id
,
depth
=
0
)
models_tracker
.
emit
.
assert_called_with
(
u'edx.grades.course.grade_calculated'
,
events_tracker
.
emit
.
assert_called_with
(
events
.
COURSE_GRADE_CALCULATED
,
{
'percent_grade'
:
0.0
,
'grading_policy_hash'
:
u'ChVp0lHGQGCevD0t4njna/C44zQ='
,
'user_id'
:
unicode
(
self
.
student
.
id
),
'letter_grade'
:
u''
,
'event_transaction_id'
:
event_transaction_id
,
'event_transaction_type'
:
unicode
(
STATE_DELETED_TYPE
)
,
'event_transaction_type'
:
events
.
STATE_DELETED_EVENT_TYPE
,
'course_id'
:
unicode
(
self
.
course
.
id
),
'course_edited_timestamp'
:
unicode
(
course
.
subtree_edited_on
),
'course_version'
:
unicode
(
course
.
course_version
),
}
)
@patch
(
'lms.djangoapps.grades.signals.handlers.tracker'
)
@patch
(
'lms.djangoapps.grades.models.tracker'
)
def
test_rescoring_events
(
self
,
models_tracker
,
handlers_tracker
):
# submit answer
def
test_rescoring_events
(
self
):
self
.
submit_question_answer
(
'p1'
,
{
'2_1'
:
'choice_choice_3'
})
models_tracker
.
reset_mock
()
handlers_tracker
.
reset_mock
()
new_problem_xml
=
MultipleChoiceResponseXMLFactory
()
.
build_xml
(
question_text
=
'The correct answer is Choice 3'
,
choices
=
[
False
,
False
,
False
,
True
],
...
...
@@ -178,56 +161,53 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe
self
.
store
.
update_item
(
self
.
problem
,
self
.
instructor
.
id
)
self
.
store
.
publish
(
self
.
problem
.
location
,
self
.
instructor
.
id
)
submit_rescore_problem_for_student
(
request
=
get_mock_request
(
self
.
instructor
),
usage_key
=
self
.
problem
.
location
,
student
=
self
.
student
,
only_if_higher
=
False
)
# check logging to make sure id's are tracked correctly across
# events
event_transaction_id
=
handlers_tracker
.
emit
.
mock_calls
[
0
][
1
][
1
][
'event_transaction_id'
]
# make sure the id is propagated throughout the event flow
for
call
in
models_tracker
.
emit
.
mock_calls
:
self
.
assertEqual
(
event_transaction_id
,
call
[
1
][
1
][
'event_transaction_id'
])
self
.
assertEqual
(
unicode
(
RESCORE_TYPE
),
call
[
1
][
1
][
'event_transaction_type'
])
with
patch
(
'lms.djangoapps.grades.events.tracker'
)
as
events_tracker
:
submit_rescore_problem_for_student
(
request
=
get_mock_request
(
self
.
instructor
),
usage_key
=
self
.
problem
.
location
,
student
=
self
.
student
,
only_if_higher
=
False
)
course
=
self
.
store
.
get_course
(
self
.
course
.
id
,
depth
=
0
)
# make sure the
models calls have re-added the course id to the context
for
args
in
model
s_tracker
.
get_tracker
()
.
context
.
call_args_list
:
# make sure the
tracker's context is updated with course info
for
args
in
event
s_tracker
.
get_tracker
()
.
context
.
call_args_list
:
self
.
assertEqual
(
args
[
0
][
1
],
{
'course_id'
:
unicode
(
self
.
course
.
id
),
'org_id'
:
unicode
(
self
.
course
.
org
)}
)
handlers_tracker
.
assert_not_called
()
handlers_tracker
.
emit
.
assert_called_with
(
unicode
(
RESCORE_TYPE
),
{
'course_id'
:
unicode
(
self
.
course
.
id
),
'user_id'
:
unicode
(
self
.
student
.
id
),
'problem_id'
:
unicode
(
self
.
problem
.
location
),
'new_weighted_earned'
:
2
,
'new_weighted_possible'
:
2
,
'only_if_higher'
:
False
,
'instructor_id'
:
unicode
(
self
.
instructor
.
id
),
'event_transaction_id'
:
event_transaction_id
,
'event_transaction_type'
:
unicode
(
RESCORE_TYPE
),
}
)
course
=
self
.
store
.
get_course
(
self
.
course
.
id
,
depth
=
0
)
models_tracker
.
emit
.
assert_called_with
(
u'edx.grades.course.grade_calculated'
,
{
'course_version'
:
unicode
(
course
.
course_version
),
'percent_grade'
:
0.02
,
'grading_policy_hash'
:
u'ChVp0lHGQGCevD0t4njna/C44zQ='
,
'user_id'
:
unicode
(
self
.
student
.
id
),
'letter_grade'
:
u''
,
'event_transaction_id'
:
event_transaction_id
,
'event_transaction_type'
:
unicode
(
RESCORE_TYPE
),
'course_id'
:
unicode
(
self
.
course
.
id
),
'course_edited_timestamp'
:
unicode
(
course
.
subtree_edited_on
),
}
event_transaction_id
=
events_tracker
.
emit
.
mock_calls
[
0
][
1
][
1
][
'event_transaction_id'
]
events_tracker
.
emit
.
assert_has_calls
(
[
mock_call
(
events
.
GRADES_RESCORE_EVENT_TYPE
,
{
'course_id'
:
unicode
(
self
.
course
.
id
),
'user_id'
:
unicode
(
self
.
student
.
id
),
'problem_id'
:
unicode
(
self
.
problem
.
location
),
'new_weighted_earned'
:
2
,
'new_weighted_possible'
:
2
,
'only_if_higher'
:
False
,
'instructor_id'
:
unicode
(
self
.
instructor
.
id
),
'event_transaction_id'
:
event_transaction_id
,
'event_transaction_type'
:
events
.
GRADES_RESCORE_EVENT_TYPE
,
},
),
mock_call
(
events
.
COURSE_GRADE_CALCULATED
,
{
'course_version'
:
unicode
(
course
.
course_version
),
'percent_grade'
:
0.02
,
'grading_policy_hash'
:
u'ChVp0lHGQGCevD0t4njna/C44zQ='
,
'user_id'
:
unicode
(
self
.
student
.
id
),
'letter_grade'
:
u''
,
'event_transaction_id'
:
event_transaction_id
,
'event_transaction_type'
:
events
.
GRADES_RESCORE_EVENT_TYPE
,
'course_id'
:
unicode
(
self
.
course
.
id
),
'course_edited_timestamp'
:
unicode
(
course
.
subtree_edited_on
),
},
),
],
any_order
=
True
,
)
lms/djangoapps/grades/tests/test_problems.py
→
lms/djangoapps/grades/tests/
integration/
test_problems.py
View file @
f8e3a3c4
...
...
@@ -16,8 +16,8 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from
xmodule.modulestore.tests.utils
import
TEST_DATA_DIR
from
xmodule.modulestore.xml_importer
import
import_course_from_xml
from
..subsection_grade_factory
import
SubsectionGradeFactory
from
.utils
import
answer_problem
,
mock_get_submissions_score
from
..
.
subsection_grade_factory
import
SubsectionGradeFactory
from
.
.
utils
import
answer_problem
,
mock_get_submissions_score
@ddt.ddt
...
...
lms/djangoapps/grades/tests/test_course_grade_factory.py
View file @
f8e3a3c4
...
...
@@ -2,25 +2,19 @@ import itertools
from
nose.plugins.attrib
import
attr
import
ddt
from
capa.tests.response_xml_factory
import
MultipleChoiceResponseXMLFactory
from
courseware.access
import
has_access
from
courseware.tests.test_submitting_problems
import
ProblemSubmissionTestMixin
from
django.conf
import
settings
from
lms.djangoapps.course_blocks.api
import
get_course_blocks
from
lms.djangoapps.grades.config.tests.utils
import
persistent_grades_feature_flags
from
mock
import
patch
from
openedx.core.djangolib.testing.utils
import
get_mock_request
from
openedx.core.djangoapps.content.block_structure.factory
import
BlockStructureFactory
from
student.models
import
CourseEnrollment
from
student.tests.factories
import
UserFactory
from
xmodule.modulestore.tests.django_utils
import
SharedModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
..config.waffle
import
ASSUME_ZERO_GRADE_IF_ABSENT
,
waffle
from
..course_grade
import
CourseGrade
,
ZeroCourseGrade
from
..course_grade_factory
import
CourseGradeFactory
from
..subsection_grade
import
SubsectionGrade
,
ZeroSubsectionGrade
from
..subsection_grade_factory
import
SubsectionGradeFactory
from
.base
import
GradeTestBase
from
.utils
import
mock_get_score
...
...
@@ -75,7 +69,7 @@ class TestCourseGradeFactory(GradeTestBase):
grade_factory
.
read
(
self
.
request
.
user
,
self
.
course
)
self
.
assertEqual
(
mock_read_grade
.
called
,
feature_flag
and
course_setting
)
def
test_read
(
self
):
def
test_read
_and_update
(
self
):
grade_factory
=
CourseGradeFactory
()
def
_assert_read
(
expected_pass
,
expected_percent
):
...
...
@@ -83,18 +77,40 @@ class TestCourseGradeFactory(GradeTestBase):
Creates the grade, ensuring it is as expected.
"""
course_grade
=
grade_factory
.
read
(
self
.
request
.
user
,
self
.
course
)
_assert_grade_values
(
course_grade
,
expected_pass
,
expected_percent
)
_assert_section_order
(
course_grade
)
def
_assert_grade_values
(
course_grade
,
expected_pass
,
expected_percent
):
self
.
assertEqual
(
course_grade
.
letter_grade
,
u'Pass'
if
expected_pass
else
None
)
self
.
assertEqual
(
course_grade
.
percent
,
expected_percent
)
with
waffle
()
.
override
(
ASSUME_ZERO_GRADE_IF_ABSENT
):
with
self
.
assertNumQueries
(
1
),
mock_get_score
(
1
,
2
):
_assert_read
(
expected_pass
=
False
,
expected_percent
=
0
)
def
_assert_section_order
(
course_grade
):
sections
=
course_grade
.
chapter_grades
[
self
.
chapter
.
location
][
'sections'
]
self
.
assertEqual
(
[
section
.
display_name
for
section
in
sections
],
[
self
.
sequence
.
display_name
,
self
.
sequence2
.
display_name
]
)
with
self
.
assertNumQueries
(
2
),
mock_get_score
(
1
,
2
):
_assert_read
(
expected_pass
=
False
,
expected_percent
=
0
)
# start off with grade of 0
with
self
.
assertNumQueries
(
29
),
mock_get_score
(
1
,
2
):
grade_factory
.
update
(
self
.
request
.
user
,
self
.
course
,
force_update_subsections
=
True
)
with
self
.
assertNumQueries
(
4
):
_assert_read
(
expected_pass
=
True
,
expected_percent
=
0.5
)
# updated to grade of .5
with
self
.
assertNumQueries
(
6
),
mock_get_score
(
1
,
4
):
grade_factory
.
update
(
self
.
request
.
user
,
self
.
course
,
force_update_subsections
=
False
)
with
self
.
assertNumQueries
(
10
),
mock_get_score
(
1
,
2
):
grade_factory
.
update
(
self
.
request
.
user
,
self
.
course
)
with
self
.
assertNumQueries
(
4
):
_assert_read
(
expected_pass
=
True
,
expected_percent
=
0.5
)
# NOT updated to grade of .25
with
self
.
assertNumQueries
(
12
),
mock_get_score
(
2
,
2
):
grade_factory
.
update
(
self
.
request
.
user
,
self
.
course
,
force_update_subsections
=
True
)
with
self
.
assertNumQueries
(
1
):
_assert_read
(
expected_pass
=
True
,
expected_percent
=
0.5
)
with
self
.
assertNumQueries
(
4
):
_assert_read
(
expected_pass
=
True
,
expected_percent
=
1.0
)
# updated to grade of 1.0
@patch.dict
(
settings
.
FEATURES
,
{
'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS'
:
False
})
@ddt.data
(
*
itertools
.
product
((
True
,
False
),
(
True
,
False
)))
...
...
@@ -109,21 +125,20 @@ class TestCourseGradeFactory(GradeTestBase):
self
.
assertIsNone
(
course_grade
)
def
test_create_zero_subs_grade_for_nonzero_course_grade
(
self
):
with
waffle
()
.
override
(
ASSUME_ZERO_GRADE_IF_ABSENT
):
subsection
=
self
.
course_structure
[
self
.
sequence
.
location
]
with
mock_get_score
(
1
,
2
):
self
.
subsection_grade_factory
.
update
(
subsection
)
course_grade
=
CourseGradeFactory
()
.
update
(
self
.
request
.
user
,
self
.
course
)
subsection1_grade
=
course_grade
.
subsection_grades
[
self
.
sequence
.
location
]
subsection2_grade
=
course_grade
.
subsection_grades
[
self
.
sequence2
.
location
]
self
.
assertIsInstance
(
subsection1_grade
,
SubsectionGrade
)
self
.
assertIsInstance
(
subsection2_grade
,
ZeroSubsectionGrade
)
subsection
=
self
.
course_structure
[
self
.
sequence
.
location
]
with
mock_get_score
(
1
,
2
):
self
.
subsection_grade_factory
.
update
(
subsection
)
course_grade
=
CourseGradeFactory
()
.
update
(
self
.
request
.
user
,
self
.
course
)
subsection1_grade
=
course_grade
.
subsection_grades
[
self
.
sequence
.
location
]
subsection2_grade
=
course_grade
.
subsection_grades
[
self
.
sequence2
.
location
]
self
.
assertIsInstance
(
subsection1_grade
,
SubsectionGrade
)
self
.
assertIsInstance
(
subsection2_grade
,
ZeroSubsectionGrade
)
@ddt.data
(
True
,
False
)
def
test_iter_force_update
(
self
,
force_update
):
with
patch
(
'lms.djangoapps.grades.subsection_grade_factory.SubsectionGradeFactory.update'
)
as
mock_update
:
set
(
CourseGradeFactory
()
.
iter
(
users
=
[
self
.
request
.
user
],
course
=
self
.
course
,
force_update
=
force_update
users
=
[
self
.
request
.
user
],
course
=
self
.
course
,
force_update
=
force_update
,
))
self
.
assertEqual
(
mock_update
.
called
,
force_update
)
...
...
@@ -149,13 +164,13 @@ class TestCourseGradeFactory(GradeTestBase):
'section_breakdown'
:
[
{
'category'
:
'Homework'
,
'detail'
:
u'Homework 1 - Test Sequential
1
- 50
%
(1/2)'
,
'detail'
:
u'Homework 1 - Test Sequential
X
- 50
%
(1/2)'
,
'label'
:
u'HW 01'
,
'percent'
:
0.5
},
{
'category'
:
'Homework'
,
'detail'
:
u'Homework 2 - Test Sequential
2
- 0
%
(0/1)'
,
'detail'
:
u'Homework 2 - Test Sequential
A
- 0
%
(0/1)'
,
'label'
:
u'HW 02'
,
'percent'
:
0.0
},
...
...
@@ -282,81 +297,3 @@ class TestGradeIteration(SharedModuleStoreTestCase):
students_to_errors
[
student
]
=
error
return
students_to_course_grades
,
students_to_errors
class
TestCourseGradeLogging
(
ProblemSubmissionTestMixin
,
SharedModuleStoreTestCase
):
"""
Tests logging in the course grades module.
Uses a larger course structure than other
unit tests.
"""
def
setUp
(
self
):
super
(
TestCourseGradeLogging
,
self
)
.
setUp
()
self
.
course
=
CourseFactory
.
create
()
with
self
.
store
.
bulk_operations
(
self
.
course
.
id
):
self
.
chapter
=
ItemFactory
.
create
(
parent
=
self
.
course
,
category
=
"chapter"
,
display_name
=
"Test Chapter"
)
self
.
sequence
=
ItemFactory
.
create
(
parent
=
self
.
chapter
,
category
=
'sequential'
,
display_name
=
"Test Sequential 1"
,
graded
=
True
)
self
.
sequence_2
=
ItemFactory
.
create
(
parent
=
self
.
chapter
,
category
=
'sequential'
,
display_name
=
"Test Sequential 2"
,
graded
=
True
)
self
.
sequence_3
=
ItemFactory
.
create
(
parent
=
self
.
chapter
,
category
=
'sequential'
,
display_name
=
"Test Sequential 3"
,
graded
=
False
)
self
.
vertical
=
ItemFactory
.
create
(
parent
=
self
.
sequence
,
category
=
'vertical'
,
display_name
=
'Test Vertical 1'
)
self
.
vertical_2
=
ItemFactory
.
create
(
parent
=
self
.
sequence_2
,
category
=
'vertical'
,
display_name
=
'Test Vertical 2'
)
self
.
vertical_3
=
ItemFactory
.
create
(
parent
=
self
.
sequence_3
,
category
=
'vertical'
,
display_name
=
'Test Vertical 3'
)
problem_xml
=
MultipleChoiceResponseXMLFactory
()
.
build_xml
(
question_text
=
'The correct answer is Choice 2'
,
choices
=
[
False
,
False
,
True
,
False
],
choice_names
=
[
'choice_0'
,
'choice_1'
,
'choice_2'
,
'choice_3'
]
)
self
.
problem
=
ItemFactory
.
create
(
parent
=
self
.
vertical
,
category
=
"problem"
,
display_name
=
"test_problem_1"
,
data
=
problem_xml
)
self
.
problem_2
=
ItemFactory
.
create
(
parent
=
self
.
vertical_2
,
category
=
"problem"
,
display_name
=
"test_problem_2"
,
data
=
problem_xml
)
self
.
problem_3
=
ItemFactory
.
create
(
parent
=
self
.
vertical_3
,
category
=
"problem"
,
display_name
=
"test_problem_3"
,
data
=
problem_xml
)
self
.
request
=
get_mock_request
(
UserFactory
())
self
.
client
.
login
(
username
=
self
.
request
.
user
.
username
,
password
=
"test"
)
self
.
course_structure
=
get_course_blocks
(
self
.
request
.
user
,
self
.
course
.
location
)
self
.
subsection_grade_factory
=
SubsectionGradeFactory
(
self
.
request
.
user
,
self
.
course
,
self
.
course_structure
)
CourseEnrollment
.
enroll
(
self
.
request
.
user
,
self
.
course
.
id
)
lms/djangoapps/grades/tests/test_models.py
View file @
f8e3a3c4
...
...
@@ -16,7 +16,6 @@ from freezegun import freeze_time
from
mock
import
patch
from
opaque_keys.edx.locator
import
BlockUsageLocator
,
CourseLocator
from
lms.djangoapps.grades.config
import
waffle
from
lms.djangoapps.grades.models
import
(
BLOCK_RECORD_LIST_VERSION
,
BlockRecord
,
...
...
@@ -131,7 +130,7 @@ class VisibleBlocksTest(GradesModelTestCase):
"""
Creates and returns a BlockRecordList for the given blocks.
"""
return
VisibleBlocks
.
objects
.
create_from_blockrecords
(
BlockRecordList
.
from_list
(
blocks
,
self
.
course_key
))
return
VisibleBlocks
.
cached_get_or_create
(
BlockRecordList
.
from_list
(
blocks
,
self
.
course_key
))
def
test_creation
(
self
):
"""
...
...
@@ -215,45 +214,30 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
"first_attempted"
:
datetime
(
2000
,
1
,
1
,
12
,
30
,
45
,
tzinfo
=
pytz
.
UTC
),
}
def
test_create
(
self
):
"""
Tests model creation, and confirms error when trying to recreate model.
"""
created_grade
=
PersistentSubsectionGrade
.
create_grade
(
**
self
.
params
)
with
self
.
assertNumQueries
(
1
):
read_grade
=
PersistentSubsectionGrade
.
read_grade
(
user_id
=
self
.
params
[
"user_id"
],
usage_key
=
self
.
params
[
"usage_key"
],
)
self
.
assertEqual
(
created_grade
,
read_grade
)
self
.
assertEqual
(
read_grade
.
visible_blocks
.
blocks
,
self
.
block_records
)
with
self
.
assertRaises
(
IntegrityError
):
PersistentSubsectionGrade
.
create_grade
(
**
self
.
params
)
@ddt.data
(
'course_version'
,
'subtree_edited_timestamp'
)
def
test_optional_fields
(
self
,
field
):
del
self
.
params
[
field
]
PersistentSubsectionGrade
.
create_grade
(
**
self
.
params
)
PersistentSubsectionGrade
.
update_or_
create_grade
(
**
self
.
params
)
@ddt.data
(
(
"user_id"
,
Integrit
yError
),
(
"user_id"
,
Ke
yError
),
(
"usage_key"
,
KeyError
),
(
"earned_all"
,
IntegrityError
),
(
"possible_all"
,
IntegrityError
),
(
"earned_graded"
,
IntegrityError
),
(
"possible_graded"
,
IntegrityError
),
(
"first_attempted"
,
KeyError
),
(
"visible_blocks"
,
KeyError
),
(
"first_attempted"
,
KeyError
),
)
@ddt.unpack
def
test_non_optional_fields
(
self
,
field
,
error
):
del
self
.
params
[
field
]
with
self
.
assertRaises
(
error
):
PersistentSubsectionGrade
.
create_grade
(
**
self
.
params
)
PersistentSubsectionGrade
.
update_or_
create_grade
(
**
self
.
params
)
@ddt.data
(
True
,
False
)
def
test_update_or_create_grade
(
self
,
already_created
):
created_grade
=
PersistentSubsectionGrade
.
create_grade
(
**
self
.
params
)
if
already_created
else
None
created_grade
=
PersistentSubsectionGrade
.
update_or_
create_grade
(
**
self
.
params
)
if
already_created
else
None
self
.
params
[
"earned_all"
]
=
7
updated_grade
=
PersistentSubsectionGrade
.
update_or_create_grade
(
**
self
.
params
)
...
...
@@ -262,53 +246,48 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
self
.
assertEqual
(
created_grade
.
id
,
updated_grade
.
id
)
self
.
assertEqual
(
created_grade
.
earned_all
,
6
)
@ddt.unpack
@ddt.data
(
(
True
,
datetime
(
2000
,
1
,
1
,
12
,
30
,
45
,
tzinfo
=
pytz
.
UTC
)),
(
False
,
None
),
# Use as now(). Freeze time needs this calculation to happen at test time.
)
def
test_update_or_create_attempted
(
self
,
is_active
,
expected_first_attempted
):
with
freeze_time
(
now
()):
if
expected_first_attempted
is
None
:
expected_first_attempted
=
now
()
with
waffle
.
waffle
()
.
override
(
waffle
.
ESTIMATE_FIRST_ATTEMPTED
,
active
=
is_active
):
grade
=
PersistentSubsectionGrade
.
update_or_create_grade
(
**
self
.
params
)
self
.
assertEqual
(
grade
.
first_attempted
,
expected_first_attempted
)
with
self
.
assertNumQueries
(
1
):
read_grade
=
PersistentSubsectionGrade
.
read_grade
(
user_id
=
self
.
params
[
"user_id"
],
usage_key
=
self
.
params
[
"usage_key"
],
)
self
.
assertEqual
(
updated_grade
,
read_grade
)
self
.
assertEqual
(
read_grade
.
visible_blocks
.
blocks
,
self
.
block_records
)
def
test_unattempted
(
self
):
self
.
params
[
'first_attempted'
]
=
None
self
.
params
[
'earned_all'
]
=
0.0
self
.
params
[
'earned_graded'
]
=
0.0
grade
=
PersistentSubsectionGrade
.
create_grade
(
**
self
.
params
)
grade
=
PersistentSubsectionGrade
.
update_or_
create_grade
(
**
self
.
params
)
self
.
assertIsNone
(
grade
.
first_attempted
)
self
.
assertEqual
(
grade
.
earned_all
,
0.0
)
self
.
assertEqual
(
grade
.
earned_graded
,
0.0
)
def
test_first_attempted_not_changed_on_update
(
self
):
PersistentSubsectionGrade
.
create_grade
(
**
self
.
params
)
PersistentSubsectionGrade
.
update_or_
create_grade
(
**
self
.
params
)
moment
=
now
()
grade
=
PersistentSubsectionGrade
.
update_or_create_grade
(
**
self
.
params
)
self
.
assertLess
(
grade
.
first_attempted
,
moment
)
def
test_unattempted_save_does_not_remove_attempt
(
self
):
PersistentSubsectionGrade
.
create_grade
(
**
self
.
params
)
PersistentSubsectionGrade
.
update_or_
create_grade
(
**
self
.
params
)
self
.
params
[
'first_attempted'
]
=
None
grade
=
PersistentSubsectionGrade
.
update_or_create_grade
(
**
self
.
params
)
self
.
assertIsInstance
(
grade
.
first_attempted
,
datetime
)
self
.
assertEqual
(
grade
.
earned_all
,
6.0
)
def
test_update_or_create_event
(
self
):
with
patch
(
'lms.djangoapps.grades.
model
s.tracker'
)
as
tracker_mock
:
with
patch
(
'lms.djangoapps.grades.
event
s.tracker'
)
as
tracker_mock
:
grade
=
PersistentSubsectionGrade
.
update_or_create_grade
(
**
self
.
params
)
self
.
_assert_tracker_emitted_event
(
tracker_mock
,
grade
)
def
test_create_event
(
self
):
with
patch
(
'lms.djangoapps.grades.
model
s.tracker'
)
as
tracker_mock
:
grade
=
PersistentSubsectionGrade
.
create_grade
(
**
self
.
params
)
with
patch
(
'lms.djangoapps.grades.
event
s.tracker'
)
as
tracker_mock
:
grade
=
PersistentSubsectionGrade
.
update_or_
create_grade
(
**
self
.
params
)
self
.
_assert_tracker_emitted_event
(
tracker_mock
,
grade
)
def
test_grade_override
(
self
):
grade
=
PersistentSubsectionGrade
.
create_grade
(
**
self
.
params
)
grade
=
PersistentSubsectionGrade
.
update_or_
create_grade
(
**
self
.
params
)
override
=
PersistentSubsectionGradeOverride
(
grade
=
grade
,
earned_all_override
=
0.0
,
earned_graded_override
=
0.0
)
override
.
save
()
grade
=
PersistentSubsectionGrade
.
update_or_create_grade
(
**
self
.
params
)
...
...
@@ -456,7 +435,7 @@ class PersistentCourseGradesTest(GradesModelTestCase):
PersistentCourseGrade
.
read
(
self
.
params
[
"user_id"
],
self
.
params
[
"course_id"
])
def
test_update_or_create_event
(
self
):
with
patch
(
'lms.djangoapps.grades.
model
s.tracker'
)
as
tracker_mock
:
with
patch
(
'lms.djangoapps.grades.
event
s.tracker'
)
as
tracker_mock
:
grade
=
PersistentCourseGrade
.
update_or_create
(
**
self
.
params
)
self
.
_assert_tracker_emitted_event
(
tracker_mock
,
grade
)
...
...
lms/djangoapps/grades/tests/test_services.py
View file @
f8e3a3c4
...
...
@@ -4,7 +4,6 @@ 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
lms.djangoapps.grades.signals.handlers
import
SUBSECTION_OVERRIDE_EVENT_TYPE
from
mock
import
patch
,
call
from
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
from
student.tests.factories
import
UserFactory
...
...
lms/djangoapps/grades/tests/test_signals.py
View file @
f8e3a3c4
"""
Tests for the score change signals defined in the courseware models module.
"""
import
itertools
import
re
from
datetime
import
datetime
...
...
@@ -10,10 +9,6 @@ import pytz
from
django.test
import
TestCase
from
mock
import
MagicMock
,
patch
from
opaque_keys.edx.locations
import
CourseLocator
from
openedx.core.djangoapps.course_groups.signals.signals
import
COHORT_MEMBERSHIP_UPDATED
from
student.signals.signals
import
ENROLLMENT_TRACK_UPDATED
from
student.tests.factories
import
UserFactory
from
submissions.models
import
score_reset
,
score_set
from
util.date_utils
import
to_timestamp
...
...
lms/djangoapps/grades/tests/test_subsection_grade.py
View file @
f8e3a3c4
from
..models
import
PersistentSubsectionGrade
from
..subsection_grade
import
SubsectionGrade
from
.utils
import
mock_get_score
from
.base
import
GradeTestBase
from
.base
import
GradeTestBase
class
SubsectionGradeTest
(
GradeTestBase
):
def
test_
save_and_lo
ad
(
self
):
def
test_
create_and_re
ad
(
self
):
with
mock_get_score
(
1
,
2
):
# Create a grade that *isn't* saved to the database
input_grade
=
SubsectionGrade
(
self
.
sequence
)
input_grade
.
init_from_structure
(
self
.
request
.
user
,
created_grade
=
SubsectionGrade
.
create
(
self
.
sequence
,
self
.
course_structure
,
self
.
subsection_grade_factory
.
_submissions_scores
,
self
.
subsection_grade_factory
.
_csm_scores
,
...
...
@@ -18,23 +17,22 @@ class SubsectionGradeTest(GradeTestBase):
self
.
assertEqual
(
PersistentSubsectionGrade
.
objects
.
count
(),
0
)
# save to db, and verify object is in database
input_grade
.
create_model
(
self
.
request
.
user
)
created_grade
.
update_or_
create_model
(
self
.
request
.
user
)
self
.
assertEqual
(
PersistentSubsectionGrade
.
objects
.
count
(),
1
)
# load from db, and ensure output matches input
loaded_grade
=
SubsectionGrade
(
self
.
sequence
)
# read from db, and ensure output matches input
saved_model
=
PersistentSubsectionGrade
.
read_grade
(
user_id
=
self
.
request
.
user
.
id
,
usage_key
=
self
.
sequence
.
location
,
)
loaded_grade
.
init_from_model
(
self
.
request
.
user
,
read_grade
=
SubsectionGrade
.
read
(
self
.
sequence
,
saved_model
,
self
.
course_structure
,
self
.
subsection_grade_factory
.
_submissions_scores
,
self
.
subsection_grade_factory
.
_csm_scores
,
)
self
.
assertEqual
(
input_grade
.
url_name
,
loade
d_grade
.
url_name
)
loaded_grade
.
all_total
.
first_attempted
=
input
_grade
.
all_total
.
first_attempted
=
None
self
.
assertEqual
(
input_grade
.
all_total
,
loade
d_grade
.
all_total
)
self
.
assertEqual
(
created_grade
.
url_name
,
rea
d_grade
.
url_name
)
read_grade
.
all_total
.
first_attempted
=
created
_grade
.
all_total
.
first_attempted
=
None
self
.
assertEqual
(
created_grade
.
all_total
,
rea
d_grade
.
all_total
)
lms/djangoapps/grades/tests/test_tasks.py
View file @
f8e3a3c4
...
...
@@ -164,10 +164,10 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self
.
assertEquals
(
mock_block_structure_create
.
call_count
,
1
)
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
7
,
True
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
7
,
False
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
7
,
True
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
7
,
False
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
5
,
True
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
5
,
False
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
5
,
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
):
...
...
@@ -179,8 +179,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self
.
_apply_recalculate_subsection_grade
()
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
7
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
7
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
5
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
5
),
)
@ddt.unpack
def
test_query_counts_dont_change_with_more_content
(
self
,
default_store
,
num_mongo_calls
,
num_sql_calls
):
...
...
@@ -240,8 +240,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
8
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
8
),
(
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
):
...
...
lms/djangoapps/instructor/enrollment.py
View file @
f8e3a3c4
...
...
@@ -20,7 +20,8 @@ from courseware.models import StudentModule
from
edxmako.shortcuts
import
render_to_string
from
eventtracking
import
tracker
from
lms.djangoapps.grades.constants
import
ScoreDatabaseTableEnum
from
lms.djangoapps.grades.signals.handlers
import
disconnect_submissions_signal_receiver
,
STATE_DELETED_EVENT_TYPE
from
lms.djangoapps.grades.events
import
STATE_DELETED_EVENT_TYPE
from
lms.djangoapps.grades.signals.handlers
import
disconnect_submissions_signal_receiver
from
lms.djangoapps.grades.signals.signals
import
PROBLEM_RAW_SCORE_CHANGED
from
openedx.core.djangoapps.lang_pref
import
LANGUAGE_KEY
from
openedx.core.djangoapps.site_configuration
import
helpers
as
configuration_helpers
...
...
lms/djangoapps/instructor_task/tasks_helper/module_state.py
View file @
f8e3a3c4
...
...
@@ -7,7 +7,6 @@ from time import time
from
django.contrib.auth.models
import
User
from
opaque_keys.edx.keys
import
UsageKey
from
xblock.runtime
import
KvsFieldData
import
dogstats_wrapper
as
dog_stats_api
from
capa.responsetypes
import
LoncapaProblemError
,
ResponseError
,
StudentInputError
...
...
@@ -15,16 +14,13 @@ from courseware.courses import get_course_by_id, get_problems_in_section
from
courseware.model_data
import
DjangoKeyValueStore
,
FieldDataCache
from
courseware.models
import
StudentModule
from
courseware.module_render
import
get_module_for_descriptor_internal
from
eventtracking
import
tracker
from
lms.djangoapps.grades.scores
import
weighted_score
from
track.contexts
import
course_context_from_course_id
from
lms.djangoapps.grades.events
import
GRADES_OVERRIDE_EVENT_TYPE
,
GRADES_RESCORE_EVENT_TYPE
from
track.event_transaction_utils
import
create_new_event_transaction_id
,
set_event_transaction_type
from
track.views
import
task_track
from
util.db
import
outer_atomic
from
xmodule.modulestore.django
import
modulestore
from
xblock.runtime
import
KvsFieldData
from
xblock.scorable
import
Score
,
ScorableXBlockMixin
from
xblock.scorable
import
Score
from
xmodule.modulestore.django
import
modulestore
from
..exceptions
import
UpdateProblemModuleStateError
from
.runner
import
TaskProgress
...
...
@@ -32,10 +28,6 @@ from .utils import UNKNOWN_TASK_ID, UPDATE_STATUS_FAILED, UPDATE_STATUS_SKIPPED,
TASK_LOG
=
logging
.
getLogger
(
'edx.celery.task'
)
# define value to be used in grading events
GRADES_RESCORE_EVENT_TYPE
=
'edx.grades.problem.rescored'
GRADES_OVERRIDE_EVENT_TYPE
=
'edx.grades.problem.score_overridden'
def
perform_module_state_update
(
update_fcn
,
filter_fcn
,
_entry_id
,
course_id
,
task_input
,
action_name
):
"""
...
...
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