Acceptance tests for grades download
Refactor instructor dash acceptance tests. Add additional acceptance tests for buttons on 'Data Download' tab. LMS-58
This commit is contained in:
@@ -54,6 +54,11 @@ def register_by_course_id(course_id, username='robot', password='test', is_staff
|
||||
|
||||
@world.absorb
|
||||
def enroll_user(user, course_id):
|
||||
# Activate user
|
||||
registration = world.RegistrationFactory(user=user)
|
||||
registration.register(user)
|
||||
registration.activate()
|
||||
# Enroll them in the course
|
||||
CourseEnrollment.enroll(user, course_id)
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ Feature: LMS.Instructor Dash Bulk Email
|
||||
I want to send email to staff and students in a course.
|
||||
|
||||
Scenario: Send bulk email
|
||||
Given I am "<Role>" for a course
|
||||
Given there is a course with a staff, instructor and student
|
||||
And I am logged in to the course as "<Role>"
|
||||
When I send email to "<Recipient>"
|
||||
Then Email is sent to "<Recipient>"
|
||||
|
||||
|
||||
@@ -14,11 +14,11 @@ from django.conf import settings
|
||||
from courseware.tests.factories import StaffFactory, InstructorFactory
|
||||
|
||||
|
||||
@step(u'Given I am "([^"]*)" for a course')
|
||||
def i_am_an_instructor(step, role): # pylint: disable=W0613
|
||||
|
||||
# Store the role
|
||||
assert_in(role, ['instructor', 'staff'])
|
||||
@step(u'Given there is a course with a staff, instructor and student')
|
||||
def make_populated_course(step): # pylint: disable=unused-argument
|
||||
## This is different than the function defined in common.py because it enrolls
|
||||
## a staff, instructor, and student member regardless of what `role` is, then
|
||||
## logs `role` in. This is to ensure we have 3 class participants to email.
|
||||
|
||||
# Clear existing courses to avoid conflicts
|
||||
world.clear_courses()
|
||||
@@ -26,56 +26,37 @@ def i_am_an_instructor(step, role): # pylint: disable=W0613
|
||||
# Create a new course
|
||||
course = world.CourseFactory.create(
|
||||
org='edx',
|
||||
number='999',
|
||||
display_name='Test Course'
|
||||
number='888',
|
||||
display_name='Bulk Email Test Course'
|
||||
)
|
||||
world.course_id = 'edx/999/Test_Course'
|
||||
world.bulk_email_course_id = 'edx/888/Bulk_Email_Test_Course'
|
||||
|
||||
try:
|
||||
# See if we've defined the instructor & staff user yet
|
||||
world.instructor
|
||||
world.bulk_email_instructor
|
||||
except AttributeError:
|
||||
# Make & register an instructor for the course
|
||||
world.instructor = InstructorFactory(course=course.location)
|
||||
world.enroll_user(world.instructor, world.course_id)
|
||||
world.bulk_email_instructor = InstructorFactory(course=course.location)
|
||||
world.enroll_user(world.bulk_email_instructor, world.bulk_email_course_id)
|
||||
|
||||
# Make & register a staff member
|
||||
world.staff = StaffFactory(course=course.location)
|
||||
world.enroll_user(world.staff, world.course_id)
|
||||
world.bulk_email_staff = StaffFactory(course=course.location)
|
||||
world.enroll_user(world.bulk_email_staff, world.bulk_email_course_id)
|
||||
|
||||
# Make & register a student
|
||||
world.register_by_course_id(
|
||||
'edx/999/Test_Course',
|
||||
'edx/888/Bulk_Email_Test_Course',
|
||||
username='student',
|
||||
password='test',
|
||||
is_staff=False
|
||||
)
|
||||
|
||||
# Log in as the an instructor or staff for the course
|
||||
my_email = None
|
||||
if role == 'instructor':
|
||||
my_email = world.instructor.email
|
||||
world.log_in(
|
||||
username=world.instructor.username,
|
||||
password='test',
|
||||
email=my_email,
|
||||
name=world.instructor.profile.name
|
||||
)
|
||||
else:
|
||||
my_email = world.staff.email
|
||||
world.log_in(
|
||||
username=world.staff.username,
|
||||
password='test',
|
||||
email=world.staff.email,
|
||||
name=world.staff.profile.name
|
||||
)
|
||||
|
||||
# Store the expected recipients
|
||||
# given each "send to" option
|
||||
staff_emails = [world.bulk_email_staff.email, world.bulk_email_instructor.email]
|
||||
world.expected_addresses = {
|
||||
'myself': [my_email],
|
||||
'course staff': [world.staff.email, world.instructor.email],
|
||||
'students, staff, and instructors': [world.staff.email, world.instructor.email, 'student@edx.org']
|
||||
'course staff': staff_emails,
|
||||
'students, staff, and instructors': staff_emails + ['student@edx.org']
|
||||
}
|
||||
|
||||
|
||||
@@ -88,8 +69,35 @@ SEND_TO_OPTIONS = {
|
||||
}
|
||||
|
||||
|
||||
@step(u'I am logged in to the course as "([^"]*)"')
|
||||
def log_into_the_course(step, role): # pylint: disable=unused-argument
|
||||
# Store the role
|
||||
assert_in(role, ['instructor', 'staff'])
|
||||
|
||||
# Log in as the an instructor or staff for the course
|
||||
my_email = world.bulk_email_instructor.email
|
||||
if role == 'instructor':
|
||||
world.log_in(
|
||||
username=world.bulk_email_instructor.username,
|
||||
password='test',
|
||||
email=my_email,
|
||||
name=world.bulk_email_instructor.profile.name
|
||||
)
|
||||
else:
|
||||
my_email = world.bulk_email_staff.email
|
||||
world.log_in(
|
||||
username=world.bulk_email_staff.username,
|
||||
password='test',
|
||||
email=my_email,
|
||||
name=world.bulk_email_staff.profile.name
|
||||
)
|
||||
|
||||
# Store the "myself" send to option
|
||||
world.expected_addresses['myself'] = [my_email]
|
||||
|
||||
|
||||
@step(u'I send email to "([^"]*)"')
|
||||
def when_i_send_an_email(step, recipient):
|
||||
def when_i_send_an_email(step, recipient): # pylint: disable=unused-argument
|
||||
|
||||
# Check that the recipient is valid
|
||||
assert_in(
|
||||
@@ -107,8 +115,8 @@ def when_i_send_an_email(step, recipient):
|
||||
call_command('loaddata', 'course_email_template.json')
|
||||
|
||||
# Go to the email section of the instructor dash
|
||||
world.visit('/courses/edx/999/Test_Course')
|
||||
world.css_click('a[href="/courses/edx/999/Test_Course/instructor"]')
|
||||
world.visit('/courses/edx/888/Bulk_Email_Test_Course')
|
||||
world.css_click('a[href="/courses/edx/888/Bulk_Email_Test_Course/instructor"]')
|
||||
world.css_click('div.beta-button-wrapper>a')
|
||||
world.css_click('a[data-section="send_email"]')
|
||||
|
||||
@@ -138,7 +146,7 @@ UNSUBSCRIBE_MSG = 'To stop receiving email like this'
|
||||
|
||||
|
||||
@step(u'Email is sent to "([^"]*)"')
|
||||
def then_the_email_is_sent(step, recipient):
|
||||
def then_the_email_is_sent(step, recipient): # pylint: disable=unused-argument
|
||||
# Check that the recipient is valid
|
||||
assert_in(
|
||||
recipient, SEND_TO_OPTIONS,
|
||||
|
||||
103
lms/djangoapps/instructor/features/common.py
Normal file
103
lms/djangoapps/instructor/features/common.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
Define common steps for instructor dashboard acceptance tests.
|
||||
"""
|
||||
|
||||
# pylint: disable=C0111
|
||||
# pylint: disable=W0621
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_in # pylint: disable=E0611
|
||||
|
||||
from courseware.tests.factories import StaffFactory, InstructorFactory
|
||||
|
||||
|
||||
@step(u'Given I am "([^"]*)" for a course')
|
||||
def i_am_staff_or_instructor(step, role): # pylint: disable=unused-argument
|
||||
## In summary: makes a test course, makes a new Staff or Instructor user
|
||||
## (depending on `role`), and logs that user in to the course
|
||||
|
||||
# Store the role
|
||||
assert_in(role, ['instructor', 'staff'])
|
||||
|
||||
# Clear existing courses to avoid conflicts
|
||||
world.clear_courses()
|
||||
|
||||
# Create a new course
|
||||
course = world.CourseFactory.create(
|
||||
org='edx',
|
||||
number='999',
|
||||
display_name='Test Course'
|
||||
)
|
||||
|
||||
world.course_id = 'edx/999/Test_Course'
|
||||
world.role = 'instructor'
|
||||
# Log in as the an instructor or staff for the course
|
||||
if role == 'instructor':
|
||||
# Make & register an instructor for the course
|
||||
world.instructor = InstructorFactory(course=course.location)
|
||||
world.enroll_user(world.instructor, world.course_id)
|
||||
|
||||
world.log_in(
|
||||
username=world.instructor.username,
|
||||
password='test',
|
||||
email=world.instructor.email,
|
||||
name=world.instructor.profile.name
|
||||
)
|
||||
|
||||
else:
|
||||
world.role = 'staff'
|
||||
# Make & register a staff member
|
||||
world.staff = StaffFactory(course=course.location)
|
||||
world.enroll_user(world.staff, world.course_id)
|
||||
|
||||
world.log_in(
|
||||
username=world.staff.username,
|
||||
password='test',
|
||||
email=world.staff.email,
|
||||
name=world.staff.profile.name
|
||||
)
|
||||
|
||||
|
||||
def go_to_section(section_name):
|
||||
# section name should be one of
|
||||
# course_info, membership, student_admin, data_download, analytics, send_email
|
||||
world.visit('/courses/edx/999/Test_Course')
|
||||
world.css_click('a[href="/courses/edx/999/Test_Course/instructor"]')
|
||||
world.css_click('div.beta-button-wrapper>a')
|
||||
world.css_click('a[data-section="{0}"]'.format(section_name))
|
||||
|
||||
|
||||
@step(u'I click "([^"]*)"')
|
||||
def click_a_button(step, button): # pylint: disable=unused-argument
|
||||
|
||||
if button == "Generate Grade Report":
|
||||
# Go to the data download section of the instructor dash
|
||||
go_to_section("data_download")
|
||||
|
||||
# Click generate grade report button
|
||||
world.css_click('input[name="calculate-grades-csv"]')
|
||||
|
||||
# Expect to see a message that grade report is being generated
|
||||
expected_msg = "Your grade report is being generated! You can view the status of the generation task in the 'Pending Instructor Tasks' section."
|
||||
world.wait_for_visible('#grade-request-response')
|
||||
assert_in(
|
||||
expected_msg, world.css_text('#grade-request-response'),
|
||||
msg="Could not find grade report generation success message."
|
||||
)
|
||||
|
||||
elif button == "Grading Configuration":
|
||||
# Go to the data download section of the instructor dash
|
||||
go_to_section("data_download")
|
||||
|
||||
world.css_click('input[name="dump-gradeconf"]')
|
||||
|
||||
elif button == "List enrolled students' profile information":
|
||||
# Go to the data download section of the instructor dash
|
||||
go_to_section("data_download")
|
||||
|
||||
world.css_click('input[name="list-profiles"]')
|
||||
|
||||
else:
|
||||
raise ValueError("Unrecognized button option " + button)
|
||||
36
lms/djangoapps/instructor/features/data_download.feature
Normal file
36
lms/djangoapps/instructor/features/data_download.feature
Normal file
@@ -0,0 +1,36 @@
|
||||
@shard_2
|
||||
Feature: LMS.Instructor Dash Data Download
|
||||
As an instructor or course staff,
|
||||
In order to manage my class
|
||||
I want to view and download data information about my students.
|
||||
|
||||
### todos when more time can be spent on instructor dashboard
|
||||
#Scenario: Download profile information as a CSV
|
||||
#Scenario: Download student anonymized IDs as a CSV
|
||||
|
||||
Scenario: List enrolled students' profile information
|
||||
Given I am "<Role>" for a course
|
||||
When I click "List enrolled students' profile information"
|
||||
Then I see a table of student profiles
|
||||
Examples:
|
||||
| Role |
|
||||
| instructor |
|
||||
| staff |
|
||||
|
||||
Scenario: View the grading configuration
|
||||
Given I am "<Role>" for a course
|
||||
When I click "Grading Configuration"
|
||||
Then I see the grading configuration for the course
|
||||
Examples:
|
||||
| Role |
|
||||
| instructor |
|
||||
| staff |
|
||||
|
||||
Scenario: Generate & download a grade report
|
||||
Given I am "<Role>" for a course
|
||||
When I click "Generate Grade Report"
|
||||
Then I see a csv file in the grade reports table
|
||||
Examples:
|
||||
| Role |
|
||||
| instructor |
|
||||
| staff |
|
||||
71
lms/djangoapps/instructor/features/data_download.py
Normal file
71
lms/djangoapps/instructor/features/data_download.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Define steps for instructor dashboard - data download tab
|
||||
acceptance tests.
|
||||
"""
|
||||
|
||||
# pylint: disable=C0111
|
||||
# pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_in, assert_regexp_matches # pylint: disable=E0611
|
||||
from terrain.steps import reload_the_page
|
||||
|
||||
|
||||
@step(u'I see a table of student profiles')
|
||||
def find_student_profile_table(step): # pylint: disable=unused-argument
|
||||
# Find the grading configuration display
|
||||
world.wait_for_visible('#data-student-profiles-table')
|
||||
if world.role == 'instructor':
|
||||
expected_data = [
|
||||
world.instructor.username,
|
||||
world.instructor.email,
|
||||
world.instructor.profile.name,
|
||||
world.instructor.profile.gender,
|
||||
world.instructor.profile.goals
|
||||
]
|
||||
elif world.role == 'staff':
|
||||
expected_data = [
|
||||
world.staff.username,
|
||||
world.staff.email,
|
||||
world.staff.profile.name,
|
||||
world.staff.profile.gender,
|
||||
world.staff.profile.goals
|
||||
]
|
||||
for datum in expected_data:
|
||||
assert_in(datum, world.css_text('#data-student-profiles-table'))
|
||||
|
||||
|
||||
@step(u"I see the grading configuration for the course")
|
||||
def find_grading_config(step): # pylint: disable=unused-argument
|
||||
# Find the grading configuration display
|
||||
world.wait_for_visible('#data-grade-config-text')
|
||||
# expected config is the default grading configuration from common/lib/xmodule/xmodule/course_module.py
|
||||
expected_config = u"""-----------------------------------------------------------------------------
|
||||
Course grader:
|
||||
<class 'xmodule.graders.WeightedSubsectionsGrader'>
|
||||
|
||||
Graded sections:
|
||||
subgrader=<class 'xmodule.graders.AssignmentFormatGrader'>, type=Homework, category=Homework, weight=0.15
|
||||
subgrader=<class 'xmodule.graders.AssignmentFormatGrader'>, type=Lab, category=Lab, weight=0.15
|
||||
subgrader=<class 'xmodule.graders.AssignmentFormatGrader'>, type=Midterm Exam, category=Midterm Exam, weight=0.3
|
||||
subgrader=<class 'xmodule.graders.AssignmentFormatGrader'>, type=Final Exam, category=Final Exam, weight=0.4
|
||||
-----------------------------------------------------------------------------
|
||||
Listing grading context for course edx/999/Test_Course
|
||||
graded sections:
|
||||
[]
|
||||
all descriptors:
|
||||
length=0"""
|
||||
assert_in(expected_config, world.css_text('#data-grade-config-text'))
|
||||
|
||||
|
||||
@step(u"I see a csv file in the grade reports table")
|
||||
def find_grade_report_csv_link(step): # pylint: disable=unused-argument
|
||||
# Need to reload the page to see the grades download table
|
||||
reload_the_page(step)
|
||||
world.wait_for_visible('#grade-downloads-table')
|
||||
# Find table and assert a .csv file is present
|
||||
expected_file_regexp = 'edx_999_Test_Course_grade_report_\d{4}-\d{2}-\d{2}-\d{4}\.csv'
|
||||
assert_regexp_matches(
|
||||
world.css_html('#grade-downloads-table'), expected_file_regexp,
|
||||
msg="Expected grade report filename was not found."
|
||||
)
|
||||
@@ -36,6 +36,10 @@ FEATURES['ENABLE_INSTRUCTOR_BETA_DASHBOARD'] = True
|
||||
|
||||
FEATURES['ENABLE_SHOPPING_CART'] = True
|
||||
|
||||
# Enable this feature for course staff grade downloads, to enable acceptance tests
|
||||
FEATURES['ENABLE_S3_GRADE_DOWNLOADS'] = True
|
||||
FEATURES['ALLOW_COURSE_STAFF_GRADE_DOWNLOADS'] = True
|
||||
|
||||
# Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it.
|
||||
WIKI_ENABLED = True
|
||||
|
||||
|
||||
@@ -4,19 +4,19 @@
|
||||
|
||||
<div class="data-download-container action-type-container">
|
||||
<h2>${_("Data Download")}</h2>
|
||||
<div class="request-response-error msg msg-error copy"></div>
|
||||
<div class="request-response-error msg msg-error copy" id="data-request-response-error"></div>
|
||||
|
||||
<p>${_("The following button displays a list of all students enrolled in this course, along with profile information such as email address and username. The data can also be downloaded as a CSV file.")}</p>
|
||||
|
||||
<p><input type="button" name="list-profiles" value="${_("List enrolled students' profile information")}" data-endpoint="${ section_data['get_students_features_url'] }">
|
||||
<input type="button" name="list-profiles" value="${_("Download profile information as a CSV")}" data-csv="true"></p>
|
||||
<div class="data-display-table"></div>
|
||||
<div class="data-display-table" id="data-student-profiles-table"></div>
|
||||
|
||||
<br>
|
||||
<p>${_("Displays the grading configuration for the course. The grading configuration is the breakdown of graded subsections of the course (such as exams and problem sets), and can be changed on the 'Grading' page (under 'Settings') in Studio.")}</p>
|
||||
<p><input type="button" name="dump-gradeconf" value="${_("Grading Configuration")}" data-endpoint="${ section_data['get_grading_config_url'] }"></p>
|
||||
|
||||
<div class="data-display-text"></div>
|
||||
<div class="data-display-text" id="data-grade-config-text"></div>
|
||||
<br>
|
||||
|
||||
<p>${_("Download a CSV of anonymized student IDs by clicking this button.")}</p>
|
||||
@@ -33,8 +33,8 @@
|
||||
|
||||
<p>${_("The report is generated in the background, meaning it is OK to navigate away from this page while your report is generating. Generated reports appear in a table below and can be downloaded.")}</p>
|
||||
|
||||
<div class="request-response msg msg-confirm copy"></div>
|
||||
<div class="request-response-error msg msg-warning copy"></div>
|
||||
<div class="request-response msg msg-confirm copy" id="grade-request-response"></div>
|
||||
<div class="request-response-error msg msg-warning copy" id="grade-request-response-error"></div>
|
||||
<br>
|
||||
|
||||
<p><input type="button" name="calculate-grades-csv" value="${_("Generate Grade Report")}" data-endpoint="${ section_data['calculate_grades_csv_url'] }"/></p>
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
<p><b>${_("Reports Available for Download")}</b></p>
|
||||
<p>${_("File links are generated on demand and expire within 5 minutes due to the sensitive nature of student grade information. Please note that the report filename contains a timestamp that represents when your file was generated; this timestamp is UTC, not your local timezone.")}</p><br>
|
||||
<div class="grade-downloads-table" data-endpoint="${ section_data['list_grade_downloads_url'] }" ></div>
|
||||
<div class="grade-downloads-table" id="grade-downloads-table" data-endpoint="${ section_data['list_grade_downloads_url'] }" ></div>
|
||||
</div>
|
||||
%endif
|
||||
|
||||
|
||||
Reference in New Issue
Block a user