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 e579593a88a2..c48efa91f9e7 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 e873861196dc..e1eadf1331de 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -29,7 +29,8 @@ 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 19f9ce9707b2..beca8db71202 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 b5bbde918290..ffc24a574b13 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 7fc839cca6bf..27eea3d339b5 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 c07069a49384..198c7e84a5d0 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 9217da506434..629532768ec0 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 cb113e18469b..626d1b8ec815 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 @@

${_("Course Information")}


${_("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 0bf21dd58ac6..b57fd7b30e46 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 @@

${_("Data Download")}

+ +%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 0ff0360e91c0..b3970c509130 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 @@

${_("Send Email")}


+ +%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 2ae60789df7e..4d608100088b 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 @@

${_('Course-specific grade adjustment')}

%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 | 83 ++++++++++++------- 5 files changed, 62 insertions(+), 37 deletions(-) diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index c48efa91f9e7..a0b830d53541 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 @@ def instructor_dashboard_2(request, course_id): """ # 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 beca8db71202..98e85be3499b 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 ffc24a574b13..f9051f5260cb 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 27eea3d339b5..14eda493a8ce 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 626d1b8ec815..c97ab0af3211 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'] }
+
-
- ${_("Course ID")}: - ${ section_data['course_id'] } -
+
+

${_("Basic Course Information")}

-
- ${_("Students Enrolled")}: - ${ section_data['enrollment_count'] } -
+
    +
  • + + ${ section_data['course_org'] } +
  • -
    - ${_("Started")}: - ${ section_data['has_started'] } -
    +
  • + + ${ section_data['course_num'] } +
  • -
    - ${_("Ended")}: - ${ section_data['has_ended'] } -
    +
  • + + ${ section_data['course_name'] } +
  • + +
  • + + ${ section_data['course_display_name'] } +
  • + +
  • + -
    - ${_("Grade Cutoffs")}: - ${ section_data['grade_cutoffs'] } + %if section_data['has_started']: + ${_("Yes")} + %else: + ${_("No")} + %endif + +
  • + +
  • + + %if section_data['has_ended']: + ${_("Yes")} + %else: + ${_("No")} + %endif +
  • + +
  • + + ${ 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 @@

${_("Course Warnings")}:


%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 | 31 ++---- .../instructor_dashboard/send_email.coffee | 31 +----- .../instructor_dashboard/student_admin.coffee | 25 +---- .../src/instructor_dashboard/util.coffee | 76 +++++++++++-- .../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 693e832e5d3b..ae8d02d63ffa 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -5,6 +5,7 @@ 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 @@ def test_send_email_no_message(self): 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 @@ def setUp(self): ), 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 facf64858056..d3ab72f09814 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -8,6 +8,7 @@ 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 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 a0b830d53541..27ee39e042e6 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -18,7 +18,7 @@ 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 d3ef8498a8c7..e34ce10ec777 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 98e85be3499b..21e719566e5b 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" - - # 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() + @instructor_tasks = new (PendingInstructorTasks()) @$section - # 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 f9051f5260cb..040a4c1f1392 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" - - # 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() 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 14eda493a8ce..80958049879d 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 198c7e84a5d0..46c041048cb1 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 629532768ec0..ccce17eb5c1c 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' + 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' - width: 30 + minWidth: 80 , - id: 'task_input' - field: 'task_input' - name: 'Input' + 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' - width: 30 + minWidth: 80 , - id: 'task_id' - field: 'task_id' - name: 'Task ID' - width: 50 + id: 'status' + field: 'status' + name: 'Task Status' + minWidth: 80 , - id: 'created' - field: 'created' - name: 'Created' + 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 92f01dce1a2d..f6a9dd2663a4 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 30cf48f5057f..0299490dacdf 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 ae8d02d63ffa..06d0f16ab84d 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -770,8 +770,8 @@ def mock_get_task_completion_info(self, *args): # pylint: disable=unused-argume """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 FakeTask(object): '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 @@ def setUp(self): 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 d3ab72f09814..8ab1aa8a7622 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -690,21 +690,25 @@ def extract_task_features(task): """ # 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 27ee39e042e6..8b5c44ebf01a 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 018b7e9c57c0..9955f8ee115b 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 21e719566e5b..c481a33bb549 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 040a4c1f1392..e6108b10555c 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 c645fcf67e51..313e5b496763 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 54b04be5db6a..03c6b705b60d 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 80958049879d..7bdb37c0e9e3 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 46c041048cb1..d930dd4b1378 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 ccce17eb5c1c..09d2ae26f328 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 c97ab0af3211..7362014b0998 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 @@

${_("Basic Course Information")}

  • - %if section_data['has_started']: - ${_("Yes")} - %else: - ${_("No")} - %endif + ${_("Yes") if section_data['grade_cutoffs'] else _("No")}