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"> +
+ % if action=="delete": + ${_("Really delete {0}").format(course_id)}? + No (Cancel) + ${_("Yes")}   + % endif + +
+     ${msg}
+    
+
+ + % for cid in sorted(ctab.iterkeys()): + <% + id = idtab[cid] + org = id.get('org','') + name = id.get('name','') + nameurl = name.replace(' ','_') + %> + + + + + + + + + % endfor +
${_("GO")} | ${_("tar.gz export")}  |${_("db dump")}  |${_("delete")}  |${cid}${name}
+ +
+
+
+ + git ssh url: + +
+
+ \ No newline at end of file diff --git a/cms/urls.py b/cms/urls.py index 893dc3288451..0303cd112bf8 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -24,6 +24,7 @@ url(r'^unpublish_unit$', 'contentstore.views.unpublish_unit', name='unpublish_unit'), url(r'^create_new_course', 'contentstore.views.create_new_course', name='create_new_course'), url(r'^reorder_static_tabs', 'contentstore.views.reorder_static_tabs', name='reorder_static_tabs'), + url(r'^sysadmin', 'contentstore.views.sysadmin', name='sysadmin'), url(r'^(?P[^/]+)/(?P[^/]+)/import/(?P[^/]+)$', 'contentstore.views.import_course', name='import_course'),