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
e14ebaaf
Commit
e14ebaaf
authored
Aug 07, 2012
by
Calen Pennington
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #345 from MITx/MITx/feature/bridger/fast_course_grading
Fast grading
parents
49b1678b
3a52e86a
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
435 additions
and
184 deletions
+435
-184
common/djangoapps/util/decorators.py
+37
-0
common/lib/xmodule/xmodule/capa_module.py
+1
-1
common/lib/xmodule/xmodule/course_module.py
+67
-19
lms/djangoapps/certificates/views.py
+2
-2
lms/djangoapps/courseware/grades.py
+144
-59
lms/djangoapps/courseware/management/commands/check_course.py
+2
-2
lms/djangoapps/courseware/models.py
+51
-24
lms/djangoapps/courseware/module_render.py
+73
-40
lms/djangoapps/courseware/views.py
+22
-16
lms/templates/gradebook.html
+15
-13
lms/templates/profile.html
+1
-1
lms/templates/profile_graphs.js
+15
-5
lms/urls.py
+5
-2
No files found.
common/djangoapps/util/decorators.py
0 → 100644
View file @
e14ebaaf
def
lazyproperty
(
fn
):
"""
Use this decorator for lazy generation of properties that
are expensive to compute. From http://stackoverflow.com/a/3013910/86828
Example:
class Test(object):
@lazyproperty
def a(self):
print 'generating "a"'
return range(5)
Interactive Session:
>>> t = Test()
>>> t.__dict__
{}
>>> t.a
generating "a"
[0, 1, 2, 3, 4]
>>> t.__dict__
{'_lazy_a': [0, 1, 2, 3, 4]}
>>> t.a
[0, 1, 2, 3, 4]
"""
attr_name
=
'_lazy_'
+
fn
.
__name__
@property
def
_lazyprop
(
self
):
if
not
hasattr
(
self
,
attr_name
):
setattr
(
self
,
attr_name
,
fn
(
self
))
return
getattr
(
self
,
attr_name
)
return
_lazyprop
\ No newline at end of file
common/lib/xmodule/xmodule/capa_module.py
View file @
e14ebaaf
...
...
@@ -130,7 +130,7 @@ class CapaModule(XModule):
if
weight_string
:
self
.
weight
=
float
(
weight_string
)
else
:
self
.
weight
=
1
self
.
weight
=
None
if
self
.
rerandomize
==
'never'
:
seed
=
1
...
...
common/lib/xmodule/xmodule/course_module.py
View file @
e14ebaaf
...
...
@@ -3,6 +3,7 @@ import time
import
dateutil.parser
import
logging
from
util.decorators
import
lazyproperty
from
xmodule.graders
import
load_grading_policy
from
xmodule.modulestore
import
Location
from
xmodule.seq_module
import
SequenceDescriptor
,
SequenceModule
...
...
@@ -16,9 +17,6 @@ class CourseDescriptor(SequenceDescriptor):
def
__init__
(
self
,
system
,
definition
=
None
,
**
kwargs
):
super
(
CourseDescriptor
,
self
)
.
__init__
(
system
,
definition
,
**
kwargs
)
self
.
_grader
=
None
self
.
_grade_cutoffs
=
None
msg
=
None
try
:
...
...
@@ -42,29 +40,79 @@ class CourseDescriptor(SequenceDescriptor):
@property
def
grader
(
self
):
self
.
__load_grading_policy
()
return
self
.
_grader
return
self
.
__grading_policy
[
'GRADER'
]
@property
def
grade_cutoffs
(
self
):
self
.
__load_grading_policy
()
return
self
.
_grade_cutoffs
return
self
.
__grading_policy
[
'GRADE_CUTOFFS'
]
def
__load_grading_policy
(
self
):
if
not
self
.
_grader
or
not
self
.
_grade_cutoffs
:
policy_string
=
""
@lazyproperty
def
__grading_policy
(
self
):
policy_string
=
""
try
:
with
self
.
system
.
resources_fs
.
open
(
"grading_policy.json"
)
as
grading_policy_file
:
policy_string
=
grading_policy_file
.
read
()
except
(
IOError
,
ResourceNotFoundError
):
log
.
warning
(
"Unable to load course settings file from grading_policy.json in course "
+
self
.
id
)
try
:
with
self
.
system
.
resources_fs
.
open
(
"grading_policy.json"
)
as
grading_policy_file
:
policy_string
=
grading_policy_file
.
read
()
except
(
IOError
,
ResourceNotFoundError
):
log
.
warning
(
"Unable to load course settings file from grading_policy.json in course "
+
self
.
id
)
grading_policy
=
load_grading_policy
(
policy_string
)
return
grading_policy
@lazyproperty
def
grading_context
(
self
):
"""
This returns a dictionary with keys necessary for quickly grading
a student. They are used by grades.grade()
The grading context has two keys:
graded_sections - This contains the sections that are graded, as
well as all possible children modules that can affect the
grading. This allows some sections to be skipped if the student
hasn't seen any part of it.
grading_policy
=
load_grading_policy
(
policy_string
)
The format is a dictionary keyed by section-type. The values are
arrays of dictionaries containing
"section_descriptor" : The section descriptor
"xmoduledescriptors" : An array of xmoduledescriptors that
could possibly be in the section, for any student
all_descriptors - This contains a list of all xmodules that can
effect grading a student. This is used to efficiently fetch
all the xmodule state for a StudentModuleCache without walking
the descriptor tree again.
self
.
_grader
=
grading_policy
[
'GRADER'
]
self
.
_grade_cutoffs
=
grading_policy
[
'GRADE_CUTOFFS'
]
"""
all_descriptors
=
[]
graded_sections
=
{}
def
yield_descriptor_descendents
(
module_descriptor
):
for
child
in
module_descriptor
.
get_children
():
yield
child
for
module_descriptor
in
yield_descriptor_descendents
(
child
):
yield
module_descriptor
for
c
in
self
.
get_children
():
sections
=
[]
for
s
in
c
.
get_children
():
if
s
.
metadata
.
get
(
'graded'
,
False
):
# TODO: Only include modules that have a score here
xmoduledescriptors
=
[
child
for
child
in
yield_descriptor_descendents
(
s
)]
section_description
=
{
'section_descriptor'
:
s
,
'xmoduledescriptors'
:
xmoduledescriptors
}
section_format
=
s
.
metadata
.
get
(
'format'
,
""
)
graded_sections
[
section_format
]
=
graded_sections
.
get
(
section_format
,
[]
)
+
[
section_description
]
all_descriptors
.
extend
(
xmoduledescriptors
)
all_descriptors
.
append
(
s
)
return
{
'graded_sections'
:
graded_sections
,
'all_descriptors'
:
all_descriptors
,}
@staticmethod
...
...
lms/djangoapps/certificates/views.py
View file @
e14ebaaf
...
...
@@ -52,7 +52,7 @@ def certificate_request(request):
return
return_error
(
survey_response
[
'error'
])
grade
=
None
student_gradesheet
=
grades
.
grade
_sheet
(
request
.
user
)
student_gradesheet
=
grades
.
grade
(
request
.
user
,
request
,
course
)
grade
=
student_gradesheet
[
'grade'
]
if
not
grade
:
...
...
@@ -65,7 +65,7 @@ def certificate_request(request):
else
:
#This is not a POST, we should render the page with the form
grade_sheet
=
grades
.
grade_sheet
(
request
.
user
)
student_gradesheet
=
grades
.
grade
(
request
.
user
,
request
,
course
)
certificate_state
=
certificate_state_for_student
(
request
.
user
,
grade_sheet
[
'grade'
])
if
certificate_state
[
'state'
]
!=
"requestable"
:
...
...
lms/djangoapps/courseware/grades.py
View file @
e14ebaaf
...
...
@@ -3,26 +3,135 @@ import logging
from
django.conf
import
settings
from
models
import
StudentModuleCache
from
module_render
import
get_module
,
get_instance_module
from
xmodule
import
graders
from
xmodule.graders
import
Score
from
models
import
StudentModule
_
log
=
logging
.
getLogger
(
"mitx.courseware"
)
log
=
logging
.
getLogger
(
"mitx.courseware"
)
def
grade_sheet
(
student
,
course
,
grader
,
student_module_cache
):
def
yield_module_descendents
(
module
):
for
child
in
module
.
get_display_items
():
yield
child
for
module
in
yield_module_descendents
(
child
):
yield
module
def
grade
(
student
,
request
,
course
,
student_module_cache
=
None
):
"""
This pulls a summary of all problems in the course. It returns a dictionary
with two datastructures:
- courseware_summary is a summary of all sections with problems in the
course. It is organized as an array of chapters, each containing an array of
sections, each containing an array of scores. This contains information for
graded and ungraded problems, and is good for displaying a course summary
with due dates, etc.
This grades a student as quickly as possible. It retuns the
output from the course grader, augmented with the final letter
grade. The keys in the output are:
- grade : A final letter grade.
- percent : The final percent for the class (rounded up).
- section_breakdown : A breakdown of each section that makes
up the grade. (For display)
- grade_breakdown : A breakdown of the major components that
make up the final grade. (For display)
More information on the format is in the docstring for CourseGrader.
"""
grading_context
=
course
.
grading_context
if
student_module_cache
==
None
:
student_module_cache
=
StudentModuleCache
(
student
,
grading_context
[
'all_descriptors'
])
totaled_scores
=
{}
# This next complicated loop is just to collect the totaled_scores, which is
# passed to the grader
for
section_format
,
sections
in
grading_context
[
'graded_sections'
]
.
iteritems
():
format_scores
=
[]
for
section
in
sections
:
section_descriptor
=
section
[
'section_descriptor'
]
section_name
=
section_descriptor
.
metadata
.
get
(
'display_name'
)
should_grade_section
=
False
# If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0%
for
moduledescriptor
in
section
[
'xmoduledescriptors'
]:
if
student_module_cache
.
lookup
(
moduledescriptor
.
category
,
moduledescriptor
.
location
.
url
()
):
should_grade_section
=
True
break
if
should_grade_section
:
scores
=
[]
# TODO: We need the request to pass into here. If we could forgo that, our arguments
# would be simpler
section_module
=
get_module
(
student
,
request
,
section_descriptor
.
location
,
student_module_cache
)
# TODO: We may be able to speed this up by only getting a list of children IDs from section_module
# Then, we may not need to instatiate any problems if they are already in the database
for
module
in
yield_module_descendents
(
section_module
):
(
correct
,
total
)
=
get_score
(
student
,
module
,
student_module_cache
)
if
correct
is
None
and
total
is
None
:
continue
if
settings
.
GENERATE_PROFILE_SCORES
:
if
total
>
1
:
correct
=
random
.
randrange
(
max
(
total
-
2
,
1
),
total
+
1
)
else
:
correct
=
total
graded
=
module
.
metadata
.
get
(
"graded"
,
False
)
if
not
total
>
0
:
#We simply cannot grade a problem that is 12/0, because we might need it as a percentage
graded
=
False
scores
.
append
(
Score
(
correct
,
total
,
graded
,
module
.
metadata
.
get
(
'display_name'
)))
section_total
,
graded_total
=
graders
.
aggregate_scores
(
scores
,
section_name
)
else
:
section_total
=
Score
(
0.0
,
1.0
,
False
,
section_name
)
graded_total
=
Score
(
0.0
,
1.0
,
True
,
section_name
)
#Add the graded total to totaled_scores
if
graded_total
.
possible
>
0
:
format_scores
.
append
(
graded_total
)
else
:
log
.
exception
(
"Unable to grade a section with a total possible score of zero. "
+
str
(
section_descriptor
.
id
))
totaled_scores
[
section_format
]
=
format_scores
grade_summary
=
course
.
grader
.
grade
(
totaled_scores
)
# We round the grade here, to make sure that the grade is an whole percentage and
# doesn't get displayed differently than it gets grades
grade_summary
[
'percent'
]
=
round
(
grade_summary
[
'percent'
]
*
100
+
0.05
)
/
100
letter_grade
=
grade_for_percentage
(
course
.
grade_cutoffs
,
grade_summary
[
'percent'
])
grade_summary
[
'grade'
]
=
letter_grade
return
grade_summary
def
grade_for_percentage
(
grade_cutoffs
,
percentage
):
"""
Returns a letter grade 'A' 'B' 'C' or None.
Arguments
- grade_cutoffs is a dictionary mapping a grade to the lowest
possible percentage to earn that grade.
- percentage is the final percent across all problems in a course
"""
letter_grade
=
None
for
possible_grade
in
[
'A'
,
'B'
,
'C'
]:
if
percentage
>=
grade_cutoffs
[
possible_grade
]:
letter_grade
=
possible_grade
break
return
letter_grade
def
progress_summary
(
student
,
course
,
grader
,
student_module_cache
):
"""
This pulls a summary of all problems in the course.
- grade_summary is the output from the course grader. More information on
the format is in the docstring for CourseGrader.
Returns
- courseware_summary is a summary of all sections with problems in the course.
It is organized as an array of chapters, each containing an array of sections,
each containing an array of scores. This contains information for graded and
ungraded problems, and is good for displaying a course summary with due dates,
etc.
Arguments:
student: A User object for the student to grade
...
...
@@ -30,49 +139,24 @@ def grade_sheet(student, course, grader, student_module_cache):
student_module_cache: A StudentModuleCache initialized with all
instance_modules for the student
"""
totaled_scores
=
{}
chapters
=
[]
for
c
in
course
.
get_children
():
sections
=
[]
for
s
in
c
.
get_children
():
def
yield_descendents
(
module
):
yield
module
for
child
in
module
.
get_display_items
():
for
module
in
yield_descendents
(
child
):
yield
module
graded
=
s
.
metadata
.
get
(
'graded'
,
False
)
scores
=
[]
for
module
in
yield_descendents
(
s
):
for
module
in
yield_
module_
descendents
(
s
):
(
correct
,
total
)
=
get_score
(
student
,
module
,
student_module_cache
)
if
correct
is
None
and
total
is
None
:
continue
if
settings
.
GENERATE_PROFILE_SCORES
:
if
total
>
1
:
correct
=
random
.
randrange
(
max
(
total
-
2
,
1
),
total
+
1
)
else
:
correct
=
total
if
not
total
>
0
:
#We simply cannot grade a problem that is 12/0, because we
#might need it as a percentage
graded
=
False
scores
.
append
(
Score
(
correct
,
total
,
graded
,
module
.
metadata
.
get
(
'display_name'
)))
scores
.
append
(
Score
(
correct
,
total
,
graded
,
module
.
metadata
.
get
(
'display_name'
)))
section_total
,
graded_total
=
graders
.
aggregate_scores
(
scores
,
s
.
metadata
.
get
(
'display_name'
))
#Add the graded total to totaled_scores
format
=
s
.
metadata
.
get
(
'format'
,
""
)
if
format
and
graded_total
.
possible
>
0
:
format_scores
=
totaled_scores
.
get
(
format
,
[])
format_scores
.
append
(
graded_total
)
totaled_scores
[
format
]
=
format_scores
sections
.
append
({
'display_name'
:
s
.
display_name
,
'url_name'
:
s
.
url_name
,
...
...
@@ -88,13 +172,10 @@ def grade_sheet(student, course, grader, student_module_cache):
'url_name'
:
c
.
url_name
,
'sections'
:
sections
})
grade_summary
=
grader
.
grade
(
totaled_scores
)
return
{
'courseware_summary'
:
chapters
,
'grade_summary'
:
grade_summary
}
return
chapters
def
get_score
(
user
,
problem
,
cache
):
def
get_score
(
user
,
problem
,
student_module_
cache
):
"""
Return the score for a user on a problem
...
...
@@ -105,17 +186,18 @@ def get_score(user, problem, cache):
correct
=
0.0
# If the ID is not in the cache, add the item
instance_module
=
cache
.
lookup
(
problem
.
category
,
problem
.
id
)
if
instance_module
is
None
:
instance_module
=
StudentModule
(
module_type
=
problem
.
category
,
module_state_key
=
problem
.
id
,
student
=
user
,
state
=
None
,
grade
=
0
,
max_grade
=
problem
.
max_score
(),
done
=
'i'
)
cache
.
append
(
instance_module
)
instance_module
.
save
()
instance_module
=
get_instance_module
(
user
,
problem
,
student_module_cache
)
# instance_module = student_module_cache.lookup(problem.category, problem.id)
# if instance_module is None:
# instance_module = StudentModule(module_type=problem.category,
# module_state_key=problem.id,
# student=user,
# state=None,
# grade=0,
# max_grade=problem.max_score(),
# done='i')
# cache.append(instance_module)
# instance_module.save()
# If this problem is ungraded/ungradable, bail
if
instance_module
.
max_grade
is
None
:
...
...
@@ -126,8 +208,11 @@ def get_score(user, problem, cache):
if
correct
is
not
None
and
total
is
not
None
:
#Now we re-weight the problem, if specified
weight
=
getattr
(
problem
,
'weight'
,
1
)
if
weight
!=
1
:
weight
=
getattr
(
problem
,
'weight'
,
None
)
if
weight
is
not
None
:
if
total
==
0
:
log
.
exception
(
"Cannot reweight a problem with zero weight. Problem: "
+
str
(
instance_module
))
return
(
correct
,
total
)
correct
=
correct
*
weight
/
total
total
=
weight
...
...
lms/djangoapps/courseware/management/commands/check_course.py
View file @
e14ebaaf
...
...
@@ -78,8 +78,8 @@ class Command(BaseCommand):
# TODO (cpennington): Get coursename in a legitimate way
course_location
=
'i4x://edx/6002xs12/course/6.002_Spring_2012'
student_module_cache
=
StudentModuleCache
(
sample_user
,
modulestore
()
.
get_item
(
course_location
))
(
course
,
_
,
_
,
_
)
=
get_module
(
sample_user
,
None
,
course_location
,
student_module_cache
)
student_module_cache
=
StudentModuleCache
.
cache_for_descriptor_descendents
(
sample_user
,
modulestore
()
.
get_item
(
course_location
))
course
=
get_module
(
sample_user
,
None
,
course_location
,
student_module_cache
)
to_run
=
[
#TODO (vshnayder) : make check_rendering work (use module_render.py),
...
...
lms/djangoapps/courseware/models.py
View file @
e14ebaaf
...
...
@@ -67,17 +67,19 @@ class StudentModuleCache(object):
"""
A cache of StudentModules for a specific student
"""
def
__init__
(
self
,
user
,
descriptor
,
depth
=
None
):
def
__init__
(
self
,
user
,
descriptor
s
):
'''
Find any StudentModule objects that are needed by any child modules of the
supplied descriptor. Avoids making multiple queries to the database
descriptor: An XModuleDescriptor
depth is the number of levels of descendent modules to load StudentModules for, in addition to
the supplied descriptor. If depth is None, load all descendent StudentModules
supplied descriptor, or caches only the StudentModule objects specifically
for every descriptor in descriptors. Avoids making multiple queries to the
database.
Arguments
user: The user for which to fetch maching StudentModules
descriptors: An array of XModuleDescriptors.
'''
if
user
.
is_authenticated
():
module_ids
=
self
.
_get_module_state_keys
(
descriptor
,
depth
)
module_ids
=
self
.
_get_module_state_keys
(
descriptor
s
)
# This works around a limitation in sqlite3 on the number of parameters
# that can be put into a single query
...
...
@@ -91,27 +93,52 @@ class StudentModuleCache(object):
else
:
self
.
cache
=
[]
def
_get_module_state_keys
(
self
,
descriptor
,
depth
):
'''
Get a list of the state_keys needed for StudentModules
required for this module descriptor
@classmethod
def
cache_for_descriptor_descendents
(
cls
,
user
,
descriptor
,
depth
=
None
,
descriptor_filter
=
lambda
descriptor
:
True
):
"""
descriptor: An XModuleDescriptor
depth is the number of levels of descendent modules to load StudentModules for, in addition to
the supplied descriptor. If depth is None, load all descendent StudentModules
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
should be cached
"""
def
get_child_descriptors
(
descriptor
,
depth
,
descriptor_filter
):
if
descriptor_filter
(
descriptor
):
descriptors
=
[
descriptor
]
else
:
descriptors
=
[]
if
depth
is
None
or
depth
>
0
:
new_depth
=
depth
-
1
if
depth
is
not
None
else
depth
for
child
in
descriptor
.
get_children
():
descriptors
.
extend
(
get_child_descriptors
(
child
,
new_depth
,
descriptor_filter
))
return
descriptors
descriptors
=
get_child_descriptors
(
descriptor
,
depth
,
descriptor_filter
)
return
StudentModuleCache
(
user
,
descriptors
)
def
_get_module_state_keys
(
self
,
descriptors
):
'''
keys
=
[
descriptor
.
location
.
url
()]
shared_state_key
=
getattr
(
descriptor
,
'shared_state_key'
,
None
)
if
shared_state_key
is
not
None
:
keys
.
append
(
shared_state_key
)
if
depth
is
None
or
depth
>
0
:
new_depth
=
depth
-
1
if
depth
is
not
None
else
depth
for
child
in
descriptor
.
get_children
():
keys
.
extend
(
self
.
_get_module_state_keys
(
child
,
new_depth
))
Get a list of the state_keys needed for StudentModules
required for this module descriptor
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
should be cached
'''
keys
=
[]
for
descriptor
in
descriptors
:
keys
.
append
(
descriptor
.
location
.
url
())
shared_state_key
=
getattr
(
descriptor
,
'shared_state_key'
,
None
)
if
shared_state_key
is
not
None
:
keys
.
append
(
shared_state_key
)
return
keys
...
...
lms/djangoapps/courseware/module_render.py
View file @
e14ebaaf
...
...
@@ -50,9 +50,9 @@ def toc_for_course(user, request, course, active_chapter, active_section):
chapters with name 'hidden' are skipped.
'''
student_module_cache
=
StudentModuleCache
(
user
,
course
,
depth
=
2
)
(
course
,
_
,
_
,
_
)
=
get_module
(
user
,
request
,
course
.
location
,
student_module_cache
)
student_module_cache
=
StudentModuleCache
.
cache_for_descriptor_descendents
(
user
,
course
,
depth
=
2
)
course
=
get_module
(
user
,
request
,
course
.
location
,
student_module_cache
)
chapters
=
list
()
for
chapter
in
course
.
get_display_items
():
...
...
@@ -121,25 +121,26 @@ def get_module(user, request, location, student_module_cache, position=None):
- position : extra information from URL for user-specified
position within module
Returns:
- a tuple (xmodule instance, instance_module, shared_module, module category).
instance_module is a StudentModule specific to this module for this student,
or None if this is an anonymous user
shared_module is a StudentModule specific to all modules with the same
'shared_state_key' attribute, or None if the module does not elect to
share state
Returns: xmodule instance
'''
descriptor
=
modulestore
()
.
get_item
(
location
)
instance_module
=
student_module_cache
.
lookup
(
descriptor
.
category
,
descriptor
.
location
.
url
())
shared_state_key
=
getattr
(
descriptor
,
'shared_state_key'
,
None
)
if
shared_state_key
is
not
None
:
shared_module
=
student_module_cache
.
lookup
(
descriptor
.
category
,
shared_state_key
)
#TODO Only check the cache if this module can possibly have state
if
user
.
is_authenticated
():
instance_module
=
student_module_cache
.
lookup
(
descriptor
.
category
,
descriptor
.
location
.
url
())
shared_state_key
=
getattr
(
descriptor
,
'shared_state_key'
,
None
)
if
shared_state_key
is
not
None
:
shared_module
=
student_module_cache
.
lookup
(
descriptor
.
category
,
shared_state_key
)
else
:
shared_module
=
None
else
:
instance_module
=
None
shared_module
=
None
instance_state
=
instance_module
.
state
if
instance_module
is
not
None
else
None
shared_state
=
shared_module
.
state
if
shared_module
is
not
None
else
None
...
...
@@ -163,9 +164,8 @@ def get_module(user, request, location, student_module_cache, position=None):
'default_queuename'
:
xqueue_default_queuename
.
replace
(
' '
,
'_'
)
}
def
_get_module
(
location
):
(
module
,
_
,
_
,
_
)
=
get_module
(
user
,
request
,
location
,
return
get_module
(
user
,
request
,
location
,
student_module_cache
,
position
)
return
module
# TODO (cpennington): When modules are shared between courses, the static
# prefix is going to have to be specific to the module, not the directory
...
...
@@ -198,30 +198,59 @@ def get_module(user, request, location, student_module_cache, position=None):
if
has_staff_access_to_course
(
user
,
module
.
location
.
course
):
module
.
get_html
=
add_histogram
(
module
.
get_html
,
module
)
# If StudentModule for this instance wasn't already in the database,
# and this isn't a guest user, create it.
return
module
def
get_instance_module
(
user
,
module
,
student_module_cache
):
"""
Returns instance_module is a StudentModule specific to this module for this student,
or None if this is an anonymous user
"""
if
user
.
is_authenticated
():
instance_module
=
student_module_cache
.
lookup
(
module
.
category
,
module
.
location
.
url
())
if
not
instance_module
:
instance_module
=
StudentModule
(
student
=
user
,
module_type
=
descriptor
.
category
,
module_type
=
module
.
category
,
module_state_key
=
module
.
id
,
state
=
module
.
get_instance_state
(),
max_grade
=
module
.
max_score
())
instance_module
.
save
()
# Add to cache. The caller and the system context have references
# to it, so the change persists past the return
student_module_cache
.
append
(
instance_module
)
if
not
shared_module
and
shared_state_key
is
not
None
:
shared_module
=
StudentModule
(
student
=
user
,
module_type
=
descriptor
.
category
,
module_state_key
=
shared_state_key
,
state
=
module
.
get_shared_state
())
shared_module
.
save
()
student_module_cache
.
append
(
shared_module
)
return
(
module
,
instance_module
,
shared_module
,
descriptor
.
category
)
return
instance_module
else
:
return
None
def
get_shared_instance_module
(
user
,
module
,
student_module_cache
):
"""
Return shared_module is a StudentModule specific to all modules with the same
'shared_state_key' attribute, or None if the module does not elect to
share state
"""
if
user
.
is_authenticated
():
# To get the shared_state_key, we need to descriptor
descriptor
=
modulestore
()
.
get_item
(
module
.
location
)
shared_state_key
=
getattr
(
module
,
'shared_state_key'
,
None
)
if
shared_state_key
is
not
None
:
shared_module
=
student_module_cache
.
lookup
(
module
.
category
,
shared_state_key
)
if
not
shared_module
:
shared_module
=
StudentModule
(
student
=
user
,
module_type
=
descriptor
.
category
,
module_state_key
=
shared_state_key
,
state
=
module
.
get_shared_state
())
shared_module
.
save
()
student_module_cache
.
append
(
shared_module
)
else
:
shared_module
=
None
return
shared_module
else
:
return
None
@csrf_exempt
...
...
@@ -240,8 +269,10 @@ def xqueue_callback(request, userid, id, dispatch):
# Retrieve target StudentModule
user
=
User
.
objects
.
get
(
id
=
userid
)
student_module_cache
=
StudentModuleCache
(
user
,
modulestore
()
.
get_item
(
id
))
instance
,
instance_module
,
shared_module
,
module_type
=
get_module
(
request
.
user
,
request
,
id
,
student_module_cache
)
student_module_cache
=
StudentModuleCache
.
cache_for_descriptor_descendents
(
user
,
modulestore
()
.
get_item
(
id
))
instance
=
get_module
(
request
.
user
,
request
,
id
,
student_module_cache
)
instance_module
=
get_instance_module
(
request
.
user
,
instance
,
student_module_cache
)
if
instance_module
is
None
:
log
.
debug
(
"Couldn't find module '
%
s' for user '
%
s'"
,
...
...
@@ -285,16 +316,18 @@ def modx_dispatch(request, dispatch=None, id=None):
- id -- the module id. Used to look up the XModule instance
'''
# ''' (fix emacs broken parsing)
# Check for submitted files
p
=
request
.
POST
.
copy
()
if
request
.
FILES
:
for
inputfile_id
in
request
.
FILES
.
keys
():
p
[
inputfile_id
]
=
request
.
FILES
[
inputfile_id
]
student_module_cache
=
StudentModuleCache
(
request
.
user
,
modulestore
()
.
get_item
(
id
))
instance
,
instance_module
,
shared_module
,
module_type
=
get_module
(
request
.
user
,
request
,
id
,
student_module_cache
)
student_module_cache
=
StudentModuleCache
.
cache_for_descriptor_descendents
(
request
.
user
,
modulestore
()
.
get_item
(
id
))
instance
=
get_module
(
request
.
user
,
request
,
id
,
student_module_cache
)
instance_module
=
get_instance_module
(
request
.
user
,
instance
,
student_module_cache
)
shared_module
=
get_shared_instance_module
(
request
.
user
,
instance
,
student_module_cache
)
# Don't track state for anonymous users (who don't have student modules)
if
instance_module
is
not
None
:
oldgrade
=
instance_module
.
grade
...
...
lms/djangoapps/courseware/views.py
View file @
e14ebaaf
...
...
@@ -34,7 +34,6 @@ log = logging.getLogger("mitx.courseware")
template_imports
=
{
'urllib'
:
urllib
}
def
user_groups
(
user
):
if
not
user
.
is_authenticated
():
return
[]
...
...
@@ -45,6 +44,8 @@ def user_groups(user):
# Kill caching on dev machines -- we switch groups a lot
group_names
=
cache
.
get
(
key
)
if
settings
.
DEBUG
:
group_names
=
None
if
group_names
is
None
:
group_names
=
[
u
.
name
for
u
in
UserTestGroup
.
objects
.
filter
(
users
=
user
)]
...
...
@@ -68,18 +69,17 @@ def gradebook(request, course_id):
if
'course_admin'
not
in
user_groups
(
request
.
user
):
raise
Http404
course
=
check_course
(
course_id
)
student_objects
=
User
.
objects
.
all
()[:
100
]
student_info
=
[]
for
student
in
student_objects
:
student_module_cache
=
StudentModuleCache
(
student
,
course
)
course
,
_
,
_
,
_
=
get_module
(
request
.
user
,
request
,
course
.
location
,
student_module_cache
)
#TODO: Only select students who are in the course
for
student
in
student_objects
:
student_info
.
append
({
'username'
:
student
.
username
,
'id'
:
student
.
id
,
'email'
:
student
.
email
,
'grade_
info'
:
grades
.
grade_sheet
(
student
,
course
,
student_module_cach
e
),
'grade_
summary'
:
grades
.
grade
(
student
,
request
,
cours
e
),
'realname'
:
UserProfile
.
objects
.
get
(
user
=
student
)
.
name
})
...
...
@@ -102,18 +102,23 @@ def profile(request, course_id, student_id=None):
user_info
=
UserProfile
.
objects
.
get
(
user
=
student
)
student_module_cache
=
StudentModuleCache
(
request
.
user
,
course
)
course_module
,
_
,
_
,
_
=
get_module
(
request
.
user
,
request
,
course
.
location
,
student_module_cache
)
student_module_cache
=
StudentModuleCache
.
cache_for_descriptor_descendents
(
request
.
user
,
course
)
course_module
=
get_module
(
request
.
user
,
request
,
course
.
location
,
student_module_cache
)
courseware_summary
=
grades
.
progress_summary
(
student
,
course_module
,
course
.
grader
,
student_module_cache
)
grade_summary
=
grades
.
grade
(
request
.
user
,
request
,
course
,
student_module_cache
)
context
=
{
'name'
:
user_info
.
name
,
'username'
:
student
.
username
,
'location'
:
user_info
.
location
,
'language'
:
user_info
.
language
,
'email'
:
student
.
email
,
'course'
:
course
,
'csrf'
:
csrf
(
request
)[
'csrf_token'
]
'csrf'
:
csrf
(
request
)[
'csrf_token'
],
'courseware_summary'
:
courseware_summary
,
'grade_summary'
:
grade_summary
}
context
.
update
(
grades
.
grade_sheet
(
student
,
course_module
,
course
.
grader
,
student_module_cache
)
)
context
.
update
()
return
render_to_response
(
'profile.html'
,
context
)
...
...
@@ -184,11 +189,12 @@ def index(request, course_id, chapter=None, section=None,
if
look_for_module
:
section_descriptor
=
get_section
(
course
,
chapter
,
section
)
if
section_descriptor
is
not
None
:
student_module_cache
=
StudentModuleCache
(
request
.
user
,
student_module_cache
=
StudentModuleCache
.
cache_for_descriptor_descendents
(
request
.
user
,
section_descriptor
)
module
,
_
,
_
,
_
=
get_module
(
request
.
user
,
request
,
section_descriptor
.
location
,
student_module_cache
)
module
=
get_module
(
request
.
user
,
request
,
section_descriptor
.
location
,
student_module_cache
)
context
[
'content'
]
=
module
.
get_html
()
else
:
log
.
warning
(
"Couldn't find a section descriptor for course_id '{0}',"
...
...
lms/templates/gradebook.html
View file @
e14ebaaf
...
...
@@ -8,6 +8,7 @@
</
%
block>
<
%
block
name=
"headextra"
>
<
%
static:css
group=
'course'
/>
<style
type=
"text/css"
>
.grade_a
{
color
:
green
;}
...
...
@@ -19,7 +20,8 @@
</
%
block>
<
%
include
file=
"navigation.html"
args=
"active_page=''"
/>
<
%
include
file=
"course_navigation.html"
args=
"active_page=''"
/>
<section
class=
"container"
>
<div
class=
"gradebook-wrapper"
>
<section
class=
"gradebook-content"
>
...
...
@@ -28,7 +30,7 @@
%if len(students) > 0:
<table>
<
%
templateSummary =
students[0]['grade_
info']['grade_
summary']
templateSummary =
students[0]['grade_summary']
%
>
...
...
@@ -42,15 +44,15 @@
<
%
def
name=
"percent_data(percentage)"
>
<
%
data_class =
"grade_none"
if
percentage
>
.87
:
data_class = "grade_a"
elif percentage > .70
:
data_class = "grade_b"
elif percentage > .6:
data_class = "grade_c"
elif percentage > 0:
data_class = "grade_f"
letter_grade =
'None'
if
percentage
>
0
:
letter_grade = 'F'
for grade in ['A', 'B', 'C']
:
if percentage >= course.grade_cutoffs[grade]:
letter_grade = grade
break
data_class = "grade_" + letter_grade
%>
<td
class=
"${data_class}"
>
${ "{0:.0%}".format( percentage ) }
</td>
</
%
def>
...
...
@@ -58,10 +60,10 @@
%for student in students:
<tr>
<td><a
href=
"/profile/${student['id']}/"
>
${student['username']}
</a></td>
%for section in student['grade_
info']['grade_
summary']['section_breakdown']:
%for section in student['grade_summary']['section_breakdown']:
${percent_data( section['percent'] )}
%endfor
<th>
${percent_data( student['grade_
info']['grade_
summary']['percent'])}
</th>
<th>
${percent_data( student['grade_summary']['percent'])}
</th>
</tr>
%endfor
</table>
...
...
lms/templates/profile.html
View file @
e14ebaaf
...
...
@@ -18,7 +18,7 @@
<script
type=
"text/javascript"
src=
"${static.url('js/vendor/flot/jquery.flot.stack.js')}"
></script>
<script
type=
"text/javascript"
src=
"${static.url('js/vendor/flot/jquery.flot.symbol.js')}"
></script>
<script>
$
{
profile_graphs
.
body
(
grade_summary
,
"grade-detail-graph"
)}
$
{
profile_graphs
.
body
(
grade_summary
,
course
.
grade_cutoffs
,
"grade-detail-graph"
)}
</script>
<script>
...
...
lms/templates/profile_graphs.js
View file @
e14ebaaf
<%
page
args
=
"grade_summary, graph_div_id, **kwargs"
/>
<%
page
args
=
"grade_summary, gra
de_cutoffs, gra
ph_div_id, **kwargs"
/>
<%!
import
json
import
math
%>
$
(
function
()
{
...
...
@@ -89,8 +90,16 @@ $(function () {
ticks
+=
[
[
overviewBarX
,
"Total"
]
]
tickIndex
+=
1
+
sectionSpacer
totalScore
=
grade_summary
[
'percent'
]
totalScore
=
math
.
floor
(
grade_summary
[
'percent'
]
*
100
)
/
100
#
We
floor
it
to
the
nearest
percent
,
80.9
won
't show up like a 90 (an A)
detail_tooltips['
Dropped
Scores
'] = dropped_score_tooltips
## ----------------------------- Grade cutoffs ------------------------- ##
grade_cutoff_ticks = [ [1, "100%"], [0, "0%"] ]
for grade in ['
A
', '
B
', '
C
']:
percent = grade_cutoffs[grade]
grade_cutoff_ticks.append( [ percent, "{0} {1:.0%}".format(grade, percent) ] )
%>
var series = ${ json.dumps( series ) };
...
...
@@ -98,6 +107,7 @@ $(function () {
var bottomTicks = ${ json.dumps(bottomTicks) };
var detail_tooltips = ${ json.dumps(detail_tooltips) };
var droppedScores = ${ json.dumps(droppedScores) };
var grade_cutoff_ticks = ${ json.dumps(grade_cutoff_ticks) }
//Alwasy be sure that one series has the xaxis set to 2, or the second xaxis labels won'
t
show
up
series
.
push
(
{
label
:
'Dropped Scores'
,
data
:
droppedScores
,
points
:
{
symbol
:
"cross"
,
show
:
true
,
radius
:
3
},
bars
:
{
show
:
false
},
color
:
"#333"
}
);
...
...
@@ -107,10 +117,10 @@ $(function () {
lines
:
{
show
:
false
,
steps
:
false
},
bars
:
{
show
:
true
,
barWidth
:
0.8
,
align
:
'center'
,
lineWidth
:
0
,
fill
:
.
8
},},
xaxis
:
{
tickLength
:
0
,
min
:
0.0
,
max
:
$
{
tickIndex
-
sectionSpacer
},
ticks
:
ticks
,
labelAngle
:
90
},
yaxis
:
{
ticks
:
[[
1
,
"100%"
],
[
0.87
,
"A 87%"
],
[
0.7
,
"B 70%"
],
[
0.6
,
"C 60%"
],
[
0
,
"0%"
]]
,
min
:
0.0
,
max
:
1.0
,
labelWidth
:
50
},
yaxis
:
{
ticks
:
grade_cutoff_ticks
,
min
:
0.0
,
max
:
1.0
,
labelWidth
:
50
},
grid
:
{
hoverable
:
true
,
clickable
:
true
,
borderWidth
:
1
,
markings
:
[
{
yaxis
:
{
from
:
0.87
,
to
:
1
},
color
:
"#ddd"
},
{
yaxis
:
{
from
:
0.7
,
to
:
0.87
},
color
:
"#e9e9e9"
},
{
yaxis
:
{
from
:
0.6
,
to
:
0.7
},
color
:
"#f3f3f3"
},
]
},
markings
:
[
{
yaxis
:
{
from
:
$
{
grade_cutoffs
[
'A'
]},
to
:
1
},
color
:
"#ddd"
},
{
yaxis
:
{
from
:
$
{
grade_cutoffs
[
'B'
]},
to
:
$
{
grade_cutoffs
[
'A'
]}
},
color
:
"#e9e9e9"
},
{
yaxis
:
{
from
:
$
{
grade_cutoffs
[
'C'
]},
to
:
$
{
grade_cutoffs
[
'B'
]}
},
color
:
"#f3f3f3"
},
]
},
legend
:
{
show
:
false
},
};
...
...
lms/urls.py
View file @
e14ebaaf
...
...
@@ -107,7 +107,6 @@ if settings.COURSEWARE_ENABLED:
# TODO: These views need to be updated before they work
# url(r'^calculate$', 'util.views.calculate'),
# url(r'^gradebook$', 'courseware.views.gradebook'),
# TODO: We should probably remove the circuit package. I believe it was only used in the old way of saving wiki circuits for the wiki
# url(r'^edit_circuit/(?P<circuit>[^/]*)$', 'circuit.views.edit_circuit'),
# url(r'^save_circuit/(?P<circuit>[^/]*)$', 'circuit.views.save_circuit'),
...
...
@@ -119,7 +118,7 @@ if settings.COURSEWARE_ENABLED:
#About the course
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/about$'
,
'courseware.views.course_about'
,
name
=
"about_course"
),
#Inside the course
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/info$'
,
'courseware.views.course_info'
,
name
=
"info"
),
...
...
@@ -137,6 +136,10 @@ if settings.COURSEWARE_ENABLED:
'courseware.views.profile'
,
name
=
"profile"
),
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile/(?P<student_id>[^/]*)/$'
,
'courseware.views.profile'
),
# For the instructor
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/gradebook$'
,
'courseware.views.gradebook'
),
)
# Multicourse wiki
...
...
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