Commit e05d0f97 by Miles Steele

fix permissions, fix ENABLE_INSTRUCTOR_BACKGROUND_TASKS usage, fix csv download

parent c6d6698a
......@@ -3,7 +3,6 @@ Instructor Dashboard API views
Non-html views which the instructor dashboard requests.
TODO add tracking
TODO a lot of these GETs should be PUTs
"""
......@@ -14,6 +13,7 @@ from django.views.decorators.cache import cache_control
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseBadRequest
from courseware.access import has_access
from courseware.courses import get_course_with_access
from django.contrib.auth.models import User
from django_comment_common.models import (Role,
......@@ -32,7 +32,10 @@ import analytics.csvs
def common_exceptions_400(func):
""" Catches common exceptions and renders matching 400 errors. (decorator) """
"""
Catches common exceptions and renders matching 400 errors.
(decorator without arguments)
"""
def wrapped(*args, **kwargs):
try:
return func(*args, **kwargs)
......@@ -44,6 +47,8 @@ def common_exceptions_400(func):
def require_query_params(*args, **kwargs):
"""
Checks for required paremters or renders a 400 error.
(decorator with arguments)
`args` is a *list of required GET parameter names.
`kwargs` is a **dict of required GET parameter names
to string explanations of the parameter
......@@ -193,12 +198,12 @@ def access_allow_revoke(request, course_id):
def list_course_role_members(request, course_id):
"""
List instructors and staff.
Requires staff access.
Requires instructor access.
rolename is one of ['instructor', 'staff', 'beta']
"""
course = get_course_with_access(
request.user, course_id, 'staff', depth=None
request.user, course_id, 'instructor', depth=None
)
rolename = request.GET.get('rolename')
......@@ -232,7 +237,7 @@ def grading_config(request, course_id):
"""
Respond with json which contains a html formatted grade summary.
TODO maybe this shouldn't be html already
TODO this shouldn't be html already
"""
course = get_course_with_access(
request.user, course_id, 'staff', depth=None
......@@ -282,7 +287,7 @@ def enrolled_students_features(request, course_id, csv=False):
)
return response
else:
header, datarows = analytics.csvs.format_dictlist(student_data)
header, datarows = analytics.csvs.format_dictlist(student_data, query_features)
return analytics.csvs.create_csv_response("enrolled_profiles.csv", header, datarows)
......@@ -378,46 +383,16 @@ def get_student_progress_url(request, course_id):
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@common_exceptions_400
@require_query_params(student_email="student email")
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')
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)
@common_exceptions_400
def reset_student_attempts(request, course_id):
"""
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. Some sub-methods limited to instructor 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
- delete_module is a boolean
- all_students is a boolean (requires instructor access) (mutually exclusive with delete_module)
- delete_module is a boolean (requires instructor access) (mutually exclusive with all_students)
"""
course = get_course_with_access(
request.user, course_id, 'staff', depth=None
......@@ -426,13 +401,18 @@ def reset_student_attempts(request, course_id):
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]
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:
if delete_module and all_students:
return HttpResponseBadRequest()
# require instructor access for some queries
if all_students or delete_module:
if not has_access(request.user, course, 'instructor'):
HttpResponseBadRequest("requires instructor accesss.")
module_state_key = _msk_from_problem_urlname(course_id, problem_to_reset)
response_payload = {}
......@@ -441,7 +421,7 @@ def reset_student_attempts(request, course_id):
if student_email:
try:
student = User.objects.get(email=student_email)
enrollment.reset_student_attempts(course_id, student, module_state_key, delete_module=will_delete_module)
enrollment.reset_student_attempts(course_id, student, module_state_key, delete_module=delete_module)
except StudentModule.DoesNotExist:
return HttpResponseBadRequest("Module does not exist.")
elif all_students:
......@@ -462,7 +442,7 @@ def reset_student_attempts(request, course_id):
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.
Limited to instructor access.
Takes either of the following query paremeters
- problem_to_reset is a urlname of a problem
......@@ -472,7 +452,7 @@ def rescore_problem(request, course_id):
all_students will be ignored if student_email is present
"""
course = get_course_with_access(
request.user, course_id, 'staff', depth=None
request.user, course_id, 'instructor', depth=None
)
problem_to_reset = request.GET.get('problem_to_reset')
......
......@@ -45,7 +45,7 @@ def instructor_dashboard_2(request, course_id):
sections = [
_section_course_info(course_id),
_section_membership(course_id, access),
_section_student_admin(course_id),
_section_student_admin(course_id, access),
_section_data_download(course_id),
_section_analytics(course_id),
]
......@@ -115,11 +115,12 @@ def _section_membership(course_id, access):
return section_data
def _section_student_admin(course_id):
def _section_student_admin(course_id, access):
""" Provide data for the corresponding dashboard section """
section_data = {
'section_key': 'student_admin',
'section_display_name': 'Student Admin',
'access': access,
'get_student_progress_url': reverse('get_student_progress_url', kwargs={'course_id': course_id}),
'enrollment_url': reverse('students_update_enrollment', kwargs={'course_id': course_id}),
'reset_student_attempts_url': reverse('reset_student_attempts', kwargs={'course_id': course_id}),
......
......@@ -144,6 +144,9 @@ MITX_FEATURES = {
# Enable instructor dash to submit background tasks
'ENABLE_INSTRUCTOR_BACKGROUND_TASKS': True,
# Enable instructor dash beta version link
'ENABLE_INSTRUCTOR_BETA_DASHBOARD': False,
# Allow use of the hint managment instructor view.
'ENABLE_HINTER_INSTRUCTOR_VIEW': False,
......
......@@ -272,6 +272,7 @@ class Membership
@$list_selector.change =>
$opt = @$list_selector.children('option:selected')
return unless $opt.length > 0
for auth_list in @auth_lists
auth_list.$container.removeClass 'active'
auth_list = $opt.data('auth_list')
......
......@@ -9,6 +9,14 @@ std_ajax_err = (handler) -> (jqXHR, textStatus, errorThrown) ->
errorThrown: #{errorThrown}"""
handler.apply this, arguments
# get jquery element and assert its existance
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
create_task_list_table = ($table_tasks, tasks_data) ->
$table_tasks.empty()
......@@ -61,39 +69,32 @@ class StudentAdmin
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
# some buttons are optional because they can be flipped by the instructor task feature switch
@$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"
@$btn_delete_state_single = @$section.find "input[name='delete-state-single']"
@$btn_rescore_problem_single = @$section.find "input[name='rescore-problem-single']"
@$btn_task_history_single = @$section.find "input[name='task-history-single']"
@$table_task_history_single = @$section.find ".task-history-single-table"
# course-specific level buttons
@$field_problem_select_all = @$section.find "input[name='problem-select-all']"
@$btn_reset_attempts_all = @$section.find "input[name='reset-attempts-all']"
@$btn_rescore_problem_all = @$section.find "input[name='rescore-problem-all']"
@$btn_task_history_all = @$section.find "input[name='task-history-all']"
@$table_task_history_all = @$section.find ".task-history-all-table"
@$table_running_tasks = @$section.find ".running-tasks-table"
@$request_response_error_single = find_and_assert @$section, ".student-specific-container .request-response-error"
@$request_response_error_all = find_and_assert @$section, ".course-specific-container .request-response-error"
@$request_response_error_all = @$section.find ".course-specific-container .request-response-error"
@start_refresh_running_task_poll_loop()
if @$table_running_tasks.length > 0
@start_refresh_running_task_poll_loop()
# go to student progress page
@$progress_link.click (e) =>
......@@ -118,7 +119,7 @@ class StudentAdmin
$.ajax
dataType: 'json'
url: @$btn_unenroll.data 'endpoint'
url: @$btn_enroll.data 'endpoint'
data: send_data
success: @clear_errors_then -> console.log "student #{send_data.emails} enrolled"
error: std_ajax_err => @$request_response_error_single.text "Error enrolling student '#{send_data.emails}'."
......@@ -259,7 +260,8 @@ class StudentAdmin
cb?.apply this, arguments
onClickTitle: ->
@start_refresh_running_task_poll_loop()
if @$table_running_tasks.length > 0
@start_refresh_running_task_poll_loop()
# onExit: ->
# clearInterval @reload_running_task_list_slot
......
......@@ -169,14 +169,16 @@
.instructor-dashboard-wrapper-2 section.idash-section#membership {
$half_width: $baseline * 20;
.vert-left {
float: left;
width: 47%;
width: $half_width;
}
.vert-right {
float: right;
width: 47%;
width: $half_width;
}
select {
......@@ -197,7 +199,7 @@
position: absolute;
display: none;
padding: $baseline;
width: $baseline * 25;
width: $half_width;
border: 1px solid $light-gray;
background-color: $white;
......
......@@ -3,7 +3,7 @@
<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" placeholder="Student Emails" spellcheck="false"></textarea>
<textarea rows="6" cols="50" name="student-emails" placeholder="Student Emails" spellcheck="false"></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'] }" >
......@@ -20,40 +20,42 @@
</div>
<div class="vert-right member-lists-management">
<h2> Member List Management </h2>
<h2> Administration List Management </h2>
<select id="member-lists-selector">
<option> Getting available lists... </option>
</select>
<div class="auth-list-container" data-rolename="staff" data-display-name="Staff">
<div class="auth-list-table" data-endpoint="${ section_data['list_course_role_members_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 class="request-response-error"></div>
</div>
%if section_data['access']['instructor']:
<div class="auth-list-container" data-rolename="instructor" data-display-name="Instructors">
<div class="auth-list-container" data-rolename="staff" data-display-name="Staff">
<div class="auth-list-table" data-endpoint="${ section_data['list_course_role_members_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">
<input type="button" name="allow" value="Grant Staff Access">
</div>
<div class="request-response-error"></div>
</div>
%endif
<div class="auth-list-container" data-rolename="beta" data-display-name="Beta Testers">
<div class="auth-list-table" data-endpoint="${ section_data['list_course_role_members_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 Beta Tester Access">
%if section_data['access']['instructor']:
<div class="auth-list-container" data-rolename="instructor" data-display-name="Instructors">
<div class="auth-list-table" data-endpoint="${ section_data['list_course_role_members_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 class="request-response-error"></div>
</div>
%endif
<div class="auth-list-container" data-rolename="beta" data-display-name="Beta Testers">
<div class="auth-list-table" data-endpoint="${ section_data['list_course_role_members_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 Beta Tester Access">
</div>
<div class="request-response-error"></div>
</div>
<div class="request-response-error"></div>
</div>
%endif
%if section_data['access']['forum_admin']:
<div class="auth-list-container" data-rolename="Administrator" data-display-name="Forum Admins">
......
......@@ -28,54 +28,63 @@
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" class="molly-guard" 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'] }">
%if section_data['access']['instructor']:
<p> You may also delete the entire state of a student for the specified module: </p>
<input type="button" class="molly-guard" name="delete-state-single" value="Delete Student State for Module" data-endpoint="${ section_data['reset_student_attempts_url'] }">
%endif
<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>
%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS') and section_data['access']['instructor']:
<input type="button" name="rescore-problem-single" value="Rescore Student Submission" data-endpoint="${ section_data['rescore_problem_url'] }">
%endif
%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS') and section_data['access']['instructor']:
<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>
<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>
%endif
</div>
<hr>
%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS') and section_data['access']['instructor']:
<hr>
<div class="course-specific-container">
<H2>Course-specific grade adjustment</h2>
<div class="request-response-error"></div>
<div class="course-specific-container">
<H2>Course-specific grade adjustment</h2>
<div class="request-response-error"></div>
<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" class="molly-guard" name="reset-attempts-all" value="Reset ALL students' attempts" data-endpoint="${ section_data['reset_student_attempts_url'] }">
<input type="button" class="molly-guard" 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>
</div>
<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" class="molly-guard" name="reset-attempts-all" value="Reset ALL students' attempts" data-endpoint="${ section_data['reset_student_attempts_url'] }">
<input type="button" class="molly-guard" 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>
</div>
<hr>
<hr>
<div class="running-tasks-container">
<h2> Pending Instructor Tasks </h2>
<div class="running-tasks-table" data-endpoint="${ section_data['list_instructor_tasks_url'] }"></div>
</div>
<div class="running-tasks-container">
<h2> Pending Instructor Tasks </h2>
<div class="running-tasks-table" data-endpoint="${ section_data['list_instructor_tasks_url'] }"></div>
</div>
%endif
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