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
50d55bba
Commit
50d55bba
authored
8 years ago
by
Eric Fischer
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'release' into release-10-06-conflict
parents
e24915d2
80cbb59f
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
487 additions
and
141 deletions
+487
-141
common/djangoapps/third_party_auth/management/commands/saml.py
+0
-4
common/test/acceptance/pages/lms/staff_view.py
+2
-2
common/test/acceptance/tests/lms/test_progress_page.py
+24
-0
lms/djangoapps/course_api/blocks/transformers/tests/test_milestones.py
+21
-22
lms/djangoapps/gating/api.py
+58
-33
lms/djangoapps/gating/signals.py
+8
-9
lms/djangoapps/gating/tests/test_api.py
+0
-0
lms/djangoapps/gating/tests/test_signals.py
+15
-9
lms/djangoapps/grades/module_grades.py
+96
-0
lms/djangoapps/grades/new/subsection_grade.py
+9
-6
lms/djangoapps/grades/scores.py
+21
-15
lms/djangoapps/grades/signals/handlers.py
+4
-8
lms/djangoapps/grades/signals/signals.py
+1
-13
lms/djangoapps/grades/tests/test_grades.py
+197
-1
lms/djangoapps/grades/tests/test_signals.py
+1
-1
lms/djangoapps/grades/tests/utils.py
+5
-6
lms/djangoapps/grades/transformer.py
+8
-2
lms/djangoapps/instructor/enrollment.py
+9
-5
lms/djangoapps/instructor/tests/test_api.py
+2
-1
lms/djangoapps/instructor/tests/test_enrollment.py
+4
-3
lms/djangoapps/instructor/tests/test_services.py
+2
-1
No files found.
common/djangoapps/third_party_auth/management/commands/saml.py
View file @
50d55bba
...
...
@@ -4,7 +4,6 @@ Management commands for third_party_auth
"""
from
django.core.management.base
import
BaseCommand
,
CommandError
import
logging
from
third_party_auth.models
import
SAMLConfiguration
from
third_party_auth.tasks
import
fetch_saml_metadata
...
...
@@ -16,9 +15,6 @@ class Command(BaseCommand):
parser
.
add_argument
(
'--pull'
,
action
=
'store_true'
,
help
=
"Pull updated metadata from external IDPs"
)
def
handle
(
self
,
*
args
,
**
options
):
if
not
SAMLConfiguration
.
is_enabled
():
raise
CommandError
(
"SAML support is disabled via SAMLConfiguration."
)
if
options
[
'pull'
]:
log_handler
=
logging
.
StreamHandler
(
self
.
stdout
)
log_handler
.
setLevel
(
logging
.
DEBUG
)
...
...
This diff is collapsed.
Click to expand it.
common/test/acceptance/pages/lms/staff_view.py
View file @
50d55bba
...
...
@@ -95,8 +95,8 @@ class StaffDebugPage(PageObject):
This delete's a student's state for the problem
"""
if
user
:
self
.
q
(
css
=
'input[id^=sd_fu_]'
)
.
fill
(
user
)
self
.
q
(
css
=
'.staff-modal .staff-debug-sdelete'
)
.
click
()
self
.
q
(
css
=
'input[id^=sd_fu_]'
)
.
fi
rst
.
fi
ll
(
user
)
self
.
q
(
css
=
'.staff-modal .staff-debug-sdelete'
)
.
first
.
click
()
def
rescore
(
self
,
user
=
None
):
"""
...
...
This diff is collapsed.
Click to expand it.
common/test/acceptance/tests/lms/test_progress_page.py
View file @
50d55bba
...
...
@@ -15,6 +15,7 @@ from ...pages.lms.courseware import CoursewarePage
from
...pages.lms.instructor_dashboard
import
InstructorDashboardPage
from
...pages.lms.problem
import
ProblemPage
from
...pages.lms.progress
import
ProgressPage
from
...pages.lms.staff_view
import
StaffPage
,
StaffDebugPage
from
...pages.studio.component_editor
import
ComponentEditorView
from
...pages.studio.utils
import
type_in_codemirror
from
...pages.studio.overview
import
CourseOutlinePage
...
...
@@ -192,6 +193,22 @@ class PersistentGradesTest(ProgressPageBaseTest):
type_in_codemirror
(
self
,
0
,
modified_content
)
modal
.
q
(
css
=
'.action-save'
)
.
click
()
def
_delete_student_state_for_problem
(
self
):
"""
As staff, clicks the "delete student state" button,
deleting the student user's state for the problem.
"""
with
self
.
_logged_in_session
(
staff
=
True
):
self
.
courseware_page
.
visit
()
staff_page
=
StaffPage
(
self
.
browser
,
self
.
course_id
)
self
.
assertEqual
(
staff_page
.
staff_view_mode
,
"Staff"
)
staff_page
.
q
(
css
=
'a.instructor-info-action'
)
.
nth
(
1
)
.
click
()
staff_debug_page
=
StaffDebugPage
(
self
.
browser
)
staff_debug_page
.
wait_for_page
()
staff_debug_page
.
delete_state
(
self
.
USERNAME
)
msg
=
staff_debug_page
.
idash_msg
[
0
]
self
.
assertEqual
(
u'Successfully deleted student state for user {0}'
.
format
(
self
.
USERNAME
),
msg
)
@ddt.data
(
_edit_problem_content
,
_change_subsection_structure
,
...
...
@@ -223,6 +240,13 @@ class PersistentGradesTest(ProgressPageBaseTest):
self
.
assertEqual
(
self
.
_get_problem_scores
(),
[(
1
,
1
),
(
0
,
1
)])
self
.
assertEqual
(
self
.
_get_section_score
(),
(
1
,
2
))
def
test_progress_page_updates_when_student_state_deleted
(
self
):
self
.
_check_progress_page_with_scored_problem
()
self
.
_delete_student_state_for_problem
()
with
self
.
_logged_in_session
():
self
.
assertEqual
(
self
.
_get_problem_scores
(),
[(
0
,
1
),
(
0
,
1
)])
self
.
assertEqual
(
self
.
_get_section_score
(),
(
0
,
2
))
class
SubsectionGradingPolicyTest
(
ProgressPageBaseTest
):
"""
...
...
This diff is collapsed.
Click to expand it.
lms/djangoapps/course_api/blocks/transformers/tests/test_milestones.py
View file @
50d55bba
"""
Tests for ProctoredExamTransformer.
"""
from
mock
import
patch
from
mock
import
patch
,
Mock
from
nose.plugins.attrib
import
attr
import
ddt
...
...
@@ -53,8 +53,7 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
'course'
,
'A'
,
'B'
,
'C'
,
'ProctoredExam'
,
'D'
,
'E'
,
'PracticeExam'
,
'F'
,
'G'
,
'H'
,
'I'
,
'TimedExam'
,
'J'
,
'K'
)
# The special exams (proctored, practice, timed) are not visible to
# students via the Courses API.
# The special exams (proctored, practice, timed) should never be visible to students
ALL_BLOCKS_EXCEPT_SPECIAL
=
(
'course'
,
'A'
,
'B'
,
'C'
,
'H'
,
'I'
)
def
get_course_hierarchy
(
self
):
...
...
@@ -135,27 +134,27 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
(
'H'
,
'A'
,
(
'course'
,
'A'
,
'B'
,
'C'
),
'B'
,
(
'course'
,
'A'
,
'B'
,
'C'
,)
),
(
'H'
,
'ProctoredExam'
,
'D'
,
(
'course'
,
'A'
,
'B'
,
'C'
),
),
)
@ddt.unpack
def
test_gated
(
self
,
gated_block_ref
,
gating_block_ref
,
expected_blocks_before_completion
):
def
test_gated
(
self
,
gated_block_ref
,
gating_block_ref
,
gating_block_child
,
expected_blocks_before_completion
):
"""
First, checks that a student cannot see the gated block when it is gated
by the gating block and no attempt has been made to complete the gating
block. Then, checks that the student can see the gated block after the
gating block has been completed.
First, checks that a student cannot see the gated block when it is gated by the gating block and no
attempt has been made to complete the gating block.
Then, checks that the student can see the gated block after the gating block has been completed.
expected_blocks_before_completion is the set of blocks we expect to be
visible to the student
before the student has completed the gating block.
expected_blocks_before_completion is the set of blocks we expect to be
visible to the student
before the student has completed the gating block.
The test data includes one special exam and one non-special block as the
gating blocks.
The test data includes one special exam and one non-special block as the gating blocks.
"""
self
.
course
.
enable_subsection_gating
=
True
self
.
setup_gated_section
(
self
.
blocks
[
gated_block_ref
],
self
.
blocks
[
gating_block_ref
])
...
...
@@ -166,16 +165,16 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
# clear the request cache to simulate a new request
self
.
clear_caches
()
# this call triggers reevaluation of prerequisites fulfilled by the
# gating block.
lms_gating_api
.
evaluate_prerequisite
(
self
.
course
,
self
.
user
,
self
.
blocks
[
gating_block_ref
]
.
location
,
100.0
,
)
# mock the api that the lms gating api calls to get the score for each block to always return 1 (ie 100%)
with
patch
(
'gating.api.get_module_score'
,
Mock
(
return_value
=
1
)):
with
self
.
assertNumQueries
(
3
):
# this call triggers reevaluation of prerequisites fulfilled by the parent of the
# block passed in, so we pass in a child of the gating block
lms_gating_api
.
evaluate_prerequisite
(
self
.
course
,
UsageKey
.
from_string
(
unicode
(
self
.
blocks
[
gating_block_child
]
.
location
)),
self
.
user
.
id
)
with
self
.
assertNumQueries
(
2
):
self
.
get_blocks_and_check_against_expected
(
self
.
user
,
self
.
ALL_BLOCKS_EXCEPT_SPECIAL
)
def
test_staff_access
(
self
):
...
...
This diff is collapsed.
Click to expand it.
lms/djangoapps/gating/api.py
View file @
50d55bba
...
...
@@ -6,14 +6,35 @@ import json
from
collections
import
defaultdict
from
django.contrib.auth.models
import
User
from
xmodule.modulestore.django
import
modulestore
from
openedx.core.lib.gating
import
api
as
gating_api
from
lms.djangoapps.grades.module_grades
import
get_module_score
from
util
import
milestones_helpers
log
=
logging
.
getLogger
(
__name__
)
def
_get_xblock_parent
(
xblock
,
category
=
None
):
"""
Returns the parent of the given XBlock. If an optional category is supplied,
traverses the ancestors of the XBlock and returns the first with the
given category.
Arguments:
xblock (XBlock): Get the parent of this XBlock
category (str): Find an ancestor with this category (e.g. sequential)
"""
parent
=
xblock
.
get_parent
()
if
parent
and
category
:
if
parent
.
category
==
category
:
return
parent
else
:
return
_get_xblock_parent
(
parent
,
category
)
return
parent
@gating_api.gating_enabled
(
default
=
False
)
def
evaluate_prerequisite
(
course
,
user
,
subsection_usage_key
,
new_score
):
def
evaluate_prerequisite
(
course
,
prereq_content_key
,
user_id
):
"""
Finds the parent subsection of the content in the course and evaluates
any milestone relationships attached to that subsection. If the calculated
...
...
@@ -21,40 +42,44 @@ def evaluate_prerequisite(course, user, subsection_usage_key, new_score):
dependent subsections, the related milestone will be fulfilled for the user.
Arguments:
user
(User):
User for which evaluation should occur
user
_id (int): ID of
User for which evaluation should occur
course (CourseModule): The course
subsection_usage_key (UsageKey): Usage key of the updated subsection
new_score (float): New score of the given subsection, in percentage.
prereq_content_key (UsageKey): The prerequisite content usage key
Returns:
None
"""
prereq_milestone
=
gating_api
.
get_gating_milestone
(
course
.
id
,
subsection_usage_key
,
'fulfills'
)
if
prereq_milestone
:
gated_content_milestones
=
defaultdict
(
list
)
for
milestone
in
gating_api
.
find_gating_milestones
(
course
.
id
,
None
,
'requires'
):
gated_content_milestones
[
milestone
[
'id'
]]
.
append
(
milestone
)
gated_content
=
gated_content_milestones
.
get
(
prereq_milestone
[
'id'
])
if
gated_content
:
for
milestone
in
gated_content
:
# Default minimum score to 100
min_score
=
100.0
requirements
=
milestone
.
get
(
'requirements'
)
if
requirements
:
try
:
min_score
=
float
(
requirements
.
get
(
'min_score'
))
except
(
ValueError
,
TypeError
):
log
.
warning
(
'Failed to find minimum score for gating milestone
%
s, defaulting to 100'
,
json
.
dumps
(
milestone
)
)
if
new_score
>=
min_score
:
milestones_helpers
.
add_user_milestone
({
'id'
:
user
.
id
},
prereq_milestone
)
else
:
milestones_helpers
.
remove_user_milestone
({
'id'
:
user
.
id
},
prereq_milestone
)
xblock
=
modulestore
()
.
get_item
(
prereq_content_key
)
sequential
=
_get_xblock_parent
(
xblock
,
'sequential'
)
if
sequential
:
prereq_milestone
=
gating_api
.
get_gating_milestone
(
course
.
id
,
sequential
.
location
.
for_branch
(
None
),
'fulfills'
)
if
prereq_milestone
:
gated_content_milestones
=
defaultdict
(
list
)
for
milestone
in
gating_api
.
find_gating_milestones
(
course
.
id
,
None
,
'requires'
):
gated_content_milestones
[
milestone
[
'id'
]]
.
append
(
milestone
)
gated_content
=
gated_content_milestones
.
get
(
prereq_milestone
[
'id'
])
if
gated_content
:
user
=
User
.
objects
.
get
(
id
=
user_id
)
score
=
get_module_score
(
user
,
course
,
sequential
)
*
100
for
milestone
in
gated_content
:
# Default minimum score to 100
min_score
=
100
requirements
=
milestone
.
get
(
'requirements'
)
if
requirements
:
try
:
min_score
=
int
(
requirements
.
get
(
'min_score'
))
except
(
ValueError
,
TypeError
):
log
.
warning
(
'Failed to find minimum score for gating milestone
%
s, defaulting to 100'
,
json
.
dumps
(
milestone
)
)
if
score
>=
min_score
:
milestones_helpers
.
add_user_milestone
({
'id'
:
user_id
},
prereq_milestone
)
else
:
milestones_helpers
.
remove_user_milestone
({
'id'
:
user_id
},
prereq_milestone
)
This diff is collapsed.
Click to expand it.
lms/djangoapps/gating/signals.py
View file @
50d55bba
...
...
@@ -2,12 +2,14 @@
Signal handlers for the gating djangoapp
"""
from
django.dispatch
import
receiver
from
lms.djangoapps.grades.signals.signals
import
SUBSECTION_SCORE_UPDATED
from
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
from
xmodule.modulestore.django
import
modulestore
from
lms.djangoapps.grades.signals.signals
import
SCORE_CHANGED
from
gating
import
api
as
gating_api
@receiver
(
S
UBSECTION_SCORE_UPDAT
ED
)
def
handle_s
ubsection_score_updat
ed
(
**
kwargs
):
@receiver
(
S
CORE_CHANG
ED
)
def
handle_s
core_chang
ed
(
**
kwargs
):
"""
Receives the SCORE_CHANGED signal sent by LMS when a student's score has changed
for a given component and triggers the evaluation of any milestone relationships
...
...
@@ -19,13 +21,10 @@ def handle_subsection_score_updated(**kwargs):
Returns:
None
"""
course
=
kwargs
[
'course'
]
course
=
modulestore
()
.
get_course
(
CourseKey
.
from_string
(
kwargs
.
get
(
'course_id'
)))
if
course
.
enable_subsection_gating
:
subsection_grade
=
kwargs
[
'subsection_grade'
]
new_score
=
subsection_grade
.
graded_total
.
earned
/
subsection_grade
.
graded_total
.
possible
*
100.0
gating_api
.
evaluate_prerequisite
(
course
,
kwargs
[
'user'
],
subsection_grade
.
location
,
new_score
,
UsageKey
.
from_string
(
kwargs
.
get
(
'usage_id'
)),
kwargs
.
get
(
'user'
)
.
id
,
)
This diff is collapsed.
Click to expand it.
lms/djangoapps/gating/tests/test_api.py
View file @
50d55bba
This diff is collapsed.
Click to expand it.
lms/djangoapps/gating/tests/test_signals.py
View file @
50d55bba
"""
Unit tests for gating.signals module
"""
from
mock
import
patch
,
MagicMock
from
mock
import
patch
from
opaque_keys.edx.keys
import
UsageKey
from
student.tests.factories
import
UserFactory
...
...
@@ -9,7 +9,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.django
import
modulestore
from
gating.signals
import
handle_s
ubsection_score_updat
ed
from
gating.signals
import
handle_s
core_chang
ed
class
TestHandleScoreChanged
(
ModuleStoreTestCase
):
...
...
@@ -24,22 +24,28 @@ class TestHandleScoreChanged(ModuleStoreTestCase):
@patch
(
'gating.signals.gating_api.evaluate_prerequisite'
)
def
test_gating_enabled
(
self
,
mock_evaluate
):
""" Test evaluate_prerequisite is called when course.enable_subsection_gating is True """
self
.
course
.
enable_subsection_gating
=
True
modulestore
()
.
update_item
(
self
.
course
,
0
)
handle_s
ubsection_score_updat
ed
(
handle_s
core_chang
ed
(
sender
=
None
,
course
=
self
.
course
,
points_possible
=
1
,
points_earned
=
1
,
user
=
self
.
user
,
subsection_grade
=
MagicMock
(),
course_id
=
unicode
(
self
.
course
.
id
),
usage_id
=
unicode
(
self
.
test_usage_key
)
)
mock_evaluate
.
assert_called
()
mock_evaluate
.
assert_called
_with
(
self
.
course
,
self
.
test_usage_key
,
self
.
user
.
id
)
# pylint: disable=no-member
@patch
(
'gating.signals.gating_api.evaluate_prerequisite'
)
def
test_gating_disabled
(
self
,
mock_evaluate
):
handle_subsection_score_updated
(
""" Test evaluate_prerequisite is not called when course.enable_subsection_gating is False """
handle_score_changed
(
sender
=
None
,
course
=
self
.
course
,
points_possible
=
1
,
points_earned
=
1
,
user
=
self
.
user
,
subsection_grade
=
MagicMock
(),
course_id
=
unicode
(
self
.
course
.
id
),
usage_id
=
unicode
(
self
.
test_usage_key
)
)
mock_evaluate
.
assert_not_called
()
This diff is collapsed.
Click to expand it.
lms/djangoapps/grades/module_grades.py
0 → 100644
View file @
50d55bba
"""
Functionality for module-level grades.
"""
# TODO The score computation in this file is not accurate
# since it is summing percentages instead of computing a
# final percentage of the individual sums.
# Regardless, this file and its code should be removed soon
# as part of TNL-5062.
from
django.test.client
import
RequestFactory
from
courseware.model_data
import
FieldDataCache
,
ScoresClient
from
courseware.module_render
import
get_module_for_descriptor
from
opaque_keys.edx.locator
import
BlockUsageLocator
from
util.module_utils
import
yield_dynamic_descriptor_descendants
def
_get_mock_request
(
student
):
"""
Make a fake request because grading code expects to be able to look at
the request. We have to attach the correct user to the request before
grading that student.
"""
request
=
RequestFactory
()
.
get
(
'/'
)
request
.
user
=
student
return
request
def
_calculate_score_for_modules
(
user_id
,
course
,
modules
):
"""
Calculates the cumulative score (percent) of the given modules
"""
# removing branch and version from exam modules locator
# otherwise student module would not return scores since module usage keys would not match
modules
=
[
m
for
m
in
modules
]
locations
=
[
BlockUsageLocator
(
course_key
=
course
.
id
,
block_type
=
module
.
location
.
block_type
,
block_id
=
module
.
location
.
block_id
)
if
isinstance
(
module
.
location
,
BlockUsageLocator
)
and
module
.
location
.
version
else
module
.
location
for
module
in
modules
]
scores_client
=
ScoresClient
(
course
.
id
,
user_id
)
scores_client
.
fetch_scores
(
locations
)
# Iterate over all of the exam modules to get score percentage of user for each of them
module_percentages
=
[]
ignore_categories
=
[
'course'
,
'chapter'
,
'sequential'
,
'vertical'
,
'randomize'
,
'library_content'
]
for
index
,
module
in
enumerate
(
modules
):
if
module
.
category
not
in
ignore_categories
and
(
module
.
graded
or
module
.
has_score
):
module_score
=
scores_client
.
get
(
locations
[
index
])
if
module_score
:
correct
=
module_score
.
correct
or
0
total
=
module_score
.
total
or
1
module_percentages
.
append
(
correct
/
total
)
return
sum
(
module_percentages
)
/
float
(
len
(
module_percentages
))
if
module_percentages
else
0
def
get_module_score
(
user
,
course
,
module
):
"""
Collects all children of the given module and calculates the cumulative
score for this set of modules for the given user.
Arguments:
user (User): The user
course (CourseModule): The course
module (XBlock): The module
Returns:
float: The cumulative score
"""
def
inner_get_module
(
descriptor
):
"""
Delegate to get_module_for_descriptor
"""
field_data_cache
=
FieldDataCache
([
descriptor
],
course
.
id
,
user
)
return
get_module_for_descriptor
(
user
,
_get_mock_request
(
user
),
descriptor
,
field_data_cache
,
course
.
id
,
course
=
course
)
modules
=
yield_dynamic_descriptor_descendants
(
module
,
user
.
id
,
inner_get_module
)
return
_calculate_score_for_modules
(
user
.
id
,
course
,
modules
)
This diff is collapsed.
Click to expand it.
lms/djangoapps/grades/new/subsection_grade.py
View file @
50d55bba
...
...
@@ -112,14 +112,14 @@ class SubsectionGrade(object):
"""
Saves the subsection grade in a persisted model.
"""
self
.
_log_event
(
log
.
info
,
u"create_model"
,
student
)
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
):
"""
Saves or updates the subsection grade in a persisted model.
"""
self
.
_log_event
(
log
.
info
,
u"update_or_create_model"
,
student
)
self
.
_log_event
(
log
.
debug
,
u"update_or_create_model"
,
student
)
return
PersistentSubsectionGrade
.
update_or_create_grade
(
**
self
.
_persisted_model_params
(
student
))
def
_compute_block_score
(
...
...
@@ -251,6 +251,11 @@ class SubsectionGradeFactory(object):
"""
Updates the SubsectionGrade object for the student and subsection.
"""
# Save ourselves the extra queries if the course does not persist
# subsection grades.
if
not
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
):
return
self
.
_log_event
(
log
.
warning
,
u"update, subsection: {}"
.
format
(
subsection
.
location
))
block_structure
=
self
.
_get_block_structure
(
block_structure
)
...
...
@@ -259,10 +264,8 @@ class SubsectionGradeFactory(object):
self
.
student
,
block_structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
)
if
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
):
grade_model
=
subsection_grade
.
update_or_create_model
(
self
.
student
)
self
.
_update_saved_subsection_grade
(
subsection
.
location
,
grade_model
)
grade_model
=
subsection_grade
.
update_or_create_model
(
self
.
student
)
self
.
_update_saved_subsection_grade
(
subsection
.
location
,
grade_model
)
return
subsection_grade
@lazy
...
...
This diff is collapsed.
Click to expand it.
lms/djangoapps/grades/scores.py
View file @
50d55bba
...
...
@@ -108,20 +108,23 @@ def get_score(submissions_scores, csm_scores, persisted_block, block):
_get_score_from_persisted_or_latest_block
(
persisted_block
,
block
,
weight
)
)
assert
weighted_possible
is
not
None
has_valid_denominator
=
weighted_possible
>
0.0
graded
=
_get_graded_from_block
(
persisted_block
,
block
)
if
has_valid_denominator
else
False
return
ProblemScore
(
raw_earned
,
raw_possible
,
weighted_earned
,
weighted_possible
,
weight
,
graded
,
display_name
=
display_name_with_default_escaped
(
block
),
module_id
=
block
.
location
,
)
if
weighted_possible
is
None
or
weighted_earned
is
None
:
return
None
else
:
has_valid_denominator
=
weighted_possible
>
0.0
graded
=
_get_graded_from_block
(
persisted_block
,
block
)
if
has_valid_denominator
else
False
return
ProblemScore
(
raw_earned
,
raw_possible
,
weighted_earned
,
weighted_possible
,
weight
,
graded
,
display_name
=
display_name_with_default_escaped
(
block
),
module_id
=
block
.
location
,
)
def
weighted_score
(
raw_earned
,
raw_possible
,
weight
):
...
...
@@ -191,7 +194,10 @@ def _get_score_from_persisted_or_latest_block(persisted_block, block, weight):
else
:
raw_possible
=
block
.
transformer_data
[
GradesTransformer
]
.
max_score
return
(
raw_earned
,
raw_possible
)
+
weighted_score
(
raw_earned
,
raw_possible
,
weight
)
if
raw_possible
is
None
:
return
(
raw_earned
,
raw_possible
)
+
(
None
,
None
)
else
:
return
(
raw_earned
,
raw_possible
)
+
weighted_score
(
raw_earned
,
raw_possible
,
weight
)
def
_get_weight_from_block
(
persisted_block
,
block
):
...
...
This diff is collapsed.
Click to expand it.
lms/djangoapps/grades/signals/handlers.py
View file @
50d55bba
...
...
@@ -13,7 +13,7 @@ from openedx.core.djangoapps.content.block_structure.api import get_course_in_ca
from
student.models
import
user_by_anonymous_id
from
submissions.models
import
score_set
,
score_reset
from
.signals
import
SCORE_CHANGED
,
SUBSECTION_SCORE_UPDATED
from
.signals
import
SCORE_CHANGED
from
..config.models
import
PersistentGradesEnabledFlag
from
..transformer
import
GradesTransformer
from
..new.subsection_grade
import
SubsectionGradeFactory
...
...
@@ -95,6 +95,8 @@ def recalculate_subsection_grade_handler(sender, **kwargs): # pylint: disable=u
"""
student
=
kwargs
[
'user'
]
course_key
=
CourseLocator
.
from_string
(
kwargs
[
'course_id'
])
if
not
PersistentGradesEnabledFlag
.
feature_enabled
(
course_key
):
return
scored_block_usage_key
=
UsageKey
.
from_string
(
kwargs
[
'usage_id'
])
.
replace
(
course_key
=
course_key
)
collected_block_structure
=
get_course_in_cache
(
course_key
)
...
...
@@ -113,12 +115,6 @@ def recalculate_subsection_grade_handler(sender, **kwargs): # pylint: disable=u
subsection_usage_key
,
collected_block_structure
=
collected_block_structure
,
)
subsection_grade
=
subsection_grade
_factory
.
update
(
subsection_grade_factory
.
update
(
transformed_subsection_structure
[
subsection_usage_key
],
transformed_subsection_structure
)
SUBSECTION_SCORE_UPDATED
.
send
(
sender
=
None
,
course
=
course
,
user
=
student
,
subsection_grade
=
subsection_grade
,
)
This diff is collapsed.
Click to expand it.
lms/djangoapps/grades/signals/signals.py
View file @
50d55bba
...
...
@@ -14,20 +14,8 @@ SCORE_CHANGED = Signal(
providing_args
=
[
'points_possible'
,
# Maximum score available for the exercise
'points_earned'
,
# Score obtained by the user
'user
'
,
# User object
'user
_id'
,
# Integer User ID
'course_id'
,
# Unicode string representing the course
'usage_id'
# Unicode string indicating the courseware instance
]
)
# Signal that indicates that a user's score for a subsection has been updated.
# This is a downstream signal of SCORE_CHANGED sent for each affected containing
# subsection.
SUBSECTION_SCORE_UPDATED
=
Signal
(
providing_args
=
[
'course'
,
# Course object
'user'
,
# User object
'subsection_grade'
,
# SubsectionGrade object
]
)
This diff is collapsed.
Click to expand it.
lms/djangoapps/grades/tests/test_grades.py
View file @
50d55bba
...
...
@@ -10,7 +10,11 @@ from nose.plugins.attrib import attr
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
capa.tests.response_xml_factory
import
MultipleChoiceResponseXMLFactory
from
courseware.tests.helpers
import
get_request_for_user
from
courseware.model_data
import
set_score
from
courseware.tests.helpers
import
(
LoginEnrollmentTestCase
,
get_request_for_user
)
from
lms.djangoapps.course_blocks.api
import
get_course_blocks
from
student.tests.factories
import
UserFactory
from
student.models
import
CourseEnrollment
...
...
@@ -23,6 +27,7 @@ from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from
.utils
import
answer_problem
from
..
import
course_grades
from
..course_grades
import
summary
as
grades_summary
from
..module_grades
import
get_module_score
from
..new.course_grade
import
CourseGradeFactory
from
..new.subsection_grade
import
SubsectionGradeFactory
...
...
@@ -337,3 +342,194 @@ class TestScoreForModule(SharedModuleStoreTestCase):
earned
,
possible
=
self
.
course_grade
.
score_for_module
(
self
.
m
.
location
)
self
.
assertEqual
(
earned
,
0
)
self
.
assertEqual
(
possible
,
0
)
class
TestGetModuleScore
(
LoginEnrollmentTestCase
,
SharedModuleStoreTestCase
):
"""
Test get_module_score
"""
@classmethod
def
setUpClass
(
cls
):
super
(
TestGetModuleScore
,
cls
)
.
setUpClass
()
cls
.
course
=
CourseFactory
.
create
()
cls
.
chapter
=
ItemFactory
.
create
(
parent
=
cls
.
course
,
category
=
"chapter"
,
display_name
=
"Test Chapter"
)
cls
.
seq1
=
ItemFactory
.
create
(
parent
=
cls
.
chapter
,
category
=
'sequential'
,
display_name
=
"Test Sequential 1"
,
graded
=
True
)
cls
.
seq2
=
ItemFactory
.
create
(
parent
=
cls
.
chapter
,
category
=
'sequential'
,
display_name
=
"Test Sequential 2"
,
graded
=
True
)
cls
.
seq3
=
ItemFactory
.
create
(
parent
=
cls
.
chapter
,
category
=
'sequential'
,
display_name
=
"Test Sequential 3"
,
graded
=
True
)
cls
.
vert1
=
ItemFactory
.
create
(
parent
=
cls
.
seq1
,
category
=
'vertical'
,
display_name
=
'Test Vertical 1'
)
cls
.
vert2
=
ItemFactory
.
create
(
parent
=
cls
.
seq2
,
category
=
'vertical'
,
display_name
=
'Test Vertical 2'
)
cls
.
vert3
=
ItemFactory
.
create
(
parent
=
cls
.
seq3
,
category
=
'vertical'
,
display_name
=
'Test Vertical 3'
)
cls
.
randomize
=
ItemFactory
.
create
(
parent
=
cls
.
vert2
,
category
=
'randomize'
,
display_name
=
'Test Randomize'
)
cls
.
library_content
=
ItemFactory
.
create
(
parent
=
cls
.
vert3
,
category
=
'library_content'
,
display_name
=
'Test Library Content'
)
problem_xml
=
MultipleChoiceResponseXMLFactory
()
.
build_xml
(
question_text
=
'The correct answer is Choice 3'
,
choices
=
[
False
,
False
,
True
,
False
],
choice_names
=
[
'choice_0'
,
'choice_1'
,
'choice_2'
,
'choice_3'
]
)
cls
.
problem1
=
ItemFactory
.
create
(
parent
=
cls
.
vert1
,
category
=
"problem"
,
display_name
=
"Test Problem 1"
,
data
=
problem_xml
)
cls
.
problem2
=
ItemFactory
.
create
(
parent
=
cls
.
vert1
,
category
=
"problem"
,
display_name
=
"Test Problem 2"
,
data
=
problem_xml
)
cls
.
problem3
=
ItemFactory
.
create
(
parent
=
cls
.
randomize
,
category
=
"problem"
,
display_name
=
"Test Problem 3"
,
data
=
problem_xml
)
cls
.
problem4
=
ItemFactory
.
create
(
parent
=
cls
.
randomize
,
category
=
"problem"
,
display_name
=
"Test Problem 4"
,
data
=
problem_xml
)
cls
.
problem5
=
ItemFactory
.
create
(
parent
=
cls
.
library_content
,
category
=
"problem"
,
display_name
=
"Test Problem 5"
,
data
=
problem_xml
)
cls
.
problem6
=
ItemFactory
.
create
(
parent
=
cls
.
library_content
,
category
=
"problem"
,
display_name
=
"Test Problem 6"
,
data
=
problem_xml
)
def
setUp
(
self
):
"""
Set up test course
"""
super
(
TestGetModuleScore
,
self
)
.
setUp
()
self
.
request
=
get_request_for_user
(
UserFactory
())
self
.
client
.
login
(
username
=
self
.
request
.
user
.
username
,
password
=
"test"
)
CourseEnrollment
.
enroll
(
self
.
request
.
user
,
self
.
course
.
id
)
self
.
course_structure
=
get_course_blocks
(
self
.
request
.
user
,
self
.
course
.
location
)
# warm up the score cache to allow accurate query counts, even if tests are run in random order
get_module_score
(
self
.
request
.
user
,
self
.
course
,
self
.
seq1
)
def
test_subsection_scores
(
self
):
"""
Test test_get_module_score
"""
# One query is for getting the list of disabled XBlocks (which is
# then stored in the request).
with
self
.
assertNumQueries
(
1
):
score
=
get_module_score
(
self
.
request
.
user
,
self
.
course
,
self
.
seq1
)
new_score
=
SubsectionGradeFactory
(
self
.
request
.
user
,
self
.
course
,
self
.
course_structure
)
.
create
(
self
.
seq1
)
self
.
assertEqual
(
score
,
0
)
self
.
assertEqual
(
new_score
.
all_total
.
earned
,
0
)
answer_problem
(
self
.
course
,
self
.
request
,
self
.
problem1
)
answer_problem
(
self
.
course
,
self
.
request
,
self
.
problem2
)
with
self
.
assertNumQueries
(
1
):
score
=
get_module_score
(
self
.
request
.
user
,
self
.
course
,
self
.
seq1
)
new_score
=
SubsectionGradeFactory
(
self
.
request
.
user
,
self
.
course
,
self
.
course_structure
)
.
create
(
self
.
seq1
)
self
.
assertEqual
(
score
,
1.0
)
self
.
assertEqual
(
new_score
.
all_total
.
earned
,
2.0
)
# These differ because get_module_score normalizes the subsection score
# to 1, which can cause incorrect aggregation behavior that will be
# fixed by TNL-5062.
answer_problem
(
self
.
course
,
self
.
request
,
self
.
problem1
)
answer_problem
(
self
.
course
,
self
.
request
,
self
.
problem2
,
0
)
with
self
.
assertNumQueries
(
1
):
score
=
get_module_score
(
self
.
request
.
user
,
self
.
course
,
self
.
seq1
)
new_score
=
SubsectionGradeFactory
(
self
.
request
.
user
,
self
.
course
,
self
.
course_structure
)
.
create
(
self
.
seq1
)
self
.
assertEqual
(
score
,
.
5
)
self
.
assertEqual
(
new_score
.
all_total
.
earned
,
1.0
)
def
test_get_module_score_with_empty_score
(
self
):
"""
Test test_get_module_score_with_empty_score
"""
set_score
(
self
.
request
.
user
.
id
,
self
.
problem1
.
location
,
None
,
None
)
# pylint: disable=no-member
set_score
(
self
.
request
.
user
.
id
,
self
.
problem2
.
location
,
None
,
None
)
# pylint: disable=no-member
with
self
.
assertNumQueries
(
1
):
score
=
get_module_score
(
self
.
request
.
user
,
self
.
course
,
self
.
seq1
)
self
.
assertEqual
(
score
,
0
)
answer_problem
(
self
.
course
,
self
.
request
,
self
.
problem1
)
with
self
.
assertNumQueries
(
1
):
score
=
get_module_score
(
self
.
request
.
user
,
self
.
course
,
self
.
seq1
)
self
.
assertEqual
(
score
,
0.5
)
answer_problem
(
self
.
course
,
self
.
request
,
self
.
problem2
)
with
self
.
assertNumQueries
(
1
):
score
=
get_module_score
(
self
.
request
.
user
,
self
.
course
,
self
.
seq1
)
self
.
assertEqual
(
score
,
1.0
)
def
test_get_module_score_with_randomize
(
self
):
"""
Test test_get_module_score_with_randomize
"""
answer_problem
(
self
.
course
,
self
.
request
,
self
.
problem3
)
answer_problem
(
self
.
course
,
self
.
request
,
self
.
problem4
)
score
=
get_module_score
(
self
.
request
.
user
,
self
.
course
,
self
.
seq2
)
self
.
assertEqual
(
score
,
1.0
)
def
test_get_module_score_with_library_content
(
self
):
"""
Test test_get_module_score_with_library_content
"""
answer_problem
(
self
.
course
,
self
.
request
,
self
.
problem5
)
answer_problem
(
self
.
course
,
self
.
request
,
self
.
problem6
)
score
=
get_module_score
(
self
.
request
.
user
,
self
.
course
,
self
.
seq3
)
self
.
assertEqual
(
score
,
1.0
)
This diff is collapsed.
Click to expand it.
lms/djangoapps/grades/tests/test_signals.py
View file @
50d55bba
...
...
@@ -244,7 +244,7 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase):
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
(
enable_subsection_grades
=
False
)
self
.
assertFalse
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
with
check_mongo_calls
(
2
)
and
self
.
assertNumQueries
(
3
):
with
check_mongo_calls
(
2
)
and
self
.
assertNumQueries
(
0
):
recalculate_subsection_grade_handler
(
None
,
**
self
.
score_changed_kwargs
)
@skip
(
"Pending completion of TNL-5089"
)
...
...
This diff is collapsed.
Click to expand it.
lms/djangoapps/grades/tests/utils.py
View file @
50d55bba
...
...
@@ -30,14 +30,12 @@ def mock_get_score(earned=0, possible=1):
def
answer_problem
(
course
,
request
,
problem
,
score
=
1
,
max_value
=
1
):
"""
Records a
n
answer for the given problem.
Records a
correct
answer for the given problem.
Arguments:
course (Course): Course object, the course the required problem is in
request (Request): request Object
problem (xblock): xblock object, the problem to be answered
score (float): The new score for the problem
max_value (float): The new maximum score for the problem
"""
user
=
request
.
user
...
...
@@ -48,10 +46,11 @@ def answer_problem(course, request, problem, score=1, max_value=1):
course
,
depth
=
2
)
# pylint: disable=protected-access
module
=
get_module
(
user
,
request
,
problem
.
location
,
problem
.
scope_ids
.
usage_id
,
field_data_cache
,
)
module
.
runtime
.
publish
(
problem
,
'grade'
,
grade_dict
)
)
.
_xmodule
module
.
system
.
publish
(
problem
,
'grade'
,
grade_dict
)
This diff is collapsed.
Click to expand it.
lms/djangoapps/grades/transformer.py
View file @
50d55bba
...
...
@@ -3,6 +3,7 @@ Grades Transformer
"""
from
django.test.client
import
RequestFactory
from
functools
import
reduce
as
functools_reduce
from
logging
import
getLogger
from
courseware.model_data
import
FieldDataCache
from
courseware.module_render
import
get_module_for_descriptor
...
...
@@ -11,6 +12,9 @@ from openedx.core.lib.block_structure.transformer import BlockStructureTransform
from
openedx.core.djangoapps.util.user_utils
import
SystemUser
log
=
getLogger
(
__name__
)
class
GradesTransformer
(
BlockStructureTransformer
):
"""
The GradesTransformer collects grading information and stores it on
...
...
@@ -119,8 +123,10 @@ class GradesTransformer(BlockStructureTransformer):
Collect the `max_score` from the given module, storing it as a
`transformer_block_field` associated with the `GradesTransformer`.
"""
score
=
module
.
max_score
()
block_structure
.
set_transformer_block_field
(
module
.
location
,
cls
,
'max_score'
,
score
)
max_score
=
module
.
max_score
()
block_structure
.
set_transformer_block_field
(
module
.
location
,
cls
,
'max_score'
,
max_score
)
if
max_score
is
None
:
log
.
warning
(
"GradesTransformer: max_score is None for {}"
.
format
(
module
.
location
))
@staticmethod
def
_iter_scorable_xmodules
(
block_structure
):
...
...
This diff is collapsed.
Click to expand it.
lms/djangoapps/instructor/enrollment.py
View file @
50d55bba
...
...
@@ -18,8 +18,8 @@ from courseware.model_data import FieldDataCache
from
courseware.module_render
import
get_module_for_descriptor
from
courseware.models
import
StudentModule
from
edxmako.shortcuts
import
render_to_string
from
grades.scores
import
weighted_score
from
grades.signals.signals
import
SCORE_CHANGED
from
lms.djangoapps.
grades.scores
import
weighted_score
from
lms.djangoapps.
grades.signals.signals
import
SCORE_CHANGED
from
lang_pref
import
LANGUAGE_KEY
from
student.models
import
CourseEnrollment
,
CourseEnrollmentAllowed
from
submissions
import
api
as
sub_api
# installed from the edx-submissions repository
...
...
@@ -317,7 +317,11 @@ def _fire_score_changed_for_block(course_id, student, block, module_state_key):
field_data_cache
=
cache
,
course_key
=
course_id
)
points_earned
,
points_possible
=
weighted_score
(
0
,
module
.
max_score
(),
getattr
(
module
,
'weight'
,
None
))
max_score
=
module
.
max_score
()
if
max_score
is
None
:
return
else
:
points_earned
,
points_possible
=
weighted_score
(
0
,
max_score
,
getattr
(
module
,
'weight'
,
None
))
else
:
points_earned
,
points_possible
=
0
,
0
SCORE_CHANGED
.
send
(
...
...
@@ -325,8 +329,8 @@ def _fire_score_changed_for_block(course_id, student, block, module_state_key):
points_possible
=
points_possible
,
points_earned
=
points_earned
,
user
=
student
,
course_id
=
course_id
,
usage_id
=
module_state_key
course_id
=
unicode
(
course_id
)
,
usage_id
=
unicode
(
module_state_key
)
)
...
...
This diff is collapsed.
Click to expand it.
lms/djangoapps/instructor/tests/test_api.py
View file @
50d55bba
...
...
@@ -3190,7 +3190,8 @@ class TestInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginEnrollmentTes
})
self
.
assertEqual
(
response
.
status_code
,
400
)
def
test_reset_student_attempts_delete
(
self
):
@patch
(
'courseware.module_render.SCORE_CHANGED.send'
)
def
test_reset_student_attempts_delete
(
self
,
_mock_signal
):
""" Test delete single student state. """
url
=
reverse
(
'reset_student_attempts'
,
kwargs
=
{
'course_id'
:
self
.
course
.
id
.
to_deprecated_string
()})
response
=
self
.
client
.
post
(
url
,
{
...
...
This diff is collapsed.
Click to expand it.
lms/djangoapps/instructor/tests/test_enrollment.py
View file @
50d55bba
...
...
@@ -378,7 +378,8 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase):
reset_student_attempts
(
self
.
course_key
,
self
.
user
,
msk
,
requesting_user
=
self
.
user
)
self
.
assertEqual
(
json
.
loads
(
module
()
.
state
)[
'attempts'
],
0
)
def
test_delete_student_attempts
(
self
):
@mock.patch
(
'courseware.module_render.SCORE_CHANGED.send'
)
def
test_delete_student_attempts
(
self
,
_mock_signal
):
msk
=
self
.
course_key
.
make_usage_key
(
'dummy'
,
'module'
)
original_state
=
json
.
dumps
({
'attempts'
:
32
,
'otherstuff'
:
'alsorobots'
})
StudentModule
.
objects
.
create
(
...
...
@@ -404,7 +405,7 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase):
# Disable the score change signal to prevent other components from being
# pulled into tests.
@mock.patch
(
'courseware.module_render.SCORE_CHANGED.send'
)
def
test_delete_submission_scores
(
self
,
_
lti_mock
):
def
test_delete_submission_scores
(
self
,
_
mock_signal
):
user
=
UserFactory
()
problem_location
=
self
.
course_key
.
make_usage_key
(
'dummy'
,
'module'
)
...
...
@@ -548,7 +549,7 @@ class TestStudentModuleGrading(SharedModuleStoreTestCase):
self
.
course
,
get_course_blocks
(
self
.
user
,
self
.
course
.
location
)
)
grade
=
subsection_grade_factory
.
upd
ate
(
self
.
sequence
)
grade
=
subsection_grade_factory
.
cre
ate
(
self
.
sequence
)
self
.
assertEqual
(
grade
.
all_total
.
earned
,
all_earned
)
self
.
assertEqual
(
grade
.
graded_total
.
earned
,
graded_earned
)
self
.
assertEqual
(
grade
.
all_total
.
possible
,
all_possible
)
...
...
This diff is collapsed.
Click to expand it.
lms/djangoapps/instructor/tests/test_services.py
View file @
50d55bba
...
...
@@ -49,7 +49,8 @@ class InstructorServiceTests(SharedModuleStoreTestCase):
state
=
json
.
dumps
({
'attempts'
:
2
}),
)
def
test_reset_student_attempts_delete
(
self
):
@mock.patch
(
'courseware.module_render.SCORE_CHANGED.send'
)
def
test_reset_student_attempts_delete
(
self
,
_mock_signal
):
"""
Test delete student state.
"""
...
...
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment