Merge pull request #1539 from edx/sarina/inst-dash-tasks
Enable Pending Tasks on beta dash // Course Info prettifying
This commit is contained in:
@@ -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})
|
||||
response = self.client.get(url, {})
|
||||
print response.content
|
||||
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, {})
|
||||
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})
|
||||
response = self.client.get(url, {
|
||||
'problem_urlname': self.problem_urlname,
|
||||
})
|
||||
print response.content
|
||||
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,
|
||||
})
|
||||
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})
|
||||
response = self.client.get(url, {
|
||||
'problem_urlname': self.problem_urlname,
|
||||
'unique_student_identifier': self.student.email,
|
||||
})
|
||||
print response.content
|
||||
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,
|
||||
})
|
||||
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),
|
||||
str(instructor_task.task_id),
|
||||
str(instructor_task.requester),
|
||||
instructor_task.created.isoformat(' '),
|
||||
duration_sec,
|
||||
str(instructor_task.task_state),
|
||||
status,
|
||||
task_message]
|
||||
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
|
||||
]
|
||||
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,
|
||||
Analytics: Analytics
|
||||
_.defaults window, InstructorDashboard: {}
|
||||
_.defaults window.InstructorDashboard, sections: {}
|
||||
_.defaults window.InstructorDashboard.sections,
|
||||
Analytics: Analytics
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
###
|
||||
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,
|
||||
CourseInfo: CourseInfo
|
||||
_.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,
|
||||
DataDownload: DataDownload
|
||||
_.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,
|
||||
Membership: Membership
|
||||
_.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,
|
||||
Email: Email
|
||||
_.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,
|
||||
StudentAdmin: StudentAdmin
|
||||
_.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
|
||||
|
||||
@@ -1,48 +1,69 @@
|
||||
<%! 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-wrapper">
|
||||
<h2>${_("Basic Course Information")}</h2>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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">
|
||||
${_("Course ID")}:
|
||||
${ section_data['course_id'] }
|
||||
</div>
|
||||
|
||||
<div class="basic-data">
|
||||
${_("Students Enrolled")}:
|
||||
${ section_data['enrollment_count'] }
|
||||
</div>
|
||||
|
||||
<div class="basic-data">
|
||||
${_("Started")}:
|
||||
${ section_data['has_started'] }
|
||||
</div>
|
||||
|
||||
<div class="basic-data">
|
||||
${_("Ended")}:
|
||||
${ section_data['has_ended'] }
|
||||
</div>
|
||||
|
||||
<div class="basic-data">
|
||||
${_("Grade Cutoffs")}:
|
||||
${ section_data['grade_cutoffs'] }
|
||||
</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
|
||||
|
||||
Reference in New Issue
Block a user