""" Instructor Views """ import csv import json import logging import os import re import requests from collections import defaultdict, OrderedDict from functools import partial 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 import xmodule.graders as xmgraders from xmodule.modulestore import MONGO_MODULESTORE_TYPE from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.html_module import HtmlDescriptor 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 courseware.roles import CourseStaffRole, CourseInstructorRole, CourseBetaTesterRole 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 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 mitxmako.shortcuts import render_to_response, render_to_string from psychometrics import psychoanalyze from student.models import CourseEnrollment, CourseEnrollmentAllowed, unique_id_for_user from student.views import course_from_id import track.views from xblock.field_data import DictFieldData from xblock.fields import ScopeIds from django.utils.translation import ugettext as _u from lms.lib.xblock.runtime import handler_prefix 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 = get_course_with_access(request.user, course_id, 'staff', depth=None) instructor_access = has_access(request.user, course, 'instructor') # an instructor can manage staff lists forum_admin_access = has_forum_access(request.user, course_id, 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', '') if idash_mode: request.session['idash_mode'] = idash_mode else: idash_mode = request.session.get('idash_mode', 'Grades') enrollment_number = CourseEnrollment.objects.filter(course_id=course_id, is_active=1).count() # assemble some course statistics for output to instructor def get_course_stats_table(): datatable = { 'header': ['Statistic', 'Value'], 'title': _u('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))]) 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'] = 'attachment; filename={0}'.format(func) else: response = file_pointer writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) writer.writerow(datatable['header']) for datarow in datatable['data']: encoded_row = [unicode(s).encode('utf-8') for s in datarow] writer.writerow(encoded_row) return response def get_module_url(urlname): """ Construct full URL for a module from its urlname. Form is either urlname or modulename/urlname. If no modulename is provided, "problem" is assumed. """ # remove whitespace urlname = strip_if_string(urlname) # tolerate an XML suffix in the urlname if urlname[-4:] == ".xml": urlname = urlname[:-4] # implement default if '/' not in urlname: urlname = "problem/" + urlname # complete the url using information about the current course: (org, course_name, _) = course_id.split("/") return "i4x://" + org + "/" + course_name + "/" + urlname 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 += "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.MITX_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_id, 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_item_errors(course.location) 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, course_id, get_grades=False, use_offline=use_offline) datatable['title'] = 'List of students enrolled in {0}'.format(course_id) 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, course_id, get_grades=True, use_offline=use_offline) datatable['title'] = 'Summary Grades of students enrolled in {0}'.format(course_id) 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, course_id, get_grades=True, get_raw_scores=True, use_offline=use_offline) datatable['title'] = 'Raw Grades of students enrolled in {0}'.format(course_id) 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_id), get_student_grade_summary_data(request, course, course_id, 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_id), get_student_grade_summary_data(request, course, course_id, 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_id), get_answers_distribution(request, course_id)) 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_urlname = request.POST.get('problem_for_all_students', '') problem_url = get_module_url(problem_urlname) try: instructor_task = submit_rescore_problem_for_all_students(request, course_id, problem_url) if instructor_task is None: msg += 'Failed to create a background task for rescoring "{0}".'.format(problem_url) else: track.views.server_track(request, "rescore-all-submissions", {"problem": problem_url, "course": course_id}, page="idashboard") except ItemNotFoundError as err: msg += 'Failed to create a background task for rescoring "{0}": problem not found.'.format(problem_url) except Exception as err: log.error("Encountered exception from rescore: {0}".format(err)) msg += 'Failed to create a background task for rescoring "{0}": {1}.'.format(problem_url, err.message) elif "Reset ALL students' attempts" in action: problem_urlname = request.POST.get('problem_for_all_students', '') problem_url = get_module_url(problem_urlname) try: instructor_task = submit_reset_problem_attempts_for_all_students(request, course_id, problem_url) if instructor_task is None: msg += 'Failed to create a background task for resetting "{0}".'.format(problem_url) else: track.views.server_track(request, "reset-all-attempts", {"problem": problem_url, "course": course_id}, page="idashboard") except ItemNotFoundError as err: log.error('Failure to reset: unknown problem "{0}"'.format(err)) msg += 'Failed to create a background task for resetting "{0}": problem not found.'.format(problem_url) except Exception as err: log.error("Encountered exception from reset: {0}".format(err)) msg += 'Failed to create a background task for resetting "{0}": {1}.'.format(problem_url, 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_urlname = request.POST.get('problem_for_student', '') problem_url = get_module_url(problem_urlname) message, datatable = get_background_task_table(course_id, problem_url, student) msg += message elif "Show Background Task History" in action: problem_urlname = request.POST.get('problem_for_all_students', '') problem_url = get_module_url(problem_urlname) message, datatable = get_background_task_table(course_id, problem_url) 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_urlname = request.POST.get('problem_for_student', '') module_state_key = get_module_url(problem_urlname) # 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: # find the module in question try: student_module = StudentModule.objects.get( student_id=student.id, course_id=course_id, module_state_key=module_state_key ) msg += "Found module. " except StudentModule.DoesNotExist as err: error_msg = "Couldn't find module with that urlname: {0}. ".format( problem_urlname ) msg += "" + error_msg + "({0}) ".format(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 += "Deleted student module state for {0}!".format(module_state_key) event = { "problem": module_state_key, "student": unique_student_identifier, "course": course_id } track.views.server_track( request, "delete-student-module-state", event, page="idashboard" ) except Exception as err: error_msg = "Failed to delete module state for {0}/{1}. ".format( unique_student_identifier, problem_urlname ) msg += "" + error_msg + "({0}) ".format(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_id } track.views.server_track(request, "reset-student-attempts", event, page="idashboard") msg += "Module state successfully reset!" except Exception as err: error_msg = "Couldn't reset module state for {0}/{1}. ".format( unique_student_identifier, problem_urlname ) msg += "" + error_msg + "({0}) ".format(err) + "" log.exception(error_msg) else: # "Rescore student's problem submission" case try: instructor_task = submit_rescore_problem_for_student(request, course_id, module_state_key, student) if instructor_task is None: msg += 'Failed to create a background task for rescoring "{0}" for student {1}.'.format(module_state_key, unique_student_identifier) else: track.views.server_track(request, "rescore-student-submission", {"problem": module_state_key, "student": unique_student_identifier, "course": course_id}, page="idashboard") except Exception as err: msg += 'Failed to create a background task for rescoring "{0}": {1}.'.format( module_state_key, 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_id, 'student_id': student.id}) track.views.server_track(request, "get-student-progress-page", {"student": unicode(student), "instructor": unicode(request.user), "course": course_id}, page="idashboard") msg += " Progress page for username: {1} with email address: {2}.".format(progress_url, student.username, 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, course_id, 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, course_id, 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 += "Please enter an assignment name" else: allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline) if aname not in allgrades['assignments']: msg += "Invalid assignment name '%s'" % aname else: aidx = allgrades['assignments'].index(aname) datatable = {'header': ['External email', aname]} ddata = [] for x in allgrades['students']: # do one by one in case there is a student who has only partial grades try: ddata.append([x.email, x.grades[aidx]]) except IndexError: log.debug('No grade for assignment %s (%s) for student %s', aidx, aname, x.email) datatable['data'] = ddata datatable['title'] = 'Grades for assignment "%s"' % aname if 'Export CSV' in action: # generate and return CSV file return return_csv('grades %s.csv' % 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.location) datatable = _role_members_table(role, "List of Staff", course_id) track.views.server_track(request, "list-staff", {}, page="idashboard") elif 'List course instructors' in action and GlobalStaff().has_user(request.user): role = CourseInstructorRole(course.location) datatable = _role_members_table(role, "List of Instructors", course_id) track.views.server_track(request, "list-instructors", {}, page="idashboard") elif action == 'Add course staff': uname = request.POST['staffuser'] role = CourseStaffRole(course.location) 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.location) msg += add_user_to_role(request, uname, role, 'instructor', 'instructor') elif action == 'Remove course staff': uname = request.POST['staffuser'] role = CourseStaffRole(course.location) 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.location) 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_id, 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(u): p = u.profile return [u.username, u.email] + [getattr(p, x, '') for x in profkeys] datatable['data'] = [getdat(u) for u in enrolled_students] datatable['title'] = 'Student profile data for course %s' % course_id return return_csv('profiledata_%s.csv' % course_id, 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: (org, course_name, _) = course_id.split("/") module_state_key = "i4x://" + org + "/" + course_name + "/problem/" + problem_to_dump smdat = StudentModule.objects.filter(course_id=course_id, module_state_key=module_state_key) smdat = smdat.order_by('student') msg += "Found %d records to dump " % len(smdat) except Exception as err: msg += "Couldn't find module with that urlname. " msg += "
%s" % 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 %s' % problem_to_dump return return_csv('student_state_from_%s.csv' % problem_to_dump, datatable) elif 'Download CSV of all student anonymized IDs' in action: students = User.objects.filter( courseenrollment__course_id=course_id, ).order_by('id') datatable = {'header': ['User ID', 'Anonymized user ID']} datatable['data'] = [[s.id, unique_id_for_user(s)] for s in students] return return_csv(course_id.replace('/', '-') + '-anon-ids.csv', datatable) #---------------------------------------- # Group management elif 'List beta testers' in action: role = CourseBetaTesterRole(course.location) datatable = _role_members_table(role, "List of Beta Testers", course_id) 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.location) 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.location) 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_id, rolename, datatable) track.views.server_track(request, "list-forum-admins", {"course": course_id}, 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_id}, 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_id}, page="idashboard") elif action == 'List course forum moderators': rolename = FORUM_ROLE_MODERATOR datatable = {} msg += _list_course_forum_members(course_id, rolename, datatable) track.views.server_track(request, "list-forum-mods", {"course": course_id}, 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_id}, 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_id}, page="idashboard") elif action == 'List course forum community TAs': rolename = FORUM_ROLE_COMMUNITY_TA datatable = {} msg += _list_course_forum_members(course_id, rolename, datatable) track.views.server_track(request, "list-forum-community-TAs", {"course": course_id}, 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_id}, 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_id}, page="idashboard") #---------------------------------------- # enrollment elif action == 'List students who may enroll but may not have yet signed up': ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_id) 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')) ret = _do_enroll_students(course, course_id, students, 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_id, 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 ret = _do_enroll_students(course, course_id, students, 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") 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_id, 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_id, email.id) # pylint: disable=E1101 except Exception as err: # 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": email_msg = '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.
Your email was successfully queued for sending.
%s' % retdict['msg'].replace('\n', '
%s' % msg.replace('<', '<') return msg def get_background_task_table(course_id, 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_id, 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_id) 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_id, problem=problem_url, student=student.username) else: msg += 'Failed to find any background tasks for course "{course}" and module "{problem}".'.format(course=course_id, 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_id) elif student is not None: datatable['title'] = "{course_id} > {location} > {student}".format(course_id=course_id, location=problem_url, student=student.username) else: datatable['title'] = "{course_id} > {location}".format(course_id=course_id, 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)