Manually merge release into master
This commit is contained in:
@@ -4,7 +4,7 @@ that gets used when sending cachability headers back with request course assets.
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from config_models.admin import ConfigurationModelAdmin
|
||||
from .models import CourseAssetCacheTtlConfig, CdnUserAgentsConfig
|
||||
from .models import CourseAssetCacheTtlConfig
|
||||
|
||||
|
||||
class CourseAssetCacheTtlConfigAdmin(ConfigurationModelAdmin):
|
||||
@@ -26,24 +26,4 @@ class CourseAssetCacheTtlConfigAdmin(ConfigurationModelAdmin):
|
||||
return self.list_display
|
||||
|
||||
|
||||
class CdnUserAgentsConfigAdmin(ConfigurationModelAdmin):
|
||||
"""
|
||||
Basic configuration for CDN user agent whitelist.
|
||||
"""
|
||||
list_display = [
|
||||
'cdn_user_agents'
|
||||
]
|
||||
|
||||
def get_list_display(self, request):
|
||||
"""
|
||||
Restore default list_display behavior.
|
||||
|
||||
ConfigurationModelAdmin overrides this, but in a way that doesn't
|
||||
respect the ordering. This lets us customize it the usual Django admin
|
||||
way.
|
||||
"""
|
||||
return self.list_display
|
||||
|
||||
|
||||
admin.site.register(CourseAssetCacheTtlConfig, CourseAssetCacheTtlConfigAdmin)
|
||||
admin.site.register(CdnUserAgentsConfig, CdnUserAgentsConfigAdmin)
|
||||
|
||||
@@ -3,14 +3,13 @@ Middleware to serve assets.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import datetime
|
||||
import newrelic.agent
|
||||
|
||||
import datetime
|
||||
from django.http import (
|
||||
HttpResponse, HttpResponseNotModified, HttpResponseForbidden,
|
||||
HttpResponseBadRequest, HttpResponseNotFound)
|
||||
from student.models import CourseEnrollment
|
||||
from contentserver.models import CourseAssetCacheTtlConfig, CdnUserAgentsConfig
|
||||
from contentserver.models import CourseAssetCacheTtlConfig
|
||||
|
||||
from header_control import force_header_for_response
|
||||
from xmodule.assetstore.assetmgr import AssetManager
|
||||
@@ -56,19 +55,6 @@ class StaticContentServer(object):
|
||||
except (ItemNotFoundError, NotFoundError):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
# Set the basics for this request.
|
||||
newrelic.agent.add_custom_parameter('course_id', loc.course_key)
|
||||
newrelic.agent.add_custom_parameter('org', loc.org)
|
||||
newrelic.agent.add_custom_parameter('contentserver.path', loc.path)
|
||||
|
||||
# Figure out if this is a CDN using us as the origin.
|
||||
is_from_cdn = StaticContentServer.is_cdn_request(request)
|
||||
newrelic.agent.add_custom_parameter('contentserver.from_cdn', True if is_from_cdn else False)
|
||||
|
||||
# Check if this content is locked or not.
|
||||
locked = self.is_content_locked(content)
|
||||
newrelic.agent.add_custom_parameter('contentserver.locked', True if locked else False)
|
||||
|
||||
# Check that user has access to the content.
|
||||
if not self.is_user_authorized(request, content, loc):
|
||||
return HttpResponseForbidden('Unauthorized')
|
||||
@@ -121,11 +107,8 @@ class StaticContentServer(object):
|
||||
response['Content-Range'] = 'bytes {first}-{last}/{length}'.format(
|
||||
first=first, last=last, length=content.length
|
||||
)
|
||||
range_len = last - first + 1
|
||||
response['Content-Length'] = str(range_len)
|
||||
response['Content-Length'] = str(last - first + 1)
|
||||
response.status_code = 206 # Partial Content
|
||||
|
||||
newrelic.agent.add_custom_parameter('contentserver.range_len', range_len)
|
||||
else:
|
||||
log.warning(
|
||||
u"Cannot satisfy ranges in Range header: %s for content: %s", header_value, unicode(loc)
|
||||
@@ -137,9 +120,6 @@ class StaticContentServer(object):
|
||||
response = HttpResponse(content.stream_data())
|
||||
response['Content-Length'] = content.length
|
||||
|
||||
newrelic.agent.add_custom_parameter('contentserver.content_len', content.length)
|
||||
newrelic.agent.add_custom_parameter('contentserver.content_type', content.content_type)
|
||||
|
||||
# "Accept-Ranges: bytes" tells the user that only "bytes" ranges are allowed
|
||||
response['Accept-Ranges'] = 'bytes'
|
||||
response['Content-Type'] = content.content_type
|
||||
@@ -166,11 +146,9 @@ class StaticContentServer(object):
|
||||
# indicate there should be no caching whatsoever.
|
||||
cache_ttl = CourseAssetCacheTtlConfig.get_cache_ttl()
|
||||
if cache_ttl > 0 and not is_locked:
|
||||
newrelic.agent.add_custom_parameter('contentserver.cacheable', True)
|
||||
response['Expires'] = StaticContentServer.get_expiration_value(datetime.datetime.utcnow(), cache_ttl)
|
||||
response['Cache-Control'] = "public, max-age={ttl}, s-maxage={ttl}".format(ttl=cache_ttl)
|
||||
elif is_locked:
|
||||
newrelic.agent.add_custom_parameter('contentserver.cacheable', False)
|
||||
response['Cache-Control'] = "private, no-cache, no-store"
|
||||
|
||||
response['Last-Modified'] = content.last_modified_at.strftime(HTTP_DATE_FORMAT)
|
||||
@@ -180,39 +158,19 @@ class StaticContentServer(object):
|
||||
# caches a version of the response without CORS headers, in turn breaking XHR requests.
|
||||
force_header_for_response(response, 'Vary', 'Origin')
|
||||
|
||||
@staticmethod
|
||||
def is_cdn_request(request):
|
||||
"""
|
||||
Attempts to determine whether or not the given request is coming from a CDN.
|
||||
|
||||
Currently, this is a static check because edx.org only uses CloudFront, but may
|
||||
be expanded in the future.
|
||||
"""
|
||||
cdn_user_agents = CdnUserAgentsConfig.get_cdn_user_agents()
|
||||
user_agent = request.META.get('HTTP_USER_AGENT', '')
|
||||
if user_agent in cdn_user_agents:
|
||||
# This is a CDN request.
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_expiration_value(now, cache_ttl):
|
||||
"""Generates an RFC1123 datetime string based on a future offset."""
|
||||
expire_dt = now + datetime.timedelta(seconds=cache_ttl)
|
||||
return expire_dt.strftime(HTTP_DATE_FORMAT)
|
||||
|
||||
def is_content_locked(self, content):
|
||||
"""
|
||||
Determines whether or not the given content is locked.
|
||||
"""
|
||||
return getattr(content, "locked", False)
|
||||
|
||||
def is_user_authorized(self, request, content, location):
|
||||
"""
|
||||
Determines whether or not the user for this request is authorized to view the given asset.
|
||||
"""
|
||||
if not self.is_content_locked(content):
|
||||
|
||||
is_locked = getattr(content, "locked", False)
|
||||
if not is_locked:
|
||||
return True
|
||||
|
||||
if not hasattr(request, "user") or not request.user.is_authenticated():
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('contentserver', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CdnUserAgentsConfig',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
|
||||
('cdn_user_agents', models.TextField(default=b'Amazon CloudFront', help_text=b'A newline-separated list of user agents that should be considered CDNs.')),
|
||||
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -2,7 +2,7 @@
|
||||
Models for contentserver
|
||||
"""
|
||||
|
||||
from django.db.models.fields import PositiveIntegerField, TextField
|
||||
from django.db.models.fields import PositiveIntegerField
|
||||
from config_models.models import ConfigurationModel
|
||||
|
||||
|
||||
@@ -27,26 +27,3 @@ class CourseAssetCacheTtlConfig(ConfigurationModel):
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(repr(self))
|
||||
|
||||
|
||||
class CdnUserAgentsConfig(ConfigurationModel):
|
||||
"""Configuration for the user agents we expect to see from CDNs."""
|
||||
|
||||
class Meta(object):
|
||||
app_label = 'contentserver'
|
||||
|
||||
cdn_user_agents = TextField(
|
||||
default='Amazon CloudFront',
|
||||
help_text="A newline-separated list of user agents that should be considered CDNs."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_cdn_user_agents(cls):
|
||||
"""Gets the list of CDN user agents, if present"""
|
||||
return cls.current().cdn_user_agents
|
||||
|
||||
def __repr__(self):
|
||||
return '<WhitelistedCdnConfig(cdn_user_agents={})>'.format(self.get_cdn_user_agents())
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(repr(self))
|
||||
|
||||
@@ -10,7 +10,6 @@ import unittest
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import RequestFactory
|
||||
from django.test.client import Client
|
||||
from django.test.utils import override_settings
|
||||
from mock import patch
|
||||
@@ -271,49 +270,6 @@ class ContentStoreToyCourseTest(SharedModuleStoreTestCase):
|
||||
near_expire_dt = StaticContentServer.get_expiration_value(start_dt, 55)
|
||||
self.assertEqual("Thu, 01 Dec 1983 20:00:55 GMT", near_expire_dt)
|
||||
|
||||
@patch('contentserver.models.CdnUserAgentsConfig.get_cdn_user_agents')
|
||||
def test_cache_is_cdn_with_normal_request(self, mock_get_cdn_user_agents):
|
||||
"""
|
||||
Tests that when a normal request is made -- i.e. from an end user with their
|
||||
browser -- that we don't classify the request as coming from a CDN.
|
||||
"""
|
||||
mock_get_cdn_user_agents.return_value = 'Amazon CloudFront'
|
||||
|
||||
request_factory = RequestFactory()
|
||||
browser_request = request_factory.get('/fake', HTTP_USER_AGENT='Chrome 1234')
|
||||
|
||||
is_from_cdn = StaticContentServer.is_cdn_request(browser_request)
|
||||
self.assertEqual(is_from_cdn, False)
|
||||
|
||||
@patch('contentserver.models.CdnUserAgentsConfig.get_cdn_user_agents')
|
||||
def test_cache_is_cdn_with_cdn_request(self, mock_get_cdn_user_agents):
|
||||
"""
|
||||
Tests that when a CDN request is made -- i.e. from an edge node back to the
|
||||
origin -- that we classify the request as coming from a CDN.
|
||||
"""
|
||||
mock_get_cdn_user_agents.return_value = 'Amazon CloudFront'
|
||||
|
||||
request_factory = RequestFactory()
|
||||
browser_request = request_factory.get('/fake', HTTP_USER_AGENT='Amazon CloudFront')
|
||||
|
||||
is_from_cdn = StaticContentServer.is_cdn_request(browser_request)
|
||||
self.assertEqual(is_from_cdn, True)
|
||||
|
||||
@patch('contentserver.models.CdnUserAgentsConfig.get_cdn_user_agents')
|
||||
def test_cache_is_cdn_with_cdn_request_multiple_user_agents(self, mock_get_cdn_user_agents):
|
||||
"""
|
||||
Tests that when a CDN request is made -- i.e. from an edge node back to the
|
||||
origin -- that we classify the request as coming from a CDN when multiple UAs
|
||||
are configured.
|
||||
"""
|
||||
mock_get_cdn_user_agents.return_value = 'Amazon CloudFront\nAkamai GHost'
|
||||
|
||||
request_factory = RequestFactory()
|
||||
browser_request = request_factory.get('/fake', HTTP_USER_AGENT='Amazon CloudFront')
|
||||
|
||||
is_from_cdn = StaticContentServer.is_cdn_request(browser_request)
|
||||
self.assertEqual(is_from_cdn, True)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class ParseRangeHeaderTestCase(unittest.TestCase):
|
||||
|
||||
@@ -22,7 +22,7 @@ from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from student.models import CourseEnrollment
|
||||
import lms.djangoapps.commerce.tests.test_utils as ecomm_test_utils
|
||||
from course_modes.models import CourseMode, Mode
|
||||
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
|
||||
from openedx.core.djangoapps.theming.test_util import with_is_edx_domain
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -352,7 +352,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
|
||||
self.assertEquals(course_modes, expected_modes)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@with_comprehensive_theme("edx.org")
|
||||
@with_is_edx_domain(True)
|
||||
def test_hide_nav(self):
|
||||
# Create the course modes
|
||||
for mode in ["honor", "verified"]:
|
||||
|
||||
@@ -9,14 +9,9 @@ import pkg_resources
|
||||
|
||||
from django.conf import settings
|
||||
from mako.lookup import TemplateLookup
|
||||
from mako.exceptions import TopLevelLookupException
|
||||
|
||||
from microsite_configuration import microsite
|
||||
from . import LOOKUP
|
||||
from openedx.core.djangoapps.theming.helpers import (
|
||||
get_template as themed_template,
|
||||
get_template_path_with_theme,
|
||||
strip_site_theme_templates_path,
|
||||
)
|
||||
|
||||
|
||||
class DynamicTemplateLookup(TemplateLookup):
|
||||
@@ -54,25 +49,15 @@ class DynamicTemplateLookup(TemplateLookup):
|
||||
|
||||
def get_template(self, uri):
|
||||
"""
|
||||
Overridden method for locating a template in either the database or the site theme.
|
||||
|
||||
If not found, template lookup will be done in comprehensive theme for current site
|
||||
by prefixing path to theme.
|
||||
e.g if uri is `main.html` then new uri would be something like this `/red-theme/lms/static/main.html`
|
||||
|
||||
If still unable to find a template, it will fallback to the default template directories after stripping off
|
||||
the prefix path to theme.
|
||||
Overridden method which will hand-off the template lookup to the microsite subsystem
|
||||
"""
|
||||
template = themed_template(uri)
|
||||
microsite_template = microsite.get_template(uri)
|
||||
|
||||
if not template:
|
||||
try:
|
||||
template = super(DynamicTemplateLookup, self).get_template(get_template_path_with_theme(uri))
|
||||
except TopLevelLookupException:
|
||||
# strip off the prefix path to theme and look in default template dirs
|
||||
template = super(DynamicTemplateLookup, self).get_template(strip_site_theme_templates_path(uri))
|
||||
|
||||
return template
|
||||
return (
|
||||
microsite_template
|
||||
if microsite_template
|
||||
else super(DynamicTemplateLookup, self).get_template(uri)
|
||||
)
|
||||
|
||||
|
||||
def clear_lookups(namespace):
|
||||
|
||||
@@ -12,18 +12,16 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponse
|
||||
from django.template import Context
|
||||
from django.http import HttpResponse
|
||||
import logging
|
||||
|
||||
from microsite_configuration import microsite
|
||||
|
||||
from edxmako import lookup_template
|
||||
from edxmako.middleware import get_template_request_context
|
||||
from openedx.core.djangoapps.theming.helpers import get_template_path
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -115,7 +113,8 @@ def microsite_footer_context_processor(request):
|
||||
|
||||
def render_to_string(template_name, dictionary, context=None, namespace='main'):
|
||||
|
||||
template_name = get_template_path(template_name)
|
||||
# see if there is an override template defined in the microsite
|
||||
template_name = microsite.get_template_path(template_name)
|
||||
|
||||
context_instance = Context(dictionary)
|
||||
# add dictionary to context_instance
|
||||
|
||||
@@ -116,12 +116,8 @@ class MakoMiddlewareTest(TestCase):
|
||||
Test render_to_string() when makomiddleware has not initialized
|
||||
the threadlocal REQUEST_CONTEXT.context. This is meant to run in LMS.
|
||||
"""
|
||||
with patch("openedx.core.djangoapps.theming.helpers.get_current_site", return_value=None):
|
||||
del context_mock.context
|
||||
self.assertIn(
|
||||
"this module is temporarily unavailable",
|
||||
render_to_string("courseware/error-message.html", None),
|
||||
)
|
||||
del context_mock.context
|
||||
self.assertIn("this module is temporarily unavailable", render_to_string("courseware/error-message.html", None))
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
|
||||
@patch("edxmako.middleware.REQUEST_CONTEXT")
|
||||
@@ -130,9 +126,8 @@ class MakoMiddlewareTest(TestCase):
|
||||
Test render_to_string() when makomiddleware has not initialized
|
||||
the threadlocal REQUEST_CONTEXT.context. This is meant to run in CMS.
|
||||
"""
|
||||
with patch("openedx.core.djangoapps.theming.helpers.get_current_site", return_value=None):
|
||||
del context_mock.context
|
||||
self.assertIn("We're having trouble rendering your component", render_to_string("html_error.html", None))
|
||||
del context_mock.context
|
||||
self.assertIn("We're having trouble rendering your component", render_to_string("html_error.html", None))
|
||||
|
||||
|
||||
def mako_middleware_process_request(request):
|
||||
|
||||
@@ -11,6 +11,7 @@ BaseMicrositeTemplateBackend is Base Class for the microsite template backend.
|
||||
from __future__ import absolute_import
|
||||
|
||||
import abc
|
||||
import edxmako
|
||||
import os.path
|
||||
import threading
|
||||
|
||||
@@ -271,7 +272,9 @@ class BaseMicrositeBackend(AbstractBaseMicrositeBackend):
|
||||
Configure the paths for the microsites feature
|
||||
"""
|
||||
microsites_root = settings.MICROSITE_ROOT_DIR
|
||||
|
||||
if os.path.isdir(microsites_root):
|
||||
edxmako.paths.add_lookup('main', microsites_root)
|
||||
settings.STATICFILES_DIRS.insert(0, microsites_root)
|
||||
|
||||
log.info('Loading microsite path at %s', microsites_root)
|
||||
@@ -289,7 +292,6 @@ class BaseMicrositeBackend(AbstractBaseMicrositeBackend):
|
||||
microsites_root = settings.MICROSITE_ROOT_DIR
|
||||
|
||||
if self.has_configuration_set():
|
||||
settings.MAKO_TEMPLATES['main'].insert(0, microsites_root)
|
||||
settings.DEFAULT_TEMPLATE_ENGINE['DIRS'].append(microsites_root)
|
||||
|
||||
|
||||
|
||||
@@ -105,23 +105,6 @@ class DatabaseMicrositeBackendTests(DatabaseMicrositeTestCase):
|
||||
microsite.clear()
|
||||
self.assertIsNone(microsite.get_value('platform_name'))
|
||||
|
||||
def test_enable_microsites_pre_startup(self):
|
||||
"""
|
||||
Tests microsite.test_enable_microsites_pre_startup works as expected.
|
||||
"""
|
||||
# remove microsite root directory paths first
|
||||
settings.DEFAULT_TEMPLATE_ENGINE['DIRS'] = [
|
||||
path for path in settings.DEFAULT_TEMPLATE_ENGINE['DIRS']
|
||||
if path != settings.MICROSITE_ROOT_DIR
|
||||
]
|
||||
with patch.dict('django.conf.settings.FEATURES', {'USE_MICROSITES': False}):
|
||||
microsite.enable_microsites_pre_startup(log)
|
||||
self.assertNotIn(settings.MICROSITE_ROOT_DIR, settings.DEFAULT_TEMPLATE_ENGINE['DIRS'])
|
||||
with patch.dict('django.conf.settings.FEATURES', {'USE_MICROSITES': True}):
|
||||
microsite.enable_microsites_pre_startup(log)
|
||||
self.assertIn(settings.MICROSITE_ROOT_DIR, settings.DEFAULT_TEMPLATE_ENGINE['DIRS'])
|
||||
self.assertIn(settings.MICROSITE_ROOT_DIR, settings.MAKO_TEMPLATES['main'])
|
||||
|
||||
@patch('edxmako.paths.add_lookup')
|
||||
def test_enable_microsites(self, add_lookup):
|
||||
"""
|
||||
@@ -139,6 +122,7 @@ class DatabaseMicrositeBackendTests(DatabaseMicrositeTestCase):
|
||||
with patch.dict('django.conf.settings.FEATURES', {'USE_MICROSITES': True}):
|
||||
microsite.enable_microsites(log)
|
||||
self.assertIn(settings.MICROSITE_ROOT_DIR, settings.STATICFILES_DIRS)
|
||||
add_lookup.assert_called_once_with('main', settings.MICROSITE_ROOT_DIR)
|
||||
|
||||
def test_get_all_configs(self):
|
||||
"""
|
||||
|
||||
@@ -4,13 +4,7 @@ from pipeline_mako import compressed_css, compressed_js
|
||||
from django.utils.translation import get_language_bidi
|
||||
from mako.exceptions import TemplateLookupException
|
||||
|
||||
from openedx.core.djangoapps.theming.helpers import (
|
||||
get_page_title_breadcrumbs,
|
||||
get_value,
|
||||
get_template_path,
|
||||
get_themed_template_path,
|
||||
is_request_in_themed_site,
|
||||
)
|
||||
from openedx.core.djangoapps.theming.helpers import get_page_title_breadcrumbs, get_value, get_template_path, get_themed_template_path, is_request_in_themed_site
|
||||
from certificates.api import get_asset_url_by_slug
|
||||
from lang_pref.api import released_languages
|
||||
%>
|
||||
|
||||
@@ -21,7 +21,7 @@ from edxmako.shortcuts import render_to_string
|
||||
from edxmako.tests import mako_middleware_process_request
|
||||
from util.request import safe_get_host
|
||||
from util.testing import EventTestMixin
|
||||
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
|
||||
from openedx.core.djangoapps.theming.test_util import with_is_edx_domain
|
||||
|
||||
|
||||
class TestException(Exception):
|
||||
@@ -99,7 +99,7 @@ class ActivationEmailTests(TestCase):
|
||||
self._create_account()
|
||||
self._assert_activation_email(self.ACTIVATION_SUBJECT, self.OPENEDX_FRAGMENTS)
|
||||
|
||||
@with_comprehensive_theme("edx.org")
|
||||
@with_is_edx_domain(True)
|
||||
def test_activation_email_edx_domain(self):
|
||||
self._create_account()
|
||||
self._assert_activation_email(self.ACTIVATION_SUBJECT, self.EDX_DOMAIN_FRAGMENTS)
|
||||
|
||||
@@ -45,6 +45,7 @@ from certificates.tests.factories import GeneratedCertificateFactory # pylint:
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
|
||||
import shoppingcart # pylint: disable=import-error
|
||||
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
|
||||
from openedx.core.djangoapps.theming.test_util import with_is_edx_domain
|
||||
|
||||
# Explicitly import the cache from ConfigurationModel so we can reset it after each test
|
||||
from config_models.models import cache
|
||||
@@ -495,6 +496,7 @@ class DashboardTest(ModuleStoreTestCase):
|
||||
self.assertEquals(response_2.status_code, 200)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@with_is_edx_domain(True)
|
||||
def test_dashboard_header_nav_has_find_courses(self):
|
||||
self.client.login(username="jack", password="test")
|
||||
response = self.client.get(reverse("dashboard"))
|
||||
|
||||
@@ -247,87 +247,6 @@ describe 'Problem', ->
|
||||
runs ->
|
||||
expect(@problem.checkButtonLabel.text).toHaveBeenCalledWith 'Check'
|
||||
|
||||
describe 'check button on problems', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
@checkDisabled = (v) -> expect(@problem.checkButton.hasClass('is-disabled')).toBe(v)
|
||||
|
||||
describe 'some basic tests for check button', ->
|
||||
it 'should become enabled after a value is entered into the text box', ->
|
||||
$('#input_example_1').val('test').trigger('input')
|
||||
@checkDisabled false
|
||||
$('#input_example_1').val('').trigger('input')
|
||||
@checkDisabled true
|
||||
|
||||
describe 'some advanced tests for check button', ->
|
||||
it 'should become enabled after a checkbox is checked', ->
|
||||
html = '''
|
||||
<div class="choicegroup">
|
||||
<label for="input_1_1_1"><input type="checkbox" name="input_1_1" id="input_1_1_1" value="1"> One</label>
|
||||
<label for="input_1_1_2"><input type="checkbox" name="input_1_1" id="input_1_1_2" value="2"> Two</label>
|
||||
<label for="input_1_1_3"><input type="checkbox" name="input_1_1" id="input_1_1_3" value="3"> Three</label>
|
||||
</div>
|
||||
'''
|
||||
$('#input_example_1').replaceWith(html)
|
||||
@problem.checkAnswersAndCheckButton true
|
||||
@checkDisabled true
|
||||
$('#input_1_1_1').attr('checked', true).trigger('click')
|
||||
@checkDisabled false
|
||||
$('#input_1_1_1').attr('checked', false).trigger('click')
|
||||
@checkDisabled true
|
||||
|
||||
it 'should become enabled after a radiobutton is checked', ->
|
||||
html = '''
|
||||
<div class="choicegroup">
|
||||
<label for="input_1_1_1"><input type="radio" name="input_1_1" id="input_1_1_1" value="1"> One</label>
|
||||
<label for="input_1_1_2"><input type="radio" name="input_1_1" id="input_1_1_2" value="2"> Two</label>
|
||||
<label for="input_1_1_3"><input type="radio" name="input_1_1" id="input_1_1_3" value="3"> Three</label>
|
||||
</div>
|
||||
'''
|
||||
$('#input_example_1').replaceWith(html)
|
||||
@problem.checkAnswersAndCheckButton true
|
||||
@checkDisabled true
|
||||
$('#input_1_1_1').attr('checked', true).trigger('click')
|
||||
@checkDisabled false
|
||||
$('#input_1_1_1').attr('checked', false).trigger('click')
|
||||
@checkDisabled true
|
||||
|
||||
it 'should become enabled after a value is selected in a selector', ->
|
||||
html = '''
|
||||
<div id="problem_sel">
|
||||
<select>
|
||||
<option value="val0"></option>
|
||||
<option value="val1">1</option>
|
||||
<option value="val2">2</option>
|
||||
</select>
|
||||
</div>
|
||||
'''
|
||||
$('#input_example_1').replaceWith(html)
|
||||
@problem.checkAnswersAndCheckButton true
|
||||
@checkDisabled true
|
||||
$("#problem_sel select").val("val2").trigger('change')
|
||||
@checkDisabled false
|
||||
$("#problem_sel select").val("val0").trigger('change')
|
||||
@checkDisabled true
|
||||
|
||||
it 'should become enabled after a radiobutton is checked and a value is entered into the text box', ->
|
||||
html = '''
|
||||
<div class="choicegroup">
|
||||
<label for="input_1_1_1"><input type="radio" name="input_1_1" id="input_1_1_1" value="1"> One</label>
|
||||
<label for="input_1_1_2"><input type="radio" name="input_1_1" id="input_1_1_2" value="2"> Two</label>
|
||||
<label for="input_1_1_3"><input type="radio" name="input_1_1" id="input_1_1_3" value="3"> Three</label>
|
||||
</div>
|
||||
'''
|
||||
$(html).insertAfter('#input_example_1')
|
||||
@problem.checkAnswersAndCheckButton true
|
||||
@checkDisabled true
|
||||
$('#input_1_1_1').attr('checked', true).trigger('click')
|
||||
@checkDisabled true
|
||||
$('#input_example_1').val('111').trigger('input')
|
||||
@checkDisabled false
|
||||
$('#input_1_1_1').attr('checked', false).trigger('click')
|
||||
@checkDisabled true
|
||||
|
||||
describe 'reset', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
|
||||
@@ -49,8 +49,6 @@ class @Problem
|
||||
window.globalTooltipManager.hide()
|
||||
|
||||
@bindResetCorrectness()
|
||||
if @checkButton.length
|
||||
@checkAnswersAndCheckButton true
|
||||
|
||||
# Collapsibles
|
||||
Collapsible.setCollapsibles(@el)
|
||||
@@ -454,58 +452,6 @@ class @Problem
|
||||
element.CodeMirror.save() if element.CodeMirror.save
|
||||
@answers = @inputs.serialize()
|
||||
|
||||
checkAnswersAndCheckButton: (bind=false) =>
|
||||
# Used to check available answers and if something is checked (or the answer is set in some textbox)
|
||||
# "Check"/"Final check" button becomes enabled. Otherwise it is disabled by default.
|
||||
# params:
|
||||
# 'bind' used on the first check to attach event handlers to input fields
|
||||
# to change "Check"/"Final check" enable status in case of some manipulations with answers
|
||||
answered = true
|
||||
|
||||
at_least_one_text_input_found = false
|
||||
one_text_input_filled = false
|
||||
@el.find("input:text").each (i, text_field) =>
|
||||
at_least_one_text_input_found = true
|
||||
if $(text_field).is(':visible')
|
||||
if $(text_field).val() isnt ''
|
||||
one_text_input_filled = true
|
||||
if bind
|
||||
$(text_field).on 'input', (e) =>
|
||||
@checkAnswersAndCheckButton()
|
||||
return
|
||||
return
|
||||
if at_least_one_text_input_found and not one_text_input_filled
|
||||
answered = false
|
||||
|
||||
@el.find(".choicegroup").each (i, choicegroup_block) =>
|
||||
checked = false
|
||||
$(choicegroup_block).find("input[type=checkbox], input[type=radio]").each (j, checkbox_or_radio) =>
|
||||
if $(checkbox_or_radio).is(':checked')
|
||||
checked = true
|
||||
if bind
|
||||
$(checkbox_or_radio).on 'click', (e) =>
|
||||
@checkAnswersAndCheckButton()
|
||||
return
|
||||
return
|
||||
if not checked
|
||||
answered = false
|
||||
return
|
||||
|
||||
@el.find("select").each (i, select_field) =>
|
||||
selected_option = $(select_field).find("option:selected").text().trim()
|
||||
if selected_option is ''
|
||||
answered = false
|
||||
if bind
|
||||
$(select_field).on 'change', (e) =>
|
||||
@checkAnswersAndCheckButton()
|
||||
return
|
||||
return
|
||||
|
||||
if answered
|
||||
@enableCheckButton true
|
||||
else
|
||||
@enableCheckButton false, false
|
||||
|
||||
bindResetCorrectness: ->
|
||||
# Loop through all input types
|
||||
# Bind the reset functions at that scope.
|
||||
|
||||
@@ -6,7 +6,6 @@ See also lettuce tests in lms/djangoapps/courseware/features/problems.feature
|
||||
import random
|
||||
import textwrap
|
||||
|
||||
from nose import SkipTest
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from nose.plugins.attrib import attr
|
||||
from selenium.webdriver import ActionChains
|
||||
@@ -136,8 +135,6 @@ class ProblemTypeTestMixin(object):
|
||||
"""
|
||||
Test cases shared amongst problem types.
|
||||
"""
|
||||
can_submit_blank = False
|
||||
|
||||
@attr('shard_7')
|
||||
def test_answer_correctly(self):
|
||||
"""
|
||||
@@ -203,34 +200,15 @@ class ProblemTypeTestMixin(object):
|
||||
Then my "<ProblemType>" answer is marked "incorrect"
|
||||
And The "<ProblemType>" problem displays a "blank" answer
|
||||
"""
|
||||
if not self.can_submit_blank:
|
||||
raise SkipTest("Test incompatible with the current problem type")
|
||||
|
||||
self.problem_page.wait_for(
|
||||
lambda: self.problem_page.problem_name == self.problem_name,
|
||||
"Make sure the correct problem is on the page"
|
||||
)
|
||||
|
||||
# Leave the problem unchanged and click check.
|
||||
self.assertNotIn('is-disabled', self.problem_page.q(css='div.problem button.check').attrs('class')[0])
|
||||
self.problem_page.click_check()
|
||||
self.wait_for_status('incorrect')
|
||||
|
||||
@attr('shard_7')
|
||||
def test_cant_submit_blank_answer(self):
|
||||
"""
|
||||
Scenario: I can't submit a blank answer
|
||||
When I try to submit blank answer
|
||||
Then I can't check a problem
|
||||
"""
|
||||
if self.can_submit_blank:
|
||||
raise SkipTest("Test incompatible with the current problem type")
|
||||
|
||||
self.problem_page.wait_for(
|
||||
lambda: self.problem_page.problem_name == self.problem_name,
|
||||
"Make sure the correct problem is on the page"
|
||||
)
|
||||
self.assertIn('is-disabled', self.problem_page.q(css='div.problem button.check').attrs('class')[0])
|
||||
|
||||
@attr('a11y')
|
||||
def test_problem_type_a11y(self):
|
||||
"""
|
||||
@@ -258,8 +236,6 @@ class AnnotationProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
|
||||
|
||||
factory = AnnotationResponseXMLFactory()
|
||||
|
||||
can_submit_blank = True
|
||||
|
||||
factory_kwargs = {
|
||||
'title': 'Annotation Problem',
|
||||
'text': 'The text being annotated',
|
||||
@@ -710,13 +686,6 @@ class CodeProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
|
||||
"""
|
||||
pass
|
||||
|
||||
def test_cant_submit_blank_answer(self):
|
||||
"""
|
||||
Overridden for script test because the testing grader always responds
|
||||
with "correct"
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ChoiceTextProbelmTypeTestBase(ProblemTypeTestBase):
|
||||
"""
|
||||
@@ -832,8 +801,6 @@ class ImageProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
|
||||
|
||||
factory = ImageResponseXMLFactory()
|
||||
|
||||
can_submit_blank = True
|
||||
|
||||
factory_kwargs = {
|
||||
'src': '/static/images/placeholder-image.png',
|
||||
'rectangle': '(0,0)-(50,50)',
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
[
|
||||
{
|
||||
"pk": 2,
|
||||
"model": "sites.Site",
|
||||
|
||||
"fields": {
|
||||
"domain": "localhost:8003",
|
||||
"name": "lms"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 3,
|
||||
"model": "sites.Site",
|
||||
|
||||
"fields": {
|
||||
"domain": "localhost:8031",
|
||||
"name": "cms"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1 +0,0 @@
|
||||
*.css
|
||||
@@ -1,255 +0,0 @@
|
||||
// studio - utilities - variables
|
||||
// ====================
|
||||
|
||||
// Table of Contents
|
||||
// * +Paths
|
||||
// * +Grid
|
||||
// * +Fonts
|
||||
// * +Colors - Utility
|
||||
// * +Colors - Primary
|
||||
// * +Colors - Shadow
|
||||
// * +Color - Application
|
||||
// * +Timing
|
||||
// * +Archetype UI
|
||||
// * +Specific UI
|
||||
// * +Deprecated
|
||||
|
||||
$baseline: 20px;
|
||||
|
||||
// +Paths
|
||||
// ====================
|
||||
$static-path: '..' !default;
|
||||
|
||||
// +Grid
|
||||
// ====================
|
||||
$gw-column: ($baseline*3);
|
||||
$gw-gutter: $baseline;
|
||||
$fg-column: $gw-column;
|
||||
$fg-gutter: $gw-gutter;
|
||||
$fg-max-columns: 12;
|
||||
$fg-max-width: 1280px;
|
||||
$fg-min-width: 900px;
|
||||
|
||||
// +Fonts
|
||||
// ====================
|
||||
$f-sans-serif: 'Open Sans','Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
$f-monospace: 'Bitstream Vera Sans Mono', Consolas, Courier, monospace;
|
||||
|
||||
// +Colors - Utility
|
||||
// ====================
|
||||
$transparent: rgba(0,0,0,0); // used when color value is needed for UI width/transitions but element is transparent
|
||||
|
||||
// +Colors - Primary
|
||||
// ====================
|
||||
$black: rgb(0,0,0);
|
||||
$black-t0: rgba($black, 0.125);
|
||||
$black-t1: rgba($black, 0.25);
|
||||
$black-t2: rgba($black, 0.5);
|
||||
$black-t3: rgba($black, 0.75);
|
||||
|
||||
$white: rgb(255,255,255);
|
||||
$white-t0: rgba($white, 0.125);
|
||||
$white-t1: rgba($white, 0.25);
|
||||
$white-t2: rgba($white, 0.5);
|
||||
$white-t3: rgba($white, 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-l7: tint($gray,99%);
|
||||
$gray-d1: shade($gray,20%);
|
||||
$gray-d2: shade($gray,40%);
|
||||
$gray-d3: shade($gray,60%);
|
||||
$gray-d4: shade($gray,80%);
|
||||
|
||||
$blue: rgb(0, 159, 230);
|
||||
$blue-l1: tint($blue,20%);
|
||||
$blue-l2: tint($blue,40%);
|
||||
$blue-l3: tint($blue,60%);
|
||||
$blue-l4: tint($blue,80%);
|
||||
$blue-l5: tint($blue,90%);
|
||||
$blue-d1: shade($blue,20%);
|
||||
$blue-d2: shade($blue,40%);
|
||||
$blue-d3: shade($blue,60%);
|
||||
$blue-d4: shade($blue,80%);
|
||||
$blue-s1: saturate($blue,15%);
|
||||
$blue-s2: saturate($blue,30%);
|
||||
$blue-s3: saturate($blue,45%);
|
||||
$blue-u1: desaturate($blue,15%);
|
||||
$blue-u2: desaturate($blue,30%);
|
||||
$blue-u3: desaturate($blue,45%);
|
||||
$blue-t0: rgba($blue, 0.125);
|
||||
$blue-t1: rgba($blue, 0.25);
|
||||
$blue-t2: rgba($blue, 0.50);
|
||||
$blue-t3: rgba($blue, 0.75);
|
||||
|
||||
$pink: rgb(183, 37, 103); // #b72567;
|
||||
$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%);
|
||||
|
||||
$red: rgb(178, 6, 16); // #b20610;
|
||||
$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); // #25b85a
|
||||
$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%);
|
||||
|
||||
$yellow: rgb(237, 189, 60);
|
||||
$yellow-l1: tint($yellow,20%);
|
||||
$yellow-l2: tint($yellow,40%);
|
||||
$yellow-l3: tint($yellow,60%);
|
||||
$yellow-l4: tint($yellow,80%);
|
||||
$yellow-l5: tint($yellow,90%);
|
||||
$yellow-d1: shade($yellow,20%);
|
||||
$yellow-d2: shade($yellow,40%);
|
||||
$yellow-d3: shade($yellow,60%);
|
||||
$yellow-d4: shade($yellow,80%);
|
||||
$yellow-s1: saturate($yellow,15%);
|
||||
$yellow-s2: saturate($yellow,30%);
|
||||
$yellow-s3: saturate($yellow,45%);
|
||||
$yellow-u1: desaturate($yellow,15%);
|
||||
$yellow-u2: desaturate($yellow,30%);
|
||||
$yellow-u3: desaturate($yellow,45%);
|
||||
|
||||
$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%);
|
||||
|
||||
// +Colors - Shadows
|
||||
// ====================
|
||||
$shadow: rgba($black, 0.2);
|
||||
$shadow-l1: rgba($black, 0.1);
|
||||
$shadow-l2: rgba($black, 0.05);
|
||||
$shadow-d1: rgba($black, 0.4);
|
||||
$shadow-d2: rgba($black, 0.6);
|
||||
|
||||
// +Colors - Application
|
||||
// ====================
|
||||
$color-draft: $gray-l3;
|
||||
$color-live: $blue;
|
||||
$color-ready: $green;
|
||||
$color-warning: $orange-l2;
|
||||
$color-error: $red-l2;
|
||||
$color-staff-only: $black;
|
||||
$color-gated: $black;
|
||||
$color-visibility-set: $black;
|
||||
|
||||
$color-heading-base: $gray-d2;
|
||||
$color-copy-base: $gray-l1;
|
||||
$color-copy-emphasized: $gray-d2;
|
||||
|
||||
// +Timing
|
||||
// ====================
|
||||
// used for animation/transition mixin syncing
|
||||
$tmg-s3: 3.0s;
|
||||
$tmg-s2: 2.0s;
|
||||
$tmg-s1: 1.0s;
|
||||
$tmg-avg: 0.75s;
|
||||
$tmg-f1: 0.50s;
|
||||
$tmg-f2: 0.25s;
|
||||
$tmg-f3: 0.125s;
|
||||
|
||||
// +Archetype UI
|
||||
// ====================
|
||||
$ui-action-primary-color: $blue-u2;
|
||||
$ui-action-primary-color-focus: $blue-s1;
|
||||
|
||||
$ui-link-color: $blue-u2;
|
||||
$ui-link-color-focus: $blue-s1;
|
||||
|
||||
// +Specific UI
|
||||
// ====================
|
||||
$ui-notification-height: ($baseline*10);
|
||||
$ui-update-color: $blue-l4;
|
||||
|
||||
// +Deprecated
|
||||
// ====================
|
||||
// do not use, future clean up will use updated styles
|
||||
$baseFontColor: $gray-d2;
|
||||
$lighter-base-font-color: rgb(100,100,100);
|
||||
$offBlack: #3c3c3c;
|
||||
$green: #108614;
|
||||
$lightGrey: #edf1f5;
|
||||
$mediumGrey: #b0b6c2;
|
||||
$darkGrey: #8891a1;
|
||||
$extraDarkGrey: #3d4043;
|
||||
$paleYellow: #fffcf1;
|
||||
$yellow: rgb(255, 254, 223);
|
||||
$green: rgb(37, 184, 90);
|
||||
$brightGreen: rgb(22, 202, 87);
|
||||
$disabledGreen: rgb(124, 206, 153);
|
||||
$darkGreen: rgb(52, 133, 76);
|
||||
|
||||
// These colors are updated for testing purposes
|
||||
$lightBluishGrey: rgb(0, 250, 0);
|
||||
$lightBluishGrey2: rgb(0, 250, 0);
|
||||
$error-red: rgb(253, 87, 87);
|
||||
|
||||
|
||||
//carryover from LMS for xmodules
|
||||
$sidebar-color: rgb(246, 246, 246);
|
||||
|
||||
// type
|
||||
$sans-serif: $f-sans-serif;
|
||||
$body-line-height: golden-ratio(.875em, 1);
|
||||
|
||||
// carried over from LMS for xmodules
|
||||
$action-primary-active-bg: #1AA1DE; // $m-blue
|
||||
$very-light-text: $white;
|
||||
@@ -1,55 +0,0 @@
|
||||
<%inherit file="base.html" />
|
||||
<%def name="online_help_token()"><% return "login" %></%def>
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
%>
|
||||
<%block name="title">${_("Sign In")}</%block>
|
||||
<%block name="bodyclass">not-signedin view-signin</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<header>
|
||||
<h1 class="title title-1">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</h1>
|
||||
<a href="${reverse('signup')}" class="action action-signin">${_("Don't have a {studio_name} Account? Sign up!").format(studio_name=settings.STUDIO_SHORT_NAME)}</a>
|
||||
</header>
|
||||
<!-- Login Page override for test-theme. -->
|
||||
<article class="content-primary" role="main">
|
||||
<form id="login_form" method="post" action="login_post" onsubmit="return false;">
|
||||
|
||||
<fieldset>
|
||||
<legend class="sr">${_("Required Information to Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</legend>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf }" />
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field text required" id="field-email">
|
||||
<label for="email">${_("E-mail")}</label>
|
||||
<input id="email" type="email" name="email" placeholder="${_('example: username@domain.com')}"/>
|
||||
</li>
|
||||
|
||||
<li class="field text required" id="field-password">
|
||||
<label for="password">${_("Password")}</label>
|
||||
<input id="password" type="password" name="password" />
|
||||
<a href="${forgot_password_link}" class="action action-forgotpassword">${_("Forgot password?")}</a>
|
||||
</li>
|
||||
</ol>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" id="submit" name="submit" class="action action-primary">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</button>
|
||||
</div>
|
||||
|
||||
<!-- no honor code for CMS, but need it because we're using the lms student object -->
|
||||
<input name="honor_code" type="checkbox" value="true" checked="true" hidden="true">
|
||||
</form>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/login"], function(LoginFactory) {
|
||||
LoginFactory("${reverse('homepage')}");
|
||||
});
|
||||
</%block>
|
||||
@@ -1 +0,0 @@
|
||||
*.css
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 493 B |
@@ -1,5 +0,0 @@
|
||||
@import 'lms/static/sass/partials/base/variables';
|
||||
|
||||
$header-bg: rgb(0,250,0);
|
||||
$footer-bg: rgb(0,250,0);
|
||||
$container-bg: rgb(0,250,0);
|
||||
@@ -1,10 +0,0 @@
|
||||
<div class="wrapper wrapper-footer">
|
||||
<footer>
|
||||
<div class="colophon">
|
||||
<div class="colophon-about">
|
||||
<p>This is a footer for test-theme.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</footer>
|
||||
</div>
|
||||
Reference in New Issue
Block a user