Commit 13ca61ba by Your Name

fix merge conflict with _variables.scss

parents 62769971 50ef0d1c
......@@ -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: Send e-mails to new Studio users (on edge only) when their course creator
status has changed. This will not be in use until the course creator table
is enabled.
LMS: Added user preferences (arbitrary user/key/value tuples, for which
which user/key is unique) and a REST API for reading users and
preferences. Access to the REST API is restricted by use of the
......
......@@ -2,7 +2,7 @@
#pylint: disable=W0621
from lettuce import world, step
from nose.tools import assert_false, assert_equal, assert_regexp_matches, assert_true
from nose.tools import assert_false, assert_equal, assert_regexp_matches
from common import type_in_codemirror, press_the_notification_button
KEY_CSS = '.key input.policy-key'
......@@ -90,18 +90,18 @@ def the_policy_key_value_is_changed(step):
############# HELPERS ###############
def assert_policy_entries(expected_keys, expected_values):
for counter in range(len(expected_keys)):
index = get_index_of(expected_keys[counter])
assert_false(index == -1, "Could not find key: " + expected_keys[counter])
assert_equal(expected_values[counter], world.css_find(VALUE_CSS)[index].value, "value is incorrect")
for key, value in zip(expected_keys, expected_values):
index = get_index_of(key)
assert_false(index == -1, "Could not find key: {key}".format(key=key))
assert_equal(value, world.css_find(VALUE_CSS)[index].value, "value is incorrect")
def get_index_of(expected_key):
for counter in range(len(world.css_find(KEY_CSS))):
# Sometimes get stale reference if I hold on to the array of elements
key = world.css_value(KEY_CSS, index=counter)
for i, element in enumerate(world.css_find(KEY_CSS)):
# Sometimes get stale reference if I hold on to the array of elements
key = world.css_value(KEY_CSS, index=i)
if key == expected_key:
return counter
return i
return -1
......
......@@ -19,7 +19,7 @@ def i_see_only_the_settings_and_values(step):
world.verify_all_setting_entries(
[
['Category', "Week 1", False],
['Display Name', "Discussion Tag", False],
['Display Name', "Discussion", False],
['Subcategory', "Topic-Level Student-Visible Label", False]
])
......
......@@ -14,4 +14,4 @@ def i_created_blank_html_page(step):
@step('I see only the HTML display name setting$')
def i_see_only_the_html_display_name(step):
world.verify_all_setting_entries([['Display Name', "Blank HTML Page", False]])
world.verify_all_setting_entries([['Display Name', "Text", False]])
......@@ -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"
......@@ -2,7 +2,6 @@
#pylint: disable=W0621
from lettuce import world, step
from common import *
@step('I fill in the registration form$')
......@@ -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 "([^"]*)"$')
......
......@@ -7,7 +7,7 @@ from lettuce import world, step
@step('I see the correct settings and default values$')
def i_see_the_correct_settings_and_values(step):
world.verify_all_setting_entries([['Default Speed', 'OEoXaMPEzfM', False],
['Display Name', 'Video Title', False],
['Display Name', 'Video', False],
['Download Track', '', False],
['Download Video', '', False],
['Show Captions', 'True', False],
......
"""
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):
......@@ -13,3 +25,171 @@ class UsersTestCase(CourseTestCase):
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,
'STUDIO_REQUEST_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
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
......@@ -40,10 +42,22 @@ def index(request):
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):
......@@ -144,3 +158,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
......@@ -6,7 +6,13 @@ 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.conf import settings
from django.dispatch import receiver
from mitxmako.shortcuts import render_to_string
import logging
log = logging.getLogger("studio.coursecreatoradmin")
def get_email(obj):
......@@ -60,4 +66,25 @@ 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'])
user = kwargs['user']
updated_state = kwargs['state']
update_course_creator_group(kwargs['caller'], user, updated_state == CourseCreator.GRANTED)
studio_request_email = settings.MITX_FEATURES.get('STUDIO_REQUEST_EMAIL','')
context = {'studio_request_email': studio_request_email}
subject = render_to_string('emails/course_creator_subject.txt', context)
subject = ''.join(subject.splitlines())
if updated_state == CourseCreator.GRANTED:
message_template = 'emails/course_creator_granted.txt'
elif updated_state == CourseCreator.DENIED:
message_template = 'emails/course_creator_denied.txt'
else:
# changed to unrequested or pending
message_template = 'emails/course_creator_revoked.txt'
message = render_to_string(message_template, context)
try:
user.email_user(subject, message, studio_request_email)
except:
log.warning("Unable to send course creator status e-mail to %s", user.email)
......@@ -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)
......@@ -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,
state=instance.state
)
instance.state_changed = timezone.now()
instance.orig_state = instance.state
instance.save()
......@@ -13,6 +13,11 @@ from course_creators.models import CourseCreator
from auth.authz import is_user_in_creator_group
def mock_render_to_string(template_name, context):
"""Return a string that encodes template_name and context"""
return str((template_name, context))
class CourseCreatorAdminTest(TestCase):
"""
Tests for course creator admin.
......@@ -32,17 +37,40 @@ class CourseCreatorAdminTest(TestCase):
self.creator_admin = CourseCreatorAdmin(self.table_entry, AdminSite())
def test_change_status(self):
@mock.patch('course_creators.admin.render_to_string', mock.Mock(side_effect=mock_render_to_string, autospec=True))
@mock.patch('django.contrib.auth.models.User.email_user')
def test_change_status(self, email_user):
"""
Tests that updates to state impact the creator group maintained in authz.py.
Tests that updates to state impact the creator group maintained in authz.py and that e-mails are sent.
"""
STUDIO_REQUEST_EMAIL = 'mark@marky.mark'
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))
context = {'studio_request_email': STUDIO_REQUEST_EMAIL}
if state == CourseCreator.GRANTED:
template = 'emails/course_creator_granted.txt'
elif state == CourseCreator.DENIED:
template = 'emails/course_creator_denied.txt'
else:
template = 'emails/course_creator_revoked.txt'
email_user.assert_called_with(
mock_render_to_string('emails/course_creator_subject.txt', context),
mock_render_to_string(template, context),
STUDIO_REQUEST_EMAIL
)
with mock.patch.dict(
'django.conf.settings.MITX_FEATURES',
{
"ENABLE_CREATOR_GROUP": True,
"STUDIO_REQUEST_EMAIL": STUDIO_REQUEST_EMAIL
}):
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))
......
......@@ -7,7 +7,7 @@ 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.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
......@@ -26,22 +26,19 @@ class CourseCreatorView(TestCase):
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).
......@@ -57,7 +54,7 @@ class CourseCreatorView(TestCase):
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))
......@@ -69,3 +66,27 @@ class CourseCreatorView(TestCase):
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))
......@@ -2,32 +2,38 @@
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):
def add_user_with_status_unrequested(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.
(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):
"""
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
(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)
update_course_creator_group(caller, user, True)
if _add_user(user, CourseCreator.GRANTED):
update_course_creator_group(caller, user, True)
def update_course_creator_group(caller, user, add):
......@@ -61,16 +67,33 @@ def get_course_creator_status(user):
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.
Returns True if user was added to table, else False.
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
(state will not be changed, method will return False).
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.save()
return True
return False
......@@ -42,8 +42,8 @@ MITX_FEATURES = {
# do not display video when running automated acceptance tests
'STUB_VIDEO_FOR_TESTING': False,
# email address for staff (eg to request course creation)
'STAFF_EMAIL': '',
# email address for studio staff (eg to request course creation)
'STUDIO_REQUEST_EMAIL': '',
'STUDIO_NPS_SURVEY': True,
......@@ -62,9 +62,6 @@ MITX_FEATURES = {
}
ENABLE_JASMINE = False
# needed to use lms student app
GENERATE_RANDOM_USER_CREDENTIALS = False
############################# SET PATH INFORMATION #############################
PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/cms
......@@ -108,9 +105,12 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.static',
'django.contrib.messages.context_processors.messages',
'django.contrib.auth.context_processors.auth', # this is required for admin
'django.core.context_processors.csrf', # necessary for csrf protection
)
# add csrf support unless disabled for load testing
if not MITX_FEATURES.get('AUTOMATIC_AUTH_FOR_LOAD_TESTING'):
TEMPLATE_CONTEXT_PROCESSORS += ('django.core.context_processors.csrf',) # necessary for csrf protection
LMS_BASE = None
#################### CAPA External Code Evaluation #############################
......@@ -142,7 +142,6 @@ MIDDLEWARE_CLASSES = (
'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'method_override.middleware.MethodOverrideMiddleware',
# Instead of AuthenticationMiddleware, we use a cache-backed version
......@@ -158,6 +157,10 @@ MIDDLEWARE_CLASSES = (
'django.middleware.transaction.TransactionMiddleware'
)
# add in csrf middleware unless disabled for load testing
if not MITX_FEATURES.get('AUTOMATIC_AUTH_FOR_LOAD_TESTING'):
MIDDLEWARE_CLASSES = MIDDLEWARE_CLASSES + ('django.middleware.csrf.CsrfViewMiddleware',)
############################ SIGNAL HANDLERS ################################
# This is imported to register the exception signal handling that logs exceptions
import monitoring.exceptions # noqa
......
......@@ -60,12 +60,10 @@ $(document).ready(function() {
$('.nav-dd .nav-item .title').removeClass('is-selected');
});
$('.nav-dd .nav-item .title').click(function(e) {
$('.nav-dd .nav-item').click(function(e) {
$subnav = $(this).parent().find('.wrapper-nav-sub');
$title = $(this).parent().find('.title');
e.preventDefault();
e.stopPropagation();
$subnav = $(this).find('.wrapper-nav-sub');
$title = $(this).find('.title');
if ($subnav.hasClass('is-shown')) {
$subnav.removeClass('is-shown');
......@@ -75,6 +73,9 @@ $(document).ready(function() {
$('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown');
$title.addClass('is-selected');
$subnav.addClass('is-shown');
// if propogation is not stopped, the event will bubble up to the
// body element, which will close the dropdown.
e.stopPropagation();
}
});
......@@ -596,11 +597,11 @@ function cancelNewSection(e) {
function addNewCourse(e) {
e.preventDefault();
$(e.target).hide();
$('.new-course-button').addClass('disabled');
$(e.target).addClass('disabled');
var $newCourse = $($('#new-course-template').html());
var $cancelButton = $newCourse.find('.new-course-cancel');
$('.inner-wrapper').prepend($newCourse);
$('.courses').prepend($newCourse);
$newCourse.find('.new-course-name').focus().select();
$newCourse.find('form').bind('submit', saveNewCourse);
$cancelButton.bind('click', cancelNewCourse);
......@@ -645,7 +646,7 @@ function saveNewCourse(e) {
function cancelNewCourse(e) {
e.preventDefault();
$('.new-course-button').show();
$('.new-course-button').removeClass('disabled');
$(this).parents('section.new-course').remove();
}
......
......@@ -368,42 +368,6 @@ p, ul, ol, dl {
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 {
......@@ -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
.nav-related, .nav-page {
......
......@@ -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/)
// ====================
// 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
// * 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
// 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
// ====================
......
// 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
.notice-incontext {
@extend .ui-well;
border-radius: ($baseline/10);
position: relative;
overflow: hidden;
margin-bottom: $baseline;
.title {
@extend .t-title7;
margin-bottom: ($baseline/4);
font-weight: 600;
font-weight: 700;
}
.copy {
@extend .t-copy-sub1;
@include transition(opacity $tmg-f2 ease-in-out 0s);
opacity: 0.75;
margin-bottom: $baseline;
&:last-child {
margin-bottom: 0;
}
}
strong {
font-weight: 600;
}
&:hover {
&.has-status {
.copy {
opacity: 1.0;
.status-indicator {
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 {
background: $yellow-l5;
.copy {
.status-indicator {
background: $yellow;
}
title {
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 {
}
}
}
// ====================
// 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 @@
body.dashboard {
.my-classes {
margin-top: $baseline;
// temp
.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 {
margin-top: 20px;
// elements - course creation rights controls
.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: 1px solid $darkGrey;
background: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1);
border: 1px solid $gray;
background: $white;
box-shadow: 0 1px 2px $shadow-l1;
li {
.course-item {
position: relative;
border-bottom: 1px solid $mediumGrey;
border-bottom: 1px solid $gray-l1;
&:last-child {
border-bottom: none;
......@@ -56,7 +345,7 @@ body.dashboard {
.view-live-button {
z-index: 10000;
position: absolute;
top: 15px;
top: ($baseline*0.75);
right: $baseline;
padding: ($baseline/4) ($baseline/2);
opacity: 0.0;
......@@ -70,17 +359,25 @@ body.dashboard {
}
.new-course {
padding: 15px 25px;
margin-top: 20px;
@include clearfix();
padding: ($baseline*0.75) ($baseline*1.25);
margin-top: $baseline;
border-radius: 3px;
border: 1px solid $darkGrey;
background: #fff;
border: 1px solid $gray;
background: $white;
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 {
margin-bottom: 15px;
@include clearfix;
@include clearfix();
margin-bottom: ($baseline*0.75);
}
.column {
......@@ -97,8 +394,8 @@ body.dashboard {
}
label {
@extend .t-title7;
display: block;
font-size: 13px;
font-weight: 700;
}
......@@ -109,7 +406,7 @@ body.dashboard {
}
.new-course-name {
font-size: 19px;
@extend .t-title5;
font-weight: 300;
}
......@@ -120,5 +417,9 @@ body.dashboard {
.new-course-cancel {
@include white-button;
}
.item-details {
padding-bottom: 0;
}
}
}
......@@ -2,14 +2,31 @@
<%inherit file="base.html" />
<%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>
<div class="wrapper-content wrapper">
<section class="content activation is-active">
<article class="content-primary" role="main">
</article>
<section class="activation">
<h1>${_("Account already active!")}</h1>
<p>${_('This account has already been activated.')}<a href="/signin">${_("Log in here.")}</a></p>
</div>
</section>
<div class="notice notice-incontext notice-instruction has-actions">
<div class="msg">
<h2 class="title">${_("Your account is already active")}</h2>
<div class="copy">
<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>
......@@ -2,12 +2,31 @@
<%inherit file="base.html" />
<%block name="content">
<section class="tos">
<div>
<h1>${_("Activation Complete!")}</h1>
<p>${_('Thanks for activating your account.')}<a href="/signin">${_("Log in here.")}</a></p>
<div class="wrapper-mast wrapper sr">
<header class="mast">
<h1 class="page-header">${_("Studio Account Activation")}</h1>
</header>
</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>
......@@ -2,14 +2,32 @@
<%inherit file="base.html" />
<%block name="content">
<section class="tos">
<div>
<h1>${_("Activation Invalid")}</h1>
<div class="wrapper-mast wrapper sr">
<header class="mast">
<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(
link_start='<a href="/">', link_end='</a>')}</p>
<ul class="list-actions">
<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>
</section>
</%block>
<%! from django.utils.translation import ugettext as _ %>
${_("Your request for course creation rights to edX Studio have been denied. If you believe this was in error, please contact: ")}
${ studio_request_email }
<%! from django.utils.translation import ugettext as _ %>
${_("Your request for course creation rights to edX Studio have been granted. To create your first course, visit:")}
% if is_secure:
https://${ site }
% else:
http://${ site }
% endif
<%! from django.utils.translation import ugettext as _ %>
${_("Your course creation rights to edX Studio have been revoked. If you believe this was in error, please contact: ")}
${ studio_request_email }
<%! from django.utils.translation import ugettext as _ %>
${_("Your course creator status for edX Studio")}
......@@ -12,6 +12,7 @@ admin.autodiscover()
urlpatterns = ('', # nopep8
url(r'^$', 'contentstore.views.howitworks', name='homepage'),
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'^subsection/(?P<location>.*?)$', 'contentstore.views.edit_subsection', name='edit_subsection'),
url(r'^preview_component/(?P<location>.*?)$', 'contentstore.views.preview_component', name='preview_component'),
......@@ -149,6 +150,12 @@ if settings.MITX_FEATURES.get('ENABLE_SERVICE_STATUS'):
urlpatterns += (url(r'^admin/', include(admin.site.urls)),)
# enable automatic login
if settings.MITX_FEATURES.get('AUTOMATIC_AUTH_FOR_LOAD_TESTING'):
urlpatterns += (
url(r'^auto_auth$', 'student.views.auto_auth'),
)
urlpatterns = patterns(*urlpatterns)
# Custom error pages
......
from django.test import TestCase
from django.test.client import Client
from django.contrib.auth.models import User
from util.testing import UrlResetMixin
from mock import patch
from django.core.urlresolvers import reverse, NoReverseMatch
class AutoAuthEnabledTestCase(UrlResetMixin, TestCase):
"""
Tests for the Auto auth view that we have for load testing.
"""
@patch.dict("django.conf.settings.MITX_FEATURES", {"AUTOMATIC_AUTH_FOR_LOAD_TESTING": True})
def setUp(self):
# Patching the settings.MITX_FEATURES['AUTOMATIC_AUTH_FOR_LOAD_TESTING']
# value affects the contents of urls.py,
# so we need to call super.setUp() which reloads urls.py (because
# of the UrlResetMixin)
super(AutoAuthEnabledTestCase, self).setUp()
self.url = '/auto_auth'
self.cms_csrf_url = "signup"
self.lms_csrf_url = "signin_user"
self.client = Client()
def test_create_user(self):
"""
Test that user gets created when visiting the page.
"""
self.client.get(self.url)
qset = User.objects.all()
# assert user was created and is active
self.assertEqual(qset.count(), 1)
user = qset[0]
assert user.is_active
@patch('student.views.random.randint')
def test_create_multiple_users(self, randint):
"""
Test to make sure multiple users are created.
"""
randint.return_value = 1
self.client.get(self.url)
randint.return_value = 2
self.client.get(self.url)
qset = User.objects.all()
# make sure that USER_1 and USER_2 were created
self.assertEqual(qset.count(), 2)
@patch.dict("django.conf.settings.MITX_FEATURES", {"MAX_AUTO_AUTH_USERS": 1})
def test_login_already_created_user(self):
"""
Test that when we have reached the limit for automatic users
a subsequent request results in an already existant one being
logged in.
"""
# auto-generate 1 user (the max)
url = '/auto_auth'
self.client.get(url)
# go to the site again
self.client.get(url)
qset = User.objects.all()
# make sure it is the same user
self.assertEqual(qset.count(), 1)
class AutoAuthDisabledTestCase(UrlResetMixin, TestCase):
"""
Test that the page is inaccessible with default settings
"""
@patch.dict("django.conf.settings.MITX_FEATURES", {"AUTOMATIC_AUTH_FOR_LOAD_TESTING": False})
def setUp(self):
# Patching the settings.MITX_FEATURES['AUTOMATIC_AUTH_FOR_LOAD_TESTING']
# value affects the contents of urls.py,
# so we need to call super.setUp() which reloads urls.py (because
# of the UrlResetMixin)
super(AutoAuthDisabledTestCase, self).setUp()
self.url = '/auto_auth'
self.client = Client()
def test_auto_auth_disabled(self):
"""
Make sure automatic authentication is disabled.
"""
response = self.client.get(self.url)
self.assertEqual(response.status_code, 404)
def test_csrf_enabled(self):
"""
test that when not load testing, csrf protection is on
"""
cms_csrf_url = "signup"
lms_csrf_url = "signin_user"
self.client = Client(enforce_csrf_checks=True)
try:
csrf_protected_url = reverse(cms_csrf_url)
response = self.client.post(csrf_protected_url)
except NoReverseMatch:
csrf_protected_url = reverse(lms_csrf_url)
response = self.client.post(csrf_protected_url)
self.assertEqual(response.status_code, 403)
......@@ -19,6 +19,7 @@ from django.core.context_processors import csrf
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.core.validators import validate_email, validate_slug, ValidationError
from django.core.exceptions import ObjectDoesNotExist
from django.db import IntegrityError, transaction
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotAllowed, Http404
from django.shortcuts import redirect
......@@ -674,18 +675,20 @@ def create_account(request, post_override=None):
subject = ''.join(subject.splitlines())
message = render_to_string('emails/activation_email.txt', d)
try:
if settings.MITX_FEATURES.get('REROUTE_ACTIVATION_EMAIL'):
dest_addr = settings.MITX_FEATURES['REROUTE_ACTIVATION_EMAIL']
message = ("Activation for %s (%s): %s\n" % (user, user.email, profile.name) +
'-' * 80 + '\n\n' + message)
send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [dest_addr], fail_silently=False)
elif not settings.GENERATE_RANDOM_USER_CREDENTIALS:
res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
except:
log.warning('Unable to send activation email to user', exc_info=True)
js['value'] = _('Could not send activation e-mail.')
return HttpResponse(json.dumps(js))
# dont send email if we are doing load testing or random user generation for some reason
if not (settings.MITX_FEATURES.get('AUTOMATIC_AUTH_FOR_LOAD_TESTING')):
try:
if settings.MITX_FEATURES.get('REROUTE_ACTIVATION_EMAIL'):
dest_addr = settings.MITX_FEATURES['REROUTE_ACTIVATION_EMAIL']
message = ("Activation for %s (%s): %s\n" % (user, user.email, profile.name) +
'-' * 80 + '\n\n' + message)
send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [dest_addr], fail_silently=False)
else:
res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
except:
log.warning('Unable to send activation email to user', exc_info=True)
js['value'] = _('Could not send activation e-mail.')
return HttpResponse(json.dumps(js))
# Immediately after a user creates an account, we log them in. They are only
# logged in until they close the browser. They can't log in again until they click
......@@ -902,32 +905,51 @@ def create_exam_registration(request, post_override=None):
return HttpResponse(json.dumps(js), mimetype="application/json")
def get_random_post_override():
def auto_auth(request):
"""
Return a dictionary suitable for passing to post_vars of _do_create_account or post_override
of create_account, with random user info.
Automatically logs the user in with a generated random credentials
This view is only accessible when
settings.MITX_SETTINGS['AUTOMATIC_AUTH_FOR_LOAD_TESTING'] is true.
"""
def id_generator(size=6, chars=string.ascii_uppercase + string.ascii_lowercase + string.digits):
return ''.join(random.choice(chars) for x in range(size))
return {'username': "random_" + id_generator(),
'email': id_generator(size=10, chars=string.ascii_lowercase) + "_dummy_test@mitx.mit.edu",
'password': id_generator(),
'name': (id_generator(size=5, chars=string.ascii_lowercase) + " " +
id_generator(size=7, chars=string.ascii_lowercase)),
'honor_code': u'true',
'terms_of_service': u'true', }
def get_dummy_post_data(username, password):
"""
Return a dictionary suitable for passing to post_vars of _do_create_account or post_override
of create_account, with specified username and password.
"""
return {'username': username,
'email': username + "_dummy_test@mitx.mit.edu",
'password': password,
'name': username + " " + username,
'honor_code': u'true',
'terms_of_service': u'true', }
def create_random_account(create_account_function):
def inner_create_random_account(request):
return create_account_function(request, post_override=get_random_post_override())
# generate random user ceredentials from a small name space (determined by settings)
name_base = 'USER_'
pass_base = 'PASS_'
return inner_create_random_account
max_users = settings.MITX_FEATURES.get('MAX_AUTO_AUTH_USERS', 200)
number = random.randint(1, max_users)
# TODO (vshnayder): do we need GENERATE_RANDOM_USER_CREDENTIALS for anything?
if settings.GENERATE_RANDOM_USER_CREDENTIALS:
create_account = create_random_account(create_account)
username = name_base + str(number)
password = pass_base + str(number)
# if they already are a user, log in
try:
user = User.objects.get(username=username)
user = authenticate(username=username, password=password)
login(request, user)
# else create and activate account info
except ObjectDoesNotExist:
post_override = get_dummy_post_data(username, password)
create_account(request, post_override=post_override)
request.user.is_active = True
request.user.save()
# return empty success
return HttpResponse('')
@ensure_csrf_cookie
......
......@@ -75,7 +75,7 @@ def initial_setup(server):
# If we were unable to get a valid session within the limit of attempts,
# then we cannot run the tests.
if not success:
raise IOError("Could not acquire valid ChromeDriver browser session.")
raise IOError("Could not acquire valid {driver} browser session.".format(driver=browser_driver))
# Set the browser size to 1280x1024
world.browser.driver.set_window_size(1280, 1024)
......
......@@ -216,22 +216,19 @@ def save_the_html(path='/tmp'):
@world.absorb
def click_course_content():
course_content_css = 'li.nav-course-courseware'
if world.browser.is_element_present_by_css(course_content_css):
world.css_click(course_content_css)
world.css_click(course_content_css)
@world.absorb
def click_course_settings():
course_settings_css = 'li.nav-course-settings'
if world.browser.is_element_present_by_css(course_settings_css):
world.css_click(course_settings_css)
world.css_click(course_settings_css)
@world.absorb
def click_tools():
tools_css = 'li.nav-course-tools'
if world.browser.is_element_present_by_css(tools_css):
world.css_click(tools_css)
world.css_click(tools_css)
@world.absorb
......
......@@ -211,6 +211,8 @@ nav.sequence-nav {
@include transition(all .1s $ease-in-out-quart 0s);
white-space: pre;
z-index: 99;
visibility: hidden;
pointer-events: none;
&:empty {
background: none;
......@@ -238,6 +240,7 @@ nav.sequence-nav {
display: block;
margin-top: 4px;
opacity: 1.0;
visibility: visible;
}
}
}
......@@ -263,6 +266,7 @@ nav.sequence-nav {
border: 1px solid #ccc;
@include linear-gradient(top, #eee, #ddd);
box-shadow: 0 1px 0 rgba(255, 255, 255, .7) inset;
z-index: 1;
&.prev, &.next {
......@@ -270,7 +274,7 @@ nav.sequence-nav {
background-position: center;
background-repeat: no-repeat;
display: block;
height: 34px;
height: 100%;
width: 40px;
text-indent: -9999px;
@include transition(all .2s $ease-in-out-quad 0s);
......
......@@ -12,10 +12,14 @@ class DiscussionFields(object):
display_name = String(
display_name="Display Name",
help="Display name for this module",
default="Discussion Tag",
scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content,
default="<discussion></discussion>")
default="Discussion",
scope=Scope.settings
)
data = String(
help="XML data for the problem",
scope=Scope.content,
default="<discussion></discussion>"
)
discussion_category = String(
display_name="Category",
default="Week 1",
......
......@@ -25,7 +25,7 @@ class HtmlFields(object):
scope=Scope.settings,
# it'd be nice to have a useful default but it screws up other things; so,
# use display_name_with_default for those
default="Blank HTML Page"
default="Text"
)
data = String(help="Html contents to display for this module", default=u"", scope=Scope.content)
source_code = String(help="Source code for LaTeX documents. This feature is not well-supported.", scope=Scope.settings)
......
......@@ -225,6 +225,8 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
non_draft_loc = location.replace(revision=None)
metadata_to_inherit = self.cached_metadata.get(non_draft_loc.url(), {})
inherit_metadata(module, metadata_to_inherit)
# decache any computed pending field settings
module.save()
return module
except:
log.warning("Failed to load descriptor", exc_info=True)
......@@ -630,6 +632,8 @@ class MongoModuleStore(ModuleStoreBase):
definition_data = {}
dbmodel = self._create_new_model_data(location.category, location, definition_data, metadata)
xmodule = xblock_class(system, dbmodel)
# decache any pending field settings from init
xmodule.save()
return xmodule
def save_xmodule(self, xmodule):
......
......@@ -116,4 +116,6 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
module.previous_version = json_data.get('previous_version')
module.update_version = json_data.get('update_version')
module.definition_locator = self.modulestore.definition_locator(definition)
# decache any pending field settings
module.save()
return module
......@@ -146,7 +146,7 @@ class Progress(object):
sending Progress objects to js to limit dependencies.
'''
if progress is None:
return "NA"
return "0"
return progress.ternary_str()
@staticmethod
......@@ -157,5 +157,5 @@ class Progress(object):
passing Progress objects to js to limit dependencies.
'''
if progress is None:
return "NA"
return "0"
return str(progress)
......@@ -90,15 +90,15 @@ class ProgressTest(unittest.TestCase):
self.assertEqual(Progress.to_js_status_str(self.not_started), "none")
self.assertEqual(Progress.to_js_status_str(self.half_done), "in_progress")
self.assertEqual(Progress.to_js_status_str(self.done), "done")
self.assertEqual(Progress.to_js_status_str(None), "NA")
self.assertEqual(Progress.to_js_status_str(None), "0")
def test_to_js_detail_str(self):
'''Test the Progress.to_js_detail_str() method'''
f = Progress.to_js_detail_str
for p in (self.not_started, self.half_done, self.done):
self.assertEqual(f(p), str(p))
# But None should be encoded as NA
self.assertEqual(f(None), "NA")
# But None should be encoded as 0
self.assertEqual(f(None), "0")
def test_add(self):
'''Test the Progress.add_counts() method'''
......
......@@ -27,11 +27,13 @@ class VideoFields(object):
scope=Scope.settings,
# it'd be nice to have a useful default but it screws up other things; so,
# use display_name_with_default for those
default="Video Title"
default="Video"
)
data = String(help="XML data for the problem",
data = String(
help="XML data for the problem",
default='',
scope=Scope.content)
scope=Scope.content
)
position = Integer(help="Current position in the video", scope=Scope.user_state, default=0)
show_captions = Boolean(help="This controls whether or not captions are shown by default.", display_name="Show Captions", scope=Scope.settings, default=True)
youtube_id_1_0 = String(help="This is the Youtube ID reference for the normal speed video.", display_name="Default Speed", scope=Scope.settings, default="OEoXaMPEzfM")
......
......@@ -537,11 +537,14 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
system: Module system
"""
return self.module_class(
# save any field changes
module = self.module_class(
system,
self,
system.xblock_model_data(self),
)
module.save()
return module
def has_dynamic_children(self):
"""
......@@ -613,7 +616,13 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
new_block = system.xblock_from_json(cls, usage_id, json_data)
if parent_xblock is not None:
parent_xblock.children.append(new_block)
children = parent_xblock.children
children.append(new_block)
# trigger setter method by using top level field access
parent_xblock.children = children
# decache pending children field settings (Note, truly persisting at this point would break b/c
# persistence assumes children is a list of ids not actual xblocks)
parent_xblock.save()
return new_block
@classmethod
......
......@@ -91,7 +91,7 @@
}
&.disabled, &[disabled] {
&.disabled, &[disabled], &.is-disabled {
cursor: default;
pointer-events: none;
opacity: 0.5;
......
import json
import logging
import re
import sys
from functools import partial
......@@ -13,7 +12,6 @@ from django.http import Http404
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
import pyparsing
from requests.auth import HTTPBasicAuth
from statsd import statsd
......@@ -599,14 +597,14 @@ def _check_files_limits(files):
# Check number of files submitted
if len(inputfiles) > settings.MAX_FILEUPLOADS_PER_INPUT:
msg = 'Submission aborted! Maximum %d files may be submitted at once' %\
msg = 'Submission aborted! Maximum %d files may be submitted at once' % \
settings.MAX_FILEUPLOADS_PER_INPUT
return msg
# Check file sizes
for inputfile in inputfiles:
if inputfile.size > settings.STUDENT_FILEUPLOAD_MAX_SIZE: # Bytes
msg = 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %\
if inputfile.size > settings.STUDENT_FILEUPLOAD_MAX_SIZE: # Bytes
msg = 'Submission aborted! Your file "%s" is too large (max size: %d MB)' % \
(inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2))
return msg
......
......@@ -221,7 +221,7 @@ class TestTOC(TestCase):
'format': '', 'due': None, 'active': False},
{'url_name': 'video_123456789012', 'display_name': 'Test Video', 'graded': True,
'format': '', 'due': None, 'active': False},
{'url_name': 'video_4f66f493ac8f', 'display_name': 'Video Title', 'graded': True,
{'url_name': 'video_4f66f493ac8f', 'display_name': 'Video', 'graded': True,
'format': '', 'due': None, 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'},
{'active': False, 'sections':
......@@ -230,7 +230,6 @@ class TestTOC(TestCase):
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, None, model_data_cache)
print actual
assert reduce(lambda x, y: x and (y in actual), expected, True)
def test_toc_toy_from_section(self):
......@@ -249,7 +248,7 @@ class TestTOC(TestCase):
'format': '', 'due': None, 'active': True},
{'url_name': 'video_123456789012', 'display_name': 'Test Video', 'graded': True,
'format': '', 'due': None, 'active': False},
{'url_name': 'video_4f66f493ac8f', 'display_name': 'Video Title', 'graded': True,
{'url_name': 'video_4f66f493ac8f', 'display_name': 'Video', 'graded': True,
'format': '', 'due': None, 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'},
{'active': False, 'sections':
......
......@@ -122,6 +122,11 @@ class UserViewSetTest(UserApiTestCase):
def test_list_unauthorized(self):
self.assertHttpForbidden(self.client.get(self.LIST_URI))
@override_settings(DEBUG=True)
@override_settings(EDX_API_KEY=None)
def test_debug_auth(self):
self.assertHttpOK(self.client.get(self.LIST_URI))
def test_get_list_empty(self):
User.objects.all().delete()
result = self.get_json(self.LIST_URI)
......@@ -220,6 +225,11 @@ class UserPreferenceViewSetTest(UserApiTestCase):
def test_list_unauthorized(self):
self.assertHttpForbidden(self.client.get(self.LIST_URI))
@override_settings(DEBUG=True)
@override_settings(EDX_API_KEY=None)
def test_debug_auth(self):
self.assertHttpOK(self.client.get(self.LIST_URI))
def test_get_list_empty(self):
UserPreference.objects.all().delete()
result = self.get_json(self.LIST_URI)
......@@ -252,6 +262,26 @@ class UserPreferenceViewSetTest(UserApiTestCase):
self.assertPrefIsValid(pref)
self.assertEqual(pref["key"], "key0")
def test_get_list_filter_user_empty(self):
def test_id(user_id):
result = self.get_json(self.LIST_URI, data={"user": user_id})
self.assertEqual(result["count"], 0)
self.assertEqual(result["results"], [])
test_id(self.users[2].id)
# TODO: If the given id does not match a user, then the filter is a no-op
# test_id(42)
# test_id("asdf")
def test_get_list_filter_user_nonempty(self):
user_id = self.users[0].id
result = self.get_json(self.LIST_URI, data={"user": user_id})
self.assertEqual(result["count"], 2)
prefs = result["results"]
self.assertEqual(len(prefs), 2)
for pref in prefs:
self.assertPrefIsValid(pref)
self.assertEqual(pref["user"]["id"], user_id)
def test_get_list_pagination(self):
first_page = self.get_json(self.LIST_URI, data={"page_size": 2})
self.assertEqual(first_page["count"], 3)
......
......@@ -12,11 +12,16 @@ class ApiKeyHeaderPermission(permissions.BasePermission):
"""
Check for permissions by matching the configured API key and header
settings.EDX_API_KEY must be set, and the X-Edx-Api-Key HTTP header must
be present in the request and match the setting.
If settings.DEBUG is True and settings.EDX_API_KEY is not set or None,
then allow the request. Otherwise, allow the request if and only if
settings.EDX_API_KEY is set and the X-Edx-Api-Key HTTP header is
present in the request and matches the setting.
"""
api_key = getattr(settings, "EDX_API_KEY", None)
return api_key is not None and request.META.get("HTTP_X_EDX_API_KEY") == api_key
return (
(settings.DEBUG and api_key is None) or
(api_key is not None and request.META.get("HTTP_X_EDX_API_KEY") == api_key)
)
class UserViewSet(viewsets.ReadOnlyModelViewSet):
......@@ -31,7 +36,7 @@ class UserPreferenceViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = (ApiKeyHeaderPermission,)
queryset = UserPreference.objects.all()
filter_backends = (filters.DjangoFilterBackend,)
filter_fields = ("key",)
filter_fields = ("key", "user")
serializer_class = UserPreferenceSerializer
paginate_by = 10
paginate_by_param = "page_size"
......@@ -178,6 +178,10 @@ for name, value in ENV_TOKENS.get("CODE_JAIL", {}).items():
COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", [])
# automatic log in for load testing
MITX_FEATURES['AUTOMATIC_AUTH_FOR_LOAD_TESTING'] = ENV_TOKENS.get('AUTOMATIC_AUTH_FOR_LOAD_TESTING')
MITX_FEATURES['MAX_AUTO_AUTH_USERS'] = ENV_TOKENS.get('MAX_AUTO_AUTH_USERS')
############################## SECURE AUTH ITEMS ###############
# Secret things: passwords, access keys, etc.
......
......@@ -37,7 +37,6 @@ PLATFORM_NAME = "edX"
COURSEWARE_ENABLED = True
ENABLE_JASMINE = False
GENERATE_RANDOM_USER_CREDENTIALS = False
PERFSTATS = False
DISCUSSION_SETTINGS = {
......@@ -145,6 +144,9 @@ MITX_FEATURES = {
# Allow use of the hint managment instructor view.
'ENABLE_HINTER_INSTRUCTOR_VIEW': False,
# for load testing
'AUTOMATIC_AUTH_FOR_LOAD_TESTING': False,
# Toggle to enable chat availability (configured on a per-course
# basis in Studio)
'ENABLE_CHAT': False
......@@ -218,7 +220,6 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'django.contrib.messages.context_processors.messages',
#'django.core.context_processors.i18n',
'django.contrib.auth.context_processors.auth', # this is required for admin
'django.core.context_processors.csrf', # necessary for csrf protection
# Added for django-wiki
'django.core.context_processors.media',
......@@ -231,6 +232,10 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'mitxmako.shortcuts.marketing_link_context_processor',
)
# add csrf support unless disabled for load testing
if not MITX_FEATURES.get('AUTOMATIC_AUTH_FOR_LOAD_TESTING'):
TEMPLATE_CONTEXT_PROCESSORS += ('django.core.context_processors.csrf',) # necessary for csrf protection
STUDENT_FILEUPLOAD_MAX_SIZE = 4 * 1000 * 1000 # 4 MB
MAX_FILEUPLOADS_PER_INPUT = 20
......@@ -469,7 +474,6 @@ MIDDLEWARE_CLASSES = (
'django_comment_client.middleware.AjaxExceptionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
# Instead of AuthenticationMiddleware, we use a cached backed version
#'django.contrib.auth.middleware.AuthenticationMiddleware',
......@@ -488,6 +492,10 @@ MIDDLEWARE_CLASSES = (
'codejail.django_integration.ConfigureCodeJailMiddleware',
)
# add in csrf middleware unless disabled for load testing
if not MITX_FEATURES.get('AUTOMATIC_AUTH_FOR_LOAD_TESTING'):
MIDDLEWARE_CLASSES = MIDDLEWARE_CLASSES + ('django.middleware.csrf.CsrfViewMiddleware',)
############################### Pipeline #######################################
STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage'
......
......@@ -257,7 +257,7 @@ if SEGMENT_IO_LMS_KEY:
########################## USER API ########################
EDX_API_KEY = ''
EDX_API_KEY = None
#####################################################################
# Lastly, see if the developer has any local overrides.
......
......@@ -2,70 +2,141 @@
// shame file - used for any bad-form/orphaned scss that knowingly violate edX FED architecture/standards (see - http://csswizardry.com/2013/04/shame-css/)
// ====================
// marketing site - registration iframe band-aid (poor form enough to isolate out)
.view-partial-mktgregister {
background: transparent;
// edx.org marketing site - 7/2013 visual button revamp
// extends btn
.m-btn {
@include box-sizing(border-box);
@include transition(color 0.25s ease-in-out, background 0.25s ease-in-out, box-shadow 0.25s ease-in-out);
display: inline-block;
cursor: pointer;
text-decoration: none;
&:hover, &:active {
// dimensions needed for course about page on marketing site
.wrapper-view {
overflow: hidden;
}
// button elements - not a better place to put these, sadly
.btn {
@include box-sizing('border-box');
display: block;
padding: $baseline/2;
text-transform: lowercase;
color: $white;
letter-spacing: 0.1rem;
cursor: pointer;
text-align: center;
border: none !important;
text-decoration: none;
text-shadow: none;
letter-spacing: 0.1rem;
font-size: 17px;
font-weight: 300;
box-shadow: 0 !important;
strong {
font-weight: 400;
text-transform: none;
}
&.disabled, &[disabled] {
cursor: default;
pointer-events: none;
}
}
.btn-primary {
@extend .btn;
@include linear-gradient($m-blue-s1 5%, $m-blue-d1 95%);
.m-btn-pill {
border-radius: ($baseline/5);
}
// no hover state conventions to follow from marketing :/
&:hover, &:active {
.m-btn-rounded {
border-radius: ($baseline/2);
}
.m-btn-edged {
border-radius: ($baseline/10);
}
// primary button
.m-btn-base {
@extend .m-btn;
@extend .m-btn-edged;
border: none;
padding:($baseline/2) ($baseline);
text-align: center;
text-shadow: none;
font-weight: 500;
letter-spacing: 0;
&.disabled, &[disabled], &.is-disabled {
background: $action-primary-disabled-bg;
&:hover {
background: $action-primary-disabled-bg !important; // needed for IE currently
}
}
}
.btn-secondary {
@extend .btn;
@include linear-gradient($m-gray 5%, $m-gray-d1 95%);
// primary button
.m-btn-primary {
@extend .m-btn-base;
box-shadow: 0 2px 1px 0 $action-primary-shadow;
background: $action-primary-bg;
color: $action-primary-fg;
// no hover state conventions to follow from marketing :/
&:hover, &:active {
&:hover, &:active {
background: $action-primary-focused-bg;
}
&.current, &.active {
box-shadow: inset 0 2px 1px 1px $action-primary-active-shadow;
background: $action-primary-active-bg;
color: $action-primary-active-fg;
&:hover, &:active {
box-shadow: inset 0 2px 1px 1px $action-primary-active-focused-shadow;
color: $action-primary-active-focused-fg;
}
}
.btn-tertiary {
@extend .btn;
background: $m-blue-l1;
color: $m-blue;
&.disabled, &[disabled] {
box-shadow: none;
background: $action-primary-disabled-bg; // needed for IE currently
}
}
// no hover state conventions to follow from marketing :/
&:hover, &:active {
// secondary button
.m-btn-secondary {
@extend .m-btn-base;
box-shadow: 0 2px 1px 0 $action-secondary-shadow;
background: $action-secondary-bg;
color: $action-secondary-fg;
&:hover, &:active {
background: $action-secondary-focused-bg;
}
&.current, &.active {
box-shadow: inset 0 2px 1px 1px $action-secondary-active-shadow;
background: $action-secondary-active-bg;
color: $action-secondary-active-fg;
&:hover, &:active {
box-shadow: inset 0 2px 1px 1px $action-secondary-active-focused-shadow;
color: $action-secondary-active-focused-fg;
}
}
&.disabled, &[disabled] {
box-shadow: none;
background: $action-secondary-disabled-bg; // needed for IE currently
}
}
// ====================
// edx.org marketing site - needed, but bad overrides with importants
.view-register, .view-login, .view-passwordreset {
.form-actions button[type="submit"] {
text-transform: none;
vertical-align: middle;
font-weight: 600 !important;
letter-spacing: 0 !important;
}
}
// ====================
// edx.org marketing site - registration iframe band-aid (poor form enough to isolate out)
.view-partial-mktgregister {
// dimensions needed for course about page on marketing site
.wrapper-view {
overflow: hidden;
}
// nav list
.list-actions {
list-style: none;
......@@ -78,31 +149,37 @@
}
.action {
font-size: 16px;
font-weight: 500;
// register or access courseware
&.action-register, &.access-courseware {
@extend .btn-primary;
@extend .m-btn-primary;
display: block;
}
// already registered but course not started or registration is closed
&.is-registered, &.registration-closed {
@extend .btn-secondary;
@extend .m-btn-secondary;
pointer-events: none !important;
display: block;
}
// coming soon
&.coming-soon {
@extend .btn-tertiary;
@extend .m-btn-secondary;
pointer-events: none !important;
outline: none;
display: block;
}
}
}
//--------------------------------------
// The Following is to enable themes to
// display H1s on login and register pages
//--------------------------------------
// ====================
// The Following is to enable themes to display H1s on login and register pages
.view-login .introduction header h1,
.view-register .introduction header h1 {
@include login_register_h1_style;
......@@ -110,4 +187,4 @@
footer .references {
@include footer_references_style;
}
\ No newline at end of file
}
@function em($pxval, $base: 16) {
@return #{$pxval / $base}em;
// mixins - font sizing
@mixin font-size($sizeValue: 16){
font-size: $sizeValue + px;
font-size: ($sizeValue/10) + rem;
}
// Line-height
@function lh($amount: 1) {
@return $body-line-height * $amount;
// mixins - line height
@mixin line-height($fontSize: auto){
line-height: ($fontSize*1.48) + px;
line-height: (($fontSize/10)*1.48) + rem;
}
// image-replacement hidden text
......@@ -31,6 +34,15 @@
display: block;
}
@function em($pxval, $base: 16) {
@return #{$pxval / $base}em;
}
// Line-height
@function lh($amount: 1) {
@return $body-line-height * $amount;
}
//-----------------
// Theme Mixin Styles
......
// lms - utilities - variables
// ====================
$baseline: 20px;
// grid
$gw-column: 80px;
$gw-gutter: 20px;
......@@ -13,9 +9,6 @@ $fg-max-columns: 12;
$fg-max-width: 1400px;
$fg-min-width: 810px;
// ====================
// fonts
$sans-serif: 'Open Sans', $verdana;
$monospace: Monaco, 'Bitstream Vera Sans Mono', 'Lucida Console', monospace;
$body-font-family: $sans-serif;
......@@ -29,115 +22,12 @@ $base-font-color: rgb(60,60,60);
$lighter-base-font-color: rgb(100,100,100);
$very-light-text: #fff;
// ====================
// colors - new reorganized colors
$black: rgb(0,0,0);
$black-t0: rgba(0,0,0,0.125);
$black-t1: rgba(0,0,0,0.25);
$black-t2: rgba(0,0,0,0.50);
$black-t3: rgba(0,0,0,0.75);
$white: rgb(255,255,255);
$white-t0: rgba(255,255,255,0.125);
$white-t1: rgba(255,255,255,0.25);
$white-t2: rgba(255,255,255,0.50);
$white-t3: rgba(255,255,255,0.75);
$gray: rgb(127,127,127);
$gray-l1: tint($gray,20%);
$gray-l2: tint($gray,40%);
$gray-l3: tint($gray,60%);
$gray-l4: tint($gray,80%);
$gray-l5: tint($gray,90%);
$gray-l6: tint($gray,95%);
$gray-d1: shade($gray,20%);
$gray-d2: shade($gray,40%);
$gray-d3: shade($gray,60%);
$gray-d4: shade($gray,80%);
//new blue
$pink: rgb(183,37,103);
$pink-l1: tint($pink,20%);
$pink-l2: tint($pink,40%);
$pink-l3: tint($pink,60%);
$pink-l4: tint($pink,80%);
$pink-l5: tint($pink,90%);
$pink-d1: shade($pink,20%);
$pink-d2: shade($pink,40%);
$pink-d3: shade($pink,60%);
$pink-d4: shade($pink,80%);
$pink-s1: saturate($pink,15%);
$pink-s2: saturate($pink,30%);
$pink-s3: saturate($pink,45%);
$pink-u1: desaturate($pink,15%);
$pink-u2: desaturate($pink,30%);
$pink-u3: desaturate($pink,45%);
$black: rgb(0,0,0);
$blue: rgb(29,157,217);
$pink: rgb(182,37,104);
$yellow: rgb(255, 252, 221);
$red: rgb(178, 6, 16);
$red-l1: tint($red,20%);
$red-l2: tint($red,40%);
$red-l3: tint($red,60%);
$red-l4: tint($red,80%);
$red-l5: tint($red,90%);
$red-d1: shade($red,20%);
$red-d2: shade($red,40%);
$red-d3: shade($red,60%);
$red-d4: shade($red,80%);
$red-s1: saturate($red,15%);
$red-s2: saturate($red,30%);
$red-s3: saturate($red,45%);
$red-u1: desaturate($red,15%);
$red-u2: desaturate($red,30%);
$red-u3: desaturate($red,45%);
$green: rgb(37, 184, 90);
$green-l1: tint($green,20%);
$green-l2: tint($green,40%);
$green-l3: tint($green,60%);
$green-l4: tint($green,80%);
$green-l5: tint($green,90%);
$green-d1: shade($green,20%);
$green-d2: shade($green,40%);
$green-d3: shade($green,60%);
$green-d4: shade($green,80%);
$green-s1: saturate($green,15%);
$green-s2: saturate($green,30%);
$green-s3: saturate($green,45%);
$green-u1: desaturate($green,15%);
$green-u2: desaturate($green,30%);
$green-u3: desaturate($green,45%);
//new yellow
$orange: rgb(237, 189, 60);
$orange-l1: tint($orange,20%);
$orange-l2: tint($orange,40%);
$orange-l3: tint($orange,60%);
$orange-l4: tint($orange,80%);
$orange-l5: tint($orange,90%);
$orange-d1: shade($orange,20%);
$orange-d2: shade($orange,40%);
$orange-d3: shade($orange,60%);
$orange-d4: shade($orange,80%);
$orange-s1: saturate($orange,15%);
$orange-s2: saturate($orange,30%);
$orange-s3: saturate($orange,45%);
$orange-u1: desaturate($orange,15%);
$orange-u2: desaturate($orange,30%);
$orange-u3: desaturate($orange,45%);
$shadow: rgba(0,0,0,0.2);
$shadow-l1: rgba(0,0,0,0.1);
$shadow-l2: rgba(0,0,0,0.05);
$shadow-d1: rgba(0,0,0,0.4);
// ====================
// colors - old variables
$blue: rgb(29,157,217); //old blue
$yellow: rgb(255, 252, 221); //old yellow
$error-red: rgb(253, 87, 87);
$light-gray: rgb(221, 221, 221);
$dark-gray: rgb(51, 51, 51);
......@@ -149,23 +39,36 @@ $outer-border-color: rgb(170, 170, 170);
$light-gray: #ddd;
$dark-gray: #333;
// edx.org-related
$m-gray-l1: rgb(203,203,203);
$m-gray-l2: rgb(246,246,246);
$m-gray: rgb(153,153,153);
$m-gray-d1: rgb(102,102,102);
$m-gray-d2: rgb(51,51,51);
$m-gray-a1: rgb(80,80,80);
$m-blue: rgb(65, 116, 170);
// $m-blue: rgb(85, 151, 221); (used in marketing redesign)
$m-blue-l1: rgb(85, 151, 221);
$m-blue-d1: shade($m-blue,15%);
$m-blue-s1: saturate($m-blue,15%);
$m-pink: rgb(204,51,102);
// edx.org marketing site variables
$m-gray: #8A8C8F;
$m-gray-l1: #97999B;
$m-gray-l2: #A4A6A8;
$m-gray-l3: #B1B2B4;
$m-gray-l4: #F5F5F5;
$m-gray-d1: #7D7F83;
$m-gray-d2: #707276;
$m-gray-d3: #646668;
$m-gray-d4: #050505;
$m-blue: #1AA1DE;
$m-blue-l1: #2BACE6;
$m-blue-l2: #42B5E9;
$m-blue-l3: #59BEEC;
$m-blue-d1: #1790C7;
$m-blue-d2: #1580B0;
$m-blue-d3: #126F9A;
$m-blue-d4: #0A4A67;
$m-pink: #B52A67;
$m-pink-l1: #CA2F73;
$m-pink-l2: #D33F80;
$m-pink-l3: #D7548E;
$m-pink-d1: #A0255B;
$m-pink-d2: #8C204F;
$m-pink-d3: #771C44;
$m-base-font-size: em(15);
$base-font-color: rgb(60,60,60);
$baseFontColor: rgb(60,60,60);
$lighter-base-font-color: rgb(100,100,100);
......@@ -184,10 +87,57 @@ $courseware-footer-border: none;
$courseware-footer-shadow: none;
$courseware-footer-margin: 0px;
// actions
$button-bg-image: linear-gradient(#fff 0%, rgb(250,250,250) 50%, rgb(237,237,237) 50%, rgb(220,220,220) 100%);
$button-bg-color: transparent;
$button-bg-hover-color: #fff;
// actions - primary
$action-primary-bg: $m-blue-d3;
$action-primary-fg: $white;
$action-primary-shadow: $m-blue-d4;
// focused - hover/active pseudo states
$action-primary-focused-bg: $m-blue-d1;
$action-primary-focused-fg: $white;
// current or active navigation item
$action-primary-active-bg: $m-blue;
$action-primary-active-fg: $m-blue-d3;
$action-primary-active-shadow: $m-blue-d2;
$action-primary-active-focused-fg: $m-blue-d4;
$action-primary-active-focused-shadow: $m-blue-d3;
// disabled
$action-primary-disabled-bg: $m-gray-d3;
$action-prmary-disabled-fg: $white;
// actions - secondary
$action-secondary-bg: $m-pink;
$action-secondary-fg: $white;
$action-secondary-shadow: $m-pink-d2;
// focused - hover/active pseudo states
$action-secondary-focused-bg: $m-pink-l3;
$action-secondary-focused-fg: $white;
// current or active navigation item
$action-secondary-active-bg: $m-pink-l2;
$action-secondary-active-fg: $m-pink-d1;
$action-secondary-active-shadow: $m-pink-d1;
$action-secondary-active-focused-fg: $m-pink-d3;
$action-secondary-active-focused-shadow: $m-pink-d2;
// disabled
$action-secondary-disabled-bg: $m-gray-d3;
$action-secondary-disabled-fg: $white;
$faded-hr-image-1: linear-gradient(180deg, rgba(200,200,200, 0) 0%, rgba(200,200,200, 1) 50%, rgba(200,200,200, 0));
$faded-hr-image-2: linear-gradient(180deg, rgba(200,200,200, 0) 0%, rgba(200,200,200, 1));
$faded-hr-image-3: linear-gradient(180deg, rgba(200,200,200, 1) 0%, rgba(200,200,200, 0));
......@@ -214,7 +164,7 @@ $border-color-3: rgb(100,100,100);
$border-color-4: rgb(252,252,252);
$link-color: $blue;
$link-color-d1: $m-blue;
$link-color-d1: $m-blue-d2;
$link-hover: $pink;
$site-status-color: $pink;
......@@ -245,4 +195,4 @@ $homepage-bg-image: '../images/homepage-bg.jpg';
$login-banner-image: url(../images/bg-banner-login.png);
$register-banner-image: url(../images/bg-banner-register.png);
$video-thumb-url: '../images/courses/video-thumb.jpg';
$video-thumb-url: '../images/courses/video-thumb.jpg';
\ No newline at end of file
......@@ -37,7 +37,7 @@
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0 !important;
color: saturate($link-color-d1,15%);
color: $m-gray-d2;
}
.heading-5 {
......@@ -390,7 +390,7 @@
@include clearfix();
button[type="submit"] {
@extend .button-primary;
@extend .m-btn-primary;
&:disabled, &.is-disabled {
opacity: 0.3;
......@@ -431,7 +431,6 @@
margin: 0 0 ($baseline/4) 0;
font-size: em(14);
font-weight: 600;
color: $m-gray-d2 !important;
}
.message-copy {
......
......@@ -278,26 +278,8 @@ header.global {
li {
display: inline-block;
a {
border-radius: 0;
@include linear-gradient(saturate($link-color-d1,15%) 5%, shade($link-color-d1,15%) 95%);
display: inline-block;
padding: $baseline/2 $baseline*2.5;
text-transform: lowercase;
color: $very-light-text;
letter-spacing: 0.1rem;
font-weight: 300;
cursor: pointer;
text-align: center;
border: none !important;
text-shadow: none;
letter-spacing: 0.1rem;
font-size: 14px;
box-shadow: none !important;
&:hover {
text-decoration: none;
}
.cta {
@extend .m-btn-primary;
}
}
......
......@@ -53,7 +53,7 @@
%elif allow_registration:
<a class="action action-register register" href="#">Register for <strong>${course.number}</strong></a>
%else:
<div class="action registration-closed">Registration Is Closed</div>
<div class="action registration-closed is-disabled">Registration Is Closed</div>
%endif
</li>
</ul>
......
......@@ -69,7 +69,7 @@
$submitButton.
removeClass('is-disabled').
removeProp('disabled').
html('Create my ${settings.PLATFORM_NAME} Account');
html('Create My ${settings.PLATFORM_NAME} Account');
}
else {
$submitButton.
......@@ -141,32 +141,32 @@
</div>
<ol class="list-input">
% if ask_for_email:
<li class="field required text" id="field-email">
<label for="email">E-mail</label>
<input class="" id="email" type="email" name="email" value="" placeholder="example: username@domain.com" />
</li>
% endif
<li class="field required text" id="field-username">
<label for="username">Public Username</label>
<input id="username" type="text" name="username" value="${extauth_username}" placeholder="example: JaneDoe" required aria-required="true" />
<span class="tip tip-input">Will be shown in any discussions or forums you participate in</span>
</li>
% if ask_for_fullname:
<li class="field required text" id="field-name">
<label for="name">Full Name</label>
<input id="name" type="text" name="name" value="" placeholder="example: Jane Doe" />
<span class="tip tip-input">Needed for any certificates you may earn <strong>(cannot be changed later)</strong></span>
</li>
% endif
</ol>
% endif
......@@ -282,7 +282,7 @@
</a>
</p>
</div>
% endif
## TODO: Use a %block tag or something to allow themes to
......
......@@ -3,7 +3,7 @@
<ul class="sequence-nav-buttons">
<li class="prev"><a href="#">Previous</a></li>
</ul>
<div class="sequence-list-wrapper">
<ol id="sequence-list">
% for idx, item in enumerate(items):
......@@ -16,7 +16,7 @@
data-id="${item['id']}"
data-element="${idx+1}"
href="javascript:void(0);">
<p class="sr">${item['title']}, ${item['type']}</p>
<p>${item['title']}<span class="sr">, ${item['type']}</span></p>
</a>
</li>
% endfor
......
......@@ -441,6 +441,12 @@ if settings.MITX_FEATURES.get('ENABLE_HINTER_INSTRUCTOR_VIEW'):
'instructor.hint_manager.hint_manager', name="hint_manager"),
)
# enable automatic login
if settings.MITX_FEATURES.get('AUTOMATIC_AUTH_FOR_LOAD_TESTING'):
urlpatterns += (
url(r'^auto_auth$', 'student.views.auto_auth'),
)
urlpatterns = patterns(*urlpatterns)
if settings.DEBUG:
......
......@@ -34,7 +34,7 @@ def parse_args():
help="Which django settings module to use from inside of lms.envs. If not provided, the DJANGO_SETTINGS_MODULE "
"environment variable will be used if it is set, otherwise will default to lms.envs.dev")
lms.add_argument(
'-s', '--service-variant',
'--service-variant',
choices=['lms', 'lms-xml', 'lms-preview'],
default='lms',
help='Which service variant to run, when using the aws environment')
......
......@@ -80,8 +80,8 @@ nosexcover==1.0.7
pep8==1.4.5
pylint==0.28
rednose==0.3
selenium==2.31.0
splinter==0.5.0
selenium==2.33.0
splinter==0.5.4
django_nose==1.1
django-jasmine==0.3.2
django_debug_toolbar
......
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