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
6a954c1e
Commit
6a954c1e
authored
Mar 17, 2017
by
Nimisha Asthagiri
Committed by
GitHub
Mar 17, 2017
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #14690 from edx/neem/fix-entrance-exam-grades
Fix grading for Entrance Exams
parents
a8277a6a
54d938ca
Show whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
138 additions
and
199 deletions
+138
-199
common/lib/xmodule/xmodule/course_module.py
+3
-0
lms/djangoapps/courseware/entrance_exams.py
+3
-105
lms/djangoapps/courseware/module_render.py
+3
-3
lms/djangoapps/courseware/tabs.py
+2
-2
lms/djangoapps/courseware/tests/test_entrance_exam.py
+19
-33
lms/djangoapps/courseware/tests/test_submitting_problems.py
+5
-7
lms/djangoapps/courseware/views/index.py
+18
-12
lms/djangoapps/courseware/views/views.py
+3
-3
lms/djangoapps/gating/api.py
+42
-23
lms/djangoapps/gating/signals.py
+16
-1
lms/djangoapps/grades/new/course_grade.py
+24
-10
No files found.
common/lib/xmodule/xmodule/course_module.py
View file @
6a954c1e
...
...
@@ -675,6 +675,9 @@ class CourseFields(object):
scope
=
Scope
.
settings
,
)
# Note: Although users enter the entrance exam minimum score
# as a percentage value, it is internally converted and stored
# as a decimal value less than 1.
entrance_exam_minimum_score_pct
=
Float
(
display_name
=
_
(
"Entrance Exam Minimum Score (
%
)"
),
help
=
_
(
...
...
lms/djangoapps/courseware/entrance_exams.py
View file @
6a954c1e
...
...
@@ -43,118 +43,16 @@ def user_can_skip_entrance_exam(user, course):
return
False
def
user_has_passed_entrance_exam
(
request
,
course
):
def
user_has_passed_entrance_exam
(
user
,
course
):
"""
Checks to see if the user has attained a sufficient score to pass the exam
Begin by short-circuiting if the course does not have an entrance exam
"""
if
not
course_has_entrance_exam
(
course
):
return
True
if
not
request
.
user
.
is_authenticated
():
return
False
entrance_exam_score
=
get_entrance_exam_score
(
request
,
course
)
if
entrance_exam_score
>=
course
.
entrance_exam_minimum_score_pct
:
return
True
return
False
# pylint: disable=invalid-name
def
user_must_complete_entrance_exam
(
request
,
user
,
course
):
"""
Some courses can be gated on an Entrance Exam, which is a specially-configured chapter module which
presents users with a problem set which they must complete. This particular workflow determines
whether or not the user is allowed to clear the Entrance Exam gate and access the rest of the course.
"""
# First, let's see if the user is allowed to skip
if
user_can_skip_entrance_exam
(
user
,
course
):
return
False
# If they can't actually skip the exam, we'll need to see if they've already passed it
if
user_has_passed_entrance_exam
(
request
,
course
):
if
not
user
.
is_authenticated
():
return
False
# Can't skip, haven't passed, must take the exam
return
True
def
_calculate_entrance_exam_score
(
user
,
course_descriptor
,
exam_modules
):
"""
Calculates the score (percent) of the entrance exam using the provided modules
"""
student_module_dict
=
{}
scores_client
=
ScoresClient
(
course_descriptor
.
id
,
user
.
id
)
# removing branch and version from exam modules locator
# otherwise student module would not return scores since module usage keys would not match
locations
=
[
BlockUsageLocator
(
course_key
=
course_descriptor
.
id
,
block_type
=
exam_module
.
location
.
block_type
,
block_id
=
exam_module
.
location
.
block_id
)
if
isinstance
(
exam_module
.
location
,
BlockUsageLocator
)
and
exam_module
.
location
.
version
else
exam_module
.
location
for
exam_module
in
exam_modules
]
scores_client
.
fetch_scores
(
locations
)
# Iterate over all of the exam modules to get score of user for each of them
for
index
,
exam_module
in
enumerate
(
exam_modules
):
exam_module_score
=
scores_client
.
get
(
locations
[
index
])
if
exam_module_score
:
student_module_dict
[
unicode
(
locations
[
index
])]
=
{
'grade'
:
exam_module_score
.
correct
,
'max_grade'
:
exam_module_score
.
total
}
exam_percentage
=
0
module_percentages
=
[]
ignore_categories
=
[
'course'
,
'chapter'
,
'sequential'
,
'vertical'
]
for
index
,
module
in
enumerate
(
exam_modules
):
if
module
.
graded
and
module
.
category
not
in
ignore_categories
:
module_percentage
=
0
module_location
=
unicode
(
locations
[
index
])
if
module_location
in
student_module_dict
and
student_module_dict
[
module_location
][
'max_grade'
]:
student_module
=
student_module_dict
[
module_location
]
module_percentage
=
student_module
[
'grade'
]
/
student_module
[
'max_grade'
]
module_percentages
.
append
(
module_percentage
)
if
module_percentages
:
exam_percentage
=
sum
(
module_percentages
)
/
float
(
len
(
module_percentages
))
return
exam_percentage
def
get_entrance_exam_score
(
request
,
course
):
"""
Gather the set of modules which comprise the entrance exam
Note that 'request' may not actually be a genuine request, due to the
circular nature of module_render calling entrance_exams and get_module_for_descriptor
being used here. In some use cases, the caller is actually mocking a request, although
in these scenarios the 'user' child object can be trusted and used as expected.
It's a much larger refactoring job to break this legacy mess apart, unfortunately.
"""
exam_key
=
UsageKey
.
from_string
(
course
.
entrance_exam_id
)
exam_descriptor
=
modulestore
()
.
get_item
(
exam_key
)
def
inner_get_module
(
descriptor
):
"""
Delegate to get_module_for_descriptor (imported here to avoid circular reference)
"""
from
courseware.module_render
import
get_module_for_descriptor
field_data_cache
=
FieldDataCache
([
descriptor
],
course
.
id
,
request
.
user
)
return
get_module_for_descriptor
(
request
.
user
,
request
,
descriptor
,
field_data_cache
,
course
.
id
,
course
=
course
)
exam_module_generators
=
yield_dynamic_descriptor_descendants
(
exam_descriptor
,
request
.
user
.
id
,
inner_get_module
)
exam_modules
=
[
module
for
module
in
exam_module_generators
]
return
_calculate_entrance_exam_score
(
request
.
user
,
course
,
exam_modules
)
return
get_entrance_exam_content
(
user
,
course
)
is
None
def
get_entrance_exam_content
(
user
,
course
):
...
...
lms/djangoapps/courseware/module_render.py
View file @
6a954c1e
...
...
@@ -33,7 +33,7 @@ from xblock.reference.plugins import FSService
import
static_replace
from
courseware.access
import
has_access
,
get_user_role
from
courseware.entrance_exams
import
(
user_
must_complete
_entrance_exam
,
user_
can_skip
_entrance_exam
,
user_has_passed_entrance_exam
)
from
courseware.masquerade
import
(
...
...
@@ -164,7 +164,7 @@ def toc_for_course(user, request, course, active_chapter, active_section, field_
required_content
=
milestones_helpers
.
get_required_content
(
course
,
user
)
# The user may not actually have to complete the entrance exam, if one is required
if
not
user_must_complete_entrance_exam
(
request
,
user
,
course
):
if
user_can_skip_entrance_exam
(
user
,
course
):
required_content
=
[
content
for
content
in
required_content
if
not
content
==
course
.
entrance_exam_id
]
previous_of_active_section
,
next_of_active_section
=
None
,
None
...
...
@@ -990,7 +990,7 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, course
and
course
\
and
getattr
(
course
,
'entrance_exam_enabled'
,
False
)
\
and
getattr
(
instance
,
'in_entrance_exam'
,
False
):
ee_data
=
{
'entrance_exam_passed'
:
user_has_passed_entrance_exam
(
request
,
course
)}
ee_data
=
{
'entrance_exam_passed'
:
user_has_passed_entrance_exam
(
request
.
user
,
course
)}
resp
=
append_data_to_webob_response
(
resp
,
ee_data
)
except
NoSuchHandlerError
:
...
...
lms/djangoapps/courseware/tabs.py
View file @
6a954c1e
...
...
@@ -6,7 +6,7 @@ from django.conf import settings
from
django.utils.translation
import
ugettext
as
_
,
ugettext_noop
from
courseware.access
import
has_access
from
courseware.entrance_exams
import
user_
must_complete
_entrance_exam
from
courseware.entrance_exams
import
user_
can_skip
_entrance_exam
from
openedx.core.lib.course_tabs
import
CourseTabPluginManager
from
student.models
import
CourseEnrollment
from
xmodule.tabs
import
CourseTab
,
CourseTabList
,
key_checker
...
...
@@ -294,7 +294,7 @@ def get_course_tab_list(request, course):
# If the user has to take an entrance exam, we'll need to hide away all but the
# "Courseware" tab. The tab is then renamed as "Entrance Exam".
course_tab_list
=
[]
must_complete_ee
=
user_must_complete_entrance_exam
(
request
,
user
,
course
)
must_complete_ee
=
not
user_can_skip_entrance_exam
(
user
,
course
)
for
tab
in
xmodule_tab_list
:
if
must_complete_ee
:
# Hide all of the tabs except for 'Courseware'
...
...
lms/djangoapps/courseware/tests/test_entrance_exam.py
View file @
6a954c1e
...
...
@@ -17,7 +17,6 @@ from courseware.tests.helpers import (
from
courseware.entrance_exams
import
(
course_has_entrance_exam
,
get_entrance_exam_content
,
get_entrance_exam_score
,
user_can_skip_entrance_exam
,
user_has_passed_entrance_exam
,
)
...
...
@@ -281,32 +280,14 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
"""
exam_chapter
=
get_entrance_exam_content
(
self
.
request
.
user
,
self
.
course
)
self
.
assertEqual
(
exam_chapter
.
url_name
,
self
.
entrance_exam
.
url_name
)
self
.
assertFalse
(
user_has_passed_entrance_exam
(
self
.
request
,
self
.
course
))
self
.
assertFalse
(
user_has_passed_entrance_exam
(
self
.
request
.
user
,
self
.
course
))
answer_entrance_exam_problem
(
self
.
course
,
self
.
request
,
self
.
problem_1
)
answer_entrance_exam_problem
(
self
.
course
,
self
.
request
,
self
.
problem_2
)
exam_chapter
=
get_entrance_exam_content
(
self
.
request
.
user
,
self
.
course
)
self
.
assertEqual
(
exam_chapter
,
None
)
self
.
assertTrue
(
user_has_passed_entrance_exam
(
self
.
request
,
self
.
course
))
def
test_entrance_exam_score
(
self
):
"""
test entrance exam score. we will hit the method get_entrance_exam_score to verify exam score.
"""
# One query is for getting the list of disabled XBlocks (which is
# then stored in the request).
with
self
.
assertNumQueries
(
1
):
exam_score
=
get_entrance_exam_score
(
self
.
request
,
self
.
course
)
self
.
assertEqual
(
exam_score
,
0
)
answer_entrance_exam_problem
(
self
.
course
,
self
.
request
,
self
.
problem_1
)
answer_entrance_exam_problem
(
self
.
course
,
self
.
request
,
self
.
problem_2
)
with
self
.
assertNumQueries
(
1
):
exam_score
=
get_entrance_exam_score
(
self
.
request
,
self
.
course
)
# 50 percent exam score should be achieved.
self
.
assertGreater
(
exam_score
*
100
,
50
)
self
.
assertTrue
(
user_has_passed_entrance_exam
(
self
.
request
.
user
,
self
.
course
))
def
test_entrance_exam_requirement_message
(
self
):
"""
...
...
@@ -332,6 +313,10 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
minimum_score_pct
=
29
self
.
course
.
entrance_exam_minimum_score_pct
=
float
(
minimum_score_pct
)
/
100
modulestore
()
.
update_item
(
self
.
course
,
self
.
request
.
user
.
id
)
# pylint: disable=no-member
# answer the problem so it results in only 20% correct.
answer_entrance_exam_problem
(
self
.
course
,
self
.
request
,
self
.
problem_1
,
value
=
1
,
max_value
=
5
)
url
=
reverse
(
'courseware_section'
,
kwargs
=
{
...
...
@@ -342,9 +327,11 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
)
resp
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
resp
.
status_code
,
200
)
self
.
assertIn
(
'To access course materials, you must score {required_score}
%
or higher'
.
format
(
required_score
=
minimum_score_pct
),
resp
.
content
)
self
.
assertIn
(
'To access course materials, you must score {}
%
or higher'
.
format
(
minimum_score_pct
),
resp
.
content
)
self
.
assertIn
(
'Your current score is 20
%
.'
,
resp
.
content
)
def
test_entrance_exam_requirement_message_hidden
(
self
):
"""
...
...
@@ -388,7 +375,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
resp
=
self
.
client
.
get
(
url
)
self
.
assertNotIn
(
'To access course materials, you must score'
,
resp
.
content
)
self
.
assertIn
(
'You have passed the entrance exam.'
,
resp
.
content
)
self
.
assertIn
(
'You
r score is 100
%
. You
have passed the entrance exam.'
,
resp
.
content
)
self
.
assertIn
(
'Lesson 1'
,
resp
.
content
)
def
test_entrance_exam_gating
(
self
):
...
...
@@ -450,7 +437,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
for
toc_section
in
self
.
expected_unlocked_toc
:
self
.
assertIn
(
toc_section
,
unlocked_toc
)
@patch
(
'courseware.entrance_exams.user_has_passed_entrance_exam'
,
Mock
(
return_value
=
False
))
def
test_courseware_page_access_without_passing_entrance_exam
(
self
):
"""
Test courseware access page without passing entrance exam
...
...
@@ -468,7 +454,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
})
self
.
assertRedirects
(
response
,
expected_url
,
status_code
=
302
,
target_status_code
=
200
)
@patch
(
'courseware.entrance_exams.user_has_passed_entrance_exam'
,
Mock
(
return_value
=
False
))
def
test_courseinfo_page_access_without_passing_entrance_exam
(
self
):
"""
Test courseware access page without passing entrance exam
...
...
@@ -481,12 +466,11 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
exam_url
=
response
.
get
(
'Location'
)
self
.
assertRedirects
(
response
,
exam_url
)
@patch
(
'courseware.entrance_exams.
user_has_passed_entrance_exam'
,
Mock
(
return_value
=
Tru
e
))
@patch
(
'courseware.entrance_exams.
get_entrance_exam_content'
,
Mock
(
return_value
=
Non
e
))
def
test_courseware_page_access_after_passing_entrance_exam
(
self
):
"""
Test courseware access page after passing entrance exam
"""
# Mocking get_required_content with empty list to assume user has passed entrance exam
self
.
_assert_chapter_loaded
(
self
.
course
,
self
.
chapter
)
@patch
(
'util.milestones_helpers.get_required_content'
,
Mock
(
return_value
=
[
'a value'
]))
...
...
@@ -528,7 +512,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
Test has_passed_entrance_exam method with anonymous user
"""
self
.
request
.
user
=
self
.
anonymous_user
self
.
assertFalse
(
user_has_passed_entrance_exam
(
self
.
request
,
self
.
course
))
self
.
assertFalse
(
user_has_passed_entrance_exam
(
self
.
request
.
user
,
self
.
course
))
def
test_course_has_entrance_exam_missing_exam_id
(
self
):
course
=
CourseFactory
.
create
(
...
...
@@ -541,7 +525,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
def
test_user_has_passed_entrance_exam_short_circuit_missing_exam
(
self
):
course
=
CourseFactory
.
create
(
)
self
.
assertTrue
(
user_has_passed_entrance_exam
(
self
.
request
,
course
))
self
.
assertTrue
(
user_has_passed_entrance_exam
(
self
.
request
.
user
,
course
))
@patch.dict
(
"django.conf.settings.FEATURES"
,
{
'ENABLE_MASQUERADE'
:
False
})
def
test_entrance_exam_xblock_response
(
self
):
...
...
@@ -599,7 +583,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
return
toc
[
'chapters'
]
def
answer_entrance_exam_problem
(
course
,
request
,
problem
,
user
=
None
):
def
answer_entrance_exam_problem
(
course
,
request
,
problem
,
user
=
None
,
value
=
1
,
max_value
=
1
):
"""
Takes a required milestone `problem` in a `course` and fulfills it.
...
...
@@ -608,11 +592,13 @@ def answer_entrance_exam_problem(course, request, problem, user=None):
request (Request): request Object
problem (xblock): xblock object, the problem to be fulfilled
user (User): User object in case it is different from request.user
value (int): raw_earned value of the problem
max_value (int): raw_possible value of the problem
"""
if
not
user
:
user
=
request
.
user
grade_dict
=
{
'value'
:
1
,
'max_value'
:
1
,
'user_id'
:
user
.
id
}
grade_dict
=
{
'value'
:
value
,
'max_value'
:
max_value
,
'user_id'
:
user
.
id
}
field_data_cache
=
FieldDataCache
.
cache_for_descriptor_descendents
(
course
.
id
,
user
,
...
...
lms/djangoapps/courseware/tests/test_submitting_problems.py
View file @
6a954c1e
...
...
@@ -296,13 +296,11 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl
"""
Returns SubsectionGrade for given url.
"""
# list of grade summaries for each section
sections_list
=
[]
for
chapter
in
self
.
get_course_grade
()
.
chapter_grades
:
sections_list
.
extend
(
chapter
[
'sections'
])
# get the first section that matches the url (there should only be one)
return
next
(
section
for
section
in
sections_list
if
section
.
url_name
==
hw_url_name
)
for
chapter
in
self
.
get_course_grade
()
.
chapter_grades
.
itervalues
():
for
section
in
chapter
[
'sections'
]:
if
section
.
url_name
==
hw_url_name
:
return
section
return
None
def
score_for_hw
(
self
,
hw_url_name
):
"""
...
...
lms/djangoapps/courseware/views/index.py
View file @
6a954c1e
...
...
@@ -22,7 +22,8 @@ import logging
import
newrelic.agent
import
urllib
from
xblock.fragment
import
Fragment
from
lms.djangoapps.gating.api
import
get_entrance_exam_score_ratio
,
get_entrance_exam_usage_key
from
lms.djangoapps.grades.new.course_grade
import
CourseGradeFactory
from
opaque_keys.edx.keys
import
CourseKey
from
openedx.core.djangoapps.lang_pref
import
LANGUAGE_KEY
from
openedx.core.djangoapps.user_api.preferences.api
import
get_user_preference
...
...
@@ -31,11 +32,12 @@ from shoppingcart.models import CourseRegistrationCode
from
student.models
import
CourseEnrollment
from
student.views
import
is_course_blocked
from
student.roles
import
GlobalStaff
from
survey.utils
import
must_answer_survey
from
util.enterprise_helpers
import
get_enterprise_consent_url
from
util.views
import
ensure_valid_course_key
from
xblock.fragment
import
Fragment
from
xmodule.modulestore.django
import
modulestore
from
xmodule.x_module
import
STUDENT_VIEW
from
survey.utils
import
must_answer_survey
from
..access
import
has_access
,
_adjust_start_date_for_beta_testers
from
..access_utils
import
in_preview_mode
...
...
@@ -43,9 +45,8 @@ from ..courses import get_studio_url, get_course_with_access
from
..entrance_exams
import
(
course_has_entrance_exam
,
get_entrance_exam_content
,
get_entrance_exam_score
,
user_has_passed_entrance_exam
,
user_
must_complete
_entrance_exam
,
user_
can_skip
_entrance_exam
,
)
from
..exceptions
import
Redirect
from
..masquerade
import
setup_masquerade
...
...
@@ -276,10 +277,7 @@ class CoursewareIndex(View):
"""
Check to see if an Entrance Exam is required for the user.
"""
if
(
course_has_entrance_exam
(
self
.
course
)
and
user_must_complete_entrance_exam
(
self
.
request
,
self
.
effective_user
,
self
.
course
)
):
if
not
user_can_skip_entrance_exam
(
self
.
effective_user
,
self
.
course
):
exam_chapter
=
get_entrance_exam_content
(
self
.
effective_user
,
self
.
course
)
if
exam_chapter
and
exam_chapter
.
get_children
():
exam_section
=
exam_chapter
.
get_children
()[
0
]
...
...
@@ -428,10 +426,7 @@ class CoursewareIndex(View):
)
# entrance exam data
if
course_has_entrance_exam
(
self
.
course
):
if
getattr
(
self
.
chapter
,
'is_entrance_exam'
,
False
):
courseware_context
[
'entrance_exam_current_score'
]
=
get_entrance_exam_score
(
self
.
request
,
self
.
course
)
courseware_context
[
'entrance_exam_passed'
]
=
user_has_passed_entrance_exam
(
self
.
request
,
self
.
course
)
self
.
_add_entrance_exam_to_context
(
courseware_context
)
# staff masquerading data
now
=
datetime
.
now
(
UTC
())
...
...
@@ -469,6 +464,17 @@ class CoursewareIndex(View):
return
courseware_context
def
_add_entrance_exam_to_context
(
self
,
courseware_context
):
"""
Adds entrance exam related information to the given context.
"""
if
course_has_entrance_exam
(
self
.
course
)
and
getattr
(
self
.
chapter
,
'is_entrance_exam'
,
False
):
courseware_context
[
'entrance_exam_passed'
]
=
user_has_passed_entrance_exam
(
self
.
effective_user
,
self
.
course
)
courseware_context
[
'entrance_exam_current_score'
]
=
get_entrance_exam_score_ratio
(
CourseGradeFactory
()
.
create
(
self
.
effective_user
,
self
.
course
),
get_entrance_exam_usage_key
(
self
.
course
),
)
def
_create_section_context
(
self
,
previous_of_active_section
,
next_of_active_section
):
"""
Returns and creates the rendering context for the section.
...
...
lms/djangoapps/courseware/views/views.py
View file @
6a954c1e
...
...
@@ -101,7 +101,7 @@ from xmodule.modulestore.django import modulestore
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
,
NoPathToItem
from
xmodule.tabs
import
CourseTabList
from
xmodule.x_module
import
STUDENT_VIEW
from
..entrance_exams
import
user_
must_complete
_entrance_exam
from
..entrance_exams
import
user_
can_skip
_entrance_exam
from
..module_render
import
get_module_for_descriptor
,
get_module
,
get_module_by_usage_id
from
web_fragments.fragment
import
Fragment
...
...
@@ -336,7 +336,7 @@ def course_info(request, course_id):
# If the user needs to take an entrance exam to access this course, then we'll need
# to send them to that specific course module before allowing them into other areas
if
user_must_complete_entrance_exam
(
request
,
user
,
course
):
if
not
user_can_skip_entrance_exam
(
user
,
course
):
return
redirect
(
reverse
(
'courseware'
,
args
=
[
unicode
(
course
.
id
)]))
# check to see if there is a required survey that must be taken before
...
...
@@ -857,7 +857,7 @@ def _progress(request, course_key, student_id):
student
=
User
.
objects
.
prefetch_related
(
"groups"
)
.
get
(
id
=
student
.
id
)
course_grade
=
CourseGradeFactory
()
.
create
(
student
,
course
)
courseware_summary
=
course_grade
.
chapter_grades
courseware_summary
=
course_grade
.
chapter_grades
.
values
()
grade_summary
=
course_grade
.
summary
studio_url
=
get_studio_url
(
course
,
'settings/grading'
)
...
...
lms/djangoapps/gating/api.py
View file @
6a954c1e
...
...
@@ -2,14 +2,12 @@
API for the gating djangoapp
"""
from
collections
import
defaultdict
from
django.test.client
import
RequestFactory
import
json
import
logging
from
lms.djangoapps.courseware.entrance_exams
import
get_entrance_exam_
score
from
lms.djangoapps.courseware.entrance_exams
import
get_entrance_exam_
content
from
openedx.core.lib.gating
import
api
as
gating_api
from
opaque_keys.edx.keys
import
UsageKey
from
xmodule.modulestore.django
import
modulestore
from
util
import
milestones_helpers
...
...
@@ -53,7 +51,7 @@ def _get_minimum_required_percentage(milestone):
min_score
=
int
(
requirements
.
get
(
'min_score'
))
except
(
ValueError
,
TypeError
):
log
.
warning
(
'
Failed to find minimum score for gating milestone
%
s, defaulting to 100'
,
u'Gating:
Failed to find minimum score for gating milestone
%
s, defaulting to 100'
,
json
.
dumps
(
milestone
)
)
return
min_score
...
...
@@ -63,35 +61,56 @@ 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
return
_calculate_ratio
(
subsection_grade
.
graded_total
.
earned
,
subsection_grade
.
graded_total
.
possible
)
*
100.0
def
evaluate_entrance_exam
(
course
,
subsection_grade
,
user
):
def
_calculate_ratio
(
earned
,
possible
):
"""
Returns the percentage of the given earned and possible values.
"""
return
float
(
earned
)
/
float
(
possible
)
if
possible
else
0.0
def
evaluate_entrance_exam
(
course_grade
,
user
):
"""
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
to the given
course. If the course
_grade meets the
minimum score required, the dependent milestone
s
will be marked
fulfilled for the user.
"""
course
=
course_grade
.
course
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
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
)
if
get_entrance_exam_content
(
user
,
course
):
exam_chapter_key
=
get_entrance_exam_usage_key
(
course
)
exam_score_ratio
=
get_entrance_exam_score_ratio
(
course_grade
,
exam_chapter_key
)
if
exam_score_ratio
>=
course
.
entrance_exam_minimum_score_pct
:
relationship_types
=
milestones_helpers
.
get_milestone_relationship_types
()
content_milestones
=
milestones_helpers
.
get_course_content_milestones
(
course
.
id
,
exam_key
,
exam_
chapter_
key
,
relationship
=
relationship_types
[
'FULFILLS'
]
)
# Mark each
milestone dependent on the entrance exam
as fulfilled by the user.
# Mark each
entrance exam dependent milestone
as fulfilled by the user.
for
milestone
in
content_milestones
:
milestones_helpers
.
add_user_milestone
({
'id'
:
request
.
user
.
id
},
milestone
)
milestones_helpers
.
add_user_milestone
({
'id'
:
user
.
id
},
milestone
)
def
get_entrance_exam_usage_key
(
course
):
"""
Returns the UsageKey of the entrance exam for the course.
"""
return
UsageKey
.
from_string
(
course
.
entrance_exam_id
)
.
replace
(
course_key
=
course
.
id
)
def
get_entrance_exam_score_ratio
(
course_grade
,
exam_chapter_key
):
"""
Returns the score for the given chapter as a ratio of the
aggregated earned over the possible points, resulting in a
decimal value less than 1.
"""
try
:
earned
,
possible
=
course_grade
.
score_for_chapter
(
exam_chapter_key
)
except
KeyError
:
earned
,
possible
=
0.0
,
0.0
log
.
warning
(
u'Gating: Unexpectedly failed to find chapter_grade for
%
s.'
,
exam_chapter_key
)
return
_calculate_ratio
(
earned
,
possible
)
lms/djangoapps/gating/signals.py
View file @
6a954c1e
...
...
@@ -5,6 +5,7 @@ from django.dispatch import receiver
from
gating
import
api
as
gating_api
from
lms.djangoapps.grades.signals.signals
import
SUBSECTION_SCORE_CHANGED
from
openedx.core.djangoapps.signals.signals
import
COURSE_GRADE_CHANGED
@receiver
(
SUBSECTION_SCORE_CHANGED
)
...
...
@@ -21,4 +22,18 @@ def evaluate_subsection_gated_milestones(**kwargs):
"""
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'
))
@receiver
(
COURSE_GRADE_CHANGED
)
def
evaluate_course_gated_milestones
(
**
kwargs
):
"""
Receives the COURSE_GRADE_CHANGED signal and triggers the
evaluation of any milestone relationships which are attached
to the course grade.
Arguments:
kwargs (dict): Contains user, course_grade
Returns:
None
"""
gating_api
.
evaluate_entrance_exam
(
kwargs
[
'course_grade'
],
kwargs
.
get
(
'user'
))
lms/djangoapps/grades/new/course_grade.py
View file @
6a954c1e
...
...
@@ -49,7 +49,7 @@ class CourseGrade(object):
a dict keyed by subsection format types.
"""
subsections_by_format
=
defaultdict
(
OrderedDict
)
for
chapter
in
self
.
chapter_grades
:
for
chapter
in
self
.
chapter_grades
.
itervalues
()
:
for
subsection_grade
in
chapter
[
'sections'
]:
if
subsection_grade
.
graded
:
graded_total
=
subsection_grade
.
graded_total
...
...
@@ -63,7 +63,7 @@ class CourseGrade(object):
Returns a dict of problem scores keyed by their locations.
"""
locations_to_scores
=
{}
for
chapter
in
self
.
chapter_grades
:
for
chapter
in
self
.
chapter_grades
.
itervalues
()
:
for
subsection_grade
in
chapter
[
'sections'
]:
locations_to_scores
.
update
(
subsection_grade
.
locations_to_scores
)
return
locations_to_scores
...
...
@@ -88,10 +88,12 @@ class CourseGrade(object):
@lazy
def
chapter_grades
(
self
):
"""
Returns a list of chapters, each containing its subsection grades,
display name, and url name.
Returns a dictionary of dictionaries.
The primary dictionary is keyed by the chapter's usage_key.
The secondary dictionary contains the chapter's
subsection grades, display name, and url name.
"""
chapter_grades
=
[]
chapter_grades
=
OrderedDict
()
for
chapter_key
in
self
.
course_structure
.
get_children
(
self
.
course
.
location
):
chapter
=
self
.
course_structure
[
chapter_key
]
chapter_subsection_grades
=
[]
...
...
@@ -101,11 +103,11 @@ class CourseGrade(object):
self
.
_subsection_grade_factory
.
create
(
self
.
course_structure
[
subsection_key
],
read_only
=
True
)
)
chapter_grades
.
append
(
{
chapter_grades
[
chapter_key
]
=
{
'display_name'
:
block_metadata_utils
.
display_name_with_default_escaped
(
chapter
),
'url_name'
:
block_metadata_utils
.
url_name_for_block
(
chapter
),
'sections'
:
chapter_subsection_grades
}
)
}
return
chapter_grades
@property
...
...
@@ -152,7 +154,7 @@ class CourseGrade(object):
If read_only is True, doesn't save any updates to the grades.
"""
subsections_total
=
sum
(
len
(
chapter
[
'sections'
])
for
chapter
in
self
.
chapter_grades
)
subsections_total
=
sum
(
len
(
chapter
[
'sections'
])
for
chapter
in
self
.
chapter_grades
.
itervalues
()
)
total_graded_subsections
=
sum
(
len
(
x
)
for
x
in
self
.
graded_subsections_by_format
.
itervalues
())
subsections_created
=
len
(
self
.
_subsection_grade_factory
.
_unsaved_subsection_grades
)
# pylint: disable=protected-access
...
...
@@ -187,6 +189,19 @@ class CourseGrade(object):
)
)
def
score_for_chapter
(
self
,
chapter_key
):
"""
Returns the aggregate weighted score for the given chapter.
Raises:
KeyError if the chapter is not found.
"""
earned
,
possible
=
0.0
,
0.0
chapter_grade
=
self
.
chapter_grades
[
chapter_key
]
for
section
in
chapter_grade
[
'sections'
]:
earned
+=
section
.
graded_total
.
earned
possible
+=
section
.
graded_total
.
possible
return
earned
,
possible
def
score_for_module
(
self
,
location
):
"""
Calculate the aggregate weighted score for any location in the course.
...
...
@@ -201,8 +216,7 @@ class CourseGrade(object):
score
=
self
.
locations_to_scores
[
location
]
return
score
.
earned
,
score
.
possible
children
=
self
.
course_structure
.
get_children
(
location
)
earned
=
0.0
possible
=
0.0
earned
,
possible
=
0.0
,
0.0
for
child
in
children
:
child_earned
,
child_possible
=
self
.
score_for_module
(
child
)
earned
+=
child_earned
...
...
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