Commit dd43d663 by Miles Steele

add instructor tasks

parent 9bdff748
...@@ -140,7 +140,7 @@ def split_input_list(str_list): ...@@ -140,7 +140,7 @@ def split_input_list(str_list):
return new_list return new_list
def reset_student_attempts(course_id, student, problem_to_reset, delete_module=False): def reset_student_attempts(course_id, student, module_state_key, delete_module=False):
""" """
Reset student attempts for a problem. Optionally deletes all student state for the specified problem. Reset student attempts for a problem. Optionally deletes all student state for the specified problem.
...@@ -151,13 +151,6 @@ def reset_student_attempts(course_id, student, problem_to_reset, delete_module=F ...@@ -151,13 +151,6 @@ def reset_student_attempts(course_id, student, problem_to_reset, delete_module=F
problem_to_reset is the name of a problem e.g. 'L2Node1'. 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. 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, module_to_reset = StudentModule.objects.get(student_id=student.id,
course_id=course_id, course_id=course_id,
module_state_key=module_state_key) module_state_key=module_state_key)
...@@ -178,12 +171,3 @@ def _reset_module_attempts(studentmodule): ...@@ -178,12 +171,3 @@ def _reset_module_attempts(studentmodule):
# save # save
studentmodule.state = json.dumps(problem_state) studentmodule.state = json.dumps(problem_state)
studentmodule.save() 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')
...@@ -21,6 +21,7 @@ from django_comment_common.models import (Role, ...@@ -21,6 +21,7 @@ from django_comment_common.models import (Role,
FORUM_ROLE_COMMUNITY_TA) FORUM_ROLE_COMMUNITY_TA)
from courseware.models import StudentModule from courseware.models import StudentModule
import instructor_task.api
import instructor.enrollment as enrollment import instructor.enrollment as enrollment
from instructor.enrollment import split_input_list, enroll_emails, unenroll_emails from instructor.enrollment import split_input_list, enroll_emails, unenroll_emails
import instructor.access as access import instructor.access as access
...@@ -45,7 +46,7 @@ def students_update_enrollment_email(request, course_id): ...@@ -45,7 +46,7 @@ def students_update_enrollment_email(request, course_id):
action = request.GET.get('action', '') action = request.GET.get('action', '')
emails = split_input_list(request.GET.get('emails', '')) emails = split_input_list(request.GET.get('emails', ''))
auto_enroll = request.GET.get('auto_enroll', '') in ['true', 'Talse', True] auto_enroll = request.GET.get('auto_enroll', '') in ['true', 'True', True]
if action == 'enroll': if action == 'enroll':
results = enroll_emails(course_id, emails, auto_enroll=auto_enroll) results = enroll_emails(course_id, emails, auto_enroll=auto_enroll)
...@@ -293,32 +294,126 @@ def redirect_to_student_progress(request, course_id): ...@@ -293,32 +294,126 @@ def redirect_to_student_progress(request, course_id):
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def reset_student_attempts(request, course_id): def reset_student_attempts(request, course_id):
""" """
Resets a students attempts counter. Optionally deletes student state for a problem. Resets a students attempts counter or starts a task to reset all students attempts counters. Optionally deletes student state for a problem.
Limited to staff access. Limited to staff access.
Takes query parameter student_email Takes either of the following query paremeters
Takes query parameter problem_to_reset - problem_to_reset is a urlname of a problem
Takes query parameter delete_module - student_email is an email
- all_students is a boolean
- delete_module is a boolean
""" """
course = get_course_with_access(request.user, course_id, 'staff', depth=None) course = get_course_with_access(request.user, course_id, 'staff', depth=None)
problem_to_reset = request.GET.get('problem_to_reset')
student_email = request.GET.get('student_email') student_email = request.GET.get('student_email')
all_students = request.GET.get('all_students', False) in ['true', 'True', True]
will_delete_module = request.GET.get('delete_module', False) in ['true', 'True', True]
if not (problem_to_reset and (all_students or student_email)):
return HttpResponseBadRequest()
if will_delete_module and all_students:
return HttpResponseBadRequest()
module_state_key = _module_state_key_from_problem_urlname(course_id, problem_to_reset)
response_payload = {}
response_payload['problem_to_reset'] = problem_to_reset
if student_email:
student = User.objects.get(email=student_email)
try:
enrollment.reset_student_attempts(course_id, student, module_state_key, delete_module=will_delete_module)
except StudentModule.DoesNotExist:
return HttpResponseBadRequest()
elif all_students:
task = instructor_task.api.submit_reset_problem_attempts_for_all_students(request, course_id, module_state_key)
response_payload['task'] = 'created'
else:
return HttpResponseBadRequest()
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 rescore_problem(request, course_id):
"""
Starts a background process a students attempts counter. Optionally deletes student state for a problem.
Limited to staff access.
Takes either of the following query paremeters
- problem_to_reset is a urlname of a problem
- student_email is an email
- all_students is a boolean
all_students will be ignored if student_email is present
"""
course = get_course_with_access(request.user, course_id, 'staff', depth=None)
problem_to_reset = request.GET.get('problem_to_reset') problem_to_reset = request.GET.get('problem_to_reset')
will_delete_module = {'true': True}.get(request.GET.get('delete_module', ''), False) student_email = request.GET.get('student_email', False)
all_students = request.GET.get('all_students', '') in ['true', 'True', True]
if not student_email or not problem_to_reset: if not (problem_to_reset and (all_students or student_email)):
return HttpResponseBadRequest() return HttpResponseBadRequest()
user = User.objects.get(email=student_email) module_state_key = _module_state_key_from_problem_urlname(course_id, problem_to_reset)
try: response_payload = {}
enrollment.reset_student_attempts(course_id, user, problem_to_reset, delete_module=will_delete_module) response_payload['problem_to_reset'] = problem_to_reset
except StudentModule.DoesNotExist:
if student_email:
response_payload['student_email'] = student_email
student = User.objects.get(email=student_email)
task = instructor_task.api.submit_rescore_problem_for_student(request, course_id, module_state_key, student)
response_payload['task'] = 'created'
elif all_students:
task = instructor_task.api.submit_rescore_problem_for_all_students(request, course_id, module_state_key)
response_payload['task'] = 'created'
else:
return HttpResponseBadRequest()
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_instructor_tasks(request, course_id):
"""
List instructor tasks.
Limited to instructor access.
Takes either of the following query paremeters
- (optional) problem_urlname (same format as problem_to_reset in other api methods)
- (optional) student_email
"""
course = get_course_with_access(request.user, course_id, 'instructor', depth=None)
problem_urlname = request.GET.get('problem_urlname', False)
student_email = request.GET.get('student_email', False)
if student_email and not problem_urlname:
return HttpResponseBadRequest() return HttpResponseBadRequest()
if problem_urlname:
module_state_key = _module_state_key_from_problem_urlname(course_id, problem_urlname)
if student_email:
student = User.objects.get(email=student_email)
tasks = instructor_task.api.get_instructor_task_history(course_id, module_state_key, student)
else:
tasks = instructor_task.api.get_instructor_task_history(course_id, module_state_key)
else:
tasks = instructor_task.api.get_running_instructor_tasks(course_id)
def extract_task_features(task):
FEATURES = ['task_type', 'task_input', 'task_id', 'requester', 'created', 'task_state']
return dict((feature, str(getattr(task, feature))) for feature in FEATURES)
response_payload = { response_payload = {
'course_id': course_id, 'tasks': map(extract_task_features, tasks),
'delete_module': will_delete_module,
} }
response = HttpResponse(json.dumps(response_payload), content_type="application/json") response = HttpResponse(json.dumps(response_payload), content_type="application/json")
return response return response
...@@ -395,3 +490,14 @@ def update_forum_role_membership(request, course_id): ...@@ -395,3 +490,14 @@ def update_forum_role_membership(request, course_id):
} }
response = HttpResponse(json.dumps(response_payload), content_type="application/json") response = HttpResponse(json.dumps(response_payload), content_type="application/json")
return response return response
def _module_state_key_from_problem_urlname(course_id, urlname):
if urlname[-4:] == ".xml":
urlname = urlname[:-4]
urlname = "problem/" + urlname
(org, course_name, _) = course_id.split("/")
module_state_key = "i4x://" + org + "/" + course_name + "/" + urlname
return module_state_key
...@@ -95,11 +95,14 @@ def _section_course_info(course_id): ...@@ -95,11 +95,14 @@ def _section_course_info(course_id):
section_data['enrollment_count'] = CourseEnrollment.objects.filter(course_id=course_id).count() section_data['enrollment_count'] = CourseEnrollment.objects.filter(course_id=course_id).count()
section_data['has_started'] = course.has_started() section_data['has_started'] = course.has_started()
section_data['has_ended'] = course.has_ended() section_data['has_ended'] = course.has_ended()
section_data['grade_cutoffs'] = "[" + reduce(lambda memo, (letter, score): "{}: {}, ".format(letter, score) + memo , course.grade_cutoffs.items(), "")[:-2] + "]" try:
section_data['offline_grades'] = offline_grades_available(course_id) section_data['grade_cutoffs'] = "" + reduce(lambda memo, (letter, score): "{}: {}, ".format(letter, score) + memo, course.grade_cutoffs.items(), "")[:-2] + ""
except:
section_data['grade_cutoffs'] = "Not Available"
# section_data['offline_grades'] = offline_grades_available(course_id)
try: try:
section_data['course_errors'] = [(escape(a), '') for (a,b) in modulestore().get_item_errors(course.location)] section_data['course_errors'] = [(escape(a), '') for (a, b) in modulestore().get_item_errors(course.location)]
except Exception: except Exception:
section_data['course_errors'] = [('Error fetching errors', '')] section_data['course_errors'] = [('Error fetching errors', '')]
...@@ -111,9 +114,7 @@ def _section_membership(course_id, access): ...@@ -111,9 +114,7 @@ def _section_membership(course_id, access):
section_data = { section_data = {
'section_key': 'membership', 'section_key': 'membership',
'section_display_name': 'Membership', 'section_display_name': 'Membership',
'access': access, 'access': access,
'enroll_button_url': reverse('students_update_enrollment_email', kwargs={'course_id': course_id}), 'enroll_button_url': reverse('students_update_enrollment_email', kwargs={'course_id': course_id}),
'unenroll_button_url': reverse('students_update_enrollment_email', kwargs={'course_id': course_id}), 'unenroll_button_url': reverse('students_update_enrollment_email', kwargs={'course_id': course_id}),
'list_course_role_members_url': reverse('list_course_role_members', kwargs={'course_id': course_id}), 'list_course_role_members_url': reverse('list_course_role_members', kwargs={'course_id': course_id}),
...@@ -130,8 +131,10 @@ def _section_student_admin(course_id): ...@@ -130,8 +131,10 @@ def _section_student_admin(course_id):
'section_key': 'student_admin', 'section_key': 'student_admin',
'section_display_name': 'Student Admin', 'section_display_name': 'Student Admin',
'get_student_progress_url': reverse('get_student_progress_url', kwargs={'course_id': course_id}), 'get_student_progress_url': reverse('get_student_progress_url', kwargs={'course_id': course_id}),
'unenroll_button_url': reverse('students_update_enrollment_email', kwargs={'course_id': course_id}), 'enrollment_url': reverse('students_update_enrollment_email', kwargs={'course_id': course_id}),
'reset_student_attempts_url': reverse('reset_student_attempts', kwargs={'course_id': course_id}), 'reset_student_attempts_url': reverse('reset_student_attempts', kwargs={'course_id': course_id}),
'rescore_problem_url': reverse('rescore_problem', kwargs={'course_id': course_id}),
'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_id}),
} }
return section_data return section_data
......
...@@ -56,6 +56,7 @@ class Analytics ...@@ -56,6 +56,7 @@ class Analytics
options = options =
enableCellNavigation: true enableCellNavigation: true
enableColumnReorder: false enableColumnReorder: false
forceFitColumns: true
columns = [ columns = [
id: feature id: feature
...@@ -76,7 +77,7 @@ class Analytics ...@@ -76,7 +77,7 @@ class Analytics
table_placeholder = $ '<div/>', class: 'slickgrid' table_placeholder = $ '<div/>', class: 'slickgrid'
@$display_table.append table_placeholder @$display_table.append table_placeholder
grid = new Slick.Grid(table_placeholder, grid_data, columns, options) grid = new Slick.Grid(table_placeholder, grid_data, columns, options)
grid.autosizeColumns() # grid.autosizeColumns()
else if feature is 'year_of_birth' else if feature is 'year_of_birth'
graph_placeholder = $ '<div/>', class: 'year-of-birth' graph_placeholder = $ '<div/>', class: 'year-of-birth'
@$display_graph.append graph_placeholder @$display_graph.append graph_placeholder
......
...@@ -24,6 +24,7 @@ class DataDownload ...@@ -24,6 +24,7 @@ class DataDownload
options = options =
enableCellNavigation: true enableCellNavigation: true
enableColumnReorder: false enableColumnReorder: false
forceFitColumns: true
columns = ({id: feature, field: feature, name: feature} for feature in data.queried_features) columns = ({id: feature, field: feature, name: feature} for feature in data.queried_features)
grid_data = data.students grid_data = data.students
...@@ -31,7 +32,7 @@ class DataDownload ...@@ -31,7 +32,7 @@ class DataDownload
$table_placeholder = $ '<div/>', class: 'slickgrid' $table_placeholder = $ '<div/>', class: 'slickgrid'
@$display_table.append $table_placeholder @$display_table.append $table_placeholder
grid = new Slick.Grid($table_placeholder, grid_data, columns, options) grid = new Slick.Grid($table_placeholder, grid_data, columns, options)
grid.autosizeColumns() # grid.autosizeColumns()
$grade_config_btn = @$section.find("input[name='dump-gradeconf']'") $grade_config_btn = @$section.find("input[name='dump-gradeconf']'")
$grade_config_btn.click (e) => $grade_config_btn.click (e) =>
......
...@@ -3,6 +3,20 @@ ...@@ -3,6 +3,20 @@
log = -> console.log.apply console, arguments log = -> console.log.apply console, arguments
plantTimeout = (ms, cb) -> setTimeout cb, ms plantTimeout = (ms, cb) -> setTimeout cb, ms
# # intercepts a jquery method
# # calls the original method after callback
# intercept_jquery_method = (method_name, callback) ->
# original = jQuery.fn[method_name]
# jQuery.fn[method_name] = ->
# callback.apply this, arguments
# original.apply this, arguments
# intercept_jquery_method 'on', (event_name) ->
# this.addClass "has-event-handler-for-#{event_name}"
CSS_INSTRUCTOR_CONTENT = 'instructor-dashboard-content-2' CSS_INSTRUCTOR_CONTENT = 'instructor-dashboard-content-2'
CSS_ACTIVE_SECTION = 'active-section' CSS_ACTIVE_SECTION = 'active-section'
CSS_IDASH_SECTION = 'idash-section' CSS_IDASH_SECTION = 'idash-section'
...@@ -42,7 +56,9 @@ setup_instructor_dashboard = (idash_content) => ...@@ -42,7 +56,9 @@ setup_instructor_dashboard = (idash_content) =>
# write deep link # write deep link
location.hash = "#{HASH_LINK_PREFIX}#{section_name}" location.hash = "#{HASH_LINK_PREFIX}#{section_name}"
log "clicked section #{section_name}"
plantTimeout 0, -> section.data('wrapper')?.onClickTitle?() plantTimeout 0, -> section.data('wrapper')?.onClickTitle?()
# plantTimeout 0, -> section.data('wrapper')?.onExit?()
# recover deep link from url # recover deep link from url
# click default or go to section specified by hash # click default or go to section specified by hash
...@@ -51,8 +67,12 @@ setup_instructor_dashboard = (idash_content) => ...@@ -51,8 +67,12 @@ setup_instructor_dashboard = (idash_content) =>
section_name = rmatch[1] section_name = rmatch[1]
link = links.filter "[data-section='#{section_name}']" link = links.filter "[data-section='#{section_name}']"
link.click() link.click()
link.data('wrapper')?.onClickTitle?()
else else
links.eq(0).click() link = links.eq(0)
link.click()
link.data('wrapper')?.onClickTitle?()
# call setup handlers for each section # call setup handlers for each section
...@@ -60,7 +80,7 @@ setup_instructor_dashboard_sections = (idash_content) -> ...@@ -60,7 +80,7 @@ setup_instructor_dashboard_sections = (idash_content) ->
log "setting up instructor dashboard sections" log "setting up instructor dashboard sections"
# fault isolation # fault isolation
# an error thrown in one section will not block other sections from exectuing # an error thrown in one section will not block other sections from exectuing
plantTimeout 0, -> new window.InstructorDashboard.sections.CourseInfo idash_content.find ".#{CSS_IDASH_SECTION}#course_info" plantTimeout 0, -> new window.InstructorDashboard.sections.CourseInfo idash_content.find ".#{CSS_IDASH_SECTION}#course_info"
plantTimeout 0, -> new window.InstructorDashboard.sections.DataDownload idash_content.find ".#{CSS_IDASH_SECTION}#data_download" 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.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.StudentAdmin idash_content.find ".#{CSS_IDASH_SECTION}#student_admin"
......
...@@ -126,7 +126,10 @@ class AuthList ...@@ -126,7 +126,10 @@ class AuthList
options = options =
enableCellNavigation: true enableCellNavigation: true
enableColumnReorder: false enableColumnReorder: false
# autoHeight: true
forceFitColumns: true
WHICH_CELL_IS_REVOKE = 3
columns = [ columns = [
id: 'username' id: 'username'
field: 'username' field: 'username'
...@@ -136,6 +139,14 @@ class AuthList ...@@ -136,6 +139,14 @@ class AuthList
field: 'email' field: 'email'
name: 'Email' name: 'Email'
, ,
id: 'first_name'
field: 'first_name'
name: 'First Name'
,
# id: 'last_name'
# field: 'last_name'
# name: 'Last Name'
# ,
id: 'revoke' id: 'revoke'
field: 'revoke' field: 'revoke'
name: 'Revoke' name: 'Revoke'
...@@ -148,11 +159,11 @@ class AuthList ...@@ -148,11 +159,11 @@ class AuthList
$table_placeholder = $ '<div/>', class: 'slickgrid' $table_placeholder = $ '<div/>', class: 'slickgrid'
@$display_table.append $table_placeholder @$display_table.append $table_placeholder
grid = new Slick.Grid($table_placeholder, table_data, columns, options) grid = new Slick.Grid($table_placeholder, table_data, columns, options)
grid.autosizeColumns() # grid.autosizeColumns()
grid.onClick.subscribe (e, args) => grid.onClick.subscribe (e, args) =>
item = args.grid.getDataItem(args.row) item = args.grid.getDataItem(args.row)
if args.cell is 2 if args.cell is WHICH_CELL_IS_REVOKE
@access_change(item.email, @rolename, 'revoke', @reload_auth_list) @access_change(item.email, @rolename, 'revoke', @reload_auth_list)
# slickgrid collapses when rendered in an invisible div # slickgrid collapses when rendered in an invisible div
......
log = -> console.log.apply console, arguments log = -> console.log.apply console, arguments
plantTimeout = (ms, cb) -> setTimeout cb, ms plantTimeout = (ms, cb) -> setTimeout cb, ms
plantInterval = (ms, cb) -> setInterval cb, ms
std_ajax_err = (handler) -> (jqXHR, textStatus, errorThrown) ->
console.warn """ajax error
textStatus: #{textStatus}
errorThrown: #{errorThrown}"""
handler.apply this, arguments
create_task_list_table = ($table_tasks, tasks_data) ->
$table_tasks.empty()
options =
enableCellNavigation: true
enableColumnReorder: false
autoHeight: true
rowHeight: 60
forceFitColumns: true
columns = [
id: 'task_type'
field: 'task_type'
name: 'Task Type'
,
id: 'requester'
field: 'requester'
name: 'Requester'
width: 30
,
id: 'task_input'
field: 'task_input'
name: 'Input'
,
id: 'task_state'
field: 'task_state'
name: 'State'
width: 30
,
id: 'task_id'
field: 'task_id'
name: 'Task ID'
width: 50
,
id: 'created'
field: 'created'
name: 'Created'
]
table_data = tasks_data
$table_placeholder = $ '<div/>', class: 'slickgrid'
$table_tasks.append $table_placeholder
grid = new Slick.Grid($table_placeholder, table_data, columns, options)
class StudentAdmin class StudentAdmin
constructor: (@$section) -> constructor: (@$section) ->
log "setting up instructor dashboard section - student admin" log "setting up instructor dashboard section - student admin"
@$section.data 'wrapper', @
# get jquery element and assert its existance
# for debugging
find_and_assert = ($root, selector) ->
item = $root.find selector
if item.length != 1
console.error "element selection failed for '#{selector}' resulted in length #{item.length}"
throw "Failed Element Selection"
else
item
# collect buttons
@$field_student_select = find_and_assert @$section, "input[name='student-select']"
@$progress_link = find_and_assert @$section, "a.progress-link"
@$btn_enroll = find_and_assert @$section, "input[name='enroll']"
@$btn_unenroll = find_and_assert @$section, "input[name='unenroll']"
@$field_problem_select_single = find_and_assert @$section, "input[name='problem-select-single']"
@$btn_reset_attempts_single = find_and_assert @$section, "input[name='reset-attempts-single']"
@$btn_delete_state_single = find_and_assert @$section, "input[name='delete-state-single']"
@$btn_rescore_problem_single = find_and_assert @$section, "input[name='rescore-problem-single']"
@$btn_task_history_single = find_and_assert @$section, "input[name='task-history-single']"
@$table_task_history_single = find_and_assert @$section, ".task-history-single-table"
@$field_problem_select_all = find_and_assert @$section, "input[name='problem-select-all']"
@$btn_reset_attempts_all = find_and_assert @$section, "input[name='reset-attempts-all']"
@$btn_rescore_problem_all = find_and_assert @$section, "input[name='rescore-problem-all']"
@$btn_task_history_all = find_and_assert @$section, "input[name='task-history-all']"
@$table_task_history_all = find_and_assert @$section, ".task-history-all-table"
@$table_running_tasks = find_and_assert @$section, ".running-tasks-table"
@$student_email_field = @$section.find("input[name='student-select']") @start_refresh_running_task_poll_loop()
@$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) => # go to student progress page
@$progress_link.click (e) =>
e.preventDefault() e.preventDefault()
email = @$student_email_field.val() email = @$field_student_select.val()
@get_student_progress_link email,
$.ajax
dataType: 'json'
url: @$progress_link.data 'endpoint'
data: student_email: email
success: (data) -> success: (data) ->
log 'redirecting...' log 'redirecting...'
window.location = data.progress_url window.location = data.progress_url
error: -> error: std_ajax_err -> console.warn 'error getting student progress url for ' + email
console.warn 'error getting student progress url for ' + email
# enroll student
@$btn_enroll.click =>
send_data =
action: 'enroll'
emails: @$field_student_select.val()
auto_enroll: false
$.ajax
dataType: 'json'
url: @$btn_unenroll.data 'endpoint'
data: send_data
success: -> console.log "student #{send_data.emails} enrolled"
error: std_ajax_err -> console.warn 'error enrolling student'
@$unenroll_btn.click => # unenroll student
@$btn_unenroll.click =>
send_data = send_data =
action: 'unenroll' action: 'unenroll'
emails: @$student_email_field.val() emails: @$field_student_select.val()
auto_enroll: false auto_enroll: false
$.getJSON @$unenroll_btn.data('endpoint'), send_data, (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' $.ajax
_.extend settings, success: handler dataType: 'json'
else url: @$btn_unenroll.data 'endpoint'
_.extend settings, handler data: send_data
success: -> console.log "student #{send_data.emails} unenrolled"
error: std_ajax_err -> console.warn 'error unenrolling student'
# reset attempts for student on problem
@$btn_reset_attempts_single.click =>
send_data =
student_email: @$field_student_select.val()
problem_to_reset: @$field_problem_select_single.val()
delete_module: false
$.ajax
dataType: 'json'
url: @$btn_reset_attempts_single.data 'endpoint'
data: send_data
success: -> log 'problem attempts reset'
error: std_ajax_err -> console.warn 'error resetting problem state'
# delete state for student on problem
@$btn_delete_state_single.click =>
send_data =
student_email: @$field_student_select.val()
problem_to_reset: @$field_problem_select_single.val()
delete_module: true
$.ajax
dataType: 'json'
url: @$btn_delete_state_single.data 'endpoint'
data: send_data
success: -> log 'module state deleted'
error: std_ajax_err -> console.warn 'error deleting problem state'
# start task to rescore problem for student
@$btn_rescore_problem_single.click =>
send_data =
student_email: @$field_student_select.val()
problem_to_reset: @$field_problem_select_single.val()
$.ajax
dataType: 'json'
url: @$btn_rescore_problem_single.data 'endpoint'
data: send_data
success: -> log 'started rescore problem task'
error: std_ajax_err -> console.warn 'error starting rescore problem (single student) task'
# list task history for student+problem
@$btn_task_history_single.click =>
send_data =
student_email: @$field_student_select.val()
problem_urlname: @$field_problem_select_single.val()
if not send_data.student_email then return
if not send_data.problem_urlname then return
$.ajax
dataType: 'json'
url: @$btn_task_history_single.data 'endpoint'
data: send_data
success: (data) =>
create_task_list_table @$table_task_history_single, data.tasks
error: std_ajax_err -> console.warn 'error listing task history for student+problem'
# start task to reset attempts on problem for all students
@$btn_reset_attempts_all.click =>
send_data =
all_students: true
problem_to_reset: @$field_problem_select_all.val()
$.ajax
dataType: 'json'
url: @$btn_reset_attempts_all.data 'endpoint'
data: send_data
success: -> log 'started reset attempts task'
error: std_ajax_err (jqXHR, textStatus, errorThrown) ->
console.warn "error starting reset attempts (all students) task"
# start task to rescore problem for all students
@$btn_rescore_problem_all.click =>
send_data =
all_students: true
problem_to_reset: @$field_problem_select_all.val()
$.ajax
dataType: 'json'
url: @$btn_rescore_problem_all.data 'endpoint'
data: send_data
success: -> log 'started rescore problem task'
error: std_ajax_err (jqXHR, textStatus, errorThrown) ->
console.warn "error starting rescore problem (all students) task"
# list task history for problem
@$btn_task_history_all.click =>
send_data =
problem_urlname: @$field_problem_select_all.val()
if not send_data.problem_urlname then return
$.ajax settings $.ajax
dataType: 'json'
url: @$btn_task_history_all.data 'endpoint'
data: send_data
success: (data) =>
create_task_list_table @$table_task_history_all, data.tasks
error: std_ajax_err -> console.warn 'error listing task history for student+problem'
# handler can be either a callback for success or a mapping e.g. {success: ->, error: ->, complete: ->} reload_running_tasks_list: =>
reset_student_progress: (student_email, problem_to_reset, delete_module, handler) -> list_endpoint = @$table_running_tasks.data 'endpoint'
settings = $.ajax
dataType: 'json' dataType: 'json'
url: @$reset_attempts_btn.data 'endpoint' url: list_endpoint
data: success: (data) => create_task_list_table @$table_running_tasks, data.tasks
student_email: student_email error: std_ajax_err -> console.warn "error listing all instructor tasks"
problem_to_reset: problem_to_reset
delete_module: delete_module
if typeof handler is 'function' start_refresh_running_task_poll_loop: ->
_.extend settings, success: handler @reload_running_tasks_list()
else if @$section.hasClass 'active-section'
_.extend settings, handler plantTimeout 5000, => @start_refresh_running_task_poll_loop()
$.ajax settings onClickTitle: ->
@start_refresh_running_task_poll_loop()
# onExit: ->
# clearInterval @reload_running_task_list_slot
# exports # exports
......
...@@ -15,8 +15,26 @@ ...@@ -15,8 +15,26 @@
width: 100%; width: 100%;
// position: relative; // position: relative;
.slick-header-column { // .has-event-handler-for-click {
height: 100%; // border: 1px solid blue;
// }
.slickgrid {
font-family: verdana,arial,sans-serif;
font-size:11px;
color:#333333;
margin-left: 1px;
.slick-header-column {
// height: 100%;
}
.slick-cell {
white-space: normal;
word-wrap: break-word;
border: 1px dotted silver;
border-collapse: collapse;
}
} }
h1 { h1 {
...@@ -157,9 +175,33 @@ ...@@ -157,9 +175,33 @@
.instructor-dashboard-wrapper-2 section.idash-section#student_admin > { .instructor-dashboard-wrapper-2 section.idash-section#student_admin > {
h3 { margin-top: 2em; } .progress-link-wrapper {
input { margin-top: 2em; } margin-top: 0.7em;
a { margin-top: 2em; } }
// .task-history-single-table { .slickgrid
// max-height: 500px;
// } }
// .running-tasks-table { .slickgrid {
// max-height: 500px;
// } }
.task-history-all-table {
margin-top: 1em;
// height: 300px;
// overflow-y: scroll;
}
.task-history-single-table {
margin-top: 1em;
// height: 300px;
// overflow-y: scroll;
}
.running-tasks-table {
margin-top: 1em;
// height: 500px;
// overflow-y: scroll;
}
} }
......
...@@ -102,7 +102,7 @@ function goto( mode) ...@@ -102,7 +102,7 @@ function goto( mode)
<section class="container"> <section class="container">
<div class="instructor-dashboard-wrapper"> <div class="instructor-dashboard-wrapper">
<div class="beta-button-wrapper"><a href="${ beta_dashboard_url }"> Beta Dashboard </a></div> <div class="beta-button-wrapper"><a href="${ beta_dashboard_url }"> Try New Beta Dashboard </a></div>
<section class="instructor-dashboard-content"> <section class="instructor-dashboard-content">
<h1>${_("Instructor Dashboard")}</h1> <h1>${_("Instructor Dashboard")}</h1>
......
...@@ -32,10 +32,10 @@ ...@@ -32,10 +32,10 @@
${ section_data['grade_cutoffs'] } ${ section_data['grade_cutoffs'] }
</div> </div>
<div class="basic-data"> ## <div class="basic-data">
Offline Grades Available: ## Offline Grades Available:
${ section_data['offline_grades'] } ## ${ section_data['offline_grades'] }
</div> ## </div>
%if len(section_data['course_errors']): %if len(section_data['course_errors']):
<div class="course-errors-wrapper"> <div class="course-errors-wrapper">
......
...@@ -3,10 +3,11 @@ ...@@ -3,10 +3,11 @@
<input type="button" name="list-profiles" value="List enrolled students with profile information" data-endpoint="${ section_data['enrolled_students_profiles_url'] }" > <input type="button" name="list-profiles" value="List enrolled students with profile information" data-endpoint="${ section_data['enrolled_students_profiles_url'] }" >
<input type="button" name="list-profiles" value="CSV" data-csv="true" class="csv" data-endpoint="${ section_data['enrolled_students_profiles_url'] }" > <input type="button" name="list-profiles" value="CSV" data-csv="true" class="csv" data-endpoint="${ section_data['enrolled_students_profiles_url'] }" >
<br> <br>
<input type="button" name="list-grades" value="Student grades"> ## <input type="button" name="list-grades" value="Student grades">
<br> ## <input type="button" name="list-profiles" value="CSV" data-csv="true" class="csv">
<input type="button" name="list-answer-distributions" value="Answer distributions (x students got y points)"> ## <br>
<br> ## <input type="button" name="list-answer-distributions" value="Answer distributions (x students got y points)">
## <br>
<input type="button" name="dump-gradeconf" value="Grading Configuration" data-endpoint="${ section_data['grading_config_url'] }"> <input type="button" name="dump-gradeconf" value="Grading Configuration" data-endpoint="${ section_data['grading_config_url'] }">
<div class="data-display"> <div class="data-display">
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
<section class="container"> <section class="container">
<div class="instructor-dashboard-wrapper-2"> <div class="instructor-dashboard-wrapper-2">
<div class="olddash-button-wrapper"><a href="${ old_dashboard_url }"> Standard Dashboard </a></div> <div class="olddash-button-wrapper"><a href="${ old_dashboard_url }"> Back to Standard Dashboard </a></div>
<section class="instructor-dashboard-content-2"> <section class="instructor-dashboard-content-2">
<h1>Instructor Dashboard</h1> <h1>Instructor Dashboard</h1>
......
<%page args="section_data"/> <%page args="section_data"/>
<h3> Select student </h3> <H2>Student-specific grade adjustment</h2>
<input type="text" name="student-select" placeholder="Student Email"> <input type="text" name="student-select" placeholder="Student Email">
<br> <br>
## <p>grade</p>
## <p>85 (B)</p> <div class="progress-link-wrapper">
<a href="" class="progress-link" data-endpoint="${ section_data['get_student_progress_url'] }">Student Progress Page</a> <a href="" class="progress-link" data-endpoint="${ section_data['get_student_progress_url'] }">Student Progress Page</a>
</div>
<br> <br>
<input type="button" name="unenroll" value="Unenroll" data-endpoint="${ section_data['unenroll_button_url'] }"> <input type="button" name="enroll" value="Enroll" data-endpoint="${ section_data['enrollment_url'] }">
<input type="button" name="unenroll" value="Unenroll" data-endpoint="${ section_data['enrollment_url'] }">
## <select class="problems"> ## <select class="problems">
## <option>Getting problems...</option> ## <option>Getting problems...</option>
## </select> ## </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'] }"> <p> Specify a particular problem in the course here by its url: </p>
<input type="button" name="delete-state" value="Delete Student State" data-endpoint="${ section_data['reset_student_attempts_url'] }"> <input type="text" name="problem-select-single" placeholder="Problem urlname">
<p>
You may use just the "urlname" if a problem, or "modulename/urlname" if not.
(For example, if the location is <tt>i4x://university/course/problem/problemname</tt>,
then just provide the <tt>problemname</tt>.
If the location is <tt>i4x://university/course/notaproblem/someothername</tt>, then
provide <tt>notaproblem/someothername</tt>.)
</p>
<input type="button" name="reset-attempts-single" value="Reset Student Attempts" data-endpoint="${ section_data['reset_student_attempts_url'] }">
<p> You may also delete the entire state of a student for the specified module: </p>
<input type="button" name="delete-state-single" value="Delete Student State for Module" data-endpoint="${ section_data['reset_student_attempts_url'] }">
<input type="button" name="rescore-problem-single" value="Rescore Student Submission" data-endpoint="${ section_data['rescore_problem_url'] }">
<p>
Rescoring runs in the background, and status for active tasks will appear in a table below.
To see status for all tasks submitted for this course and student, click on this button:
</p>
<input type="button" name="task-history-single" value="Show Background Task History for Student" data-endpoint="${ section_data['list_instructor_tasks_url'] }">
<div class="task-history-single-table"></div>
<hr>
<H2>Course-specific grade adjustment</h2>
<p>
Specify a particular problem in the course here by its url:
<input type="text" name="problem-select-all" size="60">
</p>
<p>
You may use just the "urlname" if a problem, or "modulename/urlname" if not.
(For example, if the location is <tt>i4x://university/course/problem/problemname</tt>,
then just provide the <tt>problemname</tt>.
If the location is <tt>i4x://university/course/notaproblem/someothername</tt>, then
provide <tt>notaproblem/someothername</tt>.)
</p>
<p>
Then select an action:
<input type="button" name="reset-attempts-all" value="Reset ALL students' attempts" data-endpoint="${ section_data['reset_student_attempts_url'] }">
<input type="button" name="rescore-problem-all" value="Rescore ALL students' problem submissions" data-endpoint="${ section_data['rescore_problem_url'] }">
</p>
<p>
<p>These actions run in the background, and status for active tasks will appear in a table below.
To see status for all tasks submitted for this problem, click on this button:
</p>
<input type="button" name="task-history-all" value="Show Background Task History for Problem" data-endpoint="${ section_data['list_instructor_tasks_url'] }">
<div class="task-history-all-table"></div>
</p>
<hr>
<h2> Pending Instructor Tasks </h2>
<div class="running-tasks-table" data-endpoint="${ section_data['list_instructor_tasks_url'] }"></div>
...@@ -271,6 +271,10 @@ if settings.COURSEWARE_ENABLED: ...@@ -271,6 +271,10 @@ if settings.COURSEWARE_ENABLED:
'instructor.views.api.get_student_progress_url', name="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$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/reset_student_attempts$',
'instructor.views.api.reset_student_attempts', name="reset_student_attempts"), 'instructor.views.api.reset_student_attempts', name="reset_student_attempts"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/rescore_problem$',
'instructor.views.api.rescore_problem', name="rescore_problem"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/list_instructor_tasks$',
'instructor.views.api.list_instructor_tasks', name="list_instructor_tasks"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/list_forum_members$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/list_forum_members$',
'instructor.views.api.list_forum_members', name="list_forum_members"), 'instructor.views.api.list_forum_members', name="list_forum_members"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/update_forum_role_membership$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/update_forum_role_membership$',
......
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