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
993c24b7
Commit
993c24b7
authored
Jan 08, 2013
by
Calen Pennington
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
WIP: Model data caching work
parent
507d2021
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
170 additions
and
234 deletions
+170
-234
common/lib/xmodule/xmodule/course_module.py
+1
-1
lms/djangoapps/courseware/courses.py
+30
-8
lms/djangoapps/courseware/grades.py
+57
-41
lms/djangoapps/courseware/management/commands/check_course.py
+2
-2
lms/djangoapps/courseware/model_data.py
+0
-0
lms/djangoapps/courseware/models.py
+0
-103
lms/djangoapps/courseware/module_render.py
+22
-27
lms/djangoapps/courseware/tests/test_model_data.py
+36
-27
lms/djangoapps/courseware/tests/tests.py
+5
-5
lms/djangoapps/courseware/views.py
+13
-16
lms/templates/courseware/info.html
+4
-4
No files found.
common/lib/xmodule/xmodule/course_module.py
View file @
993c24b7
...
@@ -358,7 +358,7 @@ class CourseDescriptor(SequenceDescriptor):
...
@@ -358,7 +358,7 @@ class CourseDescriptor(SequenceDescriptor):
all_descriptors - This contains a list of all xmodules that can
all_descriptors - This contains a list of all xmodules that can
effect grading a student. This is used to efficiently fetch
effect grading a student. This is used to efficiently fetch
all the xmodule state for a
StudentModule
Cache without walking
all the xmodule state for a
ModelData
Cache without walking
the descriptor tree again.
the descriptor tree again.
...
...
lms/djangoapps/courseware/courses.py
View file @
993c24b7
...
@@ -19,10 +19,10 @@ from xmodule.contentstore.content import StaticContent
...
@@ -19,10 +19,10 @@ from xmodule.contentstore.content import StaticContent
from
xmodule.modulestore.xml
import
XMLModuleStore
from
xmodule.modulestore.xml
import
XMLModuleStore
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
xmodule.x_module
import
XModule
from
xmodule.x_module
import
XModule
from
courseware.model_data
import
ModelDataCache
from
static_replace
import
replace_urls
,
try_staticfiles_lookup
from
static_replace
import
replace_urls
,
try_staticfiles_lookup
from
courseware.access
import
has_access
from
courseware.access
import
has_access
import
branding
import
branding
from
courseware.models
import
StudentModuleCache
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
log
=
logging
.
getLogger
(
__name__
)
log
=
logging
.
getLogger
(
__name__
)
...
@@ -148,12 +148,23 @@ def get_course_about_section(course, section_key):
...
@@ -148,12 +148,23 @@ def get_course_about_section(course, section_key):
request
=
get_request_for_thread
()
request
=
get_request_for_thread
()
loc
=
course
.
location
.
_replace
(
category
=
'about'
,
name
=
section_key
)
loc
=
course
.
location
.
_replace
(
category
=
'about'
,
name
=
section_key
)
course_module
=
get_module
(
request
.
user
,
request
,
loc
,
None
,
course
.
id
,
not_found_ok
=
True
,
wrap_xmodule_display
=
True
)
# Use an empty cache
model_data_cache
=
ModelDataCache
([],
course
.
id
,
request
.
user
)
about_module
=
get_module
(
request
.
user
,
request
,
loc
,
model_data_cache
,
course
.
id
,
not_found_ok
=
True
,
wrap_xmodule_display
=
True
)
html
=
''
html
=
''
if
course
_module
is
not
None
:
if
about
_module
is
not
None
:
html
=
course
_module
.
get_html
()
html
=
about
_module
.
get_html
()
return
html
return
html
...
@@ -174,7 +185,7 @@ def get_course_about_section(course, section_key):
...
@@ -174,7 +185,7 @@ def get_course_about_section(course, section_key):
def
get_course_info_section
(
request
,
c
ache
,
c
ourse
,
section_key
):
def
get_course_info_section
(
request
,
course
,
section_key
):
"""
"""
This returns the snippet of html to be rendered on the course info page,
This returns the snippet of html to be rendered on the course info page,
given the key for the section.
given the key for the section.
...
@@ -188,11 +199,22 @@ def get_course_info_section(request, cache, course, section_key):
...
@@ -188,11 +199,22 @@ def get_course_info_section(request, cache, course, section_key):
loc
=
Location
(
course
.
location
.
tag
,
course
.
location
.
org
,
course
.
location
.
course
,
'course_info'
,
section_key
)
loc
=
Location
(
course
.
location
.
tag
,
course
.
location
.
org
,
course
.
location
.
course
,
'course_info'
,
section_key
)
course_module
=
get_module
(
request
.
user
,
request
,
loc
,
cache
,
course
.
id
,
wrap_xmodule_display
=
True
)
# Use an empty cache
model_data_cache
=
ModelDataCache
([],
course
.
id
,
request
.
user
)
info_module
=
get_module
(
request
.
user
,
request
,
loc
,
model_data_cache
,
course
.
id
,
wrap_xmodule_display
=
True
)
html
=
''
html
=
''
if
course
_module
is
not
None
:
if
info
_module
is
not
None
:
html
=
course
_module
.
get_html
()
html
=
info
_module
.
get_html
()
return
html
return
html
...
...
lms/djangoapps/courseware/grades.py
View file @
993c24b7
This diff is collapsed.
Click to expand it.
lms/djangoapps/courseware/management/commands/check_course.py
View file @
993c24b7
...
@@ -13,7 +13,7 @@ import xmodule
...
@@ -13,7 +13,7 @@ import xmodule
import
mitxmako.middleware
as
middleware
import
mitxmako.middleware
as
middleware
middleware
.
MakoMiddleware
()
middleware
.
MakoMiddleware
()
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
courseware.model
s
import
StudentModule
Cache
from
courseware.model
_data
import
ModelData
Cache
from
courseware.module_render
import
get_module
from
courseware.module_render
import
get_module
...
@@ -83,7 +83,7 @@ class Command(BaseCommand):
...
@@ -83,7 +83,7 @@ class Command(BaseCommand):
# TODO (cpennington): Get coursename in a legitimate way
# TODO (cpennington): Get coursename in a legitimate way
course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012'
course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012'
student_module_cache =
StudentModule
Cache.cache_for_descriptor_descendents(
student_module_cache =
ModelData
Cache.cache_for_descriptor_descendents(
course_id,
course_id,
sample_user, modulestore().get_item(course_location))
sample_user, modulestore().get_item(course_location))
course = get_module(sample_user, None, course_location, student_module_cache)
course = get_module(sample_user, None, course_location, student_module_cache)
...
...
lms/djangoapps/courseware/model_data.py
View file @
993c24b7
This diff is collapsed.
Click to expand it.
lms/djangoapps/courseware/models.py
View file @
993c24b7
...
@@ -198,106 +198,3 @@ class XModuleStudentInfoField(models.Model):
...
@@ -198,106 +198,3 @@ class XModuleStudentInfoField(models.Model):
def
__unicode__
(
self
):
def
__unicode__
(
self
):
return
unicode
(
repr
(
self
))
return
unicode
(
repr
(
self
))
# TODO (cpennington): Remove these once the LMS switches to using XModuleDescriptors
class
StudentModuleCache
(
object
):
"""
A cache of StudentModules for a specific student
"""
def
__init__
(
self
,
course_id
,
user
,
descriptors
,
select_for_update
=
False
):
'''
Find any StudentModule objects that are needed by any descriptor
in descriptors. Avoids making multiple queries to the database.
Note: Only modules that have store_state = True or have shared
state will have a StudentModule.
Arguments
user: The user for which to fetch maching StudentModules
descriptors: An array of XModuleDescriptors.
select_for_update: Flag indicating whether the rows should be locked until end of transaction
'''
if
user
.
is_authenticated
():
module_ids
=
self
.
_get_module_state_keys
(
descriptors
)
# This works around a limitation in sqlite3 on the number of parameters
# that can be put into a single query
self
.
cache
=
[]
chunk_size
=
500
for
id_chunk
in
[
module_ids
[
i
:
i
+
chunk_size
]
for
i
in
xrange
(
0
,
len
(
module_ids
),
chunk_size
)]:
if
select_for_update
:
self
.
cache
.
extend
(
StudentModule
.
objects
.
select_for_update
()
.
filter
(
course_id
=
course_id
,
student
=
user
,
module_state_key__in
=
id_chunk
)
)
else
:
self
.
cache
.
extend
(
StudentModule
.
objects
.
filter
(
course_id
=
course_id
,
student
=
user
,
module_state_key__in
=
id_chunk
)
)
else
:
self
.
cache
=
[]
@classmethod
def
cache_for_descriptor_descendents
(
cls
,
course_id
,
user
,
descriptor
,
depth
=
None
,
descriptor_filter
=
lambda
descriptor
:
True
,
select_for_update
=
False
):
"""
course_id: the course in the context of which we want StudentModules.
user: the django user for whom to load modules.
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
select_for_update: Flag indicating whether the rows should be locked until end of transaction
"""
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
(
course_id
,
user
,
descriptors
,
select_for_update
)
def
_get_module_state_keys
(
self
,
descriptors
):
'''
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
'''
return
[
descriptor
.
location
.
url
()
for
descriptor
in
descriptors
if
descriptor
.
stores_state
]
def
lookup
(
self
,
course_id
,
module_type
,
module_state_key
):
'''
Look for a student module with the given course_id, type, and id in the cache.
cache -- list of student modules
returns first found object, or None
'''
for
o
in
self
.
cache
:
if
(
o
.
course_id
==
course_id
and
o
.
module_type
==
module_type
and
o
.
module_state_key
==
module_state_key
):
return
o
return
None
def
append
(
self
,
student_module
):
self
.
cache
.
append
(
student_module
)
lms/djangoapps/courseware/module_render.py
View file @
993c24b7
...
@@ -17,7 +17,7 @@ from capa.xqueue_interface import XQueueInterface
...
@@ -17,7 +17,7 @@ from capa.xqueue_interface import XQueueInterface
from
capa.chem
import
chemcalc
from
capa.chem
import
chemcalc
from
courseware.access
import
has_access
from
courseware.access
import
has_access
from
mitxmako.shortcuts
import
render_to_string
from
mitxmako.shortcuts
import
render_to_string
from
models
import
StudentModule
,
StudentModuleCache
from
models
import
StudentModule
from
psychometrics.psychoanalyze
import
make_psychometrics_data_update_handler
from
psychometrics.psychoanalyze
import
make_psychometrics_data_update_handler
from
static_replace
import
replace_urls
from
static_replace
import
replace_urls
from
xmodule.errortracker
import
exc_info_to_str
from
xmodule.errortracker
import
exc_info_to_str
...
@@ -28,7 +28,7 @@ from xmodule.x_module import ModuleSystem
...
@@ -28,7 +28,7 @@ from xmodule.x_module import ModuleSystem
from
xmodule.error_module
import
ErrorDescriptor
,
NonStaffErrorDescriptor
from
xmodule.error_module
import
ErrorDescriptor
,
NonStaffErrorDescriptor
from
xblock.runtime
import
DbModel
from
xblock.runtime
import
DbModel
from
xmodule_modifiers
import
replace_course_urls
,
replace_static_urls
,
add_histogram
,
wrap_xmodule
from
xmodule_modifiers
import
replace_course_urls
,
replace_static_urls
,
add_histogram
,
wrap_xmodule
from
.model_data
import
LmsKeyValueStore
,
LmsUsage
from
.model_data
import
LmsKeyValueStore
,
LmsUsage
,
ModelDataCache
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
statsd
import
statsd
from
statsd
import
statsd
...
@@ -60,7 +60,7 @@ def make_track_function(request):
...
@@ -60,7 +60,7 @@ def make_track_function(request):
return
f
return
f
def
toc_for_course
(
user
,
request
,
course
,
active_chapter
,
active_section
):
def
toc_for_course
(
user
,
request
,
course
,
active_chapter
,
active_section
,
model_data_cache
):
'''
'''
Create a table of contents from the module store
Create a table of contents from the module store
...
@@ -80,11 +80,11 @@ def toc_for_course(user, request, course, active_chapter, active_section):
...
@@ -80,11 +80,11 @@ def toc_for_course(user, request, course, active_chapter, active_section):
NOTE: assumes that if we got this far, user has access to course. Returns
NOTE: assumes that if we got this far, user has access to course. Returns
None if this is not the case.
None if this is not the case.
model_data_cache must include data from the course module and 2 levels of its descendents
'''
'''
student_module_cache
=
StudentModuleCache
.
cache_for_descriptor_descendents
(
course_module
=
get_module
(
user
,
request
,
course
.
location
,
model_data_cache
,
course
.
id
)
course
.
id
,
user
,
course
,
depth
=
2
)
course_module
=
get_module
(
user
,
request
,
course
.
location
,
student_module_cache
,
course
.
id
)
if
course_module
is
None
:
if
course_module
is
None
:
return
None
return
None
...
@@ -115,7 +115,7 @@ def toc_for_course(user, request, course, active_chapter, active_section):
...
@@ -115,7 +115,7 @@ def toc_for_course(user, request, course, active_chapter, active_section):
return
chapters
return
chapters
def
get_module
(
user
,
request
,
location
,
student_module
_cache
,
course_id
,
def
get_module
(
user
,
request
,
location
,
model_data
_cache
,
course_id
,
position
=
None
,
not_found_ok
=
False
,
wrap_xmodule_display
=
True
,
position
=
None
,
not_found_ok
=
False
,
wrap_xmodule_display
=
True
,
grade_bucket_type
=
None
):
grade_bucket_type
=
None
):
"""
"""
...
@@ -128,7 +128,7 @@ def get_module(user, request, location, student_module_cache, course_id,
...
@@ -128,7 +128,7 @@ def get_module(user, request, location, student_module_cache, course_id,
- request : current django HTTPrequest. Note: request.user isn't used for anything--all auth
- request : current django HTTPrequest. Note: request.user isn't used for anything--all auth
and such works based on user.
and such works based on user.
- location : A Location-like object identifying the module to load
- location : A Location-like object identifying the module to load
-
student_module_cache : a StudentModule
Cache
-
model_data_cache : a ModelData
Cache
- course_id : the course_id in the context of which to load module
- course_id : the course_id in the context of which to load module
- position : extra information from URL for user-specified
- position : extra information from URL for user-specified
position within module
position within module
...
@@ -138,7 +138,7 @@ def get_module(user, request, location, student_module_cache, course_id,
...
@@ -138,7 +138,7 @@ def get_module(user, request, location, student_module_cache, course_id,
if possible. If not possible, return None.
if possible. If not possible, return None.
"""
"""
try
:
try
:
return
_get_module
(
user
,
request
,
location
,
student_module
_cache
,
course_id
,
position
,
wrap_xmodule_display
)
return
_get_module
(
user
,
request
,
location
,
model_data
_cache
,
course_id
,
position
,
wrap_xmodule_display
)
except
ItemNotFoundError
:
except
ItemNotFoundError
:
if
not
not_found_ok
:
if
not
not_found_ok
:
log
.
exception
(
"Error in get_module"
)
log
.
exception
(
"Error in get_module"
)
...
@@ -149,7 +149,7 @@ def get_module(user, request, location, student_module_cache, course_id,
...
@@ -149,7 +149,7 @@ def get_module(user, request, location, student_module_cache, course_id,
return
None
return
None
def
_get_module
(
user
,
request
,
location
,
student_module
_cache
,
course_id
,
def
_get_module
(
user
,
request
,
location
,
model_data
_cache
,
course_id
,
position
=
None
,
wrap_xmodule_display
=
True
,
grade_bucket_type
=
None
):
position
=
None
,
wrap_xmodule_display
=
True
,
grade_bucket_type
=
None
):
"""
"""
Actually implement get_module. See docstring there for details.
Actually implement get_module. See docstring there for details.
...
@@ -204,11 +204,11 @@ def _get_module(user, request, location, student_module_cache, course_id,
...
@@ -204,11 +204,11 @@ def _get_module(user, request, location, student_module_cache, course_id,
Delegate to get_module. It does an access check, so may return None
Delegate to get_module. It does an access check, so may return None
"""
"""
return
get_module
(
user
,
request
,
location
,
return
get_module
(
user
,
request
,
location
,
student_module
_cache
,
course_id
,
position
)
model_data
_cache
,
course_id
,
position
)
def
xblock_model_data
(
descriptor
):
def
xblock_model_data
(
descriptor
):
return
DbModel
(
return
DbModel
(
LmsKeyValueStore
(
course_id
,
user
,
descriptor
.
_model_data
,
student_module
_cache
),
LmsKeyValueStore
(
descriptor
.
_model_data
,
model_data
_cache
),
descriptor
.
module_class
,
descriptor
.
module_class
,
user
.
id
,
user
.
id
,
LmsUsage
(
location
,
location
)
LmsUsage
(
location
,
location
)
...
@@ -218,18 +218,13 @@ def _get_module(user, request, location, student_module_cache, course_id,
...
@@ -218,18 +218,13 @@ def _get_module(user, request, location, student_module_cache, course_id,
if
event
.
get
(
'event_name'
)
!=
'grade'
:
if
event
.
get
(
'event_name'
)
!=
'grade'
:
return
return
student_module
=
student_module_cache
.
lookup
(
student_module
,
created
=
StudentModule
.
objects
.
get_or_create
(
course_id
,
descriptor
.
location
.
category
,
descriptor
.
location
.
url
()
course_id
=
course_id
,
student
=
user
,
module_type
=
descriptor
.
location
.
category
,
module_state_key
=
descriptor
.
location
.
url
(),
defaults
=
{
'state'
:
'{}'
},
)
)
if
student_module
is
None
:
student_module
=
StudentModule
(
course_id
=
course_id
,
student
=
user
,
module_type
=
descriptor
.
location
.
category
,
module_state_key
=
descriptor
.
location
.
url
(),
state
=
json
.
dumps
({})
)
student_module_cache
.
append
(
student_module
)
student_module
.
grade
=
event
.
get
(
'value'
)
student_module
.
grade
=
event
.
get
(
'value'
)
student_module
.
max_grade
=
event
.
get
(
'max_value'
)
student_module
.
max_grade
=
event
.
get
(
'max_value'
)
student_module
.
save
()
student_module
.
save
()
...
@@ -335,9 +330,9 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
...
@@ -335,9 +330,9 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
# Retrieve target StudentModule
# Retrieve target StudentModule
user
=
User
.
objects
.
get
(
id
=
userid
)
user
=
User
.
objects
.
get
(
id
=
userid
)
student_module_cache
=
StudentModule
Cache
.
cache_for_descriptor_descendents
(
course_id
,
model_data_cache
=
ModelData
Cache
.
cache_for_descriptor_descendents
(
course_id
,
user
,
modulestore
()
.
get_instance
(
course_id
,
id
),
depth
=
0
,
select_for_update
=
True
)
user
,
modulestore
()
.
get_instance
(
course_id
,
id
),
depth
=
0
,
select_for_update
=
True
)
instance
=
get_module
(
user
,
request
,
id
,
student_module
_cache
,
course_id
,
grade_bucket_type
=
'xqueue'
)
instance
=
get_module
(
user
,
request
,
id
,
model_data
_cache
,
course_id
,
grade_bucket_type
=
'xqueue'
)
if
instance
is
None
:
if
instance
is
None
:
log
.
debug
(
"No module {0} for user {1}--access denied?"
.
format
(
id
,
user
))
log
.
debug
(
"No module {0} for user {1}--access denied?"
.
format
(
id
,
user
))
raise
Http404
raise
Http404
...
@@ -394,10 +389,10 @@ def modx_dispatch(request, dispatch, location, course_id):
...
@@ -394,10 +389,10 @@ def modx_dispatch(request, dispatch, location, course_id):
return
HttpResponse
(
json
.
dumps
({
'success'
:
file_too_big_msg
}))
return
HttpResponse
(
json
.
dumps
({
'success'
:
file_too_big_msg
}))
p
[
fileinput_id
]
=
inputfiles
p
[
fileinput_id
]
=
inputfiles
student_module_cache
=
StudentModule
Cache
.
cache_for_descriptor_descendents
(
course_id
,
model_data_cache
=
ModelData
Cache
.
cache_for_descriptor_descendents
(
course_id
,
request
.
user
,
modulestore
()
.
get_instance
(
course_id
,
location
))
request
.
user
,
modulestore
()
.
get_instance
(
course_id
,
location
))
instance
=
get_module
(
request
.
user
,
request
,
location
,
student_module
_cache
,
course_id
,
grade_bucket_type
=
'ajax'
)
instance
=
get_module
(
request
.
user
,
request
,
location
,
model_data
_cache
,
course_id
,
grade_bucket_type
=
'ajax'
)
if
instance
is
None
:
if
instance
is
None
:
# Either permissions just changed, or someone is trying to be clever
# Either permissions just changed, or someone is trying to be clever
# and load something they shouldn't have access to.
# and load something they shouldn't have access to.
...
...
lms/djangoapps/courseware/tests/test_model_data.py
View file @
993c24b7
...
@@ -5,18 +5,26 @@ from django.contrib.auth.models import User
...
@@ -5,18 +5,26 @@ from django.contrib.auth.models import User
from
functools
import
partial
from
functools
import
partial
from
courseware.model_data
import
LmsKeyValueStore
,
InvalidWriteError
,
InvalidScopeError
from
courseware.model_data
import
LmsKeyValueStore
,
InvalidWriteError
,
InvalidScopeError
,
ModelDataCache
from
courseware.models
import
StudentModule
,
XModuleContentField
,
XModuleSettingsField
,
XModuleStudentInfoField
,
XModuleStudentPrefsField
,
StudentModuleCache
from
courseware.models
import
StudentModule
,
XModuleContentField
,
XModuleSettingsField
,
XModuleStudentInfoField
,
XModuleStudentPrefsField
from
xblock.core
import
Scope
,
BlockScope
from
xblock.core
import
Scope
,
BlockScope
from
xmodule.modulestore
import
Location
from
xmodule.modulestore
import
Location
from
django.test
import
TestCase
from
django.test
import
TestCase
def
mock_descriptor
():
def
mock_field
(
scope
,
name
):
field
=
Mock
()
field
.
scope
=
scope
field
.
name
=
name
return
field
def
mock_descriptor
(
fields
=
[],
lms_fields
=
[]):
descriptor
=
Mock
()
descriptor
=
Mock
()
descriptor
.
stores_state
=
True
descriptor
.
stores_state
=
True
descriptor
.
location
=
location
(
'def_id'
)
descriptor
.
location
=
location
(
'def_id'
)
descriptor
.
module_class
.
fields
=
fields
descriptor
.
module_class
.
lms
.
fields
=
lms_fields
return
descriptor
return
descriptor
location
=
partial
(
Location
,
'i4x'
,
'edX'
,
'test_course'
,
'problem'
)
location
=
partial
(
Location
,
'i4x'
,
'edX'
,
'test_course'
,
'problem'
)
...
@@ -85,7 +93,7 @@ class TestDescriptorFallback(TestCase):
...
@@ -85,7 +93,7 @@ class TestDescriptorFallback(TestCase):
'field_a'
:
'content'
,
'field_a'
:
'content'
,
'field_b'
:
'settings'
,
'field_b'
:
'settings'
,
}
}
self
.
kvs
=
LmsKeyValueStore
(
course_id
,
UserFactory
.
build
(),
self
.
desc_md
,
None
)
self
.
kvs
=
LmsKeyValueStore
(
self
.
desc_md
,
None
)
def
test_get_from_descriptor
(
self
):
def
test_get_from_descriptor
(
self
):
self
.
assertEquals
(
'content'
,
self
.
kvs
.
get
(
content_key
(
'field_a'
)))
self
.
assertEquals
(
'content'
,
self
.
kvs
.
get
(
content_key
(
'field_a'
)))
...
@@ -103,13 +111,11 @@ class TestDescriptorFallback(TestCase):
...
@@ -103,13 +111,11 @@ class TestDescriptorFallback(TestCase):
self
.
assertEquals
(
'settings'
,
self
.
desc_md
[
'field_b'
])
self
.
assertEquals
(
'settings'
,
self
.
desc_md
[
'field_b'
])
class
TestStudentStateFields
(
TestCase
):
pass
class
TestInvalidScopes
(
TestCase
):
class
TestInvalidScopes
(
TestCase
):
def
setUp
(
self
):
def
setUp
(
self
):
self
.
desc_md
=
{}
self
.
desc_md
=
{}
self
.
kvs
=
LmsKeyValueStore
(
course_id
,
UserFactory
.
build
(),
self
.
desc_md
,
None
)
self
.
mdc
=
ModelDataCache
([
mock_descriptor
([
mock_field
(
Scope
.
student_state
,
'a_field'
)])],
course_id
,
self
.
user
)
self
.
kvs
=
LmsKeyValueStore
(
self
.
desc_md
,
self
.
mdc
)
def
test_invalid_scopes
(
self
):
def
test_invalid_scopes
(
self
):
for
scope
in
(
Scope
(
student
=
True
,
block
=
BlockScope
.
DEFINITION
),
for
scope
in
(
Scope
(
student
=
True
,
block
=
BlockScope
.
DEFINITION
),
...
@@ -123,12 +129,10 @@ class TestInvalidScopes(TestCase):
...
@@ -123,12 +129,10 @@ class TestInvalidScopes(TestCase):
class
TestStudentModuleStorage
(
TestCase
):
class
TestStudentModuleStorage
(
TestCase
):
def
setUp
(
self
):
def
setUp
(
self
):
student_module
=
StudentModuleFactory
.
create
(
state
=
json
.
dumps
({
'a_field'
:
'a_value'
}))
self
.
user
=
student_module
.
student
self
.
desc_md
=
{}
self
.
desc_md
=
{}
self
.
smc
=
StudentModuleCache
(
course_id
,
self
.
user
,
[
mock_descriptor
()]
)
self
.
mdc
=
Mock
(
)
self
.
kvs
=
LmsKeyValueStore
(
course_id
,
self
.
user
,
self
.
desc_md
,
self
.
smc
)
self
.
mdc
.
find
.
return_value
.
state
=
json
.
dumps
({
'a_field'
:
'a_value'
}
)
self
.
kvs
=
LmsKeyValueStore
(
self
.
desc_md
,
self
.
mdc
)
def
test_get_existing_field
(
self
):
def
test_get_existing_field
(
self
):
"Test that getting an existing field in an existing StudentModule works"
"Test that getting an existing field in an existing StudentModule works"
...
@@ -154,7 +158,7 @@ class TestStudentModuleStorage(TestCase):
...
@@ -154,7 +158,7 @@ class TestStudentModuleStorage(TestCase):
"Test that deleting an existing field removes it from the StudentModule"
"Test that deleting an existing field removes it from the StudentModule"
self
.
kvs
.
delete
(
student_state_key
(
'a_field'
))
self
.
kvs
.
delete
(
student_state_key
(
'a_field'
))
self
.
assertEquals
(
1
,
StudentModule
.
objects
.
all
()
.
count
())
self
.
assertEquals
(
1
,
StudentModule
.
objects
.
all
()
.
count
())
self
.
assertEquals
({},
json
.
loads
(
StudentModule
.
objects
.
all
()[
0
]
.
state
)
)
self
.
assertEquals
({},
self
.
mdc
.
find
.
return_value
.
state
)
def
test_delete_missing_field
(
self
):
def
test_delete_missing_field
(
self
):
"Test that deleting a missing field from an existing StudentModule raises a KeyError"
"Test that deleting a missing field from an existing StudentModule raises a KeyError"
...
@@ -167,8 +171,8 @@ class TestMissingStudentModule(TestCase):
...
@@ -167,8 +171,8 @@ class TestMissingStudentModule(TestCase):
def
setUp
(
self
):
def
setUp
(
self
):
self
.
user
=
UserFactory
.
create
()
self
.
user
=
UserFactory
.
create
()
self
.
desc_md
=
{}
self
.
desc_md
=
{}
self
.
smc
=
StudentModuleCache
(
course_id
,
self
.
user
,
[
mock_descriptor
()]
)
self
.
mdc
=
ModelDataCache
([
mock_descriptor
()],
course_id
,
self
.
user
)
self
.
kvs
=
LmsKeyValueStore
(
course_id
,
self
.
user
,
self
.
desc_md
,
self
.
sm
c
)
self
.
kvs
=
LmsKeyValueStore
(
self
.
desc_md
,
self
.
md
c
)
def
test_get_field_from_missing_student_module
(
self
):
def
test_get_field_from_missing_student_module
(
self
):
"Test that getting a field from a missing StudentModule raises a KeyError"
"Test that getting a field from a missing StudentModule raises a KeyError"
...
@@ -176,12 +180,12 @@ class TestMissingStudentModule(TestCase):
...
@@ -176,12 +180,12 @@ class TestMissingStudentModule(TestCase):
def
test_set_field_in_missing_student_module
(
self
):
def
test_set_field_in_missing_student_module
(
self
):
"Test that setting a field in a missing StudentModule creates the student module"
"Test that setting a field in a missing StudentModule creates the student module"
self
.
assertEquals
(
0
,
len
(
self
.
sm
c
.
cache
))
self
.
assertEquals
(
0
,
len
(
self
.
md
c
.
cache
))
self
.
assertEquals
(
0
,
StudentModule
.
objects
.
all
()
.
count
())
self
.
assertEquals
(
0
,
StudentModule
.
objects
.
all
()
.
count
())
self
.
kvs
.
set
(
student_state_key
(
'a_field'
),
'a_value'
)
self
.
kvs
.
set
(
student_state_key
(
'a_field'
),
'a_value'
)
self
.
assertEquals
(
1
,
len
(
self
.
sm
c
.
cache
))
self
.
assertEquals
(
1
,
len
(
self
.
md
c
.
cache
))
self
.
assertEquals
(
1
,
StudentModule
.
objects
.
all
()
.
count
())
self
.
assertEquals
(
1
,
StudentModule
.
objects
.
all
()
.
count
())
student_module
=
StudentModule
.
objects
.
all
()[
0
]
student_module
=
StudentModule
.
objects
.
all
()[
0
]
...
@@ -201,8 +205,8 @@ class TestSettingsStorage(TestCase):
...
@@ -201,8 +205,8 @@ class TestSettingsStorage(TestCase):
settings
=
SettingsFactory
.
create
()
settings
=
SettingsFactory
.
create
()
self
.
user
=
UserFactory
.
create
()
self
.
user
=
UserFactory
.
create
()
self
.
desc_md
=
{}
self
.
desc_md
=
{}
self
.
smc
=
StudentModuleCache
(
course_id
,
self
.
user
,
[]
)
self
.
mdc
=
ModelDataCache
([
mock_descriptor
()],
course_id
,
self
.
user
)
self
.
kvs
=
LmsKeyValueStore
(
course_id
,
self
.
user
,
self
.
desc_md
,
self
.
sm
c
)
self
.
kvs
=
LmsKeyValueStore
(
self
.
desc_md
,
self
.
md
c
)
def
test_get_existing_field
(
self
):
def
test_get_existing_field
(
self
):
"Test that getting an existing field in an existing SettingsField works"
"Test that getting an existing field in an existing SettingsField works"
...
@@ -242,8 +246,8 @@ class TestContentStorage(TestCase):
...
@@ -242,8 +246,8 @@ class TestContentStorage(TestCase):
content
=
ContentFactory
.
create
()
content
=
ContentFactory
.
create
()
self
.
user
=
UserFactory
.
create
()
self
.
user
=
UserFactory
.
create
()
self
.
desc_md
=
{}
self
.
desc_md
=
{}
self
.
smc
=
StudentModuleCache
(
course_id
,
self
.
user
,
[]
)
self
.
mdc
=
ModelDataCache
([
mock_descriptor
()],
course_id
,
self
.
user
)
self
.
kvs
=
LmsKeyValueStore
(
course_id
,
self
.
user
,
self
.
desc_md
,
self
.
sm
c
)
self
.
kvs
=
LmsKeyValueStore
(
self
.
desc_md
,
self
.
md
c
)
def
test_get_existing_field
(
self
):
def
test_get_existing_field
(
self
):
"Test that getting an existing field in an existing ContentField works"
"Test that getting an existing field in an existing ContentField works"
...
@@ -283,8 +287,11 @@ class TestStudentPrefsStorage(TestCase):
...
@@ -283,8 +287,11 @@ class TestStudentPrefsStorage(TestCase):
student_pref
=
StudentPrefsFactory
.
create
()
student_pref
=
StudentPrefsFactory
.
create
()
self
.
user
=
student_pref
.
student
self
.
user
=
student_pref
.
student
self
.
desc_md
=
{}
self
.
desc_md
=
{}
self
.
smc
=
StudentModuleCache
(
course_id
,
self
.
user
,
[])
self
.
mdc
=
ModelDataCache
([
mock_descriptor
([
self
.
kvs
=
LmsKeyValueStore
(
course_id
,
self
.
user
,
self
.
desc_md
,
self
.
smc
)
mock_field
(
Scope
.
student_preferences
,
'student_pref_field'
),
mock_field
(
Scope
.
student_preferences
,
'not_student_pref_field'
),
])],
course_id
,
self
.
user
)
self
.
kvs
=
LmsKeyValueStore
(
self
.
desc_md
,
self
.
mdc
)
def
test_get_existing_field
(
self
):
def
test_get_existing_field
(
self
):
"Test that getting an existing field in an existing StudentPrefsField works"
"Test that getting an existing field in an existing StudentPrefsField works"
...
@@ -309,7 +316,6 @@ class TestStudentPrefsStorage(TestCase):
...
@@ -309,7 +316,6 @@ class TestStudentPrefsStorage(TestCase):
def
test_delete_existing_field
(
self
):
def
test_delete_existing_field
(
self
):
"Test that deleting an existing field removes it"
"Test that deleting an existing field removes it"
print
list
(
XModuleStudentPrefsField
.
objects
.
all
())
self
.
kvs
.
delete
(
student_prefs_key
(
'student_pref_field'
))
self
.
kvs
.
delete
(
student_prefs_key
(
'student_pref_field'
))
self
.
assertEquals
(
0
,
XModuleStudentPrefsField
.
objects
.
all
()
.
count
())
self
.
assertEquals
(
0
,
XModuleStudentPrefsField
.
objects
.
all
()
.
count
())
...
@@ -325,8 +331,11 @@ class TestStudentInfoStorage(TestCase):
...
@@ -325,8 +331,11 @@ class TestStudentInfoStorage(TestCase):
student_info
=
StudentInfoFactory
.
create
()
student_info
=
StudentInfoFactory
.
create
()
self
.
user
=
student_info
.
student
self
.
user
=
student_info
.
student
self
.
desc_md
=
{}
self
.
desc_md
=
{}
self
.
smc
=
StudentModuleCache
(
course_id
,
self
.
user
,
[])
self
.
mdc
=
ModelDataCache
([
mock_descriptor
([
self
.
kvs
=
LmsKeyValueStore
(
course_id
,
self
.
user
,
self
.
desc_md
,
self
.
smc
)
mock_field
(
Scope
.
student_info
,
'student_info_field'
),
mock_field
(
Scope
.
student_info
,
'not_student_info_field'
),
])],
course_id
,
self
.
user
)
self
.
kvs
=
LmsKeyValueStore
(
self
.
desc_md
,
self
.
mdc
)
def
test_get_existing_field
(
self
):
def
test_get_existing_field
(
self
):
"Test that getting an existing field in an existing StudentInfoField works"
"Test that getting an existing field in an existing StudentInfoField works"
...
...
lms/djangoapps/courseware/tests/tests.py
View file @
993c24b7
...
@@ -23,7 +23,7 @@ import xmodule.modulestore.django
...
@@ -23,7 +23,7 @@ import xmodule.modulestore.django
# Need access to internal func to put users in the right group
# Need access to internal func to put users in the right group
from
courseware
import
grades
from
courseware
import
grades
from
courseware.access
import
_course_staff_group_name
from
courseware.access
import
_course_staff_group_name
from
courseware.model
s
import
StudentModule
Cache
from
courseware.model
_data
import
ModelData
Cache
from
student.models
import
Registration
from
student.models
import
Registration
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
...
@@ -705,27 +705,27 @@ class TestCourseGrader(PageLoader):
...
@@ -705,27 +705,27 @@ class TestCourseGrader(PageLoader):
self
.
factory
=
RequestFactory
()
self
.
factory
=
RequestFactory
()
def
get_grade_summary
(
self
):
def
get_grade_summary
(
self
):
student_module_cache
=
StudentModule
Cache
.
cache_for_descriptor_descendents
(
model_data_cache
=
ModelData
Cache
.
cache_for_descriptor_descendents
(
self
.
graded_course
.
id
,
self
.
student_user
,
self
.
graded_course
)
self
.
graded_course
.
id
,
self
.
student_user
,
self
.
graded_course
)
fake_request
=
self
.
factory
.
get
(
reverse
(
'progress'
,
fake_request
=
self
.
factory
.
get
(
reverse
(
'progress'
,
kwargs
=
{
'course_id'
:
self
.
graded_course
.
id
}))
kwargs
=
{
'course_id'
:
self
.
graded_course
.
id
}))
return
grades
.
grade
(
self
.
student_user
,
fake_request
,
return
grades
.
grade
(
self
.
student_user
,
fake_request
,
self
.
graded_course
,
student_module
_cache
)
self
.
graded_course
,
model_data
_cache
)
def
get_homework_scores
(
self
):
def
get_homework_scores
(
self
):
return
self
.
get_grade_summary
()[
'totaled_scores'
][
'Homework'
]
return
self
.
get_grade_summary
()[
'totaled_scores'
][
'Homework'
]
def
get_progress_summary
(
self
):
def
get_progress_summary
(
self
):
student_module_cache
=
StudentModule
Cache
.
cache_for_descriptor_descendents
(
model_data_cache
=
ModelData
Cache
.
cache_for_descriptor_descendents
(
self
.
graded_course
.
id
,
self
.
student_user
,
self
.
graded_course
)
self
.
graded_course
.
id
,
self
.
student_user
,
self
.
graded_course
)
fake_request
=
self
.
factory
.
get
(
reverse
(
'progress'
,
fake_request
=
self
.
factory
.
get
(
reverse
(
'progress'
,
kwargs
=
{
'course_id'
:
self
.
graded_course
.
id
}))
kwargs
=
{
'course_id'
:
self
.
graded_course
.
id
}))
progress_summary
=
grades
.
progress_summary
(
self
.
student_user
,
fake_request
,
progress_summary
=
grades
.
progress_summary
(
self
.
student_user
,
fake_request
,
self
.
graded_course
,
student_module
_cache
)
self
.
graded_course
,
model_data
_cache
)
return
progress_summary
return
progress_summary
def
check_grade_percent
(
self
,
percent
):
def
check_grade_percent
(
self
,
percent
):
...
...
lms/djangoapps/courseware/views.py
View file @
993c24b7
...
@@ -23,7 +23,7 @@ from courseware import grades
...
@@ -23,7 +23,7 @@ from courseware import grades
from
courseware.access
import
has_access
from
courseware.access
import
has_access
from
courseware.courses
import
(
get_course_with_access
,
get_courses_by_university
)
from
courseware.courses
import
(
get_course_with_access
,
get_courses_by_university
)
import
courseware.tabs
as
tabs
import
courseware.tabs
as
tabs
from
courseware.model
s
import
StudentModule
Cache
from
courseware.model
_data
import
ModelData
Cache
from
module_render
import
toc_for_course
,
get_module
from
module_render
import
toc_for_course
,
get_module
from
student.models
import
UserProfile
from
student.models
import
UserProfile
...
@@ -81,7 +81,7 @@ def courses(request):
...
@@ -81,7 +81,7 @@ def courses(request):
return
render_to_response
(
"courses.html"
,
{
'universities'
:
universities
})
return
render_to_response
(
"courses.html"
,
{
'universities'
:
universities
})
def
render_accordion
(
request
,
course
,
chapter
,
section
):
def
render_accordion
(
request
,
course
,
chapter
,
section
,
model_data_cache
):
''' Draws navigation bar. Takes current position in accordion as
''' Draws navigation bar. Takes current position in accordion as
parameter.
parameter.
...
@@ -92,7 +92,7 @@ def render_accordion(request, course, chapter, section):
...
@@ -92,7 +92,7 @@ def render_accordion(request, course, chapter, section):
Returns the html string'''
Returns the html string'''
# grab the table of contents
# grab the table of contents
toc
=
toc_for_course
(
request
.
user
,
request
,
course
,
chapter
,
section
)
toc
=
toc_for_course
(
request
.
user
,
request
,
course
,
chapter
,
section
,
model_data_cache
)
context
=
dict
([(
'toc'
,
toc
),
context
=
dict
([(
'toc'
,
toc
),
(
'course_id'
,
course
.
id
),
(
'course_id'
,
course
.
id
),
...
@@ -191,24 +191,21 @@ def index(request, course_id, chapter=None, section=None,
...
@@ -191,24 +191,21 @@ def index(request, course_id, chapter=None, section=None,
return
redirect
(
reverse
(
'about_course'
,
args
=
[
course
.
id
]))
return
redirect
(
reverse
(
'about_course'
,
args
=
[
course
.
id
]))
try
:
try
:
student_module_cache
=
StudentModule
Cache
.
cache_for_descriptor_descendents
(
model_data_cache
=
ModelData
Cache
.
cache_for_descriptor_descendents
(
course
.
id
,
request
.
user
,
course
,
depth
=
2
)
course
.
id
,
request
.
user
,
course
,
depth
=
2
)
# Has this student been in this course before?
course_module
=
get_module
(
request
.
user
,
request
,
course
.
location
,
model_data_cache
,
course
.
id
)
first_time
=
student_module_cache
.
lookup
(
course_id
,
'course'
,
course
.
location
.
url
())
is
None
course_module
=
get_module
(
request
.
user
,
request
,
course
.
location
,
student_module_cache
,
course
.
id
)
if
course_module
is
None
:
if
course_module
is
None
:
log
.
warning
(
'If you see this, something went wrong: if we got this'
log
.
warning
(
'If you see this, something went wrong: if we got this'
' far, should have gotten a course module for this user'
)
' far, should have gotten a course module for this user'
)
return
redirect
(
reverse
(
'about_course'
,
args
=
[
course
.
id
]))
return
redirect
(
reverse
(
'about_course'
,
args
=
[
course
.
id
]))
if
chapter
is
None
:
if
chapter
is
None
:
return
redirect_to_course_position
(
course_module
,
first_time
)
return
redirect_to_course_position
(
course_module
,
course_module
.
first_time_user
)
context
=
{
context
=
{
'csrf'
:
csrf
(
request
)[
'csrf_token'
],
'csrf'
:
csrf
(
request
)[
'csrf_token'
],
'accordion'
:
render_accordion
(
request
,
course
,
chapter
,
section
),
'accordion'
:
render_accordion
(
request
,
course
,
chapter
,
section
,
model_data_cache
),
'COURSE_TITLE'
:
course
.
title
,
'COURSE_TITLE'
:
course
.
title
,
'course'
:
course
,
'course'
:
course
,
'init'
:
''
,
'init'
:
''
,
...
@@ -224,7 +221,7 @@ def index(request, course_id, chapter=None, section=None,
...
@@ -224,7 +221,7 @@ def index(request, course_id, chapter=None, section=None,
raise
Http404
raise
Http404
chapter_module
=
get_module
(
request
.
user
,
request
,
chapter_descriptor
.
location
,
chapter_module
=
get_module
(
request
.
user
,
request
,
chapter_descriptor
.
location
,
student_module
_cache
,
course_id
)
model_data
_cache
,
course_id
)
if
chapter_module
is
None
:
if
chapter_module
is
None
:
# User may be trying to access a chapter that isn't live yet
# User may be trying to access a chapter that isn't live yet
raise
Http404
raise
Http404
...
@@ -235,11 +232,11 @@ def index(request, course_id, chapter=None, section=None,
...
@@ -235,11 +232,11 @@ def index(request, course_id, chapter=None, section=None,
# Specifically asked-for section doesn't exist
# Specifically asked-for section doesn't exist
raise
Http404
raise
Http404
section_
student_module_cache
=
StudentModule
Cache
.
cache_for_descriptor_descendents
(
section_
model_data_cache
=
ModelData
Cache
.
cache_for_descriptor_descendents
(
course_id
,
request
.
user
,
section_descriptor
)
course_id
,
request
.
user
,
section_descriptor
)
section_module
=
get_module
(
request
.
user
,
request
,
section_module
=
get_module
(
request
.
user
,
request
,
section_descriptor
.
location
,
section_descriptor
.
location
,
section_
student_module
_cache
,
course_id
,
position
)
section_
model_data
_cache
,
course_id
,
position
)
if
section_module
is
None
:
if
section_module
is
None
:
# User may be trying to be clever and access something
# User may be trying to be clever and access something
# they don't have access to.
# they don't have access to.
...
@@ -491,12 +488,12 @@ def progress(request, course_id, student_id=None):
...
@@ -491,12 +488,12 @@ def progress(request, course_id, student_id=None):
# additional DB lookup (this kills the Progress page in particular).
# additional DB lookup (this kills the Progress page in particular).
student
=
User
.
objects
.
prefetch_related
(
"groups"
)
.
get
(
id
=
student
.
id
)
student
=
User
.
objects
.
prefetch_related
(
"groups"
)
.
get
(
id
=
student
.
id
)
student_module_cache
=
StudentModule
Cache
.
cache_for_descriptor_descendents
(
model_data_cache
=
ModelData
Cache
.
cache_for_descriptor_descendents
(
course_id
,
student
,
course
)
course_id
,
student
,
course
)
courseware_summary
=
grades
.
progress_summary
(
student
,
request
,
course
,
courseware_summary
=
grades
.
progress_summary
(
student
,
request
,
course
,
student_module
_cache
)
model_data
_cache
)
grade_summary
=
grades
.
grade
(
student
,
request
,
course
,
student_module
_cache
)
grade_summary
=
grades
.
grade
(
student
,
request
,
course
,
model_data
_cache
)
if
courseware_summary
is
None
:
if
courseware_summary
is
None
:
#This means the student didn't have access to the course (which the instructor requested)
#This means the student didn't have access to the course (which the instructor requested)
...
...
lms/templates/courseware/info.html
View file @
993c24b7
...
@@ -27,20 +27,20 @@ $(document).ready(function(){
...
@@ -27,20 +27,20 @@ $(document).ready(function(){
% if user.is_authenticated():
% if user.is_authenticated():
<section
class=
"updates"
>
<section
class=
"updates"
>
<h1>
Course Updates
&
News
</h1>
<h1>
Course Updates
&
News
</h1>
${get_course_info_section(request, c
ache, c
ourse, 'updates')}
${get_course_info_section(request, course, 'updates')}
</section>
</section>
<section
aria-label=
"Handout Navigation"
class=
"handouts"
>
<section
aria-label=
"Handout Navigation"
class=
"handouts"
>
<h1>
${course.info_sidebar_name}
</h1>
<h1>
${course.info_sidebar_name}
</h1>
${get_course_info_section(request, c
ache, c
ourse, 'handouts')}
${get_course_info_section(request, course, 'handouts')}
</section>
</section>
% else:
% else:
<section
class=
"updates"
>
<section
class=
"updates"
>
<h1>
Course Updates
&
News
</h1>
<h1>
Course Updates
&
News
</h1>
${get_course_info_section(request, c
ache, c
ourse, 'guest_updates')}
${get_course_info_section(request, course, 'guest_updates')}
</section>
</section>
<section
aria-label=
"Handout Navigation"
class=
"handouts"
>
<section
aria-label=
"Handout Navigation"
class=
"handouts"
>
<h1>
Course Handouts
</h1>
<h1>
Course Handouts
</h1>
${get_course_info_section(request, c
ache, c
ourse, 'guest_handouts')}
${get_course_info_section(request, course, 'guest_handouts')}
</section>
</section>
% endif
% endif
</div>
</div>
...
...
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