Merge pull request #345 from edx/christina/course-creator-table
Admin table for course creators.
This commit is contained in:
@@ -5,6 +5,10 @@ These are notable changes in edx-platform. This is a rolling list of changes,
|
||||
in roughly chronological order, most recent first. Add your entries at or near
|
||||
the top. Include a label indicating the component affected.
|
||||
|
||||
Studio: Add table for tracking course creator permissions (not yet used).
|
||||
Update rake django-admin[syncdb] and rake django-admin[migrate] so they
|
||||
run for both LMS and CMS.
|
||||
|
||||
Common: Student information is now passed to the tracking log via POST instead of GET.
|
||||
|
||||
Common: Add tests for documentation generation to test suite
|
||||
|
||||
@@ -209,15 +209,27 @@ def is_user_in_creator_group(user):
|
||||
return True
|
||||
|
||||
|
||||
def _grant_instructors_creator_access(caller):
|
||||
def get_users_with_instructor_role():
|
||||
"""
|
||||
This is to be called only by either a command line code path or through an app which has already
|
||||
asserted permissions to do this action.
|
||||
Returns all users with the role 'instructor'
|
||||
"""
|
||||
return _get_users_with_role(INSTRUCTOR_ROLE_NAME)
|
||||
|
||||
Gives all users with instructor role course creator rights.
|
||||
This is only intended to be run once on a given environment.
|
||||
|
||||
def get_users_with_staff_role():
|
||||
"""
|
||||
Returns all users with the role 'staff'
|
||||
"""
|
||||
return _get_users_with_role(STAFF_ROLE_NAME)
|
||||
|
||||
|
||||
def _get_users_with_role(role):
|
||||
"""
|
||||
Returns all users with the specified role.
|
||||
"""
|
||||
users = set()
|
||||
for group in Group.objects.all():
|
||||
if group.name.startswith(INSTRUCTOR_ROLE_NAME + "_"):
|
||||
if group.name.startswith(role + "_"):
|
||||
for user in group.user_set.all():
|
||||
add_user_to_creator_group(caller, user)
|
||||
users.add(user)
|
||||
return users
|
||||
|
||||
@@ -9,7 +9,8 @@ from django.core.exceptions import PermissionDenied
|
||||
|
||||
from auth.authz import add_user_to_creator_group, remove_user_from_creator_group, is_user_in_creator_group,\
|
||||
create_all_course_groups, add_user_to_course_group, STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME,\
|
||||
is_user_in_course_group_role, remove_user_from_course_group, _grant_instructors_creator_access
|
||||
is_user_in_course_group_role, remove_user_from_course_group, get_users_with_staff_role,\
|
||||
get_users_with_instructor_role
|
||||
|
||||
|
||||
class CreatorGroupTest(TestCase):
|
||||
@@ -175,41 +176,27 @@ class CourseGroupTest(TestCase):
|
||||
with self.assertRaises(PermissionDenied):
|
||||
remove_user_from_course_group(self.staff, self.staff, self.location, STAFF_ROLE_NAME)
|
||||
|
||||
def test_get_staff(self):
|
||||
# Do this test with staff in 2 different classes.
|
||||
create_all_course_groups(self.creator, self.location)
|
||||
add_user_to_course_group(self.creator, self.staff, self.location, STAFF_ROLE_NAME)
|
||||
|
||||
class GrantInstructorsCreatorAccessTest(TestCase):
|
||||
"""
|
||||
Tests granting existing instructors course creator rights.
|
||||
"""
|
||||
def create_course(self, index):
|
||||
"""
|
||||
Creates a course with one instructor and one staff member.
|
||||
"""
|
||||
creator = User.objects.create_user('testcreator' + str(index), 'testcreator+courses@edx.org', 'foo')
|
||||
staff = User.objects.create_user('teststaff' + str(index), 'teststaff+courses@edx.org', 'foo')
|
||||
location = 'i4x', 'mitX', str(index), 'course', 'test'
|
||||
create_all_course_groups(creator, location)
|
||||
add_user_to_course_group(creator, staff, location, STAFF_ROLE_NAME)
|
||||
return [creator, staff]
|
||||
location2 = 'i4x', 'mitX', '103', 'course2', 'test2'
|
||||
staff2 = User.objects.create_user('teststaff2', 'teststaff2+courses@edx.org', 'foo')
|
||||
create_all_course_groups(self.creator, location2)
|
||||
add_user_to_course_group(self.creator, staff2, location2, STAFF_ROLE_NAME)
|
||||
|
||||
def test_grant_creator_access(self):
|
||||
"""
|
||||
Test for _grant_instructors_creator_access.
|
||||
"""
|
||||
[creator1, staff1] = self.create_course(1)
|
||||
[creator2, staff2] = self.create_course(2)
|
||||
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}):
|
||||
# Initially no creators.
|
||||
self.assertFalse(is_user_in_creator_group(creator1))
|
||||
self.assertFalse(is_user_in_creator_group(creator2))
|
||||
self.assertFalse(is_user_in_creator_group(staff1))
|
||||
self.assertFalse(is_user_in_creator_group(staff2))
|
||||
self.assertSetEqual({self.staff, staff2, self.creator}, get_users_with_staff_role())
|
||||
|
||||
admin = User.objects.create_user('populate_creators_command', 'grant+creator+access@edx.org', 'foo')
|
||||
admin.is_staff = True
|
||||
_grant_instructors_creator_access(admin)
|
||||
def test_get_instructor(self):
|
||||
# Do this test with creators in 2 different classes.
|
||||
create_all_course_groups(self.creator, self.location)
|
||||
add_user_to_course_group(self.creator, self.staff, self.location, STAFF_ROLE_NAME)
|
||||
|
||||
# Now instructors only are creators.
|
||||
self.assertTrue(is_user_in_creator_group(creator1))
|
||||
self.assertTrue(is_user_in_creator_group(creator2))
|
||||
self.assertFalse(is_user_in_creator_group(staff1))
|
||||
self.assertFalse(is_user_in_creator_group(staff2))
|
||||
location2 = 'i4x', 'mitX', '103', 'course2', 'test2'
|
||||
creator2 = User.objects.create_user('testcreator2', 'testcreator2+courses@edx.org', 'foo')
|
||||
staff2 = User.objects.create_user('teststaff2', 'teststaff2+courses@edx.org', 'foo')
|
||||
create_all_course_groups(creator2, location2)
|
||||
add_user_to_course_group(creator2, staff2, location2, STAFF_ROLE_NAME)
|
||||
|
||||
self.assertSetEqual({self.creator, creator2}, get_users_with_instructor_role())
|
||||
|
||||
@@ -3,7 +3,8 @@ Script for granting existing course instructors course creator privileges.
|
||||
|
||||
This script is only intended to be run once on a given environment.
|
||||
"""
|
||||
from auth.authz import _grant_instructors_creator_access
|
||||
from auth.authz import get_users_with_instructor_role, get_users_with_staff_role
|
||||
from course_creators.views import add_user_with_status_granted, add_user_with_status_unrequested
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
@@ -31,5 +32,17 @@ class Command(BaseCommand):
|
||||
# the admin user will already exist.
|
||||
admin = User.objects.get(username=username, email=email)
|
||||
|
||||
_grant_instructors_creator_access(admin)
|
||||
for user in get_users_with_instructor_role():
|
||||
add_user_with_status_granted(admin, user)
|
||||
|
||||
# Some users will be both staff and instructors. Those folks have been
|
||||
# added with status granted above, and add_user_with_status_unrequested
|
||||
# will not try to add them again if they already exist in the course creator database.
|
||||
for user in get_users_with_staff_role():
|
||||
add_user_with_status_unrequested(admin, user)
|
||||
|
||||
# There could be users who are not in either staff or instructor (they've
|
||||
# never actually done anything in Studio). I plan to add those as unrequested
|
||||
# when they first go to their dashboard.
|
||||
|
||||
admin.delete()
|
||||
|
||||
0
cms/djangoapps/course_creators/__init__.py
Normal file
0
cms/djangoapps/course_creators/__init__.py
Normal file
63
cms/djangoapps/course_creators/admin.py
Normal file
63
cms/djangoapps/course_creators/admin.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
django admin page for the course creators table
|
||||
"""
|
||||
|
||||
from course_creators.models import CourseCreator, update_creator_state
|
||||
from course_creators.views import update_course_creator_group
|
||||
|
||||
from django.contrib import admin
|
||||
from django.dispatch import receiver
|
||||
|
||||
|
||||
def get_email(obj):
|
||||
""" Returns the email address for a user """
|
||||
return obj.user.email
|
||||
|
||||
get_email.short_description = 'email'
|
||||
|
||||
|
||||
class CourseCreatorAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin for the course creator table.
|
||||
"""
|
||||
|
||||
# Fields to display on the overview page.
|
||||
list_display = ['user', get_email, 'state', 'state_changed', 'note']
|
||||
readonly_fields = ['user', 'state_changed']
|
||||
# Controls the order on the edit form (without this, read-only fields appear at the end).
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ['user', 'state', 'state_changed', 'note']
|
||||
}),
|
||||
)
|
||||
# Fields that filtering support
|
||||
list_filter = ['state', 'state_changed']
|
||||
# Fields that search supports.
|
||||
search_fields = ['user__username', 'user__email', 'state', 'note']
|
||||
# Turn off the action bar (we have no bulk actions)
|
||||
actions = None
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return request.user.is_staff
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
# Store who is making the request.
|
||||
obj.admin = request.user
|
||||
obj.save()
|
||||
|
||||
|
||||
admin.site.register(CourseCreator, CourseCreatorAdmin)
|
||||
|
||||
|
||||
@receiver(update_creator_state, sender=CourseCreator)
|
||||
def update_creator_group_callback(sender, **kwargs):
|
||||
"""
|
||||
Callback for when the model's creator status has changed.
|
||||
"""
|
||||
update_course_creator_group(kwargs['caller'], kwargs['user'], kwargs['add'])
|
||||
74
cms/djangoapps/course_creators/migrations/0001_initial.py
Normal file
74
cms/djangoapps/course_creators/migrations/0001_initial.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding model 'CourseCreator'
|
||||
db.create_table('course_creators_coursecreator', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], unique=True)),
|
||||
('state_changed', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
|
||||
('state', self.gf('django.db.models.fields.CharField')(default='unrequested', max_length=24)),
|
||||
('note', self.gf('django.db.models.fields.CharField')(max_length=512, blank=True)),
|
||||
))
|
||||
db.send_create_signal('course_creators', ['CourseCreator'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'CourseCreator'
|
||||
db.delete_table('course_creators_coursecreator')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'course_creators.coursecreator': {
|
||||
'Meta': {'object_name': 'CourseCreator'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'note': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}),
|
||||
'state': ('django.db.models.fields.CharField', [], {'default': "'unrequested'", 'max_length': '24'}),
|
||||
'state_changed': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['course_creators']
|
||||
71
cms/djangoapps/course_creators/models.py
Normal file
71
cms/djangoapps/course_creators/models.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Table for storing information about whether or not Studio users have course creation privileges.
|
||||
"""
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_init, post_save
|
||||
from django.dispatch import receiver, Signal
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
# A signal that will be sent when users should be added or removed from the creator group
|
||||
update_creator_state = Signal(providing_args=["caller", "user", "add"])
|
||||
|
||||
|
||||
class CourseCreator(models.Model):
|
||||
"""
|
||||
Creates the database table model.
|
||||
"""
|
||||
UNREQUESTED = 'unrequested'
|
||||
PENDING = 'pending'
|
||||
GRANTED = 'granted'
|
||||
DENIED = 'denied'
|
||||
|
||||
# Second value is the "human-readable" version.
|
||||
STATES = (
|
||||
(UNREQUESTED, _(u'unrequested')),
|
||||
(PENDING, _(u'pending')),
|
||||
(GRANTED, _(u'granted')),
|
||||
(DENIED, _(u'denied')),
|
||||
)
|
||||
|
||||
user = models.ForeignKey(User, help_text=_("Studio user"), unique=True)
|
||||
state_changed = models.DateTimeField('state last updated', auto_now_add=True,
|
||||
help_text=_("The date when state was last updated"))
|
||||
state = models.CharField(max_length=24, blank=False, choices=STATES, default=UNREQUESTED,
|
||||
help_text=_("Current course creator state"))
|
||||
note = models.CharField(max_length=512, blank=True, help_text=_("Optional notes about this user (for example, "
|
||||
"why course creation access was denied)"))
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%str | %str [%str] | %str' % (self.user, self.state, self.state_changed, self.note)
|
||||
|
||||
|
||||
@receiver(post_init, sender=CourseCreator)
|
||||
def post_init_callback(sender, **kwargs):
|
||||
"""
|
||||
Extend to store previous state.
|
||||
"""
|
||||
instance = kwargs['instance']
|
||||
instance.orig_state = instance.state
|
||||
|
||||
|
||||
@receiver(post_save, sender=CourseCreator)
|
||||
def post_save_callback(sender, **kwargs):
|
||||
"""
|
||||
Extend to update state_changed time and modify the course creator group in authz.py.
|
||||
"""
|
||||
instance = kwargs['instance']
|
||||
# We only wish to modify the state_changed time if the state has been modified. We don't wish to
|
||||
# modify it for changes to the notes field.
|
||||
if instance.state != instance.orig_state:
|
||||
update_creator_state.send(
|
||||
sender=sender,
|
||||
caller=instance.admin,
|
||||
user=instance.user,
|
||||
add=instance.state == CourseCreator.GRANTED
|
||||
)
|
||||
instance.state_changed = timezone.now()
|
||||
instance.orig_state = instance.state
|
||||
instance.save()
|
||||
80
cms/djangoapps/course_creators/tests/test_admin.py
Normal file
80
cms/djangoapps/course_creators/tests/test_admin.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
Tests course_creators.admin.py.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from django.http import HttpRequest
|
||||
import mock
|
||||
|
||||
from course_creators.admin import CourseCreatorAdmin
|
||||
from course_creators.models import CourseCreator
|
||||
from auth.authz import is_user_in_creator_group
|
||||
|
||||
|
||||
class CourseCreatorAdminTest(TestCase):
|
||||
"""
|
||||
Tests for course creator admin.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
""" Test case setup """
|
||||
self.user = User.objects.create_user('test_user', 'test_user+courses@edx.org', 'foo')
|
||||
self.table_entry = CourseCreator(user=self.user)
|
||||
self.table_entry.save()
|
||||
|
||||
self.admin = User.objects.create_user('Mark', 'admin+courses@edx.org', 'foo')
|
||||
self.admin.is_staff = True
|
||||
|
||||
self.request = HttpRequest()
|
||||
self.request.user = self.admin
|
||||
|
||||
self.creator_admin = CourseCreatorAdmin(self.table_entry, AdminSite())
|
||||
|
||||
def test_change_status(self):
|
||||
"""
|
||||
Tests that updates to state impact the creator group maintained in authz.py.
|
||||
"""
|
||||
def change_state(state, is_creator):
|
||||
""" Helper method for changing state """
|
||||
self.table_entry.state = state
|
||||
self.creator_admin.save_model(self.request, self.table_entry, None, True)
|
||||
self.assertEqual(is_creator, is_user_in_creator_group(self.user))
|
||||
|
||||
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}):
|
||||
# User is initially unrequested.
|
||||
self.assertFalse(is_user_in_creator_group(self.user))
|
||||
|
||||
change_state(CourseCreator.GRANTED, True)
|
||||
|
||||
change_state(CourseCreator.DENIED, False)
|
||||
|
||||
change_state(CourseCreator.GRANTED, True)
|
||||
|
||||
change_state(CourseCreator.PENDING, False)
|
||||
|
||||
change_state(CourseCreator.GRANTED, True)
|
||||
|
||||
change_state(CourseCreator.UNREQUESTED, False)
|
||||
|
||||
def test_add_permission(self):
|
||||
"""
|
||||
Tests that staff cannot add entries
|
||||
"""
|
||||
self.assertFalse(self.creator_admin.has_add_permission(self.request))
|
||||
|
||||
def test_delete_permission(self):
|
||||
"""
|
||||
Tests that staff cannot delete entries
|
||||
"""
|
||||
self.assertFalse(self.creator_admin.has_delete_permission(self.request))
|
||||
|
||||
def test_change_permission(self):
|
||||
"""
|
||||
Tests that only staff can change entries
|
||||
"""
|
||||
self.assertTrue(self.creator_admin.has_change_permission(self.request))
|
||||
|
||||
self.request.user = self.user
|
||||
self.assertFalse(self.creator_admin.has_change_permission(self.request))
|
||||
71
cms/djangoapps/course_creators/tests/test_views.py
Normal file
71
cms/djangoapps/course_creators/tests/test_views.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Tests course_creators.views.py.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
from course_creators.views import add_user_with_status_unrequested, add_user_with_status_granted
|
||||
from course_creators.views import get_course_creator_status, update_course_creator_group
|
||||
from course_creators.models import CourseCreator
|
||||
from auth.authz import is_user_in_creator_group
|
||||
import mock
|
||||
|
||||
|
||||
class CourseCreatorView(TestCase):
|
||||
"""
|
||||
Tests for modifying the course creator table.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
""" Test case setup """
|
||||
self.user = User.objects.create_user('test_user', 'test_user+courses@edx.org', 'foo')
|
||||
self.admin = User.objects.create_user('Mark', 'admin+courses@edx.org', 'foo')
|
||||
self.admin.is_staff = True
|
||||
|
||||
def test_staff_permission_required(self):
|
||||
"""
|
||||
Tests that add methods and course creator group method must be called with staff permissions.
|
||||
"""
|
||||
with self.assertRaises(PermissionDenied):
|
||||
add_user_with_status_granted(self.user, self.user)
|
||||
|
||||
with self.assertRaises(PermissionDenied):
|
||||
add_user_with_status_unrequested(self.user, self.user)
|
||||
|
||||
with self.assertRaises(PermissionDenied):
|
||||
update_course_creator_group(self.user, self.user, True)
|
||||
|
||||
def test_table_initially_empty(self):
|
||||
self.assertIsNone(get_course_creator_status(self.user))
|
||||
|
||||
def test_add_unrequested(self):
|
||||
add_user_with_status_unrequested(self.admin, self.user)
|
||||
self.assertEqual('unrequested', get_course_creator_status(self.user))
|
||||
|
||||
# Calling add again will be a no-op (even if state is different).
|
||||
add_user_with_status_granted(self.admin, self.user)
|
||||
self.assertEqual('unrequested', get_course_creator_status(self.user))
|
||||
|
||||
def test_add_granted(self):
|
||||
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}):
|
||||
# Calling add_user_with_status_granted impacts is_user_in_course_group_role.
|
||||
self.assertFalse(is_user_in_creator_group(self.user))
|
||||
|
||||
add_user_with_status_granted(self.admin, self.user)
|
||||
self.assertEqual('granted', get_course_creator_status(self.user))
|
||||
|
||||
# Calling add again will be a no-op (even if state is different).
|
||||
add_user_with_status_unrequested(self.admin, self.user)
|
||||
self.assertEqual('granted', get_course_creator_status(self.user))
|
||||
|
||||
self.assertTrue(is_user_in_creator_group(self.user))
|
||||
|
||||
def test_update_creator_group(self):
|
||||
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}):
|
||||
self.assertFalse(is_user_in_creator_group(self.user))
|
||||
update_course_creator_group(self.admin, self.user, True)
|
||||
self.assertTrue(is_user_in_creator_group(self.user))
|
||||
update_course_creator_group(self.admin, self.user, False)
|
||||
self.assertFalse(is_user_in_creator_group(self.user))
|
||||
76
cms/djangoapps/course_creators/views.py
Normal file
76
cms/djangoapps/course_creators/views.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Methods for interacting programmatically with the user creator table.
|
||||
"""
|
||||
from course_creators.models import CourseCreator
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
from auth.authz import add_user_to_creator_group, remove_user_from_creator_group
|
||||
|
||||
|
||||
def add_user_with_status_unrequested(caller, user):
|
||||
"""
|
||||
Adds a user to the course creator table with status 'unrequested'.
|
||||
|
||||
If the user is already in the table, this method is a no-op
|
||||
(state will not be changed). Caller must have staff permissions.
|
||||
"""
|
||||
_add_user(caller, user, CourseCreator.UNREQUESTED)
|
||||
|
||||
|
||||
def add_user_with_status_granted(caller, user):
|
||||
"""
|
||||
Adds a user to the course creator table with status 'granted'.
|
||||
|
||||
If the user is already in the table, this method is a no-op
|
||||
(state will not be changed). Caller must have staff permissions.
|
||||
|
||||
This method also adds the user to the course creator group maintained by authz.py.
|
||||
"""
|
||||
_add_user(caller, user, CourseCreator.GRANTED)
|
||||
update_course_creator_group(caller, user, True)
|
||||
|
||||
|
||||
def update_course_creator_group(caller, user, add):
|
||||
"""
|
||||
Method for adding and removing users from the creator group.
|
||||
|
||||
Caller must have staff permissions.
|
||||
"""
|
||||
if add:
|
||||
add_user_to_creator_group(caller, user)
|
||||
else:
|
||||
remove_user_from_creator_group(caller, user)
|
||||
|
||||
|
||||
def get_course_creator_status(user):
|
||||
"""
|
||||
Returns the status for a particular user, or None if user is not in the table.
|
||||
|
||||
Possible return values are:
|
||||
'unrequested' = user has not requested course creation rights
|
||||
'pending' = user has requested course creation rights
|
||||
'granted' = user has been granted course creation rights
|
||||
'denied' = user has been denied course creation rights
|
||||
None = user does not exist in the table
|
||||
"""
|
||||
user = CourseCreator.objects.filter(user=user)
|
||||
if user.count() == 0:
|
||||
return None
|
||||
else:
|
||||
# User is defined to be unique, can assume a single entry.
|
||||
return user[0].state
|
||||
|
||||
|
||||
def _add_user(caller, user, state):
|
||||
"""
|
||||
Adds a user to the course creator table with the specified state.
|
||||
|
||||
If the user is already in the table, this method is a no-op
|
||||
(state will not be changed).
|
||||
"""
|
||||
if not caller.is_active or not caller.is_authenticated or not caller.is_staff:
|
||||
raise PermissionDenied
|
||||
|
||||
if CourseCreator.objects.filter(user=user).count() == 0:
|
||||
entry = CourseCreator(user=user, state=state)
|
||||
entry.save()
|
||||
@@ -331,6 +331,7 @@ INSTALLED_APPS = (
|
||||
# For CMS
|
||||
'contentstore',
|
||||
'auth',
|
||||
'course_creators',
|
||||
'student', # misleading name due to sharing with lms
|
||||
'course_groups', # not used in cms (yet), but tests run
|
||||
|
||||
@@ -345,6 +346,9 @@ INSTALLED_APPS = (
|
||||
|
||||
# comment common
|
||||
'django_comment_common',
|
||||
|
||||
# for course creator table
|
||||
'django.contrib.admin'
|
||||
)
|
||||
|
||||
################# EDX MARKETING SITE ##################################
|
||||
|
||||
@@ -143,3 +143,6 @@ MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True
|
||||
|
||||
# Enabling SQL tracking logs for testing on common/djangoapps/track
|
||||
MITX_FEATURES['ENABLE_SQL_TRACKING_LOGS'] = True
|
||||
|
||||
# This is to disable a test under the common directory that will not pass when run under CMS
|
||||
MITX_FEATURES['DISABLE_PASSWORD_RESET_EMAIL_TEST'] = True
|
||||
|
||||
@@ -5,9 +5,9 @@ from django.conf.urls import patterns, include, url
|
||||
# pylint: disable=W0611
|
||||
from . import one_time_startup
|
||||
|
||||
# Uncomment the next two lines to enable the admin:
|
||||
# from django.contrib import admin
|
||||
# admin.autodiscover()
|
||||
# There is a course creators admin table.
|
||||
from django.contrib import admin
|
||||
admin.autodiscover()
|
||||
|
||||
urlpatterns = ('', # nopep8
|
||||
url(r'^$', 'contentstore.views.howitworks', name='homepage'),
|
||||
@@ -146,6 +146,8 @@ if settings.MITX_FEATURES.get('ENABLE_SERVICE_STATUS'):
|
||||
url(r'^status/', include('service_status.urls')),
|
||||
)
|
||||
|
||||
urlpatterns += (url(r'^admin/', include(admin.site.urls)),)
|
||||
|
||||
urlpatterns = patterns(*urlpatterns)
|
||||
|
||||
# Custom error pages
|
||||
|
||||
@@ -9,15 +9,12 @@ import json
|
||||
import re
|
||||
import unittest
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.hashers import UNUSABLE_PASSWORD
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.template.loader import render_to_string, get_template, TemplateDoesNotExist
|
||||
from django.core.urlresolvers import is_valid_path
|
||||
from django.utils.http import int_to_base36
|
||||
|
||||
|
||||
@@ -33,12 +30,6 @@ COURSE_2 = 'edx/full/6.002_Spring_2012'
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
get_template('registration/password_reset_email.html')
|
||||
project_uses_password_reset = True
|
||||
except TemplateDoesNotExist:
|
||||
project_uses_password_reset = False
|
||||
|
||||
|
||||
class ResetPasswordTests(TestCase):
|
||||
""" Tests that clicking reset password sends email, and doesn't activate the user
|
||||
@@ -75,7 +66,7 @@ class ResetPasswordTests(TestCase):
|
||||
self.assertEquals(bad_email_resp.content, json.dumps({'success': False,
|
||||
'error': 'Invalid e-mail or user'}))
|
||||
|
||||
@unittest.skipUnless(project_uses_password_reset,
|
||||
@unittest.skipUnless(not settings.MITX_FEATURES.get('DISABLE_PASSWORD_RESET_EMAIL_TEST', False),
|
||||
dedent("""Skipping Test because CMS has not provided necessary templates for password reset.
|
||||
If LMS tests print this message, that needs to be fixed."""))
|
||||
@patch('django.core.mail.send_mail')
|
||||
|
||||
@@ -57,18 +57,17 @@ task :resetdb, [:env] do |t, args|
|
||||
sh(django_admin(:lms, args.env, 'migrate'))
|
||||
end
|
||||
|
||||
desc "Update the relational database to the latest migration"
|
||||
task :migrate, [:env] do |t, args|
|
||||
args.with_defaults(:env => 'dev')
|
||||
sh(django_admin(:lms, args.env, 'migrate'))
|
||||
end
|
||||
|
||||
task :runserver => :lms
|
||||
|
||||
desc "Run django-admin <action> against the specified system and environment"
|
||||
task "django-admin", [:action, :system, :env, :options] do |t, args|
|
||||
# If no system was explicitly set, we want to run both CMS and LMS for migrate and syncdb.
|
||||
no_system_set = !args.system
|
||||
args.with_defaults(:env => 'dev', :system => 'lms', :options => '')
|
||||
sh(django_admin(args.system, args.env, args.action, args.options))
|
||||
if no_system_set and (args.action == 'migrate' or args.action == 'syncdb')
|
||||
sh(django_admin('cms', args.env, args.action, args.options))
|
||||
end
|
||||
end
|
||||
|
||||
desc "Set the staff bit for a user"
|
||||
|
||||
Reference in New Issue
Block a user