diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c42d039..987d144 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 diff --git a/cms/djangoapps/auth/authz.py b/cms/djangoapps/auth/authz.py index 23dde88..0f2e60d 100644 --- a/cms/djangoapps/auth/authz.py +++ b/cms/djangoapps/auth/authz.py @@ -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) + + +def get_users_with_staff_role(): + """ + Returns all users with the role 'staff' + """ + return _get_users_with_role(STAFF_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_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 diff --git a/cms/djangoapps/auth/tests/test_authz.py b/cms/djangoapps/auth/tests/test_authz.py index 658c176..e04c108 100644 --- a/cms/djangoapps/auth/tests/test_authz.py +++ b/cms/djangoapps/auth/tests/test_authz.py @@ -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] - - 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)) - - admin = User.objects.create_user('populate_creators_command', 'grant+creator+access@edx.org', 'foo') - admin.is_staff = True - _grant_instructors_creator_access(admin) - - # 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' + 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) + + self.assertSetEqual({self.staff, staff2, self.creator}, get_users_with_staff_role()) + + 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) + + 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()) diff --git a/cms/djangoapps/contentstore/management/commands/populate_creators.py b/cms/djangoapps/contentstore/management/commands/populate_creators.py index f627df8..90d8b3c 100644 --- a/cms/djangoapps/contentstore/management/commands/populate_creators.py +++ b/cms/djangoapps/contentstore/management/commands/populate_creators.py @@ -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() diff --git a/cms/djangoapps/course_creators/__init__.py b/cms/djangoapps/course_creators/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/cms/djangoapps/course_creators/__init__.py diff --git a/cms/djangoapps/course_creators/admin.py b/cms/djangoapps/course_creators/admin.py new file mode 100644 index 0000000..7518946 --- /dev/null +++ b/cms/djangoapps/course_creators/admin.py @@ -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']) diff --git a/cms/djangoapps/course_creators/migrations/0001_initial.py b/cms/djangoapps/course_creators/migrations/0001_initial.py new file mode 100644 index 0000000..ec7fd9a --- /dev/null +++ b/cms/djangoapps/course_creators/migrations/0001_initial.py @@ -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'] \ No newline at end of file diff --git a/cms/djangoapps/course_creators/migrations/__init__.py b/cms/djangoapps/course_creators/migrations/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/cms/djangoapps/course_creators/migrations/__init__.py diff --git a/cms/djangoapps/course_creators/models.py b/cms/djangoapps/course_creators/models.py new file mode 100644 index 0000000..607dae4 --- /dev/null +++ b/cms/djangoapps/course_creators/models.py @@ -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() diff --git a/cms/djangoapps/course_creators/tests/test_admin.py b/cms/djangoapps/course_creators/tests/test_admin.py new file mode 100644 index 0000000..6ef4874 --- /dev/null +++ b/cms/djangoapps/course_creators/tests/test_admin.py @@ -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)) diff --git a/cms/djangoapps/course_creators/tests/test_views.py b/cms/djangoapps/course_creators/tests/test_views.py new file mode 100644 index 0000000..bd91208 --- /dev/null +++ b/cms/djangoapps/course_creators/tests/test_views.py @@ -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)) diff --git a/cms/djangoapps/course_creators/views.py b/cms/djangoapps/course_creators/views.py new file mode 100644 index 0000000..902406e --- /dev/null +++ b/cms/djangoapps/course_creators/views.py @@ -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() diff --git a/cms/envs/common.py b/cms/envs/common.py index 87c130a..c8840e9 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -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 ################################## diff --git a/cms/envs/test.py b/cms/envs/test.py index 86925ca..6479c93 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -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 diff --git a/cms/urls.py b/cms/urls.py index d04c311..1bd9850 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -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 diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index 844ddb5..3e36016 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -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') diff --git a/rakelib/django.rake b/rakelib/django.rake index b1adf24..eeb8135 100644 --- a/rakelib/django.rake +++ b/rakelib/django.rake @@ -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"