Commit 86c66f55 by Awais

Adding cache implementation for the programs api.

ECOM-2832
parent a8cca47c
# -*- 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
from django.db.models import BooleanField, IntegerField, URLField
from django.utils.translation import ugettext_lazy as _
from django.db import models
from config_models.models import ConfigurationModel
......@@ -20,6 +21,15 @@ class ProgramsApiConfig(ConfigurationModel):
public_service_url = URLField(verbose_name=_("Public Service URL"))
api_version_number = IntegerField(verbose_name=_("API Version"))
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
def internal_api_url(self):
......@@ -42,3 +52,8 @@ class ProgramsApiConfig(ConfigurationModel):
be enabled or not.
"""
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.
"""
import ddt
from mock import patch
from django.test import TestCase
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
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.
class ProgramsApiConfigTest(ProgramsApiConfigMixin, TestCase):
"""
......@@ -70,3 +70,28 @@ class ProgramsApiConfigTest(ProgramsApiConfigMixin, TestCase):
self.create_config(enabled=True, enable_student_dashboard=True)
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.
"""
from django.core.cache import cache
from edx_rest_api_client.client import EdxRestApiClient
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
......@@ -20,3 +21,29 @@ def programs_api_client(api_url, jwt_access_token):
api_url,
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
from openedx.core.djangoapps.util.helpers import get_id_token
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__)
......@@ -37,6 +43,12 @@ def get_course_programs_for_dashboard(user, course_keys): # pylint: disable=in
# unicode-ify the course keys for efficient lookup
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'
try:
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
log.warning("No programs found for the user '%s'.", user.id)
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
# to
# to
# course run -> program, ignoring course runs not present in the dashboard enrollments
course_programs = {}
for program in programs:
try:
for course_code in program['course_codes']:
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
except KeyError:
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