diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index db99744c3e..8479208494 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 @@ 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: diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index c5ad0cbb94..7555f909fa 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 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. diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 08f7fdfbef..df863b4545 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 c0b552659c..ae0b3a0fdd 100644 --- a/lms/djangoapps/courseware/date_summary.py +++ b/lms/djangoapps/courseware/date_summary.py @@ -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): diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index fd9197ca2b..3ea1dd36ae 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 af9b604013..e69de29bb2 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 a3a968f34f..19117b46c0 100644 --- a/lms/djangoapps/courseware/tests/helpers.py +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -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': ''} + + 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 fe31ee655c..39a7a908ac 100644 --- a/lms/djangoapps/courseware/tests/test_discussion_xblock.py +++ b/lms/djangoapps/courseware/tests/test_discussion_xblock.py @@ -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 diff --git a/lms/djangoapps/courseware/tests/test_lti_integration.py b/lms/djangoapps/courseware/tests/test_lti_integration.py index 5e1a6537c6..cf6bc9476c 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.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 diff --git a/lms/djangoapps/courseware/tests/test_video_handlers.py b/lms/djangoapps/courseware/tests/test_video_handlers.py index df37776e55..a4c04cb795 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.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."]} diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py index bf6ce1a731..db323d5fc0 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 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 diff --git a/lms/djangoapps/courseware/tests/test_word_cloud.py b/lms/djangoapps/courseware/tests/test_word_cloud.py index 7f799c817f..fd93ff77bd 100644 --- a/lms/djangoapps/courseware/tests/test_word_cloud.py +++ b/lms/djangoapps/courseware/tests/test_word_cloud.py @@ -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) diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py index ff99139cc4..0639c3452f 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 a486849078..fd8f49fd70 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -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), diff --git a/lms/djangoapps/discussion/tests/__init__.py b/lms/djangoapps/discussion/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/discussion/tests/test_views.py b/lms/djangoapps/discussion/tests/test_views.py index 406d9627fc..2277e18fdf 100644 --- a/lms/djangoapps/discussion/tests/test_views.py +++ b/lms/djangoapps/discussion/tests/test_views.py @@ -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( diff --git a/lms/djangoapps/django_comment_client/base/tests.py b/lms/djangoapps/django_comment_client/base/tests.py index e039059ff7..2a6df7f338 100644 --- a/lms/djangoapps/django_comment_client/base/tests.py +++ b/lms/djangoapps/django_comment_client/base/tests.py @@ -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 diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 7404678547..09e4129112 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 de3ceaa6fc..6885484cc3 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; - } + @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; } } diff --git a/lms/templates/courseware/info.html b/lms/templates/courseware/info.html index 5ad0f63ab7..2e85107475 100644 --- a/lms/templates/courseware/info.html +++ b/lms/templates/courseware/info.html @@ -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 - ${_("Course Tools")} - - - - ${_("Bookmarks")} - - % if SHOW_REVIEWS_TOOL_FLAG.is_enabled(course.id) and show_reviews_link: - - - ${_("Reviews")} - - % endif - - + % 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 68279cf90d..8cf1562992 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -165,7 +165,9 @@ from pipeline_mako import render_require_js_path_overrides