Commit c341975b by brianhw

Merge pull request #1072 from MITx/feature/brian/dashboard-manage-mods

Feature/brian/dashboard manage mods
parents 706a8253 8d0eb7f1
import logging
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
import logging
from courseware.courses import get_course_by_id from courseware.courses import get_course_by_id
FORUM_ROLE_ADMINISTRATOR = 'Administrator'
FORUM_ROLE_MODERATOR = 'Moderator'
FORUM_ROLE_COMMUNITY_TA = 'Community TA'
FORUM_ROLE_STUDENT = 'Student'
class Role(models.Model): class Role(models.Model):
name = models.CharField(max_length=30, null=False, blank=False) name = models.CharField(max_length=30, null=False, blank=False)
users = models.ManyToManyField(User, related_name="roles") users = models.ManyToManyField(User, related_name="roles")
...@@ -15,8 +21,8 @@ class Role(models.Model): ...@@ -15,8 +21,8 @@ class Role(models.Model):
def inherit_permissions(self, role): # TODO the name of this method is a little bit confusing, def inherit_permissions(self, role): # TODO the name of this method is a little bit confusing,
# since it's one-off and doesn't handle inheritance later # since it's one-off and doesn't handle inheritance later
if role.course_id and role.course_id != self.course_id: if role.course_id and role.course_id != self.course_id:
logging.warning("%s cannot inheret permissions from %s due to course_id inconsistency" % logging.warning("{0} cannot inherit permissions from {1} due to course_id inconsistency", \
(self, role)) self, role)
for per in role.permissions.all(): for per in role.permissions.all():
self.add_permission(per) self.add_permission(per)
...@@ -25,10 +31,10 @@ class Role(models.Model): ...@@ -25,10 +31,10 @@ class Role(models.Model):
def has_permission(self, permission): def has_permission(self, permission):
course = get_course_by_id(self.course_id) course = get_course_by_id(self.course_id)
if self.name == "Student" and \ if self.name == FORUM_ROLE_STUDENT and \
(permission.startswith('edit') or permission.startswith('update') or permission.startswith('create')) and \ (permission.startswith('edit') or permission.startswith('update') or permission.startswith('create')) and \
(not course.forum_posts_allowed): (not course.forum_posts_allowed):
return False return False
return self.permissions.filter(name=permission).exists() return self.permissions.filter(name=permission).exists()
......
import time
from collections import defaultdict from collections import defaultdict
from importlib import import_module import logging
import time
import urllib
from courseware.models import StudentModuleCache from django.contrib.auth.models import User
from courseware.module_render import get_module from django.core.urlresolvers import reverse
from xmodule.modulestore import Location from django.db import connection
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.search import path_to_location
from django.http import HttpResponse from django.http import HttpResponse
from django.utils import simplejson from django.utils import simplejson
from django.db import connection
from django.conf import settings
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django_comment_client.permissions import check_permissions_by_view
from django_comment_client.models import Role from django_comment_client.models import Role
from django_comment_client.permissions import check_permissions_by_view
from mitxmako import middleware from mitxmako import middleware
import logging
import operator
import itertools
import urllib
import pystache_custom as pystache import pystache_custom as pystache
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.search import path_to_location
# TODO these should be cached via django's caching rather than in-memory globals # TODO these should be cached via django's caching rather than in-memory globals
_FULLMODULES = None _FULLMODULES = None
...@@ -47,9 +41,16 @@ def get_role_ids(course_id): ...@@ -47,9 +41,16 @@ def get_role_ids(course_id):
staff = list(User.objects.filter(is_staff=True).values_list('id', flat=True)) staff = list(User.objects.filter(is_staff=True).values_list('id', flat=True))
roles_with_ids = {'Staff': staff} roles_with_ids = {'Staff': staff}
for role in roles: for role in roles:
roles_with_ids[role.name] = list(role.users.values_list('id', flat=True)) roles_with_ids[role.name] = list(role.users.values_list('id', flat=True))
return roles_with_ids return roles_with_ids
def has_forum_access(uname, course_id, rolename):
try:
role = Role.objects.get(name=rolename, course_id=course_id)
except Role.DoesNotExist:
return False
return role.users.filter(username=uname).exists()
def get_full_modules(): def get_full_modules():
global _FULLMODULES global _FULLMODULES
if not _FULLMODULES: if not _FULLMODULES:
...@@ -132,8 +133,6 @@ def initialize_discussion_info(course): ...@@ -132,8 +133,6 @@ def initialize_discussion_info(course):
return return
course_id = course.id course_id = course.id
url_course_id = course_id.replace('/', '_').replace('.', '_')
all_modules = get_full_modules()[course_id] all_modules = get_full_modules()[course_id]
discussion_id_map = {} discussion_id_map = {}
......
...@@ -8,21 +8,19 @@ Notes for running by hand: ...@@ -8,21 +8,19 @@ Notes for running by hand:
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/instructor django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/instructor
""" """
import courseware.tests.tests as ct
from nose import SkipTest
from mock import patch, Mock
from override_settings import override_settings from override_settings import override_settings
# Need access to internal func to put users in the right group from django.contrib.auth.models import \
from courseware.access import _course_staff_group_name Group # Need access to internal func to put users in the right group
from django.contrib.auth.models import User, Group
from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, \
FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_STUDENT
from django_comment_client.utils import has_forum_access
import xmodule.modulestore.django from courseware.access import _course_staff_group_name
import courseware.tests.tests as ct
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE) @override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
...@@ -61,24 +59,153 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader): ...@@ -61,24 +59,153 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader):
def test_download_grades_csv(self): def test_download_grades_csv(self):
print "running test_download_grades_csv"
course = self.toy course = self.toy
url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
msg = "url = %s\n" % url msg = "url = {0}\n".format(url)
response = self.client.post(url, {'action': 'Download CSV of all student grades for this course', response = self.client.post(url, {'action': 'Download CSV of all student grades for this course'})
}) msg += "instructor dashboard download csv grades: response = '{0}'\n".format(response)
msg += "instructor dashboard download csv grades: response = '%s'\n" % response
self.assertEqual(response['Content-Type'],'text/csv',msg) self.assertEqual(response['Content-Type'],'text/csv',msg)
cdisp = response['Content-Disposition'].replace('TT_2012','2012') # jenkins course_id is TT_2012_Fall instead of 2012_Fall? cdisp = response['Content-Disposition'].replace('TT_2012','2012') # jenkins course_id is TT_2012_Fall instead of 2012_Fall?
msg += "cdisp = '%s'\n" % cdisp msg += "cdisp = '{0}'\n".format(cdisp)
self.assertEqual(cdisp,'attachment; filename=grades_edX/toy/2012_Fall.csv',msg) self.assertEqual(cdisp,'attachment; filename=grades_edX/toy/2012_Fall.csv',msg)
body = response.content.replace('\r','') body = response.content.replace('\r','')
msg += "body = '%s'\n" % body msg += "body = '{0}'\n".format(body)
expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm","Final" expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm","Final"
"2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0.0","0.0" "2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0.0","0.0"
''' '''
self.assertEqual(body, expected_body, msg) self.assertEqual(body, expected_body, msg)
FORUM_ROLES = [ FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA ]
FORUM_ADMIN_ACTION_SUFFIX = { FORUM_ROLE_ADMINISTRATOR : 'admin', FORUM_ROLE_MODERATOR : 'moderator', FORUM_ROLE_COMMUNITY_TA : 'community TA'}
FORUM_ADMIN_USER = { FORUM_ROLE_ADMINISTRATOR : 'forumadmin', FORUM_ROLE_MODERATOR : 'forummoderator', FORUM_ROLE_COMMUNITY_TA : 'forummoderator'}
def action_name(operation, rolename):
if operation == 'List':
return '{0} course forum {1}s'.format(operation, FORUM_ADMIN_ACTION_SUFFIX[rolename])
else:
return '{0} forum {1}'.format(operation, FORUM_ADMIN_ACTION_SUFFIX[rolename])
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
class TestInstructorDashboardForumAdmin(ct.PageLoader):
'''
Check for change in forum admin role memberships
'''
def setUp(self):
xmodule.modulestore.django._MODULESTORES = {}
courses = modulestore().get_courses()
def find_course(name):
"""Assumes the course is present"""
return [c for c in courses if c.location.course==name][0]
self.full = find_course("full")
self.toy = find_course("toy")
# Create two accounts
self.student = 'view@test.com'
self.instructor = 'view2@test.com'
self.password = 'foo'
self.create_account('u1', self.student, self.password)
self.create_account('u2', self.instructor, self.password)
self.activate_user(self.student)
self.activate_user(self.instructor)
group_name = _course_staff_group_name(self.toy.location)
g = Group.objects.create(name=group_name)
g.user_set.add(ct.user(self.instructor))
self.logout()
self.login(self.instructor, self.password)
self.enroll(self.toy)
def initialize_roles(self, course_id):
self.admin_role = Role.objects.get_or_create(name=FORUM_ROLE_ADMINISTRATOR, course_id=course_id)[0]
self.moderator_role = Role.objects.get_or_create(name=FORUM_ROLE_MODERATOR, course_id=course_id)[0]
self.community_ta_role = Role.objects.get_or_create(name=FORUM_ROLE_COMMUNITY_TA, course_id=course_id)[0]
def test_add_forum_admin_users_for_unknown_user(self):
course = self.toy
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
username = 'unknown'
for action in ['Add', 'Remove']:
for rolename in FORUM_ROLES:
response = self.client.post(url, {'action': action_name(action, rolename), FORUM_ADMIN_USER[rolename]: username})
self.assertTrue(response.content.find('Error: unknown username "{0}"'.format(username))>=0)
def test_add_forum_admin_users_for_missing_roles(self):
course = self.toy
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
username = 'u1'
for action in ['Add', 'Remove']:
for rolename in FORUM_ROLES:
response = self.client.post(url, {'action': action_name(action, rolename), FORUM_ADMIN_USER[rolename]: username})
self.assertTrue(response.content.find('Error: unknown rolename "{0}"'.format(rolename))>=0)
def test_remove_forum_admin_users_for_missing_users(self):
course = self.toy
self.initialize_roles(course.id)
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
username = 'u1'
action = 'Remove'
for rolename in FORUM_ROLES:
response = self.client.post(url, {'action': action_name(action, rolename), FORUM_ADMIN_USER[rolename]: username})
self.assertTrue(response.content.find('Error: user "{0}" does not have rolename "{1}"'.format(username, rolename))>=0)
def test_add_and_remove_forum_admin_users(self):
course = self.toy
self.initialize_roles(course.id)
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
username = 'u2'
for rolename in FORUM_ROLES:
response = self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username})
self.assertTrue(response.content.find('Added "{0}" to "{1}" forum role = "{2}"'.format(username, course.id, rolename))>=0)
self.assertTrue(has_forum_access(username, course.id, rolename))
response = self.client.post(url, {'action': action_name('Remove', rolename), FORUM_ADMIN_USER[rolename]: username})
self.assertTrue(response.content.find('Removed "{0}" from "{1}" forum role = "{2}"'.format(username, course.id, rolename))>=0)
self.assertFalse(has_forum_access(username, course.id, rolename))
def test_add_and_readd_forum_admin_users(self):
course = self.toy
self.initialize_roles(course.id)
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
username = 'u2'
for rolename in FORUM_ROLES:
# perform an add, and follow with a second identical add:
self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username})
response = self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username})
self.assertTrue(response.content.find('Error: user "{0}" already has rolename "{1}", cannot add'.format(username, rolename))>=0)
self.assertTrue(has_forum_access(username, course.id, rolename))
def test_add_nonstaff_forum_admin_users(self):
course = self.toy
self.initialize_roles(course.id)
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
username = 'u1'
rolename = FORUM_ROLE_ADMINISTRATOR
response = self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username})
self.assertTrue(response.content.find('Error: user "{0}" should first be added as staff'.format(username))>=0)
def test_list_forum_admin_users(self):
course = self.toy
self.initialize_roles(course.id)
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
username = 'u2'
added_roles = [FORUM_ROLE_STUDENT] # u2 is already added as a student to the discussion forums
self.assertTrue(has_forum_access(username, course.id, 'Student'))
for rolename in FORUM_ROLES:
response = self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username})
self.assertTrue(has_forum_access(username, course.id, rolename))
response = self.client.post(url, {'action': action_name('List', rolename), FORUM_ADMIN_USER[rolename]: username})
for header in ['Username', 'Full name', 'Roles']:
self.assertTrue(response.content.find('<th>{0}</th>'.format(header))>0)
self.assertTrue(response.content.find('<td>{0}</td>'.format(username))>=0)
# concatenate all roles for user, in sorted order:
added_roles.append(rolename)
added_roles.sort()
roles = ', '.join(added_roles)
self.assertTrue(response.content.find('<td>{0}</td>'.format(roles))>=0, 'not finding roles "{0}"'.format(roles))
# ======== Instructor views ============================================================================= # ======== Instructor views =============================================================================
from collections import defaultdict
import csv import csv
import itertools
import json
import logging import logging
import os import os
import urllib import urllib
import track.views
from functools import partial
from collections import defaultdict
from django.conf import settings from django.conf import settings
from django.core.context_processors import csrf
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.contrib.auth.decorators import login_required from django.http import HttpResponse
from django.http import Http404, HttpResponse
from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string
#from django.views.decorators.csrf import ensure_csrf_cookie
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.views.decorators.cache import cache_control from django.views.decorators.cache import cache_control
from mitxmako.shortcuts import render_to_response
from courseware import grades from courseware import grades
from courseware.access import has_access, get_access_group_name from courseware.access import has_access, get_access_group_name
from courseware.courses import (get_course_with_access, get_courses_by_university) from courseware.courses import get_course_with_access
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 psychometrics import psychoanalyze
from student.models import UserProfile from student.models import CourseEnrollment
from student.models import UserTestGroup, CourseEnrollment
from util.cache import cache, cache_if_anonymous
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
from xmodule.modulestore.search import path_to_location from xmodule.modulestore.search import path_to_location
import track.views
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
template_imports = {'urllib': urllib} template_imports = {'urllib': urllib}
# internal commands for managing forum roles:
FORUM_ROLE_ADD = 'add'
FORUM_ROLE_REMOVE = 'remove'
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def instructor_dashboard(request, course_id): def instructor_dashboard(request, course_id):
"""Display the instructor dashboard for a course.""" """Display the instructor dashboard for a course."""
course = get_course_with_access(request.user, course_id, 'staff') course = get_course_with_access(request.user, course_id, 'staff')
instructor_access = has_access(request.user, course, 'instructor') # an instructor can manage staff lists 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 = '' msg = ''
#msg += ('POST=%s' % dict(request.POST)).replace('<','&lt;')
problems = [] problems = []
plots = [] plots = []
...@@ -81,7 +74,7 @@ def instructor_dashboard(request, course_id): ...@@ -81,7 +74,7 @@ def instructor_dashboard(request, course_id):
def return_csv(fn, datatable): def return_csv(fn, datatable):
response = HttpResponse(mimetype='text/csv') response = HttpResponse(mimetype='text/csv')
response['Content-Disposition'] = 'attachment; filename=%s' % fn response['Content-Disposition'] = 'attachment; filename={0}'.format(fn)
writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL)
writer.writerow(datatable['header']) writer.writerow(datatable['header'])
for datarow in datatable['data']: for datarow in datatable['data']:
...@@ -104,75 +97,75 @@ def instructor_dashboard(request, course_id): ...@@ -104,75 +97,75 @@ def instructor_dashboard(request, course_id):
if settings.MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD']: if settings.MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD']:
if 'GIT pull' in action: if 'GIT pull' in action:
data_dir = course.metadata['data_dir'] data_dir = course.metadata['data_dir']
log.debug('git pull %s' % (data_dir)) log.debug('git pull {0}'.format(data_dir))
gdir = settings.DATA_DIR / data_dir gdir = settings.DATA_DIR / data_dir
if not os.path.exists(gdir): if not os.path.exists(gdir):
msg += "====> ERROR in gitreload - no such directory %s" % gdir msg += "====> ERROR in gitreload - no such directory {0}".format(gdir)
else: else:
cmd = "cd %s; git reset --hard HEAD; git clean -f -d; git pull origin; chmod g+w course.xml" % gdir 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 %s:<p>" % data_dir msg += "git pull on {0}:<p>".format(data_dir)
msg += "<pre>%s</pre></p>" % escape(os.popen(cmd).read()) msg += "<pre>{0}</pre></p>".format(escape(os.popen(cmd).read()))
track.views.server_track(request, 'git pull %s' % data_dir, {}, page='idashboard') track.views.server_track(request, 'git pull {0}'.format(data_dir), {}, page='idashboard')
if 'Reload course' in action: if 'Reload course' in action:
log.debug('reloading %s (%s)' % (course_id, course)) log.debug('reloading {0} ({1})'.format(course_id, course))
try: try:
data_dir = course.metadata['data_dir'] data_dir = course.metadata['data_dir']
modulestore().try_load_course(data_dir) modulestore().try_load_course(data_dir)
msg += "<br/><p>Course reloaded from %s</p>" % data_dir msg += "<br/><p>Course reloaded from {0}</p>".format(data_dir)
track.views.server_track(request, 'reload %s' % data_dir, {}, page='idashboard') track.views.server_track(request, 'reload {0}'.format(data_dir), {}, page='idashboard')
course_errors = modulestore().get_item_errors(course.location) course_errors = modulestore().get_item_errors(course.location)
msg += '<ul>' msg += '<ul>'
for cmsg, cerr in course_errors: for cmsg, cerr in course_errors:
msg += "<li>%s: <pre>%s</pre>" % (cmsg,escape(cerr)) msg += "<li>{0}: <pre>{1}</pre>".format(cmsg,escape(cerr))
msg += '</ul>' msg += '</ul>'
except Exception as err: except Exception as err:
msg += '<br/><p>Error: %s</p>' % escape(err) msg += '<br/><p>Error: {0}</p>'.format(escape(err))
if action == 'Dump list of enrolled students': if action == 'Dump list of enrolled students':
log.debug(action) log.debug(action)
datatable = get_student_grade_summary_data(request, course, course_id, get_grades=False) datatable = get_student_grade_summary_data(request, course, course_id, get_grades=False)
datatable['title'] = 'List of students enrolled in %s' % course_id datatable['title'] = 'List of students enrolled in {0}'.format(course_id)
track.views.server_track(request, 'list-students', {}, page='idashboard') track.views.server_track(request, 'list-students', {}, page='idashboard')
elif 'Dump Grades' in action: elif 'Dump Grades' in action:
log.debug(action) log.debug(action)
datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True) datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True)
datatable['title'] = 'Summary Grades of students enrolled in %s' % course_id datatable['title'] = 'Summary Grades of students enrolled in {0}'.format(course_id)
track.views.server_track(request, 'dump-grades', {}, page='idashboard') track.views.server_track(request, 'dump-grades', {}, page='idashboard')
elif 'Dump all RAW grades' in action: elif 'Dump all RAW grades' in action:
log.debug(action) log.debug(action)
datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True, datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True,
get_raw_scores=True) get_raw_scores=True)
datatable['title'] = 'Raw Grades of students enrolled in %s' % course_id datatable['title'] = 'Raw Grades of students enrolled in {0}'.format(course_id)
track.views.server_track(request, 'dump-grades-raw', {}, page='idashboard') track.views.server_track(request, 'dump-grades-raw', {}, page='idashboard')
elif 'Download CSV of all student grades' in action: elif 'Download CSV of all student grades' in action:
track.views.server_track(request, 'dump-grades-csv', {}, page='idashboard') track.views.server_track(request, 'dump-grades-csv', {}, page='idashboard')
return return_csv('grades_%s.csv' % course_id, return return_csv('grades_{0}.csv'.format(course_id),
get_student_grade_summary_data(request, course, course_id)) get_student_grade_summary_data(request, course, course_id))
elif 'Download CSV of all RAW grades' in action: elif 'Download CSV of all RAW grades' in action:
track.views.server_track(request, 'dump-grades-csv-raw', {}, page='idashboard') track.views.server_track(request, 'dump-grades-csv-raw', {}, page='idashboard')
return return_csv('grades_%s_raw.csv' % course_id, return return_csv('grades_{0}_raw.csv'.format(course_id),
get_student_grade_summary_data(request, course, course_id, get_raw_scores=True)) get_student_grade_summary_data(request, course, course_id, get_raw_scores=True))
elif 'Download CSV of answer distributions' in action: elif 'Download CSV of answer distributions' in action:
track.views.server_track(request, 'dump-answer-dist-csv', {}, page='idashboard') track.views.server_track(request, 'dump-answer-dist-csv', {}, page='idashboard')
return return_csv('answer_dist_%s.csv' % course_id, get_answers_distribution(request, course_id)) return return_csv('answer_dist_{0}.csv'.format(course_id), get_answers_distribution(request, course_id))
#---------------------------------------- #----------------------------------------
# Admin # Admin
elif 'List course staff' in action: elif 'List course staff' in action:
group = get_staff_group(course) group = get_staff_group(course)
msg += 'Staff group = %s' % group.name msg += 'Staff group = {0}'.format(group.name)
log.debug('staffgrp=%s' % group.name) log.debug('staffgrp={0}'.format(group.name))
uset = group.user_set.all() uset = group.user_set.all()
datatable = {'header': ['Username', 'Full name']} datatable = {'header': ['Username', 'Full name']}
datatable['data'] = [[x.username, x.profile.name] for x in uset] datatable['data'] = [[x.username, x.profile.name] for x in uset]
datatable['title'] = 'List of Staff in course %s' % course_id datatable['title'] = 'List of Staff in course {0}'.format(course_id)
track.views.server_track(request, 'list-staff', {}, page='idashboard') track.views.server_track(request, 'list-staff', {}, page='idashboard')
elif action == 'Add course staff': elif action == 'Add course staff':
...@@ -180,28 +173,86 @@ def instructor_dashboard(request, course_id): ...@@ -180,28 +173,86 @@ def instructor_dashboard(request, course_id):
try: try:
user = User.objects.get(username=uname) user = User.objects.get(username=uname)
except User.DoesNotExist: except User.DoesNotExist:
msg += '<font color="red">Error: unknown username "%s"</font>' % uname msg += '<font color="red">Error: unknown username "{0}"</font>'.format(uname)
user = None user = None
if user is not None: if user is not None:
group = get_staff_group(course) group = get_staff_group(course)
msg += '<font color="green">Added %s to staff group = %s</font>' % (user, group.name) msg += '<font color="green">Added {0} to staff group = {1}</font>'.format(user, group.name)
log.debug('staffgrp=%s' % group.name) log.debug('staffgrp={0}'.format(group.name))
user.groups.add(group) user.groups.add(group)
track.views.server_track(request, 'add-staff %s' % user, {}, page='idashboard') track.views.server_track(request, 'add-staff {0}'.format(user), {}, page='idashboard')
elif action == 'Remove course staff': elif action == 'Remove course staff':
uname = request.POST['staffuser'] uname = request.POST['staffuser']
try: try:
user = User.objects.get(username=uname) user = User.objects.get(username=uname)
except User.DoesNotExist: except User.DoesNotExist:
msg += '<font color="red">Error: unknown username "%s"</font>' % uname msg += '<font color="red">Error: unknown username "{0}"</font>'.format(uname)
user = None user = None
if user is not None: if user is not None:
group = get_staff_group(course) group = get_staff_group(course)
msg += '<font color="green">Removed %s from staff group = %s</font>' % (user, group.name) msg += '<font color="green">Removed {0} from staff group = {1}</font>'.format(user, group.name)
log.debug('staffgrp=%s' % group.name) log.debug('staffgrp={0}'.format(group.name))
user.groups.remove(group) user.groups.remove(group)
track.views.server_track(request, 'remove-staff %s' % user, {}, page='idashboard') track.views.server_track(request, 'remove-staff {0}'.format(user), {}, page='idashboard')
#----------------------------------------
# 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')
#---------------------------------------- #----------------------------------------
# psychometrics # psychometrics
...@@ -210,17 +261,20 @@ def instructor_dashboard(request, course_id): ...@@ -210,17 +261,20 @@ def instructor_dashboard(request, course_id):
problem = request.POST['Problem'] problem = request.POST['Problem']
nmsg, plots = psychoanalyze.generate_plots_for_problem(problem) nmsg, plots = psychoanalyze.generate_plots_for_problem(problem)
msg += nmsg msg += nmsg
track.views.server_track(request, 'psychometrics %s' % problem, {}, page='idashboard') track.views.server_track(request, 'psychometrics {0}'.format(problem), {}, page='idashboard')
if idash_mode=='Psychometrics': if idash_mode=='Psychometrics':
problems = psychoanalyze.problems_with_psychometric_data(course_id) problems = psychoanalyze.problems_with_psychometric_data(course_id)
#---------------------------------------- #----------------------------------------
# context for rendering # context for rendering
context = {'course': course, context = {'course': course,
'staff_access': True, 'staff_access': True,
'admin_access': request.user.is_staff, 'admin_access': request.user.is_staff,
'instructor_access': instructor_access, 'instructor_access': instructor_access,
'forum_admin_access': forum_admin_access,
'datatable': datatable, 'datatable': datatable,
'msg': msg, 'msg': msg,
'modeflag': {idash_mode: 'selectedmode'}, 'modeflag': {idash_mode: 'selectedmode'},
...@@ -232,6 +286,75 @@ def instructor_dashboard(request, course_id): ...@@ -232,6 +286,75 @@ def instructor_dashboard(request, course_id):
return render_to_response('courseware/instructor_dashboard.html', context) return render_to_response('courseware/instructor_dashboard.html', context)
def _list_course_forum_members(course_id, rolename, datatable):
'''
Fills in datatable with forum membership information, for a given role,
so that it will be displayed on instructor dashboard.
course_ID = course's ID string
rolename = one of "Administrator", "Moderator", "Community TA"
Returns message status string to append to displayed message, if role is unknown.
'''
# make sure datatable is set up properly for display first, before checking for errors
datatable['header'] = ['Username', 'Full name', 'Roles']
datatable['title'] = 'List of Forum {0}s in course {1}'.format(rolename, course_id)
datatable['data'] = [];
try:
role = Role.objects.get(name=rolename, course_id=course_id)
except Role.DoesNotExist:
return '<font color="red">Error: unknown rolename "{0}"</font>'.format(rolename)
uset = role.users.all().order_by('username')
msg = 'Role = {0}'.format(rolename)
log.debug('role={0}'.format(rolename))
datatable['data'] = [[x.username, x.profile.name, ', '.join([r.name for r in x.roles.filter(course_id=course_id).order_by('name')])] for x in uset]
return msg
def _update_forum_role_membership(uname, course, rolename, add_or_remove):
'''
Supports adding a user to a course's forum role
uname = username string for user
course = course object
rolename = one of "Administrator", "Moderator", "Community TA"
add_or_remove = one of "add" or "remove"
Returns message status string to append to displayed message, Status is returned if user
or role is unknown, or if entry already exists when adding, or if entry doesn't exist when removing.
'''
# check that username and rolename are valid:
try:
user = User.objects.get(username=uname)
except User.DoesNotExist:
return '<font color="red">Error: unknown username "{0}"</font>'.format(uname)
try:
role = Role.objects.get(name=rolename, course_id=course.id)
except Role.DoesNotExist:
return '<font color="red">Error: unknown rolename "{0}"</font>'.format(rolename)
# check whether role already has the specified user:
alreadyexists = role.users.filter(username=uname).exists()
msg = ''
log.debug('rolename={0}'.format(rolename))
if add_or_remove == FORUM_ROLE_REMOVE:
if not alreadyexists:
msg ='<font color="red">Error: user "{0}" does not have rolename "{1}", cannot remove</font>'.format(uname, rolename)
else:
user.roles.remove(role)
msg = '<font color="green">Removed "{0}" from "{1}" forum role = "{2}"</font>'.format(user, course.id, rolename)
else:
if alreadyexists:
msg = '<font color="red">Error: user "{0}" already has rolename "{1}", cannot add</font>'.format(uname, rolename)
else:
if (rolename == FORUM_ROLE_ADMINISTRATOR and not has_access(user, course, 'staff')):
msg = '<font color="red">Error: user "{0}" should first be added as staff before adding as a forum administrator, cannot add</font>'.format(uname)
else:
user.roles.add(role)
msg = '<font color="green">Added "{0}" to "{1}" forum role = "{2}"</font>'.format(user, course.id, rolename)
return msg
def get_student_grade_summary_data(request, course, course_id, get_grades=True, get_raw_scores=False): def get_student_grade_summary_data(request, course, course_id, get_grades=True, get_raw_scores=False):
''' '''
...@@ -257,7 +380,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, ...@@ -257,7 +380,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True,
if get_grades: if get_grades:
# just to construct the header # just to construct the header
gradeset = grades.grade(enrolled_students[0], request, course, keep_raw_scores=get_raw_scores) gradeset = grades.grade(enrolled_students[0], request, course, keep_raw_scores=get_raw_scores)
# log.debug('student %s gradeset %s' % (enrolled_students[0], gradeset)) # log.debug('student {0} gradeset {1}'.format(enrolled_students[0], gradeset))
if get_raw_scores: if get_raw_scores:
header += [score.section for score in gradeset['raw_scores']] header += [score.section for score in gradeset['raw_scores']]
else: else:
...@@ -275,7 +398,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, ...@@ -275,7 +398,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True,
if get_grades: if get_grades:
gradeset = grades.grade(student, request, course, keep_raw_scores=get_raw_scores) gradeset = grades.grade(student, request, course, keep_raw_scores=get_raw_scores)
# log.debug('student=%s, gradeset=%s' % (student,gradeset)) # log.debug('student={0}, gradeset={1}'.format(student,gradeset))
if get_raw_scores: if get_raw_scores:
datarow += [score.earned for score in gradeset['raw_scores']] datarow += [score.earned for score in gradeset['raw_scores']]
else: else:
......
...@@ -56,7 +56,8 @@ function goto( mode) ...@@ -56,7 +56,8 @@ function goto( mode)
%if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS'): %if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS'):
<a href="#" onclick="goto('Psychometrics');" class="${modeflag.get('Psychometrics')}">Psychometrics</a> | <a href="#" onclick="goto('Psychometrics');" class="${modeflag.get('Psychometrics')}">Psychometrics</a> |
%endif %endif
<a href="#" onclick="goto('Admin');" class="${modeflag.get('Admin')}">Admin</a> ] <a href="#" onclick="goto('Admin');" class="${modeflag.get('Admin')}">Admin</a> |
<a href="#" onclick="goto('Forum Admin');" class="${modeflag.get('Forum Admin')}">Forum Admin</a> ]
</h2> </h2>
<div style="text-align:right" id="djangopid">${djangopid}</div> <div style="text-align:right" id="djangopid">${djangopid}</div>
...@@ -134,6 +135,34 @@ function goto( mode) ...@@ -134,6 +135,34 @@ function goto( mode)
%endif %endif
%endif %endif
##-----------------------------------------------------------------------------
%if modeflag.get('Forum Admin'):
%if instructor_access:
<hr width="40%" style="align:left">
<p>
<input type="submit" name="action" value="List course forum admins">
<p>
<input type="text" name="forumadmin"> <input type="submit" name="action" value="Remove forum admin">
<input type="submit" name="action" value="Add forum admin">
<hr width="40%" style="align:left">
%endif
%if instructor_access or forum_admin_access:
<p>
<input type="submit" name="action" value="List course forum moderators">
<input type="submit" name="action" value="List course forum community TAs">
<p>
<input type="text" name="forummoderator">
<input type="submit" name="action" value="Remove forum moderator">
<input type="submit" name="action" value="Add forum moderator">
<input type="submit" name="action" value="Remove forum community TA">
<input type="submit" name="action" value="Add forum community TA">
<hr width="40%" style="align:left">
%else:
<p>User requires forum administrator privileges to perform administration tasks. See instructor.</p>
%endif
%endif
</form> </form>
##----------------------------------------------------------------------------- ##-----------------------------------------------------------------------------
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment