fix merge conflict with _variables.scss
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
"""
|
||||
Tests for user.py.
|
||||
"""
|
||||
import json
|
||||
import mock
|
||||
from .utils import CourseTestCase
|
||||
from django.core.urlresolvers import reverse
|
||||
from contentstore.views.user import _get_course_creator_status
|
||||
from course_creators.views import add_user_with_status_granted
|
||||
from course_creators.admin import CourseCreatorAdmin
|
||||
from course_creators.models import CourseCreator
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
|
||||
|
||||
class UsersTestCase(CourseTestCase):
|
||||
@@ -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,14 +26,11 @@ 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)
|
||||
|
||||
@@ -41,7 +38,7 @@ class CourseCreatorView(TestCase):
|
||||
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 the user is already in the table, this method is a no-op
|
||||
(state will not be changed). Caller must have staff permissions.
|
||||
If appropriate, this method also adds the user to the course creator group maintained by authz.py.
|
||||
Caller must have staff permissions.
|
||||
|
||||
This method also adds the user to the course creator group maintained by authz.py.
|
||||
If the user is already in the table, this method is a no-op
|
||||
(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, 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.
|
||||
|
||||
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
|
||||
Returns True if user was added to table, else False.
|
||||
|
||||
if CourseCreator.objects.filter(user=user).count() == 0:
|
||||
If the user is already in the table, this method is a no-op
|
||||
(state will not be changed, method will return False).
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -1,4 +1,46 @@
|
||||
// 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
|
||||
// ====================
|
||||
|
||||
|
||||
@@ -1,40 +1,185 @@
|
||||
// studio - elements - system help
|
||||
// ====================
|
||||
|
||||
// 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);
|
||||
// view introductions - common greeting/starting points for the UI
|
||||
.content .introduction {
|
||||
@include box-sizing(border-box);
|
||||
margin-bottom: $baseline;
|
||||
|
||||
.title {
|
||||
@extend .t-title7;
|
||||
margin-bottom: ($baseline/4);
|
||||
@extend .t-title4;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.copy {
|
||||
@extend .t-copy-sub1;
|
||||
@include transition(opacity $tmg-f2 ease-in-out 0s);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
// CASE: has links alongside
|
||||
&.has-links {
|
||||
@include clearfix();
|
||||
|
||||
.copy {
|
||||
opacity: 1.0;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// particular warnings around a workflow for something
|
||||
// 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: 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;
|
||||
}
|
||||
|
||||
&.has-status {
|
||||
|
||||
.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 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;
|
||||
}
|
||||
}
|
||||
|
||||
.class-list {
|
||||
margin-top: 20px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $darkGrey;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .1);
|
||||
// ====================
|
||||
|
||||
li {
|
||||
// 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;
|
||||
border-bottom: 1px solid $mediumGrey;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 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 $gray;
|
||||
background: $white;
|
||||
box-shadow: 0 1px 2px $shadow-l1;
|
||||
|
||||
.course-item {
|
||||
position: relative;
|
||||
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">
|
||||
|
||||
<section class="tos">
|
||||
<div>
|
||||
|
||||
<section class="activation">
|
||||
<h1>${_("Account already active!")}</h1>
|
||||
<p>${_('This account has already been activated.')}<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-active">
|
||||
<article class="content-primary" role="main">
|
||||
</article>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<p>${_('Or you can go back to the {link_start}home page{link_end}.').format(
|
||||
link_start='<a href="/">', link_end='</a>')}</p>
|
||||
<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>
|
||||
|
||||
<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 — 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>
|
||||
|
||||
<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>
|
||||
|
||||
5
cms/templates/emails/course_creator_denied.txt
Normal file
5
cms/templates/emails/course_creator_denied.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
<%! 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 }
|
||||
9
cms/templates/emails/course_creator_granted.txt
Normal file
9
cms/templates/emails/course_creator_granted.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
<%! 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
|
||||
5
cms/templates/emails/course_creator_revoked.txt
Normal file
5
cms/templates/emails/course_creator_revoked.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
<%! 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 }
|
||||
2
cms/templates/emails/course_creator_subject.txt
Normal file
2
cms/templates/emails/course_creator_subject.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
${_("Your course creator status for edX Studio")}
|
||||
@@ -5,9 +5,41 @@
|
||||
<%block name="title">${_("My Courses")}</%block>
|
||||
<%block name="bodyclass">is-signedin index dashboard</%block>
|
||||
|
||||
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.js"></script>
|
||||
<script src="http://malsup.github.com/jquery.form.js"></script>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
|
||||
// showing/hiding creation rights UI
|
||||
$('.show-creationrights').click(function(e){
|
||||
(e).preventDefault();
|
||||
$(this).closest('.wrapper-creationrights').toggleClass('is-shown').find('.ui-toggle-control').toggleClass('current');
|
||||
});
|
||||
|
||||
var reloadPage = function () {
|
||||
location.reload();
|
||||
};
|
||||
|
||||
var showError = function () {
|
||||
$('#request-coursecreator-submit').toggleClass('has-error').find('.label').text('Sorry, there was error with your request');
|
||||
$('#request-coursecreator-submit').find('.icon-cog').toggleClass('icon-spin');
|
||||
};
|
||||
|
||||
$('#request-coursecreator').ajaxForm({error: showError, success: reloadPage});
|
||||
|
||||
$('#request-coursecreator-submit').click(function(e){
|
||||
$(this).toggleClass('is-disabled is-submitting').find('.label').text('Submitting Your Request');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%block name="header_extras">
|
||||
<script type="text/template" id="new-course-template">
|
||||
<section class="new-course">
|
||||
<h3 class="title">${_("Create a New Course:")}</h3>
|
||||
<div class="item-details">
|
||||
<form class="course-info">
|
||||
<div class="row">
|
||||
@@ -35,59 +67,246 @@
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions">
|
||||
<h1 class="page-header">${_("My Courses")}</h1>
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions">
|
||||
<h1 class="page-header">${_("My Courses")}</h1>
|
||||
|
||||
% if user.is_active:
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">${_("Page Actions")}</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
% if not disable_course_creation:
|
||||
<a href="#" class="button new-button new-course-button"><i class="icon-plus"></i> ${_("New Course")}</a>
|
||||
% elif settings.MITX_FEATURES.get('STAFF_EMAIL',''):
|
||||
<a href="mailto:${settings.MITX_FEATURES.get('STAFF_EMAIL','')}">${_("Email staff to create course")}</a>
|
||||
% endif
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
% endif
|
||||
</header>
|
||||
</div>
|
||||
% if user.is_active:
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">${_("Page Actions")}</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
% if course_creator_status=='granted':
|
||||
<a href="#" class="button new-button new-course-button"><i class="icon-plus icon-inline"></i>
|
||||
${_("New Course")}</a>
|
||||
% elif course_creator_status=='disallowed_for_this_site' and settings.MITX_FEATURES.get('STUDIO_REQUEST_EMAIL',''):
|
||||
<a href="mailto:${settings.MITX_FEATURES.get('STUDIO_REQUEST_EMAIL','')}">${_("Email staff to create course")}</a>
|
||||
% endif
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
% endif
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
% if user.is_active:
|
||||
<section class="content">
|
||||
<article class="content-primary" role="main">
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<div class="introduction">
|
||||
<p class="copy">
|
||||
<strong>${_("Welcome, %(name)s") % dict(name= user.username)}</strong>.
|
||||
${_("Here are all of the courses you are currently authoring in Studio:")}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<h2 class="title">${_("Welcome, {0}!".format(user.username))}</h2>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<article class="my-classes">
|
||||
% if user.is_active:
|
||||
<ul class="class-list">
|
||||
%if len(courses) > 0:
|
||||
<div class="copy">
|
||||
<p>${_("Here are all of the courses you currently have access to in Studio:")}</p>
|
||||
</div>
|
||||
|
||||
%else:
|
||||
<div class="copy">
|
||||
<p>${_("You currently aren't associated with any Studio Courses.")}</p>
|
||||
</div>
|
||||
%endif
|
||||
</div>
|
||||
|
||||
%if len(courses) > 0:
|
||||
<div class="courses">
|
||||
<ul class="list-courses">
|
||||
%for course, url, lms_link in sorted(courses, key=lambda s: s[0].lower() if s[0] is not None else ''):
|
||||
<li>
|
||||
<li class="course-item">
|
||||
<a class="class-link" href="${url}" class="class-name">
|
||||
<span class="class-name">${course}</span>
|
||||
</a>
|
||||
<a href="${lms_link}" rel="external" class="button view-button view-live-button">${_("View Live")}</a>
|
||||
</li>
|
||||
%endfor
|
||||
%endfor
|
||||
</ul>
|
||||
% else:
|
||||
<div class='warn-msg'>
|
||||
<p>
|
||||
${_("In order to start authoring courses using edX Studio, please click on the activation link in your email.")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
%else:
|
||||
<div class="courses">
|
||||
</div>
|
||||
|
||||
<div class="notice notice-incontext notice-instruction notice-instruction-nocourses list-notices">
|
||||
<div class="notice-item">
|
||||
<div class="msg">
|
||||
<h3 class="title">${_("Are you staff on an existing Studio course?")}</h3>
|
||||
<div class="copy">
|
||||
<p>${_('You will need to be added to the course in Studio by the course creator. Please get in touch with the course creator or administrator for the specific course you are helping to author.')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
%if course_creator_status == "granted":
|
||||
<div class="notice-item has-actions">
|
||||
<div class="msg">
|
||||
<h3 class="title">${_('Create Your First Course')}</h3>
|
||||
<div class="copy">
|
||||
<p>${_('Your new course is just a click away!')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action-item">
|
||||
<a href="#" class="action-primary action-create action-create-course new-course-button"><i class="icon-plus icon-inline"></i> ${_('Create Your First Course')}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
% endif
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
% endif
|
||||
|
||||
|
||||
%if course_creator_status == "unrequested":
|
||||
<div class="wrapper wrapper-creationrights">
|
||||
<h3 class="title">
|
||||
<a href="#instruction-creationrights" class="ui-toggle-control show-creationrights"><span class="label">${_('Becoming a Course Creator in Studio')}</span> <i class="icon-remove-sign"></i></a>
|
||||
</h3>
|
||||
|
||||
<div class="notice notice-incontext notice-instruction notice-instruction-creationrights ui-toggle-target" id="instruction-creationrights">
|
||||
<div class="copy">
|
||||
<p>${_('edX Studio is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by edX. Our team will evaluate your request and provide you feedback within 24 hours during the work week.')}</p>
|
||||
</div>
|
||||
|
||||
<div class="status status-creationrights is-unrequested">
|
||||
<h4 class="title">${_('Your Course Creator Request Status:')}</h4>
|
||||
|
||||
<form id="request-coursecreator" action="${request_course_creator_url}" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf}">
|
||||
<div class="form-actions">
|
||||
<button type="submit" id="request-coursecreator-submit" name="request-coursecreator-submit" class="action-primary action-request"><i class="icon-cog icon-inline icon-spin"></i> <span class="label">${_('Request the Ability to Create Courses')}</span></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
%elif course_creator_status == "denied":
|
||||
<div class="wrapper wrapper-creationrights is-shown">
|
||||
<h3 class="title">
|
||||
<a href="#instruction-creationrights" class="ui-toggle-control current show-creationrights"><span class="label">${_('Your Course Creator Request Status')}</span> <i class="icon-remove-sign"></i></a>
|
||||
</h3>
|
||||
|
||||
<div class="notice notice-incontext notice-instruction notice-instruction-creationrights ui-toggle-target" id="instruction-creationrights">
|
||||
<div class="copy">
|
||||
<p>${_('edX Studio is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by edX. Our team is has completed evaluating your request.')}</p>
|
||||
</div>
|
||||
|
||||
<div class="status status-creationrights has-status is-denied">
|
||||
<h4 class="title">${_('Your Course Creator Request Status:')}</h4>
|
||||
|
||||
<dl class="status-update">
|
||||
<dt class="label">${_('Your Course Creator request is:')}</dt>
|
||||
<dd class="value">
|
||||
<span class="status-indicator"></span>
|
||||
<span class="value-formal">${_('Denied')}</span>
|
||||
<span class="value-description">${_('Your request did not meet the criteria/guidelines specified by edX Staff.')}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
%elif course_creator_status == "pending":
|
||||
<div class="wrapper wrapper-creationrights is-shown">
|
||||
<h3 class="title">
|
||||
<a href="#instruction-creationrights" class="ui-toggle-control current show-creationrights"><span class="label">${_('Your Course Creator Request Status')}</span> <i class="icon-remove-sign"></i></a>
|
||||
</h3>
|
||||
|
||||
<div class="notice notice-incontext notice-instruction notice-instruction-creationrights ui-toggle-target" id="instruction-creationrights">
|
||||
<div class="copy">
|
||||
<p>${_('edX Studio is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by edX. Our team is currently evaluating your request.')}</p>
|
||||
</div>
|
||||
|
||||
<div class="status status-creationrights has-status is-pending">
|
||||
<h4 class="title">${_('Your Course Creator Request Status:')}</h4>
|
||||
|
||||
<dl class="status-update">
|
||||
<dt class="label">${_('Your Course Creator request is:')}</dt>
|
||||
<dd class="value">
|
||||
<span class="status-indicator"></span>
|
||||
<span class="value-formal">${_('Pending')}</span>
|
||||
<span class="value-description">${_('Your request is currently being reviewed by edX staff and should be updated shortly.')}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<div class="bit">
|
||||
<h3 class="title title-3">${_('Need help?')}</h3>
|
||||
<p>${_('If you are new to Studio and having trouble getting started, there are a few things that may be of help:')}</p>
|
||||
|
||||
<ol class="list-actions">
|
||||
<li class="action-item">
|
||||
<a href="http://files.edx.org/Getting_Started_with_Studio.pdf" title="This is a PDF Document">${_('Get started by reading Studio\'s Documentation')}</a>
|
||||
</li>
|
||||
<li class="action-item">
|
||||
<a href="http://help.edge.edx.org/discussion/new" class="show-tender" title="Use our feedback tool, Tender, to request help">${_('Request help with Studio')}</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
% if course_creator_status=='disallowed_for_this_site' and settings.MITX_FEATURES.get('STUDIO_REQUEST_EMAIL',''):
|
||||
<div class="bit">
|
||||
<h3 class="title title-3">${_('Can I create courses in Studio?')}</h3>
|
||||
<p>${_('In order to create courses in Studio, you must')} <a href="mailto:${settings.MITX_FEATURES.get('STUDIO_REQUEST_EMAIL','')}">${_("contact edX staff to help you create a course")}</a></p>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
% if course_creator_status == "unrequested":
|
||||
<div class="bit">
|
||||
<h3 class="title title-3">${_('Can I create courses in Studio?')}</h3>
|
||||
<p>${_('In order to create courses in Studio, you must have course creator privileges to create your own course.')}</p>
|
||||
</div>
|
||||
|
||||
% elif course_creator_status == "denied":
|
||||
<div class="bit">
|
||||
<h3 class="title title-3">${_('Can I create courses in Studio?')}</h3>
|
||||
<p>${_('Your request to author courses in studio has been denied. Please')} <a href="http://help.edge.edx.org/discussion/new" class="show-tender">${_('contact edX Staff with further questions')}</a></p>
|
||||
</div>
|
||||
|
||||
% endif
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
|
||||
% else:
|
||||
<section class="content">
|
||||
<article class="content-primary" role="main">
|
||||
<div class="introduction">
|
||||
<h2 class="title">${_("Thanks for signing up, %(name)s!") % dict(name= user.username)}</h2>
|
||||
</div>
|
||||
|
||||
<div class="notice notice-incontext notice-instruction notice-instruction-verification">
|
||||
<div class="msg">
|
||||
<h3 class="title">${_('We need to verify your email address')}</h3>
|
||||
<div class="copy">
|
||||
<p>${_('Almost there! In order to complete your sign up we need you to verify your email address (%(email)s). An activation message and next steps should be waiting for you there.') % dict(email=user.email)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<div class="bit">
|
||||
<h3 class="title title-3">${_('Need help?')}</h3>
|
||||
<p>${_('Please check your Junk or Spam folders in case our email isn\'t in your INBOX. Still can\'t find the verification email? Request help via the link below.')}</p>
|
||||
|
||||
<ol class='list-actions'>
|
||||
<li class="action-item">
|
||||
<a href="http://help.edge.edx.org/discussion/new" class="show-tender" title="Use our feedback tool, Tender, to request help">Request help with your Studio account</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
%endif
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
@@ -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
|
||||
|
||||
111
common/djangoapps/student/tests/test_auto_auth.py
Normal file
111
common/djangoapps/student/tests/test_auto_auth.py
Normal file
@@ -0,0 +1,111 @@
|
||||
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;
|
||||
|
||||
@@ -1,6 +1,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)
|
||||
// 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 {
|
||||
|
||||
}
|
||||
|
||||
&.disabled, &[disabled] {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.m-btn-pill {
|
||||
border-radius: ($baseline/5);
|
||||
}
|
||||
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
&: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;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled, &[disabled] {
|
||||
box-shadow: none;
|
||||
background: $action-primary-disabled-bg; // needed for IE currently
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
background: transparent;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@extend .btn;
|
||||
@include linear-gradient($m-blue-s1 5%, $m-blue-d1 95%);
|
||||
|
||||
// no hover state conventions to follow from marketing :/
|
||||
&:hover, &:active {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@extend .btn;
|
||||
@include linear-gradient($m-gray 5%, $m-gray-d1 95%);
|
||||
|
||||
// no hover state conventions to follow from marketing :/
|
||||
&:hover, &:active {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.btn-tertiary {
|
||||
@extend .btn;
|
||||
background: $m-blue-l1;
|
||||
color: $m-blue;
|
||||
|
||||
// no hover state conventions to follow from marketing :/
|
||||
&:hover, &:active {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
@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
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
// 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';
|
||||
@@ -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>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user