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
4f6d5d9c
Commit
4f6d5d9c
authored
Mar 30, 2017
by
J. Cliff Dyer
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Estimate creation time for subsections grades based on timestamp of
incoming scores. TNL-6697
parent
529df7c6
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
232 additions
and
137 deletions
+232
-137
cms/djangoapps/contentstore/views/tests/test_item.py
+1
-1
common/lib/xmodule/xmodule/graders.py
+78
-23
common/lib/xmodule/xmodule/tests/test_graders.py
+12
-6
lms/djangoapps/courseware/model_data.py
+4
-4
lms/djangoapps/courseware/tests/test_submitting_problems.py
+13
-5
lms/djangoapps/grades/config/__init__.py
+2
-2
lms/djangoapps/grades/config/waffle.py
+1
-0
lms/djangoapps/grades/management/commands/tests/test_reset_grades.py
+1
-1
lms/djangoapps/grades/models.py
+20
-17
lms/djangoapps/grades/new/course_grade.py
+6
-3
lms/djangoapps/grades/new/subsection_grade.py
+7
-19
lms/djangoapps/grades/scores.py
+12
-10
lms/djangoapps/grades/tests/test_grades.py
+6
-2
lms/djangoapps/grades/tests/test_models.py
+18
-7
lms/djangoapps/grades/tests/test_new.py
+5
-5
lms/djangoapps/grades/tests/test_scores.py
+38
-25
lms/djangoapps/grades/tests/utils.py
+4
-3
lms/djangoapps/instructor_task/tasks_helper.py
+2
-2
requirements/edx/github.txt
+2
-2
No files found.
cms/djangoapps/contentstore/views/tests/test_item.py
View file @
4f6d5d9c
...
@@ -2258,7 +2258,7 @@ class TestComponentTemplates(CourseTestCase):
...
@@ -2258,7 +2258,7 @@ class TestComponentTemplates(CourseTestCase):
def
verify_openassessment_present
(
support_level
):
def
verify_openassessment_present
(
support_level
):
""" Helper method to verify that openassessment template is present """
""" Helper method to verify that openassessment template is present """
openassessment
=
get_xblock_problem
(
'
Peer
Assessment'
)
openassessment
=
get_xblock_problem
(
'
Open Response
Assessment'
)
self
.
assertIsNotNone
(
openassessment
)
self
.
assertIsNotNone
(
openassessment
)
self
.
assertEqual
(
openassessment
.
get
(
'category'
),
'openassessment'
)
self
.
assertEqual
(
openassessment
.
get
(
'category'
),
'openassessment'
)
self
.
assertEqual
(
openassessment
.
get
(
'support_level'
),
support_level
)
self
.
assertEqual
(
openassessment
.
get
(
'support_level'
),
support_level
)
...
...
common/lib/xmodule/xmodule/graders.py
View file @
4f6d5d9c
...
@@ -6,11 +6,13 @@ from __future__ import division
...
@@ -6,11 +6,13 @@ from __future__ import division
import
abc
import
abc
from
collections
import
OrderedDict
from
collections
import
OrderedDict
from
datetime
import
datetime
# Used by pycontracts. pylint: disable=unused-import
import
inspect
import
inspect
import
logging
import
logging
import
random
import
random
import
sys
import
sys
from
contracts
import
contract
log
=
logging
.
getLogger
(
"edx.courseware"
)
log
=
logging
.
getLogger
(
"edx.courseware"
)
...
@@ -18,15 +20,22 @@ log = logging.getLogger("edx.courseware")
...
@@ -18,15 +20,22 @@ log = logging.getLogger("edx.courseware")
class
ScoreBase
(
object
):
class
ScoreBase
(
object
):
"""
"""
Abstract base class for encapsulating fields of values scores.
Abstract base class for encapsulating fields of values scores.
Field common to all scores include:
graded (boolean) - whether or not this module is graded
attempted (boolean) - whether the module was attempted
"""
"""
__metaclass__
=
abc
.
ABCMeta
__metaclass__
=
abc
.
ABCMeta
def
__init__
(
self
,
graded
,
attempted
):
@contract
(
graded
=
"bool"
,
first_attempted
=
"datetime|None"
)
def
__init__
(
self
,
graded
,
first_attempted
):
"""
Fields common to all scores include:
:param graded: Whether or not this module is graded
:type graded: bool
:param first_attempted: When the module was first attempted, or None
:type first_attempted: datetime|None
"""
self
.
graded
=
graded
self
.
graded
=
graded
self
.
attempted
=
attempted
self
.
first_attempted
=
first_
attempted
def
__eq__
(
self
,
other
):
def
__eq__
(
self
,
other
):
if
type
(
other
)
is
type
(
self
):
if
type
(
other
)
is
type
(
self
):
...
@@ -43,14 +52,27 @@ class ScoreBase(object):
...
@@ -43,14 +52,27 @@ class ScoreBase(object):
class
ProblemScore
(
ScoreBase
):
class
ProblemScore
(
ScoreBase
):
"""
"""
Encapsulates the fields of a Problem's score.
Encapsulates the fields of a Problem's score.
In addition to the fields in ScoreBase, also includes:
raw_earned (float) - raw points earned on this problem
raw_possible (float) - raw points possible to earn on this problem
weighted_earned = earned (float) - weighted value of the points earned
weighted_possible = possible (float) - weighted possible points on this problem
weight (float) - weight of this problem
"""
"""
@contract
def
__init__
(
self
,
raw_earned
,
raw_possible
,
weighted_earned
,
weighted_possible
,
weight
,
*
args
,
**
kwargs
):
def
__init__
(
self
,
raw_earned
,
raw_possible
,
weighted_earned
,
weighted_possible
,
weight
,
*
args
,
**
kwargs
):
"""
In addition to the fields in ScoreBase, arguments include:
:param raw_earned: Raw points earned on this problem
:type raw_earned: int|float|None
:param raw_possible: Raw points possible to earn on this problem
:type raw_possible: int|float|None
:param weighted_earned: Weighted value of the points earned
:type weighted_earned: int|float|None
:param weighted_possible: Weighted possible points on this problem
:type weighted_possible: int|float|None
:param weight: Weight of this problem
:type weight: int|float|None
"""
super
(
ProblemScore
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
super
(
ProblemScore
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
self
.
raw_earned
=
float
(
raw_earned
)
if
raw_earned
is
not
None
else
None
self
.
raw_earned
=
float
(
raw_earned
)
if
raw_earned
is
not
None
else
None
self
.
raw_possible
=
float
(
raw_possible
)
if
raw_possible
is
not
None
else
None
self
.
raw_possible
=
float
(
raw_possible
)
if
raw_possible
is
not
None
else
None
...
@@ -62,11 +84,18 @@ class ProblemScore(ScoreBase):
...
@@ -62,11 +84,18 @@ class ProblemScore(ScoreBase):
class
AggregatedScore
(
ScoreBase
):
class
AggregatedScore
(
ScoreBase
):
"""
"""
Encapsulates the fields of a Subsection's score.
Encapsulates the fields of a Subsection's score.
In addition to the fields in ScoreBase, also includes:
tw_earned = earned - total aggregated sum of all weighted earned values
tw_possible = possible - total aggregated sum of all weighted possible values
"""
"""
@contract
def
__init__
(
self
,
tw_earned
,
tw_possible
,
*
args
,
**
kwargs
):
def
__init__
(
self
,
tw_earned
,
tw_possible
,
*
args
,
**
kwargs
):
"""
In addition to the fields in ScoreBase, also includes:
:param tw_earned: Total aggregated sum of all weighted earned values
:type tw_earned: int|float|None
:param tw_possible: Total aggregated sum of all weighted possible values
:type tw_possible: int|float|None
"""
super
(
AggregatedScore
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
super
(
AggregatedScore
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
self
.
earned
=
float
(
tw_earned
)
if
tw_earned
is
not
None
else
None
self
.
earned
=
float
(
tw_earned
)
if
tw_earned
is
not
None
else
None
self
.
possible
=
float
(
tw_possible
)
if
tw_possible
is
not
None
else
None
self
.
possible
=
float
(
tw_possible
)
if
tw_possible
is
not
None
else
None
...
@@ -81,25 +110,32 @@ def float_sum(iterable):
...
@@ -81,25 +110,32 @@ def float_sum(iterable):
def
aggregate_scores
(
scores
):
def
aggregate_scores
(
scores
):
"""
"""
scores: A list of
ScoreBas
e objects
scores: A list of
ProblemScor
e objects
returns: A tuple (all_total, graded_total).
returns: A tuple (all_total, graded_total).
all_total: A
ScoreBas
e representing the total score summed over all input scores
all_total: A
n AggregatedScor
e representing the total score summed over all input scores
graded_total: A
ScoreBas
e representing the score summed over all graded input scores
graded_total: A
n AggregatedScor
e representing the score summed over all graded input scores
"""
"""
total_correct_graded
=
float_sum
(
score
.
earned
for
score
in
scores
if
score
.
graded
)
total_correct_graded
=
float_sum
(
score
.
earned
for
score
in
_iter_graded
(
scores
))
total_possible_graded
=
float_sum
(
score
.
possible
for
score
in
scores
if
score
.
graded
)
total_possible_graded
=
float_sum
(
score
.
possible
for
score
in
_iter_graded
(
scores
))
any_attempted_graded
=
any
(
score
.
attempted
for
score
in
scores
if
score
.
graded
)
first_attempted_graded
=
_min_or_none
(
score
.
first_attempted
for
score
in
_iter_graded
(
scores
)
if
score
.
first_attempted
)
total_correct
=
float_sum
(
score
.
earned
for
score
in
scores
)
total_correct
=
float_sum
(
score
.
earned
for
score
in
scores
)
total_possible
=
float_sum
(
score
.
possible
for
score
in
scores
)
total_possible
=
float_sum
(
score
.
possible
for
score
in
scores
)
any_attempted
=
any
(
score
.
attempted
for
score
in
scores
)
first_attempted
=
_min_or_none
(
score
.
first_attempted
for
score
in
scores
if
score
.
first_attempted
)
# regardless of whether it is graded
# regardless of whether it is graded
all_total
=
AggregatedScore
(
total_correct
,
total_possible
,
False
,
any
_attempted
)
all_total
=
AggregatedScore
(
total_correct
,
total_possible
,
False
,
first_attempted
=
first
_attempted
)
# selecting only graded things
# selecting only graded things
graded_total
=
AggregatedScore
(
graded_total
=
AggregatedScore
(
total_correct_graded
,
total_possible_graded
,
True
,
any_attempted_graded
,
total_correct_graded
,
total_possible_graded
,
True
,
first_attempted
=
first_attempted_graded
)
)
return
all_total
,
graded_total
return
all_total
,
graded_total
...
@@ -407,3 +443,22 @@ class AssignmentFormatGrader(CourseGrader):
...
@@ -407,3 +443,22 @@ class AssignmentFormatGrader(CourseGrader):
'section_breakdown'
:
breakdown
,
'section_breakdown'
:
breakdown
,
# No grade_breakdown here
# No grade_breakdown here
}
}
def
_iter_graded
(
scores
):
"""
Yield the scores that belong to explicitly graded blocks
"""
return
(
score
for
score
in
scores
if
score
.
graded
)
def
_min_or_none
(
itr
):
"""
Return the lowest value in itr, or None if itr is empty.
In python 3, this is just min(itr, default=None)
"""
try
:
return
min
(
itr
)
except
ValueError
:
return
None
common/lib/xmodule/xmodule/tests/test_graders.py
View file @
4f6d5d9c
"""Grading tests"""
"""
Grading tests
"""
from
datetime
import
datetime
import
ddt
import
ddt
import
unittest
import
unittest
...
@@ -13,8 +18,8 @@ class GradesheetTest(unittest.TestCase):
...
@@ -13,8 +18,8 @@ class GradesheetTest(unittest.TestCase):
def
test_weighted_grading
(
self
):
def
test_weighted_grading
(
self
):
scores
=
[]
scores
=
[]
agg_fields
=
dict
(
attempted
=
Fals
e
)
agg_fields
=
dict
(
first_attempted
=
Non
e
)
prob_fields
=
dict
(
raw_earned
=
0
,
raw_possible
=
0
,
weight
=
0
,
attempted
=
Fals
e
)
prob_fields
=
dict
(
raw_earned
=
0
,
raw_possible
=
0
,
weight
=
0
,
first_attempted
=
Non
e
)
# No scores
# No scores
all_total
,
graded_total
=
aggregate_scores
(
scores
)
all_total
,
graded_total
=
aggregate_scores
(
scores
)
...
@@ -40,8 +45,9 @@ class GradesheetTest(unittest.TestCase):
...
@@ -40,8 +45,9 @@ class GradesheetTest(unittest.TestCase):
)
)
# (0/5 non-graded) + (3/5 graded) = 3/10 total, 3/5 graded
# (0/5 non-graded) + (3/5 graded) = 3/10 total, 3/5 graded
prob_fields
[
'attempted'
]
=
True
now
=
datetime
.
now
()
agg_fields
[
'attempted'
]
=
True
prob_fields
[
'first_attempted'
]
=
now
agg_fields
[
'first_attempted'
]
=
now
scores
.
append
(
ProblemScore
(
weighted_earned
=
3
,
weighted_possible
=
5
,
graded
=
True
,
**
prob_fields
))
scores
.
append
(
ProblemScore
(
weighted_earned
=
3
,
weighted_possible
=
5
,
graded
=
True
,
**
prob_fields
))
all_total
,
graded_total
=
aggregate_scores
(
scores
)
all_total
,
graded_total
=
aggregate_scores
(
scores
)
self
.
assertAlmostEqual
(
self
.
assertAlmostEqual
(
...
@@ -89,7 +95,7 @@ class GraderTest(unittest.TestCase):
...
@@ -89,7 +95,7 @@ class GraderTest(unittest.TestCase):
self
.
graded_total
=
graded_total
self
.
graded_total
=
graded_total
self
.
display_name
=
display_name
self
.
display_name
=
display_name
common_fields
=
dict
(
graded
=
True
,
attempted
=
True
)
common_fields
=
dict
(
graded
=
True
,
first_attempted
=
datetime
.
now
()
)
test_gradesheet
=
{
test_gradesheet
=
{
'Homework'
:
{
'Homework'
:
{
'hw1'
:
MockGrade
(
AggregatedScore
(
tw_earned
=
2
,
tw_possible
=
20.0
,
**
common_fields
),
display_name
=
'hw1'
),
'hw1'
:
MockGrade
(
AggregatedScore
(
tw_earned
=
2
,
tw_possible
=
20.0
,
**
common_fields
),
display_name
=
'hw1'
),
...
...
lms/djangoapps/courseware/model_data.py
View file @
4f6d5d9c
...
@@ -942,7 +942,7 @@ class ScoresClient(object):
...
@@ -942,7 +942,7 @@ class ScoresClient(object):
Eventually, this should read and write scores, but at the moment it only
Eventually, this should read and write scores, but at the moment it only
handles the read side of things.
handles the read side of things.
"""
"""
Score
=
namedtuple
(
'Score'
,
'correct total'
)
Score
=
namedtuple
(
'Score'
,
'correct total
created
'
)
def
__init__
(
self
,
course_key
,
user_id
):
def
__init__
(
self
,
course_key
,
user_id
):
self
.
course_key
=
course_key
self
.
course_key
=
course_key
...
@@ -965,9 +965,9 @@ class ScoresClient(object):
...
@@ -965,9 +965,9 @@ class ScoresClient(object):
# attached to them (since old mongo identifiers don't include runs).
# attached to them (since old mongo identifiers don't include runs).
# So we have to add that info back in before we put it into our lookup.
# So we have to add that info back in before we put it into our lookup.
self
.
_locations_to_scores
.
update
({
self
.
_locations_to_scores
.
update
({
UsageKey
.
from_string
(
location
)
.
map_into_course
(
self
.
course_key
):
self
.
Score
(
correct
,
total
)
UsageKey
.
from_string
(
location
)
.
map_into_course
(
self
.
course_key
):
self
.
Score
(
correct
,
total
,
created
)
for
location
,
correct
,
total
for
location
,
correct
,
total
,
created
in
scores_qset
.
values_list
(
'module_state_key'
,
'grade'
,
'max_grade'
)
in
scores_qset
.
values_list
(
'module_state_key'
,
'grade'
,
'max_grade'
,
'created'
)
})
})
self
.
_has_fetched
=
True
self
.
_has_fetched
=
True
...
...
lms/djangoapps/courseware/tests/test_submitting_problems.py
View file @
4f6d5d9c
...
@@ -2,16 +2,20 @@
...
@@ -2,16 +2,20 @@
"""
"""
Integration tests for submitting problem responses and getting grades.
Integration tests for submitting problem responses and getting grades.
"""
"""
import
ddt
# pylint: disable=attribute-defined-outside-init
import
json
import
json
import
os
import
os
from
textwrap
import
dedent
from
textwrap
import
dedent
import
ddt
from
django.conf
import
settings
from
django.conf
import
settings
from
django.contrib.auth.models
import
User
from
django.contrib.auth.models
import
User
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
django.test
import
TestCase
from
django.test
import
TestCase
from
django.test.client
import
RequestFactory
from
django.test.client
import
RequestFactory
from
django.utils.timezone
import
now
from
mock
import
patch
from
mock
import
patch
from
nose.plugins.attrib
import
attr
from
nose.plugins.attrib
import
attr
...
@@ -386,7 +390,6 @@ class TestCourseGrader(TestSubmittingProblems):
...
@@ -386,7 +390,6 @@ class TestCourseGrader(TestSubmittingProblems):
"""
"""
Set up a simple course for testing weighted grading functionality.
Set up a simple course for testing weighted grading functionality.
"""
"""
# pylint: disable=attribute-defined-outside-init
self
.
set_weighted_policy
(
hw_weight
,
final_weight
)
self
.
set_weighted_policy
(
hw_weight
,
final_weight
)
...
@@ -449,12 +452,13 @@ class TestCourseGrader(TestSubmittingProblems):
...
@@ -449,12 +452,13 @@ class TestCourseGrader(TestSubmittingProblems):
self
.
hw3_names
=
[
'h3p1'
,
'h3p2'
]
self
.
hw3_names
=
[
'h3p1'
,
'h3p2'
]
self
.
homework1
=
self
.
add_graded_section_to_course
(
'homework1'
)
self
.
homework1
=
self
.
add_graded_section_to_course
(
'homework1'
)
self
.
homework2
=
self
.
add_graded_section_to_course
(
'homework2'
)
self
.
homework3
=
self
.
add_graded_section_to_course
(
'homework3'
)
self
.
add_dropdown_to_section
(
self
.
homework1
.
location
,
self
.
hw1_names
[
0
],
1
)
self
.
add_dropdown_to_section
(
self
.
homework1
.
location
,
self
.
hw1_names
[
0
],
1
)
self
.
add_dropdown_to_section
(
self
.
homework1
.
location
,
self
.
hw1_names
[
1
],
1
)
self
.
add_dropdown_to_section
(
self
.
homework1
.
location
,
self
.
hw1_names
[
1
],
1
)
self
.
homework2
=
self
.
add_graded_section_to_course
(
'homework2'
)
self
.
add_dropdown_to_section
(
self
.
homework2
.
location
,
self
.
hw2_names
[
0
],
1
)
self
.
add_dropdown_to_section
(
self
.
homework2
.
location
,
self
.
hw2_names
[
0
],
1
)
self
.
add_dropdown_to_section
(
self
.
homework2
.
location
,
self
.
hw2_names
[
1
],
1
)
self
.
add_dropdown_to_section
(
self
.
homework2
.
location
,
self
.
hw2_names
[
1
],
1
)
self
.
homework3
=
self
.
add_graded_section_to_course
(
'homework3'
)
self
.
add_dropdown_to_section
(
self
.
homework3
.
location
,
self
.
hw3_names
[
0
],
1
)
self
.
add_dropdown_to_section
(
self
.
homework3
.
location
,
self
.
hw3_names
[
0
],
1
)
self
.
add_dropdown_to_section
(
self
.
homework3
.
location
,
self
.
hw3_names
[
1
],
1
)
self
.
add_dropdown_to_section
(
self
.
homework3
.
location
,
self
.
hw3_names
[
1
],
1
)
...
@@ -619,7 +623,11 @@ class TestCourseGrader(TestSubmittingProblems):
...
@@ -619,7 +623,11 @@ class TestCourseGrader(TestSubmittingProblems):
with
patch
(
'submissions.api.get_scores'
)
as
mock_get_scores
:
with
patch
(
'submissions.api.get_scores'
)
as
mock_get_scores
:
mock_get_scores
.
return_value
=
{
mock_get_scores
.
return_value
=
{
self
.
problem_location
(
'p3'
)
.
to_deprecated_string
():
(
1
,
1
)
self
.
problem_location
(
'p3'
)
.
to_deprecated_string
():
{
'points_earned'
:
1
,
'points_possible'
:
1
,
'created_at'
:
now
(),
},
}
}
self
.
get_course_grade
()
self
.
get_course_grade
()
...
...
lms/djangoapps/grades/config/__init__.py
View file @
4f6d5d9c
from
lms.djangoapps.grades.config.models
import
PersistentGradesEnabledFlag
from
lms.djangoapps.grades.config.models
import
PersistentGradesEnabledFlag
from
lms.djangoapps.grades.config.waffle
import
waffle
,
ASSUME_ZERO_GRADE_IF_ABSENT
from
lms.djangoapps.grades.config.waffle
import
waffle
as
waffle_func
,
ASSUME_ZERO_GRADE_IF_ABSENT
def
assume_zero_if_absent
(
course_key
):
def
assume_zero_if_absent
(
course_key
):
"""
"""
Returns whether an absent grade should be assumed to be zero.
Returns whether an absent grade should be assumed to be zero.
"""
"""
return
should_persist_grades
(
course_key
)
and
waffle
()
.
is_enabled
(
ASSUME_ZERO_GRADE_IF_ABSENT
)
return
should_persist_grades
(
course_key
)
and
waffle
_func
()
.
is_enabled
(
ASSUME_ZERO_GRADE_IF_ABSENT
)
def
should_persist_grades
(
course_key
):
def
should_persist_grades
(
course_key
):
...
...
lms/djangoapps/grades/config/waffle.py
View file @
4f6d5d9c
...
@@ -11,6 +11,7 @@ WAFFLE_NAMESPACE = u'grades'
...
@@ -11,6 +11,7 @@ WAFFLE_NAMESPACE = u'grades'
# Switches
# Switches
WRITE_ONLY_IF_ENGAGED
=
u'write_only_if_engaged'
WRITE_ONLY_IF_ENGAGED
=
u'write_only_if_engaged'
ASSUME_ZERO_GRADE_IF_ABSENT
=
u'assume_zero_grade_if_absent'
ASSUME_ZERO_GRADE_IF_ABSENT
=
u'assume_zero_grade_if_absent'
ESTIMATE_FIRST_ATTEMPTED
=
u'estimate_first_attempted'
def
waffle
():
def
waffle
():
...
...
lms/djangoapps/grades/management/commands/tests/test_reset_grades.py
View file @
4f6d5d9c
...
@@ -81,7 +81,7 @@ class TestResetGrades(TestCase):
...
@@ -81,7 +81,7 @@ class TestResetGrades(TestCase):
"earned_graded"
:
6.0
,
"earned_graded"
:
6.0
,
"possible_graded"
:
8.0
,
"possible_graded"
:
8.0
,
"visible_blocks"
:
MagicMock
(),
"visible_blocks"
:
MagicMock
(),
"
attempted"
:
True
,
"
first_attempted"
:
datetime
.
now
()
,
}
}
for
course_key
in
courses_keys
:
for
course_key
in
courses_keys
:
...
...
lms/djangoapps/grades/models.py
View file @
4f6d5d9c
...
@@ -26,6 +26,7 @@ from coursewarehistoryextended.fields import UnsignedBigIntAutoField
...
@@ -26,6 +26,7 @@ from coursewarehistoryextended.fields import UnsignedBigIntAutoField
from
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
from
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
from
openedx.core.djangoapps.xmodule_django.models
import
CourseKeyField
,
UsageKeyField
from
openedx.core.djangoapps.xmodule_django.models
import
CourseKeyField
,
UsageKeyField
from
.config
import
waffle
log
=
logging
.
getLogger
(
__name__
)
log
=
logging
.
getLogger
(
__name__
)
...
@@ -361,9 +362,9 @@ class PersistentSubsectionGrade(DeleteGradesMixin, TimeStampedModel):
...
@@ -361,9 +362,9 @@ class PersistentSubsectionGrade(DeleteGradesMixin, TimeStampedModel):
"""
"""
cls
.
_prepare_params_and_visible_blocks
(
params
)
cls
.
_prepare_params_and_visible_blocks
(
params
)
first_attempted
=
params
.
pop
(
'first_attempted'
)
user_id
=
params
.
pop
(
'user_id'
)
user_id
=
params
.
pop
(
'user_id'
)
usage_key
=
params
.
pop
(
'usage_key'
)
usage_key
=
params
.
pop
(
'usage_key'
)
attempted
=
params
.
pop
(
'attempted'
)
grade
,
_
=
cls
.
objects
.
update_or_create
(
grade
,
_
=
cls
.
objects
.
update_or_create
(
user_id
=
user_id
,
user_id
=
user_id
,
...
@@ -371,20 +372,33 @@ class PersistentSubsectionGrade(DeleteGradesMixin, TimeStampedModel):
...
@@ -371,20 +372,33 @@ class PersistentSubsectionGrade(DeleteGradesMixin, TimeStampedModel):
usage_key
=
usage_key
,
usage_key
=
usage_key
,
defaults
=
params
,
defaults
=
params
,
)
)
if
first_attempted
is
not
None
and
grade
.
first_attempted
is
None
:
if
attempted
and
not
grade
.
first_attempted
:
if
waffle
.
waffle
()
.
is_enabled
(
waffle
.
ESTIMATE_FIRST_ATTEMPTED
):
grade
.
first_attempted
=
now
()
grade
.
first_attempted
=
first_attempted
else
:
grade
.
first_attempted
=
now
()
grade
.
save
()
grade
.
save
()
cls
.
_emit_grade_calculated_event
(
grade
)
cls
.
_emit_grade_calculated_event
(
grade
)
return
grade
return
grade
@classmethod
@classmethod
def
_prepare_first_attempted_for_create
(
cls
,
params
):
"""
Update the value of 'first_attempted' to now() if we aren't
using score-based estimates.
"""
if
params
[
'first_attempted'
]
is
not
None
and
not
waffle
.
waffle
()
.
is_enabled
(
waffle
.
ESTIMATE_FIRST_ATTEMPTED
):
params
[
'first_attempted'
]
=
now
()
@classmethod
def
create_grade
(
cls
,
**
params
):
def
create_grade
(
cls
,
**
params
):
"""
"""
Wrapper for objects.create.
Wrapper for objects.create.
"""
"""
cls
.
_prepare_params_and_visible_blocks
(
params
)
cls
.
_prepare_params_and_visible_blocks
(
params
)
cls
.
_prepare_attempted_for_create
(
params
,
now
())
cls
.
_prepare_first_attempted_for_create
(
params
)
grade
=
cls
.
objects
.
create
(
**
params
)
grade
=
cls
.
objects
.
create
(
**
params
)
cls
.
_emit_grade_calculated_event
(
grade
)
cls
.
_emit_grade_calculated_event
(
grade
)
return
grade
return
grade
...
@@ -400,9 +414,7 @@ class PersistentSubsectionGrade(DeleteGradesMixin, TimeStampedModel):
...
@@ -400,9 +414,7 @@ class PersistentSubsectionGrade(DeleteGradesMixin, TimeStampedModel):
map
(
cls
.
_prepare_params
,
grade_params_iter
)
map
(
cls
.
_prepare_params
,
grade_params_iter
)
VisibleBlocks
.
bulk_get_or_create
([
params
[
'visible_blocks'
]
for
params
in
grade_params_iter
],
course_key
)
VisibleBlocks
.
bulk_get_or_create
([
params
[
'visible_blocks'
]
for
params
in
grade_params_iter
],
course_key
)
map
(
cls
.
_prepare_params_visible_blocks_id
,
grade_params_iter
)
map
(
cls
.
_prepare_params_visible_blocks_id
,
grade_params_iter
)
first_attempt_timestamp
=
now
()
map
(
cls
.
_prepare_first_attempted_for_create
,
grade_params_iter
)
for
params
in
grade_params_iter
:
cls
.
_prepare_attempted_for_create
(
params
,
first_attempt_timestamp
)
grades
=
[
PersistentSubsectionGrade
(
**
params
)
for
params
in
grade_params_iter
]
grades
=
[
PersistentSubsectionGrade
(
**
params
)
for
params
in
grade_params_iter
]
grades
=
cls
.
objects
.
bulk_create
(
grades
)
grades
=
cls
.
objects
.
bulk_create
(
grades
)
for
grade
in
grades
:
for
grade
in
grades
:
...
@@ -429,15 +441,6 @@ class PersistentSubsectionGrade(DeleteGradesMixin, TimeStampedModel):
...
@@ -429,15 +441,6 @@ class PersistentSubsectionGrade(DeleteGradesMixin, TimeStampedModel):
params
[
'visible_blocks'
]
=
BlockRecordList
.
from_list
(
params
[
'visible_blocks'
],
params
[
'course_id'
])
params
[
'visible_blocks'
]
=
BlockRecordList
.
from_list
(
params
[
'visible_blocks'
],
params
[
'course_id'
])
@classmethod
@classmethod
def
_prepare_attempted_for_create
(
cls
,
params
,
timestamp
):
"""
When creating objects, an attempted subsection gets its timestamp set
unconditionally.
"""
if
params
.
pop
(
'attempted'
):
params
[
'first_attempted'
]
=
timestamp
@classmethod
def
_prepare_params_visible_blocks_id
(
cls
,
params
):
def
_prepare_params_visible_blocks_id
(
cls
,
params
):
"""
"""
Prepares the visible_blocks_id field for the grade record,
Prepares the visible_blocks_id field for the grade record,
...
...
lms/djangoapps/grades/new/course_grade.py
View file @
4f6d5d9c
...
@@ -24,10 +24,13 @@ class CourseGradeBase(object):
...
@@ -24,10 +24,13 @@ class CourseGradeBase(object):
self
.
passed
=
passed
self
.
passed
=
passed
def
__unicode__
(
self
):
def
__unicode__
(
self
):
return
u'Course Grade: percent:
%
s, letter_grade:
%
s, passed:
%
s'
.
format
(
return
u'Course Grade: percent: {}, letter_grade: {}, passed: {}'
.
format
(
unicode
(
self
.
percent
),
self
.
letter_grade
,
self
.
passed
,
unicode
(
self
.
percent
),
self
.
letter_grade
,
self
.
passed
,
)
)
@property
def
attempted
(
self
):
def
attempted
(
self
):
"""
"""
Returns whether at least one problem was attempted
Returns whether at least one problem was attempted
...
@@ -210,7 +213,7 @@ class CourseGrade(CourseGradeBase):
...
@@ -210,7 +213,7 @@ class CourseGrade(CourseGradeBase):
"""
"""
for
chapter
in
self
.
chapter_grades
.
itervalues
():
for
chapter
in
self
.
chapter_grades
.
itervalues
():
for
subsection_grade
in
chapter
[
'sections'
]:
for
subsection_grade
in
chapter
[
'sections'
]:
if
subsection_grade
.
attempted
:
if
subsection_grade
.
a
ll_total
.
first_a
ttempted
:
return
True
return
True
return
False
return
False
...
...
lms/djangoapps/grades/new/subsection_grade.py
View file @
4f6d5d9c
...
@@ -41,28 +41,16 @@ class SubsectionGradeBase(object):
...
@@ -41,28 +41,16 @@ class SubsectionGradeBase(object):
"""
"""
return
self
.
locations_to_scores
.
values
()
return
self
.
locations_to_scores
.
values
()
@property
def
attempted
(
self
):
"""
Returns whether any problem in this subsection
was attempted by the student.
"""
assert
self
.
all_total
is
not
None
,
(
"SubsectionGrade not fully populated yet. Call init_from_structure or init_from_model "
"before use."
)
return
self
.
all_total
.
attempted
class
ZeroSubsectionGrade
(
SubsectionGradeBase
):
class
ZeroSubsectionGrade
(
SubsectionGradeBase
):
"""
"""
Class for Subsection Grades with Zero values.
Class for Subsection Grades with Zero values.
"""
"""
def
__init__
(
self
,
subsection
,
course_data
):
def
__init__
(
self
,
subsection
,
course_data
):
super
(
ZeroSubsectionGrade
,
self
)
.
__init__
(
subsection
)
super
(
ZeroSubsectionGrade
,
self
)
.
__init__
(
subsection
)
self
.
graded_total
=
AggregatedScore
(
tw_earned
=
0
,
tw_possible
=
None
,
graded
=
False
,
attempted
=
Fals
e
)
self
.
graded_total
=
AggregatedScore
(
tw_earned
=
0
,
tw_possible
=
None
,
graded
=
False
,
first_attempted
=
Non
e
)
self
.
all_total
=
AggregatedScore
(
tw_earned
=
0
,
tw_possible
=
None
,
graded
=
self
.
graded
,
attempted
=
Fals
e
)
self
.
all_total
=
AggregatedScore
(
tw_earned
=
0
,
tw_possible
=
None
,
graded
=
self
.
graded
,
first_attempted
=
Non
e
)
self
.
course_data
=
course_data
self
.
course_data
=
course_data
@lazy
@lazy
...
@@ -118,13 +106,13 @@ class SubsectionGrade(SubsectionGradeBase):
...
@@ -118,13 +106,13 @@ class SubsectionGrade(SubsectionGradeBase):
tw_earned
=
model
.
earned_graded
,
tw_earned
=
model
.
earned_graded
,
tw_possible
=
model
.
possible_graded
,
tw_possible
=
model
.
possible_graded
,
graded
=
True
,
graded
=
True
,
attempted
=
model
.
first_attempted
is
not
None
,
first_attempted
=
model
.
first_attempted
,
)
)
self
.
all_total
=
AggregatedScore
(
self
.
all_total
=
AggregatedScore
(
tw_earned
=
model
.
earned_all
,
tw_earned
=
model
.
earned_all
,
tw_possible
=
model
.
possible_all
,
tw_possible
=
model
.
possible_all
,
graded
=
False
,
graded
=
False
,
attempted
=
model
.
first_attempted
is
not
None
,
first_attempted
=
model
.
first_attempted
,
)
)
self
.
_log_event
(
log
.
debug
,
u"init_from_model"
,
student
)
self
.
_log_event
(
log
.
debug
,
u"init_from_model"
,
student
)
return
self
return
self
...
@@ -162,7 +150,7 @@ class SubsectionGrade(SubsectionGradeBase):
...
@@ -162,7 +150,7 @@ class SubsectionGrade(SubsectionGradeBase):
Returns whether the SubsectionGrade's model should be
Returns whether the SubsectionGrade's model should be
persisted based on settings and attempted status.
persisted based on settings and attempted status.
"""
"""
return
not
waffle
()
.
is_enabled
(
WRITE_ONLY_IF_ENGAGED
)
or
self
.
a
ttempted
return
not
waffle
()
.
is_enabled
(
WRITE_ONLY_IF_ENGAGED
)
or
self
.
a
ll_total
.
first_attempted
is
not
None
def
_compute_block_score
(
def
_compute_block_score
(
self
,
self
,
...
@@ -209,7 +197,7 @@ class SubsectionGrade(SubsectionGradeBase):
...
@@ -209,7 +197,7 @@ class SubsectionGrade(SubsectionGradeBase):
earned_graded
=
self
.
graded_total
.
earned
,
earned_graded
=
self
.
graded_total
.
earned
,
possible_graded
=
self
.
graded_total
.
possible
,
possible_graded
=
self
.
graded_total
.
possible
,
visible_blocks
=
self
.
_get_visible_blocks
,
visible_blocks
=
self
.
_get_visible_blocks
,
attempted
=
self
.
attempted
first_attempted
=
self
.
all_total
.
first_attempted
,
)
)
@property
@property
...
...
lms/djangoapps/grades/scores.py
View file @
4f6d5d9c
...
@@ -101,7 +101,7 @@ def get_score(submissions_scores, csm_scores, persisted_block, block):
...
@@ -101,7 +101,7 @@ def get_score(submissions_scores, csm_scores, persisted_block, block):
# Priority order for retrieving the scores:
# Priority order for retrieving the scores:
# submissions API -> CSM -> grades persisted block -> latest block content
# submissions API -> CSM -> grades persisted block -> latest block content
raw_earned
,
raw_possible
,
weighted_earned
,
weighted_possible
,
attempted
=
(
raw_earned
,
raw_possible
,
weighted_earned
,
weighted_possible
,
first_
attempted
=
(
_get_score_from_submissions
(
submissions_scores
,
block
)
or
_get_score_from_submissions
(
submissions_scores
,
block
)
or
_get_score_from_csm
(
csm_scores
,
block
,
weight
)
or
_get_score_from_csm
(
csm_scores
,
block
,
weight
)
or
_get_score_from_persisted_or_latest_block
(
persisted_block
,
block
,
weight
)
_get_score_from_persisted_or_latest_block
(
persisted_block
,
block
,
weight
)
...
@@ -121,7 +121,7 @@ def get_score(submissions_scores, csm_scores, persisted_block, block):
...
@@ -121,7 +121,7 @@ def get_score(submissions_scores, csm_scores, persisted_block, block):
weighted_possible
,
weighted_possible
,
weight
,
weight
,
graded
,
graded
,
attempted
=
attempted
,
first_attempted
=
first_
attempted
,
)
)
...
@@ -149,10 +149,11 @@ def _get_score_from_submissions(submissions_scores, block):
...
@@ -149,10 +149,11 @@ def _get_score_from_submissions(submissions_scores, block):
if
submissions_scores
:
if
submissions_scores
:
submission_value
=
submissions_scores
.
get
(
unicode
(
block
.
location
))
submission_value
=
submissions_scores
.
get
(
unicode
(
block
.
location
))
if
submission_value
:
if
submission_value
:
attempted
=
True
first_attempted
=
submission_value
[
'created_at'
]
weighted_earned
,
weighted_possible
=
submission_value
weighted_earned
=
submission_value
[
'points_earned'
]
weighted_possible
=
submission_value
[
'points_possible'
]
assert
weighted_earned
>=
0.0
and
weighted_possible
>
0.0
# per contract from submissions API
assert
weighted_earned
>=
0.0
and
weighted_possible
>
0.0
# per contract from submissions API
return
(
None
,
None
)
+
(
weighted_earned
,
weighted_possible
)
+
(
attempted
,)
return
(
None
,
None
)
+
(
weighted_earned
,
weighted_possible
)
+
(
first_
attempted
,)
def
_get_score_from_csm
(
csm_scores
,
block
,
weight
):
def
_get_score_from_csm
(
csm_scores
,
block
,
weight
):
...
@@ -175,13 +176,14 @@ def _get_score_from_csm(csm_scores, block, weight):
...
@@ -175,13 +176,14 @@ def _get_score_from_csm(csm_scores, block, weight):
has_valid_score
=
score
and
score
.
total
is
not
None
has_valid_score
=
score
and
score
.
total
is
not
None
if
has_valid_score
:
if
has_valid_score
:
if
score
.
correct
is
not
None
:
if
score
.
correct
is
not
None
:
attempted
=
True
first_attempted
=
score
.
created
raw_earned
=
score
.
correct
raw_earned
=
score
.
correct
else
:
else
:
attempted
=
Fals
e
first_attempted
=
Non
e
raw_earned
=
0.0
raw_earned
=
0.0
raw_possible
=
score
.
total
raw_possible
=
score
.
total
return
(
raw_earned
,
raw_possible
)
+
weighted_score
(
raw_earned
,
raw_possible
,
weight
)
+
(
attempted
,)
return
(
raw_earned
,
raw_possible
)
+
weighted_score
(
raw_earned
,
raw_possible
,
weight
)
+
(
first_
attempted
,)
def
_get_score_from_persisted_or_latest_block
(
persisted_block
,
block
,
weight
):
def
_get_score_from_persisted_or_latest_block
(
persisted_block
,
block
,
weight
):
...
@@ -192,7 +194,7 @@ def _get_score_from_persisted_or_latest_block(persisted_block, block, weight):
...
@@ -192,7 +194,7 @@ def _get_score_from_persisted_or_latest_block(persisted_block, block, weight):
the latest block content.
the latest block content.
"""
"""
raw_earned
=
0.0
raw_earned
=
0.0
attempted
=
Fals
e
first_attempted
=
Non
e
if
persisted_block
:
if
persisted_block
:
raw_possible
=
persisted_block
.
raw_possible
raw_possible
=
persisted_block
.
raw_possible
...
@@ -205,7 +207,7 @@ def _get_score_from_persisted_or_latest_block(persisted_block, block, weight):
...
@@ -205,7 +207,7 @@ def _get_score_from_persisted_or_latest_block(persisted_block, block, weight):
else
:
else
:
weighted_scores
=
weighted_score
(
raw_earned
,
raw_possible
,
weight
)
weighted_scores
=
weighted_score
(
raw_earned
,
raw_possible
,
weight
)
return
(
raw_earned
,
raw_possible
)
+
weighted_scores
+
(
attempted
,)
return
(
raw_earned
,
raw_possible
)
+
weighted_scores
+
(
first_
attempted
,)
def
_get_weight_from_block
(
persisted_block
,
block
):
def
_get_weight_from_block
(
persisted_block
,
block
):
...
...
lms/djangoapps/grades/tests/test_grades.py
View file @
4f6d5d9c
...
@@ -2,8 +2,10 @@
...
@@ -2,8 +2,10 @@
Test grade calculation.
Test grade calculation.
"""
"""
import
d
dt
import
d
atetime
import
itertools
import
itertools
import
ddt
from
mock
import
patch
from
mock
import
patch
from
nose.plugins.attrib
import
attr
from
nose.plugins.attrib
import
attr
...
@@ -202,6 +204,8 @@ class TestWeightedProblems(SharedModuleStoreTestCase):
...
@@ -202,6 +204,8 @@ class TestWeightedProblems(SharedModuleStoreTestCase):
# verify all problem grades
# verify all problem grades
for
problem
in
self
.
problems
:
for
problem
in
self
.
problems
:
problem_score
=
subsection_grade
.
locations_to_scores
[
problem
.
location
]
problem_score
=
subsection_grade
.
locations_to_scores
[
problem
.
location
]
self
.
assertEqual
(
type
(
expected_score
.
first_attempted
),
type
(
problem_score
.
first_attempted
))
expected_score
.
first_attempted
=
problem_score
.
first_attempted
self
.
assertEquals
(
problem_score
,
expected_score
)
self
.
assertEquals
(
problem_score
,
expected_score
)
# verify subsection grades
# verify subsection grades
...
@@ -235,7 +239,7 @@ class TestWeightedProblems(SharedModuleStoreTestCase):
...
@@ -235,7 +239,7 @@ class TestWeightedProblems(SharedModuleStoreTestCase):
weighted_possible
=
expected_w_possible
,
weighted_possible
=
expected_w_possible
,
weight
=
weight
,
weight
=
weight
,
graded
=
expected_graded
,
graded
=
expected_graded
,
attempted
=
True
,
first_attempted
=
datetime
.
datetime
(
2010
,
1
,
1
)
,
)
)
self
.
_verify_grades
(
raw_earned
,
raw_possible
,
weight
,
expected_score
)
self
.
_verify_grades
(
raw_earned
,
raw_possible
,
weight
,
expected_score
)
...
...
lms/djangoapps/grades/tests/test_models.py
View file @
4f6d5d9c
...
@@ -14,8 +14,10 @@ from django.test import TestCase
...
@@ -14,8 +14,10 @@ from django.test import TestCase
from
django.utils.timezone
import
now
from
django.utils.timezone
import
now
from
freezegun
import
freeze_time
from
freezegun
import
freeze_time
from
opaque_keys.edx.locator
import
CourseLocator
,
BlockUsageLocator
from
opaque_keys.edx.locator
import
CourseLocator
,
BlockUsageLocator
import
pytz
from
track.event_transaction_utils
import
get_event_transaction_id
,
get_event_transaction_type
from
track.event_transaction_utils
import
get_event_transaction_id
,
get_event_transaction_type
from
lms.djangoapps.grades.config
import
waffle
from
lms.djangoapps.grades.models
import
(
from
lms.djangoapps.grades.models
import
(
BlockRecord
,
BlockRecord
,
BlockRecordList
,
BlockRecordList
,
...
@@ -212,7 +214,7 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
...
@@ -212,7 +214,7 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
"earned_graded"
:
6.0
,
"earned_graded"
:
6.0
,
"possible_graded"
:
8.0
,
"possible_graded"
:
8.0
,
"visible_blocks"
:
self
.
block_records
,
"visible_blocks"
:
self
.
block_records
,
"
attempted"
:
True
,
"
first_attempted"
:
datetime
(
2000
,
1
,
1
,
12
,
30
,
45
,
tzinfo
=
pytz
.
UTC
)
,
}
}
def
test_create
(
self
):
def
test_create
(
self
):
...
@@ -242,8 +244,8 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
...
@@ -242,8 +244,8 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
(
"possible_all"
,
IntegrityError
),
(
"possible_all"
,
IntegrityError
),
(
"earned_graded"
,
IntegrityError
),
(
"earned_graded"
,
IntegrityError
),
(
"possible_graded"
,
IntegrityError
),
(
"possible_graded"
,
IntegrityError
),
(
"first_attempted"
,
KeyError
),
(
"visible_blocks"
,
KeyError
),
(
"visible_blocks"
,
KeyError
),
(
"attempted"
,
KeyError
),
)
)
@ddt.unpack
@ddt.unpack
def
test_non_optional_fields
(
self
,
field
,
error
):
def
test_non_optional_fields
(
self
,
field
,
error
):
...
@@ -262,12 +264,21 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
...
@@ -262,12 +264,21 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
self
.
assertEqual
(
created_grade
.
id
,
updated_grade
.
id
)
self
.
assertEqual
(
created_grade
.
id
,
updated_grade
.
id
)
self
.
assertEqual
(
created_grade
.
earned_all
,
6
)
self
.
assertEqual
(
created_grade
.
earned_all
,
6
)
def
test_update_or_create_attempted
(
self
):
@ddt.unpack
grade
=
PersistentSubsectionGrade
.
update_or_create_grade
(
**
self
.
params
)
@ddt.data
(
self
.
assertIsInstance
(
grade
.
first_attempted
,
datetime
)
(
True
,
datetime
(
2000
,
1
,
1
,
12
,
30
,
45
,
tzinfo
=
pytz
.
UTC
)),
(
False
,
None
),
# Use as now(). Freeze time needs this calculation to happen at test time.
)
@freeze_time
(
now
())
def
test_update_or_create_attempted
(
self
,
is_active
,
expected_first_attempted
):
if
expected_first_attempted
is
None
:
expected_first_attempted
=
now
()
with
waffle
.
waffle
()
.
override
(
waffle
.
ESTIMATE_FIRST_ATTEMPTED
,
active
=
is_active
):
grade
=
PersistentSubsectionGrade
.
update_or_create_grade
(
**
self
.
params
)
self
.
assertEqual
(
grade
.
first_attempted
,
expected_first_attempted
)
def
test_unattempted
(
self
):
def
test_unattempted
(
self
):
self
.
params
[
'
attempted'
]
=
Fals
e
self
.
params
[
'
first_attempted'
]
=
Non
e
self
.
params
[
'earned_all'
]
=
0.0
self
.
params
[
'earned_all'
]
=
0.0
self
.
params
[
'earned_graded'
]
=
0.0
self
.
params
[
'earned_graded'
]
=
0.0
grade
=
PersistentSubsectionGrade
.
create_grade
(
**
self
.
params
)
grade
=
PersistentSubsectionGrade
.
create_grade
(
**
self
.
params
)
...
@@ -283,7 +294,7 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
...
@@ -283,7 +294,7 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
def
test_unattempted_save_does_not_remove_attempt
(
self
):
def
test_unattempted_save_does_not_remove_attempt
(
self
):
PersistentSubsectionGrade
.
create_grade
(
**
self
.
params
)
PersistentSubsectionGrade
.
create_grade
(
**
self
.
params
)
self
.
params
[
'
attempted'
]
=
Fals
e
self
.
params
[
'
first_attempted'
]
=
Non
e
grade
=
PersistentSubsectionGrade
.
update_or_create_grade
(
**
self
.
params
)
grade
=
PersistentSubsectionGrade
.
update_or_create_grade
(
**
self
.
params
)
self
.
assertIsInstance
(
grade
.
first_attempted
,
datetime
)
self
.
assertIsInstance
(
grade
.
first_attempted
,
datetime
)
self
.
assertEqual
(
grade
.
earned_all
,
6.0
)
self
.
assertEqual
(
grade
.
earned_all
,
6.0
)
...
...
lms/djangoapps/grades/tests/test_new.py
View file @
4f6d5d9c
...
@@ -181,7 +181,7 @@ class TestCourseGradeFactory(GradeTestBase):
...
@@ -181,7 +181,7 @@ class TestCourseGradeFactory(GradeTestBase):
with
self
.
assertNumQueries
(
12
),
mock_get_score
(
1
,
2
):
with
self
.
assertNumQueries
(
12
),
mock_get_score
(
1
,
2
):
_assert_create
(
expected_pass
=
True
)
_assert_create
(
expected_pass
=
True
)
with
self
.
assertNumQueries
(
1
4
),
mock_get_score
(
1
,
2
):
with
self
.
assertNumQueries
(
1
5
),
mock_get_score
(
1
,
2
):
grade_factory
.
update
(
self
.
request
.
user
,
self
.
course
)
grade_factory
.
update
(
self
.
request
.
user
,
self
.
course
)
with
self
.
assertNumQueries
(
1
):
with
self
.
assertNumQueries
(
1
):
...
@@ -294,7 +294,7 @@ class TestSubsectionGradeFactory(ProblemSubmissionTestMixin, GradeTestBase):
...
@@ -294,7 +294,7 @@ class TestSubsectionGradeFactory(ProblemSubmissionTestMixin, GradeTestBase):
self
.
assertFalse
(
mock_create_grade
.
called
)
self
.
assertFalse
(
mock_create_grade
.
called
)
self
.
assertEqual
(
grade_a
.
url_name
,
grade_b
.
url_name
)
self
.
assertEqual
(
grade_a
.
url_name
,
grade_b
.
url_name
)
grade_b
.
all_total
.
attempted
=
False
# TODO TNL-5930
grade_b
.
all_total
.
first_attempted
=
None
self
.
assertEqual
(
grade_a
.
all_total
,
grade_b
.
all_total
)
self
.
assertEqual
(
grade_a
.
all_total
,
grade_b
.
all_total
)
def
test_update
(
self
):
def
test_update
(
self
):
...
@@ -360,7 +360,7 @@ class ZeroGradeTest(GradeTestBase):
...
@@ -360,7 +360,7 @@ class ZeroGradeTest(GradeTestBase):
for
section
in
chapter_grades
[
chapter
][
'sections'
]:
for
section
in
chapter_grades
[
chapter
][
'sections'
]:
for
score
in
section
.
locations_to_scores
.
itervalues
():
for
score
in
section
.
locations_to_scores
.
itervalues
():
self
.
assertEqual
(
score
.
earned
,
0
)
self
.
assertEqual
(
score
.
earned
,
0
)
self
.
assertEqual
(
score
.
attempted
,
Fals
e
)
self
.
assertEqual
(
score
.
first_attempted
,
Non
e
)
self
.
assertEqual
(
section
.
all_total
.
earned
,
0
)
self
.
assertEqual
(
section
.
all_total
.
earned
,
0
)
...
@@ -403,7 +403,7 @@ class SubsectionGradeTest(GradeTestBase):
...
@@ -403,7 +403,7 @@ class SubsectionGradeTest(GradeTestBase):
)
)
self
.
assertEqual
(
input_grade
.
url_name
,
loaded_grade
.
url_name
)
self
.
assertEqual
(
input_grade
.
url_name
,
loaded_grade
.
url_name
)
loaded_grade
.
all_total
.
attempted
=
False
# TODO TNL-5930
loaded_grade
.
all_total
.
first_attempted
=
None
self
.
assertEqual
(
input_grade
.
all_total
,
loaded_grade
.
all_total
)
self
.
assertEqual
(
input_grade
.
all_total
,
loaded_grade
.
all_total
)
...
@@ -471,7 +471,7 @@ class TestMultipleProblemTypesSubsectionScores(SharedModuleStoreTestCase):
...
@@ -471,7 +471,7 @@ class TestMultipleProblemTypesSubsectionScores(SharedModuleStoreTestCase):
# Configure one block to return no possible score, the rest to return 3.0 earned / 7.0 possible
# Configure one block to return no possible score, the rest to return 3.0 earned / 7.0 possible
block_count
=
self
.
SCORED_BLOCK_COUNT
-
1
block_count
=
self
.
SCORED_BLOCK_COUNT
-
1
mock_score
.
side_effect
=
itertools
.
chain
(
mock_score
.
side_effect
=
itertools
.
chain
(
[(
earned_per_block
,
None
,
earned_per_block
,
None
,
True
)],
[(
earned_per_block
,
None
,
earned_per_block
,
None
,
datetime
.
datetime
(
2000
,
1
,
1
)
)],
itertools
.
repeat
(
mock_score
.
return_value
)
itertools
.
repeat
(
mock_score
.
return_value
)
)
)
score
=
subsection_factory
.
update
(
self
.
seq1
)
score
=
subsection_factory
.
update
(
self
.
seq1
)
...
...
lms/djangoapps/grades/tests/test_scores.py
View file @
4f6d5d9c
...
@@ -5,6 +5,7 @@ Tests for grades.scores module.
...
@@ -5,6 +5,7 @@ Tests for grades.scores module.
from
collections
import
namedtuple
from
collections
import
namedtuple
import
ddt
import
ddt
from
django.test
import
TestCase
from
django.test
import
TestCase
from
django.utils.timezone
import
now
import
itertools
import
itertools
from
lms.djangoapps.grades.models
import
BlockRecord
from
lms.djangoapps.grades.models
import
BlockRecord
...
@@ -15,6 +16,9 @@ from openedx.core.djangoapps.content.block_structure.block_structure import Bloc
...
@@ -15,6 +16,9 @@ from openedx.core.djangoapps.content.block_structure.block_structure import Bloc
from
xmodule.graders
import
ProblemScore
from
xmodule.graders
import
ProblemScore
NOW
=
now
()
class
TestScoredBlockTypes
(
TestCase
):
class
TestScoredBlockTypes
(
TestCase
):
"""
"""
Tests for the possibly_scored function.
Tests for the possibly_scored function.
...
@@ -47,12 +51,13 @@ class TestGetScore(TestCase):
...
@@ -47,12 +51,13 @@ class TestGetScore(TestCase):
display_name
=
'test_name'
display_name
=
'test_name'
location
=
'test_location'
location
=
'test_location'
SubmissionValue
=
namedtuple
(
'SubmissionValue'
,
'exists,
weighted_earned, weighted_possible
'
)
SubmissionValue
=
namedtuple
(
'SubmissionValue'
,
'exists,
points_earned, points_possible, created_at
'
)
CSMValue
=
namedtuple
(
'CSMValue'
,
'exists, raw_earned, raw_possible'
)
CSMValue
=
namedtuple
(
'CSMValue'
,
'exists, raw_earned, raw_possible
, created
'
)
PersistedBlockValue
=
namedtuple
(
'PersistedBlockValue'
,
'exists, raw_possible, weight, graded'
)
PersistedBlockValue
=
namedtuple
(
'PersistedBlockValue'
,
'exists, raw_possible, weight, graded'
)
ContentBlockValue
=
namedtuple
(
'ContentBlockValue'
,
'raw_possible, weight, explicit_graded'
)
ContentBlockValue
=
namedtuple
(
'ContentBlockValue'
,
'raw_possible, weight, explicit_graded'
)
ExpectedResult
=
namedtuple
(
ExpectedResult
=
namedtuple
(
'ExpectedResult'
,
'raw_earned, raw_possible, weighted_earned, weighted_possible, weight, graded, attempted'
'ExpectedResult'
,
'raw_earned, raw_possible, weighted_earned, weighted_possible, weight, graded, first_attempted'
)
)
def
_create_submissions_scores
(
self
,
submission_value
):
def
_create_submissions_scores
(
self
,
submission_value
):
...
@@ -60,7 +65,7 @@ class TestGetScore(TestCase):
...
@@ -60,7 +65,7 @@ class TestGetScore(TestCase):
Creates a stub result from the submissions API for the given values.
Creates a stub result from the submissions API for the given values.
"""
"""
if
submission_value
.
exists
:
if
submission_value
.
exists
:
return
{
self
.
location
:
(
submission_value
.
weighted_earned
,
submission_value
.
weighted_possible
)}
return
{
self
.
location
:
submission_value
.
_asdict
(
)}
else
:
else
:
return
{}
return
{}
...
@@ -69,8 +74,14 @@ class TestGetScore(TestCase):
...
@@ -69,8 +74,14 @@ class TestGetScore(TestCase):
Creates a stub result from courseware student module for the given values.
Creates a stub result from courseware student module for the given values.
"""
"""
if
csm_value
.
exists
:
if
csm_value
.
exists
:
stub_csm_record
=
namedtuple
(
'stub_csm_record'
,
'correct, total'
)
stub_csm_record
=
namedtuple
(
'stub_csm_record'
,
'correct, total, created'
)
return
{
self
.
location
:
stub_csm_record
(
correct
=
csm_value
.
raw_earned
,
total
=
csm_value
.
raw_possible
)}
return
{
self
.
location
:
stub_csm_record
(
correct
=
csm_value
.
raw_earned
,
total
=
csm_value
.
raw_possible
,
created
=
csm_value
.
created
)
}
else
:
else
:
return
{}
return
{}
...
@@ -106,64 +117,66 @@ class TestGetScore(TestCase):
...
@@ -106,64 +117,66 @@ class TestGetScore(TestCase):
return
block
return
block
@ddt.data
(
@ddt.data
(
# submissions _trumps_ other values; weighted and graded from persisted-block _trumps_ latest content values
# The value from Submissions trumps other values; The persisted value
# from persisted-block trumps latest content values
(
(
SubmissionValue
(
exists
=
True
,
weighted_earned
=
50
,
weighted_possible
=
100
),
SubmissionValue
(
exists
=
True
,
points_earned
=
50
,
points_possible
=
100
,
created_at
=
NOW
),
CSMValue
(
exists
=
True
,
raw_earned
=
10
,
raw_possible
=
40
),
CSMValue
(
exists
=
True
,
raw_earned
=
10
,
raw_possible
=
40
,
created
=
NOW
),
PersistedBlockValue
(
exists
=
True
,
raw_possible
=
5
,
weight
=
40
,
graded
=
True
),
PersistedBlockValue
(
exists
=
True
,
raw_possible
=
5
,
weight
=
40
,
graded
=
True
),
ContentBlockValue
(
raw_possible
=
1
,
weight
=
20
,
explicit_graded
=
False
),
ContentBlockValue
(
raw_possible
=
1
,
weight
=
20
,
explicit_graded
=
False
),
ExpectedResult
(
ExpectedResult
(
raw_earned
=
None
,
raw_possible
=
None
,
raw_earned
=
None
,
raw_possible
=
None
,
weighted_earned
=
50
,
weighted_possible
=
100
,
weighted_earned
=
50
,
weighted_possible
=
100
,
weight
=
40
,
graded
=
True
,
attempted
=
True
,
weight
=
40
,
graded
=
True
,
first_attempted
=
NOW
),
),
),
),
# same as above, except
s
ubmissions doesn't exist; CSM values used
# same as above, except
S
ubmissions doesn't exist; CSM values used
(
(
SubmissionValue
(
exists
=
False
,
weighted_earned
=
50
,
weighted_possible
=
100
),
SubmissionValue
(
exists
=
False
,
points_earned
=
50
,
points_possible
=
100
,
created_at
=
NOW
),
CSMValue
(
exists
=
True
,
raw_earned
=
10
,
raw_possible
=
40
),
CSMValue
(
exists
=
True
,
raw_earned
=
10
,
raw_possible
=
40
,
created
=
NOW
),
PersistedBlockValue
(
exists
=
True
,
raw_possible
=
5
,
weight
=
40
,
graded
=
True
),
PersistedBlockValue
(
exists
=
True
,
raw_possible
=
5
,
weight
=
40
,
graded
=
True
),
ContentBlockValue
(
raw_possible
=
1
,
weight
=
20
,
explicit_graded
=
False
),
ContentBlockValue
(
raw_possible
=
1
,
weight
=
20
,
explicit_graded
=
False
),
ExpectedResult
(
ExpectedResult
(
raw_earned
=
10
,
raw_possible
=
40
,
raw_earned
=
10
,
raw_possible
=
40
,
weighted_earned
=
10
,
weighted_possible
=
40
,
weighted_earned
=
10
,
weighted_possible
=
40
,
weight
=
40
,
graded
=
True
,
attempted
=
True
,
weight
=
40
,
graded
=
True
,
first_attempted
=
NOW
,
),
),
),
),
# CSM values exist, but with NULL earned score treated as not-attempted
# CSM values exist, but with NULL earned score treated as not-attempted
(
(
SubmissionValue
(
exists
=
False
,
weighted_earned
=
50
,
weighted_possible
=
100
),
SubmissionValue
(
exists
=
False
,
points_earned
=
50
,
points_possible
=
100
,
created_at
=
NOW
),
CSMValue
(
exists
=
True
,
raw_earned
=
None
,
raw_possible
=
40
),
CSMValue
(
exists
=
True
,
raw_earned
=
None
,
raw_possible
=
40
,
created
=
NOW
),
PersistedBlockValue
(
exists
=
True
,
raw_possible
=
5
,
weight
=
40
,
graded
=
True
),
PersistedBlockValue
(
exists
=
True
,
raw_possible
=
5
,
weight
=
40
,
graded
=
True
),
ContentBlockValue
(
raw_possible
=
1
,
weight
=
20
,
explicit_graded
=
False
),
ContentBlockValue
(
raw_possible
=
1
,
weight
=
20
,
explicit_graded
=
False
),
ExpectedResult
(
ExpectedResult
(
raw_earned
=
0
,
raw_possible
=
40
,
raw_earned
=
0
,
raw_possible
=
40
,
weighted_earned
=
0
,
weighted_possible
=
40
,
weighted_earned
=
0
,
weighted_possible
=
40
,
weight
=
40
,
graded
=
True
,
attempted
=
False
,
weight
=
40
,
graded
=
True
,
first_attempted
=
None
),
),
),
),
# neither submissions nor CSM exist; Persisted values used
# neither submissions nor CSM exist; Persisted values used
(
(
SubmissionValue
(
exists
=
False
,
weighted_earned
=
50
,
weighted_possible
=
100
),
SubmissionValue
(
exists
=
False
,
points_earned
=
50
,
points_possible
=
100
,
created_at
=
NOW
),
CSMValue
(
exists
=
False
,
raw_earned
=
10
,
raw_possible
=
40
),
CSMValue
(
exists
=
False
,
raw_earned
=
10
,
raw_possible
=
40
,
created
=
NOW
),
PersistedBlockValue
(
exists
=
True
,
raw_possible
=
5
,
weight
=
40
,
graded
=
True
),
PersistedBlockValue
(
exists
=
True
,
raw_possible
=
5
,
weight
=
40
,
graded
=
True
),
ContentBlockValue
(
raw_possible
=
1
,
weight
=
20
,
explicit_graded
=
False
),
ContentBlockValue
(
raw_possible
=
1
,
weight
=
20
,
explicit_graded
=
False
),
ExpectedResult
(
ExpectedResult
(
raw_earned
=
0
,
raw_possible
=
5
,
raw_earned
=
0
,
raw_possible
=
5
,
weighted_earned
=
0
,
weighted_possible
=
40
,
weighted_earned
=
0
,
weighted_possible
=
40
,
weight
=
40
,
graded
=
True
,
attempted
=
False
,
weight
=
40
,
graded
=
True
,
first_attempted
=
None
),
),
),
),
# none of submissions, CSM, or persisted exist; Latest content values used
# none of submissions, CSM, or persisted exist; Latest content values used
(
(
SubmissionValue
(
exists
=
False
,
weighted_earned
=
50
,
weighted_possible
=
100
),
SubmissionValue
(
exists
=
False
,
points_earned
=
50
,
points_possible
=
100
,
created_at
=
NOW
),
CSMValue
(
exists
=
False
,
raw_earned
=
10
,
raw_possible
=
40
),
CSMValue
(
exists
=
False
,
raw_earned
=
10
,
raw_possible
=
40
,
created
=
NOW
),
PersistedBlockValue
(
exists
=
False
,
raw_possible
=
5
,
weight
=
40
,
graded
=
True
),
PersistedBlockValue
(
exists
=
False
,
raw_possible
=
5
,
weight
=
40
,
graded
=
True
),
ContentBlockValue
(
raw_possible
=
1
,
weight
=
20
,
explicit_graded
=
False
),
ContentBlockValue
(
raw_possible
=
1
,
weight
=
20
,
explicit_graded
=
False
),
ExpectedResult
(
ExpectedResult
(
raw_earned
=
0
,
raw_possible
=
1
,
raw_earned
=
0
,
raw_possible
=
1
,
weighted_earned
=
0
,
weighted_possible
=
20
,
weighted_earned
=
0
,
weighted_possible
=
20
,
weight
=
20
,
graded
=
False
,
attempted
=
False
,
weight
=
20
,
graded
=
False
,
first_attempted
=
None
),
),
),
),
)
)
...
@@ -278,7 +291,7 @@ class TestInternalGetScoreFromBlock(TestCase):
...
@@ -278,7 +291,7 @@ class TestInternalGetScoreFromBlock(TestCase):
"""
"""
# pylint: disable=unbalanced-tuple-unpacking
# pylint: disable=unbalanced-tuple-unpacking
(
(
raw_earned
,
raw_possible
,
weighted_earned
,
weighted_possible
,
attempted
raw_earned
,
raw_possible
,
weighted_earned
,
weighted_possible
,
first_
attempted
)
=
scores
.
_get_score_from_persisted_or_latest_block
(
persisted_block
,
block
,
weight
)
)
=
scores
.
_get_score_from_persisted_or_latest_block
(
persisted_block
,
block
,
weight
)
self
.
assertEquals
(
raw_earned
,
0.0
)
self
.
assertEquals
(
raw_earned
,
0.0
)
...
@@ -288,7 +301,7 @@ class TestInternalGetScoreFromBlock(TestCase):
...
@@ -288,7 +301,7 @@ class TestInternalGetScoreFromBlock(TestCase):
self
.
assertEquals
(
weighted_possible
,
expected_r_possible
)
self
.
assertEquals
(
weighted_possible
,
expected_r_possible
)
else
:
else
:
self
.
assertEquals
(
weighted_possible
,
weight
)
self
.
assertEquals
(
weighted_possible
,
weight
)
self
.
assert
False
(
attempted
)
self
.
assert
IsNone
(
first_
attempted
)
@ddt.data
(
@ddt.data
(
*
itertools
.
product
((
0
,
1
,
5
),
(
None
,
0
,
1
,
5
))
*
itertools
.
product
((
0
,
1
,
5
),
(
None
,
0
,
1
,
5
))
...
...
lms/djangoapps/grades/tests/utils.py
View file @
4f6d5d9c
...
@@ -2,6 +2,7 @@
...
@@ -2,6 +2,7 @@
Utilities for grades related tests
Utilities for grades related tests
"""
"""
from
contextlib
import
contextmanager
from
contextlib
import
contextmanager
from
datetime
import
datetime
from
mock
import
patch
from
mock
import
patch
from
courseware.module_render
import
get_module
from
courseware.module_render
import
get_module
from
courseware.model_data
import
FieldDataCache
from
courseware.model_data
import
FieldDataCache
...
@@ -33,18 +34,18 @@ def mock_get_score(earned=0, possible=1):
...
@@ -33,18 +34,18 @@ def mock_get_score(earned=0, possible=1):
weighted_possible
=
possible
,
weighted_possible
=
possible
,
weight
=
1
,
weight
=
1
,
graded
=
True
,
graded
=
True
,
attempted
=
True
,
first_attempted
=
datetime
(
2000
,
1
,
1
,
0
,
0
,
0
)
)
)
yield
mock_score
yield
mock_score
@contextmanager
@contextmanager
def
mock_get_submissions_score
(
earned
=
0
,
possible
=
1
,
attempted
=
True
):
def
mock_get_submissions_score
(
earned
=
0
,
possible
=
1
,
first_attempted
=
datetime
(
2000
,
1
,
1
,
0
,
0
,
0
)
):
"""
"""
Mocks the _get_submissions_score function to return the specified values
Mocks the _get_submissions_score function to return the specified values
"""
"""
with
patch
(
'lms.djangoapps.grades.scores._get_score_from_submissions'
)
as
mock_score
:
with
patch
(
'lms.djangoapps.grades.scores._get_score_from_submissions'
)
as
mock_score
:
mock_score
.
return_value
=
(
earned
,
possible
,
earned
,
possible
,
attempted
)
mock_score
.
return_value
=
(
earned
,
possible
,
earned
,
possible
,
first_
attempted
)
yield
mock_score
yield
mock_score
...
...
lms/djangoapps/instructor_task/tasks_helper.py
View file @
4f6d5d9c
...
@@ -848,7 +848,7 @@ def upload_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input,
...
@@ -848,7 +848,7 @@ def upload_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input,
except
KeyError
:
except
KeyError
:
grade_results
.
append
([
u'Not Available'
])
grade_results
.
append
([
u'Not Available'
])
else
:
else
:
if
subsection_grade
.
graded_total
.
attempted
:
if
subsection_grade
.
graded_total
.
first_attempted
is
not
None
:
grade_results
.
append
(
grade_results
.
append
(
[
subsection_grade
.
graded_total
.
earned
/
subsection_grade
.
graded_total
.
possible
]
[
subsection_grade
.
graded_total
.
earned
/
subsection_grade
.
graded_total
.
possible
]
)
)
...
@@ -1028,7 +1028,7 @@ def upload_problem_grade_report(_xmodule_instance_args, _entry_id, course_id, _t
...
@@ -1028,7 +1028,7 @@ def upload_problem_grade_report(_xmodule_instance_args, _entry_id, course_id, _t
except
KeyError
:
except
KeyError
:
earned_possible_values
.
append
([
u'Not Available'
,
u'Not Available'
])
earned_possible_values
.
append
([
u'Not Available'
,
u'Not Available'
])
else
:
else
:
if
problem_score
.
attempted
:
if
problem_score
.
first_
attempted
:
earned_possible_values
.
append
([
problem_score
.
earned
,
problem_score
.
possible
])
earned_possible_values
.
append
([
problem_score
.
earned
,
problem_score
.
possible
])
else
:
else
:
earned_possible_values
.
append
([
u'Not Attempted'
,
problem_score
.
possible
])
earned_possible_values
.
append
([
u'Not Attempted'
,
problem_score
.
possible
])
...
...
requirements/edx/github.txt
View file @
4f6d5d9c
...
@@ -75,8 +75,8 @@ git+https://github.com/edx/lettuce.git@0.2.20.002#egg=lettuce==0.2.20.002
...
@@ -75,8 +75,8 @@ git+https://github.com/edx/lettuce.git@0.2.20.002#egg=lettuce==0.2.20.002
-e git+https://github.com/edx/event-tracking.git@0.2.1#egg=event-tracking==0.2.1
-e git+https://github.com/edx/event-tracking.git@0.2.1#egg=event-tracking==0.2.1
-e git+https://github.com/edx/django-splash.git@v0.2#egg=django-splash==0.2
-e git+https://github.com/edx/django-splash.git@v0.2#egg=django-splash==0.2
-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
git+https://github.com/edx/edx-ora2.git@1.3.
2#egg=ora2==1.3.2
git+https://github.com/edx/edx-ora2.git@1.3.
3#egg=ora2==1.3.3
-e git+https://github.com/edx/edx-submissions.git@
1.2.0#egg=edx-submissions==1.2
.0
-e git+https://github.com/edx/edx-submissions.git@
2.0.0#egg=edx-submissions==2.0
.0
git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3
git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3
git+https://github.com/edx/edx-val.git@0.0.13#egg=edxval==0.0.13
git+https://github.com/edx/edx-val.git@0.0.13#egg=edxval==0.0.13
git+https://github.com/pmitros/RecommenderXBlock.git@v1.2#egg=recommender-xblock==1.2
git+https://github.com/pmitros/RecommenderXBlock.git@v1.2#egg=recommender-xblock==1.2
...
...
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