Commit 69fd063d by Sarina Canelake

Merge pull request #6014 from edx/sarina/legacy-dash-cleanup

Legacy dash cleanup
parents 46545935 a1c8f8c4
"""
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')
)
"""
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)
"""
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.")
"""
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))
"""
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): ...@@ -63,6 +63,3 @@ class TestXss(ModuleStoreTestCase):
def test_dump_list_of_enrolled(self): def test_dump_list_of_enrolled(self):
self._test_action("Dump list of enrolled students") 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 ...@@ -3,7 +3,6 @@ Instructor Views
""" """
## NOTE: This is the code for the legacy instructor dashboard ## NOTE: This is the code for the legacy instructor dashboard
## We are no longer supporting this file or accepting changes into it. ## We are no longer supporting this file or accepting changes into it.
# pylint: skip-file
from contextlib import contextmanager from contextlib import contextmanager
import csv import csv
import json import json
...@@ -27,39 +26,22 @@ from django.core.urlresolvers import reverse ...@@ -27,39 +26,22 @@ from django.core.urlresolvers import reverse
from django.core.mail import send_mail from django.core.mail import send_mail
from django.utils import timezone from django.utils import timezone
from xmodule_modifiers import wrap_xblock, request_token
import xmodule.graders as xmgraders import xmodule.graders as xmgraders
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from opaque_keys.edx.locations import SlashSeparatedCourseKey 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 import grades
from courseware.access import has_access from courseware.access import has_access
from courseware.courses import get_course_with_access, get_cms_course_link 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 courseware.models import StudentModule
from django_comment_common.models import ( from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR
Role, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA
)
from django_comment_client.utils import has_forum_access from django_comment_client.utils import has_forum_access
from instructor.offline_gradecalc import student_grades, offline_grades_available 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.views.tools import strip_if_string, bulk_email_is_enabled_for_course, add_block_ids
from instructor_task.api import ( from instructor_task.api import (
get_running_instructor_tasks, get_running_instructor_tasks,
get_instructor_task_history, 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 instructor_task.views import get_task_completion_info
from edxmako.shortcuts import render_to_response, render_to_string from edxmako.shortcuts import render_to_response, render_to_string
...@@ -68,12 +50,8 @@ from psychometrics import psychoanalyze ...@@ -68,12 +50,8 @@ from psychometrics import psychoanalyze
from student.models import ( from student.models import (
CourseEnrollment, CourseEnrollment,
CourseEnrollmentAllowed, CourseEnrollmentAllowed,
unique_id_for_user,
anonymous_id_for_user
) )
import track.views import track.views
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from microsite_configuration import microsite from microsite_configuration import microsite
...@@ -108,10 +86,6 @@ def instructor_dashboard(request, course_id): ...@@ -108,10 +86,6 @@ def instructor_dashboard(request, course_id):
forum_admin_access = has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR) forum_admin_access = has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR)
msg = '' msg = ''
email_msg = ''
email_to_option = None
email_subject = None
html_message = ''
show_email_tab = False show_email_tab = False
problems = [] problems = []
plots = [] plots = []
...@@ -172,23 +146,6 @@ def instructor_dashboard(request, course_id): ...@@ -172,23 +146,6 @@ def instructor_dashboard(request, course_id):
writer.writerow(encoded_row) writer.writerow(encoded_row)
return response 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 # process actions from form POST
action = request.POST.get('action', '') action = request.POST.get('action', '')
use_offline = request.POST.get('use_offline_grades', False) use_offline = request.POST.get('use_offline_grades', False)
...@@ -227,12 +184,6 @@ def instructor_dashboard(request, course_id): ...@@ -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()) 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") 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: elif 'Dump all RAW grades' in action:
log.debug(action) log.debug(action)
datatable = get_student_grade_summary_data(request, course, get_grades=True, datatable = get_student_grade_summary_data(request, course, get_grades=True,
...@@ -240,11 +191,6 @@ def instructor_dashboard(request, course_id): ...@@ -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) 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") 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: elif 'Download CSV of all RAW grades' in action:
track.views.server_track(request, "dump-grades-csv-raw", {}, page="idashboard") track.views.server_track(request, "dump-grades-csv-raw", {}, page="idashboard")
return return_csv('grades_{0}_raw.csv'.format(course_key.to_deprecated_string()), return return_csv('grades_{0}_raw.csv'.format(course_key.to_deprecated_string()),
...@@ -254,279 +200,6 @@ def instructor_dashboard(request, course_id): ...@@ -254,279 +200,6 @@ def instructor_dashboard(request, course_id):
track.views.server_track(request, "dump-answer-dist-csv", {}, page="idashboard") 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)) 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 # export grades to remote gradebook
...@@ -551,8 +224,9 @@ def instructor_dashboard(request, course_id): ...@@ -551,8 +224,9 @@ def instructor_dashboard(request, course_id):
datatable = {'header': ['Student email', 'Match?']} datatable = {'header': ['Student email', 'Match?']}
rg_students = [x['email'] for x in rg_stud_data['retdata']] rg_students = [x['email'] for x in rg_stud_data['retdata']]
def domatch(x): def domatch(student):
return 'yes' if x.email in rg_students else 'No' """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['data'] = [[x.email, domatch(x)] for x in stud_data['students']]
datatable['title'] = action datatable['title'] = action
...@@ -598,66 +272,8 @@ def instructor_dashboard(request, course_id): ...@@ -598,66 +272,8 @@ def instructor_dashboard(request, course_id):
msg += msg2 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 # 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: elif 'Download CSV of all responses to problem' in action:
problem_to_dump = request.POST.get('problem_to_dump', '') problem_to_dump = request.POST.get('problem_to_dump', '')
...@@ -684,118 +300,6 @@ def instructor_dashboard(request, course_id): ...@@ -684,118 +300,6 @@ def instructor_dashboard(request, course_id):
datatable['title'] = _('Student state for problem {problem}').format(problem=problem_to_dump) 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) 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 # enrollment
...@@ -843,55 +347,6 @@ def instructor_dashboard(request, course_id): ...@@ -843,55 +347,6 @@ def instructor_dashboard(request, course_id):
datatable = ret['datatable'] 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 # psychometrics
elif action == 'Generate Histogram and IRT Plot': elif action == 'Generate Histogram and IRT Plot':
...@@ -933,7 +388,7 @@ def instructor_dashboard(request, course_id): ...@@ -933,7 +388,7 @@ def instructor_dashboard(request, course_id):
analytics_results = {} analytics_results = {}
if idash_mode == 'Analytics': if idash_mode == 'Analytics':
DASHBOARD_ANALYTICS = [ dashboard_analytics = [
# "StudentsAttemptedProblems", # num students who tried given problem # "StudentsAttemptedProblems", # num students who tried given problem
"StudentsDailyActivity", # active students by day "StudentsDailyActivity", # active students by day
"StudentsDropoffPerDay", # active students dropoff by day "StudentsDropoffPerDay", # active students dropoff by day
...@@ -942,7 +397,7 @@ def instructor_dashboard(request, course_id): ...@@ -942,7 +397,7 @@ def instructor_dashboard(request, course_id):
"ProblemGradeDistribution", # foreach problem, grade distribution "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) analytics_results[analytic_name] = get_analytics_result(analytic_name)
#---------------------------------------- #----------------------------------------
...@@ -975,27 +430,6 @@ def instructor_dashboard(request, course_id): ...@@ -975,27 +430,6 @@ def instructor_dashboard(request, course_id):
if is_studio_course: if is_studio_course:
studio_url = get_cms_course_link(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): if bulk_email_is_enabled_for_course(course_key):
show_email_tab = True show_email_tab = True
...@@ -1025,10 +459,6 @@ def instructor_dashboard(request, course_id): ...@@ -1025,10 +459,6 @@ def instructor_dashboard(request, course_id):
'modeflag': {idash_mode: 'selectedmode'}, 'modeflag': {idash_mode: 'selectedmode'},
'studio_url': studio_url, '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 'show_email_tab': show_email_tab, # email
'problems': problems, # psychometrics 'problems': problems, # psychometrics
...@@ -1036,7 +466,6 @@ def instructor_dashboard(request, course_id): ...@@ -1036,7 +466,6 @@ def instructor_dashboard(request, course_id):
'course_errors': modulestore().get_course_errors(course.id), 'course_errors': modulestore().get_course_errors(course.id),
'instructor_tasks': instructor_tasks, 'instructor_tasks': instructor_tasks,
'offline_grade_log': offline_grades_available(course_key), '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, 'analytics_results': analytics_results,
'disable_buttons': disable_buttons, 'disable_buttons': disable_buttons,
...@@ -1045,38 +474,38 @@ def instructor_dashboard(request, course_id): ...@@ -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()}) 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): def _do_remote_gradebook(user, course, action, args=None, files=None):
''' '''
Perform remote gradebook action. Returns msg, datatable. Perform remote gradebook action. Returns msg, datatable.
''' '''
rg = course.remote_gradebook rgb = course.remote_gradebook
if not rg: if not rgb:
msg = _("No remote gradebook defined in course metadata") msg = _("No remote gradebook defined in course metadata")
return msg, {} return msg, {}
rgurl = settings.FEATURES.get('REMOTE_GRADEBOOK_URL', '') rgburl = settings.FEATURES.get('REMOTE_GRADEBOOK_URL', '')
if not rgurl: if not rgburl:
msg = _("No remote gradebook url defined in settings.FEATURES") msg = _("No remote gradebook url defined in settings.FEATURES")
return msg, {} return msg, {}
rgname = rg.get('name', '') rgbname = rgb.get('name', '')
if not rgname: if not rgbname:
msg = _("No gradebook name defined in course remote_gradebook metadata") msg = _("No gradebook name defined in course remote_gradebook metadata")
return msg, {} return msg, {}
if args is None: if args is None:
args = {} args = {}
data = dict(submit=action, gradebook=rgname, user=user.email) data = dict(submit=action, gradebook=rgbname, user=user.email)
data.update(args) data.update(args)
try: 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) retdict = json.loads(resp.content)
except Exception as err: # pylint: disable=broad-except 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 += _("Error: {err}").format(err=err)
msg += "<br/>resp={resp}".format(resp=resp.content) msg += "<br/>resp={resp}".format(resp=resp.content)
msg += "<br/>data={data}".format(data=data) msg += "<br/>data={data}".format(data=data)
...@@ -1096,80 +525,6 @@ def _do_remote_gradebook(user, course, action, args=None, files=None): ...@@ -1096,80 +525,6 @@ def _do_remote_gradebook(user, course, action, args=None, files=None):
return msg, datatable 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): def _role_members_table(role, title, course_key):
""" """
Return a data table of usernames and names of users in group_name. 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 ...@@ -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] datarow = [student.id, student.username, student.profile.name, student.email]
try: try:
datarow.append(student.externalauthmap.external_email) datarow.append(student.externalauthmap.external_email)
except: # ExternalAuthMap.DoesNotExist except Exception: # pylint: disable=broad-except
datarow.append('') datarow.append('')
if get_grades: if get_grades:
...@@ -1448,12 +803,12 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal ...@@ -1448,12 +803,12 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal
if overload: # delete all but staff if overload: # delete all but staff
todelete = CourseEnrollment.objects.filter(course_id=course_key) todelete = CourseEnrollment.objects.filter(course_id=course_key)
for ce in todelete: for enrollee in todelete:
if not has_access(ce.user, 'staff', course) and ce.user.email.lower() not in new_students_lc: if not has_access(enrollee.user, 'staff', course) and enrollee.user.email.lower() not in new_students_lc:
status[ce.user.email] = 'deleted' status[enrollee.user.email] = 'deleted'
ce.deactivate() enrollee.deactivate()
else: else:
status[ce.user.email] = 'is staff' status[enrollee.user.email] = 'is staff'
ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_key) ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_key)
for cea in ceaset: for cea in ceaset:
status[cea.email] = 'removed from pending enrollment list' status[cea.email] = 'removed from pending enrollment list'
...@@ -1487,7 +842,7 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal ...@@ -1487,7 +842,7 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal
) )
# Composition of email # Composition of email
d = { email_data = {
'site_name': stripped_site_name, 'site_name': stripped_site_name,
'registration_url': registration_url, 'registration_url': registration_url,
'course': course, 'course': course,
...@@ -1502,11 +857,11 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal ...@@ -1502,11 +857,11 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal
user = User.objects.get(email=student) user = User.objects.get(email=student)
except User.DoesNotExist: 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) 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 # 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 # Will be 0 or 1 records as there is a unique key on email + course_id
if cea: if cea:
cea[0].auto_enroll = auto_enroll cea[0].auto_enroll = auto_enroll
cea[0].save() cea[0].save()
...@@ -1514,7 +869,7 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal ...@@ -1514,7 +869,7 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal
+ ('on' if auto_enroll else 'off') + ('on' if auto_enroll else 'off')
continue 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 = CourseEnrollmentAllowed(email=student, course_id=course_key, auto_enroll=auto_enroll)
cea.save() cea.save()
...@@ -1523,9 +878,9 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal ...@@ -1523,9 +878,9 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal
if email_students: if email_students:
# User is allowed to enroll but has not signed up yet # User is allowed to enroll but has not signed up yet
d['email_address'] = student email_data['email_address'] = student
d['message'] = 'allowed_enroll' email_data['message'] = 'allowed_enroll'
send_mail_ret = send_mail_to_student(student, d) send_mail_ret = send_mail_to_student(student, email_data)
status[student] += (', email sent' if send_mail_ret else '') status[student] += (', email sent' if send_mail_ret else '')
continue continue
...@@ -1541,13 +896,13 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal ...@@ -1541,13 +896,13 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal
if email_students: if email_students:
# User enrolled for first time, populate dict with user specific info # User enrolled for first time, populate dict with user specific info
d['email_address'] = student email_data['email_address'] = student
d['full_name'] = user.profile.name email_data['full_name'] = user.profile.name
d['message'] = 'enrolled_enroll' email_data['message'] = 'enrolled_enroll'
send_mail_ret = send_mail_to_student(student, d) send_mail_ret = send_mail_to_student(student, email_data)
status[student] += (', email sent' if send_mail_ret else '') status[student] += (', email sent' if send_mail_ret else '')
except: except Exception: # pylint: disable=broad-except
status[student] = 'rejected' status[student] = 'rejected'
datatable = {'header': ['StudentEmail', 'action']} datatable = {'header': ['StudentEmail', 'action']}
...@@ -1582,15 +937,17 @@ def _do_unenroll_students(course_key, students, email_students=False): ...@@ -1582,15 +937,17 @@ def _do_unenroll_students(course_key, students, email_students=False):
) )
if email_students: if email_students:
course = modulestore().get_course(course_key) course = modulestore().get_course(course_key)
#Composition of email # Composition of email
d = {'site_name': stripped_site_name, data = {
'course': course} 'site_name': stripped_site_name,
'course': course
}
for student in old_students: for student in old_students:
isok = False isok = False
cea = CourseEnrollmentAllowed.objects.filter(course_id=course_key, email=student) 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: if cea:
cea[0].delete() cea[0].delete()
status[student] = "un-enrolled" status[student] = "un-enrolled"
...@@ -1601,25 +958,25 @@ def _do_unenroll_students(course_key, students, email_students=False): ...@@ -1601,25 +958,25 @@ def _do_unenroll_students(course_key, students, email_students=False):
except User.DoesNotExist: except User.DoesNotExist:
if isok and email_students: if isok and email_students:
#User was allowed to join but had not signed up yet # User was allowed to join but had not signed up yet
d['email_address'] = student data['email_address'] = student
d['message'] = 'allowed_unenroll' data['message'] = 'allowed_unenroll'
send_mail_ret = send_mail_to_student(student, d) send_mail_ret = send_mail_to_student(student, data)
status[student] += (', email sent' if send_mail_ret else '') status[student] += (', email sent' if send_mail_ret else '')
continue 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): if CourseEnrollment.is_enrolled(user, course_key):
try: try:
CourseEnrollment.unenroll(user, course_key) CourseEnrollment.unenroll(user, course_key)
status[student] = "un-enrolled" status[student] = "un-enrolled"
if email_students: if email_students:
#User was enrolled # User was enrolled
d['email_address'] = student data['email_address'] = student
d['full_name'] = user.profile.name data['full_name'] = user.profile.name
d['message'] = 'enrolled_unenroll' data['message'] = 'enrolled_unenroll'
send_mail_ret = send_mail_to_student(student, d) send_mail_ret = send_mail_to_student(student, data)
status[student] += (', email sent' if send_mail_ret else '') status[student] += (', email sent' if send_mail_ret else '')
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
...@@ -1630,8 +987,7 @@ def _do_unenroll_students(course_key, students, email_students=False): ...@@ -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['data'] = [[x, status[x]] for x in sorted(status)]
datatable['title'] = _('Un-enrollment of students') datatable['title'] = _('Un-enrollment of students')
data = dict(datatable=datatable) return dict(datatable=datatable)
return data
def send_mail_to_student(student, param_dict): def send_mail_to_student(student, param_dict):
...@@ -1728,15 +1084,15 @@ def get_answers_distribution(request, course_key): ...@@ -1728,15 +1084,15 @@ def get_answers_distribution(request, course_key):
dist = grades.answer_distributions(course.id) dist = grades.answer_distributions(course.id)
d = {} dist = {}
d['header'] = ['url_name', 'display name', 'answer id', 'answer', 'count'] dist['header'] = ['url_name', 'display name', 'answer id', 'answer', 'count']
d['data'] = [ dist['data'] = [
[url_name, display_name, answer_id, a, answers[a]] [url_name, display_name, answer_id, a, answers[a]]
for (url_name, display_name, answer_id), answers in sorted(dist.items()) for (url_name, display_name, answer_id), answers in sorted(dist.items())
for a in answers for a in answers
] ]
return d return dist
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -1758,8 +1114,8 @@ def compute_course_stats(course): ...@@ -1758,8 +1114,8 @@ def compute_course_stats(course):
children = module.get_children() children = module.get_children()
category = module.__class__.__name__ # HtmlDescriptor, CapaDescriptor, ... category = module.__class__.__name__ # HtmlDescriptor, CapaDescriptor, ...
counts[category] += 1 counts[category] += 1
for c in children: for child in children:
walk(c) walk(child)
walk(course) walk(course)
stats = dict(counts) # number of each kind of module stats = dict(counts) # number of each kind of module
......
...@@ -315,6 +315,11 @@ mark { ...@@ -315,6 +315,11 @@ mark {
display: none; display: none;
} }
// UI - is deprecated
.is-deprecated {
@extend %ui-deprecated;
}
// UI - semantically hide text // UI - semantically hide text
.sr { .sr {
@extend %text-sr; @extend %text-sr;
......
...@@ -113,3 +113,10 @@ ...@@ -113,3 +113,10 @@
padding: 0; padding: 0;
} }
} }
%ui-deprecated {
@extend %t-weight4;
background: tint($warning-color, 85%);
padding: ($baseline/5) ($baseline/2);
color: shade($warning-color, 45%);
}
<%! 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>
<!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) ...@@ -136,7 +136,7 @@ function goto( mode)
<a class="instructor-info-action beta-button" href="${ standard_dashboard_url }">${_("Back To Instructor Dashboard")}</a> <a class="instructor-info-action beta-button" href="${ standard_dashboard_url }">${_("Back To Instructor Dashboard")}</a>
</div> </div>
<h1>${_("Instructor Dashboard")}</h1> <h1>${_("Legacy Instructor Dashboard")}</h1>
%if settings.FEATURES.get('IS_EDX_DOMAIN', False): %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) ## Only show this banner on the edx.org website (other sites may choose to show this if they wish)
...@@ -202,19 +202,10 @@ function goto( mode) ...@@ -202,19 +202,10 @@ function goto( mode)
% endif % endif
<p> <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 ''}"> <input type="submit" name="action" value="Dump list of enrolled students" class="${'is-disabled' if disable_buttons else ''}">
</p> </p>
<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="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 ''}"> <input type="submit" name="action" value="Download CSV of all RAW grades" class="${'is-disabled' if disable_buttons else ''}">
</p> </p>
...@@ -223,7 +214,12 @@ function goto( mode) ...@@ -223,7 +214,12 @@ function goto( mode)
%if not settings.FEATURES.get('ENABLE_ASYNC_ANSWER_DISTRIBUTION'): %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 ''}"> <input type="submit" name="action" value="Download CSV of answer distributions" class="${'is-disabled' if disable_buttons else ''}">
%endif %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> </p>
<hr width="40%" style="align:left"> <hr width="40%" style="align:left">
...@@ -263,67 +259,13 @@ function goto( mode) ...@@ -263,67 +259,13 @@ function goto( mode)
%if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'): %if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
<H2>${_("Course-specific grade adjustment")}</h2> <H2>${_("Course-specific grade adjustment")}</h2>
<p> <p class="is-deprecated">${_("To perform these actions, visit the Student Admin section of the Instructor Dashboard.")}</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>
<hr width="40%" style="align:left">
%endif %endif
<h2>${_("Student-specific grade inspection and adjustment")}</h2> <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> <p class="is-deprecated">${_("To perform these actions, visit the Student Admin section of the Instructor Dashboard.")}</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
%endif %endif
...@@ -351,25 +293,8 @@ function goto( mode) ...@@ -351,25 +293,8 @@ function goto( mode)
##----------------------------------------------------------------------------- ##-----------------------------------------------------------------------------
%if modeflag.get('Admin'): %if modeflag.get('Admin'):
%if instructor_access: %if instructor_access or admin_access:
<hr width="40%" style="align:left"> <p class="is-deprecated">${_("To add or remove course staff or instructors, visit the Membership section of the Instructor Dashboard.")}</p>
<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">
%endif %endif
%if settings.FEATURES['ENABLE_MANUAL_GIT_RELOAD'] and admin_access: %if settings.FEATURES['ENABLE_MANUAL_GIT_RELOAD'] and admin_access:
...@@ -381,38 +306,7 @@ function goto( mode) ...@@ -381,38 +306,7 @@ function goto( mode)
##----------------------------------------------------------------------------- ##-----------------------------------------------------------------------------
%if modeflag.get('Forum Admin'): %if modeflag.get('Forum Admin'):
%if instructor_access: <p class="is-deprecated">${_("To manage forum roles, visit the Membership section of the Instructor Dashboard.")}</p>
<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>
%endif %endif
##----------------------------------------------------------------------------- ##-----------------------------------------------------------------------------
...@@ -463,15 +357,13 @@ function goto( mode) ...@@ -463,15 +357,13 @@ function goto( mode)
%if modeflag.get('Data'): %if modeflag.get('Data'):
<hr width="40%" style="align:left"> <hr width="40%" style="align:left">
<p>
<input type="submit" name="action" value="Download CSV of all student profile data">
</p>
<p> ${_("Problem urlname:")} <p> ${_("Problem urlname:")}
<input type="text" name="problem_to_dump" size="40"> <input type="text" name="problem_to_dump" size="40">
<input type="submit" name="action" value="Download CSV of all responses to problem"> <input type="submit" name="action" value="Download CSV of all responses to problem">
</p> </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> </p>
<hr width="40%" style="align:left"> <hr width="40%" style="align:left">
%endif %endif
...@@ -480,103 +372,18 @@ function goto( mode) ...@@ -480,103 +372,18 @@ function goto( mode)
%if modeflag.get('Manage Groups'): %if modeflag.get('Manage Groups'):
%if instructor_access: %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: %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 %endif
%endif %endif
##----------------------------------------------------------------------------- ##-----------------------------------------------------------------------------
%if modeflag.get('Email'): %if modeflag.get('Email'):
%if email_msg: <p class="is-deprecated">${_("To send email, visit the Email section of the Instructor Dashboard.")}</p>
<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>
%endif %endif
</form> </form>
...@@ -848,8 +655,7 @@ function goto( mode) ...@@ -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/>
<br/> <br/>
<p> <p>
...@@ -870,6 +676,13 @@ function goto( mode) ...@@ -870,6 +676,13 @@ function goto( mode)
%endfor %endfor
</table> </table>
</p> </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 %endif
##----------------------------------------------------------------------------- ##-----------------------------------------------------------------------------
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment