Commit 06cd352e by Awais Qureshi

Merge pull request #10596 from edx/awais786/ECOM-2832-cache-programs-data

Awais786/ecom 2832 cache programs data
parents 244b8ab9 86c66f55
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'ProgramsApiConfig.cache_ttl'
db.add_column('programs_programsapiconfig', 'cache_ttl',
self.gf('django.db.models.fields.PositiveIntegerField')(default=0),
keep_default=False)
def backwards(self, orm):
# Deleting field 'ProgramsApiConfig.cache_ttl'
db.delete_column('programs_programsapiconfig', 'cache_ttl')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'programs.programsapiconfig': {
'Meta': {'ordering': "('-change_date',)", 'object_name': 'ProgramsApiConfig'},
'api_version_number': ('django.db.models.fields.IntegerField', [], {}),
'cache_ttl': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'enable_student_dashboard': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'internal_service_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
'public_service_url': ('django.db.models.fields.URLField', [], {'max_length': '200'})
}
}
complete_apps = ['programs']
...@@ -6,6 +6,7 @@ from urlparse import urljoin ...@@ -6,6 +6,7 @@ from urlparse import urljoin
from django.db.models import BooleanField, IntegerField, URLField from django.db.models import BooleanField, IntegerField, URLField
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db import models
from config_models.models import ConfigurationModel from config_models.models import ConfigurationModel
...@@ -20,6 +21,15 @@ class ProgramsApiConfig(ConfigurationModel): ...@@ -20,6 +21,15 @@ class ProgramsApiConfig(ConfigurationModel):
public_service_url = URLField(verbose_name=_("Public Service URL")) public_service_url = URLField(verbose_name=_("Public Service URL"))
api_version_number = IntegerField(verbose_name=_("API Version")) api_version_number = IntegerField(verbose_name=_("API Version"))
enable_student_dashboard = BooleanField(verbose_name=_("Enable Student Dashboard Displays")) enable_student_dashboard = BooleanField(verbose_name=_("Enable Student Dashboard Displays"))
cache_ttl = models.PositiveIntegerField(
verbose_name=_("Cache Time To Live"),
default=0,
help_text=_(
"Specified in seconds. Enable caching by setting this to a value greater than 0."
)
)
PROGRAMS_API_CACHE_KEY = "programs.api.data"
@property @property
def internal_api_url(self): def internal_api_url(self):
...@@ -42,3 +52,8 @@ class ProgramsApiConfig(ConfigurationModel): ...@@ -42,3 +52,8 @@ class ProgramsApiConfig(ConfigurationModel):
be enabled or not. be enabled or not.
""" """
return self.enabled and self.enable_student_dashboard return self.enabled and self.enable_student_dashboard
@property
def is_cache_enabled(self):
"""Whether responses from the Programs API will be cached."""
return self.enabled and self.cache_ttl > 0
""" """
Tests for models supporting Program-related functionality. Tests for models supporting Program-related functionality.
""" """
import ddt
from mock import patch from mock import patch
from django.test import TestCase from django.test import TestCase
from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
@ddt.ddt
@patch('config_models.models.cache.get', return_value=None) # during tests, make every cache get a miss. @patch('config_models.models.cache.get', return_value=None) # during tests, make every cache get a miss.
class ProgramsApiConfigTest(ProgramsApiConfigMixin, TestCase): class ProgramsApiConfigTest(ProgramsApiConfigMixin, TestCase):
""" """
...@@ -70,3 +70,28 @@ class ProgramsApiConfigTest(ProgramsApiConfigMixin, TestCase): ...@@ -70,3 +70,28 @@ class ProgramsApiConfigTest(ProgramsApiConfigMixin, TestCase):
self.create_config(enabled=True, enable_student_dashboard=True) self.create_config(enabled=True, enable_student_dashboard=True)
self.assertTrue(ProgramsApiConfig.current().is_student_dashboard_enabled) self.assertTrue(ProgramsApiConfig.current().is_student_dashboard_enabled)
@ddt.data(
(True, 0),
(False, 0),
(False, 1),
)
@ddt.unpack
def test_is_cache_enabled_returns_false(self, enabled, cache_ttl, _mock_cache):
"""Verify that the method 'is_cache_enabled' returns false if
'cache_ttl' value is 0 or config is not enabled.
"""
self.assertFalse(ProgramsApiConfig.current().is_cache_enabled)
self.create_config(
enabled=enabled,
cache_ttl=cache_ttl
)
self.assertFalse(ProgramsApiConfig.current().is_cache_enabled)
def test_is_cache_enabled_returns_true(self, _mock_cache):
""""Verify that is_cache_enabled returns True when Programs is enabled
and the cache TTL is greater than 0."
"""
self.create_config(enabled=True, cache_ttl=10)
self.assertTrue(ProgramsApiConfig.current().is_cache_enabled)
""" """
Helper methods for Programs. Helper methods for Programs.
""" """
from django.core.cache import cache
from edx_rest_api_client.client import EdxRestApiClient from edx_rest_api_client.client import EdxRestApiClient
from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.programs.models import ProgramsApiConfig
...@@ -20,3 +21,29 @@ def programs_api_client(api_url, jwt_access_token): ...@@ -20,3 +21,29 @@ def programs_api_client(api_url, jwt_access_token):
api_url, api_url,
jwt=jwt_access_token jwt=jwt_access_token
) )
def is_cache_enabled_for_programs(): # pylint: disable=invalid-name
"""Returns a Boolean indicating whether responses from the Programs API
will be cached.
"""
return ProgramsApiConfig.current().is_cache_enabled
def set_cached_programs_response(programs_data):
""" Set cache value for the programs data with specific ttl.
Arguments:
programs_data (dict): Programs data in dictionary format
"""
cache.set(
ProgramsApiConfig.PROGRAMS_API_CACHE_KEY,
programs_data,
ProgramsApiConfig.current().cache_ttl
)
def get_cached_programs_response():
""" Get programs data from cache against cache key."""
cache_key = ProgramsApiConfig.PROGRAMS_API_CACHE_KEY
return cache.get(cache_key)
...@@ -6,7 +6,13 @@ import logging ...@@ -6,7 +6,13 @@ import logging
from openedx.core.djangoapps.util.helpers import get_id_token from openedx.core.djangoapps.util.helpers import get_id_token
from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.utils import programs_api_client, is_student_dashboard_programs_enabled from openedx.core.djangoapps.programs.utils import (
programs_api_client,
is_student_dashboard_programs_enabled,
is_cache_enabled_for_programs,
get_cached_programs_response,
set_cached_programs_response,
)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -37,6 +43,12 @@ def get_course_programs_for_dashboard(user, course_keys): # pylint: disable=in ...@@ -37,6 +43,12 @@ def get_course_programs_for_dashboard(user, course_keys): # pylint: disable=in
# unicode-ify the course keys for efficient lookup # unicode-ify the course keys for efficient lookup
course_keys = map(unicode, course_keys) course_keys = map(unicode, course_keys)
# If cache config is enabled then get the response from cache first.
if is_cache_enabled_for_programs():
cached_programs = get_cached_programs_response()
if cached_programs is not None:
return _get_user_course_programs(cached_programs, course_keys)
# get programs slumber-based client 'EdxRestApiClient' # get programs slumber-based client 'EdxRestApiClient'
try: try:
api_client = programs_api_client(ProgramsApiConfig.current().internal_api_url, get_id_token(user, CLIENT_NAME)) api_client = programs_api_client(ProgramsApiConfig.current().internal_api_url, get_id_token(user, CLIENT_NAME))
...@@ -56,14 +68,31 @@ def get_course_programs_for_dashboard(user, course_keys): # pylint: disable=in ...@@ -56,14 +68,31 @@ def get_course_programs_for_dashboard(user, course_keys): # pylint: disable=in
log.warning("No programs found for the user '%s'.", user.id) log.warning("No programs found for the user '%s'.", user.id)
return course_programs return course_programs
# If cache config is enabled than set the cache.
if is_cache_enabled_for_programs():
set_cached_programs_response(programs)
return _get_user_course_programs(programs, course_keys)
def _get_user_course_programs(programs, users_enrolled_course_keys): # pylint: disable=invalid-name
""" Parse the raw programs according to the users enrolled courses and
return the matched course runs.
Arguments:
programs (list): List containing the programs data.
users_enrolled_course_keys (list) : List of course keys in which the user is enrolled.
"""
# reindex the result from pgm -> course code -> course run # reindex the result from pgm -> course code -> course run
# to # to
# course run -> program, ignoring course runs not present in the dashboard enrollments # course run -> program, ignoring course runs not present in the dashboard enrollments
course_programs = {}
for program in programs: for program in programs:
try: try:
for course_code in program['course_codes']: for course_code in program['course_codes']:
for run in course_code['run_modes']: for run in course_code['run_modes']:
if run['course_key'] in course_keys: if run['course_key'] in users_enrolled_course_keys:
course_programs[run['course_key']] = program course_programs[run['course_key']] = program
except KeyError: except KeyError:
log.exception('Unable to parse Programs API response: %r', program) log.exception('Unable to parse Programs API response: %r', program)
......
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