Commit 69eeca61 by Clinton Blackburn Committed by Clinton Blackburn

Opening courseware to anonymous users

Anonymous users may now view units in the courseware. This access is limited to units that are not considered problems/graded (e.g. homework, exams).
parent 9a9ef198
...@@ -135,6 +135,9 @@ def permission_blacked_out(course, role_names, permission_name): ...@@ -135,6 +135,9 @@ def permission_blacked_out(course, role_names, permission_name):
def all_permissions_for_user_in_course(user, course_id): # pylint: disable=invalid-name def all_permissions_for_user_in_course(user, course_id): # pylint: disable=invalid-name
"""Returns all the permissions the user has in the given course.""" """Returns all the permissions the user has in the given course."""
if not user.is_authenticated():
return {}
course = modulestore().get_course(course_id) course = modulestore().get_course(course_id)
if course is None: if course is None:
raise ItemNotFoundError(course_id) raise ItemNotFoundError(course_id)
......
...@@ -5,7 +5,7 @@ Utility library for working with the edx-milestones app ...@@ -5,7 +5,7 @@ Utility library for working with the edx-milestones app
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from milestones import api as milestones_api from milestones import api as milestones_api
from milestones.exceptions import InvalidMilestoneRelationshipTypeException from milestones.exceptions import InvalidMilestoneRelationshipTypeException, InvalidUserException
from milestones.models import MilestoneRelationshipType from milestones.models import MilestoneRelationshipType
from milestones.services import MilestonesService from milestones.services import MilestonesService
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
...@@ -213,21 +213,32 @@ def get_required_content(course_key, user): ...@@ -213,21 +213,32 @@ def get_required_content(course_key, user):
""" """
required_content = [] required_content = []
if settings.FEATURES.get('MILESTONES_APP'): if settings.FEATURES.get('MILESTONES_APP'):
# Get all of the outstanding milestones for this course, for this user course_run_id = unicode(course_key)
try:
milestone_paths = get_course_milestones_fulfillment_paths( if user.is_authenticated():
unicode(course_key), # Get all of the outstanding milestones for this course, for this user
serialize_user(user) try:
)
except InvalidMilestoneRelationshipTypeException: milestone_paths = get_course_milestones_fulfillment_paths(
return required_content course_run_id,
serialize_user(user)
# For each outstanding milestone, see if this content is one of its fulfillment paths )
for path_key in milestone_paths: except InvalidMilestoneRelationshipTypeException:
milestone_path = milestone_paths[path_key] return required_content
if milestone_path.get('content') and len(milestone_path['content']):
for content in milestone_path['content']: # For each outstanding milestone, see if this content is one of its fulfillment paths
required_content.append(content) for path_key in milestone_paths:
milestone_path = milestone_paths[path_key]
if milestone_path.get('content') and len(milestone_path['content']):
for content in milestone_path['content']:
required_content.append(content)
else:
if get_course_milestones(course_run_id):
# NOTE (CCB): The initial version of anonymous courseware access is very simple. We avoid accidentally
# exposing locked content by simply avoiding anonymous access altogether for courses runs with
# milestones.
raise InvalidUserException('Anonymous access is not allowed for course runs with milestones set.')
return required_content return required_content
......
...@@ -3,15 +3,17 @@ Tests for the milestones helpers library, which is the integration point for the ...@@ -3,15 +3,17 @@ Tests for the milestones helpers library, which is the integration point for the
""" """
import ddt import ddt
import pytest
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from milestones import api as milestones_api
from milestones.exceptions import InvalidCourseKeyException, InvalidUserException from milestones.exceptions import InvalidCourseKeyException, InvalidUserException
from milestones.models import MilestoneRelationshipType
from mock import patch from mock import patch
from milestones import api as milestones_api from util import milestones_helpers
from milestones.models import MilestoneRelationshipType
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from util import milestones_helpers
@patch.dict(settings.FEATURES, {'MILESTONES_APP': False}) @patch.dict(settings.FEATURES, {'MILESTONES_APP': False})
...@@ -134,3 +136,20 @@ class MilestonesHelpersTestCase(ModuleStoreTestCase): ...@@ -134,3 +136,20 @@ class MilestonesHelpersTestCase(ModuleStoreTestCase):
milestones_helpers.any_unfulfilled_milestones(None, self.user['id']) milestones_helpers.any_unfulfilled_milestones(None, self.user['id'])
with self.assertRaises(InvalidUserException): with self.assertRaises(InvalidUserException):
milestones_helpers.any_unfulfilled_milestones(self.course.id, None) milestones_helpers.any_unfulfilled_milestones(self.course.id, None)
@patch.dict(settings.FEATURES, {'MILESTONES_APP': True})
def test_get_required_content_with_anonymous_user(self):
course = CourseFactory()
required_content = milestones_helpers.get_required_content(course.id, AnonymousUser())
assert required_content == []
# NOTE (CCB): The initial version of anonymous courseware access is very simple. We avoid accidentally
# exposing locked content by simply avoiding anonymous access altogether for courses runs with milestones.
milestone = milestones_api.add_milestone({
'name': 'test',
'namespace': 'test',
})
milestones_helpers.add_course_milestone(str(course.id), 'requires', milestone)
with pytest.raises(InvalidUserException):
milestones_helpers.get_required_content(course.id, AnonymousUser())
...@@ -4,22 +4,22 @@ xModule implementation of a learning sequence ...@@ -4,22 +4,22 @@ xModule implementation of a learning sequence
# pylint: disable=abstract-method # pylint: disable=abstract-method
import collections import collections
from datetime import datetime
import json import json
import logging import logging
from pkg_resources import resource_string from datetime import datetime
from pytz import UTC
from lxml import etree from lxml import etree
from pkg_resources import resource_string
from pytz import UTC
from xblock.core import XBlock from xblock.core import XBlock
from xblock.fields import Integer, Scope, Boolean, String, List from xblock.fields import Boolean, Integer, List, Scope, String
from xblock.fragment import Fragment from xblock.fragment import Fragment
from .exceptions import NotFoundError from .exceptions import NotFoundError
from .fields import Date from .fields import Date
from .mako_module import MakoModuleDescriptor from .mako_module import MakoModuleDescriptor
from .progress import Progress from .progress import Progress
from .x_module import XModule, STUDENT_VIEW from .x_module import STUDENT_VIEW, XModule
from .xml_module import XmlDescriptor from .xml_module import XmlDescriptor
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -292,6 +292,10 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): ...@@ -292,6 +292,10 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
self.verify_current_content_visibility(hidden_date, self.hide_after_due) self.verify_current_content_visibility(hidden_date, self.hide_after_due)
) )
def is_user_authenticated(self, context):
# NOTE (CCB): We default to true to maintain the behavior in place prior to allowing anonymous access access.
return context.get('user_authenticated', True)
def _student_view(self, context, banner_text=None): def _student_view(self, context, banner_text=None):
""" """
Returns the rendered student view of the content of this Returns the rendered student view of the content of this
...@@ -312,6 +316,7 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): ...@@ -312,6 +316,7 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
'next_url': context.get('next_url'), 'next_url': context.get('next_url'),
'prev_url': context.get('prev_url'), 'prev_url': context.get('prev_url'),
'banner_text': banner_text, 'banner_text': banner_text,
'disable_navigation': not self.is_user_authenticated(context),
} }
fragment.add_content(self.system.render_template("seq_module.html", params)) fragment.add_content(self.system.render_template("seq_module.html", params))
...@@ -325,6 +330,11 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): ...@@ -325,6 +330,11 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
Update the user's sequential position given the context and the Update the user's sequential position given the context and the
number_of_display_items number_of_display_items
""" """
position = context.get('position')
if position:
self.position = position
# If we're rendering this sequence, but no position is set yet, # If we're rendering this sequence, but no position is set yet,
# or exceeds the length of the displayable items, # or exceeds the length of the displayable items,
# default the position to the first element # default the position to the first element
...@@ -341,16 +351,36 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): ...@@ -341,16 +351,36 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
display_items. Returns a list of dict objects with information about display_items. Returns a list of dict objects with information about
the given display_items. the given display_items.
""" """
bookmarks_service = self.runtime.service(self, "bookmarks") is_user_authenticated = self.is_user_authenticated(context)
context["username"] = self.runtime.service(self, "user").get_current_user().opt_attrs['edx-platform.username'] bookmarks_service = self.runtime.service(self, 'bookmarks')
context['username'] = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(
'edx-platform.username')
display_names = [ display_names = [
self.get_parent().display_name_with_default, self.get_parent().display_name_with_default,
self.display_name_with_default self.display_name_with_default
] ]
contents = [] contents = []
for item in display_items: for item in display_items:
is_bookmarked = bookmarks_service.is_bookmarked(usage_key=item.scope_ids.usage_id) # NOTE (CCB): This seems like a hack, but I don't see a better method of determining the type/category.
context["bookmarked"] = is_bookmarked item_type = item.get_icon_class()
usage_id = item.scope_ids.usage_id
if item_type == 'problem' and not is_user_authenticated:
log.info(
'Problem [%s] was not rendered because anonymous access is not allowed for graded content',
usage_id
)
continue
show_bookmark_button = False
is_bookmarked = False
if is_user_authenticated:
show_bookmark_button = True
is_bookmarked = bookmarks_service.is_bookmarked(usage_key=usage_id)
context['show_bookmark_button'] = show_bookmark_button
context['bookmarked'] = is_bookmarked
rendered_item = item.render(STUDENT_VIEW, context) rendered_item = item.render(STUDENT_VIEW, context)
fragment.add_frag_resources(rendered_item) fragment.add_frag_resources(rendered_item)
...@@ -358,8 +388,8 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): ...@@ -358,8 +388,8 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
iteminfo = { iteminfo = {
'content': rendered_item.content, 'content': rendered_item.content,
'page_title': getattr(item, 'tooltip_title', ''), 'page_title': getattr(item, 'tooltip_title', ''),
'type': item.get_icon_class(), 'type': item_type,
'id': item.scope_ids.usage_id.to_deprecated_string(), 'id': usage_id.to_deprecated_string(),
'bookmarked': is_bookmarked, 'bookmarked': is_bookmarked,
'path': " > ".join(display_names + [item.display_name_with_default]), 'path': " > ".join(display_names + [item.display_name_with_default]),
} }
......
...@@ -14,7 +14,7 @@ from django.core.cache import cache ...@@ -14,7 +14,7 @@ from django.core.cache import cache
from django.template.context_processors import csrf from django.template.context_processors import csrf
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from edx_proctoring.services import ProctoringService from edx_proctoring.services import ProctoringService
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
...@@ -925,29 +925,32 @@ def handle_xblock_callback(request, course_id, usage_id, handler, suffix=None): ...@@ -925,29 +925,32 @@ def handle_xblock_callback(request, course_id, usage_id, handler, suffix=None):
Generic view for extensions. This is where AJAX calls go. Generic view for extensions. This is where AJAX calls go.
Arguments: Arguments:
request (Request): Django request.
- request -- the django request. course_id (str): Course containing the block
- location -- the module location. Used to look up the XModule instance usage_id (str)
- course_id -- defines the course context for this request. handler (str)
suffix (str)
Return 403 error if the user is not logged in. Raises Http404 if
the location and course_id do not identify a valid module, the module is Raises:
not accessible by the user, or the module raises NotFoundError. If the Http404: If the course is not found in the modulestore.
module raises any other error, it will escape this function.
""" """
if not request.user.is_authenticated(): # NOTE (CCB): Allow anonymous GET calls (e.g. for transcripts). Modifying this view is simpler than updating
return HttpResponse('Unauthenticated', status=403) # the XBlocks to use `handle_xblock_callback_noauth`...which is practically identical to this view.
if request.method != 'GET' and not request.user.is_authenticated():
return HttpResponseForbidden()
request.user.known = request.user.is_authenticated()
try: try:
course_key = CourseKey.from_string(course_id) course_key = CourseKey.from_string(course_id)
except InvalidKeyError: except InvalidKeyError:
raise Http404("Invalid location") raise Http404('{} is not a valid course key'.format(course_id))
with modulestore().bulk_operations(course_key): with modulestore().bulk_operations(course_key):
try: try:
course = modulestore().get_course(course_key) course = modulestore().get_course(course_key)
except ItemNotFoundError: except ItemNotFoundError:
raise Http404("invalid location") raise Http404('{} does not exist in the modulestore'.format(course_id))
return _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, course=course) return _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, course=course)
......
...@@ -57,7 +57,7 @@ class TestDiscussionXBlock(XModuleRenderingTestBase): ...@@ -57,7 +57,7 @@ class TestDiscussionXBlock(XModuleRenderingTestBase):
self.block.xmodule_runtime = mock.Mock() self.block.xmodule_runtime = mock.Mock()
if self.PATCH_DJANGO_USER: if self.PATCH_DJANGO_USER:
self.django_user_canary = object() self.django_user_canary = UserFactory()
self.django_user_mock = self.add_patcher( self.django_user_mock = self.add_patcher(
mock.patch.object(DiscussionXBlock, "django_user", new_callable=mock.PropertyMock) mock.patch.object(DiscussionXBlock, "django_user", new_callable=mock.PropertyMock)
) )
...@@ -259,7 +259,7 @@ class TestXBlockInCourse(SharedModuleStoreTestCase): ...@@ -259,7 +259,7 @@ class TestXBlockInCourse(SharedModuleStoreTestCase):
Set up a user, course, and discussion XBlock for use by tests. Set up a user, course, and discussion XBlock for use by tests.
""" """
super(TestXBlockInCourse, cls).setUpClass() super(TestXBlockInCourse, cls).setUpClass()
cls.user = UserFactory.create() cls.user = UserFactory()
cls.course = ToyCourseFactory.create() cls.course = ToyCourseFactory.create()
cls.course_key = cls.course.id cls.course_key = cls.course.id
cls.course_usage_key = cls.store.make_course_usage_key(cls.course_key) cls.course_usage_key = cls.store.make_course_usage_key(cls.course_key)
...@@ -380,8 +380,8 @@ class TestXBlockQueryLoad(SharedModuleStoreTestCase): ...@@ -380,8 +380,8 @@ class TestXBlockQueryLoad(SharedModuleStoreTestCase):
""" """
Tests that the permissions queries are cached when rendering numerous discussion XBlocks. Tests that the permissions queries are cached when rendering numerous discussion XBlocks.
""" """
user = UserFactory.create() user = UserFactory()
course = ToyCourseFactory.create() course = ToyCourseFactory()
course_key = course.id course_key = course.id
course_usage_key = self.store.make_course_usage_key(course_key) course_usage_key = self.store.make_course_usage_key(course_key)
discussions = [] discussions = []
......
...@@ -290,7 +290,6 @@ class ModuleRenderTestCase(SharedModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -290,7 +290,6 @@ class ModuleRenderTestCase(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
) )
response = self.client.post(dispatch_url, {'position': 2}) response = self.client.post(dispatch_url, {'position': 2})
self.assertEquals(403, response.status_code) self.assertEquals(403, response.status_code)
self.assertEquals('Unauthenticated', response.content)
def test_missing_position_handler(self): def test_missing_position_handler(self):
""" """
......
...@@ -11,10 +11,23 @@ from urllib import quote, urlencode ...@@ -11,10 +11,23 @@ from urllib import quote, urlencode
from uuid import uuid4 from uuid import uuid4
import ddt import ddt
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.core.urlresolvers import reverse
from django.http import Http404, HttpResponseBadRequest
from django.test import TestCase
from django.test.client import Client, RequestFactory
from django.test.utils import override_settings
from freezegun import freeze_time from freezegun import freeze_time
from milestones.tests.utils import MilestonesTestCaseMixin
from mock import MagicMock, PropertyMock, create_autospec, patch from mock import MagicMock, PropertyMock, create_autospec, patch
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import Location
from pytz import UTC from pytz import UTC
from xblock.core import XBlock
from xblock.fields import Scope, String
from xblock.fragment import Fragment
import courseware.views.views as views import courseware.views.views as views
import shoppingcart import shoppingcart
...@@ -32,19 +45,9 @@ from courseware.tests.factories import GlobalStaffFactory, StudentModuleFactory ...@@ -32,19 +45,9 @@ from courseware.tests.factories import GlobalStaffFactory, StudentModuleFactory
from courseware.testutils import RenderXBlockTestMixin from courseware.testutils import RenderXBlockTestMixin
from courseware.url_helpers import get_redirect_url from courseware.url_helpers import get_redirect_url
from courseware.user_state_client import DjangoXBlockUserStateClient from courseware.user_state_client import DjangoXBlockUserStateClient
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.core.urlresolvers import reverse
from django.http import Http404, HttpResponseBadRequest
from django.test import TestCase
from django.test.client import Client, RequestFactory
from django.test.utils import override_settings
from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error
from lms.djangoapps.grades.config.waffle import waffle as grades_waffle from lms.djangoapps.grades.config.waffle import waffle as grades_waffle
from lms.djangoapps.grades.config.waffle import ASSUME_ZERO_GRADE_IF_ABSENT from lms.djangoapps.grades.config.waffle import ASSUME_ZERO_GRADE_IF_ABSENT
from milestones.tests.utils import MilestonesTestCaseMixin
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import Location
from openedx.core.djangoapps.catalog.tests.factories import CourseFactory as CatalogCourseFactory from openedx.core.djangoapps.catalog.tests.factories import CourseFactory as CatalogCourseFactory
from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory, ProgramFactory from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory, ProgramFactory
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
...@@ -52,19 +55,17 @@ from openedx.core.djangoapps.crawlers.models import CrawlersConfig ...@@ -52,19 +55,17 @@ from openedx.core.djangoapps.crawlers.models import CrawlersConfig
from openedx.core.djangoapps.credit.api import set_credit_requirements from openedx.core.djangoapps.credit.api import set_credit_requirements
from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag
from openedx.core.djangolib.testing.utils import get_mock_request from openedx.core.djangolib.testing.utils import get_mock_request
from openedx.core.lib.gating import api as gating_api from openedx.core.lib.gating import api as gating_api
from openedx.features.course_experience import COURSE_OUTLINE_PAGE_FLAG from openedx.features.course_experience import COURSE_OUTLINE_PAGE_FLAG
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired
from student.models import CourseEnrollment from student.models import CourseEnrollment
from student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory, TEST_PASSWORD from student.tests.factories import TEST_PASSWORD, AdminFactory, CourseEnrollmentFactory, UserFactory
from util.tests.test_date_utils import fake_pgettext, fake_ugettext from util.tests.test_date_utils import fake_pgettext, fake_ugettext
from util.url import reload_django_url_config from util.url import reload_django_url_config
from util.views import ensure_valid_course_key from util.views import ensure_valid_course_key
from xblock.core import XBlock
from xblock.fields import Scope, String
from xblock.fragment import Fragment
from xmodule.graders import ShowCorrectness from xmodule.graders import ShowCorrectness
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -2193,6 +2194,7 @@ class TestIndexView(ModuleStoreTestCase): ...@@ -2193,6 +2194,7 @@ class TestIndexView(ModuleStoreTestCase):
""" """
Tests of the courseware.views.index view. Tests of the courseware.views.index view.
""" """
SEO_WAFFLE_FLAG = CourseWaffleFlag(WaffleFlagNamespace(name='seo'), 'enable_anonymous_courseware_access')
@XBlock.register_temp_plugin(ViewCheckerBlock, 'view_checker') @XBlock.register_temp_plugin(ViewCheckerBlock, 'view_checker')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
...@@ -2262,6 +2264,28 @@ class TestIndexView(ModuleStoreTestCase): ...@@ -2262,6 +2264,28 @@ class TestIndexView(ModuleStoreTestCase):
) )
self.assertIn("Activate Block ID: test_block_id", response.content) self.assertIn("Activate Block ID: test_block_id", response.content)
def test_anonymous_access(self):
course = CourseFactory()
with self.store.bulk_operations(course.id):
chapter = ItemFactory(parent=course, category='chapter')
section = ItemFactory(parent=chapter, category='sequential')
url = reverse(
'courseware_section',
kwargs={
'course_id': str(course.id),
'chapter': chapter.url_name,
'section': section.url_name,
}
)
response = self.client.get(url, follow=False)
assert response.status_code == 302
waffle_flag = CourseWaffleFlag(WaffleFlagNamespace(name='seo'), 'enable_anonymous_courseware_access')
with override_waffle_flag(waffle_flag, active=True):
response = self.client.get(url, follow=False)
assert response.status_code == 200
@ddt.ddt @ddt.ddt
class TestIndexViewWithVerticalPositions(ModuleStoreTestCase): class TestIndexViewWithVerticalPositions(ModuleStoreTestCase):
......
...@@ -8,12 +8,14 @@ import logging ...@@ -8,12 +8,14 @@ import logging
import urllib import urllib
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.template.context_processors import csrf from django.contrib.auth.views import redirect_to_login
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import Http404 from django.http import Http404
from django.template.context_processors import csrf
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.translation import ugettext as _
from django.views.decorators.cache import cache_control from django.views.decorators.cache import cache_control
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import View from django.views.generic import View
...@@ -29,19 +31,20 @@ from openedx.core.djangoapps.crawlers.models import CrawlersConfig ...@@ -29,19 +31,20 @@ from openedx.core.djangoapps.crawlers.models import CrawlersConfig
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
from openedx.core.djangoapps.monitoring_utils import set_custom_metrics_for_course_key from openedx.core.djangoapps.monitoring_utils import set_custom_metrics_for_course_key
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace from openedx.core.djangoapps.util.user_messages import PageLevelMessages
from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace, WaffleFlagNamespace, CourseWaffleFlag
from openedx.core.djangolib.markup import HTML, Text
from openedx.features.course_experience import COURSE_OUTLINE_PAGE_FLAG, default_course_url_name from openedx.features.course_experience import COURSE_OUTLINE_PAGE_FLAG, default_course_url_name
from openedx.features.course_experience.views.course_sock import CourseSockFragmentView from openedx.features.course_experience.views.course_sock import CourseSockFragmentView
from openedx.features.enterprise_support.api import data_sharing_consent_required from openedx.features.enterprise_support.api import data_sharing_consent_required
from shoppingcart.models import CourseRegistrationCode from shoppingcart.models import CourseRegistrationCode
from student.views import is_course_blocked from student.views import is_course_blocked
from student.models import CourseEnrollment
from util.views import ensure_valid_course_key from util.views import ensure_valid_course_key
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.x_module import STUDENT_VIEW from xmodule.x_module import STUDENT_VIEW
from .views import CourseTabView
from ..access import has_access from ..access import has_access
from ..access_utils import in_preview_mode, check_course_open_for_learner from ..access_utils import check_course_open_for_learner
from ..courses import get_course_with_access, get_current_child, get_studio_url from ..courses import get_course_with_access, get_current_child, get_studio_url
from ..entrance_exams import ( from ..entrance_exams import (
course_has_entrance_exam, course_has_entrance_exam,
...@@ -52,9 +55,6 @@ from ..entrance_exams import ( ...@@ -52,9 +55,6 @@ from ..entrance_exams import (
from ..masquerade import setup_masquerade from ..masquerade import setup_masquerade
from ..model_data import FieldDataCache from ..model_data import FieldDataCache
from ..module_render import get_module_for_descriptor, toc_for_course from ..module_render import get_module_for_descriptor, toc_for_course
from .views import (
CourseTabView,
)
log = logging.getLogger("edx.courseware.views.index") log = logging.getLogger("edx.courseware.views.index")
...@@ -66,7 +66,12 @@ class CoursewareIndex(View): ...@@ -66,7 +66,12 @@ class CoursewareIndex(View):
""" """
View class for the Courseware page. View class for the Courseware page.
""" """
@method_decorator(login_required)
@cached_property
def enable_anonymous_courseware_access(self):
waffle_flag = CourseWaffleFlag(WaffleFlagNamespace(name='seo'), 'enable_anonymous_courseware_access')
return waffle_flag.is_enabled(self.course_key)
@method_decorator(ensure_csrf_cookie) @method_decorator(ensure_csrf_cookie)
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True)) @method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True))
@method_decorator(ensure_valid_course_key) @method_decorator(ensure_valid_course_key)
...@@ -91,7 +96,10 @@ class CoursewareIndex(View): ...@@ -91,7 +96,10 @@ class CoursewareIndex(View):
position (unicode): position in module, eg of <sequential> module position (unicode): position in module, eg of <sequential> module
""" """
self.course_key = CourseKey.from_string(course_id) self.course_key = CourseKey.from_string(course_id)
self.request = request
if not (request.user.is_authenticated() or self.enable_anonymous_courseware_access):
return redirect_to_login(request.get_full_path())
self.original_chapter_url_name = chapter self.original_chapter_url_name = chapter
self.original_section_url_name = section self.original_section_url_name = section
self.chapter_url_name = chapter self.chapter_url_name = chapter
...@@ -108,11 +116,11 @@ class CoursewareIndex(View): ...@@ -108,11 +116,11 @@ class CoursewareIndex(View):
self.course = get_course_with_access( self.course = get_course_with_access(
request.user, 'load', self.course_key, request.user, 'load', self.course_key,
depth=CONTENT_DEPTH, depth=CONTENT_DEPTH,
check_if_enrolled=True, check_if_enrolled=not self.enable_anonymous_courseware_access,
) )
self.is_staff = has_access(request.user, 'staff', self.course) self.is_staff = has_access(request.user, 'staff', self.course)
self._setup_masquerade_for_effective_user() self._setup_masquerade_for_effective_user()
return self._get(request) return self.render(request)
except Exception as exception: # pylint: disable=broad-except except Exception as exception: # pylint: disable=broad-except
return CourseTabView.handle_exceptions(request, self.course, exception) return CourseTabView.handle_exceptions(request, self.course, exception)
...@@ -131,7 +139,7 @@ class CoursewareIndex(View): ...@@ -131,7 +139,7 @@ class CoursewareIndex(View):
# Set the user in the request to the effective user. # Set the user in the request to the effective user.
self.request.user = self.effective_user self.request.user = self.effective_user
def _get(self, request): def render(self, request):
""" """
Render the index page. Render the index page.
""" """
...@@ -148,6 +156,28 @@ class CoursewareIndex(View): ...@@ -148,6 +156,28 @@ class CoursewareIndex(View):
self._save_positions() self._save_positions()
self._prefetch_and_bind_section() self._prefetch_and_bind_section()
if not request.user.is_authenticated():
qs = urllib.urlencode({
'course_id': self.course_key,
'enrollment_action': 'enroll',
'email_opt_in': False,
})
PageLevelMessages.register_warning_message(
request,
Text(_("You are not signed in. To see additional course content, {sign_in_link} or "
"{register_link}, and enroll in this course.")).format(
sign_in_link=HTML('<a href="{url}">{sign_in_label}</a>').format(
sign_in_label=_('sign in'),
url='{}?{}'.format(reverse('signin_user'), qs),
),
register_link=HTML('<a href="/{url}">{register_label}</a>').format(
register_label=_('register'),
url='{}?{}'.format(reverse('register_user'), qs),
),
)
)
return render_to_response('courseware/courseware.html', self._create_courseware_context(request)) return render_to_response('courseware/courseware.html', self._create_courseware_context(request))
def _redirect_if_not_requested_section(self): def _redirect_if_not_requested_section(self):
...@@ -186,15 +216,20 @@ class CoursewareIndex(View): ...@@ -186,15 +216,20 @@ class CoursewareIndex(View):
""" """
Redirect to dashboard if the course is blocked due to non-payment. Redirect to dashboard if the course is blocked due to non-payment.
""" """
self.real_user = User.objects.prefetch_related("groups").get(id=self.real_user.id) redeemed_registration_codes = []
redeemed_registration_codes = CourseRegistrationCode.objects.filter(
course_id=self.course_key, if self.request.user.is_authenticated():
registrationcoderedemption__redeemed_by=self.real_user self.real_user = User.objects.prefetch_related("groups").get(id=self.real_user.id)
) redeemed_registration_codes = CourseRegistrationCode.objects.filter(
course_id=self.course_key,
registrationcoderedemption__redeemed_by=self.real_user
)
if is_course_blocked(self.request, redeemed_registration_codes, self.course_key): if is_course_blocked(self.request, redeemed_registration_codes, self.course_key):
# registration codes may be generated via Bulk Purchase Scenario # registration codes may be generated via Bulk Purchase Scenario
# we have to check only for the invoice generated registration codes # we have to check only for the invoice generated registration codes
# that their invoice is valid or not # that their invoice is valid or not
# TODO Update message to account for the fact that the user is not authenticated.
log.warning( log.warning(
u'User %s cannot access the course %s because payment has not yet been received', u'User %s cannot access the course %s because payment has not yet been received',
self.real_user, self.real_user,
...@@ -218,9 +253,11 @@ class CoursewareIndex(View): ...@@ -218,9 +253,11 @@ class CoursewareIndex(View):
""" """
Returns the preferred language for the actual user making the request. Returns the preferred language for the actual user making the request.
""" """
language_preference = get_user_preference(self.real_user, LANGUAGE_KEY) language_preference = settings.LANGUAGE_CODE
if not language_preference:
language_preference = settings.LANGUAGE_CODE if self.request.user.is_authenticated():
language_preference = get_user_preference(self.real_user, LANGUAGE_KEY)
return language_preference return language_preference
def _is_masquerading_as_student(self): def _is_masquerading_as_student(self):
...@@ -445,10 +482,15 @@ class CoursewareIndex(View): ...@@ -445,10 +482,15 @@ class CoursewareIndex(View):
requested_child=requested_child, requested_child=requested_child,
) )
# NOTE (CCB): Pull the position from the URL for un-authenticated users. Otherwise, pull the saved
# state from the data store.
position = None if self.request.user.is_authenticated() else self.position
section_context = { section_context = {
'activate_block_id': self.request.GET.get('activate_block_id'), 'activate_block_id': self.request.GET.get('activate_block_id'),
'requested_child': self.request.GET.get("child"), 'requested_child': self.request.GET.get("child"),
'progress_url': reverse('progress', kwargs={'course_id': unicode(self.course_key)}), 'progress_url': reverse('progress', kwargs={'course_id': unicode(self.course_key)}),
'user_authenticated': self.request.user.is_authenticated(),
'position': position,
} }
if previous_of_active_section: if previous_of_active_section:
section_context['prev_url'] = _compute_section_url(previous_of_active_section, 'last') section_context['prev_url'] = _compute_section_url(previous_of_active_section, 'last')
......
$(document).ajaxError(function(event, jXHR) { $(document).ajaxError(function(event, jXHR) {
if (jXHR.status === 403 && jXHR.responseText === 'Unauthenticated') { if (jXHR.status === 403) {
var message = gettext( var message = gettext(
'You have been logged out of your edX account. ' + 'You have been logged out of your edX account. ' +
'Click Okay to log in again now. ' + 'Click Okay to log in again now. ' +
......
...@@ -94,6 +94,16 @@ from openedx.features.course_experience import course_home_page_title, COURSE_OU ...@@ -94,6 +94,16 @@ from openedx.features.course_experience import course_home_page_title, COURSE_OU
var $$course_id = "${course.id | n, js_escaped_string}"; var $$course_id = "${course.id | n, js_escaped_string}";
</script> </script>
% if not request.user.is_authenticated():
<script type="text/javascript">
// Disable discussions
$('.xblock-student_view-discussion button.discussion-show').attr('disabled', true);
// Insert message informing user discussions are only available to logged in users.
$('.discussion-module')
</script>
% endif
${HTML(fragment.foot_html())} ${HTML(fragment.foot_html())}
</%block> </%block>
......
...@@ -13,6 +13,15 @@ from openedx.core.djangolib.js_utils import js_escaped_string ...@@ -13,6 +13,15 @@ from openedx.core.djangolib.js_utils import js_escaped_string
data-user-create-comment="${json_dumps(can_create_comment)}" data-user-create-comment="${json_dumps(can_create_comment)}"
data-user-create-subcomment="${json_dumps(can_create_subcomment)}" data-user-create-subcomment="${json_dumps(can_create_subcomment)}"
data-read-only="${'false' if can_create_thread else 'true'}"> data-read-only="${'false' if can_create_thread else 'true'}">
% if not user.is_authenticated():
<div class="page-banner">
<div class="alert alert-warning" role="alert">
<span class="icon icon-alert fa fa fa-warning" aria-hidden="true"></span>
<div class="message-content">${login_msg}</div>
</div>
</div>
<br>
% endif
<div class="discussion-module-header"> <div class="discussion-module-header">
<h3 class="hd hd-3 discussion-module-title">${_(display_name)}</h3> <h3 class="hd hd-3 discussion-module-title">${_(display_name)}</h3>
<div class="inline-discussion-topic"><span class="inline-discussion-topic-title">${_("Topic:")}</span> ${discussion_category} <div class="inline-discussion-topic"><span class="inline-discussion-topic-title">${_("Topic:")}</span> ${discussion_category}
...@@ -21,9 +30,12 @@ from openedx.core.djangolib.js_utils import js_escaped_string ...@@ -21,9 +30,12 @@ from openedx.core.djangolib.js_utils import js_escaped_string
%endif %endif
</div> </div>
</div> </div>
<button class="discussion-show btn" data-discussion-id="${discussion_id}"> <button class="discussion-show btn"
data-discussion-id="${discussion_id}"
${"disabled=disabled" if not user.is_authenticated() else ""}>
<span class="button-text">${_("Show Discussion")}</span> <span class="button-text">${_("Show Discussion")}</span>
</button> </button>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
var $$course_id = "${course_id | n, js_escaped_string}"; var $$course_id = "${course_id | n, js_escaped_string}";
......
...@@ -36,7 +36,8 @@ ...@@ -36,7 +36,8 @@
data-element="${idx+1}" data-element="${idx+1}"
data-page-title="${item['page_title']}" data-page-title="${item['page_title']}"
data-path="${item['path']}" data-path="${item['path']}"
id="tab_${idx}"> id="tab_${idx}"
${"disabled=disabled" if disable_navigation else ""}>
<span class="icon fa seq_${item['type']}" aria-hidden="true"></span> <span class="icon fa seq_${item['type']}" aria-hidden="true"></span>
<span class="fa fa-fw fa-bookmark bookmark-icon ${"is-hidden" if not item['bookmarked'] else "bookmarked"}" aria-hidden="true"></span> <span class="fa fa-fw fa-bookmark bookmark-icon ${"is-hidden" if not item['bookmarked'] else "bookmarked"}" aria-hidden="true"></span>
<div class="sequence-tooltip sr"><span class="sr">${item['type']}&nbsp;</span>${item['page_title']}<span class="sr bookmark-icon-sr">&nbsp;${_("Bookmarked") if item['bookmarked'] else ""}</span></div> <div class="sequence-tooltip sr"><span class="sr">${item['type']}&nbsp;</span>${item['page_title']}<span class="sr bookmark-icon-sr">&nbsp;${_("Bookmarked") if item['bookmarked'] else ""}</span></div>
......
...@@ -60,17 +60,20 @@ def get_bookmarks(user, course_key=None, fields=None, serialized=True): ...@@ -60,17 +60,20 @@ def get_bookmarks(user, course_key=None, fields=None, serialized=True):
Returns: Returns:
List of dicts if serialized is True else queryset. List of dicts if serialized is True else queryset.
""" """
bookmarks_queryset = Bookmark.objects.filter(user=user) if user.is_authenticated():
bookmarks_queryset = Bookmark.objects.filter(user=user)
if course_key: if course_key:
bookmarks_queryset = bookmarks_queryset.filter(course_key=course_key) bookmarks_queryset = bookmarks_queryset.filter(course_key=course_key)
if len(set(fields or []) & set(OPTIONAL_FIELDS)) > 0: if len(set(fields or []) & set(OPTIONAL_FIELDS)) > 0:
bookmarks_queryset = bookmarks_queryset.select_related('user', 'xblock_cache') bookmarks_queryset = bookmarks_queryset.select_related('user', 'xblock_cache')
else: else:
bookmarks_queryset = bookmarks_queryset.select_related('user') bookmarks_queryset = bookmarks_queryset.select_related('user')
bookmarks_queryset = bookmarks_queryset.order_by('-created') bookmarks_queryset = bookmarks_queryset.order_by('-created')
else:
bookmarks_queryset = Bookmark.objects.none()
if serialized: if serialized:
return BookmarkSerializer(bookmarks_queryset, context={'fields': fields}, many=True).data return BookmarkSerializer(bookmarks_queryset, context={'fields': fields}, many=True).data
......
...@@ -3,21 +3,21 @@ ...@@ -3,21 +3,21 @@
Discussion XBlock Discussion XBlock
""" """
import logging import logging
import urllib
from django.contrib.staticfiles.storage import staticfiles_storage from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.urlresolvers import reverse
from django.utils.translation import get_language_bidi from django.utils.translation import get_language_bidi
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin
from xmodule.raw_module import RawDescriptor
from xblock.core import XBlock from xblock.core import XBlock
from xblock.fields import Scope, String, UNIQUE_ID from xblock.fields import Scope, String, UNIQUE_ID
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xmodule.xml_module import XmlParserMixin from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin
from openedx.core.djangolib.markup import HTML, Text
from openedx.core.lib.xblock_builtin import get_css_dependencies, get_js_dependencies from openedx.core.lib.xblock_builtin import get_css_dependencies, get_js_dependencies
from xmodule.raw_module import RawDescriptor
from xmodule.xml_module import XmlParserMixin
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
loader = ResourceLoader(__name__) # pylint: disable=invalid-name loader = ResourceLoader(__name__) # pylint: disable=invalid-name
...@@ -167,9 +167,29 @@ class DiscussionXBlock(XBlock, StudioEditableXBlockMixin, XmlParserMixin): ...@@ -167,9 +167,29 @@ class DiscussionXBlock(XBlock, StudioEditableXBlockMixin, XmlParserMixin):
self.add_resource_urls(fragment) self.add_resource_urls(fragment)
login_msg = ''
if not self.django_user.is_authenticated():
qs = urllib.urlencode({
'course_id': self.course_key,
'enrollment_action': 'enroll',
'email_opt_in': False,
})
login_msg = Text(_("You are not signed in. To view the discussion content, {sign_in_link} or "
"{register_link}, and enroll in this course.")).format(
sign_in_link=HTML('<a href="{url}">{sign_in_label}</a>').format(
sign_in_label=_('sign in'),
url='{}?{}'.format(reverse('signin_user'), qs),
),
register_link=HTML('<a href="/{url}">{register_label}</a>').format(
register_label=_('register'),
url='{}?{}'.format(reverse('register_user'), qs),
),
)
context = { context = {
'discussion_id': self.discussion_id, 'discussion_id': self.discussion_id,
'display_name': self.display_name if (self.display_name) else _("Discussion"), 'display_name': self.display_name if self.display_name else _("Discussion"),
'user': self.django_user, 'user': self.django_user,
'course_id': self.course_key, 'course_id': self.course_key,
'discussion_category': self.discussion_category, 'discussion_category': self.discussion_category,
...@@ -177,6 +197,7 @@ class DiscussionXBlock(XBlock, StudioEditableXBlockMixin, XmlParserMixin): ...@@ -177,6 +197,7 @@ class DiscussionXBlock(XBlock, StudioEditableXBlockMixin, XmlParserMixin):
'can_create_thread': self.has_permission("create_thread"), 'can_create_thread': self.has_permission("create_thread"),
'can_create_comment': self.has_permission("create_comment"), 'can_create_comment': self.has_permission("create_comment"),
'can_create_subcomment': self.has_permission("create_sub_comment"), 'can_create_subcomment': self.has_permission("create_sub_comment"),
'login_msg': login_msg,
} }
fragment.add_content(self.runtime.render_template('discussion/_discussion_inline.html', context)) fragment.add_content(self.runtime.render_template('discussion/_discussion_inline.html', context))
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment