Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
9f104eb6
Commit
9f104eb6
authored
Nov 01, 2013
by
Sarina Canelake
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1558 from edx/sarina/beta_email_background_history
Implement background email tasks on student dash
parents
f6b69eab
fff36275
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
153 additions
and
69 deletions
+153
-69
lms/djangoapps/instructor/tests/test_api.py
+31
-8
lms/djangoapps/instructor/views/api.py
+62
-42
lms/djangoapps/instructor/views/api_urls.py
+3
-1
lms/djangoapps/instructor/views/instructor_dashboard.py
+11
-7
lms/envs/common.py
+2
-1
lms/static/coffee/src/instructor_dashboard/send_email.coffee
+20
-0
lms/static/coffee/src/instructor_dashboard/util.coffee
+8
-4
lms/static/sass/course/instructor/_instructor_2.scss
+2
-4
lms/templates/instructor/instructor_dashboard_2/send_email.html
+14
-2
No files found.
lms/djangoapps/instructor/tests/test_api.py
View file @
9f104eb6
...
...
@@ -102,11 +102,6 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
CourseEnrollment
.
enroll
(
self
.
user
,
self
.
course
.
id
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
'test'
)
def
test_deny_students_update_enrollment
(
self
):
url
=
reverse
(
'students_update_enrollment'
,
kwargs
=
{
'course_id'
:
self
.
course
.
id
})
response
=
self
.
client
.
get
(
url
,
{})
self
.
assertEqual
(
response
.
status_code
,
403
)
def
test_staff_level
(
self
):
"""
Ensure that an enrolled student can't access staff or instructor endpoints.
...
...
@@ -126,11 +121,16 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
'update_forum_role_membership'
,
'proxy_legacy_analytics'
,
'send_email'
,
'list_background_email_tasks'
,
]
for
endpoint
in
staff_level_endpoints
:
url
=
reverse
(
endpoint
,
kwargs
=
{
'course_id'
:
self
.
course
.
id
})
response
=
self
.
client
.
get
(
url
,
{})
self
.
assertEqual
(
response
.
status_code
,
403
)
self
.
assertEqual
(
response
.
status_code
,
403
,
msg
=
"Student should not be allowed to access endpoint "
+
endpoint
)
def
test_instructor_level
(
self
):
"""
...
...
@@ -140,13 +140,16 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
'modify_access'
,
'list_course_role_members'
,
'reset_student_attempts'
,
'list_instructor_tasks'
,
'update_forum_role_membership'
,
]
for
endpoint
in
instructor_level_endpoints
:
url
=
reverse
(
endpoint
,
kwargs
=
{
'course_id'
:
self
.
course
.
id
})
response
=
self
.
client
.
get
(
url
,
{})
self
.
assertEqual
(
response
.
status_code
,
403
)
self
.
assertEqual
(
response
.
status_code
,
403
,
msg
=
"Staff should not be allowed to access endpoint "
+
endpoint
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MIXED_MODULESTORE
)
...
...
@@ -692,6 +695,7 @@ class TestInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollmentTestCase)
})
print
response
.
content
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertTrue
(
act
.
called
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MIXED_MODULESTORE
)
...
...
@@ -871,6 +875,25 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
self
.
assertEqual
(
actual_tasks
,
expected_tasks
)
@patch.object
(
instructor_task
.
api
,
'get_instructor_task_history'
)
def
test_list_background_email_tasks
(
self
,
act
):
"""Test list of background email tasks."""
act
.
return_value
=
self
.
tasks
url
=
reverse
(
'list_background_email_tasks'
,
kwargs
=
{
'course_id'
:
self
.
course
.
id
})
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
]
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
...
...
lms/djangoapps/instructor/views/api.py
View file @
9f104eb6
...
...
@@ -157,7 +157,7 @@ def require_level(level):
`level` is in ['instructor', 'staff']
if `level` is 'staff', instructors will also be allowed, even
if they are not in
t
he staff group.
if they are not in
t
he staff group.
"""
if
level
not
in
[
'instructor'
,
'staff'
]:
raise
ValueError
(
"unrecognized level '{}'"
.
format
(
level
))
...
...
@@ -643,13 +643,68 @@ def rescore_problem(request, course_id):
return
JsonResponse
(
response_payload
)
def
extract_task_features
(
task
):
"""
Convert task to dict for json rendering.
Expects tasks have the following features:
* task_type (str, type of task)
* task_input (dict, input(s) to the task)
* task_id (str, celery id of the task)
* requester (str, username who submitted the task)
* task_state (str, state of task eg PROGRESS, COMPLETED)
* created (datetime, when the task was completed)
* task_output (optional)
"""
# Pull out information from the task
features
=
[
'task_type'
,
'task_input'
,
'task_id'
,
'requester'
,
'task_state'
]
task_feature_dict
=
{
feature
:
str
(
getattr
(
task
,
feature
))
for
feature
in
features
}
# Some information (created, duration, status, task message) require additional formatting
task_feature_dict
[
'created'
]
=
task
.
created
.
isoformat
()
# Get duration info, if known
duration_sec
=
'unknown'
if
hasattr
(
task
,
'task_output'
)
and
task
.
task_output
is
not
None
:
try
:
task_output
=
json
.
loads
(
task
.
task_output
)
except
ValueError
:
log
.
error
(
"Could not parse task output as valid json; task output:
%
s"
,
task
.
task_output
)
else
:
if
'duration_ms'
in
task_output
:
duration_sec
=
int
(
task_output
[
'duration_ms'
]
/
1000.0
)
task_feature_dict
[
'duration_sec'
]
=
duration_sec
# Get progress status message & success information
success
,
task_message
=
get_task_completion_info
(
task
)
status
=
_
(
"Complete"
)
if
success
else
_
(
"Incomplete"
)
task_feature_dict
[
'status'
]
=
status
task_feature_dict
[
'task_message'
]
=
task_message
return
task_feature_dict
@ensure_csrf_cookie
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
@require_level
(
'instructor'
)
@require_level
(
'staff'
)
def
list_background_email_tasks
(
request
,
course_id
):
# pylint: disable=unused-argument
"""
List background email tasks.
"""
task_type
=
'bulk_course_email'
# Specifying for the history of a single task type
tasks
=
instructor_task
.
api
.
get_instructor_task_history
(
course_id
,
task_type
=
task_type
)
response_payload
=
{
'tasks'
:
map
(
extract_task_features
,
tasks
),
}
return
JsonResponse
(
response_payload
)
@ensure_csrf_cookie
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
@require_level
(
'staff'
)
def
list_instructor_tasks
(
request
,
course_id
):
"""
List instructor tasks.
Limited to instructor access.
Takes optional query paremeters.
- With no arguments, lists running tasks.
...
...
@@ -670,50 +725,15 @@ def list_instructor_tasks(request, course_id):
if
problem_urlname
:
module_state_key
=
_msk_from_problem_urlname
(
course_id
,
problem_urlname
)
if
student
:
# Specifying for a single student's history on this problem
tasks
=
instructor_task
.
api
.
get_instructor_task_history
(
course_id
,
module_state_key
,
student
)
else
:
# Specifying for single problem's history
tasks
=
instructor_task
.
api
.
get_instructor_task_history
(
course_id
,
module_state_key
)
else
:
# If no problem or student, just get currently running tasks
tasks
=
instructor_task
.
api
.
get_running_instructor_tasks
(
course_id
)
def
extract_task_features
(
task
):
"""
Convert task to dict for json rendering.
Expects tasks have the following features:
* task_type (str, type of task)
* task_input (dict, input(s) to the task)
* task_id (str, celery id of the task)
* requester (str, username who submitted the task)
* task_state (str, state of task eg PROGRESS, COMPLETED)
* created (datetime, when the task was completed)
* task_output (optional)
"""
# Pull out information from the task
features
=
[
'task_type'
,
'task_input'
,
'task_id'
,
'requester'
,
'task_state'
]
task_feature_dict
=
{
feature
:
str
(
getattr
(
task
,
feature
))
for
feature
in
features
}
# Some information (created, duration, status, task message) require additional formatting
task_feature_dict
[
'created'
]
=
task
.
created
.
isoformat
()
# Get duration info, if known
duration_sec
=
'unknown'
if
hasattr
(
task
,
'task_output'
)
and
task
.
task_output
is
not
None
:
try
:
task_output
=
json
.
loads
(
task
.
task_output
)
except
ValueError
:
log
.
error
(
"Could not parse task output as valid json; task output:
%
s"
,
task
.
task_output
)
else
:
if
'duration_ms'
in
task_output
:
duration_sec
=
int
(
task_output
[
'duration_ms'
]
/
1000.0
)
task_feature_dict
[
'duration_sec'
]
=
duration_sec
# Get progress status message & success information
success
,
task_message
=
get_task_completion_info
(
task
)
status
=
_
(
"Complete"
)
if
success
else
_
(
"Incomplete"
)
task_feature_dict
[
'status'
]
=
status
task_feature_dict
[
'task_message'
]
=
task_message
return
task_feature_dict
response_payload
=
{
'tasks'
:
map
(
extract_task_features
,
tasks
),
}
...
...
@@ -901,7 +921,7 @@ def proxy_legacy_analytics(request, course_id):
try
:
res
=
requests
.
get
(
url
)
except
Exception
:
except
Exception
:
# pylint: disable=broad-except
log
.
exception
(
"Error requesting from analytics server at
%
s"
,
url
)
return
HttpResponse
(
"Error requesting from analytics server."
,
status
=
500
)
...
...
lms/djangoapps/instructor/views/api_urls.py
View file @
9f104eb6
...
...
@@ -27,6 +27,8 @@ urlpatterns = patterns('', # nopep8
'instructor.views.api.rescore_problem'
,
name
=
"rescore_problem"
),
url
(
r'^list_instructor_tasks$'
,
'instructor.views.api.list_instructor_tasks'
,
name
=
"list_instructor_tasks"
),
url
(
r'^list_background_email_tasks$'
,
'instructor.views.api.list_background_email_tasks'
,
name
=
"list_background_email_tasks"
),
url
(
r'^list_forum_members$'
,
'instructor.views.api.list_forum_members'
,
name
=
"list_forum_members"
),
url
(
r'^update_forum_role_membership$'
,
...
...
@@ -34,5 +36,5 @@ urlpatterns = patterns('', # nopep8
url
(
r'^proxy_legacy_analytics$'
,
'instructor.views.api.proxy_legacy_analytics'
,
name
=
"proxy_legacy_analytics"
),
url
(
r'^send_email$'
,
'instructor.views.api.send_email'
,
name
=
"send_email"
)
'instructor.views.api.send_email'
,
name
=
"send_email"
)
,
)
lms/djangoapps/instructor/views/instructor_dashboard.py
View file @
9f104eb6
...
...
@@ -27,7 +27,7 @@ from bulk_email.models import CourseAuthorization
@ensure_csrf_cookie
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
def
instructor_dashboard_2
(
request
,
course_id
):
"""
Display the instructor dashboard for a course.
"""
"""
Display the instructor dashboard for a course.
"""
course
=
get_course_by_id
(
course_id
,
depth
=
None
)
is_studio_course
=
(
modulestore
()
.
get_modulestore_type
(
course_id
)
==
MONGO_MODULESTORE_TYPE
)
...
...
@@ -45,11 +45,11 @@ def instructor_dashboard_2(request, course_id):
raise
Http404
()
sections
=
[
_section_course_info
(
course_id
),
_section_course_info
(
course_id
,
access
),
_section_membership
(
course_id
,
access
),
_section_student_admin
(
course_id
,
access
),
_section_data_download
(
course_id
),
_section_analytics
(
course_id
),
_section_data_download
(
course_id
,
access
),
_section_analytics
(
course_id
,
access
),
]
# Gate access to course email by feature flag & by course-specific authorization
...
...
@@ -91,7 +91,7 @@ section_display_name will be used to generate link titles in the nav bar.
"""
# pylint: disable=W0105
def
_section_course_info
(
course_id
):
def
_section_course_info
(
course_id
,
access
):
""" Provide data for the corresponding dashboard section """
course
=
get_course_by_id
(
course_id
,
depth
=
None
)
...
...
@@ -100,6 +100,7 @@ def _section_course_info(course_id):
section_data
=
{
'section_key'
:
'course_info'
,
'section_display_name'
:
_
(
'Course Info'
),
'access'
:
access
,
'course_id'
:
course_id
,
'course_org'
:
course_org
,
'course_num'
:
course_num
,
...
...
@@ -157,11 +158,12 @@ def _section_student_admin(course_id, access):
return
section_data
def
_section_data_download
(
course_id
):
def
_section_data_download
(
course_id
,
access
):
""" Provide data for the corresponding dashboard section """
section_data
=
{
'section_key'
:
'data_download'
,
'section_display_name'
:
_
(
'Data Download'
),
'access'
:
access
,
'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
}),
...
...
@@ -183,15 +185,17 @@ def _section_send_email(course_id, access, course):
'send_email'
:
reverse
(
'send_email'
,
kwargs
=
{
'course_id'
:
course_id
}),
'editor'
:
email_editor
,
'list_instructor_tasks_url'
:
reverse
(
'list_instructor_tasks'
,
kwargs
=
{
'course_id'
:
course_id
}),
'email_background_tasks_url'
:
reverse
(
'list_background_email_tasks'
,
kwargs
=
{
'course_id'
:
course_id
}),
}
return
section_data
def
_section_analytics
(
course_id
):
def
_section_analytics
(
course_id
,
access
):
""" Provide data for the corresponding dashboard section """
section_data
=
{
'section_key'
:
'analytics'
,
'section_display_name'
:
_
(
'Analytics'
),
'access'
:
access
,
'get_distribution_url'
:
reverse
(
'get_distribution'
,
kwargs
=
{
'course_id'
:
course_id
}),
'proxy_legacy_analytics_url'
:
reverse
(
'proxy_legacy_analytics'
,
kwargs
=
{
'course_id'
:
course_id
}),
}
...
...
lms/envs/common.py
View file @
9f104eb6
...
...
@@ -118,7 +118,8 @@ MITX_FEATURES = {
'ENABLE_INSTRUCTOR_EMAIL'
:
True
,
# If True and ENABLE_INSTRUCTOR_EMAIL: Forces email to be explicitly turned on
# for each course via django-admin interface.
# If False and ENABLE_INSTRUCTOR_EMAIL: Email will be turned on by default for all courses.
# If False and ENABLE_INSTRUCTOR_EMAIL: Email will be turned on by default
# for all Mongo-backed courses.
'REQUIRE_COURSE_EMAIL_AUTH'
:
True
,
# enable analytics server.
...
...
lms/static/coffee/src/instructor_dashboard/send_email.coffee
View file @
9f104eb6
...
...
@@ -10,6 +10,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
PendingInstructorTasks
=
->
window
.
InstructorDashboard
.
util
.
PendingInstructorTasks
create_task_list_table
=
->
window
.
InstructorDashboard
.
util
.
create_task_list_table
.
apply
this
,
arguments
class
SendEmail
constructor
:
(
@
$container
)
->
...
...
@@ -20,6 +21,9 @@ class SendEmail
@
$btn_send
=
@
$container
.
find
(
"input[name='send']'"
)
@
$task_response
=
@
$container
.
find
(
".request-response"
)
@
$request_response_error
=
@
$container
.
find
(
".request-response-error"
)
@
$history_request_response_error
=
@
$container
.
find
(
".history-request-response-error"
)
@
$btn_task_history_email
=
@
$container
.
find
(
"input[name='task-history-email']'"
)
@
$table_task_history_email
=
@
$container
.
find
(
".task-history-email-table"
)
# attach click handlers
...
...
@@ -63,6 +67,22 @@ class SendEmail
@
$task_response
.
empty
()
@
$request_response_error
.
empty
()
# list task history for email
@
$btn_task_history_email
.
click
=>
url
=
@
$btn_task_history_email
.
data
'endpoint'
$
.
ajax
dataType
:
'json'
url
:
url
success
:
(
data
)
=>
if
data
.
tasks
.
length
create_task_list_table
@
$table_task_history_email
,
data
.
tasks
else
@
$history_request_response_error
.
text
gettext
(
"There is no email history for this course."
)
# Enable the msg-warning css display
$
(
".msg-warning"
).
css
({
"display"
:
"block"
})
error
:
std_ajax_err
=>
@
$history_request_response_error
.
text
gettext
(
"There was an error obtaining email task history for this course."
)
fail_with_error
:
(
msg
)
->
console
.
warn
msg
@
$task_response
.
empty
()
...
...
lms/static/coffee/src/instructor_dashboard/util.coffee
View file @
9f104eb6
...
...
@@ -36,7 +36,7 @@ create_task_list_table = ($table_tasks, tasks_data) ->
enableCellNavigation
:
true
enableColumnReorder
:
false
autoHeight
:
true
rowHeight
:
6
0
rowHeight
:
10
0
forceFitColumns
:
true
columns
=
[
...
...
@@ -120,7 +120,7 @@ class PendingInstructorTasks
# start polling for task list
# if the list is in the DOM
if
@
$table_running_tasks
.
length
>
0
if
@
$table_running_tasks
.
length
# reload every 20 seconds.
TASK_LIST_POLL_INTERVAL
=
20000
@
reload_running_tasks_list
()
...
...
@@ -132,8 +132,12 @@ class PendingInstructorTasks
$
.
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"
success
:
(
data
)
=>
if
data
.
tasks
.
length
create_task_list_table
@
$table_running_tasks
,
data
.
tasks
else
console
.
log
"No pending instructor tasks to display"
error
:
std_ajax_err
=>
console
.
error
"Error finding pending instructor tasks to display"
### /Pending Instructor Tasks Section ####
# export for use
...
...
lms/static/sass/course/instructor/_instructor_2.scss
View file @
9f104eb6
...
...
@@ -42,10 +42,8 @@
.msg-warning
{
border-top
:
2px
solid
$warning-color
;
background
:
tint
(
$warning-color
,
95%
);
.copy
{
color
:
$warning-color
;
}
display
:
none
;
color
:
$warning-color
;
}
// TYPE: confirm
...
...
lms/templates/instructor/instructor_dashboard_2/send_email.html
View file @
9f104eb6
...
...
@@ -59,11 +59,23 @@
<div
class=
"running-tasks-container action-type-container"
>
<hr>
<h2>
${_("Pending Instructor Tasks")}
</h2>
<p>
${_("
The status for any active tasks
appears in a table below.")}
</p>
<p>
${_("
Email actions run in the background. The status for any active tasks - including email tasks -
appears in a table below.")}
</p>
<br
/>
<div
class=
"running-tasks-table"
data-endpoint=
"${ section_data['list_instructor_tasks_url'] }"
></div>
</div>
%endif
<hr>
<div
class=
"vert-left email-background"
id=
"section-task-history"
>
<h2>
${_("Email Task History")}
</h2>
<p>
${_("To see the status for all bulk email tasks ever submitted for this course, click on this button:")}
</p>
<br/>
<input
type=
"button"
name=
"task-history-email"
value=
"${_("
Show
Email
Task
History
")}"
data-endpoint=
"${ section_data['email_background_tasks_url'] }"
>
<div
class=
"history-request-response-error msg msg-warning copy"
></div>
<div
class=
"task-history-email-table"
></div>
</div>
%endif
</div>
<!-- end section send-email -->
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment