From d8eca3f299c15bca27464107ef98284b764bd8f0 Mon Sep 17 00:00:00 2001 From: hasnain-naveed Date: Sun, 12 Aug 2018 13:10:48 +0500 Subject: [PATCH] WL-1687 | Added journals access in xblock rendering --- openedx/features/journals/api.py | 13 +-- .../journals/tests/test_journal_xblock.py | 85 +++++++++++++++++++ openedx/features/journals/tests/utils.py | 4 +- openedx/features/journals/urls.py | 6 ++ .../features/journals/views/journal_xblock.py | 66 ++++++++++++++ 5 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 openedx/features/journals/tests/test_journal_xblock.py create mode 100644 openedx/features/journals/views/journal_xblock.py diff --git a/openedx/features/journals/api.py b/openedx/features/journals/api.py index 7e2c4fec87..77baee101a 100644 --- a/openedx/features/journals/api.py +++ b/openedx/features/journals/api.py @@ -121,7 +121,7 @@ class JournalsApiClient(object): return User.objects.get(username=JOURNAL_WORKER_USERNAME) -def fetch_journal_access(site, user): # pylint: disable=unused-argument +def fetch_journal_access(site, user, block_id=None): # pylint: disable=unused-argument """ Retrieve journal access record for given user. Retrieve if from the cache if present, otherwise send GET request to the journal access api @@ -143,10 +143,13 @@ def fetch_journal_access(site, user): # pylint: disable=unused-argument # TODO: WL-1560: # LMS should cache responses from Journal Access API # Need strategy for updating cache when new purchase happens - journal_access_records = JournalsApiClient().client.journalaccess.get( - user=user, - get_latest=True - ) + endpoint_params = { + "user": user, + "get_latest": True, + } + if block_id: + endpoint_params['block_id'] = block_id + journal_access_records = JournalsApiClient().client.journalaccess.get(**endpoint_params) return journal_access_records.get('results', []) except ValueError: return [] diff --git a/openedx/features/journals/tests/test_journal_xblock.py b/openedx/features/journals/tests/test_journal_xblock.py new file mode 100644 index 0000000000..4a4d819dac --- /dev/null +++ b/openedx/features/journals/tests/test_journal_xblock.py @@ -0,0 +1,85 @@ +""" +Test cases for journal page views. +""" + +import uuid +import mock + +from django.conf import settings +from django.core.urlresolvers import reverse +from django.http import HttpResponse + +from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase +from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin +from openedx.features.journals.tests.utils import ( + get_mocked_journal_access, + override_switch +) +from openedx.features.journals.api import JOURNAL_INTEGRATION + + +@mock.patch.dict(settings.FEATURES, {"JOURNALS_ENABLED": True}) +class RenderXblockByJournalAccessViewTest(LoginEnrollmentTestCase, CacheIsolationTestCase, SiteMixin): + """ Tests for views responsible for rendering xblock in journals """ + + def setUp(self): + super(RenderXblockByJournalAccessViewTest, self).setUp() + self.setup_user() + self.path = reverse( + "openedx.journals.render_xblock_by_journal_access", + kwargs={ + "usage_key_string": "block-v1:edX+DemoX+Demo_Course+type@video+block@5c90cffecd9b48b188cbfea176bf7fe9" + } + ) + + @override_switch(JOURNAL_INTEGRATION, True) + @mock.patch('openedx.features.journals.views.journal_xblock.fetch_journal_access') + @mock.patch('openedx.features.journals.views.journal_xblock.render_xblock') + def test_without_journal_access(self, mocked_render_xblock, mocked_journal_access): + """ + Test the journal page without journal access. + """ + mocked_journal_access.return_value = [] + mocked_render_xblock.return_value = [] + path = "{path}?journal_uuid={journal_uuid}".format( + path=self.path, + journal_uuid=str(uuid.uuid4()) + ) + response = self.client.get(path=path) + self.assertEqual(response.status_code, 403) + + @override_switch(JOURNAL_INTEGRATION, True) + @mock.patch('openedx.features.journals.views.journal_xblock.fetch_journal_access') + @mock.patch('openedx.features.journals.views.journal_xblock.render_xblock') + def test_unauthenticated_journal_access(self, mocked_render_xblock, mocked_journal_access): + """ + Test when not logged in + """ + self.logout() + mocked_journal_access.return_value = [] + mocked_render_xblock.return_value = [] + path = "{path}?journal_uuid={journal_uuid}".format( + path=self.path, + journal_uuid=str(uuid.uuid4()) + ) + response = self.client.get(path=path) + self.assertEqual(response.status_code, 403) + + @override_switch(JOURNAL_INTEGRATION, True) + @mock.patch('openedx.features.journals.views.journal_xblock.fetch_journal_access') + @mock.patch('openedx.features.journals.views.journal_xblock.render_xblock') + def test_with_journal_access(self, mocked_render_xblock, mocked_journal_access): + """ + Test the journal page with journal access. + """ + journal_uuid = str(uuid.uuid4()) + mocked_journal_access.return_value = get_mocked_journal_access(journal_uuid=journal_uuid) + mocked_render_xblock.return_value = HttpResponse("") + path = "{path}?journal_uuid={journal_uuid}".format( + path=self.path, + journal_uuid=journal_uuid + ) + response = self.client.get(path=path) + self.assertEqual(response.status_code, 200) + mocked_render_xblock.assert_called_once() diff --git a/openedx/features/journals/tests/utils.py b/openedx/features/journals/tests/utils.py index 52a8bb5036..2533df73af 100644 --- a/openedx/features/journals/tests/utils.py +++ b/openedx/features/journals/tests/utils.py @@ -25,7 +25,7 @@ def override_switch(switch, active): return decorate -def get_mocked_journal_access(): +def get_mocked_journal_access(journal_uuid=None): """ Returns the dummy data of journal access """ @@ -35,6 +35,7 @@ def get_mocked_journal_access(): "uuid": uuid.uuid4(), "journal": { "name": "dummy-name1", + "uuid": journal_uuid if journal_uuid else str(uuid.uuid4()), "organization": "edx", "journalaboutpage": { "id": "5", @@ -47,6 +48,7 @@ def get_mocked_journal_access(): "uuid": uuid.uuid4(), "journal": { "name": "dummy-name2", + "uuid": str(uuid.uuid4()), "organization": "edx", "journalaboutpage": { "id": "5", diff --git a/openedx/features/journals/urls.py b/openedx/features/journals/urls.py index 98b2ca5f13..abf4749779 100644 --- a/openedx/features/journals/urls.py +++ b/openedx/features/journals/urls.py @@ -2,10 +2,12 @@ Defines URLs for course bookmarks. """ +from django.conf import settings from django.conf.urls import url from openedx.features.journals.views.marketing import bundle_about from openedx.features.journals.views import learner_dashboard +from openedx.features.journals.views.journal_xblock import render_xblock_by_journal_access urlpatterns = [ url(r'^bundles/{}/about'.format(r'(?P[0-9a-f-]+)',), @@ -16,4 +18,8 @@ urlpatterns = [ learner_dashboard.journal_listing, name='openedx.journals.dashboard' ), + url(r'^render_journal_block/{usage_key_string}'.format(usage_key_string=settings.USAGE_KEY_PATTERN), + render_xblock_by_journal_access, + name='openedx.journals.render_xblock_by_journal_access' + ), ] diff --git a/openedx/features/journals/views/journal_xblock.py b/openedx/features/journals/views/journal_xblock.py new file mode 100644 index 0000000000..98abb78cfc --- /dev/null +++ b/openedx/features/journals/views/journal_xblock.py @@ -0,0 +1,66 @@ +""" +View for journal page +""" +import datetime + +from django.core.cache import cache +from django.core.exceptions import PermissionDenied + +from lms.djangoapps.courseware.views.views import render_xblock +from opaque_keys.edx.keys import UsageKey +from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers +from openedx.features.journals.api import fetch_journal_access + +XBLOCK_JOURNAL_ACCESS_KEY = "journal_access_for_{username}_{journal_uuid}_{block_id}" + + +def render_xblock_by_journal_access(request, usage_key_string): + """ + Its a wrapper function for lms.djangoapps.courseware.views.views.render_xblock. + It disables 'check_if_enrolled' flag by checking that user has access on journal. + """ + block_id = UsageKey.from_string(usage_key_string).block_id + user_access = _get_cache_data(request, block_id) + if not user_access: + raise PermissionDenied() + return render_xblock(request, usage_key_string, check_if_enrolled=False) + + +def _get_cache_data(request, block_id): + """ + Get the cache data from cache if not then hit the end point + in journals to fetch the access of user on given block_id. + """ + if request.user.is_staff: + return True + + if not request.user.is_authenticated: + return False + + date_format = '%Y-%m-%d' + journal_uuid = request.GET.get('journal_uuid') + cache_key = XBLOCK_JOURNAL_ACCESS_KEY.format( + username=request.user.username, + journal_uuid=journal_uuid, + block_id=block_id + ) + user_access = cache.get(cache_key) + if user_access is None: + journal_access_data = fetch_journal_access( + request.site, + request.user, + block_id=block_id + ) + for journal_access in journal_access_data: + if journal_access['journal']['uuid'] == journal_uuid: + expiration_date = datetime.datetime.strptime(journal_access['expiration_date'], date_format) + now = datetime.datetime.strptime(datetime.datetime.now().strftime(date_format), date_format) + if expiration_date >= now: + user_access = True + + cache.set( + cache_key, + user_access, + configuration_helpers.get_value("JOURNAL_ACCESS_CACHE_TTL", 3600) + ) + return user_access