Commit bd966321 by Christina Roberts

Merge pull request #345 from edx/christina/course-creator-table

Admin table for course creators.
parents 1dbbbb10 1fd04511
......@@ -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)
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
......@@ -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())
......@@ -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()
"""
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'])
# -*- 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
"""
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()
"""
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))
"""
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))
"""
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"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment