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
cfb032a5
Commit
cfb032a5
authored
Mar 14, 2017
by
Nimisha Asthagiri
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fix grading for Gated Subsections
TNL-5955
parent
173e5922
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
338 additions
and
525 deletions
+338
-525
lms/djangoapps/course_api/blocks/transformers/tests/test_milestones.py
+9
-12
lms/djangoapps/gating/api.py
+57
-76
lms/djangoapps/gating/signals.py
+10
-14
lms/djangoapps/gating/tests/test_api.py
+23
-85
lms/djangoapps/gating/tests/test_integration.py
+192
-0
lms/djangoapps/gating/tests/test_signals.py
+17
-23
lms/djangoapps/grades/module_grades.py
+0
-96
lms/djangoapps/grades/tests/test_grades.py
+0
-196
lms/djangoapps/mobile_api/tests/test_milestones.py
+30
-23
No files found.
lms/djangoapps/course_api/blocks/transformers/tests/test_milestones.py
View file @
cfb032a5
...
...
@@ -52,7 +52,8 @@ 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) should never be visible to students
# The special exams (proctored, practice, timed) are not visible to
# students via the Courses API.
ALL_BLOCKS_EXCEPT_SPECIAL
=
(
'course'
,
'A'
,
'B'
,
'C'
,
'H'
,
'I'
)
def
get_course_hierarchy
(
self
):
...
...
@@ -133,18 +134,16 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
(
'H'
,
'A'
,
'B'
,
(
'course'
,
'A'
,
'B'
,
'C'
,)
),
(
'H'
,
'ProctoredExam'
,
'D'
,
(
'course'
,
'A'
,
'B'
,
'C'
),
),
)
@ddt.unpack
def
test_gated
(
self
,
gated_block_ref
,
gating_block_ref
,
gating_block_child
,
expected_blocks_before_completion
):
def
test_gated
(
self
,
gated_block_ref
,
gating_block_ref
,
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.
...
...
@@ -164,17 +163,15 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
# clear the request cache to simulate a new request
self
.
clear_caches
()
# 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
)):
# 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
# this call triggers reevaluation of prerequisites fulfilled by the gating block.
with
patch
(
'gating.api._get_subsection_percentage'
,
Mock
(
return_value
=
100
)):
lms_gating_api
.
evaluate_prerequisite
(
self
.
course
,
self
.
blocks
[
gating_block_child
]
,
self
.
user
.
id
,
Mock
(
location
=
self
.
blocks
[
gating_block_ref
]
.
location
)
,
self
.
user
,
)
with
self
.
assertNumQueries
(
5
):
with
self
.
assertNumQueries
(
6
):
self
.
get_blocks_and_check_against_expected
(
self
.
user
,
self
.
ALL_BLOCKS_EXCEPT_SPECIAL
)
def
test_staff_access
(
self
):
...
...
lms/djangoapps/gating/api.py
View file @
cfb032a5
...
...
@@ -2,105 +2,87 @@
API for the gating djangoapp
"""
from
collections
import
defaultdict
from
django.contrib.auth.models
import
User
from
django.test.client
import
RequestFactory
import
json
import
logging
from
lms.djangoapps.courseware.entrance_exams
import
get_entrance_exam_score
from
openedx.core.lib.gating
import
api
as
gating_api
from
opaque_keys.edx.keys
import
UsageKey
from
lms.djangoapps.courseware.entrance_exams
import
get_entrance_exam_score
from
lms.djangoapps.grades.module_grades
import
get_module_score
from
xmodule.modulestore.django
import
modulestore
from
util
import
milestones_helpers
log
=
logging
.
getLogger
(
__name__
)
def
_get_xblock_parent
(
xblock
,
category
=
None
):
@gating_api.gating_enabled
(
default
=
False
)
def
evaluate_prerequisite
(
course
,
subsection_grade
,
user
):
"""
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)
Evaluates any gating milestone relationships attached to the given
subsection. If the subsection_grade meets the minimum score required
by dependent subsections, the related milestone will be marked
fulfilled for the user.
"""
parent
=
xblock
.
get_parent
()
if
parent
and
category
:
if
parent
.
category
==
category
:
return
parent
else
:
return
_get_xblock_parent
(
parent
,
category
)
return
parent
prereq_milestone
=
gating_api
.
get_gating_milestone
(
course
.
id
,
subsection_grade
.
location
,
'fulfills'
)
if
prereq_milestone
:
gated_content_milestones
=
defaultdict
(
list
)
for
milestone
in
gating_api
.
find_gating_milestones
(
course
.
id
,
content_key
=
None
,
relationship
=
'requires'
):
gated_content_milestones
[
milestone
[
'id'
]]
.
append
(
milestone
)
@gating_api.gating_enabled
(
default
=
False
)
def
evaluate_prerequisite
(
course
,
block
,
user_id
):
"""
Finds the parent subsection of the content in the course and evaluates
any milestone relationships attached to that subsection. If the calculated
grade of the prerequisite subsection meets the minimum score required by
dependent subsections, the related milestone will be fulfilled for the user.
gated_content
=
gated_content_milestones
.
get
(
prereq_milestone
[
'id'
])
if
gated_content
:
for
milestone
in
gated_content
:
min_percentage
=
_get_minimum_required_percentage
(
milestone
)
subsection_percentage
=
_get_subsection_percentage
(
subsection_grade
)
if
subsection_percentage
>=
min_percentage
:
milestones_helpers
.
add_user_milestone
({
'id'
:
user
.
id
},
prereq_milestone
)
else
:
milestones_helpers
.
remove_user_milestone
({
'id'
:
user
.
id
},
prereq_milestone
)
Arguments:
course (CourseModule): The course
prereq_content_key (UsageKey): The prerequisite content usage key
user_id (int): ID of User for which evaluation should occur
Returns:
None
def
_get_minimum_required_percentage
(
milestone
):
"""
Returns the minimum percentage requirement for the given milestone.
"""
sequential
=
_get_xblock_parent
(
block
,
'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
)
# 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
)
)
return
min_score
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
)
def
_get_subsection_percentage
(
subsection_grade
):
"""
Returns the percentage value of the given subsection_grade.
"""
if
subsection_grade
.
graded_total
.
possible
:
return
float
(
subsection_grade
.
graded_total
.
earned
)
/
float
(
subsection_grade
.
graded_total
.
possible
)
*
100.0
else
:
return
0
def
evaluate_entrance_exam
(
course
,
block
,
user_id
):
def
evaluate_entrance_exam
(
course
,
subsection_grade
,
user
):
"""
Update milestone fulfillments for the specified content module
Evaluates any entrance exam milestone relationships attached
to the given subsection. If the subsection_grade meets the
minimum score required, the dependent milestone will be marked
fulfilled for the user.
"""
# Fulfillment Use Case: Entrance Exam
# If this module is part of an entrance exam, we'll need to see if the student
# has reached the point at which they can collect the associated milestone
if
milestones_helpers
.
is_entrance_exams_enabled
():
entrance_exam_enabled
=
getattr
(
course
,
'entrance_exam_enabled'
,
False
)
in_entrance_exam
=
getattr
(
block
,
'in_entrance_exam'
,
False
)
if
entrance_exam_enabled
and
in_entrance_exam
:
if
milestones_helpers
.
is_entrance_exams_enabled
()
and
getattr
(
course
,
'entrance_exam_enabled'
,
False
):
subsection
=
modulestore
()
.
get_item
(
subsection_grade
.
location
)
in_entrance_exam
=
getattr
(
subsection
,
'in_entrance_exam'
,
False
)
if
in_entrance_exam
:
# We don't have access to the true request object in this context, but we can use a mock
request
=
RequestFactory
()
.
request
()
request
.
user
=
User
.
objects
.
get
(
id
=
user_id
)
request
.
user
=
user
exam_pct
=
get_entrance_exam_score
(
request
,
course
)
if
exam_pct
>=
course
.
entrance_exam_minimum_score_pct
:
exam_key
=
UsageKey
.
from_string
(
course
.
entrance_exam_id
)
...
...
@@ -110,7 +92,6 @@ def evaluate_entrance_exam(course, block, user_id):
exam_key
,
relationship
=
relationship_types
[
'FULFILLS'
]
)
# Add each milestone to the user's set...
user
=
{
'id'
:
request
.
user
.
id
}
# Mark each milestone dependent on the entrance exam as fulfilled by the user.
for
milestone
in
content_milestones
:
milestones_helpers
.
add_user_milestone
(
user
,
milestone
)
milestones_helpers
.
add_user_milestone
(
{
'id'
:
request
.
user
.
id
}
,
milestone
)
lms/djangoapps/gating/signals.py
View file @
cfb032a5
...
...
@@ -4,25 +4,21 @@ Signal handlers for the gating djangoapp
from
django.dispatch
import
receiver
from
gating
import
api
as
gating_api
from
lms.djangoapps.grades.signals.signals
import
PROBLEM_WEIGHTED_SCORE_CHANGED
from
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
from
xmodule.modulestore.django
import
modulestore
from
lms.djangoapps.grades.signals.signals
import
SUBSECTION_SCORE_CHANGED
@receiver
(
PROBLEM_WEIGHTED
_SCORE_CHANGED
)
def
handle_score_changed
(
**
kwargs
):
@receiver
(
SUBSECTION
_SCORE_CHANGED
)
def
evaluate_subsection_gated_milestones
(
**
kwargs
):
"""
Receives the
PROBLEM_WEIGHTED_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
which are attached to the updated content
.
Receives the
SUBSECTION_SCORE_CHANGED signal and triggers the
evaluation of any milestone relationships which are attached
to the subsection
.
Arguments:
kwargs (dict): Contains user ID, course key, and content usage key
kwargs (dict): Contains user, course, course_structure, subsection_grade
Returns:
None
"""
course
=
modulestore
()
.
get_course
(
CourseKey
.
from_string
(
kwargs
.
get
(
'course_id'
)))
block
=
modulestore
()
.
get_item
(
UsageKey
.
from_string
(
kwargs
.
get
(
'usage_id'
)))
gating_api
.
evaluate_prerequisite
(
course
,
block
,
kwargs
.
get
(
'user_id'
))
gating_api
.
evaluate_entrance_exam
(
course
,
block
,
kwargs
.
get
(
'user_id'
))
subsection_grade
=
kwargs
[
'subsection_grade'
]
gating_api
.
evaluate_prerequisite
(
kwargs
[
'course'
],
subsection_grade
,
kwargs
.
get
(
'user'
))
gating_api
.
evaluate_entrance_exam
(
kwargs
[
'course'
],
subsection_grade
,
kwargs
.
get
(
'user'
))
lms/djangoapps/gating/tests/test_api.py
View file @
cfb032a5
"""
Unit tests for gating.signals module
"""
from
mock
import
patch
from
mock
import
patch
,
Mock
from
nose.plugins.attrib
import
attr
from
ddt
import
ddt
,
data
,
unpack
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
...
...
@@ -11,7 +11,7 @@ from courseware.tests.helpers import LoginEnrollmentTestCase
from
milestones
import
api
as
milestones_api
from
milestones.tests.utils
import
MilestonesTestCaseMixin
from
openedx.core.lib.gating
import
api
as
gating_api
from
gating.api
import
_get_xblock_parent
,
evaluate_prerequisite
from
gating.api
import
evaluate_prerequisite
class
GatingTestCase
(
LoginEnrollmentTestCase
,
ModuleStoreTestCase
):
...
...
@@ -48,60 +48,14 @@ class GatingTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
self
.
seq1
=
ItemFactory
.
create
(
parent_location
=
self
.
chapter1
.
location
,
category
=
'sequential'
,
display_name
=
'
untitled sequential 1
'
display_name
=
'
gating sequential
'
)
self
.
seq2
=
ItemFactory
.
create
(
parent_location
=
self
.
chapter1
.
location
,
category
=
'sequential'
,
display_name
=
'
untitled sequential 2
'
display_name
=
'
gated sequential
'
)
# create vertical
self
.
vert1
=
ItemFactory
.
create
(
parent_location
=
self
.
seq1
.
location
,
category
=
'vertical'
,
display_name
=
'untitled vertical 1'
)
# create problem
self
.
prob1
=
ItemFactory
.
create
(
parent_location
=
self
.
vert1
.
location
,
category
=
'problem'
,
display_name
=
'untitled problem 1'
)
# create orphan
self
.
prob2
=
ItemFactory
.
create
(
parent_location
=
self
.
course
.
location
,
category
=
'problem'
,
display_name
=
'untitled problem 2'
)
class
TestGetXBlockParent
(
GatingTestCase
):
"""
Tests for the get_xblock_parent function
"""
def
test_get_direct_parent
(
self
):
""" Test test_get_direct_parent """
result
=
_get_xblock_parent
(
self
.
vert1
)
self
.
assertEqual
(
result
.
location
,
self
.
seq1
.
location
)
def
test_get_parent_with_category
(
self
):
""" Test test_get_parent_of_category """
result
=
_get_xblock_parent
(
self
.
vert1
,
'sequential'
)
self
.
assertEqual
(
result
.
location
,
self
.
seq1
.
location
)
result
=
_get_xblock_parent
(
self
.
vert1
,
'chapter'
)
self
.
assertEqual
(
result
.
location
,
self
.
chapter1
.
location
)
def
test_get_parent_none
(
self
):
""" Test test_get_parent_none """
result
=
_get_xblock_parent
(
self
.
vert1
,
'unit'
)
self
.
assertIsNone
(
result
)
@attr
(
shard
=
3
)
@ddt
...
...
@@ -114,62 +68,46 @@ class TestEvaluatePrerequisite(GatingTestCase, MilestonesTestCaseMixin):
super
(
TestEvaluatePrerequisite
,
self
)
.
setUp
()
self
.
user_dict
=
{
'id'
:
self
.
user
.
id
}
self
.
prereq_milestone
=
None
self
.
subsection_grade
=
Mock
(
location
=
self
.
seq1
.
location
)
def
_setup_gating_milestone
(
self
,
min_score
):
"""
Setup a gating milestone for testing
"""
gating_api
.
add_prerequisite
(
self
.
course
.
id
,
self
.
seq1
.
location
)
gating_api
.
set_required_content
(
self
.
course
.
id
,
self
.
seq2
.
location
,
self
.
seq1
.
location
,
min_score
)
self
.
prereq_milestone
=
gating_api
.
get_gating_milestone
(
self
.
course
.
id
,
self
.
seq1
.
location
,
'fulfills'
)
@patch
(
'gating.api.
get_module_scor
e'
)
@data
((
.
5
,
True
),
(
1
,
True
),
(
0
,
False
))
@patch
(
'gating.api.
_get_subsection_percentag
e'
)
@data
((
50
,
True
),
(
100
,
True
),
(
0
,
False
))
@unpack
def
test_min_score_achieved
(
self
,
module_score
,
result
,
mock_module_score
):
""" Test test_min_score_achieved """
def
test_min_score_achieved
(
self
,
module_score
,
result
,
mock_score
):
self
.
_setup_gating_milestone
(
50
)
mock_score
.
return_value
=
module_score
mock_module_score
.
return_value
=
module_score
evaluate_prerequisite
(
self
.
course
,
self
.
prob1
,
self
.
user
.
id
)
evaluate_prerequisite
(
self
.
course
,
self
.
subsection_grade
,
self
.
user
)
self
.
assertEqual
(
milestones_api
.
user_has_milestone
(
self
.
user_dict
,
self
.
prereq_milestone
),
result
)
@patch
(
'gating.api.log.warning'
)
@patch
(
'gating.api.
get_module_scor
e'
)
@data
((
.
5
,
False
),
(
1
,
True
))
@patch
(
'gating.api.
_get_subsection_percentag
e'
)
@data
((
50
,
False
),
(
100
,
True
))
@unpack
def
test_invalid_min_score
(
self
,
module_score
,
result
,
mock_module_score
,
mock_log
):
""" Test test_invalid_min_score """
def
test_invalid_min_score
(
self
,
module_score
,
result
,
mock_score
,
mock_log
):
self
.
_setup_gating_milestone
(
None
)
mock_score
.
return_value
=
module_score
mock_module_score
.
return_value
=
module_score
evaluate_prerequisite
(
self
.
course
,
self
.
prob1
,
self
.
user
.
id
)
evaluate_prerequisite
(
self
.
course
,
self
.
subsection_grade
,
self
.
user
)
self
.
assertEqual
(
milestones_api
.
user_has_milestone
(
self
.
user_dict
,
self
.
prereq_milestone
),
result
)
self
.
assertTrue
(
mock_log
.
called
)
@patch
(
'gating.api.get_module_score'
)
def
test_orphaned_xblock
(
self
,
mock_module_score
):
""" Test test_orphaned_xblock """
evaluate_prerequisite
(
self
.
course
,
self
.
prob2
,
self
.
user
.
id
)
self
.
assertFalse
(
mock_module_score
.
called
)
@patch
(
'gating.api.get_module_score'
)
def
test_no_prerequisites
(
self
,
mock_module_score
):
""" Test test_no_prerequisites """
evaluate_prerequisite
(
self
.
course
,
self
.
prob1
,
self
.
user
.
id
)
self
.
assertFalse
(
mock_module_score
.
called
)
@patch
(
'gating.api.get_module_score'
)
def
test_no_gated_content
(
self
,
mock_module_score
):
""" Test test_no_gated_content """
@patch
(
'gating.api._get_subsection_percentage'
)
def
test_no_prerequisites
(
self
,
mock_score
):
evaluate_prerequisite
(
self
.
course
,
self
.
subsection_grade
,
self
.
user
)
self
.
assertFalse
(
mock_score
.
called
)
# Setup gating milestones data
@patch
(
'gating.api._get_subsection_percentage'
)
def
test_no_gated_content
(
self
,
mock_score
):
gating_api
.
add_prerequisite
(
self
.
course
.
id
,
self
.
seq1
.
location
)
evaluate_prerequisite
(
self
.
course
,
self
.
prob1
,
self
.
user
.
id
)
self
.
assertFalse
(
mock_
module_
score
.
called
)
evaluate_prerequisite
(
self
.
course
,
self
.
subsection_grade
,
self
.
user
)
self
.
assertFalse
(
mock_score
.
called
)
lms/djangoapps/gating/tests/test_integration.py
0 → 100644
View file @
cfb032a5
"""
Integration tests for gated content.
"""
import
ddt
from
nose.plugins.attrib
import
attr
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
from
lms.djangoapps.courseware.access
import
has_access
from
lms.djangoapps.grades.tests.utils
import
answer_problem
from
lms.djangoapps.grades.new.course_grade
import
CourseGradeFactory
from
milestones
import
api
as
milestones_api
from
milestones.tests.utils
import
MilestonesTestCaseMixin
from
openedx.core.djangolib.testing.utils
import
get_mock_request
from
openedx.core.lib.gating
import
api
as
gating_api
from
request_cache.middleware
import
RequestCache
from
student.tests.factories
import
UserFactory
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.tests.django_utils
import
SharedModuleStoreTestCase
@attr
(
shard
=
3
)
@ddt.ddt
class
TestGatedContent
(
MilestonesTestCaseMixin
,
SharedModuleStoreTestCase
):
"""
Base TestCase class for setting up a basic course structure
and testing the gating feature
"""
@classmethod
def
setUpClass
(
cls
):
super
(
TestGatedContent
,
cls
)
.
setUpClass
()
cls
.
set_up_course
()
def
setUp
(
self
):
super
(
TestGatedContent
,
self
)
.
setUp
()
self
.
setup_gating_milestone
(
50
)
self
.
non_staff_user
=
UserFactory
()
self
.
staff_user
=
UserFactory
(
is_staff
=
True
,
is_superuser
=
True
)
self
.
request
=
get_mock_request
(
self
.
non_staff_user
)
@classmethod
def
set_up_course
(
cls
):
"""
Set up a course for testing gated content.
"""
cls
.
course
=
CourseFactory
.
create
(
org
=
'edX'
,
number
=
'EDX101'
,
run
=
'EDX101_RUN1'
,
display_name
=
'edX 101'
)
with
modulestore
()
.
bulk_operations
(
cls
.
course
.
id
):
cls
.
course
.
enable_subsection_gating
=
True
grading_policy
=
{
"GRADER"
:
[{
"type"
:
"Homework"
,
"min_count"
:
3
,
"drop_count"
:
0
,
"short_label"
:
"HW"
,
"weight"
:
1.0
}]
}
cls
.
course
.
grading_policy
=
grading_policy
cls
.
course
.
save
()
cls
.
store
.
update_item
(
cls
.
course
,
0
)
# create chapter
cls
.
chapter1
=
ItemFactory
.
create
(
parent_location
=
cls
.
course
.
location
,
category
=
'chapter'
,
display_name
=
'chapter 1'
)
# create sequentials
cls
.
seq1
=
ItemFactory
.
create
(
parent_location
=
cls
.
chapter1
.
location
,
category
=
'sequential'
,
display_name
=
'gating sequential 1'
,
graded
=
True
,
format
=
'Homework'
,
)
cls
.
seq2
=
ItemFactory
.
create
(
parent_location
=
cls
.
chapter1
.
location
,
category
=
'sequential'
,
display_name
=
'gated sequential 2'
,
graded
=
True
,
format
=
'Homework'
,
)
cls
.
seq3
=
ItemFactory
.
create
(
parent_location
=
cls
.
chapter1
.
location
,
category
=
'sequential'
,
display_name
=
'sequential 3'
,
graded
=
True
,
format
=
'Homework'
,
)
# create problem
cls
.
gating_prob1
=
ItemFactory
.
create
(
parent_location
=
cls
.
seq1
.
location
,
category
=
'problem'
,
display_name
=
'gating problem 1'
,
)
cls
.
gated_prob2
=
ItemFactory
.
create
(
parent_location
=
cls
.
seq2
.
location
,
category
=
'problem'
,
display_name
=
'gated problem 2'
,
)
cls
.
prob3
=
ItemFactory
.
create
(
parent_location
=
cls
.
seq3
.
location
,
category
=
'problem'
,
display_name
=
'problem 3'
,
)
def
setup_gating_milestone
(
self
,
min_score
):
"""
Setup a gating milestone for testing.
Gating content: seq1 (must be fulfilled before access to seq2)
Gated content: seq2 (requires completion of seq1 before access)
"""
gating_api
.
add_prerequisite
(
self
.
course
.
id
,
str
(
self
.
seq1
.
location
))
gating_api
.
set_required_content
(
self
.
course
.
id
,
str
(
self
.
seq2
.
location
),
str
(
self
.
seq1
.
location
),
min_score
)
self
.
prereq_milestone
=
gating_api
.
get_gating_milestone
(
self
.
course
.
id
,
self
.
seq1
.
location
,
'fulfills'
)
def
assert_access_to_gated_content
(
self
,
user
,
expected_access
):
"""
Verifies access to gated content for the given user is as expected.
"""
# clear the request cache to flush any cached access results
RequestCache
.
clear_request_cache
()
# access to gating content (seq1) remains constant
self
.
assertTrue
(
bool
(
has_access
(
user
,
'load'
,
self
.
seq1
,
self
.
course
.
id
)))
# access to gated content (seq2) is as expected
self
.
assertEquals
(
bool
(
has_access
(
user
,
'load'
,
self
.
seq2
,
self
.
course
.
id
)),
expected_access
)
def
assert_user_has_prereq_milestone
(
self
,
user
,
expected_has_milestone
):
"""
Verifies whether or not the user has the prereq milestone
"""
self
.
assertEquals
(
milestones_api
.
user_has_milestone
({
'id'
:
user
.
id
},
self
.
prereq_milestone
),
expected_has_milestone
,
)
def
assert_course_grade
(
self
,
user
,
expected_percent
):
"""
Verifies the given user's course grade is the expected percentage.
Also verifies the user's grade information contains values for
all problems in the course, whether or not they are currently
gated.
"""
course_grade
=
CourseGradeFactory
()
.
create
(
user
,
self
.
course
)
for
prob
in
[
self
.
gating_prob1
,
self
.
gated_prob2
,
self
.
prob3
]:
self
.
assertIn
(
prob
.
location
,
course_grade
.
locations_to_scores
)
self
.
assertEquals
(
course_grade
.
percent
,
expected_percent
)
def
test_gated_for_nonstaff
(
self
):
self
.
assert_user_has_prereq_milestone
(
self
.
non_staff_user
,
expected_has_milestone
=
False
)
self
.
assert_access_to_gated_content
(
self
.
non_staff_user
,
expected_access
=
False
)
def
test_not_gated_for_staff
(
self
):
self
.
assert_user_has_prereq_milestone
(
self
.
staff_user
,
expected_has_milestone
=
False
)
self
.
assert_access_to_gated_content
(
self
.
staff_user
,
expected_access
=
True
)
def
test_gated_content_always_in_grades
(
self
):
# start with a grade from a non-gated subsection
answer_problem
(
self
.
course
,
self
.
request
,
self
.
prob3
,
10
,
10
)
# verify gated status and overall course grade percentage
self
.
assert_user_has_prereq_milestone
(
self
.
non_staff_user
,
expected_has_milestone
=
False
)
self
.
assert_access_to_gated_content
(
self
.
non_staff_user
,
expected_access
=
False
)
self
.
assert_course_grade
(
self
.
non_staff_user
,
.
33
)
# fulfill the gated requirements
answer_problem
(
self
.
course
,
self
.
request
,
self
.
gating_prob1
,
10
,
10
)
# verify gated status and overall course grade percentage
self
.
assert_user_has_prereq_milestone
(
self
.
non_staff_user
,
expected_has_milestone
=
True
)
self
.
assert_access_to_gated_content
(
self
.
non_staff_user
,
expected_access
=
True
)
self
.
assert_course_grade
(
self
.
non_staff_user
,
.
67
)
@ddt.data
((
1
,
1
,
True
),
(
1
,
2
,
True
),
(
1
,
3
,
False
),
(
0
,
1
,
False
))
@ddt.unpack
def
test_ungating_when_fulfilled
(
self
,
earned
,
max_possible
,
result
):
self
.
assert_user_has_prereq_milestone
(
self
.
non_staff_user
,
expected_has_milestone
=
False
)
self
.
assert_access_to_gated_content
(
self
.
non_staff_user
,
expected_access
=
False
)
answer_problem
(
self
.
course
,
self
.
request
,
self
.
gating_prob1
,
earned
,
max_possible
)
self
.
assert_user_has_prereq_milestone
(
self
.
non_staff_user
,
expected_has_milestone
=
result
)
self
.
assert_access_to_gated_content
(
self
.
non_staff_user
,
expected_access
=
result
)
lms/djangoapps/gating/tests/test_signals.py
View file @
cfb032a5
"""
Unit tests for gating.signals module
"""
from
mock
import
patch
from
mock
import
patch
,
Mock
from
student.tests.factories
import
UserFactory
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_score_changed
from
gating.signals
import
evaluate_subsection_gated_milestones
class
TestHandleScoreChanged
(
ModuleStoreTestCase
):
...
...
@@ -19,32 +19,26 @@ class TestHandleScoreChanged(ModuleStoreTestCase):
super
(
TestHandleScoreChanged
,
self
)
.
setUp
()
self
.
course
=
CourseFactory
.
create
(
org
=
'TestX'
,
number
=
'TS01'
,
run
=
'2016_Q1'
)
self
.
user
=
UserFactory
.
create
()
self
.
test_usage_key
=
self
.
course
.
location
self
.
subsection_grade
=
Mock
()
@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 """
@patch
(
'lms.djangoapps.gating.api.gating_api.get_gating_milestone'
)
def
test_gating_enabled
(
self
,
mock_gating_milestone
):
self
.
course
.
enable_subsection_gating
=
True
modulestore
()
.
update_item
(
self
.
course
,
0
)
handle_score_changed
(
evaluate_subsection_gated_milestones
(
sender
=
None
,
points_possible
=
1
,
points_earned
=
1
,
user_id
=
self
.
user
.
id
,
course_id
=
unicode
(
self
.
course
.
id
),
usage_id
=
unicode
(
self
.
test_usage_key
)
user
=
self
.
user
,
course
=
self
.
course
,
subsection_grade
=
self
.
subsection_grade
,
)
mock_evaluate
.
assert_called_with
(
self
.
course
,
self
.
course
,
self
.
user
.
id
)
# pylint: disable=no-member
self
.
assertTrue
(
mock_gating_milestone
.
called
)
@patch
(
'gating.signals.gating_api.evaluate_prerequisite'
)
def
test_gating_disabled
(
self
,
mock_evaluate
):
""" Test evaluate_prerequisite is not called when course.enable_subsection_gating is False """
handle_score_changed
(
@patch
(
'lms.djangoapps.gating.api.gating_api.get_gating_milestone'
)
def
test_gating_disabled
(
self
,
mock_gating_milestone
):
evaluate_subsection_gated_milestones
(
sender
=
None
,
points_possible
=
1
,
points_earned
=
1
,
user_id
=
self
.
user
.
id
,
course_id
=
unicode
(
self
.
course
.
id
),
usage_id
=
unicode
(
self
.
test_usage_key
)
user
=
self
.
user
,
course
=
self
.
course
,
subsection_grade
=
self
.
subsection_grade
,
)
mock_evaluate
.
assert_not_called
(
)
self
.
assertFalse
(
mock_gating_milestone
.
called
)
lms/djangoapps/grades/module_grades.py
deleted
100644 → 0
View file @
173e5922
"""
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
)
lms/djangoapps/grades/tests/test_grades.py
View file @
cfb032a5
...
...
@@ -8,9 +8,6 @@ from mock import patch
from
nose.plugins.attrib
import
attr
from
capa.tests.response_xml_factory
import
MultipleChoiceResponseXMLFactory
from
courseware.model_data
import
set_score
from
courseware.tests.helpers
import
LoginEnrollmentTestCase
from
lms.djangoapps.course_blocks.api
import
get_course_blocks
from
openedx.core.djangoapps.content.block_structure.factory
import
BlockStructureFactory
from
openedx.core.djangolib.testing.utils
import
get_mock_request
...
...
@@ -22,7 +19,6 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from
xmodule.modulestore.tests.django_utils
import
SharedModuleStoreTestCase
from
.utils
import
answer_problem
from
..module_grades
import
get_module_score
from
..new.course_grade
import
CourseGradeFactory
from
..new.subsection_grade
import
SubsectionGradeFactory
...
...
@@ -334,195 +330,3 @@ 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
()
with
cls
.
store
.
bulk_operations
(
cls
.
course
.
id
):
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_mock_request
(
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
)
lms/djangoapps/mobile_api/tests/test_milestones.py
View file @
cfb032a5
...
...
@@ -11,7 +11,6 @@ from util.milestones_helpers import (
add_prerequisite_course
,
fulfill_course_milestone
,
)
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
...
...
@@ -85,28 +84,36 @@ class MobileAPIMilestonesMixin(object):
def
_add_entrance_exam
(
self
):
""" Sets up entrance exam """
self
.
course
.
entrance_exam_enabled
=
True
self
.
entrance_exam
=
ItemFactory
.
create
(
# pylint: disable=attribute-defined-outside-init
parent
=
self
.
course
,
category
=
"chapter"
,
display_name
=
"Entrance Exam Chapter"
,
is_entrance_exam
=
True
,
in_entrance_exam
=
True
)
self
.
problem_1
=
ItemFactory
.
create
(
# pylint: disable=attribute-defined-outside-init
parent
=
self
.
entrance_exam
,
category
=
'problem'
,
display_name
=
"The Only Exam Problem"
,
graded
=
True
,
in_entrance_exam
=
True
)
add_entrance_exam_milestone
(
self
.
course
,
self
.
entrance_exam
)
self
.
course
.
entrance_exam_minimum_score_pct
=
0.50
self
.
course
.
entrance_exam_id
=
unicode
(
self
.
entrance_exam
.
location
)
modulestore
()
.
update_item
(
self
.
course
,
self
.
user
.
id
)
with
self
.
store
.
bulk_operations
(
self
.
course
.
id
):
self
.
course
.
entrance_exam_enabled
=
True
self
.
entrance_exam
=
ItemFactory
.
create
(
# pylint: disable=attribute-defined-outside-init
parent
=
self
.
course
,
category
=
"chapter"
,
display_name
=
"Entrance Exam Chapter"
,
is_entrance_exam
=
True
,
in_entrance_exam
=
True
,
)
self
.
subsection_1
=
ItemFactory
.
create
(
# pylint: disable=attribute-defined-outside-init
parent
=
self
.
entrance_exam
,
category
=
'sequential'
,
display_name
=
"The Only Exam Sequential"
,
graded
=
True
,
in_entrance_exam
=
True
,
)
self
.
problem_1
=
ItemFactory
.
create
(
# pylint: disable=attribute-defined-outside-init
parent
=
self
.
subsection_1
,
category
=
'problem'
,
display_name
=
"The Only Exam Problem"
,
graded
=
True
,
in_entrance_exam
=
True
,
)
add_entrance_exam_milestone
(
self
.
course
,
self
.
entrance_exam
)
self
.
course
.
entrance_exam_minimum_score_pct
=
0.50
self
.
course
.
entrance_exam_id
=
unicode
(
self
.
entrance_exam
.
location
)
self
.
store
.
update_item
(
self
.
course
,
self
.
user
.
id
)
def
_add_prerequisite_course
(
self
):
""" Helper method to set up the prerequisite course """
...
...
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