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).
This commit is contained in:
committed by
Clinton Blackburn
parent
9a9ef198b2
commit
69eeca61d8
@@ -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
|
||||
"""Returns all the permissions the user has in the given course."""
|
||||
if not user.is_authenticated():
|
||||
return {}
|
||||
|
||||
course = modulestore().get_course(course_id)
|
||||
if course is None:
|
||||
raise ItemNotFoundError(course_id)
|
||||
|
||||
@@ -5,7 +5,7 @@ Utility library for working with the edx-milestones app
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
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.services import MilestonesService
|
||||
from opaque_keys import InvalidKeyError
|
||||
@@ -213,21 +213,32 @@ def get_required_content(course_key, user):
|
||||
"""
|
||||
required_content = []
|
||||
if settings.FEATURES.get('MILESTONES_APP'):
|
||||
# Get all of the outstanding milestones for this course, for this user
|
||||
try:
|
||||
milestone_paths = get_course_milestones_fulfillment_paths(
|
||||
unicode(course_key),
|
||||
serialize_user(user)
|
||||
)
|
||||
except InvalidMilestoneRelationshipTypeException:
|
||||
return required_content
|
||||
course_run_id = unicode(course_key)
|
||||
|
||||
if user.is_authenticated():
|
||||
# Get all of the outstanding milestones for this course, for this user
|
||||
try:
|
||||
|
||||
milestone_paths = get_course_milestones_fulfillment_paths(
|
||||
course_run_id,
|
||||
serialize_user(user)
|
||||
)
|
||||
except InvalidMilestoneRelationshipTypeException:
|
||||
return required_content
|
||||
|
||||
# For each outstanding milestone, see if this content is one of its fulfillment paths
|
||||
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.')
|
||||
|
||||
# For each outstanding milestone, see if this content is one of its fulfillment paths
|
||||
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)
|
||||
return required_content
|
||||
|
||||
|
||||
|
||||
@@ -3,15 +3,17 @@ Tests for the milestones helpers library, which is the integration point for the
|
||||
"""
|
||||
|
||||
import ddt
|
||||
import pytest
|
||||
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.models import MilestoneRelationshipType
|
||||
from mock import patch
|
||||
|
||||
from milestones import api as milestones_api
|
||||
from milestones.models import MilestoneRelationshipType
|
||||
from util import milestones_helpers
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from util import milestones_helpers
|
||||
|
||||
|
||||
@patch.dict(settings.FEATURES, {'MILESTONES_APP': False})
|
||||
@@ -134,3 +136,20 @@ class MilestonesHelpersTestCase(ModuleStoreTestCase):
|
||||
milestones_helpers.any_unfulfilled_milestones(None, self.user['id'])
|
||||
with self.assertRaises(InvalidUserException):
|
||||
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
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
import collections
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
from pkg_resources import resource_string
|
||||
from pytz import UTC
|
||||
from datetime import datetime
|
||||
|
||||
from lxml import etree
|
||||
from pkg_resources import resource_string
|
||||
from pytz import UTC
|
||||
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 .exceptions import NotFoundError
|
||||
from .fields import Date
|
||||
from .mako_module import MakoModuleDescriptor
|
||||
from .progress import Progress
|
||||
from .x_module import XModule, STUDENT_VIEW
|
||||
from .x_module import STUDENT_VIEW, XModule
|
||||
from .xml_module import XmlDescriptor
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -292,6 +292,10 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
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):
|
||||
"""
|
||||
Returns the rendered student view of the content of this
|
||||
@@ -312,6 +316,7 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
'next_url': context.get('next_url'),
|
||||
'prev_url': context.get('prev_url'),
|
||||
'banner_text': banner_text,
|
||||
'disable_navigation': not self.is_user_authenticated(context),
|
||||
}
|
||||
fragment.add_content(self.system.render_template("seq_module.html", params))
|
||||
|
||||
@@ -325,6 +330,11 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
Update the user's sequential position given the context and the
|
||||
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,
|
||||
# or exceeds the length of the displayable items,
|
||||
# default the position to the first element
|
||||
@@ -341,16 +351,36 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
display_items. Returns a list of dict objects with information about
|
||||
the given display_items.
|
||||
"""
|
||||
bookmarks_service = self.runtime.service(self, "bookmarks")
|
||||
context["username"] = self.runtime.service(self, "user").get_current_user().opt_attrs['edx-platform.username']
|
||||
is_user_authenticated = self.is_user_authenticated(context)
|
||||
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 = [
|
||||
self.get_parent().display_name_with_default,
|
||||
self.display_name_with_default
|
||||
]
|
||||
contents = []
|
||||
for item in display_items:
|
||||
is_bookmarked = bookmarks_service.is_bookmarked(usage_key=item.scope_ids.usage_id)
|
||||
context["bookmarked"] = is_bookmarked
|
||||
# NOTE (CCB): This seems like a hack, but I don't see a better method of determining the type/category.
|
||||
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)
|
||||
fragment.add_frag_resources(rendered_item)
|
||||
@@ -358,8 +388,8 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
iteminfo = {
|
||||
'content': rendered_item.content,
|
||||
'page_title': getattr(item, 'tooltip_title', ''),
|
||||
'type': item.get_icon_class(),
|
||||
'id': item.scope_ids.usage_id.to_deprecated_string(),
|
||||
'type': item_type,
|
||||
'id': usage_id.to_deprecated_string(),
|
||||
'bookmarked': is_bookmarked,
|
||||
'path': " > ".join(display_names + [item.display_name_with_default]),
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ from django.core.cache import cache
|
||||
from django.template.context_processors import csrf
|
||||
from django.core.exceptions import PermissionDenied
|
||||
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 edx_proctoring.services import ProctoringService
|
||||
from opaque_keys import InvalidKeyError
|
||||
@@ -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.
|
||||
|
||||
Arguments:
|
||||
request (Request): Django request.
|
||||
course_id (str): Course containing the block
|
||||
usage_id (str)
|
||||
handler (str)
|
||||
suffix (str)
|
||||
|
||||
- request -- the django request.
|
||||
- location -- the module location. Used to look up the XModule instance
|
||||
- course_id -- defines the course context for this request.
|
||||
|
||||
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
|
||||
not accessible by the user, or the module raises NotFoundError. If the
|
||||
module raises any other error, it will escape this function.
|
||||
Raises:
|
||||
Http404: If the course is not found in the modulestore.
|
||||
"""
|
||||
if not request.user.is_authenticated():
|
||||
return HttpResponse('Unauthenticated', status=403)
|
||||
# NOTE (CCB): Allow anonymous GET calls (e.g. for transcripts). Modifying this view is simpler than updating
|
||||
# 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:
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
except InvalidKeyError:
|
||||
raise Http404("Invalid location")
|
||||
raise Http404('{} is not a valid course key'.format(course_id))
|
||||
|
||||
with modulestore().bulk_operations(course_key):
|
||||
try:
|
||||
course = modulestore().get_course(course_key)
|
||||
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)
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ class TestDiscussionXBlock(XModuleRenderingTestBase):
|
||||
self.block.xmodule_runtime = mock.Mock()
|
||||
|
||||
if self.PATCH_DJANGO_USER:
|
||||
self.django_user_canary = object()
|
||||
self.django_user_canary = UserFactory()
|
||||
self.django_user_mock = self.add_patcher(
|
||||
mock.patch.object(DiscussionXBlock, "django_user", new_callable=mock.PropertyMock)
|
||||
)
|
||||
@@ -259,7 +259,7 @@ class TestXBlockInCourse(SharedModuleStoreTestCase):
|
||||
Set up a user, course, and discussion XBlock for use by tests.
|
||||
"""
|
||||
super(TestXBlockInCourse, cls).setUpClass()
|
||||
cls.user = UserFactory.create()
|
||||
cls.user = UserFactory()
|
||||
cls.course = ToyCourseFactory.create()
|
||||
cls.course_key = cls.course.id
|
||||
cls.course_usage_key = cls.store.make_course_usage_key(cls.course_key)
|
||||
@@ -380,8 +380,8 @@ class TestXBlockQueryLoad(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Tests that the permissions queries are cached when rendering numerous discussion XBlocks.
|
||||
"""
|
||||
user = UserFactory.create()
|
||||
course = ToyCourseFactory.create()
|
||||
user = UserFactory()
|
||||
course = ToyCourseFactory()
|
||||
course_key = course.id
|
||||
course_usage_key = self.store.make_course_usage_key(course_key)
|
||||
discussions = []
|
||||
|
||||
@@ -290,7 +290,6 @@ class ModuleRenderTestCase(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
)
|
||||
response = self.client.post(dispatch_url, {'position': 2})
|
||||
self.assertEquals(403, response.status_code)
|
||||
self.assertEquals('Unauthenticated', response.content)
|
||||
|
||||
def test_missing_position_handler(self):
|
||||
"""
|
||||
|
||||
@@ -11,10 +11,23 @@ from urllib import quote, urlencode
|
||||
from uuid import uuid4
|
||||
|
||||
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 milestones.tests.utils import MilestonesTestCaseMixin
|
||||
from mock import MagicMock, PropertyMock, create_autospec, patch
|
||||
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 xblock.core import XBlock
|
||||
from xblock.fields import Scope, String
|
||||
from xblock.fragment import Fragment
|
||||
|
||||
import courseware.views.views as views
|
||||
import shoppingcart
|
||||
@@ -32,19 +45,9 @@ from courseware.tests.factories import GlobalStaffFactory, StudentModuleFactory
|
||||
from courseware.testutils import RenderXBlockTestMixin
|
||||
from courseware.url_helpers import get_redirect_url
|
||||
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.grades.config.waffle import waffle as grades_waffle
|
||||
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 CourseRunFactory, ProgramFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
@@ -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.models import CreditCourse, CreditProvider
|
||||
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.djangolib.testing.utils import get_mock_request
|
||||
from openedx.core.lib.gating import api as gating_api
|
||||
from openedx.features.course_experience import COURSE_OUTLINE_PAGE_FLAG
|
||||
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired
|
||||
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.url import reload_django_url_config
|
||||
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.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -2193,6 +2194,7 @@ class TestIndexView(ModuleStoreTestCase):
|
||||
"""
|
||||
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')
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@@ -2262,6 +2264,28 @@ class TestIndexView(ModuleStoreTestCase):
|
||||
)
|
||||
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
|
||||
class TestIndexViewWithVerticalPositions(ModuleStoreTestCase):
|
||||
|
||||
@@ -8,12 +8,14 @@ import logging
|
||||
import urllib
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
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.http import Http404
|
||||
from django.template.context_processors import csrf
|
||||
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.csrf import ensure_csrf_cookie
|
||||
from django.views.generic import View
|
||||
@@ -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.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.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.views.course_sock import CourseSockFragmentView
|
||||
from openedx.features.enterprise_support.api import data_sharing_consent_required
|
||||
from shoppingcart.models import CourseRegistrationCode
|
||||
from student.views import is_course_blocked
|
||||
from student.models import CourseEnrollment
|
||||
from util.views import ensure_valid_course_key
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.x_module import STUDENT_VIEW
|
||||
|
||||
from .views import CourseTabView
|
||||
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 ..entrance_exams import (
|
||||
course_has_entrance_exam,
|
||||
@@ -52,9 +55,6 @@ from ..entrance_exams import (
|
||||
from ..masquerade import setup_masquerade
|
||||
from ..model_data import FieldDataCache
|
||||
from ..module_render import get_module_for_descriptor, toc_for_course
|
||||
from .views import (
|
||||
CourseTabView,
|
||||
)
|
||||
|
||||
log = logging.getLogger("edx.courseware.views.index")
|
||||
|
||||
@@ -66,7 +66,12 @@ class CoursewareIndex(View):
|
||||
"""
|
||||
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(cache_control(no_cache=True, no_store=True, must_revalidate=True))
|
||||
@method_decorator(ensure_valid_course_key)
|
||||
@@ -91,7 +96,10 @@ class CoursewareIndex(View):
|
||||
position (unicode): position in module, eg of <sequential> module
|
||||
"""
|
||||
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_section_url_name = section
|
||||
self.chapter_url_name = chapter
|
||||
@@ -108,11 +116,11 @@ class CoursewareIndex(View):
|
||||
self.course = get_course_with_access(
|
||||
request.user, 'load', self.course_key,
|
||||
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._setup_masquerade_for_effective_user()
|
||||
return self._get(request)
|
||||
return self.render(request)
|
||||
except Exception as exception: # pylint: disable=broad-except
|
||||
return CourseTabView.handle_exceptions(request, self.course, exception)
|
||||
|
||||
@@ -131,7 +139,7 @@ class CoursewareIndex(View):
|
||||
# Set the user in the request to the effective user.
|
||||
self.request.user = self.effective_user
|
||||
|
||||
def _get(self, request):
|
||||
def render(self, request):
|
||||
"""
|
||||
Render the index page.
|
||||
"""
|
||||
@@ -148,6 +156,28 @@ class CoursewareIndex(View):
|
||||
self._save_positions()
|
||||
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))
|
||||
|
||||
def _redirect_if_not_requested_section(self):
|
||||
@@ -186,15 +216,20 @@ class CoursewareIndex(View):
|
||||
"""
|
||||
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 = CourseRegistrationCode.objects.filter(
|
||||
course_id=self.course_key,
|
||||
registrationcoderedemption__redeemed_by=self.real_user
|
||||
)
|
||||
redeemed_registration_codes = []
|
||||
|
||||
if self.request.user.is_authenticated():
|
||||
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):
|
||||
# registration codes may be generated via Bulk Purchase Scenario
|
||||
# we have to check only for the invoice generated registration codes
|
||||
# that their invoice is valid or not
|
||||
# TODO Update message to account for the fact that the user is not authenticated.
|
||||
log.warning(
|
||||
u'User %s cannot access the course %s because payment has not yet been received',
|
||||
self.real_user,
|
||||
@@ -218,9 +253,11 @@ class CoursewareIndex(View):
|
||||
"""
|
||||
Returns the preferred language for the actual user making the request.
|
||||
"""
|
||||
language_preference = get_user_preference(self.real_user, LANGUAGE_KEY)
|
||||
if not language_preference:
|
||||
language_preference = settings.LANGUAGE_CODE
|
||||
language_preference = settings.LANGUAGE_CODE
|
||||
|
||||
if self.request.user.is_authenticated():
|
||||
language_preference = get_user_preference(self.real_user, LANGUAGE_KEY)
|
||||
|
||||
return language_preference
|
||||
|
||||
def _is_masquerading_as_student(self):
|
||||
@@ -445,10 +482,15 @@ class CoursewareIndex(View):
|
||||
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 = {
|
||||
'activate_block_id': self.request.GET.get('activate_block_id'),
|
||||
'requested_child': self.request.GET.get("child"),
|
||||
'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:
|
||||
section_context['prev_url'] = _compute_section_url(previous_of_active_section, 'last')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
$(document).ajaxError(function(event, jXHR) {
|
||||
if (jXHR.status === 403 && jXHR.responseText === 'Unauthenticated') {
|
||||
if (jXHR.status === 403) {
|
||||
var message = gettext(
|
||||
'You have been logged out of your edX account. ' +
|
||||
'Click Okay to log in again now. ' +
|
||||
|
||||
@@ -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}";
|
||||
</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())}
|
||||
|
||||
</%block>
|
||||
|
||||
@@ -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-subcomment="${json_dumps(can_create_subcomment)}"
|
||||
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">
|
||||
<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}
|
||||
@@ -21,9 +30,12 @@ from openedx.core.djangolib.js_utils import js_escaped_string
|
||||
%endif
|
||||
</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>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
var $$course_id = "${course_id | n, js_escaped_string}";
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
data-element="${idx+1}"
|
||||
data-page-title="${item['page_title']}"
|
||||
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="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']} </span>${item['page_title']}<span class="sr bookmark-icon-sr"> ${_("Bookmarked") if item['bookmarked'] else ""}</span></div>
|
||||
|
||||
@@ -60,17 +60,20 @@ def get_bookmarks(user, course_key=None, fields=None, serialized=True):
|
||||
Returns:
|
||||
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:
|
||||
bookmarks_queryset = bookmarks_queryset.filter(course_key=course_key)
|
||||
if course_key:
|
||||
bookmarks_queryset = bookmarks_queryset.filter(course_key=course_key)
|
||||
|
||||
if len(set(fields or []) & set(OPTIONAL_FIELDS)) > 0:
|
||||
bookmarks_queryset = bookmarks_queryset.select_related('user', 'xblock_cache')
|
||||
if len(set(fields or []) & set(OPTIONAL_FIELDS)) > 0:
|
||||
bookmarks_queryset = bookmarks_queryset.select_related('user', 'xblock_cache')
|
||||
else:
|
||||
bookmarks_queryset = bookmarks_queryset.select_related('user')
|
||||
|
||||
bookmarks_queryset = bookmarks_queryset.order_by('-created')
|
||||
else:
|
||||
bookmarks_queryset = bookmarks_queryset.select_related('user')
|
||||
|
||||
bookmarks_queryset = bookmarks_queryset.order_by('-created')
|
||||
bookmarks_queryset = Bookmark.objects.none()
|
||||
|
||||
if serialized:
|
||||
return BookmarkSerializer(bookmarks_queryset, context={'fields': fields}, many=True).data
|
||||
|
||||
@@ -3,21 +3,21 @@
|
||||
Discussion XBlock
|
||||
"""
|
||||
import logging
|
||||
import urllib
|
||||
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
from django.core.urlresolvers import reverse
|
||||
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.fields import Scope, String, UNIQUE_ID
|
||||
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 xmodule.raw_module import RawDescriptor
|
||||
from xmodule.xml_module import XmlParserMixin
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
loader = ResourceLoader(__name__) # pylint: disable=invalid-name
|
||||
@@ -167,9 +167,29 @@ class DiscussionXBlock(XBlock, StudioEditableXBlockMixin, XmlParserMixin):
|
||||
|
||||
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 = {
|
||||
'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,
|
||||
'course_id': self.course_key,
|
||||
'discussion_category': self.discussion_category,
|
||||
@@ -177,6 +197,7 @@ class DiscussionXBlock(XBlock, StudioEditableXBlockMixin, XmlParserMixin):
|
||||
'can_create_thread': self.has_permission("create_thread"),
|
||||
'can_create_comment': self.has_permission("create_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))
|
||||
|
||||
Reference in New Issue
Block a user