Commit 166aea62 by Christina Roberts

Merge pull request #341 from edx/talbs/studio-authorship

Studio: Authorship Rights Request UI + Dashboard Clean-up
parents b143de1f 8a715c1a
...@@ -9,4 +9,4 @@ Feature: Sign in ...@@ -9,4 +9,4 @@ Feature: Sign in
And I fill in the registration form And I fill in the registration form
And I press the Create My Account button on 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 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"
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
#pylint: disable=W0621 #pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import *
@step('I fill in the registration form$') @step('I fill in the registration form$')
...@@ -25,7 +24,7 @@ def i_press_the_button_on_the_registration_form(step): ...@@ -25,7 +24,7 @@ def i_press_the_button_on_the_registration_form(step):
@step('I should see be on the studio home page$') @step('I should see be on the studio home page$')
def i_should_see_be_on_the_studio_home_page(step): 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 "([^"]*)"$') @step(u'I should see the message "([^"]*)"$')
......
"""
Tests for user.py.
"""
import json import json
import mock
from .utils import CourseTestCase from .utils import CourseTestCase
from django.core.urlresolvers import reverse 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): class UsersTestCase(CourseTestCase):
...@@ -13,3 +25,171 @@ class UsersTestCase(CourseTestCase): ...@@ -13,3 +25,171 @@ class UsersTestCase(CourseTestCase):
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
content = json.loads(resp.content) content = json.loads(resp.content)
self.assertEqual(content["Status"], "Failed") 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)
...@@ -3,15 +3,17 @@ from django.core.exceptions import PermissionDenied ...@@ -3,15 +3,17 @@ from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response 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 xmodule.modulestore.django import modulestore
from contentstore.utils import get_url_reverse, get_lms_link_for_item from contentstore.utils import get_url_reverse, get_lms_link_for_item
from util.json_request import expect_json, JsonResponse 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 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 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 from .access import has_access
...@@ -40,10 +42,22 @@ def index(request): ...@@ -40,10 +42,22 @@ def index(request):
get_lms_link_for_item(course.location, course_id=course.location.course_id)) get_lms_link_for_item(course.location, course_id=course.location.course_id))
for course in courses], for course in courses],
'user': request.user, '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 @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def manage_users(request, location): def manage_users(request, location):
...@@ -144,3 +158,28 @@ def remove_user(request, location): ...@@ -144,3 +158,28 @@ def remove_user(request, location):
remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME) remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME)
return JsonResponse({"Status": "OK"}) 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
...@@ -39,7 +39,7 @@ class CourseCreator(models.Model): ...@@ -39,7 +39,7 @@ class CourseCreator(models.Model):
"why course creation access was denied)")) "why course creation access was denied)"))
def __unicode__(self): 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) @receiver(post_init, sender=CourseCreator)
...@@ -54,18 +54,23 @@ def post_init_callback(sender, **kwargs): ...@@ -54,18 +54,23 @@ def post_init_callback(sender, **kwargs):
@receiver(post_save, sender=CourseCreator) @receiver(post_save, sender=CourseCreator)
def post_save_callback(sender, **kwargs): 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'] instance = kwargs['instance']
# We only wish to modify the state_changed time if the state has been modified. We don't wish to # 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. # modify it for changes to the notes field.
if instance.state != instance.orig_state: if instance.state != instance.orig_state:
update_creator_state.send( # If either old or new state is 'granted', we must manipulate the course creator
sender=sender, # group maintained by authz. That requires staff permissions (stored admin).
caller=instance.admin, if instance.state == CourseCreator.GRANTED or instance.orig_state == CourseCreator.GRANTED:
user=instance.user, assert hasattr(instance, 'admin'), 'Must have stored staff user to change course creator group'
add=instance.state == CourseCreator.GRANTED update_creator_state.send(
) sender=sender,
caller=instance.admin,
user=instance.user,
add=instance.state == CourseCreator.GRANTED
)
instance.state_changed = timezone.now() instance.state_changed = timezone.now()
instance.orig_state = instance.state instance.orig_state = instance.state
instance.save() instance.save()
...@@ -7,7 +7,7 @@ from django.contrib.auth.models import User ...@@ -7,7 +7,7 @@ from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied 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 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 course_creators.models import CourseCreator
from auth.authz import is_user_in_creator_group from auth.authz import is_user_in_creator_group
import mock import mock
...@@ -26,22 +26,19 @@ class CourseCreatorView(TestCase): ...@@ -26,22 +26,19 @@ class CourseCreatorView(TestCase):
def test_staff_permission_required(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): with self.assertRaises(PermissionDenied):
add_user_with_status_granted(self.user, self.user) add_user_with_status_granted(self.user, self.user)
with self.assertRaises(PermissionDenied): 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) update_course_creator_group(self.user, self.user, True)
def test_table_initially_empty(self): def test_table_initially_empty(self):
self.assertIsNone(get_course_creator_status(self.user)) self.assertIsNone(get_course_creator_status(self.user))
def test_add_unrequested(self): 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)) self.assertEqual('unrequested', get_course_creator_status(self.user))
# Calling add again will be a no-op (even if state is different). # Calling add again will be a no-op (even if state is different).
...@@ -57,7 +54,7 @@ class CourseCreatorView(TestCase): ...@@ -57,7 +54,7 @@ class CourseCreatorView(TestCase):
self.assertEqual('granted', get_course_creator_status(self.user)) self.assertEqual('granted', get_course_creator_status(self.user))
# Calling add again will be a no-op (even if state is different). # 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.assertEqual('granted', get_course_creator_status(self.user))
self.assertTrue(is_user_in_creator_group(self.user)) self.assertTrue(is_user_in_creator_group(self.user))
...@@ -69,3 +66,27 @@ class CourseCreatorView(TestCase): ...@@ -69,3 +66,27 @@ class CourseCreatorView(TestCase):
self.assertTrue(is_user_in_creator_group(self.user)) self.assertTrue(is_user_in_creator_group(self.user))
update_course_creator_group(self.admin, self.user, False) update_course_creator_group(self.admin, self.user, False)
self.assertFalse(is_user_in_creator_group(self.user)) 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))
...@@ -2,32 +2,38 @@ ...@@ -2,32 +2,38 @@
Methods for interacting programmatically with the user creator table. Methods for interacting programmatically with the user creator table.
""" """
from course_creators.models import CourseCreator 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 from auth.authz import add_user_to_creator_group, remove_user_from_creator_group
def add_user_with_status_unrequested(caller, user): def add_user_with_status_unrequested(user):
""" """
Adds a user to the course creator table with status 'unrequested'. 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 If the user is already in the table, this method is a no-op
(state will not be changed). Caller must have staff permissions. (state will not be changed).
If the user is marked as is_staff, this method is a no-op (user
will not be added to table).
""" """
_add_user(caller, user, CourseCreator.UNREQUESTED) _add_user(user, CourseCreator.UNREQUESTED)
def add_user_with_status_granted(caller, user): def add_user_with_status_granted(caller, user):
""" """
Adds a user to the course creator table with status 'granted'. Adds a user to the course creator table with status 'granted'.
If appropriate, this method also adds the user to the course creator group maintained by authz.py.
Caller must have staff permissions.
If the user is already in the table, this method is a no-op If the user is already in the table, this method is a no-op
(state will not be changed). Caller must have staff permissions. (state will not be changed).
This method also adds the user to the course creator group maintained by authz.py. If the user is marked as is_staff, this method is a no-op (user
will not be added to table, nor added to authz.py group).
""" """
_add_user(caller, user, CourseCreator.GRANTED) if _add_user(user, CourseCreator.GRANTED):
update_course_creator_group(caller, user, True) update_course_creator_group(caller, user, True)
def update_course_creator_group(caller, user, add): def update_course_creator_group(caller, user, add):
...@@ -61,16 +67,33 @@ def get_course_creator_status(user): ...@@ -61,16 +67,33 @@ def get_course_creator_status(user):
return user[0].state return user[0].state
def _add_user(caller, user, state): def user_requested_access(user):
"""
User has requested course creator access.
This changes the user state to CourseCreator.PENDING, unless the user
state is already CourseCreator.GRANTED, in which case this method is a no-op.
"""
user = CourseCreator.objects.get(user=user)
if user.state != CourseCreator.GRANTED:
user.state = CourseCreator.PENDING
user.save()
def _add_user(user, state):
""" """
Adds a user to the course creator table with the specified state. Adds a user to the course creator table with the specified state.
Returns True if user was added to table, else False.
If the user is already in the table, this method is a no-op If the user is already in the table, this method is a no-op
(state will not be changed). (state will not be changed, method will return False).
"""
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: If the user is marked as is_staff, this method is a no-op (False will be returned).
"""
if not user.is_staff and CourseCreator.objects.filter(user=user).count() == 0:
entry = CourseCreator(user=user, state=state) entry = CourseCreator(user=user, state=state)
entry.save() entry.save()
return True
return False
...@@ -597,11 +597,11 @@ function cancelNewSection(e) { ...@@ -597,11 +597,11 @@ function cancelNewSection(e) {
function addNewCourse(e) { function addNewCourse(e) {
e.preventDefault(); e.preventDefault();
$('.new-course-button').addClass('disabled');
$(e.target).hide(); $(e.target).addClass('disabled');
var $newCourse = $($('#new-course-template').html()); var $newCourse = $($('#new-course-template').html());
var $cancelButton = $newCourse.find('.new-course-cancel'); var $cancelButton = $newCourse.find('.new-course-cancel');
$('.inner-wrapper').prepend($newCourse); $('.courses').prepend($newCourse);
$newCourse.find('.new-course-name').focus().select(); $newCourse.find('.new-course-name').focus().select();
$newCourse.find('form').bind('submit', saveNewCourse); $newCourse.find('form').bind('submit', saveNewCourse);
$cancelButton.bind('click', cancelNewCourse); $cancelButton.bind('click', cancelNewCourse);
...@@ -646,7 +646,7 @@ function saveNewCourse(e) { ...@@ -646,7 +646,7 @@ function saveNewCourse(e) {
function cancelNewCourse(e) { function cancelNewCourse(e) {
e.preventDefault(); e.preventDefault();
$('.new-course-button').show(); $('.new-course-button').removeClass('disabled');
$(this).parents('section.new-course').remove(); $(this).parents('section.new-course').remove();
} }
......
...@@ -368,42 +368,6 @@ p, ul, ol, dl { ...@@ -368,42 +368,6 @@ p, ul, ol, dl {
color: $gray-d3; color: $gray-d3;
} }
} }
.introduction {
@include box-sizing(border-box);
@extend .t-copy-sub1;
width: flex-grid(12);
margin: 0 0 $baseline 0;
.copy strong {
font-weight: 600;
}
&.has-links {
@include clearfix();
.copy {
float: left;
width: flex-grid(8,12);
margin-right: flex-gutter();
}
.nav-introduction-supplementary {
@extend .t-copy-sub2;
float: right;
width: flex-grid(4,12);
display: block;
text-align: right;
.icon {
@extend .t-action3;
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
}
}
}
}
} }
.content-primary, .content-supplementary { .content-primary, .content-supplementary {
...@@ -482,6 +446,24 @@ p, ul, ol, dl { ...@@ -482,6 +446,24 @@ p, ul, ol, dl {
} }
} }
// actions
.list-actions {
@extend .cont-no-list;
.action-item {
margin-bottom: ($baseline/4);
border-bottom: 1px dotted $gray-l4;
padding-bottom: ($baseline/4);
&:last-child {
margin-bottom: 0;
border: none;
padding-bottom: 0;
}
}
}
// navigation // navigation
.nav-related, .nav-page { .nav-related, .nav-page {
......
...@@ -2,10 +2,32 @@ ...@@ -2,10 +2,32 @@
// // shame file - used for any bad-form/orphaned scss that knowingly violate edX FED architecture/standards (see - http://csswizardry.com/2013/04/shame-css/) // // shame file - used for any bad-form/orphaned scss that knowingly violate edX FED architecture/standards (see - http://csswizardry.com/2013/04/shame-css/)
// ==================== // ====================
// view - dashboard
body.dashboard {
// elements - authorship controls
.wrapper-authorshiprights {
.ui-toggle-control {
// needed to override general a element transition properties - need to fix.
transition-duration: 0.25s;
transition-timing-function: ease-in-out;
}
.icon-remove-sign {
// needed to override general a element transition properties - need to fix.
transition-duration: 0.25s;
transition-timing-function: ease-in-out;
}
}
}
// known things to do (paint the fence, sand the floor, wax on/off)
// ==================== // ====================
// known things to do (paint the fence, sand the floor, wax on/off):
// * centralize and move form styling into forms.scss - cms/static/sass/views/_textbooks.scss and cms/static/sass/views/_settings.scss // * centralize and move form styling into forms.scss - cms/static/sass/views/_textbooks.scss and cms/static/sass/views/_settings.scss
// * move dialogue styles into cms/static/sass/elements/_modal.scss // * move dialogue styles into cms/static/sass/elements/_modal.scss
// * use the @include placeholder Bourbon mixin (http://bourbon.io/docs/#placeholder) for any placeholder styling // * use the @include placeholder Bourbon mixin (http://bourbon.io/docs/#placeholder) for any placeholder styling
// studio - elements - system feedback // studio - elements - system feedback
// ====================
// messages
.message {
@extend .t-copy-sub1;
display: block;
}
.message-status {
display: none;
@include border-top-radius(2px);
@include box-sizing(border-box);
border-bottom: 2px solid $yellow-d2;
margin: 0 0 $baseline 0;
padding: ($baseline/2) $baseline;
font-weight: 500;
background: $yellow-d1;
color: $white;
[class^="icon-"] {
position: relative;
top: 1px;
@include font-size(16);
display: inline-block;
margin-right: ($baseline/2);
}
.text {
display: inline-block;
}
&.error {
border-color: $red-d3;
background: $red-l1;
}
&.is-shown {
display: block;
}
}
// alerts, notifications, prompts, and status communication // alerts, notifications, prompts, and status communication
// ==================== // ====================
......
// studio - elements - system help // studio - elements - system help
// ==================== // ====================
// view introductions - common greeting/starting points for the UI
.content .introduction {
@include box-sizing(border-box);
margin-bottom: $baseline;
.title {
@extend .t-title4;
font-weight: 600;
}
.copy {
@extend .t-copy-sub1;
}
strong {
font-weight: 600;
}
// CASE: has links alongside
&.has-links {
@include clearfix();
.copy {
float: left;
width: flex-grid(8,12);
margin-right: flex-gutter();
}
.nav-introduction-supplementary {
@extend .t-copy-sub2;
float: right;
width: flex-grid(4,12);
display: block;
text-align: right;
.icon {
@extend .t-action3;
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
}
}
}
}
// notices - in-context: to be used as notices to users within the context of a form/action // notices - in-context: to be used as notices to users within the context of a form/action
.notice-incontext { .notice-incontext {
@extend .ui-well; @extend .ui-well;
border-radius: ($baseline/10); border-radius: ($baseline/10);
position: relative;
overflow: hidden;
margin-bottom: $baseline;
.title { .title {
@extend .t-title7; @extend .t-title7;
margin-bottom: ($baseline/4); margin-bottom: ($baseline/4);
font-weight: 600; font-weight: 700;
} }
.copy { .copy {
@extend .t-copy-sub1; @extend .t-copy-sub1;
@include transition(opacity $tmg-f2 ease-in-out 0s); @include transition(opacity $tmg-f2 ease-in-out 0s);
opacity: 0.75; opacity: 0.75;
margin-bottom: $baseline;
&:last-child {
margin-bottom: 0;
}
} }
strong { strong {
font-weight: 600; font-weight: 600;
} }
&:hover { &.has-status {
.copy { .status-indicator {
opacity: 1.0; position: absolute;
top: 0;
left: 0;
display: block;
width: 100%;
height: ($baseline/4);
opacity: 0.40;
}
}
// CASE: notice has actions {
&.has-actions {
.list-actions {
margin-top: ($baseline*0.75);
.action-item {
}
.action-primary {
@extend .btn-primary-blue;
@extend .t-action3;
}
}
}
// list of notices all in one
&.list-notices {
.notice-item {
margin-bottom: $baseline;
border-bottom: 1px solid $gray-l3;
padding-bottom: $baseline;
&:last-child {
margin-bottom: 0;
border: none;
padding-bottom: 0;
}
} }
} }
} }
// particular warnings around a workflow for something // particular notice - warnings around a workflow for something
.notice-workflow { .notice-workflow {
background: $yellow-l5; background: $yellow-l5;
.copy { .status-indicator {
background: $yellow;
}
title {
color: $gray-d1; color: $gray-d1;
} }
.copy {
color: $gray;
}
}
// particular notice - instructional
.notice-instruction {
background-color: $gray-l4;
.title {
color: $gray-d2;
}
.copy {
color: $gray-d2;
}
&.has-actions {
.list-actions {
.action-item {
}
.action-primary {
@extend .btn-primary-blue;
@extend .t-action3;
}
}
}
}
// particular notice - confirmation
.notice-confirmation {
background-color: $green-l5;
.status-indicator {
background: $green-s1;
}
.title {
color: $green;
}
.copy {
color: $gray;
}
} }
...@@ -252,44 +252,3 @@ body.signup, body.signin { ...@@ -252,44 +252,3 @@ body.signup, body.signin {
} }
} }
} }
// ====================
// messages
.message {
@extend .t-copy-sub1;
display: block;
}
.message-status {
display: none;
@include border-top-radius(2px);
@include box-sizing(border-box);
border-bottom: 2px solid $yellow-d2;
margin: 0 0 $baseline 0;
padding: ($baseline/2) $baseline;
font-weight: 500;
background: $yellow-d1;
color: $white;
[class^="icon-"] {
position: relative;
top: 1px;
@include font-size(16);
display: inline-block;
margin-right: ($baseline/2);
}
.text {
display: inline-block;
}
&.error {
border-color: shade($red, 50%);
background: tint($red, 20%);
}
&.is-shown {
display: block;
}
}
...@@ -3,20 +3,309 @@ ...@@ -3,20 +3,309 @@
body.dashboard { body.dashboard {
.my-classes { // temp
margin-top: $baseline; .content {
margin-bottom: ($baseline*5);
&:last-child {
margin-bottom: 0;
}
}
// ====================
// basic layout
.content-primary, .content-supplementary {
@include box-sizing(border-box);
float: left;
}
.content-primary {
width: flex-grid(9, 12);
margin-right: flex-gutter();
}
.content-supplementary {
width: flex-grid(3, 12);
}
// ====================
// elements - notices
.content .notice-incontext {
width: flexgrid(9, 9);
// CASE: notice has actions {
&.has-actions, &.list-notices .notice-item.has-actions {
.msg, .list-actions {
display: inline-block;
vertical-align: middle;
}
.msg {
width: flex-grid(6, 9);
margin-right: flex-gutter();
}
.list-actions {
width: flex-grid(3, 9);
text-align: right;
margin-top: 0;
.action-item {
}
.action-create-course {
@extend .btn-primary-green;
@extend .t-action3;
}
}
}
}
// elements - course creation rights controls
.wrapper-creationrights {
overflow: hidden;
.ui-toggle-control {
@extend .ui-depth2;
@extend .btn-secondary-gray;
@include clearfix();
display: block;
text-align: left;
// STATE: hover - syncing up colors with current so transition is smoother
&:hover {
background: $gray-d1;
color: $white;
}
.label {
@extend .t-action3;
float: left;
width: flex-grid(8, 9);
margin: 3px flex-gutter() 0 0;
}
.icon-remove-sign {
@extend .t-action1;
@include transform(rotate(45deg));
@include transform-origin(center center);
@include transition(all $tmg-f1 linear 0s);
float: right;
text-align: right;
}
}
.ui-toggle-target {
@extend .ui-depth1;
@include transition(opacity $tmg-f1 ease-in-out 0s);
position: relative;
top: -2px;
display: none;
opacity: 0;
}
// CASE: when the content area is shown
&.is-shown {
.ui-toggle-control {
@include border-bottom-radius(0);
.icon-remove-sign {
@include transform(rotate(90deg));
@include transform-origin(center center);
}
}
.ui-toggle-target {
display: block;
opacity: 1.0;
}
}
} }
.class-list { // elements - course creation rights controls
margin-top: 20px; .status-creationrights {
margin-top: $baseline;
.title {
@extend .t-title7;
margin-bottom: ($baseline/4);
font-weight: 700;
color: $gray-d1;
}
.copy {
}
.list-actions, .form-actions {
margin-top: ($baseline*0.75);
.action-item {
}
.action-primary {
@extend .btn-primary-blue;
@extend .t-action3;
}
// specific - request button
// BT: should we abstract these states out for all buttons like this
.action-request {
position: relative;
overflow: hidden;
.icon-cog {
@include transition(all $tmg-f1 ease-in-out $tmg-f1);
@include font-size(20);
position: absolute;
top: ($baseline/2);
left: -($baseline);
visibility: hidden;
opacity: 0.0;
}
// state: submitting
&.is-submitting {
padding-left: ($baseline*2);
.icon-cog {
left: ($baseline*0.75);
visibility: visible;
opacity: 1.0;
}
}
// state: has an error
&.has-error {
padding-left: ($baseline*2);
background: $red;
border-color: $red-d1;
.icon-cog {
left: ($baseline*0.75);
visibility: visible;
opacity: 1.0;
}
}
}
}
.status-update {
.label {
@extend .cont-text-sr;
}
.value {
border-radius: ($baseline/4);
position: relative;
overflow: hidden;
padding: ($baseline/5) ($baseline/2);
background: $gray;
.status-indicator {
position: absolute;
top: 0;
left: 0;
display: block;
width: 100%;
height: ($baseline/4);
opacity: 0.40;
}
}
.value-formal, .value-description {
border-radius: ($baseline/10);
display: inline-block;
vertical-align: middle;
color: $white;
}
.value-formal {
@extend .t-title5;
margin: ($baseline/2);
font-weight: 700;
[class^="icon-"] {
margin-right: ($baseline/4);
}
}
.value-description {
@extend .t-copy-sub1;
position: relative;
color: $white;
opacity: 0.85;
}
}
// CASE: rights are not requested yet
&.is-unrequested {
.title {
@extend .cont-text-sr;
}
}
// CASE: status is pending
&.is-pending {
.status-update {
.value {
background: $orange;
}
.status-indicator {
background: $orange-d1;
}
}
}
// CASE: status is denied
&.is-denied {
.status-update {
.value {
background: $red-l1;
}
.status-indicator {
background: $red-s1;
}
}
}
}
// ====================
// course listings
.courses {
margin: $baseline 0;
}
.list-courses {
margin-top: $baseline;
border-radius: 3px; border-radius: 3px;
border: 1px solid $darkGrey; border: 1px solid $gray;
background: #fff; background: $white;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1); box-shadow: 0 1px 2px $shadow-l1;
li { .course-item {
position: relative; position: relative;
border-bottom: 1px solid $mediumGrey; border-bottom: 1px solid $gray-l1;
&:last-child { &:last-child {
border-bottom: none; border-bottom: none;
...@@ -56,7 +345,7 @@ body.dashboard { ...@@ -56,7 +345,7 @@ body.dashboard {
.view-live-button { .view-live-button {
z-index: 10000; z-index: 10000;
position: absolute; position: absolute;
top: 15px; top: ($baseline*0.75);
right: $baseline; right: $baseline;
padding: ($baseline/4) ($baseline/2); padding: ($baseline/4) ($baseline/2);
opacity: 0.0; opacity: 0.0;
...@@ -70,17 +359,25 @@ body.dashboard { ...@@ -70,17 +359,25 @@ body.dashboard {
} }
.new-course { .new-course {
padding: 15px 25px; @include clearfix();
margin-top: 20px; padding: ($baseline*0.75) ($baseline*1.25);
margin-top: $baseline;
border-radius: 3px; border-radius: 3px;
border: 1px solid $darkGrey; border: 1px solid $gray;
background: #fff; background: $white;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1); box-shadow: 0 1px 2px rgba(0, 0, 0, .1);
@include clearfix;
.title {
@extend .t-title4;
font-weight: 600;
margin-bottom: ($baseline/2);
border-bottom: 1px solid $gray-l3;
padding-bottom: ($baseline/2);
}
.row { .row {
margin-bottom: 15px; @include clearfix();
@include clearfix; margin-bottom: ($baseline*0.75);
} }
.column { .column {
...@@ -97,8 +394,8 @@ body.dashboard { ...@@ -97,8 +394,8 @@ body.dashboard {
} }
label { label {
@extend .t-title7;
display: block; display: block;
font-size: 13px;
font-weight: 700; font-weight: 700;
} }
...@@ -109,7 +406,7 @@ body.dashboard { ...@@ -109,7 +406,7 @@ body.dashboard {
} }
.new-course-name { .new-course-name {
font-size: 19px; @extend .t-title5;
font-weight: 300; font-weight: 300;
} }
...@@ -120,5 +417,9 @@ body.dashboard { ...@@ -120,5 +417,9 @@ body.dashboard {
.new-course-cancel { .new-course-cancel {
@include white-button; @include white-button;
} }
.item-details {
padding-bottom: 0;
}
} }
} }
...@@ -2,14 +2,31 @@ ...@@ -2,14 +2,31 @@
<%inherit file="base.html" /> <%inherit file="base.html" />
<%block name="content"> <%block name="content">
<div class="wrapper-mast wrapper sr">
<header class="mast">
<h1 class="page-header">${_("Studio Account Activation")}</h1>
</header>
</div>
<section class="tos"> <div class="wrapper-content wrapper">
<div> <section class="content activation is-active">
<article class="content-primary" role="main">
</article>
<section class="activation"> <div class="notice notice-incontext notice-instruction has-actions">
<h1>${_("Account already active!")}</h1> <div class="msg">
<p>${_('This account has already been activated.')}<a href="/signin">${_("Log in here.")}</a></p> <h2 class="title">${_("Your account is already active")}</h2>
</div> <div class="copy">
</section> <p>${_("This account, set up using {0}, has already been activated. Please sign in to start working within edX Studio.".format(user.email))}</p>
</div>
</div>
<ul class="list-actions">
<li class="action-item">
<a href="/signin" class="action-primary action-signin">${_("Sign into Studio")}</a>
</li>
</ul>
</div>
</section>
</div>
</%block> </%block>
...@@ -2,12 +2,31 @@ ...@@ -2,12 +2,31 @@
<%inherit file="base.html" /> <%inherit file="base.html" />
<%block name="content"> <%block name="content">
<div class="wrapper-mast wrapper sr">
<section class="tos"> <header class="mast">
<div> <h1 class="page-header">${_("Studio Account Activation")}</h1>
<h1>${_("Activation Complete!")}</h1> </header>
<p>${_('Thanks for activating your account.')}<a href="/signin">${_("Log in here.")}</a></p>
</div> </div>
</section>
<div class="wrapper-content wrapper">
<section class="content activation is-complete">
<article class="content-primary" role="main">
</article>
<div class="notice notice-incontext notice-instruction has-actions">
<div class="msg">
<h1 class="title">${_("Your account activation is complete!")}</h1>
<div class="copy">
<p>${_("Thank you for activating your account. You may now sign in and start using edX Studio to author courses.")}</p>
</div>
</div>
<ul class="list-actions">
<li class="action-item">
<a href="/signin" class="action-primary action-signin">${_("Sign into Studio")}</a>
</li>
</ul>
</div>
</section>
</div>
</%block> </%block>
...@@ -2,14 +2,32 @@ ...@@ -2,14 +2,32 @@
<%inherit file="base.html" /> <%inherit file="base.html" />
<%block name="content"> <%block name="content">
<section class="tos"> <div class="wrapper-mast wrapper sr">
<div> <header class="mast">
<h1>${_("Activation Invalid")}</h1> <h1 class="page-header">${_("Studio Account Activation")}</h1>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content activation is-invalid">
<article class="content-primary" role="main">
</article>
<p>${_('Something went wrong. Check to make sure the URL you went to was correct -- e-mail programs will sometimes split it into two lines. If you still have issues, e-mail us to let us know what happened at {email}.').format(email='<a href="mailto:bugs@mitx.mit.edu">bugs@mitx.mit.edu</a>')}</p> <div class="notice notice-incontext notice-instruction has-actions">
<div class="msg">
<h1 class="title">${_('Your account activation is invalid')}</h1>
<div class="copy">
<p>${_("We're sorry. Something went wrong with your activation. Check to make sure the URL you went to was correct &mdash; e-mail programs will sometimes split it into two lines.")}</p>
<p>${_("If you still have issues, contact edX Support. In the meatime, you can also return to")} <a href="/">{_('the Studio homepage.')}</a></p>
</div>
</div>
<p>${_('Or you can go back to the {link_start}home page{link_end}.').format( <ul class="list-actions">
link_start='<a href="/">', link_end='</a>')}</p> <li class="action-item">
<a href="http://help.edge.edx.org/discussion/new" class="action action-primary show-tender">${_('Contact edX Support')}</a>
</li>
</ul>
</div>
</section>
</div> </div>
</section>
</%block> </%block>
...@@ -12,6 +12,7 @@ admin.autodiscover() ...@@ -12,6 +12,7 @@ admin.autodiscover()
urlpatterns = ('', # nopep8 urlpatterns = ('', # nopep8
url(r'^$', 'contentstore.views.howitworks', name='homepage'), url(r'^$', 'contentstore.views.howitworks', name='homepage'),
url(r'^listing', 'contentstore.views.index', name='index'), url(r'^listing', 'contentstore.views.index', name='index'),
url(r'^request_course_creator$', 'contentstore.views.request_course_creator', name='request_course_creator'),
url(r'^edit/(?P<location>.*?)$', 'contentstore.views.edit_unit', name='edit_unit'), url(r'^edit/(?P<location>.*?)$', 'contentstore.views.edit_unit', name='edit_unit'),
url(r'^subsection/(?P<location>.*?)$', 'contentstore.views.edit_subsection', name='edit_subsection'), url(r'^subsection/(?P<location>.*?)$', 'contentstore.views.edit_subsection', name='edit_subsection'),
url(r'^preview_component/(?P<location>.*?)$', 'contentstore.views.preview_component', name='preview_component'), url(r'^preview_component/(?P<location>.*?)$', 'contentstore.views.preview_component', name='preview_component'),
......
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
} }
&.disabled, &[disabled] { &.disabled, &[disabled], &.is-disabled {
cursor: default; cursor: default;
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;
......
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