Merge remote-tracking branch 'origin/master' into feature/cale/cms-master
Conflicts: cms/djangoapps/contentstore/views.py
This commit is contained in:
0
cms/djangoapps/auth/__init__.py
Normal file
0
cms/djangoapps/auth/__init__.py
Normal file
94
cms/djangoapps/auth/authz.py
Normal file
94
cms/djangoapps/auth/authz.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
# define a couple of simple roles, we just need ADMIN and EDITOR now for our purposes
|
||||
ADMIN_ROLE_NAME = 'admin'
|
||||
EDITOR_ROLE_NAME = 'editor'
|
||||
|
||||
# we're just making a Django group for each location/role combo
|
||||
# to do this we're just creating a Group name which is a formatted string
|
||||
# of those two variables
|
||||
def get_course_groupname_for_role(location, role):
|
||||
loc = Location(location)
|
||||
groupname = loc.course_id + ':' + role
|
||||
return groupname
|
||||
|
||||
def get_users_in_course_group_by_role(location, role):
|
||||
groupname = get_course_groupname_for_role(location, role)
|
||||
group = Group.objects.get(name=groupname)
|
||||
return group.user_set.all()
|
||||
|
||||
|
||||
'''
|
||||
Create all permission groups for a new course and subscribe the caller into those roles
|
||||
'''
|
||||
def create_all_course_groups(creator, location):
|
||||
create_new_course_group(creator, location, ADMIN_GROUP_NAME)
|
||||
create_new_course_group(creator, location, EDITOR_GROUP_NAME)
|
||||
|
||||
|
||||
def create_new_course_group(creator, location, role):
|
||||
groupname = get_course_groupname_for_role(location, role)
|
||||
(group, created) =Group.get_or_create(name=groupname)
|
||||
if created:
|
||||
group.save()
|
||||
|
||||
creator.groups.add(group)
|
||||
creator.save()
|
||||
|
||||
return
|
||||
|
||||
|
||||
def add_user_to_course_group(caller, user, location, role):
|
||||
# only admins can add/remove other users
|
||||
if not is_user_in_course_group_role(caller, location, ADMIN_ROLE_NAME):
|
||||
raise PermissionDenied
|
||||
|
||||
if user.is_active and user.is_authenticated:
|
||||
groupname = get_course_groupname_for_role(location, role)
|
||||
|
||||
group = Group.objects.get(name=groupname)
|
||||
user.groups.add(group)
|
||||
user.save()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_user_by_email(email):
|
||||
user = None
|
||||
# try to look up user, return None if not found
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
except:
|
||||
pass
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def remove_user_from_course_group(caller, user, location, role):
|
||||
# only admins can add/remove other users
|
||||
if not is_user_in_course_group_role(caller, location, ADMIN_ROLE_NAME):
|
||||
raise PermissionDenied
|
||||
|
||||
# see if the user is actually in that role, if not then we don't have to do anything
|
||||
if is_user_in_course_group_role(user, location, role) == True:
|
||||
groupname = get_course_groupname_for_role(location, role)
|
||||
|
||||
group = Group.objects.get(name=groupname)
|
||||
user.groups.remove(group)
|
||||
user.save()
|
||||
|
||||
|
||||
def is_user_in_course_group_role(user, location, role):
|
||||
if user.is_active and user.is_authenticated:
|
||||
return user.groups.filter(name=get_course_groupname_for_role(location,role)).count() > 0
|
||||
|
||||
return False
|
||||
|
||||
|
||||
31
cms/djangoapps/contentstore/utils.py
Normal file
31
cms/djangoapps/contentstore/utils.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
'''
|
||||
cdodge: for a given Xmodule, return the course that it belongs to
|
||||
NOTE: This makes a lot of assumptions about the format of the course location
|
||||
Also we have to assert that this module maps to only one course item - it'll throw an
|
||||
assert if not
|
||||
'''
|
||||
def get_course_location_for_item(location):
|
||||
item_loc = Location(location)
|
||||
|
||||
# check to see if item is already a course, if so we can skip this
|
||||
if item_loc.category != 'course':
|
||||
# @hack! We need to find the course location however, we don't
|
||||
# know the 'name' parameter in this context, so we have
|
||||
# to assume there's only one item in this query even though we are not specifying a name
|
||||
course_search_location = ['i4x', item_loc.org, item_loc.course, 'course', None]
|
||||
courses = modulestore().get_items(course_search_location)
|
||||
|
||||
# make sure we found exactly one match on this above course search
|
||||
found_cnt = len(courses)
|
||||
if found_cnt == 0:
|
||||
raise BaseException('Could not find course at {0}'.format(course_search_location))
|
||||
|
||||
if found_cnt > 1:
|
||||
raise BaseException('Found more than one course at {0}. There should only be one!!!'.format(course_search_location))
|
||||
|
||||
location = courses[0].location
|
||||
|
||||
return location
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
import sys
|
||||
import mimetypes
|
||||
import StringIO
|
||||
import exceptions
|
||||
from collections import defaultdict
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -13,6 +14,7 @@ from PIL import Image
|
||||
|
||||
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.context_processors import csrf
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -37,9 +39,11 @@ from operator import attrgetter
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
|
||||
#from django.core.cache import cache
|
||||
|
||||
from cache_toolbox.core import set_cached_content, get_cached_content, del_cached_content
|
||||
from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
|
||||
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
|
||||
from auth.authz import ADMIN_ROLE_NAME, EDITOR_ROLE_NAME
|
||||
from .utils import get_course_location_for_item
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -76,6 +80,10 @@ def index(request):
|
||||
List all courses available to the logged in user
|
||||
"""
|
||||
courses = modulestore().get_items(['i4x', None, None, 'course', None])
|
||||
|
||||
# filter out courses that we don't have access to
|
||||
courses = filter(lambda course: has_access(request.user, course.location), courses)
|
||||
|
||||
return render_to_response('index.html', {
|
||||
'courses': [(course.metadata.get('display_name'),
|
||||
reverse('course_index', args=[
|
||||
@@ -88,10 +96,10 @@ def index(request):
|
||||
|
||||
# ==== Views with per-item permissions================================
|
||||
|
||||
def has_access(user, location):
|
||||
def has_access(user, location, role=EDITOR_ROLE_NAME):
|
||||
'''Return True if user allowed to access this piece of data'''
|
||||
# TODO (vshnayder): actually check perms
|
||||
return user.is_active and user.is_authenticated
|
||||
'''Note that the CMS permissions model is with respect to courses'''
|
||||
return is_user_in_course_group_role(user, get_course_location_for_item(location), role)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -103,8 +111,10 @@ def course_index(request, org, course, name):
|
||||
org, course, name: Attributes of the Location for the item to edit
|
||||
"""
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise Http404 # TODO (vshnayder): better error
|
||||
raise PermissionDenied()
|
||||
|
||||
upload_asset_callback_url = reverse('upload_asset', kwargs = {
|
||||
'org' : org,
|
||||
@@ -130,9 +140,9 @@ def edit_unit(request, location):
|
||||
|
||||
id: A Location URL
|
||||
"""
|
||||
# TODO (vshnayder): change name from id to location in coffee+html as well.
|
||||
# check that we have permissions to edit this item
|
||||
if not has_access(request.user, location):
|
||||
raise Http404 # TODO (vshnayder): better error
|
||||
raise PermissionDenied()
|
||||
|
||||
item = modulestore().get_item(location)
|
||||
|
||||
@@ -361,8 +371,10 @@ def get_module_previews(request, descriptor):
|
||||
@expect_json
|
||||
def save_item(request):
|
||||
item_location = request.POST['id']
|
||||
|
||||
# check permissions for this user within this course
|
||||
if not has_access(request.user, item_location):
|
||||
raise Http404 # TODO (vshnayder): better error
|
||||
raise PermissionDenied()
|
||||
|
||||
if request.POST['data']:
|
||||
data = request.POST['data']
|
||||
@@ -400,7 +412,7 @@ def clone_item(request):
|
||||
template = Location(request.POST['template'])
|
||||
|
||||
if not has_access(request.user, parent_location):
|
||||
raise Http404 # TODO (vshnayder): better error
|
||||
raise PermissionDenied()
|
||||
|
||||
parent = modulestore().get_item(parent_location)
|
||||
dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
|
||||
@@ -504,3 +516,84 @@ def upload_asset(request, org, course, coursename):
|
||||
logging.error('Failed to generate thumbnail for {0}. Continuing...'.format(name))
|
||||
|
||||
return HttpResponse('Upload completed')
|
||||
|
||||
'''
|
||||
This view will return all CMS users who are editors for the specified course
|
||||
'''
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def manage_users(request, org, course, name):
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location, role=ADMIN_ROLE_NAME):
|
||||
raise PermissionDenied()
|
||||
|
||||
return render_to_response('manage_users.html', {
|
||||
'editors': get_users_in_course_group_by_role(location, EDITOR_ROLE_NAME)
|
||||
})
|
||||
|
||||
|
||||
def create_json_response(errmsg = None):
|
||||
if errmsg is not None:
|
||||
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg' : errmsg}))
|
||||
else:
|
||||
resp = HttpResponse(json.dumps({'Status': 'OK'}))
|
||||
|
||||
return resp
|
||||
|
||||
'''
|
||||
This POST-back view will add a user - specified by email - to the list of editors for
|
||||
the specified course
|
||||
'''
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def add_user(request, org, course, name):
|
||||
email = request.POST["email"]
|
||||
|
||||
if email=='':
|
||||
return create_json_response('Please specify an email address.')
|
||||
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has admin permissions to this course
|
||||
if not has_access(request.user, location, role=ADMIN_ROLE_NAME):
|
||||
raise PermissionDenied()
|
||||
|
||||
user = get_user_by_email(email)
|
||||
|
||||
# user doesn't exist?!? Return error.
|
||||
if user is None:
|
||||
return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
|
||||
|
||||
# user exists, but hasn't activated account?!?
|
||||
if not user.is_active:
|
||||
return create_json_response('User {0} has registered but has not yet activated his/her account.'.format(email))
|
||||
|
||||
# ok, we're cool to add to the course group
|
||||
add_user_to_course_group(request.user, user, location, EDITOR_ROLE_NAME)
|
||||
|
||||
return create_json_response()
|
||||
|
||||
'''
|
||||
This POST-back view will remove a user - specified by email - from the list of editors for
|
||||
the specified course
|
||||
'''
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def remove_user(request, org, course, name):
|
||||
email = request.POST["email"]
|
||||
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has admin permissions on this course
|
||||
if not has_access(request.user, location, role=ADMIN_ROLE_NAME):
|
||||
raise PermissionDenied()
|
||||
|
||||
user = get_user_by_email(email)
|
||||
if user is None:
|
||||
return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
|
||||
|
||||
remove_user_from_course_group(request.user, user, location, EDITOR_ROLE_NAME)
|
||||
|
||||
return create_json_response()
|
||||
|
||||
@@ -282,6 +282,7 @@ INSTALLED_APPS = (
|
||||
|
||||
# For CMS
|
||||
'contentstore',
|
||||
'auth',
|
||||
'github_sync',
|
||||
'student', # misleading name due to sharing with lms
|
||||
|
||||
|
||||
38
cms/templates/manage_users.html
Normal file
38
cms/templates/manage_users.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<%inherit file="base.html" />
|
||||
<%block name="title">Course Editor Manager</%block>
|
||||
<%include file="widgets/header.html"/>
|
||||
|
||||
<%block name="content">
|
||||
<section class="main-container">
|
||||
|
||||
<h2>Course Editors</h2>
|
||||
<ul>
|
||||
% for user in editors:
|
||||
<li>${user.email} (${user.username})</li>
|
||||
% endfor
|
||||
</ul>
|
||||
|
||||
<form action="add_user" id="addEditorsForm">
|
||||
<label>email: </label><input type="text" name="email" placeholder="email@example.com..." />
|
||||
<input type="submit" value="add editor" />
|
||||
</form>
|
||||
<div id="result"></div>
|
||||
|
||||
<script>
|
||||
$("#addEditorsForm").submit(function(event) {
|
||||
event.preventDefault();
|
||||
var $form = $(this),
|
||||
email = $form.find('input[name="email"]').val(),
|
||||
url = $form.attr('action');
|
||||
$.post(url, {email:email},
|
||||
function(data) {
|
||||
if(data['Status'] != 'OK')
|
||||
$("#result").empty().append(data['ErrMsg']);
|
||||
else
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</section>
|
||||
</%block>
|
||||
@@ -20,7 +20,14 @@ urlpatterns = ('',
|
||||
url(r'^preview/modx/(?P<preview_id>[^/]*)/(?P<location>.*?)/(?P<dispatch>[^/]*)$',
|
||||
'contentstore.views.preview_dispatch', name='preview_dispatch'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)/upload_asset$',
|
||||
'contentstore.views.upload_asset', name='upload_asset')
|
||||
'contentstore.views.upload_asset', name='upload_asset'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/manage_users$',
|
||||
'contentstore.views.manage_users', name='manage_users'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/add_user$',
|
||||
'contentstore.views.add_user', name='add_user'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/remove_user$',
|
||||
'contentstore.views.remove_user', name='remove_user')
|
||||
|
||||
)
|
||||
|
||||
# User creation and updating views
|
||||
|
||||
Reference in New Issue
Block a user