Commit 57a57e8a by Sarina Canelake

Move PIT code into util.coffee

Add testing coverage
LMS-1242

Add "Edit This Course In Studio" link for studio courses
LMS-1291
parent 123e1810
......@@ -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)
......
......@@ -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),
......
......@@ -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,
}
......
......@@ -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:
......
......@@ -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.
......
......@@ -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()
......
......@@ -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
......
......@@ -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
......
......@@ -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
......@@ -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
......
......@@ -50,10 +50,14 @@
<section class="container">
<div class="instructor-dashboard-wrapper-2">
<div class="olddash-button-wrapper"><a href="${ old_dashboard_url }"> ${_("Back to Standard Dashboard")} </a></div>
%if studio_url:
## not checking access because if user can see this, they are at least course staff (with studio edit access)
<div class="studio-edit-link"><a href="${studio_url}" target="_blank">${_('Edit Course In Studio')}</a></div>
%endif
<section class="instructor-dashboard-content-2">
## <h1>Instructor Dashboard</h1>
<h1>${_("Instructor Dashboard")}</h1>
<hr />
## links which are tied to idash-sections below.
## the links are acativated and handled in instructor_dashboard.coffee
## when the javascript loads, it clicks on the first section
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment