Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
8da6543
Studio: adds static states for all authorship cases on dashboard (WIP)
talbs Jul 2, 2013
8f5c972
Studio: revises static states for all authorship cases on dashboard (…
talbs Jul 3, 2013
4d9d6a4
Studio: resolves blue primary button default + hover color states
talbs Jul 7, 2013
394292e
Studio: revises static dashboard states for all authorship rights sce…
talbs Jul 7, 2013
a03810e
copy changes
markchang Jul 8, 2013
f8a665e
Studio: revises notice copy to reference UI above, not below
talbs Jul 8, 2013
bcf2742
Studio: overrides authorship ui toggling animation paramters
talbs Jul 8, 2013
f6991a9
Studio: revises dashboard copy when no courses exist for user
talbs Jul 8, 2013
80f827d
Studio: reverting dashboard HTML/UI back to previous state
talbs Jul 10, 2013
52feb5f
Studio - revises dashboard logic to handle authorship cases specifically
talbs Jul 10, 2013
73fb0be
Studio: syncs up denied status naming to styling
talbs Jul 10, 2013
5e9760d
Studio: cleans up outdated Bourbon border-radius mixin from dashboard…
talbs Jul 10, 2013
4165130
Studio - revises dashboard logic to handle authorship cases specifica…
talbs Jul 10, 2013
f911c96
Pass course_creator_status to index.html.
Jul 10, 2013
135d10d
Fix unicode return value.
Jul 10, 2013
aaa67b2
Studio: revises template logic, html, and styling for Dashboard UI st…
talbs Jul 11, 2013
51c9523
Only send a single variable to index.html about authorship rights.
Jul 11, 2013
3715d6c
Only send a single variable to index.html about authorship rights.
Jul 11, 2013
52b5da3
Merge branch 'talbs/studio-authorship' of github.com:edx/edx-platform…
Jul 11, 2013
59850cb
Studio: cleans up new course form and course listings display
talbs Jul 11, 2013
dd2f34a
Studio: cleans up new course form and course listings display
talbs Jul 11, 2013
9b2c384
Add post callback to request course creator status.
Jul 11, 2013
702fb54
Merge branch 'talbs/studio-authorship' of github.com:edx/edx-platform…
Jul 11, 2013
b445737
Studio: revises and syncs up activation message UI with dashboard UI
talbs Jul 11, 2013
c360fae
Studio: revises dashboard course creation button and form interaction
talbs Jul 11, 2013
4ecceb2
Add tests for changing table status without staff permissions.
Jul 12, 2013
d417a78
Tests for course creator status as returned in index page.
Jul 12, 2013
08a1055
Updates to index page text.
Jul 12, 2013
923ba36
Merge branch 'master' into talbs/studio-authorship
Jul 12, 2013
c75041e
Studio: adds in disabled/submitting state and logic for course creati…
talbs Jul 12, 2013
b28b4bf
Studio: abstracts and revises form/notice UI
talbs Jul 12, 2013
e1c02b1
Studio: removing preventDefault from course creator request button
talbs Jul 12, 2013
6629d5d
Merge branch 'master' into talbs/studio-authorship
Jul 22, 2013
5168a08
Don't add users marked with is_staff to course creation table.
Jul 22, 2013
0833181
Studio: updates older extend references and solves Sass warnings/dupe…
talbs Jul 22, 2013
3d49a46
Add error handler.
Jul 23, 2013
1da5af5
Studio: revises styling/message copy for authorship rights request error
talbs Jul 23, 2013
60d60de
Use format for strings instead of %.
Jul 25, 2013
628994d
i18n "Create a New Course".
Jul 25, 2013
8a715c1
Studio: using defined red color variables for error-based system mess…
talbs Jul 25, 2013
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cms/djangoapps/contentstore/features/signup.feature
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ Feature: Sign in
And I fill in the registration form
And I press the Create My Account button on the registration form
Then I should see be on the studio home page
And I should see the message "please click on the activation link in your email."
And I should see the message "complete your sign up we need you to verify your email address"
3 changes: 1 addition & 2 deletions cms/djangoapps/contentstore/features/signup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
#pylint: disable=W0621

from lettuce import world, step
from common import *


@step('I fill in the registration form$')
Expand All @@ -25,7 +24,7 @@ def i_press_the_button_on_the_registration_form(step):

@step('I should see be on the studio home page$')
def i_should_see_be_on_the_studio_home_page(step):
assert world.browser.find_by_css('div.inner-wrapper')
step.given('I should see the message "My Courses"')


@step(u'I should see the message "([^"]*)"$')
Expand Down
180 changes: 180 additions & 0 deletions cms/djangoapps/contentstore/tests/test_users.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
"""
Tests for user.py.
"""
import json
import mock
from .utils import CourseTestCase
from django.core.urlresolvers import reverse
from contentstore.views.user import _get_course_creator_status
from course_creators.views import add_user_with_status_granted
from course_creators.admin import CourseCreatorAdmin
from course_creators.models import CourseCreator

from django.http import HttpRequest
from django.contrib.auth.models import User
from django.contrib.admin.sites import AdminSite


class UsersTestCase(CourseTestCase):
Expand All @@ -13,3 +25,171 @@ def test_empty(self):
self.assertEqual(resp.status_code, 400)
content = json.loads(resp.content)
self.assertEqual(content["Status"], "Failed")


class IndexCourseCreatorTests(CourseTestCase):
"""
Tests the various permutations of course creator status.
"""
def setUp(self):
super(IndexCourseCreatorTests, self).setUp()

self.index_url = reverse("index")
self.request_access_url = reverse("request_course_creator")

# Disable course creation takes precedence over enable creator group. I have enabled the
# latter to make this clear.
self.disable_course_creation = {
"DISABLE_COURSE_CREATION": True,
"ENABLE_CREATOR_GROUP": True,
'STAFF_EMAIL': 'mark@marky.mark',
}

self.enable_creator_group = {"ENABLE_CREATOR_GROUP": True}

self.admin = User.objects.create_user('Mark', 'mark+courses@edx.org', 'foo')
self.admin.is_staff = True

def test_get_course_creator_status_disable_creation(self):
# DISABLE_COURSE_CREATION is True (this is the case on edx, where we have a marketing site).
# Only edx staff can create courses.
with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.disable_course_creation):
self.assertTrue(self.user.is_staff)
self.assertEquals('granted', _get_course_creator_status(self.user))
self._set_user_non_staff()
self.assertFalse(self.user.is_staff)
self.assertEquals('disallowed_for_this_site', _get_course_creator_status(self.user))

def test_get_course_creator_status_default_cause(self):
# Neither ENABLE_CREATOR_GROUP nor DISABLE_COURSE_CREATION are enabled. Anyone can create a course.
self.assertEquals('granted', _get_course_creator_status(self.user))
self._set_user_non_staff()
self.assertEquals('granted', _get_course_creator_status(self.user))

def test_get_course_creator_status_creator_group(self):
# ENABLE_CREATOR_GROUP is True. This is the case on edge.
# Only staff members and users who have been granted access can create courses.
with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group):
# Staff members can always create courses.
self.assertEquals('granted', _get_course_creator_status(self.user))
# Non-staff must request access.
self._set_user_non_staff()
self.assertEquals('unrequested', _get_course_creator_status(self.user))
# Staff user requests access.
self.client.post(self.request_access_url)
self.assertEquals('pending', _get_course_creator_status(self.user))

def test_get_course_creator_status_creator_group_granted(self):
# ENABLE_CREATOR_GROUP is True. This is the case on edge.
# Check return value for a non-staff user who has been granted access.
with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group):
self._set_user_non_staff()
add_user_with_status_granted(self.admin, self.user)
self.assertEquals('granted', _get_course_creator_status(self.user))

def test_get_course_creator_status_creator_group_denied(self):
# ENABLE_CREATOR_GROUP is True. This is the case on edge.
# Check return value for a non-staff user who has been denied access.
with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group):
self._set_user_non_staff()
self._set_user_denied()
self.assertEquals('denied', _get_course_creator_status(self.user))

def test_disable_course_creation_enabled_non_staff(self):
# Test index page content when DISABLE_COURSE_CREATION is True, non-staff member.
with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.disable_course_creation):
self._set_user_non_staff()
self._assert_cannot_create()

def test_disable_course_creation_enabled_staff(self):
# Test index page content when DISABLE_COURSE_CREATION is True, staff member.
with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.disable_course_creation):
resp = self._assert_can_create()
self.assertFalse('Email staff to create course' in resp.content)

def test_can_create_by_default(self):
# Test index page content with neither ENABLE_CREATOR_GROUP nor DISABLE_COURSE_CREATION enabled.
# Anyone can create a course.
self._assert_can_create()
self._set_user_non_staff()
self._assert_can_create()

def test_course_creator_group_enabled(self):
# Test index page content with ENABLE_CREATOR_GROUP True.
# Staff can always create a course, others must request access.
with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group):
# Staff members can always create courses.
self._assert_can_create()

# Non-staff case.
self._set_user_non_staff()
resp = self._assert_cannot_create()
self.assertTrue(self.request_access_url in resp.content)

# Now request access.
self.client.post(self.request_access_url)

# Still cannot create a course, but the "request access button" is no longer there.
resp = self._assert_cannot_create()
self.assertFalse(self.request_access_url in resp.content)
self.assertTrue('has-status is-pending' in resp.content)

def test_course_creator_group_granted(self):
# Test index page content with ENABLE_CREATOR_GROUP True, non-staff member with access granted.
with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group):
self._set_user_non_staff()
add_user_with_status_granted(self.admin, self.user)
self._assert_can_create()

def test_course_creator_group_denied(self):
# Test index page content with ENABLE_CREATOR_GROUP True, non-staff member with access denied.
with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group):
self._set_user_non_staff()
self._set_user_denied()
resp = self._assert_cannot_create()
self.assertFalse(self.request_access_url in resp.content)
self.assertTrue('has-status is-denied' in resp.content)

def _assert_can_create(self):
"""
Helper method that posts to the index page and checks that the user can create a course.

Returns the response from the post.
"""
resp = self.client.post(self.index_url)
self.assertTrue('new-course-button' in resp.content)
self.assertFalse(self.request_access_url in resp.content)
self.assertFalse('Email staff to create course' in resp.content)
return resp

def _assert_cannot_create(self):
"""
Helper method that posts to the index page and checks that the user cannot create a course.

Returns the response from the post.
"""
resp = self.client.post(self.index_url)
self.assertFalse('new-course-button' in resp.content)
return resp

def _set_user_non_staff(self):
"""
Sets user as non-staff.
"""
self.user.is_staff = False
self.user.save()

def _set_user_denied(self):
"""
Sets course creator status to denied in admin table.
"""
self.table_entry = CourseCreator(user=self.user)
self.table_entry.save()

self.deny_request = HttpRequest()
self.deny_request.user = self.admin

self.creator_admin = CourseCreatorAdmin(self.table_entry, AdminSite())

self.table_entry.state = CourseCreator.DENIED
self.creator_admin.save_model(self.deny_request, self.table_entry, None, True)
43 changes: 41 additions & 2 deletions cms/djangoapps/contentstore/views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
from django.core.context_processors import csrf

from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from contentstore.utils import get_url_reverse, get_lms_link_for_item
from util.json_request import expect_json, JsonResponse
from auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME, 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 course_creators.views import get_course_creator_status, add_user_with_status_unrequested, user_requested_access

from .access import has_access

Expand Down Expand Up @@ -40,10 +42,22 @@ def course_filter(course):
get_lms_link_for_item(course.location, course_id=course.location.course_id))
for course in courses],
'user': request.user,
'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
'request_course_creator_url': reverse('request_course_creator'),
'course_creator_status': _get_course_creator_status(request.user),
'csrf': csrf(request)['csrf_token']
})


@require_POST
@login_required
def request_course_creator(request):
"""
User has requested course creation access.
"""
user_requested_access(request.user)
return JsonResponse({"Status": "OK"})


