From 57a57e8af74181c8c3cc68f9c3f190fc3ae149f7 Mon Sep 17 00:00:00 2001 From: Sarina Canelake Date: Mon, 21 Oct 2013 12:01:13 -0400 Subject: [PATCH] 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