Merge pull request #15456 from edx/andya/anonymous-course-access
Handle anonymous and unenrolled users on the new course home page
This commit is contained in:
@@ -134,6 +134,8 @@ def anonymous_id_for_user(user, course_id, save=True):
|
||||
save -- Whether the id should be saved in an AnonymousUserId object.
|
||||
"""
|
||||
# This part is for ability to get xblock instance in xblock_noauth handlers, where user is unauthenticated.
|
||||
assert user
|
||||
|
||||
if user.is_anonymous():
|
||||
return None
|
||||
|
||||
@@ -681,6 +683,8 @@ class PasswordHistory(models.Model):
|
||||
Returns whether a password has 'expired' and should be reset. Note there are two different
|
||||
expiry policies for staff and students
|
||||
"""
|
||||
assert user
|
||||
|
||||
if not settings.FEATURES['ADVANCED_SECURITY']:
|
||||
return False
|
||||
|
||||
@@ -736,6 +740,8 @@ class PasswordHistory(models.Model):
|
||||
"""
|
||||
Verifies that the password adheres to the reuse policies
|
||||
"""
|
||||
assert user
|
||||
|
||||
if not settings.FEATURES['ADVANCED_SECURITY']:
|
||||
return True
|
||||
|
||||
@@ -1082,6 +1088,10 @@ class CourseEnrollment(models.Model):
|
||||
Returns:
|
||||
Course enrollment object or None
|
||||
"""
|
||||
assert user
|
||||
|
||||
if user.is_anonymous():
|
||||
return None
|
||||
try:
|
||||
return cls.objects.get(
|
||||
user=user,
|
||||
@@ -1397,11 +1407,8 @@ class CourseEnrollment(models.Model):
|
||||
|
||||
`course_id` is our usual course_id string (e.g. "edX/Test101/2013_Fall)
|
||||
"""
|
||||
if not user.is_authenticated():
|
||||
return False
|
||||
else:
|
||||
enrollment_state = cls._get_enrollment_state(user, course_key)
|
||||
return enrollment_state.is_active or False
|
||||
enrollment_state = cls._get_enrollment_state(user, course_key)
|
||||
return enrollment_state.is_active or False
|
||||
|
||||
@classmethod
|
||||
def is_enrolled_by_partial(cls, user, course_id_partial):
|
||||
@@ -1497,6 +1504,8 @@ class CourseEnrollment(models.Model):
|
||||
Returns:
|
||||
str: Hash of the user's active enrollments. If the user is anonymous, `None` is returned.
|
||||
"""
|
||||
assert user
|
||||
|
||||
if user.is_anonymous():
|
||||
return None
|
||||
|
||||
@@ -1704,6 +1713,10 @@ class CourseEnrollment(models.Model):
|
||||
Returns the CourseEnrollmentState for the given user
|
||||
and course_key, caching the result for later retrieval.
|
||||
"""
|
||||
assert user
|
||||
|
||||
if user.is_anonymous():
|
||||
return CourseEnrollmentState(None, None)
|
||||
enrollment_state = cls._get_enrollment_in_request_cache(user, course_key)
|
||||
if not enrollment_state:
|
||||
try:
|
||||
|
||||
@@ -6,15 +6,19 @@ import copy
|
||||
import functools
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from enum import Enum
|
||||
|
||||
from courseware.field_overrides import OverrideFieldData # pylint: disable=import-error
|
||||
from courseware.tests.factories import StaffFactory
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from mock import patch
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationMixin, CacheIsolationTestCase, FilteredQueryCountMixin
|
||||
from openedx.core.lib.tempdir import mkdtemp_clean
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.contentstore.django import _CONTENTSTORE
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import SignalHandler, clear_existing_modulestores, modulestore
|
||||
@@ -22,6 +26,18 @@ from xmodule.modulestore.tests.factories import XMODULE_FACTORY_LOCK
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_HOST, MONGO_PORT_NUM
|
||||
|
||||
|
||||
class CourseUserType(Enum):
|
||||
"""
|
||||
Types of users to be used when testing a course.
|
||||
"""
|
||||
ANONYMOUS = 'anonymous'
|
||||
COURSE_STAFF = 'course_staff'
|
||||
ENROLLED = 'enrolled'
|
||||
GLOBAL_STAFF = 'global_staff'
|
||||
UNENROLLED = 'unenrolled'
|
||||
UNENROLLED_STAFF = 'unenrolled_staff'
|
||||
|
||||
|
||||
class StoreConstructors(object):
|
||||
"""Enumeration of store constructor types."""
|
||||
draft, split = range(2)
|
||||
@@ -308,7 +324,36 @@ class ModuleStoreIsolationMixin(CacheIsolationMixin, SignalIsolationMixin):
|
||||
cls.enable_all_signals()
|
||||
|
||||
|
||||
class SharedModuleStoreTestCase(FilteredQueryCountMixin, ModuleStoreIsolationMixin, CacheIsolationTestCase):
|
||||
class ModuleStoreTestUsersMixin():
|
||||
"""
|
||||
A mixin to help manage test users.
|
||||
"""
|
||||
TEST_PASSWORD = 'test'
|
||||
|
||||
def create_user_for_course(self, course, user_type=CourseUserType.ENROLLED):
|
||||
"""
|
||||
Create a test user for a course.
|
||||
"""
|
||||
if user_type is CourseUserType.ANONYMOUS:
|
||||
return AnonymousUser()
|
||||
|
||||
is_enrolled = user_type is CourseUserType.ENROLLED
|
||||
is_unenrolled_staff = user_type is CourseUserType.UNENROLLED_STAFF
|
||||
|
||||
# Set up the test user
|
||||
if is_unenrolled_staff:
|
||||
user = StaffFactory(course_key=course.id, password=self.TEST_PASSWORD)
|
||||
else:
|
||||
user = UserFactory(password=self.TEST_PASSWORD)
|
||||
self.client.login(username=user.username, password=self.TEST_PASSWORD)
|
||||
if is_enrolled:
|
||||
CourseEnrollment.enroll(user, course.id)
|
||||
return user
|
||||
|
||||
|
||||
class SharedModuleStoreTestCase(
|
||||
ModuleStoreTestUsersMixin, FilteredQueryCountMixin, ModuleStoreIsolationMixin, CacheIsolationTestCase
|
||||
):
|
||||
"""
|
||||
Subclass for any test case that uses a ModuleStore that can be shared
|
||||
between individual tests. This class ensures that the ModuleStore is cleaned
|
||||
@@ -391,7 +436,9 @@ class SharedModuleStoreTestCase(FilteredQueryCountMixin, ModuleStoreIsolationMix
|
||||
super(SharedModuleStoreTestCase, self).setUp()
|
||||
|
||||
|
||||
class ModuleStoreTestCase(FilteredQueryCountMixin, ModuleStoreIsolationMixin, TestCase):
|
||||
class ModuleStoreTestCase(
|
||||
ModuleStoreTestUsersMixin, FilteredQueryCountMixin, ModuleStoreIsolationMixin, TestCase
|
||||
):
|
||||
"""
|
||||
Subclass for any test case that uses a ModuleStore.
|
||||
Ensures that the ModuleStore is cleaned before/after each test.
|
||||
|
||||
@@ -113,21 +113,21 @@ def check_course_access(course, user, action, check_if_enrolled=False):
|
||||
Check that the user has the access to perform the specified action
|
||||
on the course (CourseDescriptor|CourseOverview).
|
||||
|
||||
check_if_enrolled: If true, additionally verifies that the user is either
|
||||
enrolled in the course or has staff access.
|
||||
check_if_enrolled: If true, additionally verifies that the user is enrolled.
|
||||
"""
|
||||
access_response = has_access(user, action, course, course.id)
|
||||
# Allow staff full access to the course even if not enrolled
|
||||
if has_access(user, 'staff', course.id):
|
||||
return
|
||||
|
||||
access_response = has_access(user, action, course, course.id)
|
||||
if not access_response:
|
||||
# Deliberately return a non-specific error message to avoid
|
||||
# leaking info about access control settings
|
||||
raise CoursewareAccessException(access_response)
|
||||
|
||||
if check_if_enrolled:
|
||||
# Verify that the user is either enrolled in the course or a staff
|
||||
# member. If the user is not enrolled, raise a Redirect exception
|
||||
# that will be handled by middleware.
|
||||
if not ((user.id and CourseEnrollment.is_enrolled(user, course.id)) or has_access(user, 'staff', course)):
|
||||
# If the user is not enrolled, redirect them to the about page
|
||||
if not CourseEnrollment.is_enrolled(user, course.id):
|
||||
raise CourseAccessRedirect(reverse('about_course', args=[unicode(course.id)]))
|
||||
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ class DateSummary(object):
|
||||
future.
|
||||
"""
|
||||
if self.date is not None:
|
||||
return datetime.now(utc) <= self.date
|
||||
return datetime.now(utc).date() <= self.date.date()
|
||||
return False
|
||||
|
||||
def deadline_has_passed(self):
|
||||
|
||||
@@ -36,6 +36,16 @@ class CoursewareTab(EnrolledTab):
|
||||
is_default = False
|
||||
supports_preview_menu = True
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls, course, user=None):
|
||||
"""
|
||||
Returns true if this tab is enabled.
|
||||
"""
|
||||
# If this is the unified course tab then it is always enabled
|
||||
if UNIFIED_COURSE_TAB_FLAG.is_enabled(course.id):
|
||||
return True
|
||||
return super(CoursewareTab, cls).is_enabled(course, user)
|
||||
|
||||
@property
|
||||
def link_func(self):
|
||||
"""
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
"""
|
||||
integration tests for xmodule
|
||||
|
||||
Contains:
|
||||
|
||||
1. BaseTestXmodule class provides course and users
|
||||
for testing Xmodules with mongo store.
|
||||
"""
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.client import Client
|
||||
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from lms.djangoapps.lms_xblock.field_data import LmsFieldData
|
||||
from openedx.core.lib.url_utils import quote_slashes
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from xblock.field_data import DictFieldData
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_MODULESTORE
|
||||
from xmodule.tests import get_test_system, get_test_descriptor_system
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
|
||||
class BaseTestXmodule(ModuleStoreTestCase):
|
||||
"""Base class for testing Xmodules with mongo store.
|
||||
|
||||
This class prepares course and users for tests:
|
||||
1. create test course;
|
||||
2. create, enroll and login users for this course;
|
||||
|
||||
Any xmodule should overwrite only next parameters for test:
|
||||
1. CATEGORY
|
||||
2. DATA or METADATA
|
||||
3. MODEL_DATA
|
||||
4. COURSE_DATA and USER_COUNT if needed
|
||||
|
||||
This class should not contain any tests, because CATEGORY
|
||||
should be defined in child class.
|
||||
"""
|
||||
MODULESTORE = TEST_DATA_MONGO_MODULESTORE
|
||||
|
||||
USER_COUNT = 2
|
||||
COURSE_DATA = {}
|
||||
|
||||
# Data from YAML common/lib/xmodule/xmodule/templates/NAME/default.yaml
|
||||
CATEGORY = "vertical"
|
||||
DATA = ''
|
||||
# METADATA must be overwritten for every instance that uses it. Otherwise,
|
||||
# if we'll change it in the tests, it will be changed for all other instances
|
||||
# of parent class.
|
||||
METADATA = {}
|
||||
MODEL_DATA = {'data': '<some_module></some_module>'}
|
||||
|
||||
def new_module_runtime(self):
|
||||
"""
|
||||
Generate a new ModuleSystem that is minimally set up for testing
|
||||
"""
|
||||
return get_test_system(course_id=self.course.id)
|
||||
|
||||
def new_descriptor_runtime(self):
|
||||
runtime = get_test_descriptor_system()
|
||||
runtime.get_block = modulestore().get_item
|
||||
return runtime
|
||||
|
||||
def initialize_module(self, **kwargs):
|
||||
kwargs.update({
|
||||
'parent_location': self.section.location,
|
||||
'category': self.CATEGORY
|
||||
})
|
||||
|
||||
self.item_descriptor = ItemFactory.create(**kwargs)
|
||||
|
||||
self.runtime = self.new_descriptor_runtime()
|
||||
|
||||
field_data = {}
|
||||
field_data.update(self.MODEL_DATA)
|
||||
student_data = DictFieldData(field_data)
|
||||
self.item_descriptor._field_data = LmsFieldData(self.item_descriptor._field_data, student_data)
|
||||
|
||||
self.item_descriptor.xmodule_runtime = self.new_module_runtime()
|
||||
|
||||
self.item_url = unicode(self.item_descriptor.location)
|
||||
|
||||
def setup_course(self):
|
||||
self.course = CourseFactory.create(data=self.COURSE_DATA)
|
||||
|
||||
# Turn off cache.
|
||||
modulestore().request_cache = None
|
||||
modulestore().metadata_inheritance_cache_subsystem = None
|
||||
|
||||
chapter = ItemFactory.create(
|
||||
parent_location=self.course.location,
|
||||
category="sequential",
|
||||
)
|
||||
self.section = ItemFactory.create(
|
||||
parent_location=chapter.location,
|
||||
category="sequential"
|
||||
)
|
||||
|
||||
# username = robot{0}, password = 'test'
|
||||
self.users = [
|
||||
UserFactory.create()
|
||||
for dummy0 in range(self.USER_COUNT)
|
||||
]
|
||||
|
||||
for user in self.users:
|
||||
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
|
||||
|
||||
# login all users for acces to Xmodule
|
||||
self.clients = {user.username: Client() for user in self.users}
|
||||
self.login_statuses = [
|
||||
self.clients[user.username].login(
|
||||
username=user.username, password='test')
|
||||
for user in self.users
|
||||
]
|
||||
|
||||
self.assertTrue(all(self.login_statuses))
|
||||
|
||||
def setUp(self):
|
||||
super(BaseTestXmodule, self).setUp()
|
||||
self.setup_course()
|
||||
self.initialize_module(metadata=self.METADATA, data=self.DATA)
|
||||
|
||||
def get_url(self, dispatch):
|
||||
"""Return item url with dispatch."""
|
||||
return reverse(
|
||||
'xblock_handler',
|
||||
args=(unicode(self.course.id), quote_slashes(self.item_url), 'xmodule_handler', dispatch)
|
||||
)
|
||||
|
||||
|
||||
class XModuleRenderingTestBase(BaseTestXmodule):
|
||||
|
||||
def new_module_runtime(self):
|
||||
"""
|
||||
Create a runtime that actually does html rendering
|
||||
"""
|
||||
runtime = super(XModuleRenderingTestBase, self).new_module_runtime()
|
||||
runtime.render_template = render_to_string
|
||||
return runtime
|
||||
|
||||
@@ -7,12 +7,140 @@ from django.contrib import messages
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.test.client import Client, RequestFactory
|
||||
|
||||
from courseware.access import has_access
|
||||
from courseware.masquerade import handle_ajax, setup_masquerade
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from lms.djangoapps.lms_xblock.field_data import LmsFieldData
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.lib.url_utils import quote_slashes
|
||||
from student.models import Registration
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from xblock.field_data import DictFieldData
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_MODULESTORE, ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from xmodule.tests import get_test_descriptor_system, get_test_system
|
||||
|
||||
|
||||
class BaseTestXmodule(ModuleStoreTestCase):
|
||||
"""Base class for testing Xmodules with mongo store.
|
||||
|
||||
This class prepares course and users for tests:
|
||||
1. create test course;
|
||||
2. create, enroll and login users for this course;
|
||||
|
||||
Any xmodule should overwrite only next parameters for test:
|
||||
1. CATEGORY
|
||||
2. DATA or METADATA
|
||||
3. MODEL_DATA
|
||||
4. COURSE_DATA and USER_COUNT if needed
|
||||
|
||||
This class should not contain any tests, because CATEGORY
|
||||
should be defined in child class.
|
||||
"""
|
||||
MODULESTORE = TEST_DATA_MONGO_MODULESTORE
|
||||
|
||||
USER_COUNT = 2
|
||||
COURSE_DATA = {}
|
||||
|
||||
# Data from YAML common/lib/xmodule/xmodule/templates/NAME/default.yaml
|
||||
CATEGORY = "vertical"
|
||||
DATA = ''
|
||||
# METADATA must be overwritten for every instance that uses it. Otherwise,
|
||||
# if we'll change it in the tests, it will be changed for all other instances
|
||||
# of parent class.
|
||||
METADATA = {}
|
||||
MODEL_DATA = {'data': '<some_module></some_module>'}
|
||||
|
||||
def new_module_runtime(self):
|
||||
"""
|
||||
Generate a new ModuleSystem that is minimally set up for testing
|
||||
"""
|
||||
return get_test_system(course_id=self.course.id)
|
||||
|
||||
def new_descriptor_runtime(self):
|
||||
runtime = get_test_descriptor_system()
|
||||
runtime.get_block = modulestore().get_item
|
||||
return runtime
|
||||
|
||||
def initialize_module(self, **kwargs):
|
||||
kwargs.update({
|
||||
'parent_location': self.section.location,
|
||||
'category': self.CATEGORY
|
||||
})
|
||||
|
||||
self.item_descriptor = ItemFactory.create(**kwargs)
|
||||
|
||||
self.runtime = self.new_descriptor_runtime()
|
||||
|
||||
field_data = {}
|
||||
field_data.update(self.MODEL_DATA)
|
||||
student_data = DictFieldData(field_data)
|
||||
self.item_descriptor._field_data = LmsFieldData(self.item_descriptor._field_data, student_data)
|
||||
|
||||
self.item_descriptor.xmodule_runtime = self.new_module_runtime()
|
||||
|
||||
self.item_url = unicode(self.item_descriptor.location)
|
||||
|
||||
def setup_course(self):
|
||||
self.course = CourseFactory.create(data=self.COURSE_DATA)
|
||||
|
||||
# Turn off cache.
|
||||
modulestore().request_cache = None
|
||||
modulestore().metadata_inheritance_cache_subsystem = None
|
||||
|
||||
chapter = ItemFactory.create(
|
||||
parent_location=self.course.location,
|
||||
category="sequential",
|
||||
)
|
||||
self.section = ItemFactory.create(
|
||||
parent_location=chapter.location,
|
||||
category="sequential"
|
||||
)
|
||||
|
||||
# username = robot{0}, password = 'test'
|
||||
self.users = [
|
||||
UserFactory.create()
|
||||
for dummy0 in range(self.USER_COUNT)
|
||||
]
|
||||
|
||||
for user in self.users:
|
||||
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
|
||||
|
||||
# login all users for acces to Xmodule
|
||||
self.clients = {user.username: Client() for user in self.users}
|
||||
self.login_statuses = [
|
||||
self.clients[user.username].login(
|
||||
username=user.username, password='test')
|
||||
for user in self.users
|
||||
]
|
||||
|
||||
self.assertTrue(all(self.login_statuses))
|
||||
|
||||
def setUp(self):
|
||||
super(BaseTestXmodule, self).setUp()
|
||||
self.setup_course()
|
||||
self.initialize_module(metadata=self.METADATA, data=self.DATA)
|
||||
|
||||
def get_url(self, dispatch):
|
||||
"""Return item url with dispatch."""
|
||||
return reverse(
|
||||
'xblock_handler',
|
||||
args=(unicode(self.course.id), quote_slashes(self.item_url), 'xmodule_handler', dispatch)
|
||||
)
|
||||
|
||||
|
||||
class XModuleRenderingTestBase(BaseTestXmodule):
|
||||
|
||||
def new_module_runtime(self):
|
||||
"""
|
||||
Create a runtime that actually does html rendering
|
||||
"""
|
||||
runtime = super(XModuleRenderingTestBase, self).new_module_runtime()
|
||||
runtime.render_template = render_to_string
|
||||
return runtime
|
||||
|
||||
|
||||
class LoginEnrollmentTestCase(TestCase):
|
||||
|
||||
@@ -17,7 +17,7 @@ from xblock.fragment import Fragment
|
||||
|
||||
from course_api.blocks.tests.helpers import deserialize_usage_key
|
||||
from courseware.module_render import get_module_for_descriptor_internal
|
||||
from lms.djangoapps.courseware.tests import XModuleRenderingTestBase
|
||||
from lms.djangoapps.courseware.tests.helpers import XModuleRenderingTestBase
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from xblock_discussion import DiscussionXBlock, loader
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
from courseware.tests import BaseTestXmodule
|
||||
from courseware.tests.helpers import BaseTestXmodule
|
||||
from courseware.views.views import get_course_lti_endpoints
|
||||
from openedx.core.lib.url_utils import quote_slashes
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
|
||||
@@ -22,7 +22,7 @@ from xmodule.modulestore.django import modulestore
|
||||
from xmodule.video_module.transcripts_utils import TranscriptException, TranscriptsGenerationException
|
||||
from xmodule.x_module import STUDENT_VIEW
|
||||
|
||||
from . import BaseTestXmodule
|
||||
from .helpers import BaseTestXmodule
|
||||
from .test_video_xml import SOURCE_XML
|
||||
|
||||
TRANSCRIPT = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]}
|
||||
|
||||
@@ -25,7 +25,7 @@ from xmodule.video_module import VideoDescriptor, bumper_utils, rewrite_video_ur
|
||||
from xmodule.video_module.transcripts_utils import Transcript, save_to_store
|
||||
from xmodule.x_module import STUDENT_VIEW
|
||||
|
||||
from . import BaseTestXmodule
|
||||
from .helpers import BaseTestXmodule
|
||||
from .test_video_handlers import TestVideo
|
||||
from .test_video_xml import SOURCE_XML
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from nose.plugins.attrib import attr
|
||||
|
||||
from xmodule.x_module import STUDENT_VIEW
|
||||
|
||||
from . import BaseTestXmodule
|
||||
from .helpers import BaseTestXmodule
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
"""
|
||||
View for Courseware Index
|
||||
"""
|
||||
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
|
||||
import logging
|
||||
import urllib
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
from datetime import datetime
|
||||
|
||||
import waffle
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.context_processors import csrf
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import Http404
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.timezone import UTC
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.views.generic import View
|
||||
|
||||
@@ -87,6 +87,7 @@ from openedx.core.djangoapps.programs.utils import ProgramMarketingDataExtender
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, course_home_url_name
|
||||
from openedx.features.course_experience.course_tools import CourseToolsPluginManager
|
||||
from openedx.features.course_experience.views.course_dates import CourseDatesFragmentView
|
||||
from openedx.features.enterprise_support.api import data_sharing_consent_required
|
||||
from shoppingcart.utils import is_shopping_cart_enabled
|
||||
@@ -327,6 +328,9 @@ def course_info(request, course_id):
|
||||
# Decide whether or not to show the reviews link in the course tools bar
|
||||
show_reviews_link = CourseReviewsModuleFragmentView.is_configured()
|
||||
|
||||
# Get the course tools enabled for this user and course
|
||||
course_tools = CourseToolsPluginManager.get_enabled_course_tools(request, course_key)
|
||||
|
||||
context = {
|
||||
'request': request,
|
||||
'masquerade_user': user,
|
||||
@@ -342,6 +346,8 @@ def course_info(request, course_id):
|
||||
'dates_fragment': dates_fragment,
|
||||
'url_to_enroll': url_to_enroll,
|
||||
'show_reviews_link': show_reviews_link,
|
||||
'course_tools': course_tools,
|
||||
|
||||
# TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts
|
||||
'upgrade_link': check_and_get_upgrade_link(request, user, course.id),
|
||||
'upgrade_price': get_cosmetic_verified_display_price(course),
|
||||
|
||||
0
lms/djangoapps/discussion/tests/__init__.py
Normal file
0
lms/djangoapps/discussion/tests/__init__.py
Normal file
@@ -370,18 +370,18 @@ class SingleThreadQueryCountTestCase(ForumsEnableMixin, ModuleStoreTestCase):
|
||||
# course is outside the context manager that is verifying the number of queries,
|
||||
# and with split mongo, that method ends up querying disabled_xblocks (which is then
|
||||
# cached and hence not queried as part of call_single_thread).
|
||||
(ModuleStoreEnum.Type.mongo, False, 1, 5, 3, 13, 1),
|
||||
(ModuleStoreEnum.Type.mongo, False, 50, 5, 3, 13, 1),
|
||||
(ModuleStoreEnum.Type.mongo, False, 1, 5, 3, 14, 1),
|
||||
(ModuleStoreEnum.Type.mongo, False, 50, 5, 3, 14, 1),
|
||||
# split mongo: 3 queries, regardless of thread response size.
|
||||
(ModuleStoreEnum.Type.split, False, 1, 3, 3, 12, 1),
|
||||
(ModuleStoreEnum.Type.split, False, 50, 3, 3, 12, 1),
|
||||
(ModuleStoreEnum.Type.split, False, 1, 3, 3, 13, 1),
|
||||
(ModuleStoreEnum.Type.split, False, 50, 3, 3, 13, 1),
|
||||
|
||||
# Enabling Enterprise integration should have no effect on the number of mongo queries made.
|
||||
(ModuleStoreEnum.Type.mongo, True, 1, 5, 3, 13, 1),
|
||||
(ModuleStoreEnum.Type.mongo, True, 50, 5, 3, 13, 1),
|
||||
(ModuleStoreEnum.Type.mongo, True, 1, 5, 3, 14, 1),
|
||||
(ModuleStoreEnum.Type.mongo, True, 50, 5, 3, 14, 1),
|
||||
# split mongo: 3 queries, regardless of thread response size.
|
||||
(ModuleStoreEnum.Type.split, True, 1, 3, 3, 12, 1),
|
||||
(ModuleStoreEnum.Type.split, True, 50, 3, 3, 12, 1),
|
||||
(ModuleStoreEnum.Type.split, True, 1, 3, 3, 13, 1),
|
||||
(ModuleStoreEnum.Type.split, True, 50, 3, 3, 13, 1),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_number_of_mongo_queries(
|
||||
|
||||
@@ -383,8 +383,8 @@ class ViewsQueryCountTestCase(
|
||||
return inner
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.mongo, 3, 4, 31),
|
||||
(ModuleStoreEnum.Type.split, 3, 12, 31),
|
||||
(ModuleStoreEnum.Type.mongo, 3, 4, 32),
|
||||
(ModuleStoreEnum.Type.split, 3, 12, 32),
|
||||
)
|
||||
@ddt.unpack
|
||||
@count_queries
|
||||
@@ -392,8 +392,8 @@ class ViewsQueryCountTestCase(
|
||||
self.create_thread_helper(mock_request)
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.mongo, 3, 3, 27),
|
||||
(ModuleStoreEnum.Type.split, 3, 9, 27),
|
||||
(ModuleStoreEnum.Type.mongo, 3, 3, 28),
|
||||
(ModuleStoreEnum.Type.split, 3, 9, 28),
|
||||
)
|
||||
@ddt.unpack
|
||||
@count_queries
|
||||
|
||||
@@ -681,7 +681,7 @@ def students_update_enrollment(request, course_id):
|
||||
)
|
||||
before_enrollment = before.to_dict()['enrollment']
|
||||
before_allowed = before.to_dict()['allowed']
|
||||
enrollment_obj = CourseEnrollment.get_enrollment(user, course_id)
|
||||
enrollment_obj = CourseEnrollment.get_enrollment(user, course_id) if user else None
|
||||
|
||||
if before_enrollment:
|
||||
state_transition = ENROLLED_TO_UNENROLLED
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
background: $header-bg;
|
||||
|
||||
.logo-header {
|
||||
display: inline;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.wrapper-header {
|
||||
@@ -42,7 +42,7 @@
|
||||
@include margin(12px, 10px, 0px, 10px);
|
||||
color: $lms-label-color;
|
||||
.provider {
|
||||
font-weight: bold;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,17 +63,25 @@
|
||||
|
||||
.list-inline {
|
||||
&.nav-global {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@include margin(0, 0, 0, $baseline/2);
|
||||
|
||||
&.nav-courseware {
|
||||
margin-top: 5px;
|
||||
.btn {
|
||||
text-transform: uppercase;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: $lms-active-color;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
color: $link-hover;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
font-weight: font-weight(semi-bold);
|
||||
text-transform: uppercase;
|
||||
|
||||
&.active {
|
||||
a {
|
||||
@@ -143,6 +151,6 @@
|
||||
color: $base-font-color;
|
||||
|
||||
&:visited {
|
||||
color: $base-font-color;
|
||||
color: $base-font-color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ from django.utils.translation import ugettext as _
|
||||
from courseware.courses import get_course_info_section, get_course_date_blocks
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from openedx.features.course_experience import SHOW_REVIEWS_TOOL_FLAG
|
||||
%>
|
||||
|
||||
<%block name="pagetitle">${_("{course_number} Course Info").format(course_number=course.display_number_with_default)}</%block>
|
||||
@@ -85,25 +84,20 @@ from openedx.features.course_experience import SHOW_REVIEWS_TOOL_FLAG
|
||||
|
||||
</section>
|
||||
<section aria-label="${_('Handout Navigation')}" class="handouts">
|
||||
<h3 class="hd hd-3 handouts-header">${_("Course Tools")}</h3>
|
||||
<div>
|
||||
<a class="action-show-bookmarks" href="${reverse('openedx.course_bookmarks.home', args=[course.id])}">
|
||||
<span class="icon fa fa-bookmark" aria-hidden="true"></span>
|
||||
${_("Bookmarks")}
|
||||
</a>
|
||||
% if SHOW_REVIEWS_TOOL_FLAG.is_enabled(course.id) and show_reviews_link:
|
||||
<a href="${reverse('openedx.course_experience.course_reviews', args=[course.id])}">
|
||||
<span class="icon fa fa-star" aria-hidden="true"></span>
|
||||
${_("Reviews")}
|
||||
</a>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
% if course_tools:
|
||||
<h3 class="hd hd-3 handouts-header">${_("Course Tools")}</h3>
|
||||
% for course_tool in course_tools:
|
||||
<a href="${course_tool.url(course.id)}">
|
||||
<span class="icon ${course_tool.icon_classes()}" aria-hidden="true"></span>
|
||||
${course_tool.title()}
|
||||
</a>
|
||||
% endfor
|
||||
% endif
|
||||
% if SelfPacedConfiguration.current().enable_course_home_improvements:
|
||||
${HTML(dates_fragment.body_html())}
|
||||
% endif
|
||||
<h3 class="hd hd-3 handouts-header">${_(course.info_sidebar_name)}</h3>
|
||||
${HTML(get_course_info_section(request, masquerade_user, course, 'handouts'))}
|
||||
<h3 class="hd hd-3 handouts-header">${_(course.info_sidebar_name)}</h3>
|
||||
${HTML(get_course_info_section(request, masquerade_user, course, 'handouts'))}
|
||||
</section>
|
||||
% else:
|
||||
<section class="updates">
|
||||
|
||||
@@ -165,7 +165,9 @@ from pipeline_mako import render_require_js_path_overrides
|
||||
</html>
|
||||
|
||||
<%def name="login_query()">${
|
||||
u"?next={0}".format(urlquote_plus(login_redirect_url)) if login_redirect_url else ""
|
||||
u"?next={next}".format(
|
||||
next=urlquote_plus(login_redirect_url if login_redirect_url else request.path)
|
||||
) if (login_redirect_url or request) else ""
|
||||
}</%def>
|
||||
|
||||
<!-- Performance beacon for onload times -->
|
||||
|
||||
@@ -13,7 +13,7 @@ from django.utils.translation import ugettext as _
|
||||
<%block name="navigation_global_links_authenticated">
|
||||
% if settings.FEATURES.get('COURSES_ARE_BROWSABLE') and not show_program_listing:
|
||||
<li class="item nav-global-01">
|
||||
<a href="${marketing_link('COURSES')}">${_('Explore courses')}</a>
|
||||
<a class="btn" href="${marketing_link('COURSES')}">${_('Explore courses')}</a>
|
||||
</li>
|
||||
% endif
|
||||
% if show_program_listing:
|
||||
@@ -28,12 +28,12 @@ from django.utils.translation import ugettext as _
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
%if settings.FEATURES.get('ENABLE_SYSADMIN_DASHBOARD','') and user.is_staff:
|
||||
% if settings.FEATURES.get('ENABLE_SYSADMIN_DASHBOARD','') and user.is_staff:
|
||||
<li class="item">
|
||||
## Translators: This is short for "System administration".
|
||||
<a href="${reverse('sysadmin')}">${_("Sysadmin")}</a>
|
||||
<a class="btn" href="${reverse('sysadmin')}">${_("Sysadmin")}</a>
|
||||
</li>
|
||||
%endif
|
||||
% endif
|
||||
</%block>
|
||||
</ol>
|
||||
|
||||
|
||||
@@ -13,15 +13,15 @@ from django.utils.translation import ugettext as _
|
||||
<%block name="navigation_global_links">
|
||||
% if static.get_value('ENABLE_MKTG_SITE', settings.FEATURES.get('ENABLE_MKTG_SITE', False)):
|
||||
<li class="item nav-global-01">
|
||||
<a href="${marketing_link('HOW_IT_WORKS')}">${_("How it Works")}</a>
|
||||
<a class="btn" href="${marketing_link('HOW_IT_WORKS')}">${_("How it Works")}</a>
|
||||
</li>
|
||||
% if settings.FEATURES.get('COURSES_ARE_BROWSABLE'):
|
||||
<li class="item nav-global-02">
|
||||
<a href="${marketing_link('COURSES')}">${_("Courses")}</a>
|
||||
<a class="btn" href="${marketing_link('COURSES')}">${_("Courses")}</a>
|
||||
</li>
|
||||
% endif
|
||||
<li class="item nav-global-03">
|
||||
<a href="${marketing_link('SCHOOLS')}">${_("Schools")}</a>
|
||||
<a class="btn" href="${marketing_link('SCHOOLS')}">${_("Schools")}</a>
|
||||
</li>
|
||||
% endif
|
||||
</%block>
|
||||
@@ -35,11 +35,11 @@ from django.utils.translation import ugettext as _
|
||||
%endif
|
||||
% if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
|
||||
<li class="item nav-global-04">
|
||||
<a class="btn-neutral btn-register" href="${reverse('course-specific-register', args=[course.id.to_deprecated_string()])}">${_("Register")}</a>
|
||||
<a class="btn btn-neutral btn-register" href="${reverse('course-specific-register', args=[course.id.to_deprecated_string()])}">${_("Register")}</a>
|
||||
</li>
|
||||
% elif static.get_value('ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION')):
|
||||
<li class="item nav-global-04">
|
||||
<a class="btn-neutral btn-register" href="/register${login_query()}">${_("Register")}</a>
|
||||
<a class="btn btn-neutral btn-register" href="/register${login_query()}">${_("Register")}</a>
|
||||
</li>
|
||||
% endif
|
||||
% endif
|
||||
@@ -51,11 +51,11 @@ from django.utils.translation import ugettext as _
|
||||
<li class="item nav-courseware-01">
|
||||
% if not settings.FEATURES['DISABLE_LOGIN_BUTTON'] and not combined_login_and_register:
|
||||
% if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
|
||||
<a class="btn btn-login" href="${reverse('course-specific-login', args=[course.id.to_deprecated_string()])}${login_query()}">${_("Sign in")}</a>
|
||||
<a class="btn btn-brand btn-login" href="${reverse('course-specific-login', args=[course.id.to_deprecated_string()])}${login_query()}">${_("Sign in")}</a>
|
||||
% else:
|
||||
<a class="btn btn-login" href="/login${login_query()}">${_("Sign in")}</a>
|
||||
<a class="btn brn-brand btn-login" href="/login${login_query()}">${_("Sign in")}</a>
|
||||
% endif
|
||||
% endif
|
||||
</li>
|
||||
</%block>
|
||||
</ol>
|
||||
</ol>
|
||||
|
||||
@@ -290,19 +290,21 @@ class CourseWaffleFlag(WaffleFlag):
|
||||
return None
|
||||
return course_override_callback
|
||||
|
||||
def is_enabled(self, course_id=None):
|
||||
def is_enabled(self, course_key=None):
|
||||
"""
|
||||
Returns whether or not the flag is enabled.
|
||||
|
||||
Arguments:
|
||||
course_id (CourseKey): The course to check for override before
|
||||
course_key (CourseKey): The course to check for override before
|
||||
checking waffle.
|
||||
"""
|
||||
# validate arguments
|
||||
assert issubclass(type(course_id), CourseKey), "The course_id '{}' must be a CourseKey.".format(str(course_id))
|
||||
assert issubclass(type(course_key), CourseKey), "The course_id '{}' must be a CourseKey.".format(
|
||||
str(course_key)
|
||||
)
|
||||
|
||||
return self.waffle_namespace.is_flag_active(
|
||||
self.flag_name,
|
||||
check_before_waffle_callback=self._get_course_override_callback(course_id),
|
||||
check_before_waffle_callback=self._get_course_override_callback(course_key),
|
||||
flag_undefined_default=self.flag_undefined_default
|
||||
)
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
Platform plugins to support course bookmarks.
|
||||
"""
|
||||
|
||||
from courseware.access import has_access
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from openedx.features.course_experience.course_tools import CourseTool
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
|
||||
class CourseBookmarksTool(CourseTool):
|
||||
@@ -14,9 +16,11 @@ class CourseBookmarksTool(CourseTool):
|
||||
@classmethod
|
||||
def is_enabled(cls, request, course_key):
|
||||
"""
|
||||
Always show the bookmarks tool.
|
||||
The bookmarks tool is only enabled for enrolled users or staff.
|
||||
"""
|
||||
return True
|
||||
if has_access(request.user, 'staff', course_key):
|
||||
return True
|
||||
return CourseEnrollment.is_enrolled(request.user, course_key)
|
||||
|
||||
@classmethod
|
||||
def title(cls):
|
||||
|
||||
0
openedx/features/course_bookmarks/tests/__init__.py
Normal file
0
openedx/features/course_bookmarks/tests/__init__.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
Unit tests for the course bookmarks feature.
|
||||
"""
|
||||
|
||||
import ddt
|
||||
|
||||
from django.test import RequestFactory
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import CourseUserType, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
from ..plugins import CourseBookmarksTool
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestCourseBookmarksTool(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Test the course bookmarks tool.
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Set up a course to be used for testing.
|
||||
"""
|
||||
# setUpClassAndTestData() already calls setUpClass on SharedModuleStoreTestCase
|
||||
# pylint: disable=super-method-not-called
|
||||
with super(TestCourseBookmarksTool, cls).setUpClassAndTestData():
|
||||
with cls.store.default_store(ModuleStoreEnum.Type.split):
|
||||
cls.course = CourseFactory.create()
|
||||
with cls.store.bulk_operations(cls.course.id):
|
||||
# Create a basic course structure
|
||||
chapter = ItemFactory.create(category='chapter', parent_location=cls.course.location)
|
||||
section = ItemFactory.create(category='sequential', parent_location=chapter.location)
|
||||
ItemFactory.create(category='vertical', parent_location=section.location)
|
||||
|
||||
@ddt.data(
|
||||
[CourseUserType.ANONYMOUS, False],
|
||||
[CourseUserType.ENROLLED, True],
|
||||
[CourseUserType.UNENROLLED, False],
|
||||
[CourseUserType.UNENROLLED_STAFF, True],
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_bookmarks_tool_is_enabled(self, user_type, should_be_enabled):
|
||||
request = RequestFactory().request()
|
||||
request.user = self.create_user_for_course(self.course, user_type)
|
||||
self.assertEqual(CourseBookmarksTool.is_enabled(request, self.course.id), should_be_enabled)
|
||||
@@ -65,3 +65,11 @@ class CourseToolsPluginManager(PluginManager):
|
||||
course_tools = cls.get_available_plugins().values()
|
||||
course_tools.sort(key=lambda course_tool: course_tool.title())
|
||||
return course_tools
|
||||
|
||||
@classmethod
|
||||
def get_enabled_course_tools(cls, request, course_key):
|
||||
"""
|
||||
Returns the course tools applicable to the current user and course.
|
||||
"""
|
||||
course_tools = CourseToolsPluginManager.get_course_tools()
|
||||
return filter(lambda tool: tool.is_enabled(request, course_key), course_tools)
|
||||
|
||||
@@ -8,6 +8,7 @@ from django.utils.translation import ugettext as _
|
||||
|
||||
from course_tools import CourseTool
|
||||
from courseware.courses import get_course_by_id
|
||||
from student.models import CourseEnrollment
|
||||
from views.course_reviews import CourseReviewsModuleFragmentView
|
||||
from views.course_updates import CourseUpdatesFragmentView
|
||||
|
||||
@@ -35,11 +36,14 @@ class CourseUpdatesTool(CourseTool):
|
||||
@classmethod
|
||||
def is_enabled(cls, request, course_key):
|
||||
"""
|
||||
Returns True if this tool is enabled for the specified course key.
|
||||
Returns True if the user should be shown course updates for this course.
|
||||
"""
|
||||
if not UNIFIED_COURSE_TAB_FLAG.is_enabled(course_key):
|
||||
return False
|
||||
if not CourseEnrollment.is_enrolled(request.user, course_key):
|
||||
return False
|
||||
course = get_course_by_id(course_key)
|
||||
has_updates = CourseUpdatesFragmentView.has_updates(request, course)
|
||||
return UNIFIED_COURSE_TAB_FLAG.is_enabled(course_key) and has_updates
|
||||
return CourseUpdatesFragmentView.has_updates(request, course)
|
||||
|
||||
@classmethod
|
||||
def url(cls, course_key):
|
||||
@@ -72,8 +76,9 @@ class CourseReviewsTool(CourseTool):
|
||||
"""
|
||||
Returns True if this tool is enabled for the specified course key.
|
||||
"""
|
||||
reviews_configured = CourseReviewsModuleFragmentView.is_configured()
|
||||
return SHOW_REVIEWS_TOOL_FLAG.is_enabled(course_key) and reviews_configured
|
||||
if not SHOW_REVIEWS_TOOL_FLAG.is_enabled(course_key):
|
||||
return False
|
||||
return CourseReviewsModuleFragmentView.is_configured()
|
||||
|
||||
@classmethod
|
||||
def url(cls, course_key):
|
||||
|
||||
@@ -24,13 +24,17 @@ from django.utils.translation import ugettext as _
|
||||
<p class="hd hd-6 date localized-datetime" data-format="shortDate" data-datetime="${course_date.date}" data-timezone="${user_timezone}" data-language="${user_language}" data-string="${_(course_date.relative_datestring)}"></p>
|
||||
% endif
|
||||
% if course_date.description:
|
||||
<p class="description">${course_date.description}</p>
|
||||
<p class="description">${course_date.description}</p>
|
||||
% endif
|
||||
% if course_date.link and course_date.link_text:
|
||||
<span class="date-summary-link">
|
||||
<a href="${course_date.link}">${course_date.link_text}</a>
|
||||
</span>
|
||||
<span class="date-summary-link">
|
||||
<a href="${course_date.link}">${course_date.link_text}</a>
|
||||
</span>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
% endfor
|
||||
|
||||
<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory">
|
||||
DateUtilFactory.transform('.localized-datetime');
|
||||
</%static:require_module_async>
|
||||
|
||||
@@ -15,7 +15,6 @@ from django_comment_client.permissions import has_permission
|
||||
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, SHOW_REVIEWS_TOOL_FLAG
|
||||
from openedx.features.course_experience.course_tools import CourseToolsPluginManager
|
||||
%>
|
||||
|
||||
<%block name="content">
|
||||
@@ -64,32 +63,31 @@ from openedx.features.course_experience.course_tools import CourseToolsPluginMan
|
||||
</div>
|
||||
% endif
|
||||
|
||||
${HTML(outline_fragment.body_html())}
|
||||
% if outline_fragment:
|
||||
${HTML(outline_fragment.body_html())}
|
||||
% endif
|
||||
</main>
|
||||
<aside class="course-sidebar layout-col layout-col-a">
|
||||
<%
|
||||
course_tools = CourseToolsPluginManager.get_course_tools()
|
||||
%>
|
||||
% if course_tools:
|
||||
<div class="section section-tools">
|
||||
<h3 class="hd-6">${_("Course Tools")}</h3>
|
||||
<ul class="list-unstyled">
|
||||
% for course_tool in course_tools:
|
||||
% if course_tool.is_enabled(request, course_key):
|
||||
<li>
|
||||
<a href="${course_tool.url(course_key)}">
|
||||
<span class="icon ${course_tool.icon_classes()}" aria-hidden="true"></span>
|
||||
${course_tool.title()}
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
<li>
|
||||
<a href="${course_tool.url(course_key)}">
|
||||
<span class="icon ${course_tool.icon_classes()}" aria-hidden="true"></span>
|
||||
${course_tool.title()}
|
||||
</a>
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</div>
|
||||
% endif
|
||||
<div class="section section-dates">
|
||||
${HTML(dates_fragment.body_html())}
|
||||
</div>
|
||||
% if dates_fragment:
|
||||
<div class="section section-dates">
|
||||
${HTML(dates_fragment.body_html())}
|
||||
</div>
|
||||
% endif
|
||||
% if handouts_html:
|
||||
<div class="section section-handouts">
|
||||
<h3 class="hd-6">${_("Course Handouts")}</h3>
|
||||
@@ -99,6 +97,8 @@ from openedx.features.course_experience.course_tools import CourseToolsPluginMan
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
${HTML(course_sock_fragment.body_html())}
|
||||
% if course_sock_fragment:
|
||||
${HTML(course_sock_fragment.body_html())}
|
||||
% endif
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
@@ -151,7 +151,6 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
)}
|
||||
% endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
% endif
|
||||
</main>
|
||||
|
||||
28
openedx/features/course_experience/tests/views/helpers.py
Normal file
28
openedx/features/course_experience/tests/views/helpers.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Test helpers for the course experience.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
|
||||
TEST_COURSE_PRICE = 50
|
||||
|
||||
|
||||
def add_course_mode(course, upgrade_deadline_expired=False):
|
||||
"""
|
||||
Adds a course mode to the test course.
|
||||
"""
|
||||
upgrade_exp_date = datetime.datetime.now()
|
||||
if upgrade_deadline_expired:
|
||||
upgrade_exp_date = upgrade_exp_date - datetime.timedelta(days=21)
|
||||
else:
|
||||
upgrade_exp_date = upgrade_exp_date + datetime.timedelta(days=21)
|
||||
|
||||
CourseMode(
|
||||
course_id=course.id,
|
||||
mode_slug=CourseMode.VERIFIED,
|
||||
mode_display_name="Verified Certificate",
|
||||
min_price=TEST_COURSE_PRICE,
|
||||
_expiration_datetime=upgrade_exp_date, # pylint: disable=protected-access
|
||||
).save()
|
||||
@@ -1,18 +1,24 @@
|
||||
"""
|
||||
Tests for the course home page.
|
||||
"""
|
||||
import ddt
|
||||
|
||||
from courseware.tests.factories import StaffFactory
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.http import urlquote_plus
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag
|
||||
from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG
|
||||
from openedx.features.course_experience import SHOW_REVIEWS_TOOL_FLAG, UNIFIED_COURSE_TAB_FLAG
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.django_utils import CourseUserType, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
|
||||
|
||||
from .test_course_updates import create_course_update, remove_course_updates
|
||||
from .helpers import add_course_mode
|
||||
from .test_course_updates import create_course_update
|
||||
|
||||
TEST_PASSWORD = 'test'
|
||||
TEST_CHAPTER_NAME = 'Test Chapter'
|
||||
TEST_WELCOME_MESSAGE = '<h2>Welcome!</h2>'
|
||||
TEST_UPDATE_MESSAGE = '<h2>Test Update!</h2>'
|
||||
TEST_COURSE_UPDATES_TOOL = '/course/updates">'
|
||||
@@ -32,20 +38,26 @@ def course_home_url(course):
|
||||
)
|
||||
|
||||
|
||||
class TestCourseHomePage(SharedModuleStoreTestCase):
|
||||
class CourseHomePageTestCase(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Test the course home page.
|
||||
Base class for testing the course home page.
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Set up the simplest course possible."""
|
||||
"""
|
||||
Set up a course to be used for testing.
|
||||
"""
|
||||
# setUpClassAndTestData() already calls setUpClass on SharedModuleStoreTestCase
|
||||
# pylint: disable=super-method-not-called
|
||||
with super(TestCourseHomePage, cls).setUpClassAndTestData():
|
||||
with super(CourseHomePageTestCase, cls).setUpClassAndTestData():
|
||||
with cls.store.default_store(ModuleStoreEnum.Type.split):
|
||||
cls.course = CourseFactory.create(org='edX', number='test', display_name='Test Course')
|
||||
with cls.store.bulk_operations(cls.course.id):
|
||||
chapter = ItemFactory.create(category='chapter', parent_location=cls.course.location)
|
||||
chapter = ItemFactory.create(
|
||||
category='chapter',
|
||||
parent_location=cls.course.location,
|
||||
display_name=TEST_CHAPTER_NAME,
|
||||
)
|
||||
section = ItemFactory.create(category='sequential', parent_location=chapter.location)
|
||||
section2 = ItemFactory.create(category='sequential', parent_location=chapter.location)
|
||||
ItemFactory.create(category='vertical', parent_location=section.location)
|
||||
@@ -54,9 +66,12 @@ class TestCourseHomePage(SharedModuleStoreTestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Set up and enroll our fake user in the course."""
|
||||
cls.staff_user = StaffFactory(course_key=cls.course.id, password=TEST_PASSWORD)
|
||||
cls.user = UserFactory(password=TEST_PASSWORD)
|
||||
CourseEnrollment.enroll(cls.user, cls.course.id)
|
||||
|
||||
|
||||
class TestCourseHomePage(CourseHomePageTestCase):
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up for the tests.
|
||||
@@ -109,3 +124,90 @@ class TestCourseHomePage(SharedModuleStoreTestCase):
|
||||
with check_mongo_calls(4):
|
||||
url = course_home_url(self.course)
|
||||
self.client.get(url)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestCourseHomePageAccess(CourseHomePageTestCase):
|
||||
"""
|
||||
Test access to the course home page.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestCourseHomePageAccess, self).setUp()
|
||||
|
||||
# Make this a verified course so that an upgrade message might be shown
|
||||
add_course_mode(self.course, upgrade_deadline_expired=False)
|
||||
|
||||
# Add a welcome message
|
||||
create_course_update(self.course, self.staff_user, TEST_WELCOME_MESSAGE)
|
||||
|
||||
@override_waffle_flag(UNIFIED_COURSE_TAB_FLAG, active=True)
|
||||
@override_waffle_flag(SHOW_REVIEWS_TOOL_FLAG, active=True)
|
||||
@ddt.data(
|
||||
CourseUserType.ANONYMOUS,
|
||||
CourseUserType.ENROLLED,
|
||||
CourseUserType.UNENROLLED,
|
||||
CourseUserType.UNENROLLED_STAFF,
|
||||
)
|
||||
def test_home_page(self, user_type):
|
||||
self.user = self.create_user_for_course(self.course, user_type)
|
||||
|
||||
# Render the course home page
|
||||
url = course_home_url(self.course)
|
||||
response = self.client.get(url)
|
||||
|
||||
# Verify that the course tools and dates are always shown
|
||||
self.assertContains(response, 'Course Tools')
|
||||
self.assertContains(response, 'Today is')
|
||||
|
||||
# Verify that the outline, start button, course sock, and welcome message
|
||||
# are only shown to enrolled users.
|
||||
is_enrolled = user_type is CourseUserType.ENROLLED
|
||||
is_unenrolled_staff = user_type is CourseUserType.UNENROLLED_STAFF
|
||||
expected_count = 1 if (is_enrolled or is_unenrolled_staff) else 0
|
||||
self.assertContains(response, TEST_CHAPTER_NAME, count=expected_count)
|
||||
self.assertContains(response, 'Start Course', count=expected_count)
|
||||
self.assertContains(response, 'Learn About Verified Certificate', count=expected_count)
|
||||
self.assertContains(response, TEST_WELCOME_MESSAGE, count=expected_count)
|
||||
|
||||
@override_waffle_flag(UNIFIED_COURSE_TAB_FLAG, active=False)
|
||||
@override_waffle_flag(SHOW_REVIEWS_TOOL_FLAG, active=True)
|
||||
@ddt.data(
|
||||
CourseUserType.ANONYMOUS,
|
||||
CourseUserType.ENROLLED,
|
||||
CourseUserType.UNENROLLED,
|
||||
CourseUserType.UNENROLLED_STAFF,
|
||||
)
|
||||
def test_home_page_not_unified(self, user_type):
|
||||
"""
|
||||
Verifies the course home tab when not unified.
|
||||
"""
|
||||
self.user = self.create_user_for_course(self.course, user_type)
|
||||
|
||||
# Render the course home page
|
||||
url = course_home_url(self.course)
|
||||
response = self.client.get(url)
|
||||
|
||||
# Verify that the course tools and dates are always shown
|
||||
self.assertContains(response, 'Course Tools')
|
||||
self.assertContains(response, 'Today is')
|
||||
|
||||
# Verify that welcome messages are never shown
|
||||
self.assertNotContains(response, TEST_WELCOME_MESSAGE)
|
||||
|
||||
# Verify that the outline, start button, course sock, and welcome message
|
||||
# are only shown to enrolled users.
|
||||
is_enrolled = user_type is CourseUserType.ENROLLED
|
||||
is_unenrolled_staff = user_type is CourseUserType.UNENROLLED_STAFF
|
||||
expected_count = 1 if (is_enrolled or is_unenrolled_staff) else 0
|
||||
self.assertContains(response, TEST_CHAPTER_NAME, count=expected_count)
|
||||
self.assertContains(response, 'Start Course', count=expected_count)
|
||||
self.assertContains(response, 'Learn About Verified Certificate', count=expected_count)
|
||||
|
||||
def test_sign_in_button(self):
|
||||
"""
|
||||
Verify that the sign in button will return to this page.
|
||||
"""
|
||||
url = course_home_url(self.course)
|
||||
response = self.client.get(url)
|
||||
self.assertContains(response, '/login?next={url}'.format(url=urlquote_plus(url)))
|
||||
|
||||
@@ -5,7 +5,6 @@ import datetime
|
||||
import ddt
|
||||
import json
|
||||
from markupsafe import escape
|
||||
from unittest import skip
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from pyquery import PyQuery as pq
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
Tests for course verification sock
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import ddt
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
@@ -12,11 +11,11 @@ from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from .helpers import add_course_mode
|
||||
from .test_course_home import course_home_url
|
||||
|
||||
TEST_PASSWORD = 'test'
|
||||
TEST_VERIFICATION_SOCK_LOCATOR = '<div class="verification-sock"'
|
||||
TEST_COURSE_PRICE = 50
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -34,10 +33,10 @@ class TestCourseSockView(SharedModuleStoreTestCase):
|
||||
cls.verified_course_update_expired = CourseFactory.create()
|
||||
cls.verified_course_already_enrolled = CourseFactory.create()
|
||||
|
||||
# Assign each verifiable course a upgrade deadline
|
||||
cls._add_course_mode(cls.verified_course, upgrade_deadline_expired=False)
|
||||
cls._add_course_mode(cls.verified_course_update_expired, upgrade_deadline_expired=True)
|
||||
cls._add_course_mode(cls.verified_course_already_enrolled, upgrade_deadline_expired=False)
|
||||
# Assign each verifiable course an upgrade deadline
|
||||
add_course_mode(cls.verified_course, upgrade_deadline_expired=False)
|
||||
add_course_mode(cls.verified_course_update_expired, upgrade_deadline_expired=True)
|
||||
add_course_mode(cls.verified_course_already_enrolled, upgrade_deadline_expired=False)
|
||||
|
||||
def setUp(self):
|
||||
super(TestCourseSockView, self).setUp()
|
||||
@@ -47,7 +46,9 @@ class TestCourseSockView(SharedModuleStoreTestCase):
|
||||
CourseEnrollmentFactory.create(user=self.user, course_id=self.standard_course.id)
|
||||
CourseEnrollmentFactory.create(user=self.user, course_id=self.verified_course.id)
|
||||
CourseEnrollmentFactory.create(user=self.user, course_id=self.verified_course_update_expired.id)
|
||||
CourseEnrollmentFactory.create(user=self.user, course_id=self.verified_course_already_enrolled.id, mode=CourseMode.VERIFIED)
|
||||
CourseEnrollmentFactory.create(
|
||||
user=self.user, course_id=self.verified_course_already_enrolled.id, mode=CourseMode.VERIFIED
|
||||
)
|
||||
|
||||
# Log the user in
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
@@ -101,22 +102,3 @@ class TestCourseSockView(SharedModuleStoreTestCase):
|
||||
response.content,
|
||||
msg='Student should not be able to see sock in a unverifiable course.',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _add_course_mode(cls, course, upgrade_deadline_expired=False):
|
||||
"""
|
||||
Adds a course mode to the test course.
|
||||
"""
|
||||
upgrade_exp_date = datetime.datetime.now()
|
||||
if upgrade_deadline_expired:
|
||||
upgrade_exp_date = upgrade_exp_date - datetime.timedelta(days=21)
|
||||
else:
|
||||
upgrade_exp_date = upgrade_exp_date + datetime.timedelta(days=21)
|
||||
|
||||
CourseMode(
|
||||
course_id=course.id,
|
||||
mode_slug=CourseMode.VERIFIED,
|
||||
mode_display_name="Verified Certificate",
|
||||
min_price=TEST_COURSE_PRICE,
|
||||
_expiration_datetime=upgrade_exp_date, # pylint: disable=protected-access
|
||||
).save()
|
||||
|
||||
@@ -2,19 +2,21 @@
|
||||
Views for the course home page.
|
||||
"""
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.context_processors import csrf
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from web_fragments.fragment import Fragment
|
||||
|
||||
from courseware.access import has_access
|
||||
from courseware.courses import get_course_info_section, get_course_with_access
|
||||
from lms.djangoapps.courseware.views.views import CourseTabView
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
from openedx.features.course_experience.course_tools import CourseToolsPluginManager
|
||||
from student.models import CourseEnrollment
|
||||
from util.views import ensure_valid_course_key
|
||||
from web_fragments.fragment import Fragment
|
||||
|
||||
from ..utils import get_course_outline_block_tree
|
||||
from .course_dates import CourseDatesFragmentView
|
||||
@@ -22,12 +24,13 @@ from .course_outline import CourseOutlineFragmentView
|
||||
from .course_sock import CourseSockFragmentView
|
||||
from .welcome_message import WelcomeMessageFragmentView
|
||||
|
||||
EMPTY_HANDOUTS_HTML = u'<ol></ol>'
|
||||
|
||||
|
||||
class CourseHomeView(CourseTabView):
|
||||
"""
|
||||
The home page for a course.
|
||||
"""
|
||||
@method_decorator(login_required)
|
||||
@method_decorator(ensure_csrf_cookie)
|
||||
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True))
|
||||
@method_decorator(ensure_valid_course_key)
|
||||
@@ -83,34 +86,48 @@ class CourseHomeFragmentView(EdxFragmentView):
|
||||
|
||||
return (has_visited_course, resume_course_url)
|
||||
|
||||
def _get_course_handouts(self, request, course):
|
||||
"""
|
||||
Returns the handouts for the specified course.
|
||||
"""
|
||||
handouts = get_course_info_section(request, request.user, course, 'handouts')
|
||||
if not handouts or handouts == EMPTY_HANDOUTS_HTML:
|
||||
return None
|
||||
return handouts
|
||||
|
||||
def render_to_fragment(self, request, course_id=None, **kwargs):
|
||||
"""
|
||||
Renders the course's home page as a fragment.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
|
||||
# Render the outline as a fragment
|
||||
outline_fragment = CourseOutlineFragmentView().render_to_fragment(request, course_id=course_id, **kwargs)
|
||||
|
||||
# Get resume course information
|
||||
has_visited_course, resume_course_url = self._get_resume_course_info(request, course_id)
|
||||
|
||||
# Render the welcome message as a fragment
|
||||
welcome_message_fragment = WelcomeMessageFragmentView().render_to_fragment(
|
||||
request, course_id=course_id, **kwargs
|
||||
)
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
|
||||
# Render the course dates as a fragment
|
||||
dates_fragment = CourseDatesFragmentView().render_to_fragment(request, course_id=course_id, **kwargs)
|
||||
|
||||
# TODO: Use get_course_overview_with_access and blocks api
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
|
||||
# Render the verification sock as a fragment
|
||||
course_sock_fragment = CourseSockFragmentView().render_to_fragment(request, course=course, **kwargs)
|
||||
# Render the full content to enrolled users, as well as to course and global staff.
|
||||
# Unenrolled users who are not course or global staff are given only a subset.
|
||||
is_enrolled = CourseEnrollment.is_enrolled(request.user, course_key)
|
||||
is_staff = has_access(request.user, 'staff', course_key)
|
||||
if is_enrolled or is_staff:
|
||||
outline_fragment = CourseOutlineFragmentView().render_to_fragment(request, course_id=course_id, **kwargs)
|
||||
welcome_message_fragment = WelcomeMessageFragmentView().render_to_fragment(
|
||||
request, course_id=course_id, **kwargs
|
||||
)
|
||||
course_sock_fragment = CourseSockFragmentView().render_to_fragment(request, course=course, **kwargs)
|
||||
has_visited_course, resume_course_url = self._get_resume_course_info(request, course_id)
|
||||
else:
|
||||
outline_fragment = None
|
||||
welcome_message_fragment = None
|
||||
course_sock_fragment = None
|
||||
has_visited_course = None
|
||||
resume_course_url = None
|
||||
|
||||
# Get the handouts
|
||||
handouts_html = get_course_info_section(request, request.user, course, 'handouts')
|
||||
handouts_html = self._get_course_handouts(request, course)
|
||||
|
||||
# Get the course tools enabled for this user and course
|
||||
course_tools = CourseToolsPluginManager.get_enabled_course_tools(request, course_key)
|
||||
|
||||
# Render the course home fragment
|
||||
context = {
|
||||
@@ -122,6 +139,7 @@ class CourseHomeFragmentView(EdxFragmentView):
|
||||
'handouts_html': handouts_html,
|
||||
'has_visited_course': has_visited_course,
|
||||
'resume_course_url': resume_course_url,
|
||||
'course_tools': course_tools,
|
||||
'dates_fragment': dates_fragment,
|
||||
'welcome_message_fragment': welcome_message_fragment,
|
||||
'course_sock_fragment': course_sock_fragment,
|
||||
|
||||
@@ -32,7 +32,7 @@ class CourseSockFragmentView(EdxFragmentView):
|
||||
has_verified_mode = CourseMode.has_verified_mode(available_modes)
|
||||
|
||||
# Establish whether the user is already enrolled
|
||||
is_already_verified = CourseEnrollment.is_enrolled_as_verified(request.user.id, course_key)
|
||||
is_already_verified = CourseEnrollment.is_enrolled_as_verified(request.user, course_key)
|
||||
|
||||
# Establish whether the verification deadline has already passed
|
||||
verification_deadline = VerifiedUpgradeDeadlineDate(course, request.user)
|
||||
|
||||
@@ -42,6 +42,7 @@ django==1.8.18
|
||||
django-waffle==0.12.0
|
||||
djangorestframework-jwt==1.8.0
|
||||
djangorestframework-oauth==1.1.0
|
||||
enum34==1.1.6
|
||||
edx-ccx-keys==0.2.1
|
||||
edx-celeryutils==0.2.4
|
||||
edx-drf-extensions==1.2.2
|
||||
|
||||
@@ -136,11 +136,11 @@ site_status_msg = get_site_status_msg(course_id)
|
||||
% if not settings.FEATURES['DISABLE_LOGIN_BUTTON'] and not combined_login_and_register:
|
||||
% if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
|
||||
<div class="item nav-courseware-02">
|
||||
<a class="btn-neutral btn-register" href="${reverse('course-specific-register', args=[course.id.to_deprecated_string()])}">${_("Register")}</a>
|
||||
<a class="btn btn-neutral btn-register" href="${reverse('course-specific-register', args=[course.id.to_deprecated_string()])}">${_("Register")}</a>
|
||||
</div>
|
||||
% elif static.get_value('ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION')):
|
||||
<div class="item nav-courseware-02">
|
||||
<a class="btn-neutral btn-register" href="/register">${_("Register")}</a>
|
||||
<a class="btn btn-neutral btn-register" href="/register">${_("Register")}</a>
|
||||
</div>
|
||||
% endif
|
||||
% endif
|
||||
|
||||
@@ -138,11 +138,11 @@ site_status_msg = get_site_status_msg(course_id)
|
||||
% if not settings.FEATURES['DISABLE_LOGIN_BUTTON']:
|
||||
% if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
|
||||
<li class="nav-global-04">
|
||||
<a class="btn-neutral btn-register" href="${reverse('course-specific-register', args=[course.id.to_deprecated_string()])}">${_("Register Now")}</a>
|
||||
<a class="btn btn-neutral btn-register" href="${reverse('course-specific-register', args=[course.id.to_deprecated_string()])}">${_("Register Now")}</a>
|
||||
</li>
|
||||
% else:
|
||||
<li class="nav-global-04">
|
||||
<a class="btn-neutral btn-register" href="/register">${_("Register Now")}</a>
|
||||
<a class="btn btn-neutral btn-register" href="/register">${_("Register Now")}</a>
|
||||
</li>
|
||||
% endif
|
||||
% endif
|
||||
@@ -152,9 +152,9 @@ site_status_msg = get_site_status_msg(course_id)
|
||||
<li class="nav-courseware-01">
|
||||
% if not settings.FEATURES['DISABLE_LOGIN_BUTTON']:
|
||||
% if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
|
||||
<a class="btn btn-login" href="${reverse('course-specific-login', args=[course.id.to_deprecated_string()])}${login_query()}">${_("Sign in")}</a>
|
||||
<a class="btn btn-brand btn-login" href="${reverse('course-specific-login', args=[course.id.to_deprecated_string()])}${login_query()}">${_("Sign in")}</a>
|
||||
% else:
|
||||
<a class="btn-brand btn-login" href="/login${login_query()}">${_("Sign in")}</a>
|
||||
<a class="btn btn-brand btn-login" href="/login${login_query()}">${_("Sign in")}</a>
|
||||
% endif
|
||||
% endif
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user