Merge pull request #6014 from edx/sarina/legacy-dash-cleanup
Legacy dash cleanup
This commit is contained in:
@@ -1,67 +0,0 @@
|
||||
"""
|
||||
Unit tests for instructor dashboard
|
||||
|
||||
Based on (and depends on) unit tests for courseware.
|
||||
|
||||
Notes for running by hand:
|
||||
|
||||
./manage.py lms --settings test test lms/djangoapps/instructor
|
||||
"""
|
||||
|
||||
from django.test.utils import override_settings
|
||||
|
||||
# Need access to internal func to put users in the right group
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MOCK_MODULESTORE
|
||||
import instructor.views.legacy
|
||||
from student.roles import CourseStaffRole
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from mock import Mock, patch
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MOCK_MODULESTORE)
|
||||
class TestInstructorDashboardAnonCSV(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
'''
|
||||
Check for download of csv
|
||||
'''
|
||||
|
||||
# Note -- I copied this setUp from a similar test
|
||||
def setUp(self):
|
||||
# clear_existing_modulestores()
|
||||
self.toy = CourseFactory.create(org='edX', course='toy', display_name='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)
|
||||
|
||||
CourseStaffRole(self.toy.id).add_users(User.objects.get(email=self.instructor))
|
||||
|
||||
self.logout()
|
||||
self.login(self.instructor, self.password)
|
||||
self.enroll(self.toy)
|
||||
|
||||
@patch.object(instructor.views.legacy, 'anonymous_id_for_user', Mock(return_value='42'))
|
||||
@patch.object(instructor.views.legacy, 'unique_id_for_user', Mock(return_value='41'))
|
||||
def test_download_anon_csv(self):
|
||||
course = self.toy
|
||||
url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
response = self.client.post(url, {'action': 'Download CSV of all student anonymized IDs'})
|
||||
|
||||
self.assertEqual(response['Content-Type'], 'text/csv')
|
||||
body = response.content.replace('\r', '')
|
||||
self.assertEqual(
|
||||
body,
|
||||
('"User ID","Anonymized User ID","Course Specific Anonymized User ID"'
|
||||
'\n"2","41","42"\n')
|
||||
)
|
||||
@@ -1,72 +0,0 @@
|
||||
"""
|
||||
Unit tests for instructor dashboard
|
||||
|
||||
Based on (and depends on) unit tests for courseware.
|
||||
|
||||
Notes for running by hand:
|
||||
|
||||
./manage.py lms --settings test test lms/djangoapps/instructor
|
||||
"""
|
||||
|
||||
from django.test.utils import override_settings
|
||||
|
||||
# Need access to internal func to put users in the right group
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MOCK_MODULESTORE
|
||||
from student.roles import CourseStaffRole
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MOCK_MODULESTORE)
|
||||
class TestInstructorDashboardGradeDownloadCSV(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
'''
|
||||
Check for download of csv
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
# clear_existing_modulestores()
|
||||
self.toy = CourseFactory.create(org='edX', course='toy', display_name='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)
|
||||
|
||||
CourseStaffRole(self.toy.id).add_users(User.objects.get(email=self.instructor))
|
||||
|
||||
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_legacy', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
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.to_deprecated_string()), 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","Final"
|
||||
"2","u2","username","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"
|
||||
'''
|
||||
|
||||
self.assertEqual(body, expected_body, msg)
|
||||
@@ -1,144 +0,0 @@
|
||||
"""
|
||||
Unit tests for email feature flag in legacy instructor dashboard.
|
||||
Additionally tests that bulk email is always disabled for non-Mongo
|
||||
backed courses, regardless of email feature flag, and that the
|
||||
view is conditionally available when Course Auth is turned on.
|
||||
"""
|
||||
from django.test.utils import override_settings
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from mock import patch
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MOCK_MODULESTORE
|
||||
from student.tests.factories import AdminFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
|
||||
from bulk_email.models import CourseAuthorization
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MOCK_MODULESTORE)
|
||||
class TestInstructorDashboardEmailView(ModuleStoreTestCase):
|
||||
"""
|
||||
Check for email view displayed with flag
|
||||
"""
|
||||
def setUp(self):
|
||||
self.course = CourseFactory.create()
|
||||
|
||||
# Create instructor account
|
||||
instructor = AdminFactory.create()
|
||||
self.client.login(username=instructor.username, password="test")
|
||||
|
||||
# URL for instructor dash
|
||||
self.url = reverse('instructor_dashboard_legacy', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
# URL for email view
|
||||
self.email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>'
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Undo all patches.
|
||||
"""
|
||||
patch.stopall()
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False})
|
||||
def test_email_flag_true(self):
|
||||
# Assert that the URL for the email view is in the response
|
||||
response = self.client.get(self.url)
|
||||
self.assertTrue(self.email_link in response.content)
|
||||
|
||||
# Select the Email view of the instructor dash
|
||||
session = self.client.session
|
||||
session[u'idash_mode:{0}'.format(self.course.location.course_key.to_deprecated_string())] = 'Email'
|
||||
session.save()
|
||||
response = self.client.get(self.url)
|
||||
|
||||
# Ensure we've selected the view properly and that the send_to field is present.
|
||||
selected_email_link = '<a href="#" onclick="goto(\'Email\')" class="selectedmode">Email</a>'
|
||||
self.assertTrue(selected_email_link in response.content)
|
||||
send_to_label = '<label for="id_to">Send to:</label>'
|
||||
self.assertTrue(send_to_label in response.content)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
|
||||
def test_email_flag_unauthorized(self):
|
||||
# Assert that the URL for the email view is not in the response
|
||||
# email is enabled, but this course is not authorized to send email
|
||||
response = self.client.get(self.url)
|
||||
self.assertFalse(self.email_link in response.content)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
|
||||
def test_email_flag_authorized(self):
|
||||
# Assert that the URL for the email view is in the response
|
||||
# email is enabled, and this course is authorized to send email
|
||||
|
||||
# Assert that instructor email is not enabled for this course
|
||||
self.assertFalse(CourseAuthorization.instructor_email_enabled(self.course.id))
|
||||
response = self.client.get(self.url)
|
||||
self.assertFalse(self.email_link in response.content)
|
||||
|
||||
# Authorize the course to use email
|
||||
cauth = CourseAuthorization(course_id=self.course.id, email_enabled=True)
|
||||
cauth.save()
|
||||
|
||||
# Assert that instructor email is enabled for this course
|
||||
self.assertTrue(CourseAuthorization.instructor_email_enabled(self.course.id))
|
||||
response = self.client.get(self.url)
|
||||
self.assertTrue(self.email_link in response.content)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': False})
|
||||
def test_email_flag_false(self):
|
||||
# Assert that the URL for the email view is not in the response
|
||||
response = self.client.get(self.url)
|
||||
self.assertFalse(self.email_link in response.content)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True})
|
||||
def test_email_flag_true_xml_store(self):
|
||||
# If the enable email setting is enabled, but this is an XML backed course,
|
||||
# the email view shouldn't be available on the instructor dashboard.
|
||||
|
||||
# The course factory uses a MongoModuleStore backing, so patch the
|
||||
# `get_modulestore_type` method to pretend to be XML-backed.
|
||||
# This is OK; we're simply testing that the `is_mongo_modulestore_type` flag
|
||||
# in `instructor/views/legacy.py` is doing the correct thing.
|
||||
|
||||
with patch('xmodule.modulestore.mongo.base.MongoModuleStore.get_modulestore_type') as mock_modulestore:
|
||||
mock_modulestore.return_value = ModuleStoreEnum.Type.xml
|
||||
|
||||
# Assert that the URL for the email view is not in the response
|
||||
response = self.client.get(self.url)
|
||||
self.assertFalse(self.email_link in response.content)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True})
|
||||
def test_send_mail_unauthorized(self):
|
||||
""" Test 'Send email' action returns an error if course is not authorized to send email. """
|
||||
|
||||
response = self.client.post(
|
||||
self.url, {
|
||||
'action': 'Send email',
|
||||
'to_option': 'all',
|
||||
'subject': "Welcome to the course!",
|
||||
'message': "Lets start with an introduction!"
|
||||
}
|
||||
)
|
||||
self.assertContains(response, "Email is not enabled for this course.")
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True})
|
||||
def test_send_mail_authorized(self):
|
||||
""" Test 'Send email' action when course is authorized to send email. """
|
||||
|
||||
course_authorization = CourseAuthorization(course_id=self.course.id, email_enabled=True)
|
||||
course_authorization.save()
|
||||
|
||||
session = self.client.session
|
||||
session[u'idash_mode:{0}'.format(self.course.location.course_key.to_deprecated_string())] = 'Email'
|
||||
session.save()
|
||||
|
||||
response = self.client.post(
|
||||
self.url, {
|
||||
'action': 'Send email',
|
||||
'to_option': 'all',
|
||||
'subject': 'Welcome to the course!',
|
||||
'message': 'Lets start with an introduction!',
|
||||
}
|
||||
)
|
||||
self.assertContains(response, "Your email was successfully queued for sending.")
|
||||
@@ -1,144 +0,0 @@
|
||||
"""
|
||||
Unit tests for instructor dashboard forum administration
|
||||
"""
|
||||
|
||||
|
||||
from django.test.utils import override_settings
|
||||
|
||||
# Need access to internal func to put users in the right group
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django_comment_common.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 courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MOCK_MODULESTORE
|
||||
from student.roles import CourseStaffRole
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
|
||||
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])
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MOCK_MODULESTORE)
|
||||
class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
'''
|
||||
Check for change in forum admin role memberships
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
self.toy = CourseFactory.create(org='edX', course='toy', display_name='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)
|
||||
|
||||
CourseStaffRole(self.toy.id).add_users(User.objects.get(email=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_legacy', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
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_legacy', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
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_legacy', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
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_legacy', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
username = 'u2'
|
||||
for rolename in FORUM_ROLES:
|
||||
response = self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username})
|
||||
self.assertContains(response, 'Added "{0}" to "{1}" forum role = "{2}"'.format(username, course.id.to_deprecated_string(), rolename))
|
||||
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.assertContains(response, 'Removed "{0}" from "{1}" forum role = "{2}"'.format(username, course.id.to_deprecated_string(), rolename))
|
||||
self.assertFalse(has_forum_access(username, course.id, rolename))
|
||||
|
||||
def test_add_and_read_forum_admin_users(self):
|
||||
course = self.toy
|
||||
self.initialize_roles(course.id)
|
||||
url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
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_legacy', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
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_legacy', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
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))
|
||||
@@ -1,70 +0,0 @@
|
||||
"""
|
||||
View-level tests for resetting student state in legacy instructor dash.
|
||||
"""
|
||||
|
||||
import json
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MOCK_MODULESTORE
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory
|
||||
|
||||
from courseware.models import StudentModule
|
||||
|
||||
from submissions import api as sub_api
|
||||
from student.models import anonymous_id_for_user
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MOCK_MODULESTORE)
|
||||
class InstructorResetStudentStateTest(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
Reset student state from the legacy instructor dash.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Log in as an instructor, and create a course/student to reset.
|
||||
"""
|
||||
instructor = AdminFactory.create()
|
||||
self.client.login(username=instructor.username, password='test')
|
||||
self.student = UserFactory.create(username='test', email='test@example.com')
|
||||
self.course = CourseFactory.create()
|
||||
CourseEnrollmentFactory.create(user=self.student, course_id=self.course.id)
|
||||
|
||||
def test_delete_student_state_resets_scores(self):
|
||||
problem_location = self.course.id.make_usage_key('dummy', 'module')
|
||||
|
||||
# Create a student module for the user
|
||||
StudentModule.objects.create(
|
||||
student=self.student,
|
||||
course_id=self.course.id,
|
||||
module_state_key=problem_location,
|
||||
state=json.dumps({})
|
||||
)
|
||||
|
||||
# Create a submission and score for the student using the submissions API
|
||||
student_item = {
|
||||
'student_id': anonymous_id_for_user(self.student, self.course.id),
|
||||
'course_id': self.course.id.to_deprecated_string(),
|
||||
'item_id': problem_location.to_deprecated_string(),
|
||||
'item_type': 'openassessment'
|
||||
}
|
||||
submission = sub_api.create_submission(student_item, 'test answer')
|
||||
sub_api.set_score(submission['uuid'], 1, 2)
|
||||
|
||||
# Delete student state using the instructor dash
|
||||
url = reverse('instructor_dashboard_legacy', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.post(url, {
|
||||
'action': 'Delete student state for module',
|
||||
'unique_student_identifier': self.student.email,
|
||||
'problem_for_student': problem_location.to_deprecated_string(),
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify that the student's scores have been reset in the submissions API
|
||||
score = sub_api.get_score(student_item)
|
||||
self.assertIs(score, None)
|
||||
@@ -63,6 +63,3 @@ class TestXss(ModuleStoreTestCase):
|
||||
|
||||
def test_dump_list_of_enrolled(self):
|
||||
self._test_action("Dump list of enrolled students")
|
||||
|
||||
def test_dump_grades(self):
|
||||
self._test_action("Dump Grades for all students in this course")
|
||||
|
||||
@@ -3,7 +3,6 @@ Instructor Views
|
||||
"""
|
||||
## NOTE: This is the code for the legacy instructor dashboard
|
||||
## We are no longer supporting this file or accepting changes into it.
|
||||
# pylint: skip-file
|
||||
from contextlib import contextmanager
|
||||
import csv
|
||||
import json
|
||||
@@ -27,39 +26,22 @@ from django.core.urlresolvers import reverse
|
||||
from django.core.mail import send_mail
|
||||
from django.utils import timezone
|
||||
|
||||
from xmodule_modifiers import wrap_xblock, request_token
|
||||
import xmodule.graders as xmgraders
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.html_module import HtmlDescriptor
|
||||
from opaque_keys import InvalidKeyError
|
||||
from lms.lib.xblock.runtime import quote_slashes
|
||||
|
||||
from submissions import api as sub_api # installed from the edx-submissions repository
|
||||
|
||||
from bulk_email.models import CourseEmail, CourseAuthorization
|
||||
from courseware import grades
|
||||
from courseware.access import has_access
|
||||
from courseware.courses import get_course_with_access, get_cms_course_link
|
||||
from student.roles import (
|
||||
CourseStaffRole, CourseInstructorRole, CourseBetaTesterRole, GlobalStaff
|
||||
)
|
||||
from courseware.models import StudentModule
|
||||
from django_comment_common.models import (
|
||||
Role, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA
|
||||
)
|
||||
from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR
|
||||
from django_comment_client.utils import has_forum_access
|
||||
from instructor.offline_gradecalc import student_grades, offline_grades_available
|
||||
from instructor.views.tools import strip_if_string, bulk_email_is_enabled_for_course, add_block_ids
|
||||
from instructor_task.api import (
|
||||
get_running_instructor_tasks,
|
||||
get_instructor_task_history,
|
||||
submit_rescore_problem_for_all_students,
|
||||
submit_rescore_problem_for_student,
|
||||
submit_reset_problem_attempts_for_all_students,
|
||||
submit_bulk_course_email
|
||||
)
|
||||
from instructor_task.views import get_task_completion_info
|
||||
from edxmako.shortcuts import render_to_response, render_to_string
|
||||
@@ -68,12 +50,8 @@ from psychometrics import psychoanalyze
|
||||
from student.models import (
|
||||
CourseEnrollment,
|
||||
CourseEnrollmentAllowed,
|
||||
unique_id_for_user,
|
||||
anonymous_id_for_user
|
||||
)
|
||||
import track.views
|
||||
from xblock.field_data import DictFieldData
|
||||
from xblock.fields import ScopeIds
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from microsite_configuration import microsite
|
||||
@@ -108,10 +86,6 @@ def instructor_dashboard(request, course_id):
|
||||
forum_admin_access = has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR)
|
||||
|
||||
msg = ''
|
||||
email_msg = ''
|
||||
email_to_option = None
|
||||
email_subject = None
|
||||
html_message = ''
|
||||
show_email_tab = False
|
||||
problems = []
|
||||
plots = []
|
||||
@@ -172,23 +146,6 @@ def instructor_dashboard(request, course_id):
|
||||
writer.writerow(encoded_row)
|
||||
return response
|
||||
|
||||
def get_student_from_identifier(unique_student_identifier):
|
||||
"""Gets a student object using either an email address or username"""
|
||||
unique_student_identifier = strip_if_string(unique_student_identifier)
|
||||
msg = ""
|
||||
try:
|
||||
if "@" in unique_student_identifier:
|
||||
student = User.objects.get(email=unique_student_identifier)
|
||||
else:
|
||||
student = User.objects.get(username=unique_student_identifier)
|
||||
msg += _("Found a single student. ")
|
||||
except User.DoesNotExist:
|
||||
student = None
|
||||
msg += "<font color='red'>{text}</font>".format(
|
||||
text=_("Couldn't find student with that email or username.")
|
||||
)
|
||||
return msg, student
|
||||
|
||||
# process actions from form POST
|
||||
action = request.POST.get('action', '')
|
||||
use_offline = request.POST.get('use_offline_grades', False)
|
||||
@@ -227,12 +184,6 @@ def instructor_dashboard(request, course_id):
|
||||
datatable['title'] = _('List of students enrolled in {course_key}').format(course_key=course_key.to_deprecated_string())
|
||||
track.views.server_track(request, "list-students", {}, page="idashboard")
|
||||
|
||||
elif 'Dump Grades' in action:
|
||||
log.debug(action)
|
||||
datatable = get_student_grade_summary_data(request, course, get_grades=True, use_offline=use_offline)
|
||||
datatable['title'] = _('Summary Grades of students enrolled in {course_key}').format(course_key=course_key.to_deprecated_string())
|
||||
track.views.server_track(request, "dump-grades", {}, page="idashboard")
|
||||
|
||||
elif 'Dump all RAW grades' in action:
|
||||
log.debug(action)
|
||||
datatable = get_student_grade_summary_data(request, course, get_grades=True,
|
||||
@@ -240,11 +191,6 @@ def instructor_dashboard(request, course_id):
|
||||
datatable['title'] = _('Raw Grades of students enrolled in {course_key}').format(course_key=course_key)
|
||||
track.views.server_track(request, "dump-grades-raw", {}, page="idashboard")
|
||||
|
||||
elif 'Download CSV of all student grades' in action:
|
||||
track.views.server_track(request, "dump-grades-csv", {}, page="idashboard")
|
||||
return return_csv('grades_{0}.csv'.format(course_key.to_deprecated_string()),
|
||||
get_student_grade_summary_data(request, course, use_offline=use_offline))
|
||||
|
||||
elif 'Download CSV of all RAW grades' in action:
|
||||
track.views.server_track(request, "dump-grades-csv-raw", {}, page="idashboard")
|
||||
return return_csv('grades_{0}_raw.csv'.format(course_key.to_deprecated_string()),
|
||||
@@ -254,279 +200,6 @@ def instructor_dashboard(request, course_id):
|
||||
track.views.server_track(request, "dump-answer-dist-csv", {}, page="idashboard")
|
||||
return return_csv('answer_dist_{0}.csv'.format(course_key.to_deprecated_string()), get_answers_distribution(request, course_key))
|
||||
|
||||
elif 'Dump description of graded assignments configuration' in action:
|
||||
# what is "graded assignments configuration"?
|
||||
track.views.server_track(request, "dump-graded-assignments-config", {}, page="idashboard")
|
||||
msg += dump_grading_context(course)
|
||||
|
||||
elif "Rescore ALL students' problem submissions" in action:
|
||||
problem_location_str = strip_if_string(request.POST.get('problem_for_all_students', ''))
|
||||
try:
|
||||
problem_location = course_key.make_usage_key_from_deprecated_string(problem_location_str)
|
||||
instructor_task = submit_rescore_problem_for_all_students(request, problem_location)
|
||||
if instructor_task is None:
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Failed to create a background task for rescoring "{problem_url}".').format(
|
||||
problem_url=problem_location_str
|
||||
)
|
||||
)
|
||||
else:
|
||||
track.views.server_track(
|
||||
request,
|
||||
"rescore-all-submissions",
|
||||
{
|
||||
"problem": problem_location_str,
|
||||
"course": course_key.to_deprecated_string()
|
||||
},
|
||||
page="idashboard"
|
||||
)
|
||||
|
||||
except (InvalidKeyError, ItemNotFoundError) as err:
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Failed to create a background task for rescoring "{problem_url}": problem not found.').format(
|
||||
problem_url=problem_location_str
|
||||
)
|
||||
)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
log.error("Encountered exception from rescore: {0}".format(err))
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Failed to create a background task for rescoring "{url}": {message}.').format(
|
||||
url=problem_location_str, message=err.message
|
||||
)
|
||||
)
|
||||
|
||||
elif "Reset ALL students' attempts" in action:
|
||||
problem_location_str = strip_if_string(request.POST.get('problem_for_all_students', ''))
|
||||
try:
|
||||
problem_location = course_key.make_usage_key_from_deprecated_string(problem_location_str)
|
||||
instructor_task = submit_reset_problem_attempts_for_all_students(request, problem_location)
|
||||
if instructor_task is None:
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Failed to create a background task for resetting "{problem_url}".').format(problem_url=problem_location_str)
|
||||
)
|
||||
else:
|
||||
track.views.server_track(
|
||||
request,
|
||||
"reset-all-attempts",
|
||||
{
|
||||
"problem": problem_location_str,
|
||||
"course": course_key.to_deprecated_string()
|
||||
},
|
||||
page="idashboard"
|
||||
)
|
||||
except (InvalidKeyError, ItemNotFoundError) as err:
|
||||
log.error('Failure to reset: unknown problem "{0}"'.format(err))
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Failed to create a background task for resetting "{problem_url}": problem not found.').format(
|
||||
problem_url=problem_location_str
|
||||
)
|
||||
)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
log.error("Encountered exception from reset: {0}".format(err))
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Failed to create a background task for resetting "{url}": {message}.').format(
|
||||
url=problem_location_str, message=err.message
|
||||
)
|
||||
)
|
||||
|
||||
elif "Show Background Task History for Student" in action:
|
||||
# put this before the non-student case, since the use of "in" will cause this to be missed
|
||||
unique_student_identifier = request.POST.get('unique_student_identifier', '')
|
||||
message, student = get_student_from_identifier(unique_student_identifier)
|
||||
if student is None:
|
||||
msg += message
|
||||
else:
|
||||
problem_location_str = strip_if_string(request.POST.get('problem_for_student', ''))
|
||||
try:
|
||||
problem_location = course_key.make_usage_key_from_deprecated_string(problem_location_str)
|
||||
except InvalidKeyError:
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Could not find problem location "{url}".').format(
|
||||
url=problem_location_str
|
||||
)
|
||||
)
|
||||
else:
|
||||
message, datatable = get_background_task_table(course_key, problem_location, student)
|
||||
msg += message
|
||||
|
||||
elif "Show Background Task History" in action:
|
||||
problem_location_str = strip_if_string(request.POST.get('problem_for_all_students', ''))
|
||||
try:
|
||||
problem_location = course_key.make_usage_key_from_deprecated_string(problem_location_str)
|
||||
except InvalidKeyError:
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Could not find problem location "{url}".').format(
|
||||
url=problem_location_str
|
||||
)
|
||||
)
|
||||
else:
|
||||
message, datatable = get_background_task_table(course_key, problem_location)
|
||||
msg += message
|
||||
|
||||
elif ("Reset student's attempts" in action or
|
||||
"Delete student state for module" in action or
|
||||
"Rescore student's problem submission" in action):
|
||||
# get the form data
|
||||
unique_student_identifier = request.POST.get(
|
||||
'unique_student_identifier', ''
|
||||
)
|
||||
problem_location_str = strip_if_string(request.POST.get('problem_for_student', ''))
|
||||
try:
|
||||
module_state_key = course_key.make_usage_key_from_deprecated_string(problem_location_str)
|
||||
except InvalidKeyError:
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Could not find problem location "{url}".').format(
|
||||
url=problem_location_str
|
||||
)
|
||||
)
|
||||
else:
|
||||
# try to uniquely id student by email address or username
|
||||
message, student = get_student_from_identifier(unique_student_identifier)
|
||||
msg += message
|
||||
student_module = None
|
||||
if student is not None:
|
||||
# Reset the student's score in the submissions API
|
||||
# Currently this is used only by open assessment (ORA 2)
|
||||
# We need to do this *before* retrieving the `StudentModule` model,
|
||||
# because it's possible for a score to exist even if no student module exists.
|
||||
if "Delete student state for module" in action:
|
||||
try:
|
||||
sub_api.reset_score(
|
||||
anonymous_id_for_user(student, course_key),
|
||||
course_key.to_deprecated_string(),
|
||||
module_state_key.to_deprecated_string(),
|
||||
)
|
||||
except sub_api.SubmissionError:
|
||||
# Trust the submissions API to log the error
|
||||
error_msg = _("An error occurred while deleting the score.")
|
||||
msg += "<font color='red'>{err}</font> ".format(err=error_msg)
|
||||
|
||||
# find the module in question
|
||||
try:
|
||||
student_module = StudentModule.objects.get(
|
||||
student_id=student.id,
|
||||
course_id=course_key,
|
||||
module_state_key=module_state_key
|
||||
)
|
||||
msg += _("Found module. ")
|
||||
|
||||
except StudentModule.DoesNotExist as err:
|
||||
error_msg = _("Couldn't find module with that urlname: {url}. ").format(url=problem_location_str)
|
||||
msg += "<font color='red'>{err_msg} ({err})</font>".format(err_msg=error_msg, err=err)
|
||||
log.debug(error_msg)
|
||||
|
||||
if student_module is not None:
|
||||
if "Delete student state for module" in action:
|
||||
# delete the state
|
||||
try:
|
||||
student_module.delete()
|
||||
|
||||
msg += "<font color='red'>{text}</font>".format(
|
||||
text=_("Deleted student module state for {state}!").format(state=module_state_key)
|
||||
)
|
||||
event = {
|
||||
"problem": problem_location_str,
|
||||
"student": unique_student_identifier,
|
||||
"course": course_key.to_deprecated_string()
|
||||
}
|
||||
track.views.server_track(
|
||||
request,
|
||||
"delete-student-module-state",
|
||||
event,
|
||||
page="idashboard"
|
||||
)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
error_msg = _("Failed to delete module state for {id}/{url}. ").format(
|
||||
id=unique_student_identifier, url=problem_location_str
|
||||
)
|
||||
msg += "<font color='red'>{err_msg} ({err})</font>".format(err_msg=error_msg, err=err)
|
||||
log.exception(error_msg)
|
||||
elif "Reset student's attempts" in action:
|
||||
# modify the problem's state
|
||||
try:
|
||||
# load the state json
|
||||
problem_state = json.loads(student_module.state)
|
||||
old_number_of_attempts = problem_state["attempts"]
|
||||
problem_state["attempts"] = 0
|
||||
# save
|
||||
student_module.state = json.dumps(problem_state)
|
||||
student_module.save()
|
||||
event = {
|
||||
"old_attempts": old_number_of_attempts,
|
||||
"student": unicode(student),
|
||||
"problem": student_module.module_state_key,
|
||||
"instructor": unicode(request.user),
|
||||
"course": course_key.to_deprecated_string()
|
||||
}
|
||||
track.views.server_track(request, "reset-student-attempts", event, page="idashboard")
|
||||
msg += "<font color='green'>{text}</font>".format(
|
||||
text=_("Module state successfully reset!")
|
||||
)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
error_msg = _("Couldn't reset module state for {id}/{url}. ").format(
|
||||
id=unique_student_identifier, url=problem_location_str
|
||||
)
|
||||
msg += "<font color='red'>{err_msg} ({err})</font>".format(err_msg=error_msg, err=err)
|
||||
log.exception(error_msg)
|
||||
else:
|
||||
# "Rescore student's problem submission" case
|
||||
try:
|
||||
instructor_task = submit_rescore_problem_for_student(request, module_state_key, student)
|
||||
if instructor_task is None:
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Failed to create a background task for rescoring "{key}" for student {id}.').format(
|
||||
key=module_state_key, id=unique_student_identifier
|
||||
)
|
||||
)
|
||||
else:
|
||||
track.views.server_track(
|
||||
request,
|
||||
"rescore-student-submission",
|
||||
{
|
||||
"problem": module_state_key,
|
||||
"student": unique_student_identifier,
|
||||
"course": course_key.to_deprecated_string()
|
||||
},
|
||||
page="idashboard"
|
||||
)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Failed to create a background task for rescoring "{key}": {id}.').format(
|
||||
key=module_state_key, id=err.message
|
||||
)
|
||||
)
|
||||
log.exception("Encountered exception from rescore: student '{0}' problem '{1}'".format(
|
||||
unique_student_identifier, module_state_key
|
||||
)
|
||||
)
|
||||
|
||||
elif "Get link to student's progress page" in action:
|
||||
unique_student_identifier = request.POST.get('unique_student_identifier', '')
|
||||
# try to uniquely id student by email address or username
|
||||
message, student = get_student_from_identifier(unique_student_identifier)
|
||||
msg += message
|
||||
if student is not None:
|
||||
progress_url = reverse('student_progress', kwargs={
|
||||
'course_id': course_key.to_deprecated_string(),
|
||||
'student_id': student.id
|
||||
})
|
||||
track.views.server_track(
|
||||
request,
|
||||
"get-student-progress-page",
|
||||
{
|
||||
"student": unicode(student),
|
||||
"instructor": unicode(request.user),
|
||||
"course": course_key.to_deprecated_string()
|
||||
},
|
||||
page="idashboard"
|
||||
)
|
||||
msg += "<a href='{url}' target='_blank'>{text}</a>.".format(
|
||||
url=progress_url,
|
||||
text=_("Progress page for username: {username} with email address: {email}").format(
|
||||
username=student.username, email=student.email
|
||||
)
|
||||
)
|
||||
|
||||
#----------------------------------------
|
||||
# export grades to remote gradebook
|
||||
|
||||
@@ -551,8 +224,9 @@ def instructor_dashboard(request, course_id):
|
||||
datatable = {'header': ['Student email', 'Match?']}
|
||||
rg_students = [x['email'] for x in rg_stud_data['retdata']]
|
||||
|
||||
def domatch(x):
|
||||
return 'yes' if x.email in rg_students else 'No'
|
||||
def domatch(student):
|
||||
"""Returns 'yes' if student is pressent in the remote gradebook student list, else returns 'No'"""
|
||||
return 'yes' if student.email in rg_students else 'No'
|
||||
datatable['data'] = [[x.email, domatch(x)] for x in stud_data['students']]
|
||||
datatable['title'] = action
|
||||
|
||||
@@ -597,67 +271,9 @@ def instructor_dashboard(request, course_id):
|
||||
msg2, __ = _do_remote_gradebook(request.user, course, 'post-grades', files=files)
|
||||
msg += msg2
|
||||
|
||||
#----------------------------------------
|
||||
# Admin
|
||||
|
||||
elif 'List course staff' in action:
|
||||
role = CourseStaffRole(course.id)
|
||||
datatable = _role_members_table(role, _("List of Staff"), course_key)
|
||||
track.views.server_track(request, "list-staff", {}, page="idashboard")
|
||||
|
||||
elif 'List course instructors' in action and GlobalStaff().has_user(request.user):
|
||||
role = CourseInstructorRole(course.id)
|
||||
datatable = _role_members_table(role, _("List of Instructors"), course_key)
|
||||
track.views.server_track(request, "list-instructors", {}, page="idashboard")
|
||||
|
||||
elif action == 'Add course staff':
|
||||
uname = request.POST['staffuser']
|
||||
role = CourseStaffRole(course.id)
|
||||
msg += add_user_to_role(request, uname, role, 'staff', 'staff')
|
||||
|
||||
elif action == 'Add instructor' and request.user.is_staff:
|
||||
uname = request.POST['instructor']
|
||||
role = CourseInstructorRole(course.id)
|
||||
msg += add_user_to_role(request, uname, role, 'instructor', 'instructor')
|
||||
|
||||
elif action == 'Remove course staff':
|
||||
uname = request.POST['staffuser']
|
||||
role = CourseStaffRole(course.id)
|
||||
msg += remove_user_from_role(request, uname, role, 'staff', 'staff')
|
||||
|
||||
elif action == 'Remove instructor' and request.user.is_staff:
|
||||
uname = request.POST['instructor']
|
||||
role = CourseInstructorRole(course.id)
|
||||
msg += remove_user_from_role(request, uname, role, 'instructor', 'instructor')
|
||||
|
||||
#----------------------------------------
|
||||
# DataDump
|
||||
|
||||
elif 'Download CSV of all student profile data' in action:
|
||||
enrolled_students = User.objects.filter(
|
||||
courseenrollment__course_id=course_key,
|
||||
courseenrollment__is_active=1,
|
||||
).order_by('username').select_related("profile")
|
||||
profkeys = ['name', 'language', 'location', 'year_of_birth', 'gender', 'level_of_education',
|
||||
'mailing_address', 'goals']
|
||||
datatable = {'header': ['username', 'email'] + profkeys}
|
||||
|
||||
def getdat(user):
|
||||
"""
|
||||
Return a list of profile data for the given user.
|
||||
"""
|
||||
profile = user.profile
|
||||
return [user.username, user.email] + [getattr(profile, xkey, '') for xkey in profkeys]
|
||||
|
||||
datatable['data'] = [getdat(u) for u in enrolled_students]
|
||||
datatable['title'] = _('Student profile data for course {course_id}').format(
|
||||
course_id=course_key.to_deprecated_string()
|
||||
)
|
||||
return return_csv(
|
||||
'profiledata_{course_id}.csv'.format(course_id=course_key.to_deprecated_string()),
|
||||
datatable
|
||||
)
|
||||
|
||||
elif 'Download CSV of all responses to problem' in action:
|
||||
problem_to_dump = request.POST.get('problem_to_dump', '')
|
||||
|
||||
@@ -684,118 +300,6 @@ def instructor_dashboard(request, course_id):
|
||||
datatable['title'] = _('Student state for problem {problem}').format(problem=problem_to_dump)
|
||||
return return_csv('student_state_from_{problem}.csv'.format(problem=problem_to_dump), datatable)
|
||||
|
||||
elif 'Download CSV of all student anonymized IDs' in action:
|
||||
students = User.objects.filter(
|
||||
courseenrollment__course_id=course_key,
|
||||
).order_by('id')
|
||||
|
||||
datatable = {'header': ['User ID', 'Anonymized User ID', 'Course Specific Anonymized User ID']}
|
||||
datatable['data'] = [[s.id, unique_id_for_user(s, save=False), anonymous_id_for_user(s, course_key, save=False)] for s in students]
|
||||
return return_csv(course_key.to_deprecated_string().replace('/', '-') + '-anon-ids.csv', datatable)
|
||||
|
||||
#----------------------------------------
|
||||
# Group management
|
||||
|
||||
elif 'List beta testers' in action:
|
||||
role = CourseBetaTesterRole(course.id)
|
||||
datatable = _role_members_table(role, _("List of Beta Testers"), course_key)
|
||||
track.views.server_track(request, "list-beta-testers", {}, page="idashboard")
|
||||
|
||||
elif action == 'Add beta testers':
|
||||
users = request.POST['betausers']
|
||||
log.debug("users: {0!r}".format(users))
|
||||
role = CourseBetaTesterRole(course.id)
|
||||
for username_or_email in split_by_comma_and_whitespace(users):
|
||||
msg += "<p>{0}</p>".format(
|
||||
add_user_to_role(request, username_or_email, role, 'beta testers', 'beta-tester'))
|
||||
|
||||
elif action == 'Remove beta testers':
|
||||
users = request.POST['betausers']
|
||||
role = CourseBetaTesterRole(course.id)
|
||||
for username_or_email in split_by_comma_and_whitespace(users):
|
||||
msg += "<p>{0}</p>".format(
|
||||
remove_user_from_role(request, username_or_email, role, 'beta testers', 'beta-tester'))
|
||||
|
||||
#----------------------------------------
|
||||
# forum administration
|
||||
|
||||
elif action == 'List course forum admins':
|
||||
rolename = FORUM_ROLE_ADMINISTRATOR
|
||||
datatable = {}
|
||||
msg += _list_course_forum_members(course_key, rolename, datatable)
|
||||
track.views.server_track(
|
||||
request, "list-forum-admins", {"course": course_key.to_deprecated_string()}, page="idashboard"
|
||||
)
|
||||
|
||||
elif action == 'Remove forum admin':
|
||||
uname = request.POST['forumadmin']
|
||||
msg += _update_forum_role_membership(uname, course, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_REMOVE)
|
||||
track.views.server_track(
|
||||
request, "remove-forum-admin", {"username": uname, "course": course_key.to_deprecated_string()},
|
||||
page="idashboard"
|
||||
)
|
||||
|
||||
elif action == 'Add forum admin':
|
||||
uname = request.POST['forumadmin']
|
||||
msg += _update_forum_role_membership(uname, course, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_ADD)
|
||||
track.views.server_track(
|
||||
request, "add-forum-admin", {"username": uname, "course": course_key.to_deprecated_string()},
|
||||
page="idashboard"
|
||||
)
|
||||
|
||||
elif action == 'List course forum moderators':
|
||||
rolename = FORUM_ROLE_MODERATOR
|
||||
datatable = {}
|
||||
msg += _list_course_forum_members(course_key, rolename, datatable)
|
||||
track.views.server_track(
|
||||
request, "list-forum-mods", {"course": course_key.to_deprecated_string()}, page="idashboard"
|
||||
)
|
||||
|
||||
elif action == 'Remove forum moderator':
|
||||
uname = request.POST['forummoderator']
|
||||
msg += _update_forum_role_membership(uname, course, FORUM_ROLE_MODERATOR, FORUM_ROLE_REMOVE)
|
||||
track.views.server_track(
|
||||
request, "remove-forum-mod", {"username": uname, "course": course_key.to_deprecated_string()},
|
||||
page="idashboard"
|
||||
)
|
||||
|
||||
elif action == 'Add forum moderator':
|
||||
uname = request.POST['forummoderator']
|
||||
msg += _update_forum_role_membership(uname, course, FORUM_ROLE_MODERATOR, FORUM_ROLE_ADD)
|
||||
track.views.server_track(
|
||||
request, "add-forum-mod", {"username": uname, "course": course_key.to_deprecated_string()},
|
||||
page="idashboard"
|
||||
)
|
||||
|
||||
elif action == 'List course forum community TAs':
|
||||
rolename = FORUM_ROLE_COMMUNITY_TA
|
||||
datatable = {}
|
||||
msg += _list_course_forum_members(course_key, rolename, datatable)
|
||||
track.views.server_track(
|
||||
request, "list-forum-community-TAs", {"course": course_key.to_deprecated_string()},
|
||||
page="idashboard"
|
||||
)
|
||||
|
||||
elif action == 'Remove forum community TA':
|
||||
uname = request.POST['forummoderator']
|
||||
msg += _update_forum_role_membership(uname, course, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_REMOVE)
|
||||
track.views.server_track(
|
||||
request, "remove-forum-community-TA", {
|
||||
"username": uname, "course": course_key.to_deprecated_string()
|
||||
},
|
||||
page="idashboard"
|
||||
)
|
||||
|
||||
elif action == 'Add forum community TA':
|
||||
uname = request.POST['forummoderator']
|
||||
msg += _update_forum_role_membership(uname, course, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_ADD)
|
||||
track.views.server_track(
|
||||
request, "add-forum-community-TA", {
|
||||
"username": uname, "course": course_key.to_deprecated_string()
|
||||
},
|
||||
page="idashboard"
|
||||
)
|
||||
|
||||
#----------------------------------------
|
||||
# enrollment
|
||||
|
||||
@@ -842,55 +346,6 @@ def instructor_dashboard(request, course_id):
|
||||
ret = _do_enroll_students(course, course_key, students, secure=secure, overload=overload)
|
||||
datatable = ret['datatable']
|
||||
|
||||
#----------------------------------------
|
||||
# email
|
||||
|
||||
elif action == 'Send email':
|
||||
email_to_option = request.POST.get("to_option")
|
||||
email_subject = request.POST.get("subject")
|
||||
html_message = request.POST.get("message")
|
||||
|
||||
if bulk_email_is_enabled_for_course(course_key):
|
||||
try:
|
||||
# Create the CourseEmail object. This is saved immediately, so that
|
||||
# any transaction that has been pending up to this point will also be
|
||||
# committed.
|
||||
email = CourseEmail.create(
|
||||
course_key.to_deprecated_string(), request.user, email_to_option, email_subject, html_message
|
||||
)
|
||||
|
||||
# Submit the task, so that the correct InstructorTask object gets created (for monitoring purposes)
|
||||
submit_bulk_course_email(request, course_key, email.id) # pylint: disable=no-member
|
||||
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
# Catch any errors and deliver a message to the user
|
||||
error_msg = "Failed to send email! ({0})".format(err)
|
||||
msg += "<font color='red'>" + error_msg + "</font>"
|
||||
log.exception(error_msg)
|
||||
|
||||
else:
|
||||
# If sending the task succeeds, deliver a success message to the user.
|
||||
if email_to_option == "all":
|
||||
text = _(
|
||||
"Your email was successfully queued for sending. "
|
||||
"Please note that for large classes, it may take up to an hour "
|
||||
"(or more, if other courses are simultaneously sending email) "
|
||||
"to send all emails."
|
||||
)
|
||||
else:
|
||||
text = _('Your email was successfully queued for sending.')
|
||||
email_msg = '<div class="msg msg-confirm"><p class="copy">{text}</p></div>'.format(text=text)
|
||||
else:
|
||||
msg += "<font color='red'>Email is not enabled for this course.</font>"
|
||||
|
||||
elif "Show Background Email Task History" in action:
|
||||
message, datatable = get_background_task_table(course_key, task_type='bulk_course_email')
|
||||
msg += message
|
||||
|
||||
elif "Show Background Email Task History" in action:
|
||||
message, datatable = get_background_task_table(course_key, task_type='bulk_course_email')
|
||||
msg += message
|
||||
|
||||
#----------------------------------------
|
||||
# psychometrics
|
||||
|
||||
@@ -933,7 +388,7 @@ def instructor_dashboard(request, course_id):
|
||||
analytics_results = {}
|
||||
|
||||
if idash_mode == 'Analytics':
|
||||
DASHBOARD_ANALYTICS = [
|
||||
dashboard_analytics = [
|
||||
# "StudentsAttemptedProblems", # num students who tried given problem
|
||||
"StudentsDailyActivity", # active students by day
|
||||
"StudentsDropoffPerDay", # active students dropoff by day
|
||||
@@ -942,7 +397,7 @@ def instructor_dashboard(request, course_id):
|
||||
"ProblemGradeDistribution", # foreach problem, grade distribution
|
||||
]
|
||||
|
||||
for analytic_name in DASHBOARD_ANALYTICS:
|
||||
for analytic_name in dashboard_analytics:
|
||||
analytics_results[analytic_name] = get_analytics_result(analytic_name)
|
||||
|
||||
#----------------------------------------
|
||||
@@ -975,27 +430,6 @@ def instructor_dashboard(request, course_id):
|
||||
if is_studio_course:
|
||||
studio_url = get_cms_course_link(course)
|
||||
|
||||
email_editor = None
|
||||
# HTML editor for email
|
||||
if idash_mode == 'Email' and is_studio_course:
|
||||
html_module = HtmlDescriptor(
|
||||
course.system,
|
||||
DictFieldData({'data': html_message}),
|
||||
ScopeIds(None, None, None, course_key.make_usage_key('html', 'dummy'))
|
||||
)
|
||||
fragment = html_module.render('studio_view')
|
||||
fragment = wrap_xblock(
|
||||
'LmsRuntime', html_module, 'studio_view', fragment, None,
|
||||
extra_data={"course-id": course_key.to_deprecated_string()},
|
||||
usage_id_serializer=lambda usage_id: quote_slashes(usage_id.to_deprecated_string()),
|
||||
request_token=request_token(request),
|
||||
)
|
||||
email_editor = fragment.content
|
||||
|
||||
# Enable instructor email only if the following conditions are met:
|
||||
# 1. Feature flag is on
|
||||
# 2. We have explicitly enabled email for the given course via django-admin
|
||||
# 3. It is NOT an XML course
|
||||
if bulk_email_is_enabled_for_course(course_key):
|
||||
show_email_tab = True
|
||||
|
||||
@@ -1025,10 +459,6 @@ def instructor_dashboard(request, course_id):
|
||||
'modeflag': {idash_mode: 'selectedmode'},
|
||||
'studio_url': studio_url,
|
||||
|
||||
'to_option': email_to_option, # email
|
||||
'subject': email_subject, # email
|
||||
'editor': email_editor, # email
|
||||
'email_msg': email_msg, # email
|
||||
'show_email_tab': show_email_tab, # email
|
||||
|
||||
'problems': problems, # psychometrics
|
||||
@@ -1036,7 +466,6 @@ def instructor_dashboard(request, course_id):
|
||||
'course_errors': modulestore().get_course_errors(course.id),
|
||||
'instructor_tasks': instructor_tasks,
|
||||
'offline_grade_log': offline_grades_available(course_key),
|
||||
'cohorts_ajax_url': reverse('cohorts', kwargs={'course_key_string': course_key.to_deprecated_string()}),
|
||||
|
||||
'analytics_results': analytics_results,
|
||||
'disable_buttons': disable_buttons,
|
||||
@@ -1045,38 +474,38 @@ def instructor_dashboard(request, course_id):
|
||||
|
||||
context['standard_dashboard_url'] = reverse('instructor_dashboard', kwargs={'course_id': course_key.to_deprecated_string()})
|
||||
|
||||
return render_to_response('courseware/instructor_dashboard.html', context)
|
||||
return render_to_response('courseware/legacy_instructor_dashboard.html', context)
|
||||
|
||||
|
||||
def _do_remote_gradebook(user, course, action, args=None, files=None):
|
||||
'''
|
||||
Perform remote gradebook action. Returns msg, datatable.
|
||||
'''
|
||||
rg = course.remote_gradebook
|
||||
if not rg:
|
||||
rgb = course.remote_gradebook
|
||||
if not rgb:
|
||||
msg = _("No remote gradebook defined in course metadata")
|
||||
return msg, {}
|
||||
|
||||
rgurl = settings.FEATURES.get('REMOTE_GRADEBOOK_URL', '')
|
||||
if not rgurl:
|
||||
rgburl = settings.FEATURES.get('REMOTE_GRADEBOOK_URL', '')
|
||||
if not rgburl:
|
||||
msg = _("No remote gradebook url defined in settings.FEATURES")
|
||||
return msg, {}
|
||||
|
||||
rgname = rg.get('name', '')
|
||||
if not rgname:
|
||||
rgbname = rgb.get('name', '')
|
||||
if not rgbname:
|
||||
msg = _("No gradebook name defined in course remote_gradebook metadata")
|
||||
return msg, {}
|
||||
|
||||
if args is None:
|
||||
args = {}
|
||||
data = dict(submit=action, gradebook=rgname, user=user.email)
|
||||
data = dict(submit=action, gradebook=rgbname, user=user.email)
|
||||
data.update(args)
|
||||
|
||||
try:
|
||||
resp = requests.post(rgurl, data=data, verify=False, files=files)
|
||||
resp = requests.post(rgburl, data=data, verify=False, files=files)
|
||||
retdict = json.loads(resp.content)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
msg = _("Failed to communicate with gradebook server at {url}").format(url=rgurl) + "<br/>"
|
||||
msg = _("Failed to communicate with gradebook server at {url}").format(url=rgburl) + "<br/>"
|
||||
msg += _("Error: {err}").format(err=err)
|
||||
msg += "<br/>resp={resp}".format(resp=resp.content)
|
||||
msg += "<br/>data={data}".format(data=data)
|
||||
@@ -1096,80 +525,6 @@ def _do_remote_gradebook(user, course, action, args=None, files=None):
|
||||
return msg, datatable
|
||||
|
||||
|
||||
def _list_course_forum_members(course_key, rolename, datatable):
|
||||
"""
|
||||
Fills in datatable with forum membership information, for a given role,
|
||||
so that it will be displayed on instructor dashboard.
|
||||
|
||||
course_ID = the CourseKey for a course
|
||||
rolename = one of "Administrator", "Moderator", "Community TA"
|
||||
|
||||
Returns message status string to append to displayed message, if role is unknown.
|
||||
"""
|
||||
# make sure datatable is set up properly for display first, before checking for errors
|
||||
datatable['header'] = [_('Username'), _('Full name'), _('Roles')]
|
||||
datatable['title'] = _('List of Forum {name}s in course {id}').format(
|
||||
name=rolename, id=course_key.to_deprecated_string()
|
||||
)
|
||||
datatable['data'] = []
|
||||
try:
|
||||
role = Role.objects.get(name=rolename, course_id=course_key)
|
||||
except Role.DoesNotExist:
|
||||
return '<font color="red">' + _('Error: unknown rolename "{rolename}"').format(rolename=rolename) + '</font>'
|
||||
uset = role.users.all().order_by('username')
|
||||
msg = 'Role = {0}'.format(rolename)
|
||||
log.debug('role={0}'.format(rolename))
|
||||
datatable['data'] = [[x.username, x.profile.name, ', '.join([
|
||||
r.name for r in x.roles.filter(course_id=course_key).order_by('name')
|
||||
])] for x in uset]
|
||||
return msg
|
||||
|
||||
|
||||
def _update_forum_role_membership(uname, course, rolename, add_or_remove):
|
||||
'''
|
||||
Supports adding a user to a course's forum role
|
||||
|
||||
uname = username string for user
|
||||
course = course object
|
||||
rolename = one of "Administrator", "Moderator", "Community TA"
|
||||
add_or_remove = one of "add" or "remove"
|
||||
|
||||
Returns message status string to append to displayed message, Status is returned if user
|
||||
or role is unknown, or if entry already exists when adding, or if entry doesn't exist when removing.
|
||||
'''
|
||||
# check that username and rolename are valid:
|
||||
try:
|
||||
user = User.objects.get(username=uname)
|
||||
except User.DoesNotExist:
|
||||
return '<font color="red">' + _('Error: unknown username "{username}"').format(username=uname) + '</font>'
|
||||
try:
|
||||
role = Role.objects.get(name=rolename, course_id=course.id)
|
||||
except Role.DoesNotExist:
|
||||
return '<font color="red">' + _('Error: unknown rolename "{rolename}"').format(rolename=rolename) + '</font>'
|
||||
|
||||
# check whether role already has the specified user:
|
||||
alreadyexists = role.users.filter(username=uname).exists()
|
||||
msg = ''
|
||||
log.debug('rolename={0}'.format(rolename))
|
||||
if add_or_remove == FORUM_ROLE_REMOVE:
|
||||
if not alreadyexists:
|
||||
msg = '<font color="red">' + _('Error: user "{username}" does not have rolename "{rolename}", cannot remove').format(username=uname, rolename=rolename) + '</font>'
|
||||
else:
|
||||
user.roles.remove(role)
|
||||
msg = '<font color="green">' + _('Removed "{username}" from "{course_id}" forum role = "{rolename}"').format(username=user, course_id=course.id.to_deprecated_string(), rolename=rolename) + '</font>'
|
||||
else:
|
||||
if alreadyexists:
|
||||
msg = '<font color="red">' + _('Error: user "{username}" already has rolename "{rolename}", cannot add').format(username=uname, rolename=rolename) + '</font>'
|
||||
else:
|
||||
if (rolename == FORUM_ROLE_ADMINISTRATOR and not has_access(user, 'staff', course)):
|
||||
msg = '<font color="red">' + _('Error: user "{username}" should first be added as staff before adding as a forum administrator, cannot add').format(username=uname) + '</font>'
|
||||
else:
|
||||
user.roles.add(role)
|
||||
msg = '<font color="green">' + _('Added "{username}" to "{course_id}" forum role = "{rolename}"').format(username=user, course_id=course.id.to_deprecated_string(), rolename=rolename) + '</font>'
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def _role_members_table(role, title, course_key):
|
||||
"""
|
||||
Return a data table of usernames and names of users in group_name.
|
||||
@@ -1389,7 +744,7 @@ def get_student_grade_summary_data(request, course, get_grades=True, get_raw_sco
|
||||
datarow = [student.id, student.username, student.profile.name, student.email]
|
||||
try:
|
||||
datarow.append(student.externalauthmap.external_email)
|
||||
except: # ExternalAuthMap.DoesNotExist
|
||||
except Exception: # pylint: disable=broad-except
|
||||
datarow.append('')
|
||||
|
||||
if get_grades:
|
||||
@@ -1448,12 +803,12 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal
|
||||
|
||||
if overload: # delete all but staff
|
||||
todelete = CourseEnrollment.objects.filter(course_id=course_key)
|
||||
for ce in todelete:
|
||||
if not has_access(ce.user, 'staff', course) and ce.user.email.lower() not in new_students_lc:
|
||||
status[ce.user.email] = 'deleted'
|
||||
ce.deactivate()
|
||||
for enrollee in todelete:
|
||||
if not has_access(enrollee.user, 'staff', course) and enrollee.user.email.lower() not in new_students_lc:
|
||||
status[enrollee.user.email] = 'deleted'
|
||||
enrollee.deactivate()
|
||||
else:
|
||||
status[ce.user.email] = 'is staff'
|
||||
status[enrollee.user.email] = 'is staff'
|
||||
ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_key)
|
||||
for cea in ceaset:
|
||||
status[cea.email] = 'removed from pending enrollment list'
|
||||
@@ -1487,7 +842,7 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal
|
||||
)
|
||||
|
||||
# Composition of email
|
||||
d = {
|
||||
email_data = {
|
||||
'site_name': stripped_site_name,
|
||||
'registration_url': registration_url,
|
||||
'course': course,
|
||||
@@ -1502,11 +857,11 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal
|
||||
user = User.objects.get(email=student)
|
||||
except User.DoesNotExist:
|
||||
|
||||
#Student not signed up yet, put in pending enrollment allowed table
|
||||
# Student not signed up yet, put in pending enrollment allowed table
|
||||
cea = CourseEnrollmentAllowed.objects.filter(email=student, course_id=course_key)
|
||||
|
||||
#If enrollmentallowed already exists, update auto_enroll flag to however it was set in UI
|
||||
#Will be 0 or 1 records as there is a unique key on email + course_id
|
||||
# If enrollmentallowed already exists, update auto_enroll flag to however it was set in UI
|
||||
# Will be 0 or 1 records as there is a unique key on email + course_id
|
||||
if cea:
|
||||
cea[0].auto_enroll = auto_enroll
|
||||
cea[0].save()
|
||||
@@ -1514,7 +869,7 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal
|
||||
+ ('on' if auto_enroll else 'off')
|
||||
continue
|
||||
|
||||
#EnrollmentAllowed doesn't exist so create it
|
||||
# EnrollmentAllowed doesn't exist so create it
|
||||
cea = CourseEnrollmentAllowed(email=student, course_id=course_key, auto_enroll=auto_enroll)
|
||||
cea.save()
|
||||
|
||||
@@ -1523,9 +878,9 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal
|
||||
|
||||
if email_students:
|
||||
# User is allowed to enroll but has not signed up yet
|
||||
d['email_address'] = student
|
||||
d['message'] = 'allowed_enroll'
|
||||
send_mail_ret = send_mail_to_student(student, d)
|
||||
email_data['email_address'] = student
|
||||
email_data['message'] = 'allowed_enroll'
|
||||
send_mail_ret = send_mail_to_student(student, email_data)
|
||||
status[student] += (', email sent' if send_mail_ret else '')
|
||||
continue
|
||||
|
||||
@@ -1541,13 +896,13 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal
|
||||
|
||||
if email_students:
|
||||
# User enrolled for first time, populate dict with user specific info
|
||||
d['email_address'] = student
|
||||
d['full_name'] = user.profile.name
|
||||
d['message'] = 'enrolled_enroll'
|
||||
send_mail_ret = send_mail_to_student(student, d)
|
||||
email_data['email_address'] = student
|
||||
email_data['full_name'] = user.profile.name
|
||||
email_data['message'] = 'enrolled_enroll'
|
||||
send_mail_ret = send_mail_to_student(student, email_data)
|
||||
status[student] += (', email sent' if send_mail_ret else '')
|
||||
|
||||
except:
|
||||
except Exception: # pylint: disable=broad-except
|
||||
status[student] = 'rejected'
|
||||
|
||||
datatable = {'header': ['StudentEmail', 'action']}
|
||||
@@ -1582,15 +937,17 @@ def _do_unenroll_students(course_key, students, email_students=False):
|
||||
)
|
||||
if email_students:
|
||||
course = modulestore().get_course(course_key)
|
||||
#Composition of email
|
||||
d = {'site_name': stripped_site_name,
|
||||
'course': course}
|
||||
# Composition of email
|
||||
data = {
|
||||
'site_name': stripped_site_name,
|
||||
'course': course
|
||||
}
|
||||
|
||||
for student in old_students:
|
||||
|
||||
isok = False
|
||||
cea = CourseEnrollmentAllowed.objects.filter(course_id=course_key, email=student)
|
||||
#Will be 0 or 1 records as there is a unique key on email + course_id
|
||||
# Will be 0 or 1 records as there is a unique key on email + course_id
|
||||
if cea:
|
||||
cea[0].delete()
|
||||
status[student] = "un-enrolled"
|
||||
@@ -1601,25 +958,25 @@ def _do_unenroll_students(course_key, students, email_students=False):
|
||||
except User.DoesNotExist:
|
||||
|
||||
if isok and email_students:
|
||||
#User was allowed to join but had not signed up yet
|
||||
d['email_address'] = student
|
||||
d['message'] = 'allowed_unenroll'
|
||||
send_mail_ret = send_mail_to_student(student, d)
|
||||
# User was allowed to join but had not signed up yet
|
||||
data['email_address'] = student
|
||||
data['message'] = 'allowed_unenroll'
|
||||
send_mail_ret = send_mail_to_student(student, data)
|
||||
status[student] += (', email sent' if send_mail_ret else '')
|
||||
|
||||
continue
|
||||
|
||||
#Will be 0 or 1 records as there is a unique key on user + course_id
|
||||
# Will be 0 or 1 records as there is a unique key on user + course_id
|
||||
if CourseEnrollment.is_enrolled(user, course_key):
|
||||
try:
|
||||
CourseEnrollment.unenroll(user, course_key)
|
||||
status[student] = "un-enrolled"
|
||||
if email_students:
|
||||
#User was enrolled
|
||||
d['email_address'] = student
|
||||
d['full_name'] = user.profile.name
|
||||
d['message'] = 'enrolled_unenroll'
|
||||
send_mail_ret = send_mail_to_student(student, d)
|
||||
# User was enrolled
|
||||
data['email_address'] = student
|
||||
data['full_name'] = user.profile.name
|
||||
data['message'] = 'enrolled_unenroll'
|
||||
send_mail_ret = send_mail_to_student(student, data)
|
||||
status[student] += (', email sent' if send_mail_ret else '')
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
@@ -1630,8 +987,7 @@ def _do_unenroll_students(course_key, students, email_students=False):
|
||||
datatable['data'] = [[x, status[x]] for x in sorted(status)]
|
||||
datatable['title'] = _('Un-enrollment of students')
|
||||
|
||||
data = dict(datatable=datatable)
|
||||
return data
|
||||
return dict(datatable=datatable)
|
||||
|
||||
|
||||
def send_mail_to_student(student, param_dict):
|
||||
@@ -1728,15 +1084,15 @@ def get_answers_distribution(request, course_key):
|
||||
|
||||
dist = grades.answer_distributions(course.id)
|
||||
|
||||
d = {}
|
||||
d['header'] = ['url_name', 'display name', 'answer id', 'answer', 'count']
|
||||
dist = {}
|
||||
dist['header'] = ['url_name', 'display name', 'answer id', 'answer', 'count']
|
||||
|
||||
d['data'] = [
|
||||
dist['data'] = [
|
||||
[url_name, display_name, answer_id, a, answers[a]]
|
||||
for (url_name, display_name, answer_id), answers in sorted(dist.items())
|
||||
for a in answers
|
||||
]
|
||||
return d
|
||||
return dist
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
@@ -1758,8 +1114,8 @@ def compute_course_stats(course):
|
||||
children = module.get_children()
|
||||
category = module.__class__.__name__ # HtmlDescriptor, CapaDescriptor, ...
|
||||
counts[category] += 1
|
||||
for c in children:
|
||||
walk(c)
|
||||
for child in children:
|
||||
walk(child)
|
||||
|
||||
walk(course)
|
||||
stats = dict(counts) # number of each kind of module
|
||||
|
||||
@@ -315,6 +315,11 @@ mark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// UI - is deprecated
|
||||
.is-deprecated {
|
||||
@extend %ui-deprecated;
|
||||
}
|
||||
|
||||
// UI - semantically hide text
|
||||
.sr {
|
||||
@extend %text-sr;
|
||||
|
||||
@@ -113,3 +113,10 @@
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
%ui-deprecated {
|
||||
@extend %t-weight4;
|
||||
background: tint($warning-color, 85%);
|
||||
padding: ($baseline/5) ($baseline/2);
|
||||
color: shade($warning-color, 45%);
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<section class="cohort_manager" data-ajax_url="${cohorts_ajax_url}">
|
||||
<h3>${_("Cohort groups")}</h3>
|
||||
|
||||
<div class="controls" style="padding-top:15px">
|
||||
<a href="#" class="button show_cohorts">${_("Show cohorts")}</a>
|
||||
</div>
|
||||
|
||||
<ul class="errors">
|
||||
</ul>
|
||||
|
||||
<div class="summary" style="display:none">
|
||||
<h3>${_("Cohorts in the course")}</h3>
|
||||
<ul class="cohorts">
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<input class="cohort_name"/>
|
||||
<a href="#" class="button add_cohort">${_("Add cohort")}</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="detail" style="display:none">
|
||||
<h3 class="header"></h3>
|
||||
<table class="users">
|
||||
</table>
|
||||
<span class="page_num"></span>
|
||||
|
||||
<p>
|
||||
${_("Add users by username or email. One per line or comma-separated.")}
|
||||
</p>
|
||||
<textarea cols="50" row="30" class="users_area" style="height: 200px"></textarea>
|
||||
<a href="#" class="button add_members">${_("Add cohort members")}</a>
|
||||
|
||||
<ul class="op_results">
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
@@ -1,18 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<html lang="${LANGUAGE_CODE}">
|
||||
<head>
|
||||
## "edX" should not be translated
|
||||
<%block name="pagetitle"></%block>
|
||||
|
||||
<script type="text/javascript" src="/static/js/vendor/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="/static/js/course_groups/cohorts.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="<%block name='bodyclass'/>">
|
||||
<%include file="/course_groups/cohort_management.html" />
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -136,7 +136,7 @@ function goto( mode)
|
||||
<a class="instructor-info-action beta-button" href="${ standard_dashboard_url }">${_("Back To Instructor Dashboard")}</a>
|
||||
</div>
|
||||
|
||||
<h1>${_("Instructor Dashboard")}</h1>
|
||||
<h1>${_("Legacy Instructor Dashboard")}</h1>
|
||||
|
||||
%if settings.FEATURES.get('IS_EDX_DOMAIN', False):
|
||||
## Only show this banner on the edx.org website (other sites may choose to show this if they wish)
|
||||
@@ -201,19 +201,10 @@ function goto( mode)
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<p>
|
||||
<a href="${reverse('spoc_gradebook', kwargs=dict(course_id=course.id.to_deprecated_string()))}" class="${'is-disabled' if disable_buttons else ''}">${_("Gradebook")}</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input type="submit" name="action" value="Dump list of enrolled students" class="${'is-disabled' if disable_buttons else ''}">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input type="submit" name="action" value="Dump Grades for all students in this course" class="${'is-disabled' if disable_buttons else ''}">
|
||||
<input type="submit" name="action" value="Download CSV of all student grades for this course" class="${'is-disabled' if disable_buttons else ''}">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input type="submit" name="action" value="Dump all RAW grades for all students in this course" class="${'is-disabled' if disable_buttons else ''}">
|
||||
<input type="submit" name="action" value="Download CSV of all RAW grades" class="${'is-disabled' if disable_buttons else ''}">
|
||||
@@ -223,7 +214,12 @@ function goto( mode)
|
||||
%if not settings.FEATURES.get('ENABLE_ASYNC_ANSWER_DISTRIBUTION'):
|
||||
<input type="submit" name="action" value="Download CSV of answer distributions" class="${'is-disabled' if disable_buttons else ''}">
|
||||
%endif
|
||||
<input type="submit" name="action" value="Dump description of graded assignments configuration">
|
||||
<p class="is-deprecated">
|
||||
${_("To download student grades and view the grading configuration for your course, visit the Data Download section of the Instructor Dashboard.")}
|
||||
</p>
|
||||
<p class="is-deprecated">
|
||||
${_("To view the Gradebook (only available for courses with a small number of enrolled students), visit the Student Admin section of the Instructor Dashboard.")}
|
||||
</p>
|
||||
</p>
|
||||
<hr width="40%" style="align:left">
|
||||
|
||||
@@ -263,67 +259,13 @@ function goto( mode)
|
||||
%if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
|
||||
<H2>${_("Course-specific grade adjustment")}</h2>
|
||||
|
||||
<p>
|
||||
${_("Specify a problem in the course here with its complete location:")}
|
||||
<input type="text" name="problem_for_all_students" size="60">
|
||||
</p>
|
||||
## Translators: A location (string of text) follows this sentence.
|
||||
<p>${_("You must provide the complete location of the problem. In the Staff Debug viewer, the location looks like this:")}<br/>
|
||||
<tt>i4x://edX/Open_DemoX/problem/78c98390884243b89f6023745231c525</tt></p>
|
||||
<p>
|
||||
${_("Then select an action:")}
|
||||
<input type="submit" name="action" value="Reset ALL students' attempts">
|
||||
<input type="submit" name="action" value="Rescore ALL students' problem submissions">
|
||||
</p>
|
||||
<p>
|
||||
<p>${_("These actions run in the background, and status for active tasks will appear in a table below. To see status for all tasks submitted for this problem, click on this button:")}
|
||||
</p>
|
||||
<p>
|
||||
<input type="submit" name="action" value="Show Background Task History">
|
||||
</p>
|
||||
<p class="is-deprecated">${_("To perform these actions, visit the Student Admin section of the Instructor Dashboard.")}</p>
|
||||
|
||||
<hr width="40%" style="align:left">
|
||||
%endif
|
||||
|
||||
<h2>${_("Student-specific grade inspection and adjustment")}</h2>
|
||||
<p>
|
||||
${_("Specify the {platform_name} email address or username of a student here:").format(platform_name=settings.PLATFORM_NAME)}
|
||||
<input type="text" name="unique_student_identifier">
|
||||
</p>
|
||||
<p>
|
||||
${_("Click this, and a link to student's progress page will appear below:")}
|
||||
<input type="submit" name="action" value="Get link to student's progress page">
|
||||
</p>
|
||||
<p>
|
||||
${_("Specify a problem in the course here with its complete location:")}
|
||||
<input type="text" name="problem_for_student" size="60">
|
||||
</p>
|
||||
## Translators: A location (string of text) follows this sentence.
|
||||
<p>${_("You must provide the complete location of the problem. In the Staff Debug viewer, the location looks like this:")}<br/>
|
||||
<tt>i4x://edX/Open_DemoX/problem/78c98390884243b89f6023745231c525</tt></p>
|
||||
|
||||
<p>
|
||||
${_("Then select an action:")}
|
||||
<input type="submit" name="action" value="Reset student's attempts">
|
||||
%if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
|
||||
<input type="submit" name="action" value="Rescore student's problem submission">
|
||||
%endif
|
||||
</p>
|
||||
|
||||
%if instructor_access:
|
||||
<p>
|
||||
${_("You may also delete the entire state of a student for the specified module:")}
|
||||
<input type="submit" name="action" value="Delete student state for module">
|
||||
</p>
|
||||
%endif
|
||||
%if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
|
||||
<p>${_("Rescoring runs in the background, and status for active tasks will appear in a table below. "
|
||||
"To see status for all tasks submitted for this problem and student, click on this button:")}
|
||||
</p>
|
||||
<p>
|
||||
<input type="submit" name="action" value="Show Background Task History for Student">
|
||||
</p>
|
||||
%endif
|
||||
<p class="is-deprecated">${_("To perform these actions, visit the Student Admin section of the Instructor Dashboard.")}</p>
|
||||
|
||||
%endif
|
||||
|
||||
@@ -351,25 +293,8 @@ function goto( mode)
|
||||
##-----------------------------------------------------------------------------
|
||||
%if modeflag.get('Admin'):
|
||||
|
||||
%if instructor_access:
|
||||
<hr width="40%" style="align:left">
|
||||
<p>
|
||||
<input type="submit" name="action" value="List course staff members">
|
||||
<p>
|
||||
<input type="text" name="staffuser">
|
||||
<input type="submit" name="action" value="Remove course staff">
|
||||
<input type="submit" name="action" value="Add course staff">
|
||||
<hr width="40%" style="align:left">
|
||||
%endif
|
||||
|
||||
%if admin_access:
|
||||
<hr width="40%" style="align:left">
|
||||
<p>
|
||||
<input type="submit" name="action" value="List course instructors">
|
||||
<p>
|
||||
<input type="text" name="instructor"> <input type="submit" name="action" value="Remove instructor">
|
||||
<input type="submit" name="action" value="Add instructor">
|
||||
<hr width="40%" style="align:left">
|
||||
%if instructor_access or admin_access:
|
||||
<p class="is-deprecated">${_("To add or remove course staff or instructors, visit the Membership section of the Instructor Dashboard.")}</p>
|
||||
%endif
|
||||
|
||||
%if settings.FEATURES['ENABLE_MANUAL_GIT_RELOAD'] and admin_access:
|
||||
@@ -381,38 +306,7 @@ function goto( mode)
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
%if modeflag.get('Forum Admin'):
|
||||
%if instructor_access:
|
||||
<hr width="40%" style="align:left">
|
||||
<p>
|
||||
<input type="submit" name="action" value="List course forum admins">
|
||||
<p>
|
||||
<input type="text" name="forumadmin"> <input type="submit" name="action" value="Remove forum admin">
|
||||
<input type="submit" name="action" value="Add forum admin">
|
||||
<hr width="40%" style="align:left">
|
||||
%endif
|
||||
|
||||
%if instructor_access or forum_admin_access:
|
||||
<p>
|
||||
<input type="submit" name="action" value="List course forum moderators">
|
||||
<input type="submit" name="action" value="List course forum community TAs">
|
||||
<p>
|
||||
<input type="text" name="forummoderator">
|
||||
<input type="submit" name="action" value="Remove forum moderator">
|
||||
<input type="submit" name="action" value="Add forum moderator">
|
||||
<input type="submit" name="action" value="Remove forum community TA">
|
||||
<input type="submit" name="action" value="Add forum community TA">
|
||||
<hr width="40%" style="align:left">
|
||||
%else:
|
||||
<p>${_("User requires forum administrator privileges to perform administration tasks. See instructor.")}</p>
|
||||
%endif
|
||||
|
||||
<br />
|
||||
<h2>${_("Explanation of Roles:")}</h2>
|
||||
<p>${_("Forum Moderators: can edit or delete any post, remove misuse flags, close and re-open threads, endorse "
|
||||
"responses, and see posts from all cohorts (if the course is cohorted). Moderators' posts are marked as 'staff'.")}</p>
|
||||
<p>${_("Forum Admins: have moderator privileges, as well as the ability to edit the list of forum moderators "
|
||||
"(e.g. to appoint a new moderator). Admins' posts are marked as 'staff'.")}</p>
|
||||
<p>${_("Community TAs: have forum moderator privileges, and their posts are labelled 'Community TA'.")}</p>
|
||||
<p class="is-deprecated">${_("To manage forum roles, visit the Membership section of the Instructor Dashboard.")}</p>
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
@@ -463,15 +357,13 @@ function goto( mode)
|
||||
|
||||
%if modeflag.get('Data'):
|
||||
<hr width="40%" style="align:left">
|
||||
<p>
|
||||
<input type="submit" name="action" value="Download CSV of all student profile data">
|
||||
</p>
|
||||
<p> ${_("Problem urlname:")}
|
||||
<input type="text" name="problem_to_dump" size="40">
|
||||
<input type="submit" name="action" value="Download CSV of all responses to problem">
|
||||
</p>
|
||||
<p>
|
||||
<input type="submit" name="action" value="Download CSV of all student anonymized IDs">
|
||||
|
||||
<p class="is-deprecated">
|
||||
${_("To download student profile data and anonymized IDs, visit the Data Download section of the Instructor Dashboard.")}
|
||||
</p>
|
||||
<hr width="40%" style="align:left">
|
||||
%endif
|
||||
@@ -480,103 +372,18 @@ function goto( mode)
|
||||
|
||||
%if modeflag.get('Manage Groups'):
|
||||
%if instructor_access:
|
||||
<hr width="40%" style="align:left">
|
||||
<p>
|
||||
<input type="submit" name="action" value="List beta testers">
|
||||
<p>
|
||||
## Translators: days_early_for_beta should not be translated
|
||||
${_("Enter usernames or emails for students who should be beta-testers, one per line, or separated by commas. They will get to "
|
||||
"see course materials early, as configured via the <tt>days_early_for_beta</tt> option in the course policy.")}
|
||||
</p>
|
||||
<p>
|
||||
<textarea cols="50" row="30" name="betausers"></textarea>
|
||||
<input type="submit" name="action" value="Remove beta testers">
|
||||
<input type="submit" name="action" value="Add beta testers">
|
||||
</p>
|
||||
<hr width="40%" style="align:left">
|
||||
|
||||
%if course.is_cohorted:
|
||||
<%include file="/course_groups/cohort_management.html" />
|
||||
<p class="is-deprecated">${_("To manage beta tester roles and cohort groups, visit the Membership section of the Instructor Dashboard.")}</p>
|
||||
%else:
|
||||
<p class="is-deprecated">${_("To manage beta tester roles, visit the Membership section of the Instructor Dashboard.")}</p>
|
||||
%endif
|
||||
|
||||
%endif
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
|
||||
%if modeflag.get('Email'):
|
||||
%if email_msg:
|
||||
<p></p><p>${email_msg}</p>
|
||||
%endif
|
||||
|
||||
<ul class="list-fields">
|
||||
<li class="field">
|
||||
<label for="id_to">${_("Send to:")}</label>
|
||||
<select id="id_to" name="to_option">
|
||||
<option value="myself">${_("Myself")}</option>
|
||||
%if to_option == "staff":
|
||||
<option value="staff" selected="selected">${_("Staff and instructors")}</option>
|
||||
%else:
|
||||
<option value="staff">${_("Staff and instructors")}</option>
|
||||
%endif
|
||||
%if to_option == "all":
|
||||
<option value="all" selected="selected">${_("All (students, staff and instructors)")}</option>
|
||||
%else:
|
||||
<option value="all">${_("All (students, staff and instructors)")}</option>
|
||||
%endif
|
||||
</select>
|
||||
</li>
|
||||
|
||||
<li class="field">
|
||||
<label for="id_subject">${_("Subject: ")}</label>
|
||||
%if subject:
|
||||
<input type="text" id="id_subject" name="subject" maxlength="128" size="75" value="${subject}">
|
||||
%else:
|
||||
<input type="text" id="id_subject" name="subject" maxlength="128" size="75">
|
||||
%endif
|
||||
<span class="tip">${_("(Max 128 characters)")}</span>
|
||||
</li>
|
||||
|
||||
<li class="field">
|
||||
<label>Message:</label>
|
||||
<div class="email-editor">
|
||||
${editor}
|
||||
</div>
|
||||
<input type="hidden" name="message" value="">
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="submit-email-action">
|
||||
<p class="copy">${_("Please try not to email students more than once per week. Important things to consider before sending:")}</p>
|
||||
<ul class="list-advice">
|
||||
<li class="item">${_("Have you read over the email to make sure it says everything you want to say?")}</li>
|
||||
<li class="item">${_("Have you sent the email to yourself first to make sure you're happy with how it's displayed, and that embedded links and images work properly?")}</li>
|
||||
</ul>
|
||||
<div class="submit-email-warning">
|
||||
<p class="copy"><span style="color: red;"><b>${_("CAUTION!")}</b></span>
|
||||
${_("Once the 'Send Email' button is clicked, your email will be queued for sending.")}
|
||||
<b>${_("A queued email CANNOT be cancelled.")}</b></p>
|
||||
</div>
|
||||
<br />
|
||||
<input type="submit" name="action" value="Send email">
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
var emailEditor = XBlock.initializeBlock($('.xblock-studio_view'));
|
||||
document.idashform.onsubmit = function() {
|
||||
this.message.value = emailEditor.save()['data'];
|
||||
return true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<br />
|
||||
<p>These email actions run in the background, and status for active email tasks will appear in a table below.
|
||||
To see status for all bulk email tasks submitted for this course, click on this button:
|
||||
</p>
|
||||
<p>
|
||||
<input type="submit" name="action" value="Show Background Email Task History">
|
||||
</p>
|
||||
<p class="is-deprecated">${_("To send email, visit the Email section of the Instructor Dashboard.")}</p>
|
||||
%endif
|
||||
|
||||
</form>
|
||||
@@ -848,8 +655,7 @@ function goto( mode)
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
|
||||
%if course_stats and modeflag.get('Psychometrics') is None:
|
||||
|
||||
%if modeflag.get('Admin') and course_stats:
|
||||
<br/>
|
||||
<br/>
|
||||
<p>
|
||||
@@ -870,6 +676,13 @@ function goto( mode)
|
||||
%endfor
|
||||
</table>
|
||||
</p>
|
||||
%else:
|
||||
<br/>
|
||||
<br/>
|
||||
<h2>${_("Course Statistics At A Glance")}</h2>
|
||||
<p class="is-deprecated">
|
||||
${_("View course statistics in the Admin section of this legacy instructor dashboard.")}
|
||||
</p>
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
Reference in New Issue
Block a user