""" 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 import logging import os import re import requests from collections import defaultdict, OrderedDict from markupsafe import escape from requests.status_codes import codes from StringIO import StringIO from django.conf import settings from django.contrib.auth.models import User from django.http import HttpResponse from django_future.csrf import ensure_csrf_cookie from django.views.decorators.cache import cache_control 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_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 from class_dashboard import dashboard_data 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 from opaque_keys.edx.locations import i4xEncoder log = logging.getLogger(__name__) # internal commands for managing forum roles: FORUM_ROLE_ADD = 'add' FORUM_ROLE_REMOVE = 'remove' # For determining if a shibboleth course SHIBBOLETH_DOMAIN_PREFIX = 'shib:' def split_by_comma_and_whitespace(a_str): """ Return string a_str, split by , or whitespace """ return re.split(r'[\s,]', a_str) @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) def instructor_dashboard(request, course_id): """Display the instructor dashboard for a course.""" course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access(request.user, 'staff', course_key, depth=None) instructor_access = has_access(request.user, 'instructor', course) # an instructor can manage staff lists 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 = [] datatable = {} # the instructor dashboard page is modal: grades, psychometrics, admin # keep that state in request.session (defaults to grades mode) idash_mode = request.POST.get('idash_mode', '') idash_mode_key = u'idash_mode:{0}'.format(course_id) if idash_mode: request.session[idash_mode_key] = idash_mode else: idash_mode = request.session.get(idash_mode_key, 'Grades') enrollment_number = CourseEnrollment.num_enrolled_in(course_key) # assemble some course statistics for output to instructor def get_course_stats_table(): datatable = { 'header': ['Statistic', 'Value'], 'title': _('Course Statistics At A Glance'), } data = [['# Enrolled', enrollment_number]] data += [['Date', timezone.now().isoformat()]] data += compute_course_stats(course).items() if request.user.is_staff: for field in course.fields.values(): if getattr(field.scope, 'user', False): continue data.append([ field.name, json.dumps(field.read_json(course), cls=i4xEncoder) ]) datatable['data'] = data return datatable def return_csv(func, datatable, file_pointer=None): """Outputs a CSV file from the contents of a datatable.""" if file_pointer is None: response = HttpResponse(mimetype='text/csv') response['Content-Disposition'] = (u'attachment; filename={0}'.format(func)).encode('utf-8') else: response = file_pointer writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) encoded_row = [unicode(s).encode('utf-8') for s in datatable['header']] writer.writerow(encoded_row) for datarow in datatable['data']: # 's' here may be an integer, float (eg score) or string (eg student name) encoded_row = [ # If s is already a UTF-8 string, trying to make a unicode # object out of it will fail unless we pass in an encoding to # the constructor. But we can't do that across the board, # because s is often a numeric type. So just do this. s if isinstance(s, str) else unicode(s).encode('utf-8') for s in datarow ] 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 += "{text}".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) if settings.FEATURES['ENABLE_MANUAL_GIT_RELOAD']: if 'GIT pull' in action: data_dir = course.data_dir log.debug('git pull {0}'.format(data_dir)) gdir = settings.DATA_DIR / data_dir if not os.path.exists(gdir): msg += "====> ERROR in gitreload - no such directory {0}".format(gdir) else: cmd = "cd {0}; git reset --hard HEAD; git clean -f -d; git pull origin; chmod g+w course.xml".format(gdir) msg += "git pull on {0}:
".format(data_dir) msg += "
{0}".format(escape(os.popen(cmd).read()))
track.views.server_track(request, "git-pull", {"directory": data_dir}, page="idashboard")
if 'Reload course' in action:
log.debug('reloading {0} ({1})'.format(course_key, course))
try:
data_dir = course.data_dir
modulestore().try_load_course(data_dir)
msg += "Course reloaded from {0}
".format(data_dir) track.views.server_track(request, "reload", {"directory": data_dir}, page="idashboard") course_errors = modulestore().get_course_errors(course.id) msg += '{1}".format(cmsg, escape(cerr))
msg += 'Error: {0}
'.format(escape(err)) if action == 'Dump list of enrolled students' or action == 'List enrolled students': log.debug(action) datatable = get_student_grade_summary_data(request, course, get_grades=False, use_offline=use_offline) 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, get_raw_scores=True, use_offline=use_offline) 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()), get_student_grade_summary_data(request, course, get_raw_scores=True, use_offline=use_offline)) elif 'Download CSV of answer distributions' in action: 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 += '{text}'.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 += '{text}'.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 += '{text}'.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 += '{text}'.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 += '{text}'.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 += '{text}'.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 += '{text}'.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 += '{text}'.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 += '{text}'.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 += "{err} ".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 += "{err_msg} ({err})".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 += "{text}".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 += "{err_msg} ({err})".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 += "{text}".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 += "{err_msg} ({err})".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 += '{text}'.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 += '{text}'.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 += "{text}.".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 elif action == 'List assignments available in remote gradebook': msg2, datatable = _do_remote_gradebook(request.user, course, 'get-assignments') msg += msg2 elif action == 'List assignments available for this course': log.debug(action) allgrades = get_student_grade_summary_data(request, course, get_grades=True, use_offline=use_offline) assignments = [[x] for x in allgrades['assignments']] datatable = {'header': [_('Assignment Name')]} datatable['data'] = assignments datatable['title'] = action msg += 'assignments=%s' % assignments elif action == 'List enrolled students matching remote gradebook': stud_data = get_student_grade_summary_data(request, course, get_grades=False, use_offline=use_offline) msg2, rg_stud_data = _do_remote_gradebook(request.user, course, 'get-membership') 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' datatable['data'] = [[x.email, domatch(x)] for x in stud_data['students']] datatable['title'] = action elif action in ['Display grades for assignment', 'Export grades for assignment to remote gradebook', 'Export CSV file of grades for assignment']: log.debug(action) datatable = {} aname = request.POST.get('assignment_name', '') if not aname: msg += "{text}".format(text=_("Please enter an assignment name")) else: allgrades = get_student_grade_summary_data(request, course, get_grades=True, use_offline=use_offline) if aname not in allgrades['assignments']: msg += "{text}".format( text=_("Invalid assignment name '{name}'").format(name=aname) ) else: aidx = allgrades['assignments'].index(aname) datatable = {'header': [_('External email'), aname]} ddata = [] for student in allgrades['students']: # do one by one in case there is a student who has only partial grades try: ddata.append([student.email, student.grades[aidx]]) except IndexError: log.debug('No grade for assignment {idx} ({name}) for student {email}'.format( idx=aidx, name=aname, email=student.email) ) datatable['data'] = ddata datatable['title'] = _('Grades for assignment "{name}"').format(name=aname) if 'Export CSV' in action: # generate and return CSV file return return_csv('grades {name}.csv'.format(name=aname), datatable) elif 'remote gradebook' in action: file_pointer = StringIO() return_csv('', datatable, file_pointer=file_pointer) file_pointer.seek(0) files = {'datafile': file_pointer} 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', '') if problem_to_dump[-4:] == ".xml": problem_to_dump = problem_to_dump[:-4] try: module_state_key = course_key.make_usage_key_from_deprecated_string(problem_to_dump) smdat = StudentModule.objects.filter( course_id=course_key, module_state_key=module_state_key ) smdat = smdat.order_by('student') msg += _("Found {num} records to dump.").format(num=smdat) except Exception as err: # pylint: disable=broad-except msg += "{text}
{err}".format(
text=_("Couldn't find module with that urlname."),
err=escape(err)
)
smdat = []
if smdat:
datatable = {'header': ['username', 'state']}
datatable['data'] = [[x.student.username, x.state] for x in smdat]
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 += "{0}
".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 += "{0}
".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 elif action == 'List students who may enroll but may not have yet signed up': ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_key) datatable = {'header': ['StudentEmail']} datatable['data'] = [[x.email] for x in ceaset] datatable['title'] = action elif action == 'Enroll multiple students': is_shib_course = uses_shib(course) students = request.POST.get('multiple_students', '') auto_enroll = bool(request.POST.get('auto_enroll')) email_students = bool(request.POST.get('email_students')) secure = request.is_secure() ret = _do_enroll_students(course, course_key, students, secure=secure, auto_enroll=auto_enroll, email_students=email_students, is_shib_course=is_shib_course) datatable = ret['datatable'] elif action == 'Unenroll multiple students': students = request.POST.get('multiple_students', '') email_students = bool(request.POST.get('email_students')) ret = _do_unenroll_students(course_key, students, email_students=email_students) datatable = ret['datatable'] elif action == 'List sections available in remote gradebook': msg2, datatable = _do_remote_gradebook(request.user, course, 'get-sections') msg += msg2 elif action in ['List students in section in remote gradebook', 'Overload enrollment list using remote gradebook', 'Merge enrollment list with remote gradebook']: section = request.POST.get('gradebook_section', '') msg2, datatable = _do_remote_gradebook(request.user, course, 'get-membership', dict(section=section)) msg += msg2 if not 'List' in action: students = ','.join([x['email'] for x in datatable['retdata']]) overload = 'Overload' in action secure = request.is_secure() 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=E1101 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 += "" + error_msg + "" 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 = '{text}
{msg}'.format(msg=retdict['msg'].replace('\n', '%s' % msg.replace('<', '<') return msg def get_background_task_table(course_key, problem_url=None, student=None, task_type=None): """ Construct the "datatable" structure to represent background task history. Filters the background task history to the specified course and problem. If a student is provided, filters to only those tasks for which that student was specified. Returns a tuple of (msg, datatable), where the msg is a possible error message, and the datatable is the datatable to be used for display. """ history_entries = get_instructor_task_history(course_key, problem_url, student, task_type) datatable = {} msg = "" # first check to see if there is any history at all # (note that we don't have to check that the arguments are valid; it # just won't find any entries.) if (history_entries.count()) == 0: if problem_url is None: msg += 'Failed to find any background tasks for course "{course}".'.format( course=course_key.to_deprecated_string() ) elif student is not None: template = '' + _('Failed to find any background tasks for course "{course}", module "{problem}" and student "{student}".') + '' msg += template.format(course=course_key.to_deprecated_string(), problem=problem_url, student=student.username) else: msg += '' + _('Failed to find any background tasks for course "{course}" and module "{problem}".').format( course=course_key.to_deprecated_string(), problem=problem_url ) + '' else: datatable['header'] = ["Task Type", "Task Id", "Requester", "Submitted", "Duration (sec)", "Task State", "Task Status", "Task Output"] datatable['data'] = [] for instructor_task in history_entries: # get duration info, if known: duration_sec = 'unknown' if hasattr(instructor_task, 'task_output') and instructor_task.task_output is not None: task_output = json.loads(instructor_task.task_output) if 'duration_ms' in task_output: duration_sec = int(task_output['duration_ms'] / 1000.0) # get progress status message: success, task_message = get_task_completion_info(instructor_task) status = "Complete" if success else "Incomplete" # generate row for this task: row = [ str(instructor_task.task_type), str(instructor_task.task_id), str(instructor_task.requester), instructor_task.created.isoformat(' '), duration_sec, str(instructor_task.task_state), status, task_message ] datatable['data'].append(row) if problem_url is None: datatable['title'] = "{course_id}".format(course_id=course_key.to_deprecated_string()) elif student is not None: datatable['title'] = "{course_id} > {location} > {student}".format( course_id=course_key.to_deprecated_string(), location=problem_url, student=student.username ) else: datatable['title'] = "{course_id} > {location}".format( course_id=course_key.to_deprecated_string(), location=problem_url ) return msg, datatable def uses_shib(course): """ Used to return whether course has Shibboleth as the enrollment domain Returns a boolean indicating if Shibboleth authentication is set for this course. """ return course.enrollment_domain and course.enrollment_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX)