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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user