""" This module creates a sysadmin dashboard for managing and viewing courses. """ import json import logging import os import subprocess import mongoengine import unicodecsv as csv from django.conf import settings from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db import IntegrityError from django.http import Http404, HttpResponse from django.utils.decorators import method_decorator from django.utils.html import escape from django.utils.translation import ugettext as _ from django.views.decorators.cache import cache_control from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.http import condition from django.views.generic.base import TemplateView from opaque_keys.edx.keys import CourseKey from path import Path as path from six import StringIO, text_type import dashboard.git_import as git_import import track.views from dashboard.git_import import GitImportError from dashboard.models import CourseImportLog from edxmako.shortcuts import render_to_response from lms.djangoapps.courseware.courses import get_course_by_id from openedx.core.djangolib.markup import HTML from student.models import CourseEnrollment, Registration, UserProfile from student.roles import CourseInstructorRole, CourseStaffRole from xmodule.modulestore.django import modulestore log = logging.getLogger(__name__) class SysadminDashboardView(TemplateView): """Base class for sysadmin dashboard views with common methods""" template_name = 'sysadmin_dashboard.html' def __init__(self, **kwargs): """ Initialize base sysadmin dashboard class with modulestore, modulestore_type and return msg """ self.def_ms = modulestore() self.msg = u'' self.datatable = [] super(SysadminDashboardView, self).__init__(**kwargs) @method_decorator(ensure_csrf_cookie) @method_decorator(login_required) @method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True)) @method_decorator(condition(etag_func=None)) def dispatch(self, *args, **kwargs): return super(SysadminDashboardView, self).dispatch(*args, **kwargs) def get_courses(self): """ Get an iterable list of courses.""" return self.def_ms.get_courses() def return_csv(self, filename, header, data): """ Convenient function for handling the http response of a csv. data should be iterable and is used to stream object over http """ csv_file = StringIO() writer = csv.writer(csv_file, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) writer.writerow(header) # Setup streaming of the data def read_and_flush(): """Read and clear buffer for optimization""" csv_file.seek(0) csv_data = csv_file.read() csv_file.seek(0) csv_file.truncate() return csv_data def csv_data(): """Generator for handling potentially large CSVs""" for row in data: writer.writerow(row) csv_data = read_and_flush() yield csv_data response = HttpResponse(csv_data(), content_type='text/csv') response['Content-Disposition'] = u'attachment; filename={0}'.format( filename) return response class Users(SysadminDashboardView): """ The status view provides Web based user management, a listing of courses loaded, and user statistics """ def create_user(self, uname, name, password=None): """ Creates a user """ if not uname: return _('Must provide username') if not name: return _('Must provide full name') msg = u'' if not password: return _('Password must be supplied') email = uname if '@' not in email: msg += _('email address required (not username)') return msg new_password = password user = User(username=uname, email=email, is_active=True) user.set_password(new_password) try: user.save() except IntegrityError: msg += _(u'Oops, failed to create user {user}, {error}').format( user=user, error="IntegrityError" ) return msg reg = Registration() reg.register(user) profile = UserProfile(user=user) profile.name = name profile.save() msg += _(u'User {user} created successfully!').format(user=user) return msg def delete_user(self, uname): """Deletes a user from django auth""" if not uname: return _('Must provide username') if '@' in uname: try: user = User.objects.get(email=uname) except User.DoesNotExist as err: msg = _(u'Cannot find user with email address {email_addr}').format(email_addr=uname) return msg else: try: user = User.objects.get(username=uname) except User.DoesNotExist as err: msg = _(u'Cannot find user with username {username} - {error}').format( username=uname, error=str(err) ) return msg user.delete() return _(u'Deleted user {username}').format(username=uname) def make_datatable(self): """ Build the datatable for this view """ datatable = { 'header': [ _('Statistic'), _('Value'), ], 'title': _('Site statistics'), 'data': [ [ _('Total number of users'), User.objects.all().count(), ], ], } return datatable def get(self, request): if not request.user.is_staff: raise Http404 context = { 'datatable': self.make_datatable(), 'msg': self.msg, 'djangopid': os.getpid(), 'modeflag': {'users': 'active-section'}, } return render_to_response(self.template_name, context) def post(self, request): """Handle various actions available on page""" if not request.user.is_staff: raise Http404 action = request.POST.get('action', '') track.views.server_track(request, action, {}, page='user_sysdashboard') if action == 'download_users': header = [_('username'), _('email'), ] data = ([u.username, u.email] for u in (User.objects.all().iterator())) return self.return_csv('users_{0}.csv'.format( request.META['SERVER_NAME']), header, data) elif action == 'create_user': uname = request.POST.get('student_uname', '').strip() name = request.POST.get('student_fullname', '').strip() password = request.POST.get('student_password', '').strip() self.msg = HTML(u'
{1}
{1}
{0}").format(escape(ret))
return msg
def make_datatable(self, courses=None):
"""Creates course information datatable"""
data = []
courses = courses or self.get_courses()
for course in courses:
gdir = course.id.course
data.append([course.display_name, text_type(course.id)]
+ self.git_info_for_course(gdir))
return dict(header=[_('Course Name'),
_('Directory/ID'),
# Translators: "Git Commit" is a computer command; see http://gitref.org/basic/#commit
_('Git Commit'),
_('Last Change'),
_('Last Editor')],
title=_('Information about all courses'),
data=data)
def get(self, request):
"""Displays forms and course information"""
if not request.user.is_staff:
raise Http404
context = {
'datatable': self.make_datatable(),
'msg': self.msg,
'djangopid': os.getpid(),
'modeflag': {'courses': 'active-section'},
}
return render_to_response(self.template_name, context)
def post(self, request):
"""Handle all actions from courses view"""
if not request.user.is_staff:
raise Http404
action = request.POST.get('action', '')
track.views.server_track(request, action, {},
page='courses_sysdashboard')
courses = {course.id: course for course in self.get_courses()}
if action == 'add_course':
gitloc = request.POST.get('repo_location', '').strip().replace(' ', '').replace(';', '')
branch = request.POST.get('repo_branch', '').strip().replace(' ', '').replace(';', '')
self.msg += self.get_course_from_git(gitloc, branch)
elif action == 'del_course':
course_id = request.POST.get('course_id', '').strip()
course_key = CourseKey.from_string(course_id)
course_found = False
if course_key in courses:
course_found = True
course = courses[course_key]
else:
try:
course = get_course_by_id(course_key)
course_found = True
except Exception as err: # pylint: disable=broad-except
self.msg += _(
HTML(u'Error - cannot get course with ID {0}{1}')
).format(
course_key,
escape(str(err))
)
if course_found:
# delete course that is stored with mongodb backend
self.def_ms.delete_course(course.id, request.user.id)
# don't delete user permission groups, though
self.msg += \
HTML(u"{0} {1} = {2} ({3})").format(
_('Deleted'), text_type(course.location), text_type(course.id), course.display_name)
context = {
'datatable': self.make_datatable(list(courses.values())),
'msg': self.msg,
'djangopid': os.getpid(),
'modeflag': {'courses': 'active-section'},
}
return render_to_response(self.template_name, context)
class Staffing(SysadminDashboardView):
"""
The status view provides a view of staffing and enrollment in
courses that include an option to download the data as a csv.
"""
def get(self, request):
"""Displays course Enrollment and staffing course statistics"""
if not request.user.is_staff:
raise Http404
data = []
for course in self.get_courses():
datum = [course.display_name, course.id]
datum += [CourseEnrollment.objects.filter(
course_id=course.id).count()]
datum += [CourseStaffRole(course.id).users_with_role().count()]
datum += [','.join([x.username for x in CourseInstructorRole(
course.id).users_with_role()])]
data.append(datum)
datatable = dict(header=[_('Course Name'), _('course_id'),
_('# enrolled'), _('# staff'),
_('instructors')],
title=_('Enrollment information for all courses'),
data=data)
context = {
'datatable': datatable,
'msg': self.msg,
'djangopid': os.getpid(),
'modeflag': {'staffing': 'active-section'},
}
return render_to_response(self.template_name, context)
def post(self, request):
"""Handle all actions from staffing and enrollment view"""
action = request.POST.get('action', '')
track.views.server_track(request, action, {},
page='staffing_sysdashboard')
if action == 'get_staff_csv':
data = []
roles = [CourseInstructorRole, CourseStaffRole, ]
for course in self.get_courses():
for role in roles:
for user in role(course.id).users_with_role():
datum = [course.id, role, user.username, user.email,
user.profile.name.encode('utf-8')]
data.append(datum)
header = [_('course_id'),
_('role'), _('username'),
_('email'), _('full_name'), ]
return self.return_csv('staff_{0}.csv'.format(
request.META['SERVER_NAME']), header, data)
return self.get(request)
class GitLogs(TemplateView):
"""
This provides a view into the import of courses from git repositories.
It is convenient for allowing course teams to see what may be wrong with
their xml
"""
template_name = 'sysadmin_dashboard_gitlogs.html'
@method_decorator(login_required)
def get(self, request, *args, **kwargs):
"""Shows logs of imports that happened as a result of a git import"""
course_id = kwargs.get('course_id')
if course_id:
course_id = CourseKey.from_string(course_id)
page_size = 10
# Set mongodb defaults even if it isn't defined in settings
mongo_db = {
'host': 'localhost',
'user': '',
'password': '',
'db': 'xlog',
}
# Allow overrides
if hasattr(settings, 'MONGODB_LOG'):
for config_item in ['host', 'user', 'password', 'db', ]:
mongo_db[config_item] = settings.MONGODB_LOG.get(
config_item, mongo_db[config_item])
mongouri = 'mongodb://{user}:{password}@{host}/{db}'.format(**mongo_db)
error_msg = ''
try:
if mongo_db['user'] and mongo_db['password']:
mdb = mongoengine.connect(mongo_db['db'], host=mongouri)
else:
mdb = mongoengine.connect(mongo_db['db'], host=mongo_db['host'])
except mongoengine.connection.ConnectionError:
log.exception('Unable to connect to mongodb to save log, '
'please check MONGODB_LOG settings.')
if course_id is None:
# Require staff if not going to specific course
if not request.user.is_staff:
raise Http404
cilset = CourseImportLog.objects.order_by('-created')
else:
# Allow only course team, instructors, and staff
if not (request.user.is_staff or
CourseInstructorRole(course_id).has_user(request.user) or
CourseStaffRole(course_id).has_user(request.user)):
raise Http404
log.debug('course_id=%s', course_id)
cilset = CourseImportLog.objects.filter(
course_id=course_id
).order_by('-created')
log.debug(u'cilset length=%s', len(cilset))
# Paginate the query set
paginator = Paginator(cilset, page_size)
try:
logs = paginator.page(request.GET.get('page'))
except PageNotAnInteger:
logs = paginator.page(1)
except EmptyPage:
# If the page is too high or low
given_page = int(request.GET.get('page'))
page = min(max(1, given_page), paginator.num_pages)
logs = paginator.page(page)
mdb.close()
context = {
'logs': logs,
'course_id': text_type(course_id) if course_id else None,
'error_msg': error_msg,
'page_size': page_size
}
return render_to_response(self.template_name, context)