Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cms/djangoapps/contentstore/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from .public import *
from .user import *
from .tabs import *
from .sysadmin import *

try:
from .dev import *
except ImportError:
Expand Down
113 changes: 113 additions & 0 deletions cms/djangoapps/contentstore/views/sysadmin.py
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a module level docstring describing what functionality this set of views implements.


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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ensure_csrf?

"""
sysadmin page: for now, just list courses and allow deletion
"""
if (not request.user) or (not request.user.is_staff):
return redirect('login')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this view is @login_required, request.user will always be set.


if not settings.MITX_FEATURES.get('ENABLE_MITX_SYSADMIN_PAGE', False):
raise Http404

collection = modulestore().collection

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will still break when we move to split mongo


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':

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this use a request with method == DELETE?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no - it's not a RESTful API; this action is from a button

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This style of if ... elif ... elif is very hard to test. If, instead, each of the actions were in a separate function, it would be much easier to tell what information the action needed in order to execute, and also much easier to test each action in isolation (without requiring posts through the UI, or creating a full request, or the like.

if not course_id:
msg += "<font color='red'>{0}</font>".format(_('Error - no course specified'))
else:
nrec = collection.find({'_id.course': course_id}).count()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will still delete courses from other orgs

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 += "<font color='red'>{0}</font>".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 = "<font color='red'>{0} {1}</font>".format(_("Added course from"), giturl)
msg += "<pre>{0}</pre>".format(ret.replace('<','&lt;'))

#-----------------------------------------------------------------------------
# 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)
8 changes: 8 additions & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down
43 changes: 43 additions & 0 deletions cms/templates/sysadmin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<%inherit file="base.html" />
<%! from django.utils.translation import ugettext as _ %>

<%block name="content">
<article>
% if action=="delete":
<font color="red">${_("Really delete {0}").format(course_id)}?</font>
<a href="/sysadmin">No (Cancel)</a>
<a style="float:right" href="/sysadmin?action=delete&course_id=${course_id}&really=1">${_("Yes")}&nbsp;&nbsp;</a>
% endif

<pre>
${msg}
</pre>
<hr width="50%"/>
<table border="1">
% for cid in sorted(ctab.iterkeys()):
<%
id = idtab[cid]
org = id.get('org','')
name = id.get('name','')
nameurl = name.replace(' ','_')
%>
<tr>
<td><a href="/${org}/${cid}/course/${nameurl}">${_("GO")}</a>&nbsp;| </td>
<td><a href="/${org}/${cid}/generate_export/${nameurl}">${_("tar.gz export")}</a>&nbsp; |</td>
<td><a href="/sysadmin?action=dump&course_id=${cid}">${_("db dump")}</a>&nbsp; |</td>
<td><a href="/sysadmin?action=delete&course_id=${cid}">${_("delete")}</a>&nbsp; |</td>
<td>${cid}</td>
<td>${name}</td>
</tr>
% endfor
</table>

<br/>
<hr width="75%" />
<form name="sysadminform" method="POST" action="/sysadmin">
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
git ssh url: <input size="60" name="giturl" />
<input type="submit" name="action" value="${_("Add Course")}"/>
</form>
</article>
</%block>
1 change: 1 addition & 0 deletions cms/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<org>[^/]+)/(?P<course>[^/]+)/import/(?P<name>[^/]+)$',
'contentstore.views.import_course', name='import_course'),
Expand Down