""" Instructor Views """ from collections import defaultdict import csv import json import logging from markupsafe import escape import os import re import requests from requests.status_codes import codes from collections import OrderedDict from StringIO import StringIO from django.conf import settings from django.contrib.auth.models import User, Group 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 import xmodule.graders as xmgraders from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError from courseware import grades from courseware.access import (has_access, get_access_group_name, course_beta_test_group_name) from courseware.courses import get_course_with_access 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_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) from instructor_task.views import get_task_completion_info from mitxmako.shortcuts import render_to_response from psychometrics import psychoanalyze from student.models import CourseEnrollment, CourseEnrollmentAllowed import track.views from mitxmako.shortcuts import render_to_string log = logging.getLogger(__name__) # internal commands for managing forum roles: FORUM_ROLE_ADD = 'add' FORUM_ROLE_REMOVE = 'remove' def split_by_comma_and_whitespace(s): """ Return string s, split by , or whitespace """ return re.split(r'[\s,]', s) @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 = '' 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') # 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', CourseEnrollment.objects.filter(course_id=course_id).count()]] data += [['Date', timezone.now().isoformat()]] data += compute_course_stats(course).items() if request.user.is_staff: for field in course.fields: if getattr(field.scope, 'user', False): continue data.append([field.name, json.dumps(field.read_json(course))]) for namespace in course.namespaces: for field in getattr(course, namespace).fields: if getattr(field.scope, 'user', False): continue data.append(["{}.{}".format(namespace, field.name), json.dumps(field.read_json(course))]) datatable['data'] = data return datatable def return_csv(fn, datatable, fp=None): """Outputs a CSV file from the contents of a datatable.""" if fp is None: response = HttpResponse(mimetype='text/csv') response['Content-Disposition'] = 'attachment; filename={0}'.format(fn) else: response = fp 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_staff_group(course): """Get or create the staff access group""" return get_group(course, 'staff') def get_instructor_group(course): """Get or create the instructor access group""" return get_group(course, 'instructor') def get_group(course, groupname): """Get or create an access group""" grpname = get_access_group_name(course, groupname) try: group = Group.objects.get(name=grpname) except Group.DoesNotExist: group = Group(name=grpname) # create the group group.save() return group def get_beta_group(course): """ Get the group for beta testers of course. """ # Not using get_group because there is no access control action called # 'beta', so adding it to get_access_group_name doesn't really make # sense. name = course_beta_test_group_name(course.location) (group, _) = Group.objects.get_or_create(name=name) return group 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. """ # 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""" 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 = getattr(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 = getattr(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 e: msg += 'Failed to create a background task for rescoring "{0}": problem not found.'.format(problem_url) except Exception as e: log.error("Encountered exception from rescore: {0}".format(e)) msg += 'Failed to create a background task for rescoring "{0}": {1}.'.format(problem_url, e.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 e: log.error('Failure to reset: unknown problem "{0}"'.format(e)) msg += 'Failed to create a background task for resetting "{0}": problem not found.'.format(problem_url) except Exception as e: log.error("Encountered exception from reset: {0}".format(e)) msg += 'Failed to create a background task for resetting "{0}": {1}.'.format(problem_url, e.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: msg += "Couldn't find module with that urlname. " 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 %s!" % module_state_key event = {"problem": problem_url, "student": unique_student_identifier, "course": course_id} track.views.server_track(request, "delete-student-module-state", event, page="idashboard") except: msg += "Failed to delete module state for %s/%s" % (unique_student_identifier, problem_urlname) 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": student, "problem": student_module.module_state_key, "instructor": request.user, "course": course_id} track.views.server_track(request, "reset-student-attempts", event, page="idashboard") msg += "Module state successfully reset!" except: msg += "Couldn't reset module state. " 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 e: log.exception("Encountered exception from rescore: {0}") msg += 'Failed to create a background task for rescoring "{0}": {1}.'.format(module_state_key, e.message) 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]} datatable['data'] = [[x.email, x.grades[aidx]] for x in allgrades['students']] 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: fp = StringIO() return_csv('', datatable, fp=fp) fp.seek(0) files = {'datafile': fp} msg2, _ = _do_remote_gradebook(request.user, course, 'post-grades', files=files) msg += msg2 #---------------------------------------- # Admin elif 'List course staff' in action: group = get_staff_group(course) msg += 'Staff group = {0}'.format(group.name) datatable = _group_members_table(group, "List of Staff", course_id) track.views.server_track(request, "list-staff", {}, page="idashboard") elif 'List course instructors' in action and request.user.is_staff: group = get_instructor_group(course) msg += 'Instructor group = {0}'.format(group.name) log.debug('instructor grp={0}'.format(group.name)) uset = group.user_set.all() datatable = {'header': ['Username', 'Full name']} datatable['data'] = [[x.username, x.profile.name] for x in uset] datatable['title'] = 'List of Instructors in course {0}'.format(course_id) track.views.server_track(request, "list-instructors", {}, page="idashboard") elif action == 'Add course staff': uname = request.POST['staffuser'] group = get_staff_group(course) msg += add_user_to_group(request, uname, group, 'staff', 'staff') elif action == 'Add instructor' and request.user.is_staff: uname = request.POST['instructor'] try: user = User.objects.get(username=uname) except User.DoesNotExist: msg += 'Error: unknown username "{0}"'.format(uname) user = None if user is not None: group = get_instructor_group(course) msg += 'Added {0} to instructor group = {1}'.format(user, group.name) log.debug('staffgrp={0}'.format(group.name)) user.groups.add(group) track.views.server_track(request, "add-instructor", {"instructor": unicode(user)}, page="idashboard") elif action == 'Remove course staff': uname = request.POST['staffuser'] group = get_staff_group(course) msg += remove_user_from_group(request, uname, group, 'staff', 'staff') elif action == 'Remove instructor' and request.user.is_staff: uname = request.POST['instructor'] try: user = User.objects.get(username=uname) except User.DoesNotExist: msg += 'Error: unknown username "{0}"'.format(uname) user = None if user is not None: group = get_instructor_group(course) msg += 'Removed {0} from instructor group = {1}'.format(user, group.name) log.debug('instructorgrp={0}'.format(group.name)) user.groups.remove(group) track.views.server_track(request, "remove-instructor", {"instructor": unicode(user)}, page="idashboard") #---------------------------------------- # DataDump elif 'Download CSV of all student profile data' in action: enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).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) #---------------------------------------- # Group management elif 'List beta testers' in action: group = get_beta_group(course) msg += 'Beta test group = {0}'.format(group.name) datatable = _group_members_table(group, "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)) group = get_beta_group(course) for username_or_email in split_by_comma_and_whitespace(users): msg += "
{0}
".format( add_user_to_group(request, username_or_email, group, 'beta testers', 'beta-tester')) elif action == 'Remove beta testers': users = request.POST['betausers'] group = get_beta_group(course) for username_or_email in split_by_comma_and_whitespace(users): msg += "{0}
".format( remove_user_from_group(request, username_or_email, group, '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': 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) 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'] #---------------------------------------- # psychometrics elif action == 'Generate Histogram and IRT Plot': problem = request.POST['Problem'] nmsg, plots = psychoanalyze.generate_plots_for_problem(problem) msg += nmsg track.views.server_track(request, "psychometrics-histogram-generation", {"problem": unicode(problem)}, page="idashboard") if idash_mode == 'Psychometrics': problems = psychoanalyze.problems_with_psychometric_data(course_id) #---------------------------------------- # analytics def get_analytics_result(analytics_name): """Return data for an Analytic piece, or None if it doesn't exist. It logs and swallows errors. """ url = settings.ANALYTICS_SERVER_URL + \ "get?aname={}&course_id={}&apikey={}".format(analytics_name, course_id, settings.ANALYTICS_API_KEY) try: res = requests.get(url) except Exception: log.exception("Error trying to access analytics at %s", url) return None if res.status_code == codes.OK: # WARNING: do not use req.json because the preloaded json doesn't # preserve the order of the original record (hence OrderedDict). return json.loads(res.content, object_pairs_hook=OrderedDict) else: log.error("Error fetching %s, code: %s, msg: %s", url, res.status_code, res.content) return None analytics_results = {} if idash_mode == 'Analytics': DASHBOARD_ANALYTICS = [ # "StudentsAttemptedProblems", # num students who tried given problem "StudentsDailyActivity", # active students by day "StudentsDropoffPerDay", # active students dropoff by day # "OverallGradeDistribution", # overall point distribution for course "StudentsActive", # num students active in time period (default = 1wk) "StudentsEnrolled", # num students enrolled # "StudentsPerProblemCorrect", # foreach problem, num students correct "ProblemGradeDistribution", # foreach problem, grade distribution ] for analytic_name in DASHBOARD_ANALYTICS: analytics_results[analytic_name] = get_analytics_result(analytic_name) #---------------------------------------- # offline grades? if use_offline: msg += "%s' % retdict['msg'].replace('\n', '
%s' % msg.replace('<', '<') return msg def get_background_task_table(course_id, problem_url, student=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) 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 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 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