From 7ee34aae858a6235c184d7eaf2227bec073b2e40 Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 16 Aug 2013 17:52:10 -0400 Subject: [PATCH 1/7] add sysadmin dashboard for Studio --- cms/djangoapps/contentstore/views/__init__.py | 3 + cms/djangoapps/contentstore/views/sysadmin.py | 105 ++++++++++++++++++ cms/templates/sysadmin.html | 42 +++++++ cms/urls.py | 1 + 4 files changed, 151 insertions(+) create mode 100644 cms/djangoapps/contentstore/views/sysadmin.py create mode 100644 cms/templates/sysadmin.html diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py index 57e04f09d85e..e5545840c4bc 100644 --- a/cms/djangoapps/contentstore/views/__init__.py +++ b/cms/djangoapps/contentstore/views/__init__.py @@ -16,6 +16,9 @@ from .public import * from .user import * from .tabs import * +from .requests 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..7ceeb3f90f71 --- /dev/null +++ b/cms/djangoapps/contentstore/views/sysadmin.py @@ -0,0 +1,105 @@ +import subprocess +import logging + +from django_future.csrf import ensure_csrf_cookie +from django.core.context_processors import csrf +from django.shortcuts import redirect +from django.conf import settings +from django.http import HttpResponse + +from mitxmako.shortcuts import render_to_response + +import pymongo +import time +import json + +""" +sysadmin +""" + +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') + + client = pymongo.MongoClient() + db = client.xmodule + + msg = '' + bdir = "DATA-BACKUP" + + action = request.GET.get('action', request.POST.get('action', '')) + course_id = request.GET.get('course_id', '') + + if action=='delete': + if not course_id: + msg += "Error - no course specified" + else: + nrec = db.modulestore.find({'_id.course': course_id}).count() + if not 'really' in request.GET: + msg += "Really delete course %s?\n" % course_id + msg += "%d records for this course in the database" % nrec + logging.debug('Delete course %s requested' % course_id) + else: + msg += "deleting %s" % course_id + data = db.modulestore.find({'_id.course': course_id}) + fn = 'course-%s-dump-%s.json' % (course_id, time.ctime(time.time()).replace(' ','_')) + fp = open('%s/%s' % (bdir,fn), 'w') + for d in data: + fp.write(json.dumps(d)+'\n') + fp.close() + db.modulestore.remove({'_id.course': course_id}) + msg += "%d records for %s removed (backup file %s)" % (nrec, course_id, fn) + logging.debug('Course %s deleted!' % course_id) + action = "" + + elif action=='dump': + if not course_id: + msg += "Error - no course specified" + else: + data = db.modulestore.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 = db.modulestore.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 = "Added course from {0}".format(giturl) + msg += "
{0}
".format(ret.replace('<','<')) + + #----------------------------------------------------------------------------- + # get list of courses + + ctab = {} + idtab = {} + courses = db.modulestore.distinct('_id.course') + for course in courses: + cinfo = db.modulestore.find_one({'_id.course':course, '_id.category':'course'}) or {} + id = cinfo.get('_id',cinfo) + name = id.get('name',id) + #print "course %s: %s" % (course, name) + 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/templates/sysadmin.html b/cms/templates/sysadmin.html new file mode 100644 index 000000000000..d2d0d1ddf331 --- /dev/null +++ b/cms/templates/sysadmin.html @@ -0,0 +1,42 @@ +<%inherit file="base.html" /> + +<%block name="content"> +
+ % if action=="delete": + Really delete ${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'), From f6b47992c8c0e48d402737b0454e093c2cb99fd6 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 7 Oct 2013 21:16:19 -0400 Subject: [PATCH 2/7] address jkarni's comments on studio sysadmin panel PR --- cms/djangoapps/contentstore/views/sysadmin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views/sysadmin.py b/cms/djangoapps/contentstore/views/sysadmin.py index 7ceeb3f90f71..5278fbf2d161 100644 --- a/cms/djangoapps/contentstore/views/sysadmin.py +++ b/cms/djangoapps/contentstore/views/sysadmin.py @@ -17,6 +17,9 @@ sysadmin """ + +@login_required +@ensure_csrf_cookie def sysadmin(request): """ sysadmin page: for now, just list courses and allow deletion @@ -92,7 +95,6 @@ def sysadmin(request): cinfo = db.modulestore.find_one({'_id.course':course, '_id.category':'course'}) or {} id = cinfo.get('_id',cinfo) name = id.get('name',id) - #print "course %s: %s" % (course, name) ctab[course] = name idtab[course] = id From d9cfa9f2f7a34e3a93f7b10e2b23c7789fe9d8a1 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 16 Oct 2013 21:24:33 -0400 Subject: [PATCH 3/7] add internationalization support --- cms/djangoapps/contentstore/views/__init__.py | 1 - cms/djangoapps/contentstore/views/sysadmin.py | 17 +++++++++-------- cms/templates/sysadmin.html | 15 ++++++++------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py index e5545840c4bc..5a1b942efc0b 100644 --- a/cms/djangoapps/contentstore/views/__init__.py +++ b/cms/djangoapps/contentstore/views/__init__.py @@ -16,7 +16,6 @@ from .public import * from .user import * from .tabs import * -from .requests import * from .sysadmin import * try: diff --git a/cms/djangoapps/contentstore/views/sysadmin.py b/cms/djangoapps/contentstore/views/sysadmin.py index 5278fbf2d161..8f52916e09a2 100644 --- a/cms/djangoapps/contentstore/views/sysadmin.py +++ b/cms/djangoapps/contentstore/views/sysadmin.py @@ -2,10 +2,11 @@ import logging from django_future.csrf import ensure_csrf_cookie -from django.core.context_processors import csrf +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 mitxmako.shortcuts import render_to_response @@ -38,15 +39,15 @@ def sysadmin(request): if action=='delete': if not course_id: - msg += "Error - no course specified" + msg += "{0}".format(_('Error - no course specified')) else: nrec = db.modulestore.find({'_id.course': course_id}).count() if not 'really' in request.GET: - msg += "Really delete course %s?\n" % course_id - msg += "%d records for this course in the database" % nrec + 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 %s" % course_id + msg += _("deleting {0}").format(course_id) data = db.modulestore.find({'_id.course': course_id}) fn = 'course-%s-dump-%s.json' % (course_id, time.ctime(time.time()).replace(' ','_')) fp = open('%s/%s' % (bdir,fn), 'w') @@ -54,13 +55,13 @@ def sysadmin(request): fp.write(json.dumps(d)+'\n') fp.close() db.modulestore.remove({'_id.course': course_id}) - msg += "%d records for %s removed (backup file %s)" % (nrec, course_id, fn) + 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 += "Error - no course specified" + msg += "{0}".format(_("Error - no course specified")) else: data = db.modulestore.find({'_id.course': course_id}) response = HttpResponse(mimetype='text/json') @@ -82,7 +83,7 @@ def sysadmin(request): ret = subprocess.Popen(cmd, shell=True, executable = "/bin/bash", stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() ret = ''.join(ret) - msg = "Added course from {0}".format(giturl) + msg = "{0} {1}".format(_("Added course from"), giturl) msg += "
{0}
".format(ret.replace('<','<')) #----------------------------------------------------------------------------- diff --git a/cms/templates/sysadmin.html b/cms/templates/sysadmin.html index d2d0d1ddf331..b654fcbe6221 100644 --- a/cms/templates/sysadmin.html +++ b/cms/templates/sysadmin.html @@ -1,11 +1,12 @@ <%inherit file="base.html" /> +<%! from django.utils.translation import ugettext as _ %> <%block name="content"> \ No newline at end of file From 5ae00e1800945604c69cb0e758a65dfe38cfd2cb Mon Sep 17 00:00:00 2001 From: ichuang Date: Wed, 16 Oct 2013 21:51:47 -0400 Subject: [PATCH 4/7] add settings conf for DELETED_COURSE_BACKUPS_DIR --- cms/djangoapps/contentstore/views/sysadmin.py | 11 ++++++----- cms/envs/common.py | 5 +++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/cms/djangoapps/contentstore/views/sysadmin.py b/cms/djangoapps/contentstore/views/sysadmin.py index 8f52916e09a2..a054869a8c27 100644 --- a/cms/djangoapps/contentstore/views/sysadmin.py +++ b/cms/djangoapps/contentstore/views/sysadmin.py @@ -32,7 +32,7 @@ def sysadmin(request): db = client.xmodule msg = '' - bdir = "DATA-BACKUP" + bdir = settings.getattr('DELETED_COURSE_BACKUPS_DIR', None) action = request.GET.get('action', request.POST.get('action', '')) course_id = request.GET.get('course_id', '') @@ -50,10 +50,11 @@ def sysadmin(request): msg += _("deleting {0}").format(course_id) data = db.modulestore.find({'_id.course': course_id}) fn = 'course-%s-dump-%s.json' % (course_id, time.ctime(time.time()).replace(' ','_')) - fp = open('%s/%s' % (bdir,fn), 'w') - for d in data: - fp.write(json.dumps(d)+'\n') - fp.close() + if bdir is not None: + fp = open('%s/%s' % (bdir,fn), 'w') + for d in data: + fp.write(json.dumps(d)+'\n') + fp.close() db.modulestore.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) diff --git a/cms/envs/common.py b/cms/envs/common.py index 05a1b63f5f8d..0cb0c0f9999f 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -81,6 +81,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. From 46e84f366f3ecd8ea0fab981e548f4d37ccb24a0 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 16 Oct 2013 22:36:25 -0400 Subject: [PATCH 5/7] fix getattr --- cms/djangoapps/contentstore/views/sysadmin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views/sysadmin.py b/cms/djangoapps/contentstore/views/sysadmin.py index a054869a8c27..4756d710150e 100644 --- a/cms/djangoapps/contentstore/views/sysadmin.py +++ b/cms/djangoapps/contentstore/views/sysadmin.py @@ -32,7 +32,7 @@ def sysadmin(request): db = client.xmodule msg = '' - bdir = settings.getattr('DELETED_COURSE_BACKUPS_DIR', None) + bdir = getattr(settings, 'DELETED_COURSE_BACKUPS_DIR', None) action = request.GET.get('action', request.POST.get('action', '')) course_id = request.GET.get('course_id', '') From e3048036d276ae0d7f8215f466f1375b5fc82da8 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 17 Oct 2013 21:22:56 -0400 Subject: [PATCH 6/7] use modulestore for configurability --- cms/djangoapps/contentstore/views/sysadmin.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/cms/djangoapps/contentstore/views/sysadmin.py b/cms/djangoapps/contentstore/views/sysadmin.py index 4756d710150e..4fb7ef3cf4db 100644 --- a/cms/djangoapps/contentstore/views/sysadmin.py +++ b/cms/djangoapps/contentstore/views/sysadmin.py @@ -8,9 +8,11 @@ from django.http import HttpResponse from django.utils.translation import ugettext as _ +from xmodule.modulestore.django import modulestore + from mitxmako.shortcuts import render_to_response -import pymongo +#import pymongo import time import json @@ -27,9 +29,8 @@ def sysadmin(request): """ if (not request.user) or (not request.user.is_staff): return redirect('login') - - client = pymongo.MongoClient() - db = client.xmodule + + collection = modulestore().collection msg = '' bdir = getattr(settings, 'DELETED_COURSE_BACKUPS_DIR', None) @@ -41,21 +42,21 @@ def sysadmin(request): if not course_id: msg += "{0}".format(_('Error - no course specified')) else: - nrec = db.modulestore.find({'_id.course': course_id}).count() + 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 = db.modulestore.find({'_id.course': course_id}) + 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: fp = open('%s/%s' % (bdir,fn), 'w') for d in data: fp.write(json.dumps(d)+'\n') fp.close() - db.modulestore.remove({'_id.course': course_id}) + 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 = "" @@ -64,11 +65,11 @@ def sysadmin(request): if not course_id: msg += "{0}".format(_("Error - no course specified")) else: - data = db.modulestore.find({'_id.course': course_id}) + 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 = db.modulestore.find({'_id.course': course_id}) + data = collection.find({'_id.course': course_id}) for d in data: response.write(json.dumps(d)+'\n') return response @@ -92,9 +93,9 @@ def sysadmin(request): ctab = {} idtab = {} - courses = db.modulestore.distinct('_id.course') + courses = collection.distinct('_id.course') for course in courses: - cinfo = db.modulestore.find_one({'_id.course':course, '_id.category':'course'}) or {} + cinfo = collection.find_one({'_id.course':course, '_id.category':'course'}) or {} id = cinfo.get('_id',cinfo) name = id.get('name',id) ctab[course] = name From b7d70b7bd4a0f4094f3151330b208e07b072432b Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 17 Oct 2013 21:52:20 -0400 Subject: [PATCH 7/7] add FEATURE FLAG to enable/disable sysadmin page --- cms/djangoapps/contentstore/views/sysadmin.py | 19 +++++++++++-------- cms/envs/common.py | 3 +++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/cms/djangoapps/contentstore/views/sysadmin.py b/cms/djangoapps/contentstore/views/sysadmin.py index 4fb7ef3cf4db..e9addcbf2053 100644 --- a/cms/djangoapps/contentstore/views/sysadmin.py +++ b/cms/djangoapps/contentstore/views/sysadmin.py @@ -1,3 +1,7 @@ +""" +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 @@ -7,6 +11,7 @@ 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 @@ -16,10 +21,6 @@ import time import json -""" -sysadmin -""" - @login_required @ensure_csrf_cookie @@ -30,6 +31,9 @@ def sysadmin(request): 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 = '' @@ -52,10 +56,9 @@ def sysadmin(request): 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: - fp = open('%s/%s' % (bdir,fn), 'w') - for d in data: - fp.write(json.dumps(d)+'\n') - fp.close() + 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) diff --git a/cms/envs/common.py b/cms/envs/common.py index 0cb0c0f9999f..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