@login_required
@ensure_csrf_cookie
def manage_users(request, location):
Expand Down Expand Up @@ -141,3 +155,28 @@ def remove_user(request, location):
remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME)

return JsonResponse({"Status": "OK"})


def _get_course_creator_status(user):
"""
Helper method for returning the course creator status for a particular user,
taking into account the values of DISABLE_COURSE_CREATION and ENABLE_CREATOR_GROUP.

If the user passed in has not previously visited the index page, it will be
added with status 'unrequested' if the course creator group is in use.
"""
if user.is_staff:
course_creator_status = 'granted'
elif settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False):
course_creator_status = 'disallowed_for_this_site'
elif settings.MITX_FEATURES.get('ENABLE_CREATOR_GROUP', False):
course_creator_status = get_course_creator_status(user)
if course_creator_status is None:
# User not grandfathered in as an existing user, has not previously visited the dashboard page.
# Add the user to the course creator admin table with status 'unrequested'.
add_user_with_status_unrequested(user)
course_creator_status = get_course_creator_status(user)
else:
course_creator_status = 'granted'

return course_creator_status
21 changes: 13 additions & 8 deletions cms/djangoapps/course_creators/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class CourseCreator(models.Model):
"why course creation access was denied)"))

def __unicode__(self):
return u'%str | %str [%str] | %str' % (self.user, self.state, self.state_changed, self.note)
return u"{0} | {1} [{2}]".format(self.user, self.state, self.state_changed)


@receiver(post_init, sender=CourseCreator)
Expand All @@ -54,18 +54,23 @@ def post_init_callback(sender, **kwargs):
@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.
Extend to update state_changed time and fire event to update course creator group, if appropriate.
"""
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
)
# If either old or new state is 'granted', we must manipulate the course creator
# group maintained by authz. That requires staff permissions (stored admin).
if instance.state == CourseCreator.GRANTED or instance.orig_state == CourseCreator.GRANTED:
assert hasattr(instance, 'admin'), 'Must have stored staff user to change course creator group'
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()
35 changes: 28 additions & 7 deletions cms/djangoapps/course_creators/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
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.views import get_course_creator_status, update_course_creator_group, user_requested_access
from course_creators.models import CourseCreator
from auth.authz import is_user_in_creator_group
import mock
Expand All @@ -26,22 +26,19 @@ def setUp(self):

def test_staff_permission_required(self):
"""
Tests that add methods and course creator group method must be called with staff permissions.
Tests that any method changing the course creator authz group 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)
add_user_with_status_unrequested(self.user)
self.assertEqual('unrequested', get_course_creator_status(self.user))

# Calling add again will be a no-op (even if state is different).
Expand All @@ -57,7 +54,7 @@ def test_add_granted(self):
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)
add_user_with_status_unrequested(self.user)
self.assertEqual('granted', get_course_creator_status(self.user))

self.assertTrue(is_user_in_creator_group(self.user))
Expand All @@ -69,3 +66,27 @@ def test_update_creator_group(self):
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))

def test_user_requested_access(self):
add_user_with_status_unrequested(self.user)
self.assertEqual('unrequested', get_course_creator_status(self.user))
user_requested_access(self.user)
self.assertEqual('pending', get_course_creator_status(self.user))

def test_user_requested_already_granted(self):
add_user_with_status_granted(self.admin, self.user)
self.assertEqual('granted', get_course_creator_status(self.user))
# Will not "downgrade" to pending because that would require removing the
# user from the authz course creator group (and that can only be done by an admin).
user_requested_access(self.user)
self.assertEqual('granted', get_course_creator_status(self.user))

def test_add_user_unrequested_staff(self):
# Users marked as is_staff will not be added to the course creator table.
add_user_with_status_unrequested(self.admin)
self.assertIsNone(get_course_creator_status(self.admin))

def test_add_user_granted_staff(self):
# Users marked as is_staff will not be added to the course creator table.
add_user_with_status_granted(self.admin, self.admin)
self.assertIsNone(get_course_creator_status(self.admin))
Loading