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
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
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
):
"""
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.
"""
import
re
import
json
from
django.contrib.auth.models
import
User
from
student.models
import
CourseEnrollment
,
CourseEnrollmentAllowed
from
courseware.models
import
StudentModule
def
enroll_emails
(
course_id
,
student_emails
,
auto_enroll
=
False
):
...
...
@@ -136,3 +138,52 @@ def split_input_list(str_list):
new_list
=
[
s
for
s
in
new_list
if
s
!=
''
]
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
import
json
from
django_future.csrf
import
ensure_csrf_cookie
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
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.access
import
allow_access
,
revoke_access
from
instructor.access
import
allow_access
,
revoke_access
,
list_with_level
import
analytics.basic
import
analytics.distributions
import
analytics.csvs
...
...
@@ -27,6 +30,7 @@ import analytics.csvs
def
enroll_unenroll
(
request
,
course_id
):
"""
Enroll or unenroll students by email.
Requires staff access.
"""
course
=
get_course_with_access
(
request
.
user
,
course_id
,
'staff'
,
depth
=
None
)
...
...
@@ -48,7 +52,8 @@ def enroll_unenroll(request, course_id):
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
def
access_allow_revoke
(
request
,
course_id
):
"""
Modify staff/instructor access. (instructor available only)
Modify staff/instructor access.
Requires instructor access.
Query parameters:
email is the target users email
...
...
@@ -71,7 +76,33 @@ def access_allow_revoke(request, course_id):
raise
ValueError
(
"unrecognized mode '{}'"
.
format
(
mode
))
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"
)
return
response
...
...
@@ -178,3 +209,95 @@ def profile_distribution(request, course_id):
}
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
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):
'section_display_name'
:
'Membership'
,
'enroll_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
...
...
@@ -115,6 +117,9 @@ def _section_student_admin(course_id):
section_data
=
{
'section_key'
:
'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
...
...
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 @@
.instructor-dashboard-wrapper-2
section
.idash-section
#membership
{
div
{
margin-top
:
2em
;
.vert-left
{
float
:
left
;
width
:
45%
;
}
textarea
{
height
:
100px
;
width
:
500px
;
.vert-right
{
float
:
right
;
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
{
h3
{
color
:
#646464
;
.batch-enrollment
{
textarea
{
height
:
100px
;
width
:
500px
;
}
ul
{
padding
:
0
;
margin
:
0
;
margin-top
:
0
.5em
;
line-height
:
1
.5em
;
list-style-type
:
none
;
li
{
.task-res-section
{
margin-top
:
1
.5em
;
h3
{
color
:
#646464
;
}
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
{
.h-row
{
margin-bottom
:
1em
;
clear
:
both
;
}
p
input
select
{
float
:
left
;
}
.instructor-dashboard-wrapper-2
section
.idash-section
#student_admin
>
{
h3
{
margin-top
:
2em
;
}
input
{
margin-top
:
2em
;
}
a
{
margin-top
:
2em
;
}
}
...
...
lms/templates/courseware/instructor_dashboard_2/membership.html
View file @
5cdf3fb8
<
%
page
args=
"section_data"
/>
<div>
<div
class=
"vert-left batch-enrollment"
>
<h2>
Batch Enrollment
</h2>
<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=
"unenroll"
value=
"Unenroll"
data-endpoint=
"${ section_data['unenroll_button_url'] }"
>
<div
class=
"task-response"
></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"
/>
<div
class=
"h-row"
>
<p>
Select student
</p>
<input
type=
"text"
name=
"student-select"
value=
"Jerry Smort"
>
</div>
##
<div
class=
"h-row"
>
<p>
grade
</p>
<p>
85 (B)
</p>
</div>
##
<div
class=
"h-row"
>
<a
href=
""
class=
"progress-link"
>
progress link
</a>
<input
type=
"button"
name=
"unenroll"
value=
"Unenroll"
>
</div>
##
<div
class=
"h-row"
>
<select
class=
"problems"
>
<option>
Getting problems...
</option>
</select>
<input
type=
"button"
name=
"reset-attempts"
value=
"Reset Student Attempts"
>
</div>
<h3>
Select student
</h3>
<input
type=
"text"
name=
"student-select"
placeholder=
"Student Email"
>
<br>
##
<p>
grade
</p>
##
<p>
85 (B)
</p>
<a
href=
""
class=
"progress-link"
data-endpoint=
"${ section_data['get_student_progress_url'] }"
>
Student Progress Page
</a>
<br>
<input
type=
"button"
name=
"unenroll"
value=
"Unenroll"
data-endpoint=
"${ section_data['unenroll_button_url'] }"
>
##
<select
class=
"problems"
>
##
<option>
Getting problems...
</option>
##
</select>
<input
type=
"text"
name=
"problem-select"
placeholder=
"Problem URL-name"
>
<input
type=
"button"
name=
"reset-attempts"
value=
"Reset Student Attempts"
data-endpoint=
"${ section_data['reset_student_attempts_url'] }"
>
<input
type=
"button"
name=
"delete-state"
value=
"Delete Student State"
data-endpoint=
"${ section_data['reset_student_attempts_url'] }"
>
lms/urls.py
View file @
5cdf3fb8
...
...
@@ -257,6 +257,8 @@ if settings.COURSEWARE_ENABLED:
# api endpoints for instructor
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/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$'
,
'instructor.views.api.access_allow_revoke'
,
name
=
"access_allow_revoke"
),
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/grading_config$'
,
...
...
@@ -265,7 +267,10 @@ if settings.COURSEWARE_ENABLED:
'instructor.views.api.enrolled_students_profiles'
,
name
=
"enrolled_students_profiles"
),
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/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$'
,
'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