Commit dd43d663 by Miles Steele

add instructor tasks

parent 9bdff748
......@@ -140,7 +140,7 @@ def split_input_list(str_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.
......@@ -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'.
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)
......@@ -178,12 +171,3 @@ def _reset_module_attempts(studentmodule):
# 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')
......@@ -21,6 +21,7 @@ from django_comment_common.models import (Role,
FORUM_ROLE_COMMUNITY_TA)
from courseware.models import StudentModule
import instructor_task.api
import instructor.enrollment as enrollment
from instructor.enrollment import split_input_list, enroll_emails, unenroll_emails
import instructor.access as access
......@@ -45,7 +46,7 @@ def students_update_enrollment_email(request, course_id):
action = request.GET.get('action', '')
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':
results = enroll_emails(course_id, emails, auto_enroll=auto_enroll)
......@@ -293,32 +294,126 @@ def redirect_to_student_progress(request, course_id):
@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.
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.
Takes query parameter student_email
Takes query parameter problem_to_reset
Takes query parameter delete_module
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
- delete_module is a boolean
"""
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')
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')
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()
user = User.objects.get(email=student_email)
module_state_key = _module_state_key_from_problem_urlname(course_id, problem_to_reset)
try:
enrollment.reset_student_attempts(course_id, user, problem_to_reset, delete_module=will_delete_module)
except StudentModule.DoesNotExist:
response_payload = {}
response_payload['problem_to_reset'] = problem_to_reset
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()
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 = {
'course_id': course_id,
'delete_module': will_delete_module,
'tasks': map(extract_task_features, tasks),
}
response = HttpResponse(json.dumps(response_payload), content_type="application/json")
return response
......@@ -395,3 +490,14 @@ def update_forum_role_membership(request, course_id):
}
response = HttpResponse(json.dumps(response_payload), content_type="application/json")
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):
section_data['enrollment_count'] = CourseEnrollment.objects.filter(course_id=course_id).count()
section_data['has_started'] = course.has_started()
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] + "]"
section_data['offline_grades'] = offline_grades_available(course_id)
try:
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:
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:
section_data['course_errors'] = [('Error fetching errors', '')]
......@@ -111,9 +114,7 @@ def _section_membership(course_id, access):
section_data = {
'section_key': 'membership',
'section_display_name': 'Membership',
'access': access,
'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}),
'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):
'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('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}),
'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
......
......@@ -56,6 +56,7 @@ class Analytics
options =
enableCellNavigation: true
enableColumnReorder: false
forceFitColumns: true
columns = [
id: feature
......@@ -76,7 +77,7 @@ class Analytics
table_placeholder = $ '<div/>', class: 'slickgrid'
@$display_table.append table_placeholder
grid = new Slick.Grid(table_placeholder, grid_data, columns, options)
grid.autosizeColumns()
# grid.autosizeColumns()
else if feature is 'year_of_birth'
graph_placeholder = $ '<div/>', class: 'year-of-birth'
@$display_graph.append graph_placeholder
......
......@@ -24,6 +24,7 @@ class DataDownload
options =
enableCellNavigation: true
enableColumnReorder: false
forceFitColumns: true
columns = ({id: feature, field: feature, name: feature} for feature in data.queried_features)
grid_data = data.students
......@@ -31,7 +32,7 @@ class DataDownload
$table_placeholder = $ '<div/>', class: 'slickgrid'
@$display_table.append $table_placeholder
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.click (e) =>
......
......@@ -3,6 +3,20 @@
log = -> console.log.apply console, arguments
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_ACTIVE_SECTION = 'active-section'
CSS_IDASH_SECTION = 'idash-section'
......@@ -42,7 +56,9 @@ setup_instructor_dashboard = (idash_content) =>
# write deep link
location.hash = "#{HASH_LINK_PREFIX}#{section_name}"
log "clicked section #{section_name}"
plantTimeout 0, -> section.data('wrapper')?.onClickTitle?()
# plantTimeout 0, -> section.data('wrapper')?.onExit?()
# recover deep link from url
# click default or go to section specified by hash
......@@ -51,8 +67,12 @@ setup_instructor_dashboard = (idash_content) =>
section_name = rmatch[1]
link = links.filter "[data-section='#{section_name}']"
link.click()
link.data('wrapper')?.onClickTitle?()
else
links.eq(0).click()
link = links.eq(0)
link.click()
link.data('wrapper')?.onClickTitle?()
# call setup handlers for each section
......@@ -60,7 +80,7 @@ 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.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.Membership idash_content.find ".#{CSS_IDASH_SECTION}#membership"
plantTimeout 0, -> new window.InstructorDashboard.sections.StudentAdmin idash_content.find ".#{CSS_IDASH_SECTION}#student_admin"
......
......@@ -126,7 +126,10 @@ class AuthList
options =
enableCellNavigation: true
enableColumnReorder: false
# autoHeight: true
forceFitColumns: true
WHICH_CELL_IS_REVOKE = 3
columns = [
id: 'username'
field: 'username'
......@@ -136,6 +139,14 @@ class AuthList
field: 'email'
name: 'Email'
,
id: 'first_name'
field: 'first_name'
name: 'First Name'
,
# id: 'last_name'
# field: 'last_name'
# name: 'Last Name'
# ,
id: 'revoke'
field: 'revoke'
name: 'Revoke'
......@@ -148,11 +159,11 @@ class AuthList
$table_placeholder = $ '<div/>', class: 'slickgrid'
@$display_table.append $table_placeholder
grid = new Slick.Grid($table_placeholder, table_data, columns, options)
grid.autosizeColumns()
# grid.autosizeColumns()
grid.onClick.subscribe (e, args) =>
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)
# slickgrid collapses when rendered in an invisible div
......
......@@ -15,8 +15,26 @@
width: 100%;
// position: relative;
.slick-header-column {
height: 100%;
// .has-event-handler-for-click {
// 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 {
......@@ -157,9 +175,33 @@
.instructor-dashboard-wrapper-2 section.idash-section#student_admin > {
h3 { margin-top: 2em; }
input { margin-top: 2em; }
a { margin-top: 2em; }
.progress-link-wrapper {
margin-top: 0.7em;
}
// .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)
<section class="container">
<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">
<h1>${_("Instructor Dashboard")}</h1>
......
......@@ -32,10 +32,10 @@
${ section_data['grade_cutoffs'] }
</div>
<div class="basic-data">
Offline Grades Available:
${ section_data['offline_grades'] }
</div>
## <div class="basic-data">
## Offline Grades Available:
## ${ section_data['offline_grades'] }
## </div>
%if len(section_data['course_errors']):
<div class="course-errors-wrapper">
......
......@@ -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="CSV" data-csv="true" class="csv" data-endpoint="${ section_data['enrolled_students_profiles_url'] }" >
<br>
<input type="button" name="list-grades" value="Student grades">
<br>
<input type="button" name="list-answer-distributions" value="Answer distributions (x students got y points)">
<br>
## <input type="button" name="list-grades" value="Student grades">
## <input type="button" name="list-profiles" value="CSV" data-csv="true" class="csv">
## <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'] }">
<div class="data-display">
......
......@@ -27,7 +27,7 @@
<section class="container">
<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">
<h1>Instructor Dashboard</h1>
......
<%page args="section_data"/>
<h3> Select student </h3>
<H2>Student-specific grade adjustment</h2>
<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>
<div class="progress-link-wrapper">
<a href="" class="progress-link" data-endpoint="${ section_data['get_student_progress_url'] }">Student Progress Page</a>
</div>
<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">
## <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'] }">
<p> Specify a particular problem in the course here by its url: </p>
<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:
'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>[^/]+/[^/]+/[^/]+)/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$',
'instructor.views.api.list_forum_members', name="list_forum_members"),
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