From 9330ef65a8957626edfdd858f3be88a2838dc287 Mon Sep 17 00:00:00 2001
From: Sarina Canelake
Date: Tue, 15 Oct 2013 11:31:27 -0400
Subject: [PATCH 1/4] Enable Pending Tasks section on new dash
Add Pending Tasks section to the following tabs:
* Course Info
* Student Email
* Data Download
* Email
LMS-1242
---
.../instructor/views/instructor_dashboard.py | 14 ++--
lms/envs/dev.py | 3 +-
.../instructor_dashboard/course_info.coffee | 29 +++++++-
.../instructor_dashboard/data_download.coffee | 26 ++++++-
.../instructor_dashboard/send_email.coffee | 26 +++++++
.../instructor_dashboard/student_admin.coffee | 69 ++++---------------
.../src/instructor_dashboard/util.coffee | 49 +++++++++++++
.../instructor_dashboard_2/course_info.html | 1 +
.../instructor_dashboard_2/data_download.html | 12 ++++
.../instructor_dashboard_2/send_email.html | 12 ++++
.../instructor_dashboard_2/student_admin.html | 12 ++++
11 files changed, 186 insertions(+), 67 deletions(-)
diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py
index e579593a88..c48efa91f9 100644
--- a/lms/djangoapps/instructor/views/instructor_dashboard.py
+++ b/lms/djangoapps/instructor/views/instructor_dashboard.py
@@ -52,17 +52,17 @@ def instructor_dashboard_2(request, 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))
+
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}),
@@ -156,6 +156,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 +172,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
diff --git a/lms/envs/dev.py b/lms/envs/dev.py
index e873861196..e1eadf1331 100644
--- a/lms/envs/dev.py
+++ b/lms/envs/dev.py
@@ -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
diff --git a/lms/static/coffee/src/instructor_dashboard/course_info.coffee b/lms/static/coffee/src/instructor_dashboard/course_info.coffee
index 19f9ce9707..beca8db712 100644
--- a/lms/static/coffee/src/instructor_dashboard/course_info.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/course_info.coffee
@@ -1,15 +1,17 @@
###
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).
###
+# Load utilities
plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments
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
+
# A typical section object.
# constructed with $section, a jquery object
@@ -37,6 +39,29 @@ class CourseInfo
else
@$course_errors_wrapper.addClass 'open'
+ ### Pending Instructor Tasks Section ####
+ # Currently running tasks
+ @$table_running_tasks = @$section.find ".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 (load_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.
diff --git a/lms/static/coffee/src/instructor_dashboard/data_download.coffee b/lms/static/coffee/src/instructor_dashboard/data_download.coffee
index b5bbde9182..ffc24a574b 100644
--- a/lms/static/coffee/src/instructor_dashboard/data_download.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/data_download.coffee
@@ -8,7 +8,8 @@ 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_IntervalManager = -> window.InstructorDashboard.util.IntervalManager
+create_task_list_table = -> window.InstructorDashboard.util.create_task_list_table.apply this, arguments
# Data Download Section
class DataDownload
@@ -81,6 +82,29 @@ class DataDownload
@$display_text.html data['grading_config_summary']
+ ### Pending Instructor Tasks Section ####
+ # Currently running tasks
+ @$table_running_tasks = @$section.find ".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 (load_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 ####
+
clear_display: ->
@$display_text.empty()
@$display_table.empty()
diff --git a/lms/static/coffee/src/instructor_dashboard/send_email.coffee b/lms/static/coffee/src/instructor_dashboard/send_email.coffee
index 7fc839cca6..27eea3d339 100644
--- a/lms/static/coffee/src/instructor_dashboard/send_email.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/send_email.coffee
@@ -6,8 +6,11 @@ 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
+load_IntervalManager = -> window.InstructorDashboard.util.IntervalManager
+create_task_list_table = -> window.InstructorDashboard.util.create_task_list_table.apply this, arguments
class SendEmail
constructor: (@$container) ->
@@ -87,6 +90,29 @@ class Email
# isolate # initialize SendEmail subsection
plantTimeout 0, => new SendEmail @$section.find '.send-email'
+ ### Pending Instructor Tasks Section ####
+ # Currently running tasks
+ @$table_running_tasks = @$section.find ".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 (load_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 ####
+
# handler for when the section title is clicked.
onClickTitle: ->
diff --git a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee
index c07069a493..198c7e84a5 100644
--- a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee
@@ -6,10 +6,12 @@ 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
plantInterval = -> window.InstructorDashboard.util.plantInterval.apply this, arguments
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
# get jquery element and assert its existance
@@ -21,54 +23,6 @@ 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 = $ '', class: 'slickgrid'
- $table_tasks.append $table_placeholder
- grid = new Slick.Grid($table_placeholder, table_data, columns, options)
-
class StudentAdmin
constructor: (@$section) ->
@@ -100,15 +54,6 @@ class StudentAdmin
@$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,6 +239,16 @@ 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.")
+ # 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()
+
+ # Populate the running tasks list
reload_running_tasks_list: =>
list_endpoint = @$table_running_tasks.data 'endpoint'
$.ajax
diff --git a/lms/static/coffee/src/instructor_dashboard/util.coffee b/lms/static/coffee/src/instructor_dashboard/util.coffee
index 9217da5064..629532768e 100644
--- a/lms/static/coffee/src/instructor_dashboard/util.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/util.coffee
@@ -17,6 +17,54 @@ 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'
+ ,
+ 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 = $ '', 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
@@ -47,3 +95,4 @@ if _?
plantInterval: plantInterval
std_ajax_err: std_ajax_err
IntervalManager: IntervalManager
+ create_task_list_table: create_task_list_table
diff --git a/lms/templates/instructor/instructor_dashboard_2/course_info.html b/lms/templates/instructor/instructor_dashboard_2/course_info.html
index cb113e1846..626d1b8ec8 100644
--- a/lms/templates/instructor/instructor_dashboard_2/course_info.html
+++ b/lms/templates/instructor/instructor_dashboard_2/course_info.html
@@ -43,6 +43,7 @@
${_("Pending Instructor Tasks")}
${_("The status for any active tasks appears in a table below.")}
+
diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download.html b/lms/templates/instructor/instructor_dashboard_2/data_download.html
index 0bf21dd58a..b57fd7b30e 100644
--- a/lms/templates/instructor/instructor_dashboard_2/data_download.html
+++ b/lms/templates/instructor/instructor_dashboard_2/data_download.html
@@ -19,4 +19,16 @@
+
+%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
+
+
+
${_("Pending Instructor Tasks")}
+
${_("The status for any active tasks appears in a table below.")}
+
+
+
+
+
+%endif
diff --git a/lms/templates/instructor/instructor_dashboard_2/send_email.html b/lms/templates/instructor/instructor_dashboard_2/send_email.html
index 0ff0360e91..b3970c5091 100644
--- a/lms/templates/instructor/instructor_dashboard_2/send_email.html
+++ b/lms/templates/instructor/instructor_dashboard_2/send_email.html
@@ -54,4 +54,16 @@
+
+%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
+
+
+
${_("Pending Instructor Tasks")}
+
${_("The status for any active tasks appears in a table below.")}
+
+
+
+
+
+%endif
diff --git a/lms/templates/instructor/instructor_dashboard_2/student_admin.html b/lms/templates/instructor/instructor_dashboard_2/student_admin.html
index 2ae60789df..4d60810008 100644
--- a/lms/templates/instructor/instructor_dashboard_2/student_admin.html
+++ b/lms/templates/instructor/instructor_dashboard_2/student_admin.html
@@ -109,3 +109,15 @@
%endif
+
+%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
+
+
+
${_("Pending Instructor Tasks")}
+
${_("The status for any active tasks appears in a table below.")}
+
+
+
+
+
+%endif
From 123e18109ddedff33e8bab1a50fe23a392473970 Mon Sep 17 00:00:00 2001
From: Sarina Canelake
Date: Tue, 15 Oct 2013 18:48:31 -0400
Subject: [PATCH 2/4] Reorganize Course Info dash section
LMS-1242
---
.../instructor/views/instructor_dashboard.py | 10 +-
.../instructor_dashboard/course_info.coffee | 2 +-
.../instructor_dashboard/data_download.coffee | 2 +-
.../instructor_dashboard/send_email.coffee | 2 +-
.../instructor_dashboard_2/course_info.html | 95 +++++++++++--------
5 files changed, 68 insertions(+), 43 deletions(-)
diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py
index c48efa91f9..a0b830d535 100644
--- a/lms/djangoapps/instructor/views/instructor_dashboard.py
+++ b/lms/djangoapps/instructor/views/instructor_dashboard.py
@@ -45,7 +45,7 @@ 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),
@@ -86,15 +86,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(),
diff --git a/lms/static/coffee/src/instructor_dashboard/course_info.coffee b/lms/static/coffee/src/instructor_dashboard/course_info.coffee
index beca8db712..98e85be349 100644
--- a/lms/static/coffee/src/instructor_dashboard/course_info.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/course_info.coffee
@@ -41,7 +41,7 @@ class CourseInfo
### Pending Instructor Tasks Section ####
# Currently running tasks
- @$table_running_tasks = @$section.find ".running-tasks-table"
+ @$table_running_tasks = @$section.find ".running-tasks-table"
# start polling for task list
# if the list is in the DOM
diff --git a/lms/static/coffee/src/instructor_dashboard/data_download.coffee b/lms/static/coffee/src/instructor_dashboard/data_download.coffee
index ffc24a574b..f9051f5260 100644
--- a/lms/static/coffee/src/instructor_dashboard/data_download.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/data_download.coffee
@@ -84,7 +84,7 @@ class DataDownload
### Pending Instructor Tasks Section ####
# Currently running tasks
- @$table_running_tasks = @$section.find ".running-tasks-table"
+ @$table_running_tasks = @$section.find ".running-tasks-table"
# start polling for task list
# if the list is in the DOM
diff --git a/lms/static/coffee/src/instructor_dashboard/send_email.coffee b/lms/static/coffee/src/instructor_dashboard/send_email.coffee
index 27eea3d339..14eda493a8 100644
--- a/lms/static/coffee/src/instructor_dashboard/send_email.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/send_email.coffee
@@ -92,7 +92,7 @@ class Email
### Pending Instructor Tasks Section ####
# Currently running tasks
- @$table_running_tasks = @$section.find ".running-tasks-table"
+ @$table_running_tasks = @$section.find ".running-tasks-table"
# start polling for task list
# if the list is in the DOM
diff --git a/lms/templates/instructor/instructor_dashboard_2/course_info.html b/lms/templates/instructor/instructor_dashboard_2/course_info.html
index 626d1b8ec8..c97ab0af32 100644
--- a/lms/templates/instructor/instructor_dashboard_2/course_info.html
+++ b/lms/templates/instructor/instructor_dashboard_2/course_info.html
@@ -1,44 +1,68 @@
<%! from django.utils.translation import ugettext as _ %>
<%page args="section_data"/>
-${_("Course Information")}
+
+
${_("Enrollment Information")}
+
${_("Total number of enrollees (instructors, staff members, and students)")}
+
+
${ section_data['enrollment_count'] }
-
- ${_("Course Name")}:
- ${ section_data['course_display_name'] }
+
+
+
+
+
${_("Basic Course Information")}
+
+
-
- ${_("Course ID")}:
- ${ section_data['course_id'] }
-
-
- ${_("Students Enrolled")}:
- ${ section_data['enrollment_count'] }
-
-
-
- ${_("Started")}:
- ${ section_data['has_started'] }
-
-
-
- ${_("Ended")}:
- ${ section_data['has_ended'] }
-
-
-
- ${_("Grade Cutoffs")}:
- ${ section_data['grade_cutoffs'] }
-
-
-##
-## Offline Grades Available:
-## ${ section_data['offline_grades'] }
-##
-
-%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS') and section_data['access']['instructor']:
+%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
${_("Pending Instructor Tasks")}
@@ -70,6 +94,3 @@
%endif
-
-
-
From 57a57e8af74181c8c3cc68f9c3f190fc3ae149f7 Mon Sep 17 00:00:00 2001
From: Sarina Canelake
Date: Mon, 21 Oct 2013 12:01:13 -0400
Subject: [PATCH 3/4] Move PIT code into util.coffee Add testing coverage
LMS-1242
Add "Edit This Course In Studio" link for studio courses
LMS-1291
---
lms/djangoapps/instructor/tests/test_api.py | 103 ++++++++++++++----
lms/djangoapps/instructor/views/api.py | 37 ++++++-
.../instructor/views/instructor_dashboard.py | 7 +-
lms/djangoapps/instructor/views/legacy.py | 18 +--
.../instructor_dashboard/course_info.coffee | 30 +----
.../instructor_dashboard/data_download.coffee | 29 +----
.../instructor_dashboard/send_email.coffee | 31 +-----
.../instructor_dashboard/student_admin.coffee | 25 +----
.../src/instructor_dashboard/util.coffee | 78 ++++++++++---
.../sass/course/instructor/_instructor_2.scss | 11 +-
.../instructor_dashboard_2.html | 8 +-
11 files changed, 234 insertions(+), 143 deletions(-)
diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py
index 693e832e5d..ae8d02d63f 100644
--- a/lms/djangoapps/instructor/tests/test_api.py
+++ b/lms/djangoapps/instructor/tests/test_api.py
@@ -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,44 @@ 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',
+ 'task_output'
+ ]
- def __init__(self):
+ def __init__(self, completion):
for feature in self.FEATURES:
setattr(self, feature, 'expected')
+ # Make 'created' into a datetime
+ setattr(self, 'created', datetime.datetime(2013, 10, 25, 11, 42, 35))
+ # set 'status' and 'task_message' attrs
+ success, task_message = completion()
+ if success:
+ setattr(self, 'status', "Complete")
+ else:
+ setattr(self, 'status', "Incomplete")
+ setattr(self, 'task_message', task_message)
+ # Set 'task_output' attr, which will be parsed to the 'duration_sec' attr.
+ setattr(self, 'task_output', '{"duration_ms": 1035000}')
+ setattr(self, 'duration_sec', 1035000 / 1000.0)
+
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()
+ # Don't actually want task_output in the attribute dictionary, as this
+ # is not explicitly extracted in extract_task_features
+ del attr_dict['task_output']
+ return attr_dict
def setUp(self):
self.instructor = AdminFactory.create()
@@ -797,58 +839,77 @@ 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(6)]
- 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)
diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py
index facf648580..d3ab72f098 100644
--- a/lms/djangoapps/instructor/views/api.py
+++ b/lms/djangoapps/instructor/views/api.py
@@ -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,38 @@ 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 = 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:
+ task_output = json.loads(task.task_output)
+ 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),
diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py
index a0b830d535..27ee39e042 100644
--- a/lms/djangoapps/instructor/views/instructor_dashboard.py
+++ b/lms/djangoapps/instructor/views/instructor_dashboard.py
@@ -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
@@ -57,6 +57,10 @@ def instructor_dashboard_2(request, course_id):
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")
@@ -66,6 +70,7 @@ def instructor_dashboard_2(request, course_id):
context = {
'course': course,
'old_dashboard_url': reverse('instructor_dashboard', kwargs={'course_id': course_id}),
+ 'studio_url': studio_url,
'sections': sections,
'disable_buttons': disable_buttons,
}
diff --git a/lms/djangoapps/instructor/views/legacy.py b/lms/djangoapps/instructor/views/legacy.py
index d3ef8498a8..e34ce10ec7 100644
--- a/lms/djangoapps/instructor/views/legacy.py
+++ b/lms/djangoapps/instructor/views/legacy.py
@@ -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:
diff --git a/lms/static/coffee/src/instructor_dashboard/course_info.coffee b/lms/static/coffee/src/instructor_dashboard/course_info.coffee
index 98e85be349..21e719566e 100644
--- a/lms/static/coffee/src/instructor_dashboard/course_info.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/course_info.coffee
@@ -9,9 +9,7 @@ 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
-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
# A typical section object.
# constructed with $section, a jquery object
@@ -39,29 +37,13 @@ class CourseInfo
else
@$course_errors_wrapper.addClass 'open'
- ### Pending Instructor Tasks Section ####
- # Currently running tasks
- @$table_running_tasks = @$section.find ".running-tasks-table"
+ @instructor_tasks = new (PendingInstructorTasks()) @$section
- # 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()
-
- # 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 ####
+ # 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.
diff --git a/lms/static/coffee/src/instructor_dashboard/data_download.coffee b/lms/static/coffee/src/instructor_dashboard/data_download.coffee
index f9051f5260..040a4c1f13 100644
--- a/lms/static/coffee/src/instructor_dashboard/data_download.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/data_download.coffee
@@ -8,8 +8,7 @@ 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_IntervalManager = -> window.InstructorDashboard.util.IntervalManager
-create_task_list_table = -> window.InstructorDashboard.util.create_task_list_table.apply this, arguments
+PendingInstructorTasks = -> window.InstructorDashboard.util.PendingInstructorTasks
# Data Download Section
class DataDownload
@@ -81,29 +80,13 @@ class DataDownload
@clear_display()
@$display_text.html data['grading_config_summary']
+ @instructor_tasks = new (PendingInstructorTasks()) @$section
- ### Pending Instructor Tasks Section ####
- # Currently running tasks
- @$table_running_tasks = @$section.find ".running-tasks-table"
+ # handler for when the section title is clicked.
+ onClickTitle: -> @instructor_tasks.task_poller?.start()
- # 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()
-
- # 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 ####
+ # handler for when the section is closed
+ onExit: -> @instructor_tasks.task_poller?.stop()
clear_display: ->
@$display_text.empty()
diff --git a/lms/static/coffee/src/instructor_dashboard/send_email.coffee b/lms/static/coffee/src/instructor_dashboard/send_email.coffee
index 14eda493a8..8095804987 100644
--- a/lms/static/coffee/src/instructor_dashboard/send_email.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/send_email.coffee
@@ -9,8 +9,7 @@ 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
-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
class SendEmail
constructor: (@$container) ->
@@ -90,31 +89,13 @@ class Email
# isolate # initialize SendEmail subsection
plantTimeout 0, => new SendEmail @$section.find '.send-email'
- ### Pending Instructor Tasks Section ####
- # Currently running tasks
- @$table_running_tasks = @$section.find ".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 (load_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 ####
+ @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
diff --git a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee
index 198c7e84a5..46c041048c 100644
--- a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee
@@ -12,6 +12,7 @@ plantInterval = -> window.InstructorDashboard.util.plantInterval.apply this, arg
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
@@ -47,7 +48,7 @@ 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"
@@ -239,24 +240,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.")
- # 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()
-
- # 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"
-
# wraps a function, but first clear the error displays
clear_errors_then: (cb) ->
@$request_response_error_progress.empty()
@@ -272,10 +255,10 @@ 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
diff --git a/lms/static/coffee/src/instructor_dashboard/util.coffee b/lms/static/coffee/src/instructor_dashboard/util.coffee
index 629532768e..ccce17eb5c 100644
--- a/lms/static/coffee/src/instructor_dashboard/util.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/util.coffee
@@ -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
@@ -34,29 +43,47 @@ create_task_list_table = ($table_tasks, tasks_data) ->
id: 'task_type'
field: 'task_type'
name: 'Task Type'
- ,
- id: 'requester'
- field: 'requester'
- name: 'Requester'
- width: 30
+ minWidth: 100
,
id: 'task_input'
field: 'task_input'
- name: 'Input'
- ,
- id: 'task_state'
- field: 'task_state'
- name: 'State'
- width: 30
+ name: 'Task inputs'
+ minWidth: 150
,
id: 'task_id'
field: 'task_id'
name: 'Task ID'
- width: 50
+ minWidth: 150
+ ,
+ id: 'requester'
+ field: 'requester'
+ name: 'Requester'
+ minWidth: 80
,
id: 'created'
field: 'created'
- name: '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
@@ -85,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.
@@ -96,3 +147,4 @@ if _?
std_ajax_err: std_ajax_err
IntervalManager: IntervalManager
create_task_list_table: create_task_list_table
+ PendingInstructorTasks: PendingInstructorTasks
diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss
index 92f01dce1a..f6a9dd2663 100644
--- a/lms/static/sass/course/instructor/_instructor_2.scss
+++ b/lms/static/sass/course/instructor/_instructor_2.scss
@@ -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
diff --git a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html
index 30cf48f505..0299490dac 100644
--- a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html
+++ b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html
@@ -50,10 +50,14 @@
+%if studio_url:
+ ## not checking access because if user can see this, they are at least course staff (with studio edit access)
+
+%endif
- ## Instructor Dashboard
-
+ ${_("Instructor Dashboard")}
+
## 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
From b86e912905679c8188b00a4efcde52b34c3ee926 Mon Sep 17 00:00:00 2001
From: Sarina Canelake
Date: Tue, 29 Oct 2013 16:56:49 -0400
Subject: [PATCH 4/4] Make event handlers fire properly
Respond to review comments
LMS-1242
---
lms/djangoapps/instructor/tests/test_api.py | 33 ++++++++++---------
lms/djangoapps/instructor/views/api.py | 14 +++++---
.../instructor/views/instructor_dashboard.py | 2 +-
.../src/instructor_dashboard/analytics.coffee | 10 +++---
.../instructor_dashboard/course_info.coffee | 24 +++++++-------
.../instructor_dashboard/data_download.coffee | 23 +++++++------
.../instructor_dashboard.coffee | 2 +-
.../instructor_dashboard/membership.coffee | 10 +++---
.../instructor_dashboard/send_email.coffee | 19 +++++------
.../instructor_dashboard/student_admin.coffee | 19 +++++------
.../src/instructor_dashboard/util.coffee | 2 +-
.../instructor_dashboard_2/course_info.html | 6 +---
12 files changed, 78 insertions(+), 86 deletions(-)
diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py
index ae8d02d63f..06d0f16ab8 100644
--- a/lms/djangoapps/instructor/tests/test_api.py
+++ b/lms/djangoapps/instructor/tests/test_api.py
@@ -770,8 +770,8 @@ class MockCompletionInfo(object):
"""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')
+ return True, 'Task Completed'
+ return False, 'Task Errored In Some Way'
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
@@ -791,34 +791,36 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
'created',
'status',
'task_message',
- 'duration_sec',
- 'task_output'
+ 'duration_sec'
]
def __init__(self, completion):
for feature in self.FEATURES:
setattr(self, feature, 'expected')
- # Make 'created' into a datetime
- setattr(self, 'created', datetime.datetime(2013, 10, 25, 11, 42, 35))
+ # 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:
- setattr(self, 'status', "Complete")
+ self.status = "Complete"
else:
- setattr(self, 'status', "Incomplete")
- setattr(self, 'task_message', task_message)
+ self.status = "Incomplete"
+ self.task_message = task_message
# Set 'task_output' attr, which will be parsed to the 'duration_sec' attr.
- setattr(self, 'task_output', '{"duration_ms": 1035000}')
- setattr(self, 'duration_sec', 1035000 / 1000.0)
+ 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. """
attr_dict = {key: getattr(self, key) for key in self.FEATURES}
attr_dict['created'] = attr_dict['created'].isoformat()
- # Don't actually want task_output in the attribute dictionary, as this
- # is not explicitly extracted in extract_task_features
- del attr_dict['task_output']
return attr_dict
def setUp(self):
@@ -840,7 +842,8 @@ 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(6)]
+ self.tasks = [self.FakeTask(mock_factory.mock_get_task_completion_info) for _ in xrange(7)]
+ self.tasks[-1].make_invalid_output()
def tearDown(self):
"""
diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py
index d3ab72f098..8ab1aa8a76 100644
--- a/lms/djangoapps/instructor/views/api.py
+++ b/lms/djangoapps/instructor/views/api.py
@@ -690,21 +690,25 @@ def list_instructor_tasks(request, course_id):
"""
# Pull out information from the task
features = ['task_type', 'task_input', 'task_id', 'requester', 'task_state']
- task_feature_dict = dict((feature, str(getattr(task, feature))) for feature in features)
+ 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:
- task_output = json.loads(task.task_output)
- if 'duration_ms' in task_output:
- duration_sec = int(task_output['duration_ms'] / 1000.0)
+ 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"
+ status = _("Complete") if success else _("Incomplete")
task_feature_dict['status'] = status
task_feature_dict['task_message'] = task_message
diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py
index 27ee39e042..8b5c44ebf0 100644
--- a/lms/djangoapps/instructor/views/instructor_dashboard.py
+++ b/lms/djangoapps/instructor/views/instructor_dashboard.py
@@ -95,7 +95,7 @@ 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('/')
+ course_org, course_num, course_name = course_id.split('/')
section_data = {
'section_key': 'course_info',
diff --git a/lms/static/coffee/src/instructor_dashboard/analytics.coffee b/lms/static/coffee/src/instructor_dashboard/analytics.coffee
index 018b7e9c57..9955f8ee11 100644
--- a/lms/static/coffee/src/instructor_dashboard/analytics.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/analytics.coffee
@@ -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
diff --git a/lms/static/coffee/src/instructor_dashboard/course_info.coffee b/lms/static/coffee/src/instructor_dashboard/course_info.coffee
index 21e719566e..c481a33bb5 100644
--- a/lms/static/coffee/src/instructor_dashboard/course_info.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/course_info.coffee
@@ -7,8 +7,6 @@ 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
# A typical section object.
@@ -16,6 +14,12 @@ PendingInstructorTasks = -> window.InstructorDashboard.util.PendingInstructorTas
# 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,19 +41,15 @@ class CourseInfo
else
@$course_errors_wrapper.addClass 'open'
- @instructor_tasks = new (PendingInstructorTasks()) @$section
-
# handler for when the section title is clicked.
- onClickTitle: -> @instructor_tasks.task_poller?.start()
+ onClickTitle: -> @instructor_tasks.task_poller.start()
# handler for when the section is closed
- onExit: -> @instructor_tasks.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,
- CourseInfo: CourseInfo
+_.defaults window, InstructorDashboard: {}
+_.defaults window.InstructorDashboard, sections: {}
+_.defaults window.InstructorDashboard.sections,
+ CourseInfo: CourseInfo
diff --git a/lms/static/coffee/src/instructor_dashboard/data_download.coffee b/lms/static/coffee/src/instructor_dashboard/data_download.coffee
index 040a4c1f13..e6108b1055 100644
--- a/lms/static/coffee/src/instructor_dashboard/data_download.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/data_download.coffee
@@ -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,13 +83,11 @@ class DataDownload
@clear_display()
@$display_text.html data['grading_config_summary']
- @instructor_tasks = new (PendingInstructorTasks()) @$section
-
# handler for when the section title is clicked.
- onClickTitle: -> @instructor_tasks.task_poller?.start()
+ onClickTitle: -> @instructor_tasks.task_poller.start()
# handler for when the section is closed
- onExit: -> @instructor_tasks.task_poller?.stop()
+ onExit: -> @instructor_tasks.task_poller.stop()
clear_display: ->
@$display_text.empty()
@@ -96,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
diff --git a/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee b/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee
index c645fcf67e..313e5b4967 100644
--- a/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee
@@ -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
diff --git a/lms/static/coffee/src/instructor_dashboard/membership.coffee b/lms/static/coffee/src/instructor_dashboard/membership.coffee
index 54b04be5db..03c6b705b6 100644
--- a/lms/static/coffee/src/instructor_dashboard/membership.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/membership.coffee
@@ -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
diff --git a/lms/static/coffee/src/instructor_dashboard/send_email.coffee b/lms/static/coffee/src/instructor_dashboard/send_email.coffee
index 8095804987..7bdb37c0e9 100644
--- a/lms/static/coffee/src/instructor_dashboard/send_email.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/send_email.coffee
@@ -81,9 +81,8 @@ 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
@@ -92,17 +91,15 @@ class Email
@instructor_tasks = new (PendingInstructorTasks()) @$section
# handler for when the section title is clicked.
- onClickTitle: -> @instructor_tasks.task_poller?.start()
+ onClickTitle: -> @instructor_tasks.task_poller.start()
# handler for when the section is closed
- onExit: -> @instructor_tasks.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,
- Email: Email
+_.defaults window, InstructorDashboard: {}
+_.defaults window.InstructorDashboard, sections: {}
+_.defaults window.InstructorDashboard.sections,
+ Email: Email
diff --git a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee
index 46c041048c..d930dd4b13 100644
--- a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee
@@ -7,10 +7,7 @@ such that the value can be defined later than this assignment (file load order).
###
# Load utilities
-plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments
-plantInterval = -> window.InstructorDashboard.util.plantInterval.apply this, arguments
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
@@ -27,6 +24,8 @@ find_and_assert = ($root, selector) ->
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
@@ -255,17 +254,15 @@ class StudentAdmin
@$request_response_error_all.empty()
# handler for when the section title is clicked.
- onClickTitle: -> @instructor_tasks.task_poller?.start()
+ onClickTitle: -> @instructor_tasks.task_poller.start()
# handler for when the section is closed
- onExit: -> @instructor_tasks.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
diff --git a/lms/static/coffee/src/instructor_dashboard/util.coffee b/lms/static/coffee/src/instructor_dashboard/util.coffee
index ccce17eb5c..09d2ae26f3 100644
--- a/lms/static/coffee/src/instructor_dashboard/util.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/util.coffee
@@ -101,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
diff --git a/lms/templates/instructor/instructor_dashboard_2/course_info.html b/lms/templates/instructor/instructor_dashboard_2/course_info.html
index c97ab0af32..7362014b09 100644
--- a/lms/templates/instructor/instructor_dashboard_2/course_info.html
+++ b/lms/templates/instructor/instructor_dashboard_2/course_info.html
@@ -37,11 +37,7 @@
- %if section_data['has_started']:
- ${_("Yes")}
- %else:
- ${_("No")}
- %endif
+ ${_("Yes") if section_data['grade_cutoffs'] else _("No")}