Commit 5cdf3fb8 by Miles Steele

add staff management subsection, add student_admin subsection, refactor sections

parent cc737cb1
......@@ -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
......
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
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
# 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"
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
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%;
.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; }
}
......
<%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>
<%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'),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment