318 lines
14 KiB
Python
318 lines
14 KiB
Python
"""
|
|
Unit tests for instructor dashboard
|
|
|
|
Based on (and depends on) unit tests for courseware.
|
|
|
|
Notes for running by hand:
|
|
|
|
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/instructor
|
|
"""
|
|
|
|
import courseware.tests.tests as ct
|
|
|
|
import json
|
|
|
|
from nose import SkipTest
|
|
from mock import patch, Mock
|
|
|
|
from override_settings import override_settings
|
|
|
|
# Need access to internal func to put users in the right group
|
|
from django.contrib.auth.models import Group
|
|
|
|
from django.core.urlresolvers import reverse
|
|
from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, \
|
|
FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_STUDENT
|
|
from django_comment_client.utils import has_forum_access
|
|
|
|
from instructor import staff_grading_service
|
|
from courseware.access import _course_staff_group_name
|
|
import courseware.tests.tests as ct
|
|
from xmodule.modulestore.django import modulestore
|
|
import xmodule.modulestore.django
|
|
|
|
|
|
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
|
|
class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader):
|
|
'''
|
|
Check for download of csv
|
|
'''
|
|
|
|
def setUp(self):
|
|
xmodule.modulestore.django._MODULESTORES = {}
|
|
|
|
self.full = modulestore().get_course("edX/full/6.002_Spring_2012")
|
|
self.toy = modulestore().get_course("edX/toy/2012_Fall")
|
|
|
|
# Create two accounts
|
|
self.student = 'view@test.com'
|
|
self.instructor = 'view2@test.com'
|
|
self.password = 'foo'
|
|
self.create_account('u1', self.student, self.password)
|
|
self.create_account('u2', self.instructor, self.password)
|
|
self.activate_user(self.student)
|
|
self.activate_user(self.instructor)
|
|
|
|
def make_instructor(course):
|
|
group_name = _course_staff_group_name(course.location)
|
|
g = Group.objects.create(name=group_name)
|
|
g.user_set.add(ct.user(self.instructor))
|
|
|
|
make_instructor(self.toy)
|
|
|
|
self.logout()
|
|
self.login(self.instructor, self.password)
|
|
self.enroll(self.toy)
|
|
|
|
|
|
def test_download_grades_csv(self):
|
|
course = self.toy
|
|
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
|
msg = "url = {0}\n".format(url)
|
|
response = self.client.post(url, {'action': 'Download CSV of all student grades for this course'})
|
|
msg += "instructor dashboard download csv grades: response = '{0}'\n".format(response)
|
|
|
|
self.assertEqual(response['Content-Type'],'text/csv',msg)
|
|
|
|
cdisp = response['Content-Disposition']
|
|
msg += "Content-Disposition = '%s'\n" % cdisp
|
|
self.assertEqual(cdisp, 'attachment; filename=grades_{0}.csv'.format(course.id), msg)
|
|
|
|
body = response.content.replace('\r','')
|
|
msg += "body = '{0}'\n".format(body)
|
|
|
|
# All the not-actually-in-the-course hw and labs come from the
|
|
# default grading policy string in graders.py
|
|
expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm 01","Midterm Avg","Final 01","Final Avg"
|
|
"2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0"
|
|
'''
|
|
|
|
self.assertEqual(body, expected_body, msg)
|
|
|
|
|
|
FORUM_ROLES = [ FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA ]
|
|
FORUM_ADMIN_ACTION_SUFFIX = { FORUM_ROLE_ADMINISTRATOR : 'admin', FORUM_ROLE_MODERATOR : 'moderator', FORUM_ROLE_COMMUNITY_TA : 'community TA'}
|
|
FORUM_ADMIN_USER = { FORUM_ROLE_ADMINISTRATOR : 'forumadmin', FORUM_ROLE_MODERATOR : 'forummoderator', FORUM_ROLE_COMMUNITY_TA : 'forummoderator'}
|
|
|
|
def action_name(operation, rolename):
|
|
if operation == 'List':
|
|
return '{0} course forum {1}s'.format(operation, FORUM_ADMIN_ACTION_SUFFIX[rolename])
|
|
else:
|
|
return '{0} forum {1}'.format(operation, FORUM_ADMIN_ACTION_SUFFIX[rolename])
|
|
|
|
|
|
_mock_service = staff_grading_service.MockStaffGradingService()
|
|
|
|
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
|
|
class TestInstructorDashboardForumAdmin(ct.PageLoader):
|
|
'''
|
|
Check for change in forum admin role memberships
|
|
'''
|
|
|
|
def setUp(self):
|
|
xmodule.modulestore.django._MODULESTORES = {}
|
|
courses = modulestore().get_courses()
|
|
|
|
|
|
self.course_id = "edX/toy/2012_Fall"
|
|
self.toy = modulestore().get_course(self.course_id)
|
|
|
|
# Create two accounts
|
|
self.student = 'view@test.com'
|
|
self.instructor = 'view2@test.com'
|
|
self.password = 'foo'
|
|
self.create_account('u1', self.student, self.password)
|
|
self.create_account('u2', self.instructor, self.password)
|
|
self.activate_user(self.student)
|
|
self.activate_user(self.instructor)
|
|
|
|
group_name = _course_staff_group_name(self.toy.location)
|
|
g = Group.objects.create(name=group_name)
|
|
g.user_set.add(ct.user(self.instructor))
|
|
|
|
self.logout()
|
|
self.login(self.instructor, self.password)
|
|
self.enroll(self.toy)
|
|
|
|
|
|
|
|
def initialize_roles(self, course_id):
|
|
self.admin_role = Role.objects.get_or_create(name=FORUM_ROLE_ADMINISTRATOR, course_id=course_id)[0]
|
|
self.moderator_role = Role.objects.get_or_create(name=FORUM_ROLE_MODERATOR, course_id=course_id)[0]
|
|
self.community_ta_role = Role.objects.get_or_create(name=FORUM_ROLE_COMMUNITY_TA, course_id=course_id)[0]
|
|
|
|
def test_add_forum_admin_users_for_unknown_user(self):
|
|
course = self.toy
|
|
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
|
username = 'unknown'
|
|
for action in ['Add', 'Remove']:
|
|
for rolename in FORUM_ROLES:
|
|
response = self.client.post(url, {'action': action_name(action, rolename), FORUM_ADMIN_USER[rolename]: username})
|
|
self.assertTrue(response.content.find('Error: unknown username "{0}"'.format(username))>=0)
|
|
|
|
def test_add_forum_admin_users_for_missing_roles(self):
|
|
course = self.toy
|
|
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
|
username = 'u1'
|
|
for action in ['Add', 'Remove']:
|
|
for rolename in FORUM_ROLES:
|
|
response = self.client.post(url, {'action': action_name(action, rolename), FORUM_ADMIN_USER[rolename]: username})
|
|
self.assertTrue(response.content.find('Error: unknown rolename "{0}"'.format(rolename))>=0)
|
|
|
|
def test_remove_forum_admin_users_for_missing_users(self):
|
|
course = self.toy
|
|
self.initialize_roles(course.id)
|
|
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
|
username = 'u1'
|
|
action = 'Remove'
|
|
for rolename in FORUM_ROLES:
|
|
response = self.client.post(url, {'action': action_name(action, rolename), FORUM_ADMIN_USER[rolename]: username})
|
|
self.assertTrue(response.content.find('Error: user "{0}" does not have rolename "{1}"'.format(username, rolename))>=0)
|
|
|
|
def test_add_and_remove_forum_admin_users(self):
|
|
course = self.toy
|
|
self.initialize_roles(course.id)
|
|
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
|
username = 'u2'
|
|
for rolename in FORUM_ROLES:
|
|
response = self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username})
|
|
self.assertTrue(response.content.find('Added "{0}" to "{1}" forum role = "{2}"'.format(username, course.id, rolename))>=0)
|
|
self.assertTrue(has_forum_access(username, course.id, rolename))
|
|
response = self.client.post(url, {'action': action_name('Remove', rolename), FORUM_ADMIN_USER[rolename]: username})
|
|
self.assertTrue(response.content.find('Removed "{0}" from "{1}" forum role = "{2}"'.format(username, course.id, rolename))>=0)
|
|
self.assertFalse(has_forum_access(username, course.id, rolename))
|
|
|
|
def test_add_and_readd_forum_admin_users(self):
|
|
course = self.toy
|
|
self.initialize_roles(course.id)
|
|
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
|
username = 'u2'
|
|
for rolename in FORUM_ROLES:
|
|
# perform an add, and follow with a second identical add:
|
|
self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username})
|
|
response = self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username})
|
|
self.assertTrue(response.content.find('Error: user "{0}" already has rolename "{1}", cannot add'.format(username, rolename))>=0)
|
|
self.assertTrue(has_forum_access(username, course.id, rolename))
|
|
|
|
def test_add_nonstaff_forum_admin_users(self):
|
|
course = self.toy
|
|
self.initialize_roles(course.id)
|
|
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
|
username = 'u1'
|
|
rolename = FORUM_ROLE_ADMINISTRATOR
|
|
response = self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username})
|
|
self.assertTrue(response.content.find('Error: user "{0}" should first be added as staff'.format(username))>=0)
|
|
|
|
def test_list_forum_admin_users(self):
|
|
course = self.toy
|
|
self.initialize_roles(course.id)
|
|
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
|
username = 'u2'
|
|
added_roles = [FORUM_ROLE_STUDENT] # u2 is already added as a student to the discussion forums
|
|
self.assertTrue(has_forum_access(username, course.id, 'Student'))
|
|
for rolename in FORUM_ROLES:
|
|
response = self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username})
|
|
self.assertTrue(has_forum_access(username, course.id, rolename))
|
|
response = self.client.post(url, {'action': action_name('List', rolename), FORUM_ADMIN_USER[rolename]: username})
|
|
for header in ['Username', 'Full name', 'Roles']:
|
|
self.assertTrue(response.content.find('<th>{0}</th>'.format(header))>0)
|
|
self.assertTrue(response.content.find('<td>{0}</td>'.format(username))>=0)
|
|
# concatenate all roles for user, in sorted order:
|
|
added_roles.append(rolename)
|
|
added_roles.sort()
|
|
roles = ', '.join(added_roles)
|
|
self.assertTrue(response.content.find('<td>{0}</td>'.format(roles))>=0, 'not finding roles "{0}"'.format(roles))
|
|
|
|
|
|
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
|
|
class TestStaffGradingService(ct.PageLoader):
|
|
'''
|
|
Check that staff grading service proxy works. Basically just checking the
|
|
access control and error handling logic -- all the actual work is on the
|
|
backend.
|
|
'''
|
|
def setUp(self):
|
|
xmodule.modulestore.django._MODULESTORES = {}
|
|
|
|
self.student = 'view@test.com'
|
|
self.instructor = 'view2@test.com'
|
|
self.password = 'foo'
|
|
self.location = 'TestLocation'
|
|
self.create_account('u1', self.student, self.password)
|
|
self.create_account('u2', self.instructor, self.password)
|
|
self.activate_user(self.student)
|
|
self.activate_user(self.instructor)
|
|
|
|
self.course_id = "edX/toy/2012_Fall"
|
|
self.toy = modulestore().get_course(self.course_id)
|
|
def make_instructor(course):
|
|
group_name = _course_staff_group_name(course.location)
|
|
g = Group.objects.create(name=group_name)
|
|
g.user_set.add(ct.user(self.instructor))
|
|
|
|
make_instructor(self.toy)
|
|
|
|
self.mock_service = staff_grading_service.grading_service()
|
|
|
|
self.logout()
|
|
|
|
def test_access(self):
|
|
"""
|
|
Make sure only staff have access.
|
|
"""
|
|
self.login(self.student, self.password)
|
|
|
|
# both get and post should return 404
|
|
for view_name in ('staff_grading_get_next', 'staff_grading_save_grade'):
|
|
url = reverse(view_name, kwargs={'course_id': self.course_id})
|
|
self.check_for_get_code(404, url)
|
|
self.check_for_post_code(404, url)
|
|
|
|
|
|
def test_get_next(self):
|
|
self.login(self.instructor, self.password)
|
|
|
|
url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id})
|
|
data = {'location': self.location}
|
|
|
|
r = self.check_for_post_code(200, url, data)
|
|
d = json.loads(r.content)
|
|
self.assertTrue(d['success'])
|
|
self.assertEquals(d['submission_id'], self.mock_service.cnt)
|
|
self.assertIsNotNone(d['submission'])
|
|
self.assertIsNotNone(d['num_graded'])
|
|
self.assertIsNotNone(d['min_for_ml'])
|
|
self.assertIsNotNone(d['num_pending'])
|
|
self.assertIsNotNone(d['prompt'])
|
|
self.assertIsNotNone(d['ml_error_info'])
|
|
self.assertIsNotNone(d['max_score'])
|
|
self.assertIsNotNone(d['rubric'])
|
|
|
|
|
|
def test_save_grade(self):
|
|
self.login(self.instructor, self.password)
|
|
|
|
url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id})
|
|
|
|
data = {'score': '12',
|
|
'feedback': 'great!',
|
|
'submission_id': '123',
|
|
'location': self.location}
|
|
r = self.check_for_post_code(200, url, data)
|
|
d = json.loads(r.content)
|
|
self.assertTrue(d['success'], str(d))
|
|
self.assertEquals(d['submission_id'], self.mock_service.cnt)
|
|
|
|
def test_get_problem_list(self):
|
|
self.login(self.instructor, self.password)
|
|
|
|
url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id})
|
|
data = {}
|
|
|
|
r = self.check_for_post_code(200, url, data)
|
|
d = json.loads(r.content)
|
|
self.assertTrue(d['success'], str(d))
|
|
self.assertIsNotNone(d['problem_list'])
|
|
|
|
|