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):
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
# 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)
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.')
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 -- 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.
request (Request): Django request.
course_id (str): Course containing the block
usage_id (str)
handler (str)
suffix (str)
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')
......
$(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']}&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):
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')
else:
bookmarks_queryset = bookmarks_queryset.select_related('user')
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')
bookmarks_queryset = bookmarks_queryset.order_by('-created')
else:
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))
......
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