diff --git a/lms/djangoapps/course_blocks/management/commands/generate_course_blocks.py b/lms/djangoapps/course_blocks/management/commands/generate_course_blocks.py index 590576c9d2..58f6155b30 100644 --- a/lms/djangoapps/course_blocks/management/commands/generate_course_blocks.py +++ b/lms/djangoapps/course_blocks/management/commands/generate_course_blocks.py @@ -8,7 +8,7 @@ from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey from xmodule.modulestore.django import modulestore -from ...api import get_course_in_cache +from ...api import get_course_in_cache, update_course_in_cache log = logging.getLogger(__name__) @@ -39,6 +39,12 @@ class Command(BaseCommand): action='store_true', default=False, ) + parser.add_argument( + '--force', + help='Force update of the course blocks for the requested courses.', + action='store_true', + default=False, + ) def handle(self, *args, **options): @@ -57,7 +63,10 @@ class Command(BaseCommand): for course_key in course_keys: try: - block_structure = get_course_in_cache(course_key) + if options.get('force'): + block_structure = update_course_in_cache(course_key) + else: + block_structure = get_course_in_cache(course_key) if options.get('dags'): self._find_and_log_dags(block_structure, course_key) except Exception as ex: # pylint: disable=broad-except diff --git a/lms/djangoapps/course_blocks/management/commands/tests/test_generate_course_blocks.py b/lms/djangoapps/course_blocks/management/commands/tests/test_generate_course_blocks.py index c0459a5e21..a5ecf5ee38 100644 --- a/lms/djangoapps/course_blocks/management/commands/tests/test_generate_course_blocks.py +++ b/lms/djangoapps/course_blocks/management/commands/tests/test_generate_course_blocks.py @@ -44,6 +44,21 @@ class TestGenerateCourseBlocks(ModuleStoreTestCase): self._assert_courses_not_in_block_cache(self.course_1.id, self.course_2.id) self.command.handle(all=True) self._assert_courses_in_block_cache(self.course_1.id, self.course_2.id) + with patch( + 'openedx.core.lib.block_structure.factory.BlockStructureFactory.create_from_modulestore' + ) as mock_update_from_store: + self.command.handle(all=True) + mock_update_from_store.assert_not_called() + + def test_generate_force(self): + self._assert_courses_not_in_block_cache(self.course_1.id, self.course_2.id) + self.command.handle(all=True) + self._assert_courses_in_block_cache(self.course_1.id, self.course_2.id) + with patch( + 'openedx.core.lib.block_structure.factory.BlockStructureFactory.create_from_modulestore' + ) as mock_update_from_store: + self.command.handle(all=True, force=True) + mock_update_from_store.assert_called() def test_generate_one(self): self._assert_courses_not_in_block_cache(self.course_1.id, self.course_2.id) diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py index c589a6bedc..c0fd9daad0 100644 --- a/lms/djangoapps/courseware/date_summary.py +++ b/lms/djangoapps/courseware/date_summary.py @@ -111,7 +111,7 @@ class DateSummary(object): date_format = _(u"{relative} ago - {absolute}") if date_has_passed else _(u"in {relative} - {absolute}") return date_format.format( relative=relative_date, - absolute=self.date.strftime(self.date_format), + absolute=self.date.strftime(self.date_format.encode('utf-8')).decode('utf-8'), ) @property @@ -157,7 +157,9 @@ class TodaysDate(DateSummary): @property def title(self): - return _(u'Today is {date}').format(date=datetime.now(pytz.UTC).strftime(self.date_format)) + return _(u'Today is {date}').format( + date=datetime.now(pytz.UTC).strftime(self.date_format.encode('utf-8')).decode('utf-8') + ) class CourseStartDate(DateSummary): diff --git a/openedx/core/djangoapps/api_admin/utils.py b/openedx/core/djangoapps/api_admin/utils.py index 57393b3108..a19eabf98c 100644 --- a/openedx/core/djangoapps/api_admin/utils.py +++ b/openedx/core/djangoapps/api_admin/utils.py @@ -1,19 +1,53 @@ """ Course Discovery API Service. """ -from django.conf import settings +import datetime +from django.conf import settings from edx_rest_api_client.client import EdxRestApiClient +import jwt + from openedx.core.djangoapps.theming import helpers -from openedx.core.lib.token_utils import get_id_token from provider.oauth2.models import Client +from student.models import UserProfile, anonymous_id_for_user CLIENT_NAME = 'course-discovery' +def get_id_token(user): + """ + Return a JWT for `user`, suitable for use with the course discovery service. + + Arguments: + user (User): User for whom to generate the JWT. + + Returns: + str: The JWT. + """ + try: + # Service users may not have user profiles. + full_name = UserProfile.objects.get(user=user).name + except UserProfile.DoesNotExist: + full_name = None + + now = datetime.datetime.utcnow() + expires_in = getattr(settings, 'OAUTH_ID_TOKEN_EXPIRATION', 30) + + payload = { + 'preferred_username': user.username, + 'name': full_name, + 'email': user.email, + 'administrator': user.is_staff, + 'iss': helpers.get_value('OAUTH_OIDC_ISSUER', settings.OAUTH_OIDC_ISSUER), + 'exp': now + datetime.timedelta(seconds=expires_in), + 'iat': now, + 'aud': helpers.get_value('JWT_AUTH', settings.JWT_AUTH)['JWT_AUDIENCE'], + 'sub': anonymous_id_for_user(user, None), + } + secret_key = helpers.get_value('JWT_AUTH', settings.JWT_AUTH)['JWT_SECRET_KEY'] + + return jwt.encode(payload, secret_key) + + def course_discovery_api_client(user): """ Returns a Course Discovery API client setup with authentication for the specified user. """ course_discovery_client = Client.objects.get(name=CLIENT_NAME) - secret_key = helpers.get_value('JWT_AUTH', settings.JWT_AUTH)['JWT_SECRET_KEY'] - return EdxRestApiClient( - course_discovery_client.url, - jwt=get_id_token(user, CLIENT_NAME, secret_key=secret_key) - ) + return EdxRestApiClient(course_discovery_client.url, jwt=get_id_token(user)) diff --git a/openedx/core/djangoapps/content/course_structures/tasks.py b/openedx/core/djangoapps/content/course_structures/tasks.py index aa56ff01cf..551be7d04c 100644 --- a/openedx/core/djangoapps/content/course_structures/tasks.py +++ b/openedx/core/djangoapps/content/course_structures/tasks.py @@ -96,3 +96,13 @@ def update_course_structure(course_key): structure_model.structure_json = structure_json structure_model.discussion_id_map_json = discussion_id_map_json structure_model.save() + + # TODO (TNL-4630) For temporary hotfix to update the block_structure cache. + # Should be moved to proper location. + from django.core.cache import cache + from openedx.core.lib.block_structure.manager import BlockStructureManager + + store = modulestore() + course_usage_key = store.make_course_usage_key(course_key) + block_structure_manager = BlockStructureManager(course_usage_key, store, cache) + block_structure_manager.update_collected() diff --git a/openedx/core/lib/block_structure/cache.py b/openedx/core/lib/block_structure/cache.py index fda6e04fc0..57a483da02 100644 --- a/openedx/core/lib/block_structure/cache.py +++ b/openedx/core/lib/block_structure/cache.py @@ -45,13 +45,13 @@ class BlockStructureCache(object): ) zp_data_to_cache = zpickle(data_to_cache) - # Set the timeout value for the cache to None. This caches the - # value forever. The expectation is that the caller will delete - # the cached value once it is outdated. + # Set the timeout value for the cache to 1 day as a fail-safe + # in case the signal to invalidate the cache doesn't come through. + timeout_in_seconds = 60 * 60 * 24 self._cache.set( self._encode_root_cache_key(block_structure.root_block_usage_key), zp_data_to_cache, - timeout=None, + timeout=timeout_in_seconds, ) logger.info( diff --git a/openedx/core/lib/block_structure/tests/test_cache.py b/openedx/core/lib/block_structure/tests/test_cache.py index 8d3bc3b620..fce8094cd9 100644 --- a/openedx/core/lib/block_structure/tests/test_cache.py +++ b/openedx/core/lib/block_structure/tests/test_cache.py @@ -36,7 +36,7 @@ class TestBlockStructureCache(ChildrenMapTestMixin, TestCase): self.add_transformers() self.block_structure_cache.add(self.block_structure) - self.assertEquals(self.mock_cache.timeout_from_last_call, None) + self.assertEquals(self.mock_cache.timeout_from_last_call, 60 * 60 * 24) cached_value = self.block_structure_cache.get(self.block_structure.root_block_usage_key) self.assertIsNotNone(cached_value)