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
OpenEdx
edx-platform
Commits
5cdf3fb8
Commit
5cdf3fb8
authored
Jun 17, 2013
by
Miles Steele
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
add staff management subsection, add student_admin subsection, refactor sections
parent
cc737cb1
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
780 additions
and
333 deletions
+780
-333
lms/djangoapps/instructor/access.py
+8
-0
lms/djangoapps/instructor/enrollment.py
+51
-0
lms/djangoapps/instructor/views/api.py
+127
-4
lms/djangoapps/instructor/views/instructor_dashboard.py
+5
-0
lms/static/coffee/src/instructor_dashboard.coffee
+0
-281
lms/static/coffee/src/instructor_dashboard/analytics.coffee
+113
-0
lms/static/coffee/src/instructor_dashboard/data_download.coffee
+54
-0
lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee
+69
-0
lms/static/coffee/src/instructor_dashboard/membership.coffee
+183
-0
lms/static/coffee/src/instructor_dashboard/student_admin.coffee
+83
-0
lms/static/sass/course/instructor/_instructor_2.scss
+44
-24
lms/templates/courseware/instructor_dashboard_2/membership.html
+23
-2
lms/templates/courseware/instructor_dashboard_2/student_admin.html
+14
-21
lms/urls.py
+6
-1
No files found.
lms/djangoapps/instructor/access.py
View file @
5cdf3fb8
...
@@ -13,6 +13,14 @@ from django.contrib.auth.models import User, Group
...
@@ -13,6 +13,14 @@ from django.contrib.auth.models import User, Group
from
courseware.access
import
get_access_group_name
from
courseware.access
import
get_access_group_name
def
list_with_level
(
course
,
level
):
grpname
=
get_access_group_name
(
course
,
level
)
try
:
return
Group
.
objects
.
get
(
name
=
grpname
)
.
user_set
.
all
()
except
Group
.
DoesNotExist
:
return
[]
def
allow_access
(
course
,
user
,
level
):
def
allow_access
(
course
,
user
,
level
):
"""
"""
Allow user access to course modification.
Allow user access to course modification.
...
...
lms/djangoapps/instructor/enrollment.py
View file @
5cdf3fb8
...
@@ -5,8 +5,10 @@ Does not include any access control, be sure to check access before calling.
...
@@ -5,8 +5,10 @@ Does not include any access control, be sure to check access before calling.
"""
"""
import
re
import
re
import
json
from
django.contrib.auth.models
import
User
from
django.contrib.auth.models
import
User
from
student.models
import
CourseEnrollment
,
CourseEnrollmentAllowed
from
student.models
import
CourseEnrollment
,
CourseEnrollmentAllowed
from
courseware.models
import
StudentModule
def
enroll_emails
(
course_id
,
student_emails
,
auto_enroll
=
False
):
def
enroll_emails
(
course_id
,
student_emails
,
auto_enroll
=
False
):
...
@@ -136,3 +138,52 @@ def split_input_list(str_list):
...
@@ -136,3 +138,52 @@ def split_input_list(str_list):
new_list
=
[
s
for
s
in
new_list
if
s
!=
''
]
new_list
=
[
s
for
s
in
new_list
if
s
!=
''
]
return
new_list
return
new_list
def
reset_student_attempts
(
course_id
,
student
,
problem_to_reset
,
delete_module
=
False
):
"""
Reset student attempts for a problem. Optionally deletes all student state for the specified problem.
In the previous instructor dashboard it was possible to modify/delete
modules that were not problems. That has been disabled for safety.
student is a User
problem_to_reset is the name of a problem e.g. 'L2Node1'.
To build the module_state_key 'problem/' and course information will be appended to problem_to_reset.
"""
if
problem_to_reset
[
-
4
:]
==
".xml"
:
problem_to_reset
=
problem_to_reset
[:
-
4
]
problem_to_reset
=
"problem/"
+
problem_to_reset
(
org
,
course_name
,
_
)
=
course_id
.
split
(
"/"
)
module_state_key
=
"i4x://"
+
org
+
"/"
+
course_name
+
"/"
+
problem_to_reset
module_to_reset
=
StudentModule
.
objects
.
get
(
student_id
=
student
.
id
,
course_id
=
course_id
,
module_state_key
=
module_state_key
)
if
delete_module
:
module_to_reset
.
delete
()
else
:
_reset_module_attempts
(
module_to_reset
)
def
_reset_module_attempts
(
studentmodule
):
""" Reset the number of attempts on a studentmodule. """
# load the state json
problem_state
=
json
.
loads
(
studentmodule
.
state
)
# old_number_of_attempts = problem_state["attempts"]
problem_state
[
"attempts"
]
=
0
# save
studentmodule
.
state
=
json
.
dumps
(
problem_state
)
studentmodule
.
save
()
# track.views.server_track(request,
# '{instructor} reset attempts from {old_attempts} to 0 for {student} on problem {problem} in {course}'.format(
# old_attempts=old_number_of_attempts,
# student=student_to_reset,
# problem=studentmodule.module_state_key,
# instructor=request.user,
# course=course_id),
# {},
# page='idashboard')
lms/djangoapps/instructor/views/api.py
View file @
5cdf3fb8
...
@@ -10,13 +10,16 @@ TODO a lot of these GETs should be PUTs
...
@@ -10,13 +10,16 @@ TODO a lot of these GETs should be PUTs
import
json
import
json
from
django_future.csrf
import
ensure_csrf_cookie
from
django_future.csrf
import
ensure_csrf_cookie
from
django.views.decorators.cache
import
cache_control
from
django.views.decorators.cache
import
cache_control
from
django.http
import
HttpResponse
from
django.core.urlresolvers
import
reverse
from
django.http
import
HttpResponse
,
HttpResponseBadRequest
from
courseware.courses
import
get_course_with_access
from
courseware.courses
import
get_course_with_access
from
django.contrib.auth.models
import
User
,
Group
from
django.contrib.auth.models
import
User
,
Group
from
courseware.models
import
StudentModule
import
instructor.enrollment
as
enrollment
from
instructor.enrollment
import
split_input_list
,
enroll_emails
,
unenroll_emails
from
instructor.enrollment
import
split_input_list
,
enroll_emails
,
unenroll_emails
from
instructor.access
import
allow_access
,
revoke_access
from
instructor.access
import
allow_access
,
revoke_access
,
list_with_level
import
analytics.basic
import
analytics.basic
import
analytics.distributions
import
analytics.distributions
import
analytics.csvs
import
analytics.csvs
...
@@ -27,6 +30,7 @@ import analytics.csvs
...
@@ -27,6 +30,7 @@ import analytics.csvs
def
enroll_unenroll
(
request
,
course_id
):
def
enroll_unenroll
(
request
,
course_id
):
"""
"""
Enroll or unenroll students by email.
Enroll or unenroll students by email.
Requires staff access.
"""
"""
course
=
get_course_with_access
(
request
.
user
,
course_id
,
'staff'
,
depth
=
None
)
course
=
get_course_with_access
(
request
.
user
,
course_id
,
'staff'
,
depth
=
None
)
...
@@ -48,7 +52,8 @@ def enroll_unenroll(request, course_id):
...
@@ -48,7 +52,8 @@ def enroll_unenroll(request, course_id):
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
def
access_allow_revoke
(
request
,
course_id
):
def
access_allow_revoke
(
request
,
course_id
):
"""
"""
Modify staff/instructor access. (instructor available only)
Modify staff/instructor access.
Requires instructor access.
Query parameters:
Query parameters:
email is the target users email
email is the target users email
...
@@ -71,7 +76,33 @@ def access_allow_revoke(request, course_id):
...
@@ -71,7 +76,33 @@ def access_allow_revoke(request, course_id):
raise
ValueError
(
"unrecognized mode '{}'"
.
format
(
mode
))
raise
ValueError
(
"unrecognized mode '{}'"
.
format
(
mode
))
response_payload
=
{
response_payload
=
{
'done'
:
'yup'
,
'DONE'
:
'YES'
,
}
response
=
HttpResponse
(
json
.
dumps
(
response_payload
),
content_type
=
"application/json"
)
return
response
@ensure_csrf_cookie
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
def
list_instructors_staff
(
request
,
course_id
):
"""
List instructors and staff.
Requires staff access.
"""
course
=
get_course_with_access
(
request
.
user
,
course_id
,
'staff'
,
depth
=
None
)
def
extract_user
(
user
):
return
{
'username'
:
user
.
username
,
'email'
:
user
.
email
,
'first_name'
:
user
.
first_name
,
'last_name'
:
user
.
last_name
,
}
response_payload
=
{
'course_id'
:
course_id
,
'instructor'
:
map
(
extract_user
,
list_with_level
(
course
,
'instructor'
)),
'staff'
:
map
(
extract_user
,
list_with_level
(
course
,
'staff'
)),
}
}
response
=
HttpResponse
(
json
.
dumps
(
response_payload
),
content_type
=
"application/json"
)
response
=
HttpResponse
(
json
.
dumps
(
response_payload
),
content_type
=
"application/json"
)
return
response
return
response
...
@@ -178,3 +209,95 @@ def profile_distribution(request, course_id):
...
@@ -178,3 +209,95 @@ def profile_distribution(request, course_id):
}
}
response
=
HttpResponse
(
json
.
dumps
(
response_payload
),
content_type
=
"application/json"
)
response
=
HttpResponse
(
json
.
dumps
(
response_payload
),
content_type
=
"application/json"
)
return
response
return
response
@ensure_csrf_cookie
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
def
get_student_progress_url
(
request
,
course_id
):
"""
Get the progress url of a student.
Limited to staff access.
Takes query paremeter student_email and if the student exists
returns e.g. {
'progress_url': '/../...'
}
"""
course
=
get_course_with_access
(
request
.
user
,
course_id
,
'staff'
,
depth
=
None
)
student_email
=
request
.
GET
.
get
(
'student_email'
)
if
not
student_email
:
# TODO Is there a way to do a - say - 'raise Http400'?
return
HttpResponseBadRequest
()
user
=
User
.
objects
.
get
(
email
=
student_email
)
progress_url
=
reverse
(
'student_progress'
,
kwargs
=
{
'course_id'
:
course_id
,
'student_id'
:
user
.
id
})
response_payload
=
{
'course_id'
:
course_id
,
'progress_url'
:
progress_url
,
}
response
=
HttpResponse
(
json
.
dumps
(
response_payload
),
content_type
=
"application/json"
)
return
response
@ensure_csrf_cookie
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
def
redirect_to_student_progress
(
request
,
course_id
):
"""
Redirects to the specified students progress page
Limited to staff access.
Takes query parameter student_email
"""
course
=
get_course_with_access
(
request
.
user
,
course_id
,
'staff'
,
depth
=
None
)
student_email
=
request
.
GET
.
get
(
'student_email'
)
if
not
student_email
:
# TODO Is there a way to do a - say - 'raise Http400'?
return
HttpResponseBadRequest
()
user
=
User
.
objects
.
get
(
email
=
student_email
)
progress_url
=
reverse
(
'student_progress'
,
kwargs
=
{
'course_id'
:
course_id
,
'student_id'
:
user
.
id
})
response_payload
=
{
'course_id'
:
course_id
,
'progress_url'
:
progress_url
,
}
response
=
HttpResponse
(
json
.
dumps
(
response_payload
),
content_type
=
"application/json"
)
return
response
@ensure_csrf_cookie
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
def
reset_student_attempts
(
request
,
course_id
):
"""
Resets a students attempts counter. Optionally deletes student state for a problem.
Limited to staff access.
Takes query parameter student_email
Takes query parameter problem_to_reset
Takes query parameter delete_module
"""
course
=
get_course_with_access
(
request
.
user
,
course_id
,
'staff'
,
depth
=
None
)
student_email
=
request
.
GET
.
get
(
'student_email'
)
problem_to_reset
=
request
.
GET
.
get
(
'problem_to_reset'
)
will_delete_module
=
{
'true'
:
True
}
.
get
(
request
.
GET
.
get
(
'delete_module'
,
''
),
False
)
if
not
student_email
or
not
problem_to_reset
:
return
HttpResponseBadRequest
()
user
=
User
.
objects
.
get
(
email
=
student_email
)
try
:
enrollment
.
reset_student_attempts
(
course_id
,
user
,
problem_to_reset
,
delete_module
=
will_delete_module
)
except
StudentModule
.
DoesNotExist
:
return
HttpResponseBadRequest
()
response_payload
=
{
'course_id'
:
course_id
,
'delete_module'
:
will_delete_module
,
}
response
=
HttpResponse
(
json
.
dumps
(
response_payload
),
content_type
=
"application/json"
)
return
response
lms/djangoapps/instructor/views/instructor_dashboard.py
View file @
5cdf3fb8
...
@@ -106,6 +106,8 @@ def _section_membership(course_id):
...
@@ -106,6 +106,8 @@ def _section_membership(course_id):
'section_display_name'
:
'Membership'
,
'section_display_name'
:
'Membership'
,
'enroll_button_url'
:
reverse
(
'enroll_unenroll'
,
kwargs
=
{
'course_id'
:
course_id
}),
'enroll_button_url'
:
reverse
(
'enroll_unenroll'
,
kwargs
=
{
'course_id'
:
course_id
}),
'unenroll_button_url'
:
reverse
(
'enroll_unenroll'
,
kwargs
=
{
'course_id'
:
course_id
}),
'unenroll_button_url'
:
reverse
(
'enroll_unenroll'
,
kwargs
=
{
'course_id'
:
course_id
}),
'list_instructors_staff_url'
:
reverse
(
'list_instructors_staff'
,
kwargs
=
{
'course_id'
:
course_id
}),
'access_allow_revoke_url'
:
reverse
(
'access_allow_revoke'
,
kwargs
=
{
'course_id'
:
course_id
}),
}
}
return
section_data
return
section_data
...
@@ -115,6 +117,9 @@ def _section_student_admin(course_id):
...
@@ -115,6 +117,9 @@ def _section_student_admin(course_id):
section_data
=
{
section_data
=
{
'section_key'
:
'student_admin'
,
'section_key'
:
'student_admin'
,
'section_display_name'
:
'Student Admin'
,
'section_display_name'
:
'Student Admin'
,
'get_student_progress_url'
:
reverse
(
'get_student_progress_url'
,
kwargs
=
{
'course_id'
:
course_id
}),
'unenroll_button_url'
:
reverse
(
'enroll_unenroll'
,
kwargs
=
{
'course_id'
:
course_id
}),
'reset_student_attempts_url'
:
reverse
(
'reset_student_attempts'
,
kwargs
=
{
'course_id'
:
course_id
}),
}
}
return
section_data
return
section_data
...
...
lms/static/coffee/src/instructor_dashboard.coffee
deleted
100644 → 0
View file @
cc737cb1
This diff is collapsed.
Click to expand it.
lms/static/coffee/src/instructor_dashboard/analytics.coffee
0 → 100644
View file @
5cdf3fb8
log
=
->
console
.
log
.
apply
console
,
arguments
plantTimeout
=
(
ms
,
cb
)
->
setTimeout
cb
,
ms
class
Analytics
constructor
:
(
@
$section
)
->
log
"setting up instructor dashboard section - analytics"
$display
=
@
$section
.
find
(
'.distribution-display'
)
@
$display_text
=
$display
.
find
(
'.distribution-display-text'
)
@
$display_graph
=
$display
.
find
(
'.distribution-display-graph'
)
@
$display_table
=
$display
.
find
(
'.distribution-display-table'
)
@
$distribution_select
=
@
$section
.
find
(
'select#distributions'
)
@
populate_selector
=>
@
$distribution_select
.
change
=>
@
on_selector_change
()
reset_display
:
->
@
$display_text
.
empty
()
@
$display_graph
.
empty
()
@
$display_table
.
empty
()
populate_selector
:
(
cb
)
->
@
get_profile_distributions
[],
(
data
)
=>
@
$distribution_select
.
find
(
'option'
).
eq
(
0
).
text
"-- Select distribution"
for
feature
in
data
.
available_features
opt
=
$
'<option/>'
,
text
:
data
.
display_names
[
feature
]
data
:
feature
:
feature
@
$distribution_select
.
append
opt
cb
?
()
on_selector_change
:
->
# log 'changeargs', arguments
opt
=
@
$distribution_select
.
children
(
'option:selected'
)
feature
=
opt
.
data
'feature'
log
"distribution selected:
#{
feature
}
"
@
reset_display
()
return
unless
feature
@
get_profile_distributions
[
feature
],
(
data
)
=>
feature_res
=
data
.
feature_results
[
feature
]
# feature response format: {'error': 'optional error string', 'type': 'SOME_TYPE', 'data': [stuff]}
if
feature_res
.
error
console
.
warn
(
feature_res
.
error
)
@
$display_text
.
text
'Error fetching data'
else
if
feature_res
.
type
is
'EASY_CHOICE'
# setup SlickGrid
options
=
enableCellNavigation
:
true
enableColumnReorder
:
false
columns
=
[
id
:
feature
field
:
feature
name
:
feature
,
id
:
'count'
field
:
'count'
name
:
'Count'
]
grid_data
=
_
.
map
feature_res
.
data
,
(
value
,
key
)
->
datapoint
=
{}
datapoint
[
feature
]
=
key
datapoint
[
'count'
]
=
value
datapoint
table_placeholder
=
$
'<div/>'
,
class
:
'slickgrid'
@
$display_table
.
append
table_placeholder
grid
=
new
Slick
.
Grid
(
table_placeholder
,
grid_data
,
columns
,
options
)
grid
.
autosizeColumns
()
else
if
feature
is
'year_of_birth'
graph_placeholder
=
$
'<div/>'
,
class
:
'year-of-birth'
@
$display_graph
.
append
graph_placeholder
graph_data
=
_
.
map
feature_res
.
data
,
(
value
,
key
)
->
[
parseInt
(
key
),
value
]
$
.
plot
graph_placeholder
,
[
data
:
graph_data
]
else
console
.
warn
(
"don't know how to show
#{
feature_res
.
type
}
"
)
@
$display_text
.
text
'Unavailable Metric
\n
'
+
JSON
.
stringify
(
feature_res
)
# handler can be either a callback for success or a mapping e.g. {success: ->, error: ->, complete: ->}
get_profile_distributions
:
(
featurelist
,
handler
)
->
settings
=
dataType
:
'json'
url
:
@
$distribution_select
.
data
'endpoint'
data
:
features
:
JSON
.
stringify
featurelist
if
typeof
handler
is
'function'
_
.
extend
settings
,
success
:
handler
else
_
.
extend
settings
,
handler
$
.
ajax
settings
# exports
_
.
defaults
window
,
InstructorDashboard
:
{}
_
.
defaults
window
.
InstructorDashboard
,
sections
:
{}
_
.
defaults
window
.
InstructorDashboard
.
sections
,
Analytics
:
Analytics
lms/static/coffee/src/instructor_dashboard/data_download.coffee
0 → 100644
View file @
5cdf3fb8
log
=
->
console
.
log
.
apply
console
,
arguments
plantTimeout
=
(
ms
,
cb
)
->
setTimeout
cb
,
ms
class
DataDownload
constructor
:
(
@
$section
)
->
log
"setting up instructor dashboard section - data download"
$display
=
@
$section
.
find
(
'.data-display'
)
@
$display_text
=
$display
.
find
(
'.data-display-text'
)
@
$display_table
=
$display
.
find
(
'.data-display-table'
)
$list_studs_btn
=
@
$section
.
find
(
"input[name='list-profiles']'"
)
$list_studs_btn
.
click
(
e
)
=>
log
"fetching student list"
url
=
$list_studs_btn
.
data
(
'endpoint'
)
if
$
(
e
.
target
).
data
'csv'
url
+=
'/csv'
location
.
href
=
url
else
@
reset_display
()
$
.
getJSON
url
,
(
data
)
=>
# setup SlickGrid
options
=
enableCellNavigation
:
true
enableColumnReorder
:
false
columns
=
({
id
:
feature
,
field
:
feature
,
name
:
feature
}
for
feature
in
data
.
queried_features
)
grid_data
=
data
.
students
$table_placeholder
=
$
'<div/>'
,
class
:
'slickgrid'
@
$display_table
.
append
$table_placeholder
grid
=
new
Slick
.
Grid
(
$table_placeholder
,
grid_data
,
columns
,
options
)
grid
.
autosizeColumns
()
$grade_config_btn
=
@
$section
.
find
(
"input[name='dump-gradeconf']'"
)
$grade_config_btn
.
click
(
e
)
=>
log
"fetching grading config"
url
=
$grade_config_btn
.
data
(
'endpoint'
)
$
.
getJSON
url
,
(
data
)
=>
@
reset_display
()
@
$display_text
.
html
data
[
'grading_config_summary'
]
reset_display
:
->
@
$display_text
.
empty
()
@
$display_table
.
empty
()
# exports
_
.
defaults
window
,
InstructorDashboard
:
{}
_
.
defaults
window
.
InstructorDashboard
,
sections
:
{}
_
.
defaults
window
.
InstructorDashboard
.
sections
,
DataDownload
:
DataDownload
lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee
0 → 100644
View file @
5cdf3fb8
# Instructor Dashboard Tab Manager
log
=
->
console
.
log
.
apply
console
,
arguments
plantTimeout
=
(
ms
,
cb
)
->
setTimeout
cb
,
ms
CSS_INSTRUCTOR_CONTENT
=
'instructor-dashboard-content-2'
CSS_ACTIVE_SECTION
=
'active-section'
CSS_IDASH_SECTION
=
'idash-section'
CSS_INSTRUCTOR_NAV
=
'instructor-nav'
HASH_LINK_PREFIX
=
'#view-'
# once we're ready, check if this page has the instructor dashboard
$
=>
instructor_dashboard_content
=
$
".
#{
CSS_INSTRUCTOR_CONTENT
}
"
if
instructor_dashboard_content
.
length
!=
0
log
"setting up instructor dashboard"
setup_instructor_dashboard
instructor_dashboard_content
setup_instructor_dashboard_sections
instructor_dashboard_content
# enable links
setup_instructor_dashboard
=
(
idash_content
)
=>
links
=
idash_content
.
find
(
".
#{
CSS_INSTRUCTOR_NAV
}
"
).
find
(
'a'
)
# setup section header click handlers
for
link
in
(
$
link
for
link
in
links
)
link
.
click
(
e
)
->
# deactivate (styling) all sections
idash_content
.
find
(
".
#{
CSS_IDASH_SECTION
}
"
).
removeClass
CSS_ACTIVE_SECTION
idash_content
.
find
(
".
#{
CSS_INSTRUCTOR_NAV
}
"
).
children
().
removeClass
CSS_ACTIVE_SECTION
# find paired section
section_name
=
$
(
this
).
data
'section'
section
=
idash_content
.
find
"#
#{
section_name
}
"
section
.
data
(
'wrapper'
)
?
.
onClickTitle
?
()
# activate (styling) active
section
.
addClass
CSS_ACTIVE_SECTION
$
(
this
).
addClass
CSS_ACTIVE_SECTION
# write deep link
location
.
hash
=
"
#{
HASH_LINK_PREFIX
}#{
section_name
}
"
log
"clicked
#{
section_name
}
"
e
.
preventDefault
()
# recover deep link from url
# click default or go to section specified by hash
if
(
new
RegExp
"^
#{
HASH_LINK_PREFIX
}
"
).
test
location
.
hash
rmatch
=
(
new
RegExp
"^
#{
HASH_LINK_PREFIX
}
(.*)"
).
exec
location
.
hash
section_name
=
rmatch
[
1
]
link
=
links
.
filter
"[data-section='
#{
section_name
}
']"
link
.
click
()
else
links
.
eq
(
0
).
click
()
# call setup handlers for each section
setup_instructor_dashboard_sections
=
(
idash_content
)
->
log
"setting up instructor dashboard sections"
# fault isolation
# an error thrown in one section will not block other sections from exectuing
plantTimeout
0
,
->
new
window
.
InstructorDashboard
.
sections
.
DataDownload
idash_content
.
find
".
#{
CSS_IDASH_SECTION
}
#data_download"
plantTimeout
0
,
->
new
window
.
InstructorDashboard
.
sections
.
Membership
idash_content
.
find
".
#{
CSS_IDASH_SECTION
}
#membership"
plantTimeout
0
,
->
new
window
.
InstructorDashboard
.
sections
.
StudentAdmin
idash_content
.
find
".
#{
CSS_IDASH_SECTION
}
#student_admin"
plantTimeout
0
,
->
new
window
.
InstructorDashboard
.
sections
.
Analytics
idash_content
.
find
".
#{
CSS_IDASH_SECTION
}
#analytics"
lms/static/coffee/src/instructor_dashboard/membership.coffee
0 → 100644
View file @
5cdf3fb8
log
=
->
console
.
log
.
apply
console
,
arguments
plantTimeout
=
(
ms
,
cb
)
->
setTimeout
cb
,
ms
class
BatchEnrollment
constructor
:
(
@
$container
)
->
log
"setting up instructor dashboard subsection - batch enrollment"
$emails_input
=
@
$container
.
find
(
"textarea[name='student-emails']'"
)
$btn_enroll
=
@
$container
.
find
(
"input[name='enroll']'"
)
$btn_unenroll
=
@
$container
.
find
(
"input[name='unenroll']'"
)
$task_response
=
@
$container
.
find
(
".task-response"
)
$emails_input
.
click
->
log
'click $emails_input'
$btn_enroll
.
click
->
log
'click $btn_enroll'
$btn_unenroll
.
click
->
log
'click $btn_unenroll'
$btn_enroll
.
click
->
$
.
getJSON
$btn_enroll
.
data
(
'endpoint'
),
enroll
:
$emails_input
.
val
()
,
(
data
)
->
log
'received response for enroll button'
,
data
display_response
(
data
)
$btn_unenroll
.
click
->
log
'VAL'
,
$emails_input
.
val
()
$
.
getJSON
$btn_unenroll
.
data
(
'endpoint'
),
unenroll
:
$emails_input
.
val
()
,
(
data
)
->
# log 'received response for unenroll button', data
# display_response(data)
display_response
=
(
data_from_server
)
->
$task_response
.
empty
()
response_code_dict
=
_
.
extend
{},
data_from_server
.
enrolled
,
data_from_server
.
unenrolled
# response_code_dict e.g. {'code': ['email1', 'email2'], ...}
message_ordering
=
[
'msg_error_enroll'
'msg_error_unenroll'
'msg_enrolled'
'msg_unenrolled'
'msg_willautoenroll'
'msg_allowed'
'msg_disallowed'
'msg_already_enrolled'
'msg_notenrolled'
]
msg_to_txt
=
{
msg_already_enrolled
:
"Already enrolled:"
msg_enrolled
:
"Enrolled:"
msg_error_enroll
:
"There was an error enrolling these students:"
msg_allowed
:
"These students will be allowed to enroll once they register:"
msg_willautoenroll
:
"These students will be enrolled once they register:"
msg_unenrolled
:
"Unenrolled:"
msg_error_unenroll
:
"There was an error unenrolling these students:"
msg_disallowed
:
"These students were removed from those who can enroll once they register:"
msg_notenrolled
:
"These students were not enrolled:"
}
msg_to_codes
=
{
msg_already_enrolled
:
[
'user/ce/alreadyenrolled'
]
msg_enrolled
:
[
'user/!ce/enrolled'
]
msg_error_enroll
:
[
'user/!ce/rejected'
]
msg_allowed
:
[
'!user/cea/allowed'
,
'!user/!cea/allowed'
]
msg_willautoenroll
:
[
'!user/cea/willautoenroll'
,
'!user/!cea/willautoenroll'
]
msg_unenrolled
:
[
'ce/unenrolled'
]
msg_error_unenroll
:
[
'ce/rejected'
]
msg_disallowed
:
[
'cea/disallowed'
]
msg_notenrolled
:
[
'!ce/notenrolled'
]
}
for
msg_symbol
in
message_ordering
# $task_response.text JSON.stringify(data)
msg_txt
=
msg_to_txt
[
msg_symbol
]
task_res_section
=
$
'<div/>'
,
class
:
'task-res-section'
task_res_section
.
append
$
'<h3/>'
,
text
:
msg_txt
email_list
=
$
'<ul/>'
task_res_section
.
append
email_list
will_attach
=
false
for
code
in
msg_to_codes
[
msg_symbol
]
log
'logging code'
,
code
emails
=
response_code_dict
[
code
]
log
'emails'
,
emails
if
emails
and
emails
.
length
for
email
in
emails
log
'logging email'
,
email
email_list
.
append
$
'<li/>'
,
text
:
email
will_attach
=
true
if
will_attach
$task_response
.
append
task_res_section
else
task_res_section
.
remove
()
# manages a list of instructors or staff and the control of their access.
class
AuthorityList
# level is in ['instructor', 'staff']
constructor
:
(
@
$container
,
@
level
)
->
log
'setting up instructor dashboard subsection - authlist management for #{@level}'
@
$display_table
=
@
$container
.
find
(
'.auth-list-table'
)
$add_section
=
@
$container
.
find
(
'.auth-list-add'
)
$allow_field
=
$add_section
.
find
(
"input[name='email']"
)
$allow_button
=
$add_section
.
find
(
"input[name='allow']"
)
@
list_endpoint
=
@
$display_table
.
data
'endpoint'
@
access_change_endpoint
=
$add_section
.
data
'endpoint'
$allow_button
.
click
=>
@
access_change
(
$allow_field
.
val
(),
@
level
,
'allow'
,
@
reload_auth_list
)
$allow_field
.
val
''
@
reload_auth_list
()
reload_auth_list
:
=>
$
.
getJSON
@
list_endpoint
,
(
data
)
=>
log
data
@
$display_table
.
empty
()
options
=
enableCellNavigation
:
true
enableColumnReorder
:
false
columns
=
[
id
:
'username'
field
:
'username'
name
:
'Username'
,
id
:
'email'
field
:
'email'
name
:
'Email'
,
id
:
'revoke'
field
:
'revoke'
name
:
'Revoke'
formatter
:
(
row
,
cell
,
value
,
columnDef
,
dataContext
)
->
"<span class='revoke-link'>Revoke Access</span>"
]
table_data
=
data
[
@
level
]
log
'table_data'
,
table_data
$table_placeholder
=
$
'<div/>'
,
class
:
'slickgrid'
@
$display_table
.
append
$table_placeholder
log
'@$display_table'
,
$table_placeholder
grid
=
new
Slick
.
Grid
(
$table_placeholder
,
table_data
,
columns
,
options
)
grid
.
autosizeColumns
()
grid
.
onClick
.
subscribe
(
e
,
args
)
=>
item
=
args
.
grid
.
getDataItem
(
args
.
row
)
if
args
.
cell
is
2
@
access_change
(
item
.
email
,
@
level
,
'revoke'
,
@
reload_auth_list
)
access_change
:
(
email
,
level
,
mode
,
cb
)
->
url
=
@
access_change_endpoint
$
.
getJSON
@
access_change_endpoint
,
{
email
:
email
,
level
:
@
level
,
mode
:
mode
},
(
data
)
->
log
data
cb
?
()
class
Membership
constructor
:
(
@
$section
)
->
log
"setting up instructor dashboard section - membership"
@
$section
.
data
'wrapper'
,
@
# isolate sections from each other's errors.
plantTimeout
0
,
=>
@
batchenrollment
=
new
BatchEnrollment
@
$section
.
find
'.batch-enrollment'
plantTimeout
0
,
=>
@
stafflist
=
new
AuthorityList
(
@
$section
.
find
'.auth-list-container.auth-list-staff'
),
'staff'
plantTimeout
0
,
=>
@
instructorlist
=
new
AuthorityList
(
@
$section
.
find
'.auth-list-container.auth-list-instructor'
),
'instructor'
onClickTitle
:
->
@
stafflist
.
$display_table
.
empty
()
@
stafflist
.
reload_auth_list
()
@
instructorlist
.
$display_table
.
empty
()
@
instructorlist
.
reload_auth_list
()
# exports
_
.
defaults
window
,
InstructorDashboard
:
{}
_
.
defaults
window
.
InstructorDashboard
,
sections
:
{}
_
.
defaults
window
.
InstructorDashboard
.
sections
,
Membership
:
Membership
lms/static/coffee/src/instructor_dashboard/student_admin.coffee
0 → 100644
View file @
5cdf3fb8
log
=
->
console
.
log
.
apply
console
,
arguments
plantTimeout
=
(
ms
,
cb
)
->
setTimeout
cb
,
ms
class
StudentAdmin
constructor
:
(
@
$section
)
->
log
"setting up instructor dashboard section - student admin"
@
$student_email_field
=
@
$section
.
find
(
"input[name='student-select']"
)
@
$student_progress_link
=
@
$section
.
find
(
'a.progress-link'
)
@
$unenroll_btn
=
@
$section
.
find
(
"input[name='unenroll']"
)
@
$problem_select_field
=
@
$section
.
find
(
"input[name='problem-select']"
)
@
$reset_attempts_btn
=
@
$section
.
find
(
"input[name='reset-attempts']"
)
@
$delete_states_btn
=
@
$section
.
find
(
"input[name='delete-state']"
)
@
$student_progress_link
.
click
(
e
)
=>
e
.
preventDefault
()
email
=
@
$student_email_field
.
val
()
@
get_student_progress_link
email
,
success
:
(
data
)
->
log
'redirecting...'
window
.
location
=
data
.
progress_url
error
:
->
console
.
warn
'error getting student progress url for '
+
email
@
$unenroll_btn
.
click
=>
$
.
getJSON
@
$unenroll_btn
.
data
(
'endpoint'
),
unenroll
:
@
$student_email_field
.
val
(),
(
data
)
->
log
'data'
@
$reset_attempts_btn
.
click
=>
email
=
@
$student_email_field
.
val
()
problem_to_reset
=
@
$problem_select_field
.
val
()
@
reset_student_progress
email
,
problem_to_reset
,
false
,
success
:
->
log
'problem attempts reset!'
error
:
->
console
.
warn
'error resetting problem state'
@
$delete_states_btn
.
click
=>
email
=
@
$student_email_field
.
val
()
problem_to_reset
=
@
$problem_select_field
.
val
()
@
reset_student_progress
email
,
problem_to_reset
,
true
,
success
:
->
log
'problem state deleted!'
error
:
->
console
.
warn
'error deleting problem state'
# handler can be either a callback for success or a mapping e.g. {success: ->, error: ->, complete: ->}
get_student_progress_link
:
(
student_email
,
handler
)
->
settings
=
dataType
:
'json'
url
:
@
$student_progress_link
.
data
'endpoint'
data
:
student_email
:
student_email
if
typeof
handler
is
'function'
_
.
extend
settings
,
success
:
handler
else
_
.
extend
settings
,
handler
$
.
ajax
settings
# handler can be either a callback for success or a mapping e.g. {success: ->, error: ->, complete: ->}
reset_student_progress
:
(
student_email
,
problem_to_reset
,
delete_module
,
handler
)
->
settings
=
dataType
:
'json'
url
:
@
$reset_attempts_btn
.
data
'endpoint'
data
:
student_email
:
student_email
problem_to_reset
:
problem_to_reset
delete_module
:
delete_module
if
typeof
handler
is
'function'
_
.
extend
settings
,
success
:
handler
else
_
.
extend
settings
,
handler
$
.
ajax
settings
# exports
_
.
defaults
window
,
InstructorDashboard
:
{}
_
.
defaults
window
.
InstructorDashboard
,
sections
:
{}
_
.
defaults
window
.
InstructorDashboard
.
sections
,
StudentAdmin
:
StudentAdmin
lms/static/sass/course/instructor/_instructor_2.scss
View file @
5cdf3fb8
...
@@ -77,42 +77,62 @@
...
@@ -77,42 +77,62 @@
.instructor-dashboard-wrapper-2
section
.idash-section
#membership
{
.instructor-dashboard-wrapper-2
section
.idash-section
#membership
{
div
{
.vert-left
{
margin-top
:
2em
;
float
:
left
;
width
:
45%
;
}
}
textarea
{
.vert-right
{
height
:
100px
;
float
:
right
;
width
:
500px
;
width
:
45%
;
.auth-list-container
{
margin-bottom
:
1
.5em
;
.auth-list-table
{
.slickgrid
{
height
:
250px
;
}
}
.auth-list-add
{
margin-top
:
0
.5em
;
}
}
}
}
.task-res-section
{
.batch-enrollment
{
h3
{
textarea
{
color
:
#646464
;
height
:
100px
;
width
:
500px
;
}
}
ul
{
.task-res-section
{
padding
:
0
;
margin-top
:
1
.5em
;
margin
:
0
;
margin-top
:
0
.5em
;
h3
{
line-height
:
1
.5em
;
color
:
#646464
;
list-style-type
:
none
;
}
li
{
ul
{
padding
:
0
;
margin
:
0
;
margin-top
:
0
.5em
;
line-height
:
1
.5em
;
list-style-type
:
none
;
li
{
}
}
}
}
}
}
}
}
}
.instructor-dashboard-wrapper-2
section
.idash-section
#student_admin
{
.instructor-dashboard-wrapper-2
section
.idash-section
#student_admin
>
{
.h-row
{
h3
{
margin-top
:
2em
;
}
margin-bottom
:
1em
;
input
{
margin-top
:
2em
;
}
clear
:
both
;
a
{
margin-top
:
2em
;
}
}
p
input
select
{
float
:
left
;
}
}
}
...
...
lms/templates/courseware/instructor_dashboard_2/membership.html
View file @
5cdf3fb8
<
%
page
args=
"section_data"
/>
<
%
page
args=
"section_data"
/>
<div>
<div
class=
"vert-left batch-enrollment"
>
<h2>
Batch Enrollment
</h2>
<h2>
Batch Enrollment
</h2>
<p>
Enter student emails separated by new lines or commas.
</p>
<p>
Enter student emails separated by new lines or commas.
</p>
<textarea
rows=
"6"
cols=
"70"
name=
"student-emails"
>
Student Emails
</textarea>
<textarea
rows=
"6"
cols=
"70"
name=
"student-emails"
placeholder=
"Student Emails"
></textarea>
<br>
<input
type=
"button"
name=
"enroll"
value=
"Enroll"
data-endpoint=
"${ section_data['enroll_button_url'] }"
>
<input
type=
"button"
name=
"enroll"
value=
"Enroll"
data-endpoint=
"${ section_data['enroll_button_url'] }"
>
<input
type=
"button"
name=
"unenroll"
value=
"Unenroll"
data-endpoint=
"${ section_data['unenroll_button_url'] }"
>
<input
type=
"button"
name=
"unenroll"
value=
"Unenroll"
data-endpoint=
"${ section_data['unenroll_button_url'] }"
>
<div
class=
"task-response"
></div>
<div
class=
"task-response"
></div>
</div>
</div>
<div
class=
"vert-right instructor-staff-management"
>
<div
class=
"auth-list-container auth-list-staff"
>
<h2>
Staff Management
</h2>
<div
class=
"auth-list-table"
data-endpoint=
"${ section_data['list_instructors_staff_url'] }"
></div>
<div
class=
"auth-list-add"
data-endpoint=
"${ section_data['access_allow_revoke_url'] }"
>
<input
type=
"text"
name=
"email"
placeholder=
"Enter Email"
spellcheck=
"false"
>
<input
type=
"button"
name=
"allow"
value=
"Grant Staff Access"
>
</div>
</div>
<div
class=
"auth-list-container auth-list-instructor"
>
<h2>
Instructor Management
</h2>
<div
class=
"auth-list-table"
data-endpoint=
"${ section_data['list_instructors_staff_url'] }"
></div>
<div
class=
"auth-list-add"
data-endpoint=
"${ section_data['access_allow_revoke_url'] }"
>
<input
type=
"text"
name=
"email"
placeholder=
"Enter Email"
spellcheck=
"false"
>
<input
type=
"button"
name=
"allow"
value=
"Grant Instructor Access"
>
</div>
</div>
</div>
lms/templates/courseware/instructor_dashboard_2/student_admin.html
View file @
5cdf3fb8
<
%
page
args=
"section_data"
/>
<
%
page
args=
"section_data"
/>
<div
class=
"h-row"
>
<h3>
Select student
</h3>
<p>
Select student
</p>
<input
type=
"text"
name=
"student-select"
placeholder=
"Student Email"
>
<input
type=
"text"
name=
"student-select"
value=
"Jerry Smort"
>
<br>
</div>
##
<p>
grade
</p>
##
##
<p>
85 (B)
</p>
<div
class=
"h-row"
>
<a
href=
""
class=
"progress-link"
data-endpoint=
"${ section_data['get_student_progress_url'] }"
>
Student Progress Page
</a>
<p>
grade
</p>
<br>
<p>
85 (B)
</p>
<input
type=
"button"
name=
"unenroll"
value=
"Unenroll"
data-endpoint=
"${ section_data['unenroll_button_url'] }"
>
</div>
##
<select
class=
"problems"
>
##
##
<option>
Getting problems...
</option>
<div
class=
"h-row"
>
##
</select>
<a
href=
""
class=
"progress-link"
>
progress link
</a>
<input
type=
"text"
name=
"problem-select"
placeholder=
"Problem URL-name"
>
<input
type=
"button"
name=
"unenroll"
value=
"Unenroll"
>
<input
type=
"button"
name=
"reset-attempts"
value=
"Reset Student Attempts"
data-endpoint=
"${ section_data['reset_student_attempts_url'] }"
>
</div>
<input
type=
"button"
name=
"delete-state"
value=
"Delete Student State"
data-endpoint=
"${ section_data['reset_student_attempts_url'] }"
>
##
<div
class=
"h-row"
>
<select
class=
"problems"
>
<option>
Getting problems...
</option>
</select>
<input
type=
"button"
name=
"reset-attempts"
value=
"Reset Student Attempts"
>
</div>
lms/urls.py
View file @
5cdf3fb8
...
@@ -257,6 +257,8 @@ if settings.COURSEWARE_ENABLED:
...
@@ -257,6 +257,8 @@ if settings.COURSEWARE_ENABLED:
# api endpoints for instructor
# api endpoints for instructor
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/enroll_unenroll$'
,
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/enroll_unenroll$'
,
'instructor.views.api.enroll_unenroll'
,
name
=
"enroll_unenroll"
),
'instructor.views.api.enroll_unenroll'
,
name
=
"enroll_unenroll"
),
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/list_instructors_staff$'
,
'instructor.views.api.list_instructors_staff'
,
name
=
"list_instructors_staff"
),
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/access_allow_revoke$'
,
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/access_allow_revoke$'
,
'instructor.views.api.access_allow_revoke'
,
name
=
"access_allow_revoke"
),
'instructor.views.api.access_allow_revoke'
,
name
=
"access_allow_revoke"
),
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/grading_config$'
,
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/grading_config$'
,
...
@@ -265,7 +267,10 @@ if settings.COURSEWARE_ENABLED:
...
@@ -265,7 +267,10 @@ if settings.COURSEWARE_ENABLED:
'instructor.views.api.enrolled_students_profiles'
,
name
=
"enrolled_students_profiles"
),
'instructor.views.api.enrolled_students_profiles'
,
name
=
"enrolled_students_profiles"
),
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/profile_distribution$'
,
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/profile_distribution$'
,
'instructor.views.api.profile_distribution'
,
name
=
"profile_distribution"
),
'instructor.views.api.profile_distribution'
,
name
=
"profile_distribution"
),
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/get_student_progress_url$'
,
'instructor.views.api.get_student_progress_url'
,
name
=
"get_student_progress_url"
),
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/reset_student_attempts$'
,
'instructor.views.api.reset_student_attempts'
,
name
=
"reset_student_attempts"
),
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/gradebook$'
,
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/gradebook$'
,
'instructor.views.legacy.gradebook'
,
name
=
'gradebook'
),
'instructor.views.legacy.gradebook'
,
name
=
'gradebook'
),
...
...
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