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
5a11f75a
Commit
5a11f75a
authored
Oct 28, 2015
by
Afzal Wali
Committed by
Chris Dodge
Oct 29, 2015
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add the ability for self-service course survey reports
parent
d210ca6e
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
239 additions
and
1 deletions
+239
-1
lms/djangoapps/instructor/views/api.py
+23
-0
lms/djangoapps/instructor/views/api_urls.py
+2
-0
lms/djangoapps/instructor/views/instructor_dashboard.py
+2
-1
lms/djangoapps/instructor_task/api.py
+15
-0
lms/djangoapps/instructor_task/tasks.py
+13
-0
lms/djangoapps/instructor_task/tasks_helper.py
+57
-0
lms/djangoapps/instructor_task/tests/test_api.py
+7
-0
lms/djangoapps/instructor_task/tests/test_tasks_helper.py
+95
-0
lms/static/coffee/src/instructor_dashboard/data_download.coffee
+20
-0
lms/templates/instructor/instructor_dashboard_2/data_download.html
+5
-0
No files found.
lms/djangoapps/instructor/views/api.py
View file @
5a11f75a
...
...
@@ -1361,6 +1361,29 @@ def get_exec_summary_report(request, course_id):
@ensure_csrf_cookie
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
@require_level
(
'staff'
)
def
get_course_survey_results
(
request
,
course_id
):
"""
get the survey results report for the particular course.
"""
course_key
=
SlashSeparatedCourseKey
.
from_deprecated_string
(
course_id
)
try
:
instructor_task
.
api
.
submit_course_survey_report
(
request
,
course_key
)
status_response
=
_
(
"The survey report is being created."
" To view the status of the report, see Pending Instructor Tasks below."
)
except
AlreadyRunningError
:
status_response
=
_
(
"The survey report is currently being created."
" To view the status of the report, see Pending Instructor Tasks below."
" You will be able to download the report when it is complete."
)
return
JsonResponse
({
"status"
:
status_response
})
@ensure_csrf_cookie
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
@require_level
(
'staff'
)
def
get_proctored_exam_results
(
request
,
course_id
):
"""
get the proctored exam resultsreport for the particular course.
...
...
lms/djangoapps/instructor/views/api_urls.py
View file @
5a11f75a
...
...
@@ -115,6 +115,8 @@ urlpatterns = patterns(
'instructor.views.api.get_enrollment_report'
,
name
=
"get_enrollment_report"
),
url
(
r'get_exec_summary_report$'
,
'instructor.views.api.get_exec_summary_report'
,
name
=
"get_exec_summary_report"
),
url
(
r'get_course_survey_results$'
,
'instructor.views.api.get_course_survey_results'
,
name
=
"get_course_survey_results"
),
# Coupon Codes..
url
(
r'get_coupon_codes'
,
...
...
lms/djangoapps/instructor/views/instructor_dashboard.py
View file @
5a11f75a
...
...
@@ -169,7 +169,6 @@ def instructor_dashboard_2(request, course_id):
'disable_buttons'
:
disable_buttons
,
'analytics_dashboard_message'
:
analytics_dashboard_message
}
return
render_to_response
(
'instructor/instructor_dashboard_2/instructor_dashboard_2.html'
,
context
)
...
...
@@ -516,6 +515,8 @@ def _section_data_download(course, access):
'list_report_downloads_url'
:
reverse
(
'list_report_downloads'
,
kwargs
=
{
'course_id'
:
unicode
(
course_key
)}),
'calculate_grades_csv_url'
:
reverse
(
'calculate_grades_csv'
,
kwargs
=
{
'course_id'
:
unicode
(
course_key
)}),
'problem_grade_report_url'
:
reverse
(
'problem_grade_report'
,
kwargs
=
{
'course_id'
:
unicode
(
course_key
)}),
'course_has_survey'
:
True
if
course
.
course_survey_name
else
False
,
'course_survey_results_url'
:
reverse
(
'get_course_survey_results'
,
kwargs
=
{
'course_id'
:
unicode
(
course_key
)}),
}
return
section_data
...
...
lms/djangoapps/instructor_task/api.py
View file @
5a11f75a
...
...
@@ -26,6 +26,7 @@ from instructor_task.tasks import (
enrollment_report_features_csv
,
calculate_may_enroll_csv
,
exec_summary_report_csv
,
course_survey_report_csv
,
generate_certificates
,
proctored_exam_results_csv
)
...
...
@@ -436,6 +437,20 @@ def submit_executive_summary_report(request, course_key): # pylint: disable=inv
return
submit_task
(
request
,
task_type
,
task_class
,
course_key
,
task_input
,
task_key
)
def
submit_course_survey_report
(
request
,
course_key
):
# pylint: disable=invalid-name
"""
Submits a task to generate a HTML File containing the executive summary report.
Raises AlreadyRunningError if HTML File is already being updated.
"""
task_type
=
'course_survey_report'
task_class
=
course_survey_report_csv
task_input
=
{}
task_key
=
""
return
submit_task
(
request
,
task_type
,
task_class
,
course_key
,
task_input
,
task_key
)
def
submit_proctored_exam_results_report
(
request
,
course_key
,
features
):
# pylint: disable=invalid-name
"""
Submits a task to generate a HTML File containing the executive summary report.
...
...
lms/djangoapps/instructor_task/tasks.py
View file @
5a11f75a
...
...
@@ -42,6 +42,7 @@ from instructor_task.tasks_helper import (
upload_enrollment_report
,
upload_may_enroll_csv
,
upload_exec_summary_report
,
upload_course_survey_report
,
generate_students_certificates
,
upload_proctored_exam_results_report
)
...
...
@@ -228,6 +229,18 @@ def exec_summary_report_csv(entry_id, xmodule_instance_args):
@task
(
base
=
BaseInstructorTask
,
routing_key
=
settings
.
GRADES_DOWNLOAD_ROUTING_KEY
)
# pylint: disable=not-callable
def
course_survey_report_csv
(
entry_id
,
xmodule_instance_args
):
"""
Compute the survey report for a course and upload the
generated report to an S3 bucket for download.
"""
# Translators: This is a past-tense verb that is inserted into task progress messages as {action}.
action_name
=
ugettext_noop
(
'generated'
)
task_fn
=
partial
(
upload_course_survey_report
,
xmodule_instance_args
)
return
run_main_task
(
entry_id
,
task_fn
,
action_name
)
@task
(
base
=
BaseInstructorTask
,
routing_key
=
settings
.
GRADES_DOWNLOAD_ROUTING_KEY
)
# pylint: disable=not-callable
def
proctored_exam_results_csv
(
entry_id
,
xmodule_instance_args
):
"""
Compute proctored exam results report for a course and upload the
...
...
lms/djangoapps/instructor_task/tasks_helper.py
View file @
5a11f75a
...
...
@@ -29,6 +29,7 @@ from shoppingcart.models import (
PaidCourseRegistration
,
CourseRegCodeItem
,
InvoiceTransaction
,
Invoice
,
CouponRedemption
,
RegistrationCodeRedemption
,
CourseRegistrationCode
)
from
survey.models
import
SurveyAnswer
from
track.views
import
task_track
from
util.file
import
course_filename_prefix_generator
,
UniversalNewlineIterator
...
...
@@ -1307,6 +1308,62 @@ def upload_exec_summary_report(_xmodule_instance_args, _entry_id, course_id, _ta
return
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
def
upload_course_survey_report
(
_xmodule_instance_args
,
_entry_id
,
course_id
,
_task_input
,
action_name
):
# pylint: disable=invalid-name
"""
For a given `course_id`, generate a html report containing the survey results for a course.
"""
start_time
=
time
()
start_date
=
datetime
.
now
(
UTC
)
num_reports
=
1
task_progress
=
TaskProgress
(
action_name
,
num_reports
,
start_time
)
current_step
=
{
'step'
:
'Gathering course survey report information'
}
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
distinct_survey_fields_queryset
=
SurveyAnswer
.
objects
.
filter
(
course_key
=
course_id
)
.
values
(
'field_name'
)
.
distinct
()
survey_fields
=
[]
for
unique_field_row
in
distinct_survey_fields_queryset
:
survey_fields
.
append
(
unique_field_row
[
'field_name'
])
survey_fields
.
sort
()
user_survey_answers
=
OrderedDict
()
survey_answers_for_course
=
SurveyAnswer
.
objects
.
filter
(
course_key
=
course_id
)
for
survey_field_record
in
survey_answers_for_course
:
user_id
=
survey_field_record
.
user
.
id
if
user_id
not
in
user_survey_answers
.
keys
():
user_survey_answers
[
user_id
]
=
{}
user_survey_answers
[
user_id
][
survey_field_record
.
field_name
]
=
survey_field_record
.
field_value
header
=
[
"User ID"
,
"User Name"
,
"Email"
]
header
.
extend
(
survey_fields
)
csv_rows
=
[]
for
user_id
in
user_survey_answers
.
keys
():
row
=
[]
row
.
append
(
user_id
)
user_obj
=
User
.
objects
.
get
(
id
=
user_id
)
row
.
append
(
user_obj
.
username
)
row
.
append
(
user_obj
.
email
)
for
survey_field
in
survey_fields
:
row
.
append
(
user_survey_answers
[
user_id
]
.
get
(
survey_field
,
''
))
csv_rows
.
append
(
row
)
task_progress
.
attempted
=
task_progress
.
succeeded
=
len
(
csv_rows
)
task_progress
.
skipped
=
task_progress
.
total
-
task_progress
.
attempted
csv_rows
.
insert
(
0
,
header
)
current_step
=
{
'step'
:
'Uploading CSV'
}
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
# Perform the upload
upload_csv_to_report_store
(
csv_rows
,
'course_survey_results'
,
course_id
,
start_date
)
return
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
def
upload_proctored_exam_results_report
(
_xmodule_instance_args
,
_entry_id
,
course_id
,
_task_input
,
action_name
):
# pylint: disable=invalid-name
"""
For a given `course_id`, generate a CSV file containing
...
...
lms/djangoapps/instructor_task/tests/test_api.py
View file @
5a11f75a
...
...
@@ -20,6 +20,7 @@ from instructor_task.api import (
submit_detailed_enrollment_features_csv
,
submit_calculate_may_enroll_csv
,
submit_executive_summary_report
,
submit_course_survey_report
,
generate_certificates_for_all_students
,
)
...
...
@@ -231,6 +232,12 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa
)
self
.
_test_resubmission
(
api_call
)
def
test_submit_course_survey_report
(
self
):
api_call
=
lambda
:
submit_course_survey_report
(
self
.
create_task_request
(
self
.
instructor
),
self
.
course
.
id
)
self
.
_test_resubmission
(
api_call
)
def
test_submit_calculate_may_enroll
(
self
):
api_call
=
lambda
:
submit_calculate_may_enroll_csv
(
self
.
create_task_request
(
self
.
instructor
),
...
...
lms/djangoapps/instructor_task/tests/test_tasks_helper.py
View file @
5a11f75a
...
...
@@ -32,6 +32,7 @@ from verify_student.tests.factories import SoftwareSecurePhotoVerificationFactor
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
from
xmodule.partitions.partitions
import
Group
,
UserPartition
from
instructor_task.models
import
ReportStore
from
survey.models
import
SurveyForm
,
SurveyAnswer
from
instructor_task.tasks_helper
import
(
cohort_students_and_upload
,
upload_problem_responses_csv
,
...
...
@@ -41,6 +42,7 @@ from instructor_task.tasks_helper import (
upload_may_enroll_csv
,
upload_enrollment_report
,
upload_exec_summary_report
,
upload_course_survey_report
,
generate_students_certificates
,
)
from
instructor_analytics.basic
import
UNAVAILABLE
...
...
@@ -954,6 +956,99 @@ class TestExecutiveSummaryReport(TestReportMixin, InstructorTaskCourseTestCase):
@ddt.ddt
class
TestCourseSurveyReport
(
TestReportMixin
,
InstructorTaskCourseTestCase
):
"""
Tests that Course Survey report generation works.
"""
def
setUp
(
self
):
super
(
TestCourseSurveyReport
,
self
)
.
setUp
()
self
.
course
=
CourseFactory
.
create
()
self
.
question1
=
"question1"
self
.
question2
=
"question2"
self
.
question3
=
"question3"
self
.
answer1
=
"answer1"
self
.
answer2
=
"answer2"
self
.
answer3
=
"answer3"
self
.
student1
=
UserFactory
()
self
.
student2
=
UserFactory
()
self
.
test_survey_name
=
'TestSurvey'
self
.
test_form
=
'<input name="field1"></input>'
self
.
survey_form
=
SurveyForm
.
create
(
self
.
test_survey_name
,
self
.
test_form
)
self
.
survey1
=
SurveyAnswer
.
objects
.
create
(
user
=
self
.
student1
,
form
=
self
.
survey_form
,
course_key
=
self
.
course
.
id
,
field_name
=
self
.
question1
,
field_value
=
self
.
answer1
)
self
.
survey2
=
SurveyAnswer
.
objects
.
create
(
user
=
self
.
student1
,
form
=
self
.
survey_form
,
course_key
=
self
.
course
.
id
,
field_name
=
self
.
question2
,
field_value
=
self
.
answer2
)
self
.
survey3
=
SurveyAnswer
.
objects
.
create
(
user
=
self
.
student2
,
form
=
self
.
survey_form
,
course_key
=
self
.
course
.
id
,
field_name
=
self
.
question1
,
field_value
=
self
.
answer3
)
self
.
survey4
=
SurveyAnswer
.
objects
.
create
(
user
=
self
.
student2
,
form
=
self
.
survey_form
,
course_key
=
self
.
course
.
id
,
field_name
=
self
.
question2
,
field_value
=
self
.
answer2
)
self
.
survey5
=
SurveyAnswer
.
objects
.
create
(
user
=
self
.
student2
,
form
=
self
.
survey_form
,
course_key
=
self
.
course
.
id
,
field_name
=
self
.
question3
,
field_value
=
self
.
answer1
)
def
test_successfully_generate_course_survey_report
(
self
):
"""
Test that successfully generates the course survey report.
"""
task_input
=
{
'features'
:
[]}
with
patch
(
'instructor_task.tasks_helper._get_current_task'
):
result
=
upload_course_survey_report
(
None
,
None
,
self
.
course
.
id
,
task_input
,
'generating course survey report'
)
self
.
assertDictContainsSubset
({
'attempted'
:
2
,
'succeeded'
:
2
,
'failed'
:
0
},
result
)
@patch.dict
(
'django.conf.settings.FEATURES'
,
{
'ENABLE_PAID_COURSE_REGISTRATION'
:
True
})
def
test_generate_course_survey_report
(
self
):
"""
test to generate course survey report
and then test the report authenticity.
"""
task_input
=
{
'features'
:
[]}
with
patch
(
'instructor_task.tasks_helper._get_current_task'
):
result
=
upload_course_survey_report
(
None
,
None
,
self
.
course
.
id
,
task_input
,
'generating course survey report'
)
report_store
=
ReportStore
.
from_config
(
config_name
=
'GRADES_DOWNLOAD'
)
header_row
=
","
.
join
([
'User ID'
,
'User Name'
,
'Email'
,
self
.
question1
,
self
.
question2
,
self
.
question3
])
student1_row
=
","
.
join
([
str
(
self
.
student1
.
id
),
# pylint: disable=no-member
self
.
student1
.
username
,
self
.
student1
.
email
,
self
.
answer1
,
self
.
answer2
])
student2_row
=
","
.
join
([
str
(
self
.
student2
.
id
),
# pylint: disable=no-member
self
.
student2
.
username
,
self
.
student2
.
email
,
self
.
answer3
,
self
.
answer2
,
self
.
answer1
])
expected_data
=
[
header_row
,
student1_row
,
student2_row
]
self
.
assertDictContainsSubset
({
'attempted'
:
2
,
'succeeded'
:
2
,
'failed'
:
0
},
result
)
self
.
_verify_csv_file_report
(
report_store
,
expected_data
)
def
_verify_csv_file_report
(
self
,
report_store
,
expected_data
):
"""
Verify course survey data.
"""
report_csv_filename
=
report_store
.
links_for
(
self
.
course
.
id
)[
0
][
0
]
with
open
(
report_store
.
path_to
(
self
.
course
.
id
,
report_csv_filename
))
as
csv_file
:
csv_file_data
=
csv_file
.
read
()
for
data
in
expected_data
:
self
.
assertIn
(
data
,
csv_file_data
)
@ddt.ddt
class
TestStudentReport
(
TestReportMixin
,
InstructorTaskCourseTestCase
):
"""
Tests that CSV student profile report generation works.
...
...
lms/static/coffee/src/instructor_dashboard/data_download.coffee
View file @
5a11f75a
...
...
@@ -75,6 +75,7 @@ class DataDownload
@
$list_studs_btn
=
@
$section
.
find
(
"input[name='list-profiles']'"
)
@
$list_studs_csv_btn
=
@
$section
.
find
(
"input[name='list-profiles-csv']'"
)
@
$list_proctored_exam_results_csv_btn
=
@
$section
.
find
(
"input[name='proctored-exam-results-report']'"
)
@
$survey_results_csv_btn
=
@
$section
.
find
(
"input[name='survey-results-report']'"
)
@
$list_may_enroll_csv_btn
=
@
$section
.
find
(
"input[name='list-may-enroll-csv']"
)
@
$list_problem_responses_csv_input
=
@
$section
.
find
(
"input[name='problem-location']"
)
@
$list_problem_responses_csv_btn
=
@
$section
.
find
(
"input[name='list-problem-responses-csv']"
)
...
...
@@ -121,6 +122,25 @@ class DataDownload
@
$reports_request_response
.
text
data
[
'status'
]
$
(
".msg-confirm"
).
css
({
"display"
:
"block"
})
# attach click handlers
# The list_proctored_exam_results case is always CSV
@
$survey_results_csv_btn
.
click
(
e
)
=>
url
=
@
$survey_results_csv_btn
.
data
'endpoint'
# display html from survey results config endpoint
$
.
ajax
dataType
:
'json'
url
:
url
error
:
(
std_ajax_err
)
=>
@
clear_display
()
@
$reports_request_response_error
.
text
gettext
(
"Error generating survey results. Please try again."
)
$
(
".msg-error"
).
css
({
"display"
:
"block"
})
success
:
(
data
)
=>
@
clear_display
()
@
$reports_request_response
.
text
data
[
'status'
]
$
(
".msg-confirm"
).
css
({
"display"
:
"block"
})
# this handler binds to both the download
# and the csv button
@
$list_studs_csv_btn
.
click
(
e
)
=>
...
...
lms/templates/instructor/instructor_dashboard_2/data_download.html
View file @
5a11f75a
...
...
@@ -40,6 +40,11 @@
<p><input
type=
"button"
name=
"proctored-exam-results-report"
value=
"${_("
Generate
Proctored
Exam
Results
Report
")}"
data-endpoint=
"${ section_data['list_proctored_results_url'] }"
/></p>
%endif
%if section_data['course_has_survey']:
<p>
${_("Click to generate a CSV file of survey results for this course.")}
</p>
<p><input
type=
"button"
name=
"survey-results-report"
value=
"${_("
Generate
Survey
Results
Report
")}"
data-endpoint=
"${ section_data['course_survey_results_url'] }"
/></p>
%endif
<p>
${_("To generate a CSV file that lists all student answers to a given problem, enter the location of the problem (from its Staff Debug Info).")}
</p>
<p>
...
...
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