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
efaca489
Commit
efaca489
authored
Dec 08, 2016
by
Brittney Exline
Committed by
GitHub
Dec 08, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' into bexline/footer_add_enterprise
parents
a42bbd57
2b2ecd74
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
619 additions
and
84 deletions
+619
-84
common/djangoapps/track/event_transaction_utils.py
+50
-0
common/djangoapps/track/utils.py
+1
-1
common/lib/xmodule/xmodule/capa_base.py
+5
-2
lms/djangoapps/grades/models.py
+57
-1
lms/djangoapps/grades/signals/handlers.py
+43
-4
lms/djangoapps/grades/tasks.py
+40
-15
lms/djangoapps/grades/tests/integration/__init__.py
+0
-0
lms/djangoapps/grades/tests/integration/test_events.py
+225
-0
lms/djangoapps/grades/tests/test_models.py
+63
-2
lms/djangoapps/grades/tests/test_tasks.py
+18
-16
lms/djangoapps/instructor/enrollment.py
+41
-11
lms/djangoapps/instructor/tests/test_enrollment.py
+2
-2
lms/djangoapps/instructor_task/api_helper.py
+1
-0
lms/djangoapps/instructor_task/tasks_helper.py
+58
-25
lms/djangoapps/instructor_task/tests/test_tasks.py
+14
-4
requirements/edx/github.txt
+1
-1
No files found.
common/djangoapps/track/event_transaction_utils.py
0 → 100644
View file @
efaca489
"""
Helper functions to access and update the id and type
used in event tracking.
"""
from
uuid
import
uuid4
,
UUID
from
request_cache
import
get_cache
def
get_event_transaction_id
():
"""
Retrieves the current event transaction id from the request
cache.
"""
return
get_cache
(
'event_transaction'
)
.
get
(
'id'
,
None
)
def
get_event_transaction_type
():
"""
Retrieves the current event transaction type from the request
cache.
"""
return
get_cache
(
'event_transaction'
)
.
get
(
'type'
,
None
)
def
create_new_event_transaction_id
():
"""
Sets the event transaction id to a newly-
generated UUID.
"""
new_id
=
uuid4
()
get_cache
(
'event_transaction'
)[
'id'
]
=
new_id
return
new_id
def
set_event_transaction_id
(
new_id
):
"""
Sets the event transaction id to a UUID object
generated from new_id.
new_id must be a parsable string version
of a UUID.
"""
get_cache
(
'event_transaction'
)[
'id'
]
=
UUID
(
new_id
)
def
set_event_transaction_type
(
action_type
):
"""
Takes a string and stores it in the request cache
as the user action type.
"""
get_cache
(
'event_transaction'
)[
'type'
]
=
action_type
common/djangoapps/track/utils.py
View file @
efaca489
"""Utility functions and classes for track backends"""
from
datetime
import
datetime
,
date
import
json
from
datetime
import
datetime
,
date
from
pytz
import
UTC
...
...
common/lib/xmodule/xmodule/capa_base.py
View file @
efaca489
...
...
@@ -1440,7 +1440,6 @@ class CapaMixin(CapaFields):
# rescoring should have no effect on attempts, so don't
# need to increment here, or mark done. Just save.
self
.
set_state_from_lcp
()
self
.
publish_grade
(
only_if_higher
)
new_score
=
self
.
lcp
.
get_score
()
...
...
@@ -1460,7 +1459,11 @@ class CapaMixin(CapaFields):
event_info
[
'attempts'
]
=
self
.
attempts
self
.
track_function_unmask
(
'problem_rescore'
,
event_info
)
return
{
'success'
:
success
}
return
{
'success'
:
success
,
'new_raw_earned'
:
new_score
[
'score'
],
'new_raw_possible'
:
new_score
[
'total'
],
}
def
save_problem
(
self
,
data
):
"""
...
...
lms/djangoapps/grades/models.py
View file @
efaca489
...
...
@@ -18,7 +18,9 @@ import logging
from
django.core.exceptions
import
ValidationError
from
django.db
import
models
from
django.utils.timezone
import
now
from
eventtracking
import
tracker
from
model_utils.models
import
TimeStampedModel
from
track.event_transaction_utils
import
get_event_transaction_id
,
get_event_transaction_type
from
coursewarehistoryextended.fields
import
UnsignedBigIntAutoField
from
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
...
...
@@ -341,10 +343,12 @@ class PersistentSubsectionGrade(TimeStampedModel):
usage_key
=
usage_key
,
defaults
=
params
,
)
if
attempted
and
not
grade
.
first_attempted
:
grade
.
first_attempted
=
now
()
grade
.
save
()
grade
.
full_clean
()
cls
.
_emit_grade_calculated_event
(
grade
)
return
grade
@classmethod
...
...
@@ -357,6 +361,7 @@ class PersistentSubsectionGrade(TimeStampedModel):
grade
=
cls
(
**
params
)
grade
.
full_clean
()
grade
.
save
()
cls
.
_emit_grade_calculated_event
(
grade
)
return
grade
@classmethod
...
...
@@ -376,7 +381,10 @@ class PersistentSubsectionGrade(TimeStampedModel):
grades
=
[
PersistentSubsectionGrade
(
**
params
)
for
params
in
grade_params_iter
]
for
grade
in
grades
:
grade
.
full_clean
()
return
cls
.
objects
.
bulk_create
(
grades
)
grades
=
cls
.
objects
.
bulk_create
(
grades
)
for
grade
in
grades
:
cls
.
_emit_grade_calculated_event
(
grade
)
return
grades
@classmethod
def
_prepare_params_and_visible_blocks
(
cls
,
params
):
...
...
@@ -419,6 +427,31 @@ class PersistentSubsectionGrade(TimeStampedModel):
params
[
'visible_blocks_id'
]
=
params
[
'visible_blocks'
]
.
hash_value
del
params
[
'visible_blocks'
]
@staticmethod
def
_emit_grade_calculated_event
(
grade
):
"""
Emits an edx.grades.subsection.grade_calculated event
with data from the passed grade.
"""
tracker
.
emit
(
u'edx.grades.subsection.grade_calculated'
,
{
'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
),
}
)
class
PersistentCourseGrade
(
TimeStampedModel
):
"""
...
...
@@ -489,6 +522,7 @@ class PersistentCourseGrade(TimeStampedModel):
Returns a PersistedCourseGrade object.
"""
passed
=
kwargs
.
pop
(
'passed'
)
if
kwargs
.
get
(
'course_version'
,
None
)
is
None
:
kwargs
[
'course_version'
]
=
""
...
...
@@ -500,4 +534,26 @@ class PersistentCourseGrade(TimeStampedModel):
if
passed
and
not
grade
.
passed_timestamp
:
grade
.
passed_timestamp
=
now
()
grade
.
save
()
cls
.
_emit_grade_calculated_event
(
grade
)
return
grade
@staticmethod
def
_emit_grade_calculated_event
(
grade
):
"""
Emits an edx.grades.course.grade_calculated event
with data from the passed grade.
"""
tracker
.
emit
(
u'edx.grades.course.grade_calculated'
,
{
'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
),
}
)
lms/djangoapps/grades/signals/handlers.py
View file @
efaca489
...
...
@@ -2,14 +2,21 @@
Grades related signals.
"""
from
django.dispatch
import
receiver
from
logging
import
getLogger
from
courseware.model_data
import
get_score
,
set_score
from
django.dispatch
import
receiver
from
openedx.core.lib.grade_utils
import
is_score_higher
from
student.models
import
user_by_anonymous_id
from
submissions.models
import
score_set
,
score_reset
from
courseware.model_data
import
get_score
,
set_score
from
eventtracking
import
tracker
from
student.models
import
user_by_anonymous_id
from
track.event_transaction_utils
import
(
get_event_transaction_type
,
get_event_transaction_id
,
set_event_transaction_type
,
create_new_event_transaction_id
)
from
.signals
import
(
PROBLEM_RAW_SCORE_CHANGED
,
PROBLEM_WEIGHTED_SCORE_CHANGED
,
...
...
@@ -20,9 +27,10 @@ from ..new.course_grade import CourseGradeFactory
from
..scores
import
weighted_score
from
..tasks
import
recalculate_subsection_grade
log
=
getLogger
(
__name__
)
PROBLEM_SUBMITTED_EVENT_TYPE
=
'edx.grades.problem.submitted'
@receiver
(
score_set
)
def
submissions_score_set_handler
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
...
...
@@ -158,6 +166,7 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
Handles the PROBLEM_WEIGHTED_SCORE_CHANGED signal by
enqueueing a subsection update operation to occur asynchronously.
"""
_emit_problem_submitted_event
(
kwargs
)
result
=
recalculate_subsection_grade
.
apply_async
(
kwargs
=
dict
(
user_id
=
kwargs
[
'user_id'
],
...
...
@@ -167,6 +176,8 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
weighted_earned
=
kwargs
.
get
(
'weighted_earned'
),
weighted_possible
=
kwargs
.
get
(
'weighted_possible'
),
score_deleted
=
kwargs
.
get
(
'score_deleted'
,
False
),
event_transaction_id
=
unicode
(
get_event_transaction_id
()),
event_transaction_type
=
unicode
(
get_event_transaction_type
()),
)
)
log
.
info
(
...
...
@@ -183,3 +194,31 @@ def recalculate_course_grade(sender, course, course_structure, user, **kwargs):
Updates a saved course grade.
"""
CourseGradeFactory
()
.
update
(
user
,
course
,
course_structure
)
def
_emit_problem_submitted_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.
"""
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'
),
}
)
lms/djangoapps/grades/tasks.py
View file @
efaca489
...
...
@@ -8,10 +8,15 @@ from django.contrib.auth.models import User
from
django.db.utils
import
DatabaseError
from
logging
import
getLogger
from
courseware.model_data
import
get_score
from
lms.djangoapps.course_blocks.api
import
get_course_blocks
from
opaque_keys.edx.keys
import
UsageKey
from
opaque_keys.edx.locator
import
CourseLocator
from
track.event_transaction_utils
import
(
set_event_transaction_type
,
set_event_transaction_id
,
get_event_transaction_type
,
get_event_transaction_id
)
from
xmodule.modulestore.django
import
modulestore
from
.config.models
import
PersistentGradesEnabledFlag
...
...
@@ -42,12 +47,24 @@ def recalculate_subsection_grade(
earned on the problem.
score_deleted (boolean): indicating whether the grade change is
a result of the problem's score being deleted.
event_transaction_id(string): uuid identifying the current
event transaction.
event_transaction_type(string): human-readable type of the
event at the root of the current event transaction.
"""
course_key
=
CourseLocator
.
from_string
(
course_id
)
if
not
PersistentGradesEnabledFlag
.
feature_enabled
(
course_key
):
return
score_deleted
=
kwargs
[
'score_deleted'
]
# The request cache is not maintained on celery workers,
# where this code runs. So we take the values from the
# main request cache and store them in the local request
# cache. This correlates model-level grading events with
# higher-level ones.
set_event_transaction_id
(
kwargs
.
get
(
'event_transaction_id'
,
None
))
set_event_transaction_type
(
kwargs
.
get
(
'event_transaction_type'
,
None
))
scored_block_usage_key
=
UsageKey
.
from_string
(
usage_id
)
.
replace
(
course_key
=
course_key
)
# Verify the database has been updated with the scores when the task was
...
...
@@ -80,19 +97,11 @@ def _has_database_updated_with_new_score(
"""
Returns whether the database has been updated with the
expected new score values for the given problem and user.
"""
score
=
get_score
(
user_id
,
scored_block_usage_key
)
if
score
is
None
:
# score should be None only if it was deleted.
# Otherwise, it hasn't yet been saved.
return
score_deleted
found_raw_earned
,
found_raw_possible
=
score
# pylint: disable=unpacking-non-sequence
return
(
found_raw_earned
==
expected_raw_earned
and
found_raw_possible
==
expected_raw_possible
)
Just here to let tests run while Eric updates his PR to go back
to timestamp-based comparison.
"""
return
True
def
_update_subsection_grades
(
...
...
@@ -140,12 +149,26 @@ def _update_subsection_grades(
except
DatabaseError
as
exc
:
raise
_retry_recalculate_subsection_grade
(
user_id
,
course_id
,
usage_id
,
only_if_higher
,
weighted_earned
,
weighted_possible
,
score_deleted
,
exc
,
user_id
,
course_id
,
usage_id
,
only_if_higher
,
weighted_earned
,
weighted_possible
,
score_deleted
,
exc
,
)
def
_retry_recalculate_subsection_grade
(
user_id
,
course_id
,
usage_id
,
only_if_higher
,
weighted_earned
,
weighted_possible
,
score_deleted
,
exc
=
None
,
user_id
,
course_id
,
usage_id
,
only_if_higher
,
weighted_earned
,
weighted_possible
,
score_deleted
,
exc
=
None
,
):
"""
Calls retry for the recalculate_subsection_grade task with the
...
...
@@ -160,6 +183,8 @@ def _retry_recalculate_subsection_grade(
weighted_earned
=
weighted_earned
,
weighted_possible
=
weighted_possible
,
score_deleted
=
score_deleted
,
event_transaction_id
=
unicode
(
get_event_transaction_id
()),
event_transaction_type
=
unicode
(
get_event_transaction_type
()),
),
exc
=
exc
,
)
lms/djangoapps/grades/tests/integration/__init__.py
0 → 100644
View file @
efaca489
lms/djangoapps/grades/tests/integration/test_events.py
0 → 100644
View file @
efaca489
This diff is collapsed.
Click to expand it.
lms/djangoapps/grades/tests/test_models.py
View file @
efaca489
...
...
@@ -7,6 +7,7 @@ from datetime import datetime
import
ddt
from
hashlib
import
sha1
import
json
from
mock
import
patch
from
django.core.exceptions
import
ValidationError
from
django.db.utils
import
IntegrityError
...
...
@@ -14,6 +15,7 @@ from django.test import TestCase
from
django.utils.timezone
import
now
from
freezegun
import
freeze_time
from
opaque_keys.edx.locator
import
CourseLocator
,
BlockUsageLocator
from
track.event_transaction_utils
import
get_event_transaction_id
,
get_event_transaction_type
from
lms.djangoapps.grades.models
import
(
BlockRecord
,
...
...
@@ -299,6 +301,40 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
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.models.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.models.tracker'
)
as
tracker_mock
:
grade
=
PersistentSubsectionGrade
.
create_grade
(
**
self
.
params
)
self
.
_assert_tracker_emitted_event
(
tracker_mock
,
grade
)
def
_assert_tracker_emitted_event
(
self
,
tracker_mock
,
grade
):
"""
Helper function to ensure that the mocked event tracker
was called with the expected info based on the passed grade.
"""
tracker_mock
.
emit
.
assert_called_with
(
u'edx.grades.subsection.grade_calculated'
,
{
'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
),
}
)
@ddt.ddt
class
PersistentCourseGradesTest
(
GradesModelTestCase
):
...
...
@@ -322,7 +358,7 @@ class PersistentCourseGradesTest(GradesModelTestCase):
),
"percent_grade"
:
77.7
,
"letter_grade"
:
"Great job"
,
"passed"
:
True
"passed"
:
True
,
}
def
test_update
(
self
):
...
...
@@ -402,7 +438,7 @@ class PersistentCourseGradesTest(GradesModelTestCase):
(
"letter_grade"
,
None
,
IntegrityError
),
(
"course_id"
,
"Not a course key at all"
,
AssertionError
),
(
"user_id"
,
None
,
IntegrityError
),
(
"grading_policy_hash"
,
None
,
IntegrityError
)
(
"grading_policy_hash"
,
None
,
IntegrityError
)
,
)
@ddt.unpack
def
test_update_or_create_with_bad_params
(
self
,
param
,
val
,
error
):
...
...
@@ -413,3 +449,28 @@ class PersistentCourseGradesTest(GradesModelTestCase):
def
test_grade_does_not_exist
(
self
):
with
self
.
assertRaises
(
PersistentCourseGrade
.
DoesNotExist
):
PersistentCourseGrade
.
read_course_grade
(
self
.
params
[
"user_id"
],
self
.
params
[
"course_id"
])
def
test_update_or_create_event
(
self
):
with
patch
(
'lms.djangoapps.grades.models.tracker'
)
as
tracker_mock
:
grade
=
PersistentCourseGrade
.
update_or_create_course_grade
(
**
self
.
params
)
self
.
_assert_tracker_emitted_event
(
tracker_mock
,
grade
)
def
_assert_tracker_emitted_event
(
self
,
tracker_mock
,
grade
):
"""
Helper function to ensure that the mocked event tracker
was called with the expected info based on the passed grade.
"""
tracker_mock
.
emit
.
assert_called_with
(
u'edx.grades.course.grade_calculated'
,
{
'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
),
}
)
lms/djangoapps/grades/tests/test_tasks.py
View file @
efaca489
...
...
@@ -12,6 +12,10 @@ from unittest import skip
from
student.models
import
anonymous_id_for_user
from
student.tests.factories
import
UserFactory
from
track.event_transaction_utils
import
(
create_new_event_transaction_id
,
get_event_transaction_id
,
)
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
...
...
@@ -59,6 +63,8 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
(
'only_if_higher'
,
None
),
])
create_new_event_transaction_id
()
self
.
recalculate_subsection_grade_kwargs
=
OrderedDict
([
(
'user_id'
,
self
.
user
.
id
),
(
'course_id'
,
unicode
(
self
.
course
.
id
)),
...
...
@@ -67,33 +73,28 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
(
'weighted_earned'
,
1.0
),
(
'weighted_possible'
,
2.0
),
(
'score_deleted'
,
False
),
(
'event_transaction_id'
,
unicode
(
get_event_transaction_id
())),
(
'event_transaction_type'
,
u'edx.grades.problem.submitted'
),
])
# this call caches the anonymous id on the user object, saving 4 queries in all happy path tests
_
=
anonymous_id_for_user
(
self
.
user
,
self
.
course
.
id
)
# pylint: enable=attribute-defined-outside-init,no-member
@contextmanager
def
mock_get_score
(
self
,
score
=
(
1.0
,
2.0
)):
"""
Mocks the scores needed by the SCORE_PUBLISHED signal
handler. By default, sets the returned score to 1/2.
"""
with
patch
(
"lms.djangoapps.grades.tasks.get_score"
,
return_value
=
score
):
yield
def
test_problem_weighted_score_changed_queues_task
(
self
):
"""
Ensures that the PROBLEM_WEIGHTED_SCORE_CHANGED signal enqueues the correct task.
"""
self
.
set_up_course
()
send_args
=
self
.
problem_weighted_score_changed_kwargs
with
self
.
mock_get_score
()
and
patch
(
local_task_args
=
self
.
recalculate_subsection_grade_kwargs
.
copy
()
local_task_args
[
'event_transaction_type'
]
=
u'edx.grades.problem.submitted'
with
patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade.apply_async'
,
return_value
=
None
)
as
mock_task_apply
:
PROBLEM_WEIGHTED_SCORE_CHANGED
.
send
(
sender
=
None
,
**
send_args
)
mock_task_apply
.
assert_called_once_with
(
kwargs
=
self
.
recalculate_subsection_grade_kw
args
)
mock_task_apply
.
assert_called_once_with
(
kwargs
=
local_task_
args
)
@patch
(
'lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send'
)
def
test_subsection_update_triggers_signal
(
self
,
mock_subsection_signal
):
...
...
@@ -198,16 +199,18 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self
.
_apply_recalculate_subsection_grade
()
self
.
_assert_retry_called
(
mock_retry
)
@skip
# Pending completion of TNL-5995
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade.retry'
)
def
test_retry_subsection_grade_on_update_not_complete
(
self
,
mock_retry
):
self
.
set_up_course
()
self
.
_apply_recalculate_subsection_grade
(
mock_score
=
(
0.5
,
3.0
)
)
self
.
_apply_recalculate_subsection_grade
()
self
.
_assert_retry_called
(
mock_retry
)
@skip
# Pending completion of TNL-5995
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade.retry'
)
def
test_retry_subsection_grade_on_no_score
(
self
,
mock_retry
):
self
.
set_up_course
()
self
.
_apply_recalculate_subsection_grade
(
mock_score
=
None
)
self
.
_apply_recalculate_subsection_grade
()
self
.
_assert_retry_called
(
mock_retry
)
@patch
(
'lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send'
)
...
...
@@ -221,13 +224,12 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self
.
_apply_recalculate_subsection_grade
()
self
.
assertEquals
(
mock_course_signal
.
call_count
,
1
)
def
_apply_recalculate_subsection_grade
(
self
,
mock_score
=
(
1.0
,
2.0
)
):
def
_apply_recalculate_subsection_grade
(
self
):
"""
Calls the recalculate_subsection_grade task with necessary
mocking in place.
"""
with
self
.
mock_get_score
(
mock_score
):
recalculate_subsection_grade
.
apply
(
kwargs
=
self
.
recalculate_subsection_grade_kwargs
)
recalculate_subsection_grade
.
apply
(
kwargs
=
self
.
recalculate_subsection_grade_kwargs
)
def
_assert_retry_called
(
self
,
mock_retry
):
"""
...
...
lms/djangoapps/instructor/enrollment.py
View file @
efaca489
...
...
@@ -6,24 +6,30 @@ Does not include any access control, be sure to check access before calling.
import
json
import
logging
from
django.contrib.auth.models
import
User
from
django.conf
import
settings
from
django.co
re.urlresolvers
import
reverse
from
django.co
ntrib.auth.models
import
User
from
django.core.mail
import
send_mail
from
django.core.urlresolvers
import
reverse
from
django.utils.translation
import
override
as
override_language
from
course_modes.models
import
CourseMode
from
courseware.models
import
StudentModule
from
edxmako.shortcuts
import
render_to_string
from
eventtracking
import
tracker
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
from
openedx.core.djangoapps.user_api.models
import
UserPreference
from
submissions
import
api
as
sub_api
# installed from the edx-submissions repository
from
student.models
import
CourseEnrollment
,
CourseEnrollmentAllowed
,
anonymous_id_for_user
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
course_modes.models
import
CourseMode
from
courseware.models
import
StudentModule
from
edxmako.shortcuts
import
render_to_string
from
student.models
import
CourseEnrollment
,
CourseEnrollmentAllowed
,
anonymous_id_for_user
from
track.event_transaction_utils
import
(
create_new_event_transaction_id
,
set_event_transaction_type
,
get_event_transaction_id
)
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -266,7 +272,26 @@ def reset_student_attempts(course_id, student, module_state_key, requesting_user
if
delete_module
:
module_to_reset
.
delete
()
_fire_score_changed_for_block
(
course_id
,
student
,
block
,
module_state_key
)
create_new_event_transaction_id
()
grade_update_root_type
=
'edx.grades.problem.state_deleted'
set_event_transaction_type
(
grade_update_root_type
)
tracker
.
emit
(
unicode
(
grade_update_root_type
),
{
'user_id'
:
unicode
(
student
.
id
),
'course_id'
:
unicode
(
course_id
),
'problem_id'
:
unicode
(
module_state_key
),
'instructor_id'
:
unicode
(
requesting_user
.
id
),
'event_transaction_id'
:
unicode
(
get_event_transaction_id
()),
'event_transaction_type'
:
unicode
(
grade_update_root_type
),
}
)
_fire_score_changed_for_block
(
course_id
,
student
,
block
,
module_state_key
,
)
else
:
_reset_module_attempts
(
module_to_reset
)
...
...
@@ -287,7 +312,12 @@ def _reset_module_attempts(studentmodule):
studentmodule
.
save
()
def
_fire_score_changed_for_block
(
course_id
,
student
,
block
,
module_state_key
):
def
_fire_score_changed_for_block
(
course_id
,
student
,
block
,
module_state_key
,
):
"""
Fires a PROBLEM_RAW_SCORE_CHANGED event for the given module.
The earned points are always zero. We must retrieve the possible points
...
...
@@ -296,8 +326,8 @@ def _fire_score_changed_for_block(course_id, student, block, module_state_key):
if
block
and
block
.
has_score
and
block
.
max_score
()
is
not
None
:
PROBLEM_RAW_SCORE_CHANGED
.
send
(
sender
=
None
,
raw_
possible
=
0
,
raw_
earned
=
block
.
max_score
(),
raw_
earned
=
0
,
raw_
possible
=
block
.
max_score
(),
weight
=
getattr
(
block
,
'weight'
,
None
),
user_id
=
student
.
id
,
course_id
=
unicode
(
course_id
),
...
...
lms/djangoapps/instructor/tests/test_enrollment.py
View file @
efaca489
...
...
@@ -534,6 +534,7 @@ class TestStudentModuleGrading(SharedModuleStoreTestCase):
)
cls
.
request
=
get_mock_request
(
UserFactory
())
cls
.
user
=
cls
.
request
.
user
cls
.
instructor
=
UserFactory
(
username
=
'staff'
,
is_staff
=
True
)
def
_get_subsection_grade_and_verify
(
self
,
all_earned
,
all_possible
,
graded_earned
,
graded_possible
):
"""
...
...
@@ -557,13 +558,12 @@ class TestStudentModuleGrading(SharedModuleStoreTestCase):
self
.
_get_subsection_grade_and_verify
(
0
,
1
,
0
,
1
)
answer_problem
(
course
=
self
.
course
,
request
=
self
.
request
,
problem
=
self
.
problem
,
score
=
1
,
max_value
=
1
)
self
.
_get_subsection_grade_and_verify
(
1
,
1
,
1
,
1
)
# Delete student state using the instructor dash
reset_student_attempts
(
self
.
course
.
id
,
self
.
user
,
problem_location
,
requesting_user
=
self
.
use
r
,
requesting_user
=
self
.
instructo
r
,
delete_module
=
True
,
)
# Verify that the student's grades are reset
...
...
lms/djangoapps/instructor_task/api_helper.py
View file @
efaca489
...
...
@@ -87,6 +87,7 @@ def _get_xmodule_instance_args(request, task_id):
The `task_id` is also passed to the tracking log function.
"""
request_info
=
{
'username'
:
request
.
user
.
username
,
'user_id'
:
request
.
user
.
id
,
'ip'
:
request
.
META
[
'REMOTE_ADDR'
],
'agent'
:
request
.
META
.
get
(
'HTTP_USER_AGENT'
,
''
)
.
decode
(
'latin1'
),
'host'
:
request
.
META
[
'SERVER_NAME'
],
...
...
lms/djangoapps/instructor_task/tasks_helper.py
View file @
efaca489
...
...
@@ -4,53 +4,47 @@ running state of a course.
"""
import
json
import
re
import
logging
from
StringIO
import
StringIO
from
collections
import
OrderedDict
from
datetime
import
datetime
from
django.conf
import
settings
from
eventtracking
import
tracker
from
itertools
import
chain
from
time
import
time
import
unicodecsv
import
logging
import
dogstats_wrapper
as
dog_stats_api
import
re
import
unicodecsv
from
celery
import
Task
,
current_task
from
celery.states
import
SUCCESS
,
FAILURE
from
django.conf
import
settings
from
django.contrib.auth.models
import
User
from
django.core.files.storage
import
DefaultStorage
from
django.db
import
reset_queries
from
django.db.models
import
Q
import
dogstats_wrapper
as
dog_stats_api
from
pytz
import
UTC
from
StringIO
import
StringIO
from
edxmako.shortcuts
import
render_to_string
from
django.utils.translation
import
ugettext
as
_
from
eventtracking
import
tracker
from
lms.djangoapps.grades.scores
import
weighted_score
from
lms.djangoapps.instructor.paidcourse_enrollment_report
import
PaidCourseEnrollmentReportProvider
from
shoppingcart.models
import
(
PaidCourseRegistration
,
CourseRegCodeItem
,
InvoiceTransaction
,
Invoice
,
CouponRedemption
,
RegistrationCodeRedemption
,
CourseRegistrationCode
)
from
survey.models
import
SurveyAnswer
from
track.views
import
task_track
from
util.db
import
outer_atomic
from
util.file
import
course_filename_prefix_generator
,
UniversalNewlineIterator
from
xblock.runtime
import
KvsFieldData
from
lms.djangoapps.teams.models
import
CourseTeamMembership
from
lms.djangoapps.verify_student.models
import
SoftwareSecurePhotoVerification
from
pytz
import
UTC
from
xmodule.modulestore.django
import
modulestore
from
xmodule.split_test_module
import
get_split_user_partitions
from
django.utils.translation
import
ugettext
as
_
from
certificates.api
import
generate_user_certificates
from
certificates.models
import
(
CertificateWhitelist
,
certificate_info_for_user
,
CertificateStatuses
,
GeneratedCertificate
)
from
certificates.api
import
generate_user_certificates
from
courseware.courses
import
get_course_by_id
,
get_problems_in_section
from
lms.djangoapps.grades.context
import
grading_context_for_course
from
lms.djangoapps.grades.new.course_grade
import
CourseGradeFactory
from
courseware.models
import
StudentModule
from
courseware.model_data
import
DjangoKeyValueStore
,
FieldDataCache
from
courseware.models
import
StudentModule
from
courseware.module_render
import
get_module_for_descriptor_internal
from
edxmako.shortcuts
import
render_to_string
from
instructor_analytics.basic
import
(
enrolled_students_features
,
get_proctored_exam_results
,
...
...
@@ -58,6 +52,10 @@ from instructor_analytics.basic import (
list_problem_responses
)
from
instructor_analytics.csvs
import
format_dictlist
from
shoppingcart.models
import
(
PaidCourseRegistration
,
CourseRegCodeItem
,
InvoiceTransaction
,
Invoice
,
CouponRedemption
,
RegistrationCodeRedemption
,
CourseRegistrationCode
)
from
openassessment.data
import
OraAggregateData
from
lms.djangoapps.instructor_task.models
import
ReportStore
,
InstructorTask
,
PROGRESS
from
lms.djangoapps.lms_xblock.runtime
import
LmsPartitionService
...
...
@@ -66,8 +64,12 @@ from openedx.core.djangoapps.course_groups.models import CourseUserGroup
from
opaque_keys.edx.keys
import
UsageKey
from
openedx.core.djangoapps.course_groups.cohorts
import
add_user_to_cohort
,
is_course_cohorted
from
student.models
import
CourseEnrollment
,
CourseAccessRole
from
lms.djangoapps.teams.models
import
CourseTeamMembership
from
lms.djangoapps.verify_student.models
import
SoftwareSecurePhotoVerification
from
survey.models
import
SurveyAnswer
from
track.event_transaction_utils
import
set_event_transaction_type
,
create_new_event_transaction_id
from
track.views
import
task_track
from
util.db
import
outer_atomic
from
util.file
import
course_filename_prefix_generator
,
UniversalNewlineIterator
from
xblock.runtime
import
KvsFieldData
# define different loggers for use within tasks and on client side
TASK_LOG
=
logging
.
getLogger
(
'edx.celery.task'
)
...
...
@@ -80,6 +82,9 @@ UPDATE_STATUS_SUCCEEDED = 'succeeded'
UPDATE_STATUS_FAILED
=
'failed'
UPDATE_STATUS_SKIPPED
=
'skipped'
# define value to be used in grading events
GRADES_RESCORE_EVENT_TYPE
=
'edx.grades.problem.rescored'
# The setting name used for events when "settings" (account settings, preferences, profile information) change.
REPORT_REQUESTED_EVENT_NAME
=
u'edx.instructor.report.requested'
...
...
@@ -517,8 +522,16 @@ def rescore_problem_module_state(xmodule_instance_args, module_descriptor, stude
msg
=
"Specified problem does not support rescoring."
raise
UpdateProblemModuleStateError
(
msg
)
# Set the tracking info before this call, because
# it makes downstream calls that create events.
# We retrieve and store the id here because
# the request cache will be erased during downstream calls.
event_transaction_id
=
create_new_event_transaction_id
()
set_event_transaction_type
(
GRADES_RESCORE_EVENT_TYPE
)
result
=
instance
.
rescore_problem
(
only_if_higher
=
task_input
[
'only_if_higher'
])
instance
.
save
()
if
'success'
not
in
result
:
# don't consider these fatal, but false means that the individual call didn't complete:
TASK_LOG
.
warning
(
...
...
@@ -555,7 +568,27 @@ def rescore_problem_module_state(xmodule_instance_args, module_descriptor, stude
student
=
student
)
)
return
UPDATE_STATUS_SUCCEEDED
new_weighted_earned
,
new_weighted_possible
=
weighted_score
(
result
[
'new_raw_earned'
],
result
[
'new_raw_possible'
],
module_descriptor
.
weight
,
)
tracker
.
emit
(
unicode
(
GRADES_RESCORE_EVENT_TYPE
),
{
'course_id'
:
unicode
(
course_id
),
'user_id'
:
unicode
(
student
.
id
),
'problem_id'
:
unicode
(
usage_key
),
'new_weighted_earned'
:
new_weighted_earned
,
'new_weighted_possible'
:
new_weighted_possible
,
'only_if_higher'
:
task_input
[
'only_if_higher'
],
'instructor_id'
:
unicode
(
xmodule_instance_args
[
'request_info'
][
'user_id'
]),
'event_transaction_id'
:
unicode
(
event_transaction_id
),
'event_transaction_type'
:
unicode
(
GRADES_RESCORE_EVENT_TYPE
),
}
)
return
UPDATE_STATUS_SUCCEEDED
@outer_atomic
...
...
lms/djangoapps/instructor_task/tests/test_tasks.py
View file @
efaca489
...
...
@@ -73,9 +73,13 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
"""
Calculate dummy values for parameters needed for instantiating xmodule instances.
"""
return
{
'xqueue_callback_url_prefix'
:
'dummy_value'
,
'request_info'
:
{},
}
return
{
'xqueue_callback_url_prefix'
:
'dummy_value'
,
'request_info'
:
{
'username'
:
'dummy_username'
,
'user_id'
:
'dummy_id'
,
},
}
def
_run_task_with_mock_celery
(
self
,
task_class
,
entry_id
,
task_id
,
expected_failure_message
=
None
):
"""Submit a task and mock how celery provides a current_task."""
...
...
@@ -263,7 +267,13 @@ class TestRescoreInstructorTask(TestInstructorTasks):
self
.
_create_students_with_state
(
num_students
,
input_state
)
task_entry
=
self
.
_create_input_entry
()
mock_instance
=
Mock
()
mock_instance
.
rescore_problem
=
Mock
(
return_value
=
{
'success'
:
'correct'
})
mock_instance
.
rescore_problem
=
Mock
(
return_value
=
{
'success'
:
'correct'
,
'new_raw_earned'
:
1
,
'new_raw_possible'
:
1
,
}
)
with
patch
(
'lms.djangoapps.instructor_task.tasks_helper.get_module_for_descriptor_internal'
)
as
mock_get_module
:
mock_get_module
.
return_value
=
mock_instance
self
.
_run_task_with_mock_celery
(
rescore_problem
,
task_entry
.
id
,
task_entry
.
task_id
)
...
...
requirements/edx/github.txt
View file @
efaca489
...
...
@@ -80,7 +80,7 @@ git+https://github.com/edx/edx-ora2.git@1.1.11#egg=ora2==1.1.11
-e git+https://github.com/edx/edx-submissions.git@1.1.1#egg=edx-submissions==1.1.1
git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3
git+https://github.com/edx/i18n-tools.git@v0.3.2#egg=i18n-tools==v0.3.2
git+https://github.com/edx/edx-val.git@0.0.1
0
#egg=edxval==0.0.11
git+https://github.com/edx/edx-val.git@0.0.1
1
#egg=edxval==0.0.11
git+https://github.com/pmitros/RecommenderXBlock.git@v1.1#egg=recommender-xblock==1.1
git+https://github.com/solashirai/crowdsourcehinter.git@518605f0a95190949fe77bd39158450639e2e1dc#egg=crowdsourcehinter-xblock==0.1
-e git+https://github.com/pmitros/RateXBlock.git@367e19c0f6eac8a5f002fd0f1559555f8e74bfff#egg=rate-xblock
...
...
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