add staff management subsection, add student_admin subsection, refactor sections
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,281 +0,0 @@
|
||||
# Instructor Dashboard Tab Manager
|
||||
|
||||
log = -> console.log.apply console, arguments
|
||||
|
||||
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}"
|
||||
|
||||
# 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"
|
||||
setup_section_data_download idash_content.find(".#{CSS_IDASH_SECTION}#data_download")
|
||||
setup_section_membership idash_content.find(".#{CSS_IDASH_SECTION}#membership")
|
||||
setup_section_analytics idash_content.find(".#{CSS_IDASH_SECTION}#analytics")
|
||||
|
||||
|
||||
# setup the data download section
|
||||
setup_section_membership = (section) ->
|
||||
log "setting up instructor dashboard section - membership"
|
||||
|
||||
emails_input = section.find("textarea[name='student-emails']'")
|
||||
btn_enroll = section.find("input[name='enroll']'")
|
||||
btn_unenroll = section.find("input[name='unenroll']'")
|
||||
task_response = section.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 -> $.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()
|
||||
|
||||
|
||||
# setup the data download section
|
||||
setup_section_data_download = (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')
|
||||
|
||||
reset_display = ->
|
||||
display_text.empty()
|
||||
display_table.empty()
|
||||
|
||||
list_studs_btn = section.find("input[name='list-profiles']'")
|
||||
list_studs_btn.click (e) ->
|
||||
log "fetching student list"
|
||||
url = $(this).data('endpoint')
|
||||
if $(this).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 = $(this).data('endpoint')
|
||||
$.getJSON url, (data) ->
|
||||
reset_display()
|
||||
display_text.html data['grading_config_summary']
|
||||
|
||||
|
||||
# setup the analytics section
|
||||
setup_section_analytics = (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')
|
||||
|
||||
reset_display = ->
|
||||
display_text.empty()
|
||||
display_graph.empty()
|
||||
display_table.empty()
|
||||
|
||||
distribution_select = section.find('select#distributions')
|
||||
|
||||
# ask for available distributions
|
||||
$.getJSON distribution_select.data('endpoint'), features: JSON.stringify([]), (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
|
||||
|
||||
distribution_select.change ->
|
||||
opt = $(this).children('option:selected')
|
||||
log "distribution selected: #{opt.data 'feature'}"
|
||||
feature = opt.data 'feature'
|
||||
reset_display()
|
||||
$.getJSON distribution_select.data('endpoint'), features: JSON.stringify([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'
|
||||
# display_text.text JSON.stringify(feature_res.data)
|
||||
log feature_res.data
|
||||
|
||||
# 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
|
||||
|
||||
log grid_data
|
||||
|
||||
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]
|
||||
log graph_data
|
||||
|
||||
$.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)
|
||||
113
lms/static/coffee/src/instructor_dashboard/analytics.coffee
Normal file
113
lms/static/coffee/src/instructor_dashboard/analytics.coffee
Normal file
@@ -0,0 +1,113 @@
|
||||
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
|
||||
@@ -0,0 +1,54 @@
|
||||
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
|
||||
@@ -0,0 +1,69 @@
|
||||
# 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"
|
||||
183
lms/static/coffee/src/instructor_dashboard/membership.coffee
Normal file
183
lms/static/coffee/src/instructor_dashboard/membership.coffee
Normal file
@@ -0,0 +1,183 @@
|
||||
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
|
||||
@@ -0,0 +1,83 @@
|
||||
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
|
||||
@@ -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%;
|
||||
|
||||
.task-res-section {
|
||||
h3 {
|
||||
color: #646464;
|
||||
.auth-list-container {
|
||||
margin-bottom: 1.5em;
|
||||
|
||||
.auth-list-table {
|
||||
.slickgrid {
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-list-add {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-top: 0.5em;
|
||||
line-height: 1.5em;
|
||||
list-style-type: none;
|
||||
li {
|
||||
}
|
||||
|
||||
.batch-enrollment {
|
||||
textarea {
|
||||
height: 100px;
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.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; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
<%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>
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
<%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'] }">
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user