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)} @@ -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 <%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 "" } diff --git a/lms/templates/navigation/navbar-authenticated.html b/lms/templates/navigation/navbar-authenticated.html index e1cd325f95..20d236cb13 100644 --- a/lms/templates/navigation/navbar-authenticated.html +++ b/lms/templates/navigation/navbar-authenticated.html @@ -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: % endif % if show_program_listing: @@ -28,12 +28,12 @@ from django.utils.translation import ugettext as _ % endif - %if settings.FEATURES.get('ENABLE_SYSADMIN_DASHBOARD','') and user.is_staff: + % if settings.FEATURES.get('ENABLE_SYSADMIN_DASHBOARD','') and user.is_staff:
  • ## Translators: This is short for "System administration". - ${_("Sysadmin")} + ${_("Sysadmin")}
  • - %endif + % endif diff --git a/lms/templates/navigation/navbar-not-authenticated.html b/lms/templates/navigation/navbar-not-authenticated.html index 1804cc253a..3b3486801f 100644 --- a/lms/templates/navigation/navbar-not-authenticated.html +++ b/lms/templates/navigation/navbar-not-authenticated.html @@ -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)): % if settings.FEATURES.get('COURSES_ARE_BROWSABLE'): % endif % endif @@ -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: % elif static.get_value('ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION')): % endif % endif @@ -51,11 +51,11 @@ from django.utils.translation import ugettext as _ - \ No newline at end of file + diff --git a/openedx/core/djangoapps/waffle_utils/__init__.py b/openedx/core/djangoapps/waffle_utils/__init__.py index 3d2823cb38..bcae90833c 100644 --- a/openedx/core/djangoapps/waffle_utils/__init__.py +++ b/openedx/core/djangoapps/waffle_utils/__init__.py @@ -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 ) diff --git a/openedx/features/course_bookmarks/plugins.py b/openedx/features/course_bookmarks/plugins.py index 17880af8b9..95e73f0af8 100644 --- a/openedx/features/course_bookmarks/plugins.py +++ b/openedx/features/course_bookmarks/plugins.py @@ -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): diff --git a/openedx/features/course_bookmarks/tests/__init__.py b/openedx/features/course_bookmarks/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/features/course_bookmarks/tests/test_course_bookmarks.py b/openedx/features/course_bookmarks/tests/test_course_bookmarks.py new file mode 100644 index 0000000000..1841499a53 --- /dev/null +++ b/openedx/features/course_bookmarks/tests/test_course_bookmarks.py @@ -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) diff --git a/openedx/features/course_experience/course_tools.py b/openedx/features/course_experience/course_tools.py index 76ecb4e857..82fe4be708 100644 --- a/openedx/features/course_experience/course_tools.py +++ b/openedx/features/course_experience/course_tools.py @@ -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) diff --git a/openedx/features/course_experience/plugins.py b/openedx/features/course_experience/plugins.py index 4cb091544f..e2aa0fe2bf 100644 --- a/openedx/features/course_experience/plugins.py +++ b/openedx/features/course_experience/plugins.py @@ -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): diff --git a/openedx/features/course_experience/templates/course_experience/course-dates-fragment.html b/openedx/features/course_experience/templates/course_experience/course-dates-fragment.html index 962405e36c..a1295fff0e 100644 --- a/openedx/features/course_experience/templates/course_experience/course-dates-fragment.html +++ b/openedx/features/course_experience/templates/course_experience/course-dates-fragment.html @@ -24,13 +24,17 @@ from django.utils.translation import ugettext as _

    % endif % if course_date.description: -

    ${course_date.description}

    +

    ${course_date.description}

    % endif % if course_date.link and course_date.link_text: - - ${course_date.link_text} - + + ${course_date.link_text} + % endif % endfor + +<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory"> + DateUtilFactory.transform('.localized-datetime'); + diff --git a/openedx/features/course_experience/templates/course_experience/course-home-fragment.html b/openedx/features/course_experience/templates/course_experience/course-home-fragment.html index 8f858d31db..90812cfe3c 100644 --- a/openedx/features/course_experience/templates/course_experience/course-home-fragment.html +++ b/openedx/features/course_experience/templates/course_experience/course-home-fragment.html @@ -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 % endif - ${HTML(outline_fragment.body_html())} + % if outline_fragment: + ${HTML(outline_fragment.body_html())} + % endif - ${HTML(course_sock_fragment.body_html())} + % if course_sock_fragment: + ${HTML(course_sock_fragment.body_html())} + % endif diff --git a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html b/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html index 269ce54245..fb28689b78 100644 --- a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +++ b/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html @@ -151,7 +151,6 @@ from openedx.core.djangolib.markup import HTML, Text )} % endif - % endif diff --git a/openedx/features/course_experience/tests/views/helpers.py b/openedx/features/course_experience/tests/views/helpers.py new file mode 100644 index 0000000000..7999403a5f --- /dev/null +++ b/openedx/features/course_experience/tests/views/helpers.py @@ -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() diff --git a/openedx/features/course_experience/tests/views/test_course_home.py b/openedx/features/course_experience/tests/views/test_course_home.py index 2ec4e0927c..07a840902f 100644 --- a/openedx/features/course_experience/tests/views/test_course_home.py +++ b/openedx/features/course_experience/tests/views/test_course_home.py @@ -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 = '

    Welcome!

    ' TEST_UPDATE_MESSAGE = '

    Test Update!

    ' 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))) diff --git a/openedx/features/course_experience/tests/views/test_course_outline.py b/openedx/features/course_experience/tests/views/test_course_outline.py index 8e518bb621..2ba7122648 100644 --- a/openedx/features/course_experience/tests/views/test_course_outline.py +++ b/openedx/features/course_experience/tests/views/test_course_outline.py @@ -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 diff --git a/openedx/features/course_experience/tests/views/test_course_sock.py b/openedx/features/course_experience/tests/views/test_course_sock.py index 3319a92268..3d3beff433 100644 --- a/openedx/features/course_experience/tests/views/test_course_sock.py +++ b/openedx/features/course_experience/tests/views/test_course_sock.py @@ -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 = '
    ' + 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, diff --git a/openedx/features/course_experience/views/course_sock.py b/openedx/features/course_experience/views/course_sock.py index f61454d54b..f55edb755a 100644 --- a/openedx/features/course_experience/views/course_sock.py +++ b/openedx/features/course_experience/views/course_sock.py @@ -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) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 6290963702..76391242ed 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -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 diff --git a/themes/edx.org/lms/templates/header.html b/themes/edx.org/lms/templates/header.html index e08cb19a39..9be3a19a0f 100644 --- a/themes/edx.org/lms/templates/header.html +++ b/themes/edx.org/lms/templates/header.html @@ -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: % elif static.get_value('ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION')): % endif % endif diff --git a/themes/red-theme/lms/templates/header.html b/themes/red-theme/lms/templates/header.html index 1200057d89..2e4078370d 100755 --- a/themes/red-theme/lms/templates/header.html +++ b/themes/red-theme/lms/templates/header.html @@ -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: % else: % endif % endif @@ -152,9 +152,9 @@ site_status_msg = get_site_status_msg(course_id)