""" This module creates a sysadmin dashboard for managing and viewing courses. """ import json import logging import os from six import StringIO 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 text_type import dashboard.git_import as git_import import track.views from lms.djangoapps.courseware.courses import get_course_by_id from dashboard.git_import import GitImportError from dashboard.models import CourseImportLog from edxmako.shortcuts import render_to_response 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)