# ======== Instructor views ============================================================================= from collections import defaultdict import csv import itertools import json import logging import os import re import requests import urllib import json 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 mitxmako.shortcuts import render_to_response from django.core.urlresolvers import reverse 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_client.models import (Role, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA) from django_comment_client.utils import has_forum_access from psychometrics import psychoanalyze from student.models import CourseEnrollment, CourseEnrollmentAllowed from xmodule.course_module import CourseDescriptor from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem from xmodule.modulestore.search import path_to_location import track.views from .offline_gradecalc import student_grades, offline_grades_available log = logging.getLogger(__name__) template_imports = {'urllib': urllib} # internal commands for managing forum roles: FORUM_ROLE_ADD = 'add' FORUM_ROLE_REMOVE = 'remove' def split_by_comma_and_whitespace(s): 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') 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 = [] # 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') def escape(s): """escape HTML special characters in string""" return str(s).replace('<', '<').replace('>', '>') # assemble some course statistics for output to instructor datatable = {'header': ['Statistic', 'Value'], 'title': 'Course Statistics At A Glance', } data = [['# Enrolled', CourseEnrollment.objects.filter(course_id=course_id).count()]] data += compute_course_stats(course).items() if request.user.is_staff: data.append(['metadata', escape(str(course.metadata))]) datatable['data'] = data def return_csv(fn, datatable, fp=None): 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): return get_group(course, 'staff') def get_instructor_group(course): return get_group(course, 'instructor') def get_group(course, groupname): 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, created) = Group.objects.get_or_create(name=name) return group # 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.metadata['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 {0}'.format(data_dir), {}, page='idashboard')
if 'Reload course' in action:
log.debug('reloading {0} ({1})'.format(course_id, course))
try:
data_dir = course.metadata['data_dir']
modulestore().try_load_course(data_dir)
msg += "Course reloaded from {0}
".format(data_dir) track.views.server_track(request, 'reload {0}'.format(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 "Reset student's attempts" in action: # get the form data unique_student_identifier = request.POST.get('unique_student_identifier', '') problem_to_reset = request.POST.get('problem_to_reset', '') if problem_to_reset[-4:] == ".xml": problem_to_reset = problem_to_reset[:-4] # try to uniquely id student by email address or username try: if "@" in unique_student_identifier: student_to_reset = User.objects.get(email=unique_student_identifier) else: student_to_reset = User.objects.get(username=unique_student_identifier) msg += "Found a single student to reset. " except: student_to_reset = None msg += "Couldn't find student with that email or username. " if student_to_reset is not None: # find the module in question try: (org, course_name, run) = course_id.split("/") module_state_key = "i4x://" + org + "/" + course_name + "/problem/" + problem_to_reset module_to_reset = StudentModule.objects.get(student_id=student_to_reset.id, course_id=course_id, module_state_key=module_state_key) msg += "Found module to reset. " except Exception as e: msg += "Couldn't find module with that urlname. " # modify the problem's state try: # load the state json problem_state = json.loads(module_to_reset.state) old_number_of_attempts = problem_state["attempts"] problem_state["attempts"] = 0 # save module_to_reset.state = json.dumps(problem_state) module_to_reset.save() track.views.server_track(request, '{instructor} reset attempts from {old_attempts} to 0 for {student} on problem {problem} in {course}'.format( old_attempts=old_number_of_attempts, student=student_to_reset, problem=module_to_reset.module_state_key, instructor=request.user, course=course_id), {}, page='idashboard') msg += "Module state successfully reset!" except: msg += "Couldn't reset module state. " elif "Get link to student's progress page" in action: unique_student_identifier = request.POST.get('unique_student_identifier', '') try: if "@" in unique_student_identifier: student_to_reset = User.objects.get(email=unique_student_identifier) else: student_to_reset = User.objects.get(username=unique_student_identifier) progress_url = reverse('student_progress', kwargs={'course_id': course_id, 'student_id': student_to_reset.id}) track.views.server_track(request, '{instructor} requested progress page for {student} in {course}'.format( student=student_to_reset, instructor=request.user, course=course_id), {}, page='idashboard') msg += " Progress page for username: {1} with email address: {2}.".format(progress_url, student_to_reset.username, student_to_reset.email) except: msg += "Couldn't find student with that username. " #---------------------------------------- # 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, dataset = _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 {0}'.format(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 {0}'.format(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, run)=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 module to reset. " 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-{0}'.format(rolename), {}, 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, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_REMOVE, uname, FORUM_ROLE_ADMINISTRATOR, 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, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_ADD, uname, FORUM_ROLE_ADMINISTRATOR, 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-{0}'.format(rolename), {}, 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, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_REMOVE, uname, FORUM_ROLE_MODERATOR, 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, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_ADD, uname, FORUM_ROLE_MODERATOR, 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-{0}'.format(rolename), {}, 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, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_REMOVE, uname, FORUM_ROLE_COMMUNITY_TA, 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, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_ADD, uname, FORUM_ROLE_COMMUNITY_TA, 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 student': student = request.POST.get('enstudent', '') ret = _do_enroll_students(course, course_id, student) datatable = ret['datatable'] elif action == 'Un-enroll student': student = request.POST.get('enstudent', '') datatable = {} isok = False cea = CourseEnrollmentAllowed.objects.filter(course_id=course_id, email=student) if cea: cea.delete() msg += "Un-enrolled student with email '%s'" % student isok = True try: nce = CourseEnrollment.objects.get(user=User.objects.get(email=student), course_id=course_id) nce.delete() msg += "Un-enrolled student with email '%s'" % student except Exception as err: if not isok: msg += "Error! Failed to un-enroll student with email '%s'\n" % student msg += str(err) + '\n' elif action == 'Un-enroll ALL students': ret = _do_enroll_students(course, course_id, '', overload=True) datatable = ret['datatable'] elif action == 'Enroll multiple students': students = request.POST.get('enroll_multiple', '') ret = _do_enroll_students(course, course_id, 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 {0}'.format(problem), {}, page='idashboard') if idash_mode == 'Psychometrics': problems = psychoanalyze.problems_with_psychometric_data(course_id) #---------------------------------------- # offline grades? if use_offline: msg += "%s' % retdict['msg'].replace('\n', '