Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
86c66f55
Commit
86c66f55
authored
Nov 12, 2015
by
Awais
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Adding cache implementation for the programs api.
ECOM-2832
parent
a8cca47c
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
286 additions
and
67 deletions
+286
-67
openedx/core/djangoapps/programs/migrations/0002_auto__add_field_programsapiconfig_cache_ttl.py
+73
-0
openedx/core/djangoapps/programs/models.py
+15
-0
openedx/core/djangoapps/programs/tests/test_models.py
+27
-2
openedx/core/djangoapps/programs/tests/test_views.py
+112
-62
openedx/core/djangoapps/programs/utils.py
+27
-0
openedx/core/djangoapps/programs/views.py
+32
-3
No files found.
openedx/core/djangoapps/programs/migrations/0002_auto__add_field_programsapiconfig_cache_ttl.py
0 → 100644
View file @
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'
]
openedx/core/djangoapps/programs/models.py
View file @
86c66f55
...
...
@@ -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
openedx/core/djangoapps/programs/tests/test_models.py
View file @
86c66f55
"""
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
)
openedx/core/djangoapps/programs/tests/test_views.py
View file @
86c66f55
This diff is collapsed.
Click to expand it.
openedx/core/djangoapps/programs/utils.py
View file @
86c66f55
"""
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
)
openedx/core/djangoapps/programs/views.py
View file @
86c66f55
...
...
@@ -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
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment