Commit cb4025b1 by Sarina Canelake

Merge pull request #1539 from edx/sarina/inst-dash-tasks

Enable Pending Tasks on beta dash // Course Info prettifying 
parents eb1b9260 b86e9129
......@@ -5,6 +5,7 @@ Unit tests for instructor.api methods.
import unittest
import json
import requests
import datetime
from urllib import quote
from django.test import TestCase
from nose.tools import raises
......@@ -761,6 +762,18 @@ class TestInstructorSendEmail(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.assertEqual(response.status_code, 400)
class MockCompletionInfo(object):
"""Mock for get_task_completion_info"""
times_called = 0
def mock_get_task_completion_info(self, *args): # pylint: disable=unused-argument
"""Mock for get_task_completion_info"""
self.times_called += 1
if self.times_called % 2 == 0:
return True, 'Task Completed'
return False, 'Task Errored In Some Way'
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
......@@ -769,15 +782,46 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
class FakeTask(object):
""" Fake task object """
FEATURES = ['task_type', 'task_input', 'task_id', 'requester', 'created', 'task_state']
FEATURES = [
'task_type',
'task_input',
'task_id',
'requester',
'task_state',
'created',
'status',
'task_message',
'duration_sec'
]
def __init__(self):
def __init__(self, completion):
for feature in self.FEATURES:
setattr(self, feature, 'expected')
# created needs to be a datetime
self.created = datetime.datetime(2013, 10, 25, 11, 42, 35)
# set 'status' and 'task_message' attrs
success, task_message = completion()
if success:
self.status = "Complete"
else:
self.status = "Incomplete"
self.task_message = task_message
# Set 'task_output' attr, which will be parsed to the 'duration_sec' attr.
self.task_output = '{"duration_ms": 1035000}'
self.duration_sec = 1035000 / 1000.0
def make_invalid_output(self):
"""Munge task_output to be invalid json"""
self.task_output = 'HI MY NAME IS INVALID JSON'
# This should be given the value of 'unknown' if the task output
# can't be properly parsed
self.duration_sec = 'unknown'
def to_dict(self):
""" Convert fake task to dictionary representation. """
return {key: 'expected' for key in self.FEATURES}
attr_dict = {key: getattr(self, key) for key in self.FEATURES}
attr_dict['created'] = attr_dict['created'].isoformat()
return attr_dict
def setUp(self):
self.instructor = AdminFactory.create()
......@@ -797,58 +841,78 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
),
state=json.dumps({'attempts': 10}),
)
mock_factory = MockCompletionInfo()
self.tasks = [self.FakeTask(mock_factory.mock_get_task_completion_info) for _ in xrange(7)]
self.tasks[-1].make_invalid_output()
self.tasks = [self.FakeTask() for _ in xrange(6)]
def tearDown(self):
"""
Undo all patches.
"""
patch.stopall()
@patch.object(instructor_task.api, 'get_running_instructor_tasks')
def test_list_instructor_tasks_running(self, act):
""" Test list of all running tasks. """
act.return_value = self.tasks
url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id})
mock_factory = MockCompletionInfo()
with patch('instructor.views.api.get_task_completion_info') as mock_completion_info:
mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info
response = self.client.get(url, {})
print response.content
self.assertEqual(response.status_code, 200)
# check response
self.assertTrue(act.called)
expected_tasks = [ftask.to_dict() for ftask in self.tasks]
expected_res = {'tasks': expected_tasks}
self.assertEqual(json.loads(response.content), expected_res)
actual_tasks = json.loads(response.content)['tasks']
for exp_task, act_task in zip(expected_tasks, actual_tasks):
self.assertDictEqual(exp_task, act_task)
self.assertEqual(actual_tasks, expected_tasks)
@patch.object(instructor_task.api, 'get_instructor_task_history')
def test_list_instructor_tasks_problem(self, act):
""" Test list task history for problem. """
act.return_value = self.tasks
url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id})
mock_factory = MockCompletionInfo()
with patch('instructor.views.api.get_task_completion_info') as mock_completion_info:
mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info
response = self.client.get(url, {
'problem_urlname': self.problem_urlname,
})
print response.content
self.assertEqual(response.status_code, 200)
# check response
self.assertTrue(act.called)
expected_tasks = [ftask.to_dict() for ftask in self.tasks]
expected_res = {'tasks': expected_tasks}
self.assertEqual(json.loads(response.content), expected_res)
actual_tasks = json.loads(response.content)['tasks']
for exp_task, act_task in zip(expected_tasks, actual_tasks):
self.assertDictEqual(exp_task, act_task)
self.assertEqual(actual_tasks, expected_tasks)
@patch.object(instructor_task.api, 'get_instructor_task_history')
def test_list_instructor_tasks_problem_student(self, act):
""" Test list task history for problem AND student. """
act.return_value = self.tasks
url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id})
mock_factory = MockCompletionInfo()
with patch('instructor.views.api.get_task_completion_info') as mock_completion_info:
mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info
response = self.client.get(url, {
'problem_urlname': self.problem_urlname,
'unique_student_identifier': self.student.email,
})
print response.content
self.assertEqual(response.status_code, 200)
# check response
self.assertTrue(act.called)
expected_tasks = [ftask.to_dict() for ftask in self.tasks]
expected_res = {'tasks': expected_tasks}
self.assertEqual(json.loads(response.content), expected_res)
actual_tasks = json.loads(response.content)['tasks']
for exp_task, act_task in zip(expected_tasks, actual_tasks):
self.assertDictEqual(exp_task, act_task)
self.assertEqual(actual_tasks, expected_tasks)
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
......
......@@ -8,6 +8,7 @@ Many of these GETs may become PUTs in the future.
import re
import logging
import json
import requests
from django.conf import settings
from django_future.csrf import ensure_csrf_cookie
......@@ -30,6 +31,7 @@ from courseware.models import StudentModule
from student.models import unique_id_for_user
import instructor_task.api
from instructor_task.api_helper import AlreadyRunningError
from instructor_task.views import get_task_completion_info
import instructor.enrollment as enrollment
from instructor.enrollment import enroll_email, unenroll_email
from instructor.views.tools import strip_if_string, get_student_from_identifier
......@@ -675,9 +677,42 @@ def list_instructor_tasks(request, course_id):
tasks = instructor_task.api.get_running_instructor_tasks(course_id)
def extract_task_features(task):
""" Convert task to dict for json rendering """
features = ['task_type', 'task_input', 'task_id', 'requester', 'created', 'task_state']
return dict((feature, str(getattr(task, feature))) for feature in features)
"""
Convert task to dict for json rendering.
Expects tasks have the following features:
* task_type (str, type of task)
* task_input (dict, input(s) to the task)
* task_id (str, celery id of the task)
* requester (str, username who submitted the task)
* task_state (str, state of task eg PROGRESS, COMPLETED)
* created (datetime, when the task was completed)
* task_output (optional)
"""
# Pull out information from the task
features = ['task_type', 'task_input', 'task_id', 'requester', 'task_state']
task_feature_dict = {feature: str(getattr(task, feature)) for feature in features}
# Some information (created, duration, status, task message) require additional formatting
task_feature_dict['created'] = task.created.isoformat()
# Get duration info, if known
duration_sec = 'unknown'
if hasattr(task, 'task_output') and task.task_output is not None:
try:
task_output = json.loads(task.task_output)
except ValueError:
log.error("Could not parse task output as valid json; task output: %s", task.task_output)
else:
if 'duration_ms' in task_output:
duration_sec = int(task_output['duration_ms'] / 1000.0)
task_feature_dict['duration_sec'] = duration_sec
# Get progress status message & success information
success, task_message = get_task_completion_info(task)
status = _("Complete") if success else _("Incomplete")
task_feature_dict['status'] = status
task_feature_dict['task_message'] = task_message
return task_feature_dict
response_payload = {
'tasks': map(extract_task_features, tasks),
......
......@@ -18,7 +18,7 @@ from xmodule.modulestore.django import modulestore
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from courseware.access import has_access
from courseware.courses import get_course_by_id
from courseware.courses import get_course_by_id, get_cms_course_link_by_id
from django_comment_client.utils import has_forum_access
from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR
from student.models import CourseEnrollment
......@@ -45,27 +45,32 @@ def instructor_dashboard_2(request, course_id):
raise Http404()
sections = [
_section_course_info(course_id, access),
_section_course_info(course_id),
_section_membership(course_id, access),
_section_student_admin(course_id, access),
_section_data_download(course_id),
_section_analytics(course_id),
]
# Gate access to course email by feature flag & by course-specific authorization
if settings.MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and \
is_studio_course and CourseAuthorization.instructor_email_enabled(course_id):
sections.append(_section_send_email(course_id, access, course))
studio_url = None
if is_studio_course:
studio_url = get_cms_course_link_by_id(course_id)
enrollment_count = sections[0]['enrollment_count']
disable_buttons = False
max_enrollment_for_buttons = settings.MITX_FEATURES.get("MAX_ENROLLMENT_INSTR_BUTTONS")
if max_enrollment_for_buttons is not None:
disable_buttons = enrollment_count > max_enrollment_for_buttons
# Gate access by feature flag & by course-specific authorization
if settings.MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and \
is_studio_course and CourseAuthorization.instructor_email_enabled(course_id):
sections.append(_section_send_email(course_id, access, course))
context = {
'course': course,
'old_dashboard_url': reverse('instructor_dashboard', kwargs={'course_id': course_id}),
'studio_url': studio_url,
'sections': sections,
'disable_buttons': disable_buttons,
}
......@@ -86,15 +91,19 @@ section_display_name will be used to generate link titles in the nav bar.
""" # pylint: disable=W0105
def _section_course_info(course_id, access):
def _section_course_info(course_id):
""" Provide data for the corresponding dashboard section """
course = get_course_by_id(course_id, depth=None)
course_org, course_num, course_name = course_id.split('/')
section_data = {
'section_key': 'course_info',
'section_display_name': _('Course Info'),
'course_id': course_id,
'access': access,
'course_org': course_org,
'course_num': course_num,
'course_name': course_name,
'course_display_name': course.display_name,
'enrollment_count': CourseEnrollment.objects.filter(course_id=course_id).count(),
'has_started': course.has_started(),
......@@ -156,6 +165,7 @@ def _section_data_download(course_id):
'get_grading_config_url': reverse('get_grading_config', kwargs={'course_id': course_id}),
'get_students_features_url': reverse('get_students_features', kwargs={'course_id': course_id}),
'get_anon_ids_url': reverse('get_anon_ids', kwargs={'course_id': course_id}),
'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_id}),
}
return section_data
......@@ -171,7 +181,8 @@ def _section_send_email(course_id, access, course):
'section_display_name': _('Email'),
'access': access,
'send_email': reverse('send_email', kwargs={'course_id': course_id}),
'editor': email_editor
'editor': email_editor,
'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_id}),
}
return section_data
......
......@@ -1589,14 +1589,16 @@ def get_background_task_table(course_id, problem_url=None, student=None, task_ty
success, task_message = get_task_completion_info(instructor_task)
status = "Complete" if success else "Incomplete"
# generate row for this task:
row = [str(instructor_task.task_type),
row = [
str(instructor_task.task_type),
str(instructor_task.task_id),
str(instructor_task.requester),
instructor_task.created.isoformat(' '),
duration_sec,
str(instructor_task.task_state),
status,
task_message]
task_message
]
datatable['data'].append(row)
if problem_url is None:
......
......@@ -29,7 +29,8 @@ MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True
MITX_FEATURES['ENABLE_PSYCHOMETRICS'] = False # real-time psychometrics (eg item response theory analysis in instructor dashboard)
MITX_FEATURES['ENABLE_INSTRUCTOR_ANALYTICS'] = True
MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True
MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = True
MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = True # Enable email for all Studio courses
MITX_FEATURES['REQUIRE_COURSE_EMAIL_AUTH'] = False # Give all courses email (don't require django-admin perms)
MITX_FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True
MITX_FEATURES['ENABLE_INSTRUCTOR_BETA_DASHBOARD'] = True
MITX_FEATURES['MULTIPLE_ENROLLMENT_ROLES'] = True
......
......@@ -230,9 +230,7 @@ class Analytics
# export for use
# create parent namespaces if they do not already exist.
# abort if underscore can not be found.
if _?
_.defaults window, InstructorDashboard: {}
_.defaults window.InstructorDashboard, sections: {}
_.defaults window.InstructorDashboard.sections,
_.defaults window, InstructorDashboard: {}
_.defaults window.InstructorDashboard, sections: {}
_.defaults window.InstructorDashboard.sections,
Analytics: Analytics
###
Course Info Section
This is the implementation of the simplest section
of the instructor dashboard.
imports from other modules.
wrap in (-> ... apply) to defer evaluation
such that the value can be defined later than this assignment (file load order).
###
plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments
std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments
# Load utilities
PendingInstructorTasks = -> window.InstructorDashboard.util.PendingInstructorTasks
# A typical section object.
# constructed with $section, a jquery object
# which holds the section body container.
class CourseInfo
constructor: (@$section) ->
# attach self to html so that instructor_dashboard.coffee can find
# this object to call event handlers like 'onClickTitle'
@$section.data 'wrapper', @
# gather elements
@instructor_tasks = new (PendingInstructorTasks()) @$section
@$course_errors_wrapper = @$section.find '.course-errors-wrapper'
# if there are errors
......@@ -37,12 +41,15 @@ class CourseInfo
else
@$course_errors_wrapper.addClass 'open'
# handler for when the section title is clicked.
onClickTitle: -> @instructor_tasks.task_poller.start()
# handler for when the section is closed
onExit: -> @instructor_tasks.task_poller.stop()
# export for use
# create parent namespaces if they do not already exist.
# abort if underscore can not be found.
if _?
_.defaults window, InstructorDashboard: {}
_.defaults window.InstructorDashboard, sections: {}
_.defaults window.InstructorDashboard.sections,
_.defaults window, InstructorDashboard: {}
_.defaults window.InstructorDashboard, sections: {}
_.defaults window.InstructorDashboard.sections,
CourseInfo: CourseInfo
......@@ -6,13 +6,16 @@ wrap in (-> ... apply) to defer evaluation
such that the value can be defined later than this assignment (file load order).
###
plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments
# Load utilities
std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments
PendingInstructorTasks = -> window.InstructorDashboard.util.PendingInstructorTasks
# Data Download Section
class DataDownload
constructor: (@$section) ->
# attach self to html so that instructor_dashboard.coffee can find
# this object to call event handlers like 'onClickTitle'
@$section.data 'wrapper', @
# gather elements
@$display = @$section.find '.data-display'
@$display_text = @$display.find '.data-display-text'
......@@ -21,9 +24,9 @@ class DataDownload
@$list_studs_btn = @$section.find("input[name='list-profiles']'")
@$list_anon_btn = @$section.find("input[name='list-anon-ids']'")
@$grade_config_btn = @$section.find("input[name='dump-gradeconf']'")
@instructor_tasks = new (PendingInstructorTasks()) @$section
# attach click handlers
# The list-anon case is always CSV
@$list_anon_btn.click (e) =>
url = @$list_anon_btn.data 'endpoint'
......@@ -80,6 +83,11 @@ class DataDownload
@clear_display()
@$display_text.html data['grading_config_summary']
# handler for when the section title is clicked.
onClickTitle: -> @instructor_tasks.task_poller.start()
# handler for when the section is closed
onExit: -> @instructor_tasks.task_poller.stop()
clear_display: ->
@$display_text.empty()
......@@ -89,9 +97,7 @@ class DataDownload
# export for use
# create parent namespaces if they do not already exist.
# abort if underscore can not be found.
if _?
_.defaults window, InstructorDashboard: {}
_.defaults window.InstructorDashboard, sections: {}
_.defaults window.InstructorDashboard.sections,
_.defaults window, InstructorDashboard: {}
_.defaults window.InstructorDashboard, sections: {}
_.defaults window.InstructorDashboard.sections,
DataDownload: DataDownload
......@@ -118,7 +118,7 @@ setup_instructor_dashboard = (idash_content) =>
location.hash = "#{HASH_LINK_PREFIX}#{section_name}"
sections_have_loaded.after ->
$section.data('wrapper')?.onClickTitle?()
$section.data('wrapper').onClickTitle()
# call onExit handler if exiting a section to a different section.
unless $section.is $active_section
......
......@@ -487,9 +487,7 @@ class Membership
# export for use
# create parent namespaces if they do not already exist.
# abort if underscore can not be found.
if _?
_.defaults window, InstructorDashboard: {}
_.defaults window.InstructorDashboard, sections: {}
_.defaults window.InstructorDashboard.sections,
_.defaults window, InstructorDashboard: {}
_.defaults window.InstructorDashboard, sections: {}
_.defaults window.InstructorDashboard.sections,
Membership: Membership
......@@ -6,8 +6,10 @@ wrap in (-> ... apply) to defer evaluation
such that the value can be defined later than this assignment (file load order).
###
# Load utilities
plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments
std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments
PendingInstructorTasks = -> window.InstructorDashboard.util.PendingInstructorTasks
class SendEmail
constructor: (@$container) ->
......@@ -79,23 +81,25 @@ class SendEmail
class Email
# enable subsections.
constructor: (@$section) ->
# attach self to html
# so that instructor_dashboard.coffee can find this object
# to call event handlers like 'onClickTitle'
# attach self to html so that instructor_dashboard.coffee can find
# this object to call event handlers like 'onClickTitle'
@$section.data 'wrapper', @
# isolate # initialize SendEmail subsection
plantTimeout 0, => new SendEmail @$section.find '.send-email'
@instructor_tasks = new (PendingInstructorTasks()) @$section
# handler for when the section title is clicked.
onClickTitle: ->
onClickTitle: -> @instructor_tasks.task_poller.start()
# handler for when the section is closed
onExit: -> @instructor_tasks.task_poller.stop()
# export for use
# create parent namespaces if they do not already exist.
# abort if underscore can not be found.
if _?
_.defaults window, InstructorDashboard: {}
_.defaults window.InstructorDashboard, sections: {}
_.defaults window.InstructorDashboard.sections,
_.defaults window, InstructorDashboard: {}
_.defaults window.InstructorDashboard, sections: {}
_.defaults window.InstructorDashboard.sections,
Email: Email
......@@ -6,10 +6,10 @@ wrap in (-> ... apply) to defer evaluation
such that the value can be defined later than this assignment (file load order).
###
plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments
plantInterval = -> window.InstructorDashboard.util.plantInterval.apply this, arguments
# Load utilities
std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments
load_IntervalManager = -> window.InstructorDashboard.util.IntervalManager
create_task_list_table = -> window.InstructorDashboard.util.create_task_list_table.apply this, arguments
PendingInstructorTasks = -> window.InstructorDashboard.util.PendingInstructorTasks
# get jquery element and assert its existance
......@@ -21,57 +21,11 @@ find_and_assert = ($root, selector) ->
else
item
# render a task list table to the DOM
# `$table_tasks` the $element in which to put the table
# `tasks_data`
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
constructor: (@$section) ->
# attach self to html so that instructor_dashboard.coffee can find
# this object to call event handlers like 'onClickTitle'
@$section.data 'wrapper', @
# gather buttons
......@@ -93,22 +47,13 @@ class StudentAdmin
@$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"
@instructor_tasks = new (PendingInstructorTasks()) @$section
# response areas
@$request_response_error_progress = find_and_assert @$section, ".student-specific-container .request-response-error"
@$request_response_error_grade = find_and_assert @$section, ".student-grade-container .request-response-error"
@$request_response_error_all = @$section.find ".course-specific-container .request-response-error"
# start polling for task list
# if the list is in the DOM
if @$table_running_tasks.length > 0
# reload every 20 seconds.
TASK_LIST_POLL_INTERVAL = 20000
@reload_running_tasks_list()
@task_poller = new (load_IntervalManager()) TASK_LIST_POLL_INTERVAL, =>
@reload_running_tasks_list()
# attach click handlers
# go to student progress page
......@@ -294,14 +239,6 @@ class StudentAdmin
create_task_list_table @$table_task_history_all, data.tasks
error: std_ajax_err => @$request_response_error_all.text gettext("Error listing task history for this student and problem.")
reload_running_tasks_list: =>
list_endpoint = @$table_running_tasks.data 'endpoint'
$.ajax
dataType: 'json'
url: list_endpoint
success: (data) => create_task_list_table @$table_running_tasks, data.tasks
error: std_ajax_err => console.warn "error listing all instructor tasks"
# wraps a function, but first clear the error displays
clear_errors_then: (cb) ->
@$request_response_error_progress.empty()
......@@ -317,17 +254,15 @@ class StudentAdmin
@$request_response_error_all.empty()
# handler for when the section title is clicked.
onClickTitle: -> @task_poller?.start()
onClickTitle: -> @instructor_tasks.task_poller.start()
# handler for when the section is closed
onExit: -> @task_poller?.stop()
onExit: -> @instructor_tasks.task_poller.stop()
# export for use
# create parent namespaces if they do not already exist.
# abort if underscore can not be found.
if _?
_.defaults window, InstructorDashboard: {}
_.defaults window.InstructorDashboard, sections: {}
_.defaults window.InstructorDashboard.sections,
_.defaults window, InstructorDashboard: {}
_.defaults window.InstructorDashboard, sections: {}
_.defaults window.InstructorDashboard.sections,
StudentAdmin: StudentAdmin
......@@ -6,6 +6,15 @@ plantTimeout = (ms, cb) -> setTimeout cb, ms
plantInterval = (ms, cb) -> setInterval cb, ms
# 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
# standard ajax error wrapper
#
# wraps a `handler` function so that first
......@@ -17,6 +26,72 @@ std_ajax_err = (handler) -> (jqXHR, textStatus, errorThrown) ->
handler.apply this, arguments
# render a task list table to the DOM
# `$table_tasks` the $element in which to put the table
# `tasks_data`
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'
minWidth: 100
,
id: 'task_input'
field: 'task_input'
name: 'Task inputs'
minWidth: 150
,
id: 'task_id'
field: 'task_id'
name: 'Task ID'
minWidth: 150
,
id: 'requester'
field: 'requester'
name: 'Requester'
minWidth: 80
,
id: 'created'
field: 'created'
name: 'Submitted'
minWidth: 120
,
id: 'duration_sec'
field: 'duration_sec'
name: 'Duration (sec)'
minWidth: 80
,
id: 'task_state'
field: 'task_state'
name: 'State'
minWidth: 80
,
id: 'status'
field: 'status'
name: 'Task Status'
minWidth: 80
,
id: 'task_message'
field: 'task_message'
name: 'Task Progress'
minWidth: 120
]
table_data = tasks_data
$table_placeholder = $ '<div/>', class: 'slickgrid'
$table_tasks.append $table_placeholder
grid = new Slick.Grid($table_placeholder, table_data, columns, options)
# Helper class for managing the execution of interval tasks.
# Handles pausing and restarting.
class IntervalManager
......@@ -26,8 +101,8 @@ class IntervalManager
@intervalID = null
# Start or restart firing every `ms` milliseconds.
# Soes not fire immediately.
start: ->
@fn()
if @intervalID is null
@intervalID = setInterval @fn, @ms
......@@ -37,6 +112,30 @@ class IntervalManager
@intervalID = null
class PendingInstructorTasks
### Pending Instructor Tasks Section ####
constructor: (@$section) ->
# Currently running tasks
@$table_running_tasks = find_and_assert @$section, ".running-tasks-table"
# start polling for task list
# if the list is in the DOM
if @$table_running_tasks.length > 0
# reload every 20 seconds.
TASK_LIST_POLL_INTERVAL = 20000
@reload_running_tasks_list()
@task_poller = new IntervalManager(TASK_LIST_POLL_INTERVAL, => @reload_running_tasks_list())
# Populate the running tasks list
reload_running_tasks_list: =>
list_endpoint = @$table_running_tasks.data 'endpoint'
$.ajax
dataType: 'json'
url: list_endpoint
success: (data) => create_task_list_table @$table_running_tasks, data.tasks
error: std_ajax_err => console.warn "error listing all instructor tasks"
### /Pending Instructor Tasks Section ####
# export for use
# create parent namespaces if they do not already exist.
# abort if underscore can not be found.
......@@ -47,3 +146,5 @@ if _?
plantInterval: plantInterval
std_ajax_err: std_ajax_err
IntervalManager: IntervalManager
create_task_list_table: create_task_list_table
PendingInstructorTasks: PendingInstructorTasks
......@@ -14,9 +14,16 @@
.olddash-button-wrapper {
position: absolute;
top: 17px;
top: 16px;
right: 15px;
@include font-size(14);
@include font-size(16);
}
.studio-edit-link{
position: absolute;
top: 40px;
right: 15px;
@include font-size(16);
}
// system feedback - messages
......
<%! from django.utils.translation import ugettext as _ %>
<%page args="section_data"/>
<h2>${_("Course Information")}</h2>
<div class="enrollment-wrapper">
<h2>${_("Enrollment Information")}</h2>
<span class="tip">${_("Total number of enrollees (instructors, staff members, and students)")}</span>
<br/><br/>
<span style="color: green;"><b>${ section_data['enrollment_count'] }</b></span>
<div class="basic-data">
${_("Course Name")}:
${ section_data['course_display_name'] }
</div>
<hr>
<div class="basic-data">
${_("Course ID")}:
${ section_data['course_id'] }
</div>
<div class="basic-wrapper">
<h2>${_("Basic Course Information")}</h2>
<div class="basic-data">
${_("Students Enrolled")}:
${ section_data['enrollment_count'] }
</div>
<ul class="list-input">
<li class="field text is-not-editable" id="field-course-organization">
<label for="course-organization">${_("Organization:")}</label>
<b>${ section_data['course_org'] }</b>
</li>
<div class="basic-data">
${_("Started")}:
${ section_data['has_started'] }
</div>
<li class="field text is-not-editable" id="field-course-number">
<label for="course-number">${_("Course Number:")}</label>
<b>${ section_data['course_num'] }</b>
</li>
<div class="basic-data">
${_("Ended")}:
${ section_data['has_ended'] }
</div>
<li class="field text is-not-editable" id="field-course-name">
<label for="course-name">${_("Course Name:")}</label>
<b>${ section_data['course_name'] }</b>
</li>
<li class="field text is-not-editable" id="field-course-display-name">
<label for="course-display-name">${_("Course Display Name:")}</label>
<b>${ section_data['course_display_name'] }</b>
</li>
<li class="field text is-not-editable" id="field-course-started">
<label for="start-date">${_("Has the course started?")}</label>
<div class="basic-data">
${_("Grade Cutoffs")}:
${ section_data['grade_cutoffs'] }
<b>${_("Yes") if section_data['grade_cutoffs'] else _("No")}</b>
</li>
<li class="field text is-not-editable" id="field-course-ended">
<label for="start-date">${_("Has the course ended?")}</label>
%if section_data['has_ended']:
<b>${_("Yes")}</b>
%else:
<b>${_("No")}</b>
%endif
</li>
<li class="field text is-not-editable" id="field-grade-cutoffs">
<label for="start-date">${_("Grade Cutoffs:")}</label>
<b>${ section_data['grade_cutoffs'] }</b>
</li>
</ul>
</div>
## <div class="basic-data">
## Offline Grades Available:
## ${ section_data['offline_grades'] }
## </div>
%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS') and section_data['access']['instructor']:
%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
<div class="running-tasks-container action-type-container">
<hr>
<h2> ${_("Pending Instructor Tasks")} </h2>
<p>${_("The status for any active tasks appears in a table below.")} </p>
<br />
<div class="running-tasks-table" data-endpoint="${ section_data['list_instructor_tasks_url'] }"></div>
</div>
......@@ -69,6 +90,3 @@
</div>
<br>
%endif
......@@ -19,4 +19,16 @@
<div class="data-display-text"></div>
<div class="data-display-table"></div>
<div class="request-response-error"></div>
%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
<div class="running-tasks-container action-type-container">
<hr>
<h2> ${_("Pending Instructor Tasks")} </h2>
<p>${_("The status for any active tasks appears in a table below.")} </p>
<br />
<div class="running-tasks-table" data-endpoint="${ section_data['list_instructor_tasks_url'] }"></div>
</div>
%endif
</div>
......@@ -50,10 +50,14 @@
<section class="container">
<div class="instructor-dashboard-wrapper-2">
<div class="olddash-button-wrapper"><a href="${ old_dashboard_url }"> ${_("Back to Standard Dashboard")} </a></div>
%if studio_url:
## not checking access because if user can see this, they are at least course staff (with studio edit access)
<div class="studio-edit-link"><a href="${studio_url}" target="_blank">${_('Edit Course In Studio')}</a></div>
%endif
<section class="instructor-dashboard-content-2">
## <h1>Instructor Dashboard</h1>
<h1>${_("Instructor Dashboard")}</h1>
<hr />
## links which are tied to idash-sections below.
## the links are acativated and handled in instructor_dashboard.coffee
## when the javascript loads, it clicks on the first section
......
......@@ -54,4 +54,16 @@
<br />
<input type="button" name="send" value="${_("Send Email")}" data-endpoint="${ section_data['send_email'] }" >
<div class="request-response-error"></div>
%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
<div class="running-tasks-container action-type-container">
<hr>
<h2> ${_("Pending Instructor Tasks")} </h2>
<p>${_("The status for any active tasks appears in a table below.")} </p>
<br />
<div class="running-tasks-table" data-endpoint="${ section_data['list_instructor_tasks_url'] }"></div>
</div>
%endif
</div>
......@@ -109,3 +109,15 @@
</p>
</div>
%endif
%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
<div class="running-tasks-container action-type-container">
<hr>
<h2> ${_("Pending Instructor Tasks")} </h2>
<p>${_("The status for any active tasks appears in a table below.")} </p>
<br />
<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