diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index db99744c3e41..847920849419 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -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 @@ def should_user_reset_password_now(cls, user):
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 @@ def is_allowable_password_reuse(cls, user, new_password):
"""
Verifies that the password adheres to the reuse policies
"""
+ assert user
+
if not settings.FEATURES['ADVANCED_SECURITY']:
return True
@@ -1082,6 +1088,10 @@ def get_enrollment(cls, user, course_key):
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 @@ def is_enrolled(cls, user, course_key):
`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 @@ def generate_enrollment_status_hash(cls, user):
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 @@ def _get_enrollment_state(cls, user, course_key):
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:
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py
index c5ad0cbb9492..7555f909fa83 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py
@@ -6,15 +6,19 @@
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.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 @@ def end_modulestore_isolation(cls):
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 @@ def setUp(self):
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.
diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py
index 08f7fdfbefbf..df863b4545c1 100644
--- a/lms/djangoapps/courseware/courses.py
+++ b/lms/djangoapps/courseware/courses.py
@@ -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)]))
diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py
index c0b552659cdf..ae0b3a0fdd4a 100644
--- a/lms/djangoapps/courseware/date_summary.py
+++ b/lms/djangoapps/courseware/date_summary.py
@@ -115,7 +115,7 @@ def is_enabled(self):
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):
diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py
index fd9197ca2beb..3ea1dd36aec9 100644
--- a/lms/djangoapps/courseware/tabs.py
+++ b/lms/djangoapps/courseware/tabs.py
@@ -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):
"""
diff --git a/lms/djangoapps/courseware/tests/__init__.py b/lms/djangoapps/courseware/tests/__init__.py
index af9b60401331..e69de29bb2d1 100644
--- a/lms/djangoapps/courseware/tests/__init__.py
+++ b/lms/djangoapps/courseware/tests/__init__.py
@@ -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': ''}
-
- 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
diff --git a/lms/djangoapps/courseware/tests/helpers.py b/lms/djangoapps/courseware/tests/helpers.py
index a3a968f34f17..19117b46c0d2 100644
--- a/lms/djangoapps/courseware/tests/helpers.py
+++ b/lms/djangoapps/courseware/tests/helpers.py
@@ -7,12 +7,140 @@
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': ''}
+
+ 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):
diff --git a/lms/djangoapps/courseware/tests/test_discussion_xblock.py b/lms/djangoapps/courseware/tests/test_discussion_xblock.py
index fe31ee655c85..39a7a908ace4 100644
--- a/lms/djangoapps/courseware/tests/test_discussion_xblock.py
+++ b/lms/djangoapps/courseware/tests/test_discussion_xblock.py
@@ -17,7 +17,7 @@
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
diff --git a/lms/djangoapps/courseware/tests/test_lti_integration.py b/lms/djangoapps/courseware/tests/test_lti_integration.py
index 5e1a6537c611..cf6bc9476c7b 100644
--- a/lms/djangoapps/courseware/tests/test_lti_integration.py
+++ b/lms/djangoapps/courseware/tests/test_lti_integration.py
@@ -10,7 +10,7 @@
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
diff --git a/lms/djangoapps/courseware/tests/test_video_handlers.py b/lms/djangoapps/courseware/tests/test_video_handlers.py
index df37776e55c4..a4c04cb795fe 100644
--- a/lms/djangoapps/courseware/tests/test_video_handlers.py
+++ b/lms/djangoapps/courseware/tests/test_video_handlers.py
@@ -22,7 +22,7 @@
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."]}
diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py
index bf6ce1a7311a..db323d5fc030 100644
--- a/lms/djangoapps/courseware/tests/test_video_mongo.py
+++ b/lms/djangoapps/courseware/tests/test_video_mongo.py
@@ -25,7 +25,7 @@
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
diff --git a/lms/djangoapps/courseware/tests/test_word_cloud.py b/lms/djangoapps/courseware/tests/test_word_cloud.py
index 7f799c817f2b..fd93ff77bd5f 100644
--- a/lms/djangoapps/courseware/tests/test_word_cloud.py
+++ b/lms/djangoapps/courseware/tests/test_word_cloud.py
@@ -8,7 +8,7 @@
from xmodule.x_module import STUDENT_VIEW
-from . import BaseTestXmodule
+from .helpers import BaseTestXmodule
@attr(shard=1)
diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py
index ff99139cc4e7..0639c3452ff1 100644
--- a/lms/djangoapps/courseware/views/index.py
+++ b/lms/djangoapps/courseware/views/index.py
@@ -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
diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py
index a48684907874..fd8f49fd7016 100644
--- a/lms/djangoapps/courseware/views/views.py
+++ b/lms/djangoapps/courseware/views/views.py
@@ -87,6 +87,7 @@
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 get_last_accessed_courseware(course, request, user):
# 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 get_last_accessed_courseware(course, request, user):
'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),
diff --git a/lms/djangoapps/discussion/tests/__init__.py b/lms/djangoapps/discussion/tests/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/lms/djangoapps/discussion/tests/test_views.py b/lms/djangoapps/discussion/tests/test_views.py
index 406d9627fcdb..2277e18fdf7a 100644
--- a/lms/djangoapps/discussion/tests/test_views.py
+++ b/lms/djangoapps/discussion/tests/test_views.py
@@ -370,18 +370,18 @@ def setUp(self):
# 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(
diff --git a/lms/djangoapps/django_comment_client/base/tests.py b/lms/djangoapps/django_comment_client/base/tests.py
index e039059ff7b1..2a6df7f3383b 100644
--- a/lms/djangoapps/django_comment_client/base/tests.py
+++ b/lms/djangoapps/django_comment_client/base/tests.py
@@ -383,8 +383,8 @@ def inner(self, default_store, module_count, mongo_calls, sql_queries, *args, **
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 @@ def test_create_thread(self, mock_request):
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
diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py
index 740467854703..09e41291126d 100644
--- a/lms/djangoapps/instructor/views/api.py
+++ b/lms/djangoapps/instructor/views/api.py
@@ -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
diff --git a/lms/static/sass/shared-v2/_header.scss b/lms/static/sass/shared-v2/_header.scss
index de3ceaa6fc89..6885484cc3df 100644
--- a/lms/static/sass/shared-v2/_header.scss
+++ b/lms/static/sass/shared-v2/_header.scss
@@ -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;
- }
-
- &.nav-courseware {
- margin-top: 5px;
+ @include margin(0, 0, 0, $baseline/2);
+
+ .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;
}
}
diff --git a/lms/templates/courseware/info.html b/lms/templates/courseware/info.html
index 5ad0f63ab75c..2e85107475f1 100644
--- a/lms/templates/courseware/info.html
+++ b/lms/templates/courseware/info.html
@@ -12,7 +12,6 @@
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 @@
${_("Course Updates and News")}
- ${_("Course Tools")}
-
-
+ % if course_tools:
+ ${_("Course Tools")}
+ % for course_tool in course_tools:
+
+
+ ${course_tool.title()}
+
+ % endfor
+ % endif
% if SelfPacedConfiguration.current().enable_course_home_improvements:
${HTML(dates_fragment.body_html())}
% endif
- ${_(course.info_sidebar_name)}
- ${HTML(get_course_info_section(request, masquerade_user, course, 'handouts'))}
+ ${_(course.info_sidebar_name)}
+ ${HTML(get_course_info_section(request, masquerade_user, course, 'handouts'))}
% else:
diff --git a/lms/templates/main.html b/lms/templates/main.html
index 68279cf90d34..8cf15629926a 100644
--- a/lms/templates/main.html
+++ b/lms/templates/main.html
@@ -165,7 +165,9 @@