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
3d7246ec
Commit
3d7246ec
authored
Jul 01, 2015
by
Sven Marnach
Committed by
Braden MacDonald
Jul 13, 2015
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Allow masquerading as a specific user different from the logged in user.
parent
d494fe22
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
24 changed files
with
539 additions
and
159 deletions
+539
-159
common/lib/xmodule/xmodule/abtest_module.py
+2
-0
common/lib/xmodule/xmodule/capa_module.py
+1
-0
common/lib/xmodule/xmodule/conditional_module.py
+2
-0
common/lib/xmodule/xmodule/foldit_module.py
+2
-0
common/lib/xmodule/xmodule/html_module.py
+1
-0
common/lib/xmodule/xmodule/library_content_module.py
+2
-0
common/lib/xmodule/xmodule/randomize_module.py
+2
-0
common/lib/xmodule/xmodule/seq_module.py
+2
-0
common/lib/xmodule/xmodule/split_test_module.py
+2
-0
common/lib/xmodule/xmodule/vertical_block.py
+2
-0
common/lib/xmodule/xmodule/video_module/video_module.py
+2
-0
common/lib/xmodule/xmodule/x_module.py
+5
-0
lms/djangoapps/courseware/masquerade.py
+148
-23
lms/djangoapps/courseware/model_data.py
+20
-33
lms/djangoapps/courseware/module_render.py
+47
-20
lms/djangoapps/courseware/tests/test_entrance_exam.py
+1
-0
lms/djangoapps/courseware/tests/test_masquerade.py
+147
-5
lms/djangoapps/courseware/tests/test_module_render.py
+6
-6
lms/djangoapps/courseware/tests/test_submitting_problems.py
+29
-24
lms/djangoapps/courseware/tests/test_views.py
+2
-2
lms/djangoapps/courseware/views.py
+17
-23
lms/djangoapps/instructor_task/tasks_helper.py
+4
-2
lms/static/sass/course/layout/_courseware_preview.scss
+18
-0
lms/templates/courseware/course_navigation.html
+75
-21
No files found.
common/lib/xmodule/xmodule/abtest_module.py
View file @
3d7246ec
...
@@ -80,6 +80,8 @@ class ABTestModule(ABTestFields, XModule):
...
@@ -80,6 +80,8 @@ class ABTestModule(ABTestFields, XModule):
class
ABTestDescriptor
(
ABTestFields
,
RawDescriptor
,
XmlDescriptor
):
class
ABTestDescriptor
(
ABTestFields
,
RawDescriptor
,
XmlDescriptor
):
module_class
=
ABTestModule
module_class
=
ABTestModule
show_in_read_only_mode
=
True
@classmethod
@classmethod
def
definition_from_xml
(
cls
,
xml_object
,
system
):
def
definition_from_xml
(
cls
,
xml_object
,
system
):
"""
"""
...
...
common/lib/xmodule/xmodule/capa_module.py
View file @
3d7246ec
...
@@ -119,6 +119,7 @@ class CapaDescriptor(CapaFields, RawDescriptor):
...
@@ -119,6 +119,7 @@ class CapaDescriptor(CapaFields, RawDescriptor):
module_class
=
CapaModule
module_class
=
CapaModule
has_score
=
True
has_score
=
True
show_in_read_only_mode
=
True
template_dir_name
=
'problem'
template_dir_name
=
'problem'
mako_template
=
"widgets/problem-edit.html"
mako_template
=
"widgets/problem-edit.html"
js
=
{
'coffee'
:
[
resource_string
(
__name__
,
'js/src/problem/edit.coffee'
)]}
js
=
{
'coffee'
:
[
resource_string
(
__name__
,
'js/src/problem/edit.coffee'
)]}
...
...
common/lib/xmodule/xmodule/conditional_module.py
View file @
3d7246ec
...
@@ -188,6 +188,8 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor):
...
@@ -188,6 +188,8 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor):
has_score
=
False
has_score
=
False
show_in_read_only_mode
=
True
def
__init__
(
self
,
*
args
,
**
kwargs
):
def
__init__
(
self
,
*
args
,
**
kwargs
):
"""
"""
Create an instance of the conditional module.
Create an instance of the conditional module.
...
...
common/lib/xmodule/xmodule/foldit_module.py
View file @
3d7246ec
...
@@ -188,6 +188,8 @@ class FolditDescriptor(FolditFields, XmlDescriptor, EditingDescriptor):
...
@@ -188,6 +188,8 @@ class FolditDescriptor(FolditFields, XmlDescriptor, EditingDescriptor):
has_score
=
True
has_score
=
True
show_in_read_only_mode
=
True
js
=
{
'coffee'
:
[
resource_string
(
__name__
,
'js/src/html/edit.coffee'
)]}
js
=
{
'coffee'
:
[
resource_string
(
__name__
,
'js/src/html/edit.coffee'
)]}
js_module_name
=
"HTMLEditingDescriptor"
js_module_name
=
"HTMLEditingDescriptor"
...
...
common/lib/xmodule/xmodule/html_module.py
View file @
3d7246ec
...
@@ -96,6 +96,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): # pylint: d
...
@@ -96,6 +96,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): # pylint: d
filename_extension
=
"xml"
filename_extension
=
"xml"
template_dir_name
=
"html"
template_dir_name
=
"html"
has_responsive_ui
=
True
has_responsive_ui
=
True
show_in_read_only_mode
=
True
js
=
{
'coffee'
:
[
resource_string
(
__name__
,
'js/src/html/edit.coffee'
)]}
js
=
{
'coffee'
:
[
resource_string
(
__name__
,
'js/src/html/edit.coffee'
)]}
js_module_name
=
"HTMLEditingDescriptor"
js_module_name
=
"HTMLEditingDescriptor"
...
...
common/lib/xmodule/xmodule/library_content_module.py
View file @
3d7246ec
...
@@ -299,6 +299,8 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe
...
@@ -299,6 +299,8 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe
js
=
{
'coffee'
:
[
resource_string
(
__name__
,
'js/src/vertical/edit.coffee'
)]}
js
=
{
'coffee'
:
[
resource_string
(
__name__
,
'js/src/vertical/edit.coffee'
)]}
js_module_name
=
"VerticalDescriptor"
js_module_name
=
"VerticalDescriptor"
show_in_read_only_mode
=
True
@property
@property
def
non_editable_metadata_fields
(
self
):
def
non_editable_metadata_fields
(
self
):
non_editable_fields
=
super
(
LibraryContentDescriptor
,
self
)
.
non_editable_metadata_fields
non_editable_fields
=
super
(
LibraryContentDescriptor
,
self
)
.
non_editable_metadata_fields
...
...
common/lib/xmodule/xmodule/randomize_module.py
View file @
3d7246ec
...
@@ -101,6 +101,8 @@ class RandomizeDescriptor(RandomizeFields, SequenceDescriptor):
...
@@ -101,6 +101,8 @@ class RandomizeDescriptor(RandomizeFields, SequenceDescriptor):
filename_extension
=
"xml"
filename_extension
=
"xml"
show_in_read_only_mode
=
True
def
definition_to_xml
(
self
,
resource_fs
):
def
definition_to_xml
(
self
,
resource_fs
):
xml_object
=
etree
.
Element
(
'randomize'
)
xml_object
=
etree
.
Element
(
'randomize'
)
...
...
common/lib/xmodule/xmodule/seq_module.py
View file @
3d7246ec
...
@@ -157,6 +157,8 @@ class SequenceDescriptor(SequenceFields, MakoModuleDescriptor, XmlDescriptor):
...
@@ -157,6 +157,8 @@ class SequenceDescriptor(SequenceFields, MakoModuleDescriptor, XmlDescriptor):
mako_template
=
'widgets/sequence-edit.html'
mako_template
=
'widgets/sequence-edit.html'
module_class
=
SequenceModule
module_class
=
SequenceModule
show_in_read_only_mode
=
True
js
=
{
js
=
{
'coffee'
:
[
resource_string
(
__name__
,
'js/src/sequence/edit.coffee'
)],
'coffee'
:
[
resource_string
(
__name__
,
'js/src/sequence/edit.coffee'
)],
}
}
...
...
common/lib/xmodule/xmodule/split_test_module.py
View file @
3d7246ec
...
@@ -375,6 +375,8 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
...
@@ -375,6 +375,8 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
mako_template
=
"widgets/metadata-only-edit.html"
mako_template
=
"widgets/metadata-only-edit.html"
show_in_read_only_mode
=
True
child_descriptor
=
module_attr
(
'child_descriptor'
)
child_descriptor
=
module_attr
(
'child_descriptor'
)
log_child_render
=
module_attr
(
'log_child_render'
)
log_child_render
=
module_attr
(
'log_child_render'
)
get_content_titles
=
module_attr
(
'get_content_titles'
)
get_content_titles
=
module_attr
(
'get_content_titles'
)
...
...
common/lib/xmodule/xmodule/vertical_block.py
View file @
3d7246ec
...
@@ -29,6 +29,8 @@ class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParse
...
@@ -29,6 +29,8 @@ class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParse
has_children
=
True
has_children
=
True
show_in_read_only_mode
=
True
def
student_view
(
self
,
context
):
def
student_view
(
self
,
context
):
"""
"""
Renders the student view of the block in the LMS.
Renders the student view of the block in the LMS.
...
...
common/lib/xmodule/xmodule/video_module/video_module.py
View file @
3d7246ec
...
@@ -339,6 +339,8 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
...
@@ -339,6 +339,8 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
module_class
=
VideoModule
module_class
=
VideoModule
transcript
=
module_attr
(
'transcript'
)
transcript
=
module_attr
(
'transcript'
)
show_in_read_only_mode
=
True
tabs
=
[
tabs
=
[
{
{
'name'
:
_
(
"Basic"
),
'name'
:
_
(
"Basic"
),
...
...
common/lib/xmodule/xmodule/x_module.py
View file @
3d7246ec
...
@@ -278,6 +278,10 @@ class XModuleMixin(XModuleFields, XBlock):
...
@@ -278,6 +278,10 @@ class XModuleMixin(XModuleFields, XBlock):
# (like a practice problem).
# (like a practice problem).
has_score
=
False
has_score
=
False
# Whether this module can be displayed in read-only mode. It is safe to set this to True if
# all user state is handled through the FieldData API.
show_in_read_only_mode
=
False
# Class level variable
# Class level variable
# True if this descriptor always requires recalculation of grades, for
# True if this descriptor always requires recalculation of grades, for
...
@@ -754,6 +758,7 @@ class XModule(HTMLSnippet, XModuleMixin): # pylint: disable=abstract-method
...
@@ -754,6 +758,7 @@ class XModule(HTMLSnippet, XModuleMixin): # pylint: disable=abstract-method
entry_point
=
"xmodule.v1"
entry_point
=
"xmodule.v1"
has_score
=
descriptor_attr
(
'has_score'
)
has_score
=
descriptor_attr
(
'has_score'
)
show_in_read_only_mode
=
descriptor_attr
(
'show_in_read_only_mode'
)
_field_data_cache
=
descriptor_attr
(
'_field_data_cache'
)
_field_data_cache
=
descriptor_attr
(
'_field_data_cache'
)
_field_data
=
descriptor_attr
(
'_field_data'
)
_field_data
=
descriptor_attr
(
'_field_data'
)
_dirty_fields
=
descriptor_attr
(
'_dirty_fields'
)
_dirty_fields
=
descriptor_attr
(
'_dirty_fields'
)
...
...
lms/djangoapps/courseware/masquerade.py
View file @
3d7246ec
...
@@ -8,10 +8,15 @@ import logging
...
@@ -8,10 +8,15 @@ import logging
from
django.conf
import
settings
from
django.conf
import
settings
from
django.contrib.auth.decorators
import
login_required
from
django.contrib.auth.decorators
import
login_required
from
django.contrib.auth.models
import
User
from
django.utils.translation
import
ugettext
as
_
from
django.views.decorators.http
import
require_POST
from
django.views.decorators.http
import
require_POST
from
student.models
import
CourseEnrollment
from
util.json_request
import
expect_json
,
JsonResponse
from
util.json_request
import
expect_json
,
JsonResponse
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
from
xblock.fragment
import
Fragment
from
xblock.runtime
import
KeyValueStore
log
=
logging
.
getLogger
(
__name__
)
log
=
logging
.
getLogger
(
__name__
)
...
@@ -19,16 +24,21 @@ log = logging.getLogger(__name__)
...
@@ -19,16 +24,21 @@ log = logging.getLogger(__name__)
# The value is a dict from course keys to CourseMasquerade objects.
# The value is a dict from course keys to CourseMasquerade objects.
MASQUERADE_SETTINGS_KEY
=
'masquerade_settings'
MASQUERADE_SETTINGS_KEY
=
'masquerade_settings'
# The key used to store temporary XBlock field data in the Django session. This is where field
# data is stored to avoid modifying the state of the user we are masquerading as.
MASQUERADE_DATA_KEY
=
'masquerade_data'
class
CourseMasquerade
(
object
):
class
CourseMasquerade
(
object
):
"""
"""
Masquerade settings for a particular course.
Masquerade settings for a particular course.
"""
"""
def
__init__
(
self
,
course_key
,
role
=
'student'
,
user_partition_id
=
None
,
group_id
=
None
):
def
__init__
(
self
,
course_key
,
role
=
'student'
,
user_partition_id
=
None
,
group_id
=
None
,
user_name
=
None
):
self
.
course_key
=
course_key
self
.
course_key
=
course_key
self
.
role
=
role
self
.
role
=
role
self
.
user_partition_id
=
user_partition_id
self
.
user_partition_id
=
user_partition_id
self
.
group_id
=
group_id
self
.
group_id
=
group_id
self
.
user_name
=
user_name
@require_POST
@require_POST
...
@@ -46,40 +56,73 @@ def handle_ajax(request, course_key_string):
...
@@ -46,40 +56,73 @@ def handle_ajax(request, course_key_string):
role
=
request_json
.
get
(
'role'
,
'student'
)
role
=
request_json
.
get
(
'role'
,
'student'
)
user_partition_id
=
request_json
.
get
(
'user_partition_id'
,
None
)
user_partition_id
=
request_json
.
get
(
'user_partition_id'
,
None
)
group_id
=
request_json
.
get
(
'group_id'
,
None
)
group_id
=
request_json
.
get
(
'group_id'
,
None
)
user_name
=
request_json
.
get
(
'user_name'
,
None
)
if
user_name
:
users_in_course
=
CourseEnrollment
.
objects
.
users_enrolled_in
(
course_key
)
try
:
if
'@'
in
user_name
:
user_name
=
users_in_course
.
get
(
email
=
user_name
)
.
username
else
:
users_in_course
.
get
(
username
=
user_name
)
except
User
.
DoesNotExist
:
return
JsonResponse
({
'success'
:
False
,
'error'
:
_
(
'There is no user with the username or email address {user_name} '
'enrolled in this course.'
)
.
format
(
user_name
=
user_name
)
})
masquerade_settings
[
course_key
]
=
CourseMasquerade
(
masquerade_settings
[
course_key
]
=
CourseMasquerade
(
course_key
,
course_key
,
role
=
role
,
role
=
role
,
user_partition_id
=
user_partition_id
,
user_partition_id
=
user_partition_id
,
group_id
=
group_id
group_id
=
group_id
,
user_name
=
user_name
,
)
)
request
.
session
[
MASQUERADE_SETTINGS_KEY
]
=
masquerade_settings
request
.
session
[
MASQUERADE_SETTINGS_KEY
]
=
masquerade_settings
return
JsonResponse
()
return
JsonResponse
(
{
'success'
:
True
}
)
def
setup_masquerade
(
request
,
course_key
,
staff_access
=
False
):
def
setup_masquerade
(
request
,
course_key
,
staff_access
=
False
,
reset_masquerade_data
=
False
):
"""
Sets up masquerading for the current user within the current request. The
request's user is updated to have a 'masquerade_settings' attribute with
the dict of all masqueraded settings if called from within a request context.
The function then returns the CourseMasquerade object for the specified
course key, or None if there isn't one.
"""
"""
if
request
.
user
is
None
:
Sets up masquerading for the current user within the current request. The request's user is
return
None
updated to have a 'masquerade_settings' attribute with the dict of all masqueraded settings if
called from within a request context. The function then returns a pair (CourseMasquerade, User)
if
not
settings
.
FEATURES
.
get
(
'ENABLE_MASQUERADE'
,
False
):
with the masquerade settings for the specified course key or None if there isn't one, and the
return
None
user we are masquerading as or request.user if masquerading as a specific user is not active.
if
not
staff_access
:
# can masquerade only if user has staff access to course
return
None
masquerade_settings
=
request
.
session
.
get
(
MASQUERADE_SETTINGS_KEY
,
{})
If the reset_masquerade_data flag is set, the field data stored in the session will be cleared.
"""
if
(
request
.
user
is
None
or
not
settings
.
FEATURES
.
get
(
'ENABLE_MASQUERADE'
,
False
)
or
not
staff_access
):
return
None
,
request
.
user
if
reset_masquerade_data
:
request
.
session
.
pop
(
MASQUERADE_DATA_KEY
,
None
)
masquerade_settings
=
request
.
session
.
setdefault
(
MASQUERADE_SETTINGS_KEY
,
{})
# Store the masquerade settings on the user so it can be accessed without the request
# Store the masquerade settings on the user so it can be accessed without the request
request
.
user
.
masquerade_settings
=
masquerade_settings
request
.
user
.
masquerade_settings
=
masquerade_settings
course_masquerade
=
masquerade_settings
.
get
(
course_key
,
None
)
# Return the masquerade for the current course, or none if there isn't one
masquerade_user
=
None
return
masquerade_settings
.
get
(
course_key
,
None
)
if
course_masquerade
and
course_masquerade
.
user_name
:
try
:
masquerade_user
=
CourseEnrollment
.
objects
.
users_enrolled_in
(
course_key
)
.
get
(
username
=
course_masquerade
.
user_name
)
except
User
.
DoesNotExist
:
# This can only happen if the user was unenrolled from the course since masquerading
# was enabled. We silently reset the masquerading configuration in this case.
course_masquerade
=
None
del
masquerade_settings
[
course_key
]
request
.
session
.
modified
=
True
else
:
# Store the masquerading settings on the masquerade_user as well, since this user will
# be used in some places instead of request.user.
masquerade_user
.
masquerade_settings
=
request
.
user
.
masquerade_settings
masquerade_user
.
real_user
=
request
.
user
return
course_masquerade
,
masquerade_user
or
request
.
user
def
get_course_masquerade
(
user
,
course_key
):
def
get_course_masquerade
(
user
,
course_key
):
...
@@ -106,6 +149,14 @@ def is_masquerading_as_student(user, course_key):
...
@@ -106,6 +149,14 @@ def is_masquerading_as_student(user, course_key):
return
get_masquerade_role
(
user
,
course_key
)
==
'student'
return
get_masquerade_role
(
user
,
course_key
)
==
'student'
def
is_masquerading_as_specific_student
(
user
,
course_key
):
# pylint: disable=invalid-name
"""
Returns whether the user is a staff member masquerading as a specific student.
"""
course_masquerade
=
get_course_masquerade
(
user
,
course_key
)
return
bool
(
course_masquerade
and
course_masquerade
.
user_name
)
def
get_masquerading_group_info
(
user
,
course_key
):
def
get_masquerading_group_info
(
user
,
course_key
):
"""
"""
If the user is masquerading as belonging to a group, then this method returns
If the user is masquerading as belonging to a group, then this method returns
...
@@ -116,3 +167,77 @@ def get_masquerading_group_info(user, course_key):
...
@@ -116,3 +167,77 @@ def get_masquerading_group_info(user, course_key):
if
not
course_masquerade
:
if
not
course_masquerade
:
return
None
,
None
return
None
,
None
return
course_masquerade
.
group_id
,
course_masquerade
.
user_partition_id
return
course_masquerade
.
group_id
,
course_masquerade
.
user_partition_id
# Sentinel object to mark deleted objects in the session cache
_DELETED_SENTINEL
=
object
()
class
MasqueradingKeyValueStore
(
KeyValueStore
):
"""
A `KeyValueStore` to avoid affecting the user state when masquerading.
This `KeyValueStore` wraps an underlying `KeyValueStore`. Reads are forwarded to the underlying
store, but writes go to a Django session (or other dictionary-like object).
"""
def
__init__
(
self
,
kvs
,
session
):
"""
Arguments:
kvs: The KeyValueStore to wrap.
session: The Django session used to store temporary data in.
"""
self
.
kvs
=
kvs
self
.
session
=
session
self
.
session_data
=
session
.
setdefault
(
MASQUERADE_DATA_KEY
,
{})
def
_serialize_key
(
self
,
key
):
"""
Convert the key of Type KeyValueStore.Key to a string.
Keys are not JSON-serializable, so we can't use them as keys for the Django session.
The implementation is taken from cms/djangoapps/contentstore/views/session_kv_store.py.
"""
return
repr
(
tuple
(
key
))
def
get
(
self
,
key
):
key_str
=
self
.
_serialize_key
(
key
)
try
:
value
=
self
.
session_data
[
key_str
]
except
KeyError
:
return
self
.
kvs
.
get
(
key
)
else
:
if
value
is
_DELETED_SENTINEL
:
raise
KeyError
(
key_str
)
return
value
def
set
(
self
,
key
,
value
):
self
.
session_data
[
self
.
_serialize_key
(
key
)]
=
value
self
.
session
.
modified
=
True
def
delete
(
self
,
key
):
# We can't simply delete the key from the session, since it might still exist in the kvs,
# which we are not allowed to modify, so we mark it as deleted by setting it to
# _DELETED_SENTINEL in the session.
self
.
set
(
key
,
_DELETED_SENTINEL
)
def
has
(
self
,
key
):
try
:
value
=
self
.
session_data
[
self
.
_serialize_key
(
key
)]
except
KeyError
:
return
self
.
kvs
.
has
(
key
)
else
:
return
value
!=
_DELETED_SENTINEL
def
filter_displayed_blocks
(
block
,
unused_view
,
frag
,
unused_context
):
"""
A wrapper to only show XBlocks that set `show_in_read_only_mode` when masquerading as a specific user.
We don't want to modify the state of the user we are masquerading as, so we can't show XBlocks
that store information outside of the XBlock fields API.
"""
if
getattr
(
block
,
'show_in_read_only_mode'
,
False
):
return
frag
return
Fragment
(
_
(
u'This type of component cannot be shown while viewing the course as a specific student.'
)
)
lms/djangoapps/courseware/model_data.py
View file @
3d7246ec
...
@@ -480,27 +480,6 @@ class UserStateCache(object):
...
@@ -480,27 +480,6 @@ class UserStateCache(object):
kvs_key
.
field_name
in
self
.
_cache
[
cache_key
]
kvs_key
.
field_name
in
self
.
_cache
[
cache_key
]
)
)
# @contract(user_id=int, usage_key=UsageKey, score="number|None", max_score="number|None")
def
set_score
(
self
,
user_id
,
usage_key
,
score
,
max_score
):
"""
UNSUPPORTED METHOD
Set the score and max_score for the specified user and xblock usage.
"""
student_module
,
created
=
StudentModule
.
objects
.
get_or_create
(
student_id
=
user_id
,
module_state_key
=
usage_key
,
course_id
=
usage_key
.
course_key
,
defaults
=
{
'grade'
:
score
,
'max_grade'
:
max_score
,
}
)
if
not
created
:
student_module
.
grade
=
score
student_module
.
max_grade
=
max_score
student_module
.
save
()
def
__len__
(
self
):
def
__len__
(
self
):
return
len
(
self
.
_cache
)
return
len
(
self
.
_cache
)
...
@@ -923,18 +902,6 @@ class FieldDataCache(object):
...
@@ -923,18 +902,6 @@ class FieldDataCache(object):
return
self
.
cache
[
key
.
scope
]
.
has
(
key
)
return
self
.
cache
[
key
.
scope
]
.
has
(
key
)
# @contract(user_id=int, usage_key=UsageKey, score="number|None", max_score="number|None")
def
set_score
(
self
,
user_id
,
usage_key
,
score
,
max_score
):
"""
UNSUPPORTED METHOD
Set the score and max_score for the specified user and xblock usage.
"""
assert
not
self
.
user
.
is_anonymous
()
assert
user_id
==
self
.
user
.
id
assert
usage_key
.
course_key
==
self
.
course_id
self
.
cache
[
Scope
.
user_state
]
.
set_score
(
user_id
,
usage_key
,
score
,
max_score
)
@contract
(
key
=
DjangoKeyValueStore
.
Key
,
returns
=
"datetime|None"
)
@contract
(
key
=
DjangoKeyValueStore
.
Key
,
returns
=
"datetime|None"
)
def
last_modified
(
self
,
key
):
def
last_modified
(
self
,
key
):
"""
"""
...
@@ -1017,3 +984,23 @@ class ScoresClient(object):
...
@@ -1017,3 +984,23 @@ class ScoresClient(object):
client
=
cls
(
fd_cache
.
course_id
,
fd_cache
.
user
.
id
)
client
=
cls
(
fd_cache
.
course_id
,
fd_cache
.
user
.
id
)
client
.
fetch_scores
(
fd_cache
.
scorable_locations
)
client
.
fetch_scores
(
fd_cache
.
scorable_locations
)
return
client
return
client
# @contract(user_id=int, usage_key=UsageKey, score="number|None", max_score="number|None")
def
set_score
(
user_id
,
usage_key
,
score
,
max_score
):
"""
Set the score and max_score for the specified user and xblock usage.
"""
student_module
,
created
=
StudentModule
.
objects
.
get_or_create
(
student_id
=
user_id
,
module_state_key
=
usage_key
,
course_id
=
usage_key
.
course_key
,
defaults
=
{
'grade'
:
score
,
'max_grade'
:
max_score
,
}
)
if
not
created
:
student_module
.
grade
=
score
student_module
.
max_grade
=
max_score
student_module
.
save
()
lms/djangoapps/courseware/module_render.py
View file @
3d7246ec
This diff is collapsed.
Click to expand it.
lms/djangoapps/courseware/tests/test_entrance_exam.py
View file @
3d7246ec
...
@@ -528,6 +528,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
...
@@ -528,6 +528,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
self
.
entrance_exam
self
.
entrance_exam
)
)
return
toc_for_course
(
return
toc_for_course
(
self
.
request
.
user
,
self
.
request
,
self
.
request
,
self
.
course
,
self
.
course
,
self
.
entrance_exam
.
url_name
,
self
.
entrance_exam
.
url_name
,
...
...
lms/djangoapps/courseware/tests/test_masquerade.py
View file @
3d7246ec
...
@@ -7,13 +7,21 @@ from nose.plugins.attrib import attr
...
@@ -7,13 +7,21 @@ from nose.plugins.attrib import attr
from
datetime
import
datetime
from
datetime
import
datetime
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
django.test
import
TestCase
from
django.utils.timezone
import
UTC
from
django.utils.timezone
import
UTC
from
capa.tests.response_xml_factory
import
OptionResponseXMLFactory
from
capa.tests.response_xml_factory
import
OptionResponseXMLFactory
from
courseware.masquerade
import
handle_ajax
,
setup_masquerade
,
get_masquerading_group_info
from
courseware.masquerade
import
(
MasqueradingKeyValueStore
,
handle_ajax
,
setup_masquerade
,
get_masquerading_group_info
)
from
courseware.tests.factories
import
StaffFactory
from
courseware.tests.factories
import
StaffFactory
from
courseware.tests.helpers
import
LoginEnrollmentTestCase
,
get_request_for_user
from
courseware.tests.helpers
import
LoginEnrollmentTestCase
,
get_request_for_user
from
courseware.tests.test_submitting_problems
import
ProblemSubmissionTestMixin
from
student.tests.factories
import
UserFactory
from
student.tests.factories
import
UserFactory
from
xblock.runtime
import
DictKeyValueStore
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
ItemFactory
,
CourseFactory
from
xmodule.modulestore.tests.factories
import
ItemFactory
,
CourseFactory
...
@@ -54,7 +62,7 @@ class MasqueradeTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase):
...
@@ -54,7 +62,7 @@ class MasqueradeTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase):
options
=
[
'Correct'
,
'Incorrect'
],
options
=
[
'Correct'
,
'Incorrect'
],
correct_option
=
'Correct'
correct_option
=
'Correct'
)
)
self
.
problem_display_name
=
"Test
Masquerade
Problem"
self
.
problem_display_name
=
"Test
Masquerade
Problem"
self
.
problem
=
ItemFactory
.
create
(
self
.
problem
=
ItemFactory
.
create
(
parent_location
=
self
.
vertical
.
location
,
parent_location
=
self
.
vertical
.
location
,
category
=
'problem'
,
category
=
'problem'
,
...
@@ -158,7 +166,7 @@ class StaffMasqueradeTestCase(MasqueradeTestCase):
...
@@ -158,7 +166,7 @@ class StaffMasqueradeTestCase(MasqueradeTestCase):
"""
"""
return
StaffFactory
(
course_key
=
self
.
course
.
id
)
return
StaffFactory
(
course_key
=
self
.
course
.
id
)
def
update_masquerade
(
self
,
role
,
group_id
=
None
):
def
update_masquerade
(
self
,
role
,
group_id
=
None
,
user_name
=
None
):
"""
"""
Toggle masquerade state.
Toggle masquerade state.
"""
"""
...
@@ -170,10 +178,10 @@ class StaffMasqueradeTestCase(MasqueradeTestCase):
...
@@ -170,10 +178,10 @@ class StaffMasqueradeTestCase(MasqueradeTestCase):
)
)
response
=
self
.
client
.
post
(
response
=
self
.
client
.
post
(
masquerade_url
,
masquerade_url
,
json
.
dumps
({
"role"
:
role
,
"group_id"
:
group_id
}),
json
.
dumps
({
"role"
:
role
,
"group_id"
:
group_id
,
"user_name"
:
user_name
}),
"application/json"
"application/json"
)
)
self
.
assertEqual
(
response
.
status_code
,
20
4
)
self
.
assertEqual
(
response
.
status_code
,
20
0
)
return
response
return
response
...
@@ -216,6 +224,80 @@ class TestStaffMasqueradeAsStudent(StaffMasqueradeTestCase):
...
@@ -216,6 +224,80 @@ class TestStaffMasqueradeAsStudent(StaffMasqueradeTestCase):
@attr
(
'shard_1'
)
@attr
(
'shard_1'
)
class
TestStaffMasqueradeAsSpecificStudent
(
StaffMasqueradeTestCase
,
ProblemSubmissionTestMixin
):
"""
Check for staff being able to masquerade as a specific student.
"""
def
setUp
(
self
):
super
(
TestStaffMasqueradeAsSpecificStudent
,
self
)
.
setUp
()
self
.
student_user
=
self
.
create_user
()
self
.
login_student
()
self
.
enroll
(
self
.
course
,
True
)
def
login_staff
(
self
):
""" Login as a staff user """
self
.
login
(
self
.
test_user
.
email
,
'test'
)
def
login_student
(
self
):
""" Login as a student """
self
.
login
(
self
.
student_user
.
email
,
'test'
)
def
submit_answer
(
self
,
response1
,
response2
):
"""
Submit an answer to the single problem in our test course.
"""
return
self
.
submit_question_answer
(
self
.
problem_display_name
,
{
'2_1'
:
response1
,
'2_2'
:
response2
}
)
def
get_progress_detail
(
self
):
"""
Return the reported progress detail for the problem in our test course.
The return value is a string like u'1/2'.
"""
return
json
.
loads
(
self
.
look_at_question
(
self
.
problem_display_name
)
.
content
)[
'progress_detail'
]
@patch.dict
(
'django.conf.settings.FEATURES'
,
{
'DISABLE_START_DATES'
:
False
})
def
test_masquerade_as_specific_student
(
self
):
"""
Test masquerading as a specific user.
We answer the problem in our test course as the student and as staff user, and we use the
progress as a proxy to determine who's state we currently see.
"""
# Answer correctly as the student, and check progress.
self
.
login_student
()
self
.
submit_answer
(
'Correct'
,
'Correct'
)
self
.
assertEqual
(
self
.
get_progress_detail
(),
u'2/2'
)
# Log in as staff, and check the problem is unanswered.
self
.
login_staff
()
self
.
assertEqual
(
self
.
get_progress_detail
(),
u'0/2'
)
# Masquerade as the student, and check we can see the student state.
self
.
update_masquerade
(
role
=
'student'
,
user_name
=
self
.
student_user
.
username
)
self
.
assertEqual
(
self
.
get_progress_detail
(),
u'2/2'
)
# Temporarily override the student state.
self
.
submit_answer
(
'Correct'
,
'Incorrect'
)
self
.
assertEqual
(
self
.
get_progress_detail
(),
u'1/2'
)
# Reload the page and check we see the student state again.
self
.
get_courseware_page
()
self
.
assertEqual
(
self
.
get_progress_detail
(),
u'2/2'
)
# Become the staff user again, and check the problem is still unanswered.
self
.
update_masquerade
(
role
=
'staff'
)
self
.
assertEqual
(
self
.
get_progress_detail
(),
u'0/2'
)
# Verify the student state did not change.
self
.
login_student
()
self
.
assertEqual
(
self
.
get_progress_detail
(),
u'2/2'
)
@attr
(
'shard_1'
)
class
TestGetMasqueradingGroupId
(
StaffMasqueradeTestCase
):
class
TestGetMasqueradingGroupId
(
StaffMasqueradeTestCase
):
"""
"""
Check for staff being able to masquerade as belonging to a group.
Check for staff being able to masquerade as belonging to a group.
...
@@ -252,3 +334,63 @@ class TestGetMasqueradingGroupId(StaffMasqueradeTestCase):
...
@@ -252,3 +334,63 @@ class TestGetMasqueradingGroupId(StaffMasqueradeTestCase):
group_id
,
user_partition_id
=
get_masquerading_group_info
(
self
.
test_user
,
self
.
course
.
id
)
group_id
,
user_partition_id
=
get_masquerading_group_info
(
self
.
test_user
,
self
.
course
.
id
)
self
.
assertEqual
(
group_id
,
1
)
self
.
assertEqual
(
group_id
,
1
)
self
.
assertEqual
(
user_partition_id
,
0
)
self
.
assertEqual
(
user_partition_id
,
0
)
class
ReadOnlyKeyValueStore
(
DictKeyValueStore
):
"""
A KeyValueStore that raises an exception on attempts to modify it.
Used to make sure MasqueradingKeyValueStore does not try to modify the underlying KeyValueStore.
"""
def
set
(
self
,
key
,
value
):
assert
False
,
"ReadOnlyKeyValueStore may not be modified."
def
delete
(
self
,
key
):
assert
False
,
"ReadOnlyKeyValueStore may not be modified."
def
set_many
(
self
,
update_dict
):
# pylint: disable=unused-argument
assert
False
,
"ReadOnlyKeyValueStore may not be modified."
class
FakeSession
(
dict
):
""" Mock for Django session object. """
modified
=
False
# We need dict semantics with a writable 'modified' property
class
MasqueradingKeyValueStoreTest
(
TestCase
):
"""
Unit tests for the MasqueradingKeyValueStore class.
"""
def
setUp
(
self
):
super
(
MasqueradingKeyValueStoreTest
,
self
)
.
setUp
()
self
.
ro_kvs
=
ReadOnlyKeyValueStore
({
'a'
:
42
,
'b'
:
None
,
'c'
:
'OpenCraft'
})
self
.
session
=
FakeSession
()
self
.
kvs
=
MasqueradingKeyValueStore
(
self
.
ro_kvs
,
self
.
session
)
def
test_all
(
self
):
self
.
assertEqual
(
self
.
kvs
.
get
(
'a'
),
42
)
self
.
assertEqual
(
self
.
kvs
.
get
(
'b'
),
None
)
self
.
assertEqual
(
self
.
kvs
.
get
(
'c'
),
'OpenCraft'
)
with
self
.
assertRaises
(
KeyError
):
self
.
kvs
.
get
(
'd'
)
self
.
assertTrue
(
self
.
kvs
.
has
(
'a'
))
self
.
assertTrue
(
self
.
kvs
.
has
(
'b'
))
self
.
assertTrue
(
self
.
kvs
.
has
(
'c'
))
self
.
assertFalse
(
self
.
kvs
.
has
(
'd'
))
self
.
kvs
.
set_many
({
'a'
:
'Norwegian Blue'
,
'd'
:
'Giraffe'
})
self
.
kvs
.
set
(
'b'
,
7
)
self
.
assertEqual
(
self
.
kvs
.
get
(
'a'
),
'Norwegian Blue'
)
self
.
assertEqual
(
self
.
kvs
.
get
(
'b'
),
7
)
self
.
assertEqual
(
self
.
kvs
.
get
(
'c'
),
'OpenCraft'
)
self
.
assertEqual
(
self
.
kvs
.
get
(
'd'
),
'Giraffe'
)
for
key
in
'abd'
:
self
.
assertTrue
(
self
.
kvs
.
has
(
key
))
self
.
kvs
.
delete
(
key
)
with
self
.
assertRaises
(
KeyError
):
self
.
kvs
.
get
(
key
)
self
.
assertEqual
(
self
.
kvs
.
get
(
'c'
),
'OpenCraft'
)
lms/djangoapps/courseware/tests/test_module_render.py
View file @
3d7246ec
...
@@ -637,7 +637,7 @@ class TestTOC(ModuleStoreTestCase):
...
@@ -637,7 +637,7 @@ class TestTOC(ModuleStoreTestCase):
course
=
self
.
store
.
get_course
(
self
.
toy_course
.
id
,
depth
=
2
)
course
=
self
.
store
.
get_course
(
self
.
toy_course
.
id
,
depth
=
2
)
with
check_mongo_calls
(
toc_finds
):
with
check_mongo_calls
(
toc_finds
):
actual
=
render
.
toc_for_course
(
actual
=
render
.
toc_for_course
(
self
.
request
,
course
,
self
.
chapter
,
None
,
self
.
field_data_cache
self
.
request
.
user
,
self
.
request
,
course
,
self
.
chapter
,
None
,
self
.
field_data_cache
)
)
for
toc_section
in
expected
:
for
toc_section
in
expected
:
self
.
assertIn
(
toc_section
,
actual
)
self
.
assertIn
(
toc_section
,
actual
)
...
@@ -676,7 +676,7 @@ class TestTOC(ModuleStoreTestCase):
...
@@ -676,7 +676,7 @@ class TestTOC(ModuleStoreTestCase):
with
check_mongo_calls
(
toc_finds
):
with
check_mongo_calls
(
toc_finds
):
actual
=
render
.
toc_for_course
(
actual
=
render
.
toc_for_course
(
self
.
request
,
self
.
toy_course
,
self
.
chapter
,
section
,
self
.
field_data_cache
self
.
request
.
user
,
self
.
request
,
self
.
toy_course
,
self
.
chapter
,
section
,
self
.
field_data_cache
)
)
for
toc_section
in
expected
:
for
toc_section
in
expected
:
self
.
assertIn
(
toc_section
,
actual
)
self
.
assertIn
(
toc_section
,
actual
)
...
@@ -1173,7 +1173,7 @@ class TestAnonymousStudentId(ModuleStoreTestCase, LoginEnrollmentTestCase):
...
@@ -1173,7 +1173,7 @@ class TestAnonymousStudentId(ModuleStoreTestCase, LoginEnrollmentTestCase):
return
render
.
get_module_for_descriptor_internal
(
return
render
.
get_module_for_descriptor_internal
(
user
=
self
.
user
,
user
=
self
.
user
,
descriptor
=
descriptor
,
descriptor
=
descriptor
,
field_data_cache
=
Mock
(
spec
=
FieldDataCache
,
name
=
'field_data_cache
'
),
student_data
=
Mock
(
spec
=
FieldData
,
name
=
'student_data
'
),
course_id
=
course_id
,
course_id
=
course_id
,
track_function
=
Mock
(
name
=
'track_function'
),
# Track Function
track_function
=
Mock
(
name
=
'track_function'
),
# Track Function
xqueue_callback_url_prefix
=
Mock
(
name
=
'xqueue_callback_url_prefix'
),
# XQueue Callback Url Prefix
xqueue_callback_url_prefix
=
Mock
(
name
=
'xqueue_callback_url_prefix'
),
# XQueue Callback Url Prefix
...
@@ -1468,7 +1468,7 @@ class LMSXBlockServiceBindingTest(ModuleStoreTestCase):
...
@@ -1468,7 +1468,7 @@ class LMSXBlockServiceBindingTest(ModuleStoreTestCase):
"""
"""
super
(
LMSXBlockServiceBindingTest
,
self
)
.
setUp
()
super
(
LMSXBlockServiceBindingTest
,
self
)
.
setUp
()
self
.
user
=
UserFactory
()
self
.
user
=
UserFactory
()
self
.
field_data_cache
=
Mock
()
self
.
student_data
=
Mock
()
self
.
course
=
CourseFactory
.
create
()
self
.
course
=
CourseFactory
.
create
()
self
.
track_function
=
Mock
()
self
.
track_function
=
Mock
()
self
.
xqueue_callback_url_prefix
=
Mock
()
self
.
xqueue_callback_url_prefix
=
Mock
()
...
@@ -1483,7 +1483,7 @@ class LMSXBlockServiceBindingTest(ModuleStoreTestCase):
...
@@ -1483,7 +1483,7 @@ class LMSXBlockServiceBindingTest(ModuleStoreTestCase):
descriptor
=
ItemFactory
(
category
=
"pure"
,
parent
=
self
.
course
)
descriptor
=
ItemFactory
(
category
=
"pure"
,
parent
=
self
.
course
)
runtime
,
_
=
render
.
get_module_system_for_user
(
runtime
,
_
=
render
.
get_module_system_for_user
(
self
.
user
,
self
.
user
,
self
.
field_data_cache
,
self
.
student_data
,
descriptor
,
descriptor
,
self
.
course
.
id
,
self
.
course
.
id
,
self
.
track_function
,
self
.
track_function
,
...
@@ -1502,7 +1502,7 @@ class LMSXBlockServiceBindingTest(ModuleStoreTestCase):
...
@@ -1502,7 +1502,7 @@ class LMSXBlockServiceBindingTest(ModuleStoreTestCase):
descriptor
.
days_early_for_beta
=
5
descriptor
.
days_early_for_beta
=
5
runtime
,
_
=
render
.
get_module_system_for_user
(
runtime
,
_
=
render
.
get_module_system_for_user
(
self
.
user
,
self
.
user
,
self
.
field_data_cache
,
self
.
student_data
,
descriptor
,
descriptor
,
self
.
course
.
id
,
self
.
course
.
id
,
self
.
track_function
,
self
.
track_function
,
...
...
lms/djangoapps/courseware/tests/test_submitting_problems.py
View file @
3d7246ec
...
@@ -9,6 +9,7 @@ from textwrap import dedent
...
@@ -9,6 +9,7 @@ from textwrap import dedent
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.client
import
RequestFactory
from
django.test.client
import
RequestFactory
from
mock
import
patch
from
mock
import
patch
from
nose.plugins.attrib
import
attr
from
nose.plugins.attrib
import
attr
...
@@ -33,31 +34,10 @@ from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider
...
@@ -33,31 +34,10 @@ from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider
from
openedx.core.djangoapps.user_api.tests.factories
import
UserCourseTagFactory
from
openedx.core.djangoapps.user_api.tests.factories
import
UserCourseTagFactory
class
TestSubmittingProblems
(
ModuleStoreTestCase
,
LoginEnrollment
TestCase
):
class
ProblemSubmissionTestMixin
(
TestCase
):
"""
"""
Check that a course gets graded properly
.
TestCase mixin that provides functions to submit answers to problems
.
"""
"""
# arbitrary constant
COURSE_SLUG
=
"100"
COURSE_NAME
=
"test_course"
def
setUp
(
self
):
super
(
TestSubmittingProblems
,
self
)
.
setUp
(
create_user
=
False
)
# Create course
self
.
course
=
CourseFactory
.
create
(
display_name
=
self
.
COURSE_NAME
,
number
=
self
.
COURSE_SLUG
)
assert
self
.
course
,
"Couldn't load course
%
r"
%
self
.
COURSE_NAME
# create a test student
self
.
student
=
'view@test.com'
self
.
password
=
'foo'
self
.
create_account
(
'u1'
,
self
.
student
,
self
.
password
)
self
.
activate_user
(
self
.
student
)
self
.
enroll
(
self
.
course
)
self
.
student_user
=
User
.
objects
.
get
(
email
=
self
.
student
)
self
.
factory
=
RequestFactory
()
def
refresh_course
(
self
):
def
refresh_course
(
self
):
"""
"""
Re-fetch the course from the database so that the object being dealt with has everything added to it.
Re-fetch the course from the database so that the object being dealt with has everything added to it.
...
@@ -68,7 +48,6 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase):
...
@@ -68,7 +48,6 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
"""
Returns the url of the problem given the problem's name
Returns the url of the problem given the problem's name
"""
"""
return
self
.
course
.
id
.
make_usage_key
(
'problem'
,
problem_url_name
)
return
self
.
course
.
id
.
make_usage_key
(
'problem'
,
problem_url_name
)
def
modx_url
(
self
,
problem_location
,
dispatch
):
def
modx_url
(
self
,
problem_location
,
dispatch
):
...
@@ -136,6 +115,32 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase):
...
@@ -136,6 +115,32 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase):
resp
=
self
.
client
.
post
(
modx_url
)
resp
=
self
.
client
.
post
(
modx_url
)
return
resp
return
resp
class
TestSubmittingProblems
(
ModuleStoreTestCase
,
LoginEnrollmentTestCase
,
ProblemSubmissionTestMixin
):
"""
Check that a course gets graded properly.
"""
# arbitrary constant
COURSE_SLUG
=
"100"
COURSE_NAME
=
"test_course"
def
setUp
(
self
):
super
(
TestSubmittingProblems
,
self
)
.
setUp
(
create_user
=
False
)
# Create course
self
.
course
=
CourseFactory
.
create
(
display_name
=
self
.
COURSE_NAME
,
number
=
self
.
COURSE_SLUG
)
assert
self
.
course
,
"Couldn't load course
%
r"
%
self
.
COURSE_NAME
# create a test student
self
.
student
=
'view@test.com'
self
.
password
=
'foo'
self
.
create_account
(
'u1'
,
self
.
student
,
self
.
password
)
self
.
activate_user
(
self
.
student
)
self
.
enroll
(
self
.
course
)
self
.
student_user
=
User
.
objects
.
get
(
email
=
self
.
student
)
self
.
factory
=
RequestFactory
()
def
add_dropdown_to_section
(
self
,
section_location
,
name
,
num_inputs
=
2
):
def
add_dropdown_to_section
(
self
,
section_location
,
name
,
num_inputs
=
2
):
"""
"""
Create and return a dropdown problem.
Create and return a dropdown problem.
...
...
lms/djangoapps/courseware/tests/test_views.py
View file @
3d7246ec
...
@@ -673,8 +673,8 @@ class TestAccordionDueDate(BaseDueDateTests):
...
@@ -673,8 +673,8 @@ class TestAccordionDueDate(BaseDueDateTests):
def
get_text
(
self
,
course
):
def
get_text
(
self
,
course
):
""" Returns the HTML for the accordion """
""" Returns the HTML for the accordion """
return
views
.
render_accordion
(
return
views
.
render_accordion
(
self
.
request
,
course
,
course
.
get_children
()[
0
]
.
scope_ids
.
usage_id
.
to_deprecated_string
()
,
self
.
request
.
user
,
self
.
request
,
course
,
None
,
None
unicode
(
course
.
get_children
()[
0
]
.
scope_ids
.
usage_id
),
None
,
None
)
)
...
...
lms/djangoapps/courseware/views.py
View file @
3d7246ec
...
@@ -139,7 +139,7 @@ def courses(request):
...
@@ -139,7 +139,7 @@ def courses(request):
)
)
def
render_accordion
(
request
,
course
,
chapter
,
section
,
field_data_cache
):
def
render_accordion
(
user
,
request
,
course
,
chapter
,
section
,
field_data_cache
):
"""
"""
Draws navigation bar. Takes current position in accordion as
Draws navigation bar. Takes current position in accordion as
parameter.
parameter.
...
@@ -151,7 +151,7 @@ def render_accordion(request, course, chapter, section, field_data_cache):
...
@@ -151,7 +151,7 @@ def render_accordion(request, course, chapter, section, field_data_cache):
Returns the html string
Returns the html string
"""
"""
# grab the table of contents
# grab the table of contents
toc
=
toc_for_course
(
request
,
course
,
chapter
,
section
,
field_data_cache
)
toc
=
toc_for_course
(
user
,
request
,
course
,
chapter
,
section
,
field_data_cache
)
context
=
dict
([
context
=
dict
([
(
'toc'
,
toc
),
(
'toc'
,
toc
),
...
@@ -378,10 +378,10 @@ def _index_bulk_op(request, course_key, chapter, section, position):
...
@@ -378,10 +378,10 @@ def _index_bulk_op(request, course_key, chapter, section, position):
except
ValueError
:
except
ValueError
:
raise
Http404
(
u"Position {} is not an integer!"
.
format
(
position
))
raise
Http404
(
u"Position {} is not an integer!"
.
format
(
position
))
user
=
request
.
user
course
=
get_course_with_access
(
request
.
user
,
'load'
,
course_key
,
depth
=
2
)
course
=
get_course_with_access
(
user
,
'load'
,
course_key
,
depth
=
2
)
staff_access
=
has_access
(
request
.
user
,
'staff'
,
course
)
masquerade
,
user
=
setup_masquerade
(
request
,
course_key
,
staff_access
,
reset_masquerade_data
=
True
)
staff_access
=
has_access
(
user
,
'staff'
,
course
)
registered
=
registered_for_course
(
course
,
user
)
registered
=
registered_for_course
(
course
,
user
)
if
not
registered
:
if
not
registered
:
# TODO (vshnayder): do course instructors need to be registered to see course?
# TODO (vshnayder): do course instructors need to be registered to see course?
...
@@ -413,8 +413,6 @@ def _index_bulk_op(request, course_key, chapter, section, position):
...
@@ -413,8 +413,6 @@ def _index_bulk_op(request, course_key, chapter, section, position):
if
survey
.
utils
.
must_answer_survey
(
course
,
user
):
if
survey
.
utils
.
must_answer_survey
(
course
,
user
):
return
redirect
(
reverse
(
'course_survey'
,
args
=
[
unicode
(
course
.
id
)]))
return
redirect
(
reverse
(
'course_survey'
,
args
=
[
unicode
(
course
.
id
)]))
masquerade
=
setup_masquerade
(
request
,
course_key
,
staff_access
)
try
:
try
:
field_data_cache
=
FieldDataCache
.
cache_for_descriptor_descendents
(
field_data_cache
=
FieldDataCache
.
cache_for_descriptor_descendents
(
course_key
,
user
,
course
,
depth
=
2
)
course_key
,
user
,
course
,
depth
=
2
)
...
@@ -431,7 +429,7 @@ def _index_bulk_op(request, course_key, chapter, section, position):
...
@@ -431,7 +429,7 @@ def _index_bulk_op(request, course_key, chapter, section, position):
context
=
{
context
=
{
'csrf'
:
csrf
(
request
)[
'csrf_token'
],
'csrf'
:
csrf
(
request
)[
'csrf_token'
],
'accordion'
:
render_accordion
(
request
,
course
,
chapter
,
section
,
field_data_cache
),
'accordion'
:
render_accordion
(
user
,
request
,
course
,
chapter
,
section
,
field_data_cache
),
'COURSE_TITLE'
:
course
.
display_name_with_default
,
'COURSE_TITLE'
:
course
.
display_name_with_default
,
'course'
:
course
,
'course'
:
course
,
'init'
:
''
,
'init'
:
''
,
...
@@ -475,7 +473,7 @@ def _index_bulk_op(request, course_key, chapter, section, position):
...
@@ -475,7 +473,7 @@ def _index_bulk_op(request, course_key, chapter, section, position):
# settings.
# settings.
show_chat
=
course
.
show_chat
and
settings
.
FEATURES
[
'ENABLE_CHAT'
]
show_chat
=
course
.
show_chat
and
settings
.
FEATURES
[
'ENABLE_CHAT'
]
if
show_chat
:
if
show_chat
:
context
[
'chat'
]
=
chat_settings
(
course
,
user
)
context
[
'chat'
]
=
chat_settings
(
course
,
request
.
user
)
# If we couldn't load the chat settings, then don't show
# If we couldn't load the chat settings, then don't show
# the widget in the courseware.
# the widget in the courseware.
if
context
[
'chat'
]
is
None
:
if
context
[
'chat'
]
is
None
:
...
@@ -536,7 +534,7 @@ def _index_bulk_op(request, course_key, chapter, section, position):
...
@@ -536,7 +534,7 @@ def _index_bulk_op(request, course_key, chapter, section, position):
)
)
section_module
=
get_module_for_descriptor
(
section_module
=
get_module_for_descriptor
(
request
.
user
,
user
,
request
,
request
,
section_descriptor
,
section_descriptor
,
field_data_cache
,
field_data_cache
,
...
@@ -550,7 +548,7 @@ def _index_bulk_op(request, course_key, chapter, section, position):
...
@@ -550,7 +548,7 @@ def _index_bulk_op(request, course_key, chapter, section, position):
# they don't have access to.
# they don't have access to.
raise
Http404
raise
Http404
# Save where we are in the chapter
# Save where we are in the chapter
.
save_child_position
(
chapter_module
,
section
)
save_child_position
(
chapter_module
,
section
)
context
[
'fragment'
]
=
section_module
.
render
(
STUDENT_VIEW
)
context
[
'fragment'
]
=
section_module
.
render
(
STUDENT_VIEW
)
context
[
'section_title'
]
=
section_descriptor
.
display_name_with_default
context
[
'section_title'
]
=
section_descriptor
.
display_name_with_default
...
@@ -598,12 +596,8 @@ def _index_bulk_op(request, course_key, chapter, section, position):
...
@@ -598,12 +596,8 @@ def _index_bulk_op(request, course_key, chapter, section, position):
raise
raise
else
:
else
:
log
.
exception
(
log
.
exception
(
u"Error in index view: user=
%
s, course=
%
s, chapter=
%
s, section=
%
s, position=
%
s"
,
u"Error in index view: user=
%
s, effective_user=
%
s, course=
%
s, chapter=
%
s section=
%
s position=
%
s"
,
user
,
request
.
user
,
user
,
course
,
chapter
,
section
,
position
course
,
chapter
,
section
,
position
)
)
try
:
try
:
result
=
render_to_response
(
'courseware/courseware-error.html'
,
{
result
=
render_to_response
(
'courseware/courseware-error.html'
,
{
...
@@ -683,19 +677,19 @@ def course_info(request, course_id):
...
@@ -683,19 +677,19 @@ def course_info(request, course_id):
with
modulestore
()
.
bulk_operations
(
course_key
):
with
modulestore
()
.
bulk_operations
(
course_key
):
course
=
get_course_with_access
(
request
.
user
,
'load'
,
course_key
)
course
=
get_course_with_access
(
request
.
user
,
'load'
,
course_key
)
staff_access
=
has_access
(
request
.
user
,
'staff'
,
course
)
masquerade
,
user
=
setup_masquerade
(
request
,
course_key
,
staff_access
,
reset_masquerade_data
=
True
)
# If the user needs to take an entrance exam to access this course, then we'll need
# If the user needs to take an entrance exam to access this course, then we'll need
# to send them to that specific course module before allowing them into other areas
# to send them to that specific course module before allowing them into other areas
if
user_must_complete_entrance_exam
(
request
,
request
.
user
,
course
):
if
user_must_complete_entrance_exam
(
request
,
user
,
course
):
return
redirect
(
reverse
(
'courseware'
,
args
=
[
unicode
(
course
.
id
)]))
return
redirect
(
reverse
(
'courseware'
,
args
=
[
unicode
(
course
.
id
)]))
# check to see if there is a required survey that must be taken before
# check to see if there is a required survey that must be taken before
# the user can access the course.
# the user can access the course.
if
request
.
user
.
is_authenticated
()
and
survey
.
utils
.
must_answer_survey
(
course
,
request
.
user
):
if
request
.
user
.
is_authenticated
()
and
survey
.
utils
.
must_answer_survey
(
course
,
user
):
return
redirect
(
reverse
(
'course_survey'
,
args
=
[
unicode
(
course
.
id
)]))
return
redirect
(
reverse
(
'course_survey'
,
args
=
[
unicode
(
course
.
id
)]))
staff_access
=
has_access
(
request
.
user
,
'staff'
,
course
)
masquerade
=
setup_masquerade
(
request
,
course_key
,
staff_access
)
# allow staff to masquerade on the info page
studio_url
=
get_studio_url
(
course
,
'course_info'
)
studio_url
=
get_studio_url
(
course
,
'course_info'
)
# link to where the student should go to enroll in the course:
# link to where the student should go to enroll in the course:
...
@@ -704,7 +698,7 @@ def course_info(request, course_id):
...
@@ -704,7 +698,7 @@ def course_info(request, course_id):
if
settings
.
FEATURES
.
get
(
'ENABLE_MKTG_SITE'
):
if
settings
.
FEATURES
.
get
(
'ENABLE_MKTG_SITE'
):
url_to_enroll
=
marketing_link
(
'COURSES'
)
url_to_enroll
=
marketing_link
(
'COURSES'
)
show_enroll_banner
=
request
.
user
.
is_authenticated
()
and
not
CourseEnrollment
.
is_enrolled
(
request
.
user
,
course
.
id
)
show_enroll_banner
=
request
.
user
.
is_authenticated
()
and
not
CourseEnrollment
.
is_enrolled
(
user
,
course
.
id
)
context
=
{
context
=
{
'request'
:
request
,
'request'
:
request
,
...
@@ -719,7 +713,7 @@ def course_info(request, course_id):
...
@@ -719,7 +713,7 @@ def course_info(request, course_id):
}
}
now
=
datetime
.
now
(
UTC
())
now
=
datetime
.
now
(
UTC
())
effective_start
=
_adjust_start_date_for_beta_testers
(
request
.
user
,
course
,
course_key
)
effective_start
=
_adjust_start_date_for_beta_testers
(
user
,
course
,
course_key
)
if
not
in_preview_mode
()
and
staff_access
and
now
<
effective_start
:
if
not
in_preview_mode
()
and
staff_access
and
now
<
effective_start
:
# Disable student view button if user is staff and
# Disable student view button if user is staff and
# course is not yet visible to students.
# course is not yet visible to students.
...
...
lms/djangoapps/instructor_task/tasks_helper.py
View file @
3d7246ec
...
@@ -31,6 +31,7 @@ from shoppingcart.models import (
...
@@ -31,6 +31,7 @@ from shoppingcart.models import (
from
track.views
import
task_track
from
track.views
import
task_track
from
util.file
import
course_filename_prefix_generator
,
UniversalNewlineIterator
from
util.file
import
course_filename_prefix_generator
,
UniversalNewlineIterator
from
xblock.runtime
import
KvsFieldData
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
xmodule.split_test_module
import
get_split_user_partitions
from
xmodule.split_test_module
import
get_split_user_partitions
from
django.utils.translation
import
ugettext
as
_
from
django.utils.translation
import
ugettext
as
_
...
@@ -43,7 +44,7 @@ from certificates.api import generate_user_certificates
...
@@ -43,7 +44,7 @@ from certificates.api import generate_user_certificates
from
courseware.courses
import
get_course_by_id
,
get_problems_in_section
from
courseware.courses
import
get_course_by_id
,
get_problems_in_section
from
courseware.grades
import
iterate_grades_for
from
courseware.grades
import
iterate_grades_for
from
courseware.models
import
StudentModule
from
courseware.models
import
StudentModule
from
courseware.model_data
import
FieldDataCache
from
courseware.model_data
import
DjangoKeyValueStore
,
FieldDataCache
from
courseware.module_render
import
get_module_for_descriptor_internal
from
courseware.module_render
import
get_module_for_descriptor_internal
from
instructor_analytics.basic
import
enrolled_students_features
,
list_may_enroll
from
instructor_analytics.basic
import
enrolled_students_features
,
list_may_enroll
from
instructor_analytics.csvs
import
format_dictlist
from
instructor_analytics.csvs
import
format_dictlist
...
@@ -422,6 +423,7 @@ def _get_module_instance_for_task(course_id, student, module_descriptor, xmodule
...
@@ -422,6 +423,7 @@ def _get_module_instance_for_task(course_id, student, module_descriptor, xmodule
"""
"""
# reconstitute the problem's corresponding XModule:
# reconstitute the problem's corresponding XModule:
field_data_cache
=
FieldDataCache
.
cache_for_descriptor_descendents
(
course_id
,
student
,
module_descriptor
)
field_data_cache
=
FieldDataCache
.
cache_for_descriptor_descendents
(
course_id
,
student
,
module_descriptor
)
student_data
=
KvsFieldData
(
DjangoKeyValueStore
(
field_data_cache
))
# get request-related tracking information from args passthrough, and supplement with task-specific
# get request-related tracking information from args passthrough, and supplement with task-specific
# information:
# information:
...
@@ -444,7 +446,7 @@ def _get_module_instance_for_task(course_id, student, module_descriptor, xmodule
...
@@ -444,7 +446,7 @@ def _get_module_instance_for_task(course_id, student, module_descriptor, xmodule
return
get_module_for_descriptor_internal
(
return
get_module_for_descriptor_internal
(
user
=
student
,
user
=
student
,
descriptor
=
module_descriptor
,
descriptor
=
module_descriptor
,
field_data_cache
=
field_data_cache
,
student_data
=
student_data
,
course_id
=
course_id
,
course_id
=
course_id
,
track_function
=
make_track_function
(),
track_function
=
make_track_function
(),
xqueue_callback_url_prefix
=
xqueue_callback_url_prefix
,
xqueue_callback_url_prefix
=
xqueue_callback_url_prefix
,
...
...
lms/static/sass/course/layout/_courseware_preview.scss
View file @
3d7246ec
...
@@ -22,6 +22,24 @@
...
@@ -22,6 +22,24 @@
margin-bottom
:
0
;
margin-bottom
:
0
;
vertical-align
:
middle
;
vertical-align
:
middle
;
}
}
.action-preview-select
{
margin-right
:
$baseline
;
}
.action-preview-username-container
{
display
:
none
;
.action-preview-username
{
vertical-align
:
middle
;
height
:
25px
;
}
}
}
}
}
}
.preview-specific-student-notice
{
margin-top
:
(
$baseline
/
2
);
font-size
:
90%
;
}
}
}
lms/templates/courseware/course_navigation.html
View file @
3d7246ec
...
@@ -18,12 +18,17 @@ def url_class(is_active):
...
@@ -18,12 +18,17 @@ def url_class(is_active):
if
is_active:
if
is_active:
return
"
active
"
return
"
active
"
return
""
return
""
%
>
<
%
def
selected
(
is_selected
)
:
cohorted_user_partition =
get_cohorted_user_partition(course.id)
return
"
selected
"
if
is_selected
else
""
show_preview_menu =
not
disable_preview_menu
and
staff_access
and
active_page
in
['
courseware
',
'
info
']
is_student_masquerade =
masquerade
and
masquerade
.
role =
=
'
student
'
show_preview_menu =
not
disable_preview_menu
and
staff_access
and
active_page
in
["
courseware
",
"
info
"]
masquerade_group_id =
masquerade.group_id
if
masquerade
else
None
cohorted_user_partition =
get_cohorted_user_partition(course.id)
masquerade_user_name =
masquerade.user_name
if
masquerade
else
None
masquerade_group_id =
masquerade.group_id
if
masquerade
else
None
staff_selected =
selected(not
masquerade
or
masquerade
.
role
!=
"
student
")
specific_student_selected =
selected(not
staff_selected
and
masquerade
.
user_name
)
student_selected =
selected(not
staff_selected
and
not
specific_student_selected
and
not
masquerade_group_id
)
%
>
%
>
% if show_preview_menu:
% if show_preview_menu:
...
@@ -34,20 +39,32 @@ def url_class(is_active):
...
@@ -34,20 +39,32 @@ def url_class(is_active):
<form
action=
"#"
class=
"action-preview-form"
method=
"post"
>
<form
action=
"#"
class=
"action-preview-form"
method=
"post"
>
<label
for=
"action-preview-select"
class=
"action-preview-label"
>
${_("View this course as:")}
</label>
<label
for=
"action-preview-select"
class=
"action-preview-label"
>
${_("View this course as:")}
</label>
<select
class=
"action-preview-select"
id=
"action-preview-select"
name=
"select"
>
<select
class=
"action-preview-select"
id=
"action-preview-select"
name=
"select"
>
<option
value=
"staff"
${"
selected
"
if
not
is_student_masquerade
else
""}
>
${_("Staff")}
</option>
<option
value=
"staff"
${
staff_selected
}
>
${_("Staff")}
</option>
<option
value=
"student"
${"
selected
"
if
is_student_masquerade
and
not
masquerade_group_id
else
""}
>
${_("Student")}
</option>
<option
value=
"student"
${
student_selected
}
>
${_("Student")}
</option>
<option
value=
"specific student"
${
specific_student_selected
}
>
${_("Specific student")}
</option>
% if cohorted_user_partition:
% if cohorted_user_partition:
% for group in sorted(cohorted_user_partition.groups, key=lambda group: group.name):
% for group in sorted(cohorted_user_partition.groups, key=lambda group: group.name):
<option
value=
"group.id"
data-group-id=
"${group.id}"
${
"
selected
"
if
masquerade_group_id =
=
group
.
id
else
""
}
>
<option
value=
"group.id"
data-group-id=
"${group.id}"
${
selected
(
masquerade_group_id =
=
group
.
id
)
}
>
${_("Student in {content_group}").format(content_group=group.name)}
${_("Student in {content_group}").format(content_group=group.name)}
</option>
</option>
% endfor
% endfor
% endif
% endif
</select>
</select>
<button
type=
"submit"
class=
"sr"
name=
"submit"
value=
"submit"
>
${_("set preview mode")}
</button>
<div
class=
"action-preview-username-container"
>
<label
for=
"action-preview-username"
class=
"action-preview-label"
>
${_("Username or email:")}
</label>
<input
type=
"text"
class=
"action-preview-username"
id=
"action-preview-username"
>
</div>
<button
type=
"submit"
class=
"sr"
name=
"submit"
value=
"submit"
>
${_("Set preview mode")}
</button>
</form>
</form>
</li>
</li>
</ol>
</ol>
% if specific_student_selected:
<div
class=
"preview-specific-student-notice"
>
<p>
${_("You are now viewing the course as
<i>
{user_name}
</i>
.").format(user_name=masquerade_user_name)}
</p>
</div>
% endif
</div>
</div>
</nav>
</nav>
% endif
% endif
...
@@ -84,22 +101,55 @@ def url_class(is_active):
...
@@ -84,22 +101,55 @@ def url_class(is_active):
% if show_preview_menu:
% if show_preview_menu:
<script
type=
"text/javascript"
>
<script
type=
"text/javascript"
>
(
function
()
{
(
function
()
{
var
element
=
$
(
'.action-preview-select'
);
var
selectElement
=
$
(
'.action-preview-select'
);
var
userNameElement
=
$
(
'#action-preview-username'
);
var
userNameContainer
=
$
(
'.action-preview-username-container'
)
%
if
disable_student_access
:
%
if
disable_student_access
:
element
.
attr
(
"disabled"
,
true
);
selectElement
.
attr
(
"disabled"
,
true
);
element
.
attr
(
"title"
,
"${_("
Course
is
not
yet
visible
to
students
.
")}"
);
selectElement
.
attr
(
"title"
,
"${_("
Course
is
not
yet
visible
to
students
.
")}"
);
%
endif
%
if
specific_student_selected
:
userNameContainer
.
css
(
'display'
,
'inline-block'
);
userNameElement
.
val
(
'${masquerade_user_name}'
);
%
endif
%
endif
e
lement
.
change
(
function
()
{
selectE
lement
.
change
(
function
()
{
var
selectedOption
,
data
;
var
selectedOption
;
if
(
e
lement
.
attr
(
"disabled"
))
{
if
(
selectE
lement
.
attr
(
"disabled"
))
{
return
alert
(
"${_("
You
cannot
view
the
course
as
a
student
or
beta
tester
before
the
course
release
date
.
")}"
);
return
alert
(
"${_("
You
cannot
view
the
course
as
a
student
or
beta
tester
before
the
course
release
date
.
")}"
);
}
}
selectedOption
=
element
.
find
(
'option:selected'
);
selectedOption
=
selectElement
.
find
(
'option:selected'
);
data
=
{
if
(
selectedOption
.
val
()
===
'specific student'
)
{
userNameContainer
.
css
(
'display'
,
'inline-block'
);
}
else
{
userNameContainer
.
hide
();
masquerade
(
selectedOption
);
}
});
userNameElement
.
keypress
(
function
(
event
)
{
if
(
event
.
keyCode
===
13
)
{
// Avoid submitting the form on enter, since the submit action isn't implemented. Instead, blur the
// element to trigger a change event in case the value was edited, which in turn will trigger an AJAX
// request to update the masquerading data.
userNameElement
.
blur
();
return
false
;
}
return
true
;
});
userNameElement
.
change
(
function
()
{
masquerade
(
selectElement
.
find
(
'option:selected'
));
});
function
masquerade
(
selectedOption
)
{
var
data
=
{
role
:
selectedOption
.
val
()
===
'staff'
?
'staff'
:
'student'
,
role
:
selectedOption
.
val
()
===
'staff'
?
'staff'
:
'student'
,
user_partition_id
:
$
{
cohorted_user_partition
.
id
if
cohorted_user_partition
else
'null'
},
user_partition_id
:
$
{
cohorted_user_partition
.
id
if
cohorted_user_partition
else
'null'
},
group_id
:
selectedOption
.
data
(
'group-id'
)
group_id
:
selectedOption
.
data
(
'group-id'
),
user_name
:
selectedOption
.
val
()
===
'specific student'
?
userNameElement
.
val
()
:
null
};
};
$
.
ajax
({
$
.
ajax
({
url
:
'/courses/${course.id}/masquerade'
,
url
:
'/courses/${course.id}/masquerade'
,
...
@@ -108,13 +158,17 @@ def url_class(is_active):
...
@@ -108,13 +158,17 @@ def url_class(is_active):
contentType
:
'application/json'
,
contentType
:
'application/json'
,
data
:
JSON
.
stringify
(
data
),
data
:
JSON
.
stringify
(
data
),
success
:
function
(
result
)
{
success
:
function
(
result
)
{
location
.
reload
();
if
(
result
.
success
)
{
location
.
reload
();
}
else
{
alert
(
result
.
error
);
}
},
},
error
:
function
()
{
error
:
function
()
{
alert
(
'Error: cannot connect to server'
);
alert
(
'Error: cannot connect to server'
);
}
}
});
});
}
);
}
}());
}());
</script>
</script>
% endif
% endif
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