+ ${msg}
+
+ +
| ${_("GO")} | | +${_("tar.gz export")} | | +${_("db dump")} | | +${_("delete")} | | +${cid} | +${name} | +
+
+ +
diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py index 57e04f09d85e..5a1b942efc0b 100644 --- a/cms/djangoapps/contentstore/views/__init__.py +++ b/cms/djangoapps/contentstore/views/__init__.py @@ -16,6 +16,8 @@ from .public import * from .user import * from .tabs import * +from .sysadmin import * + try: from .dev import * except ImportError: diff --git a/cms/djangoapps/contentstore/views/sysadmin.py b/cms/djangoapps/contentstore/views/sysadmin.py new file mode 100644 index 000000000000..e9addcbf2053 --- /dev/null +++ b/cms/djangoapps/contentstore/views/sysadmin.py @@ -0,0 +1,113 @@ +""" +sysadmin - a custom page for MITx that they use to manage their instance. This page is enabled via +a FEATURE FLAG +""" +import subprocess +import logging + +from django_future.csrf import ensure_csrf_cookie +from django.contrib.auth.decorators import login_required +from django.shortcuts import redirect +from django.conf import settings +from django.http import HttpResponse +from django.utils.translation import ugettext as _ +from django.http import Http404 + +from xmodule.modulestore.django import modulestore + +from mitxmako.shortcuts import render_to_response + +#import pymongo +import time +import json + + +@login_required +@ensure_csrf_cookie +def sysadmin(request): + """ + sysadmin page: for now, just list courses and allow deletion + """ + if (not request.user) or (not request.user.is_staff): + return redirect('login') + + if not settings.MITX_FEATURES.get('ENABLE_MITX_SYSADMIN_PAGE', False): + raise Http404 + + collection = modulestore().collection + + msg = '' + bdir = getattr(settings, 'DELETED_COURSE_BACKUPS_DIR', None) + + action = request.GET.get('action', request.POST.get('action', '')) + course_id = request.GET.get('course_id', '') + + if action=='delete': + if not course_id: + msg += "{0}".format(_('Error - no course specified')) + else: + nrec = collection.find({'_id.course': course_id}).count() + if not 'really' in request.GET: + msg += _("Really delete course {0}?\n").format(course_id) + msg += _("{0} records for this course in the database").format(nrec) + logging.debug('Delete course %s requested' % course_id) + else: + msg += _("deleting {0}: ").format(course_id) + data = collection.find({'_id.course': course_id}) + fn = 'course-%s-dump-%s.json' % (course_id, time.ctime(time.time()).replace(' ','_')) + if bdir is not None: + with open('%s/%s' % (bdir, fn), 'w') as fp: + for d in data: + fp.write(json.dumps(d)+'\n') + collection.remove({'_id.course': course_id}) + msg += _("{0} records for {1} removed (backup file {2})").format(nrec, course_id, fn) + logging.debug('Course %s deleted!' % course_id) + action = "" + + elif action=='dump': + if not course_id: + msg += "{0}".format(_("Error - no course specified")) + else: + data = collection.find({'_id.course': course_id}) + response = HttpResponse(mimetype='text/json') + fn = 'course-%s-dump-%s.json' % (course_id, time.ctime(time.time()).replace(' ','_')) + response['Content-Disposition'] = 'attachment; filename={0}'.format(fn) + data = collection.find({'_id.course': course_id}) + for d in data: + response.write(json.dumps(d)+'\n') + return response + + elif action=='Add Course' and request.method=='POST': + ''' + Add course by running external script, given git URL provided in input form + ''' + giturl = request.POST.get('giturl', '') + acscript = getattr(settings, 'CMS_ADD_COURSE_SCRIPT', '') + cmd = '{0} "{1}"'.format(acscript, giturl) + logging.debug('Adding course with command: {0}'.format(cmd)) + ret = subprocess.Popen(cmd, shell=True, executable = "/bin/bash", + stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() + ret = ''.join(ret) + msg = "{0} {1}".format(_("Added course from"), giturl) + msg += "
{0}".format(ret.replace('<','<'))
+
+ #-----------------------------------------------------------------------------
+ # get list of courses
+
+ ctab = {}
+ idtab = {}
+ courses = collection.distinct('_id.course')
+ for course in courses:
+ cinfo = collection.find_one({'_id.course':course, '_id.category':'course'}) or {}
+ id = cinfo.get('_id',cinfo)
+ name = id.get('name',id)
+ ctab[course] = name
+ idtab[course] = id
+
+ context = {'ctab': ctab,
+ 'idtab': idtab,
+ 'msg': msg,
+ 'course_id': course_id,
+ 'action': action,
+ }
+ return render_to_response('sysadmin.html', context)
diff --git a/cms/envs/common.py b/cms/envs/common.py
index 05a1b63f5f8d..4e33c6a5e5c2 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -62,6 +62,9 @@
# If set to True, new Studio users won't be able to author courses unless
# edX has explicitly added them to the course creator group.
'ENABLE_CREATOR_GROUP': False,
+
+ # Turns on or off the 'sysadmin' page used by MITx instances
+ 'ENABLE_MITX_SYSADMIN_PAGE': False
}
ENABLE_JASMINE = False
@@ -81,6 +84,11 @@
sys.path.append(COMMON_ROOT / 'djangoapps')
sys.path.append(COMMON_ROOT / 'lib')
+########## DIRECTORY WHERE DELETED COURSE CONTENT IS BACKED UP ##############
+
+# for example:
+# DELETED_COURSE_BACKUPS_DIR = ENV_ROOT / "data_backup"
+DELETED_COURSE_BACKUPS_DIR = None # no backups
############################# WEB CONFIGURATION #############################
# This is where we stick our compiled template files.
diff --git a/cms/templates/sysadmin.html b/cms/templates/sysadmin.html
new file mode 100644
index 000000000000..b654fcbe6221
--- /dev/null
+++ b/cms/templates/sysadmin.html
@@ -0,0 +1,43 @@
+<%inherit file="base.html" />
+<%! from django.utils.translation import ugettext as _ %>
+
+<%block name="content">
+
+ ${msg}
+
+ | ${_("GO")} | | +${_("tar.gz export")} | | +${_("db dump")} | | +${_("delete")} | | +${cid} | +${name} | +