version-based mobile app upgrade
This commit is contained in:
@@ -4,6 +4,21 @@ Django admin dashboard configuration for LMS XBlock infrastructure.
|
||||
|
||||
from django.contrib import admin
|
||||
from config_models.admin import ConfigurationModelAdmin
|
||||
from mobile_api.models import MobileApiConfig
|
||||
from mobile_api.models import MobileApiConfig, AppVersionConfig
|
||||
|
||||
admin.site.register(MobileApiConfig, ConfigurationModelAdmin)
|
||||
|
||||
|
||||
class AppVersionConfigAdmin(admin.ModelAdmin):
|
||||
""" Admin class for AppVersionConfig model """
|
||||
fields = ('platform', 'version', 'expire_at', 'enabled')
|
||||
list_filter = ['platform']
|
||||
|
||||
class Meta(object):
|
||||
ordering = ['-major_version', '-minor_version', '-patch_version']
|
||||
|
||||
def get_list_display(self, __):
|
||||
""" defines fields to display in list view """
|
||||
return ['platform', 'version', 'expire_at', 'enabled', 'created_at', 'updated_at']
|
||||
|
||||
admin.site.register(AppVersionConfig, AppVersionConfigAdmin)
|
||||
|
||||
@@ -9,7 +9,6 @@ from xmodule.html_module import CourseInfoModule
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.xml_importer import import_course_from_xml
|
||||
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
|
||||
from ..testutils import (
|
||||
|
||||
133
lms/djangoapps/mobile_api/middleware.py
Normal file
133
lms/djangoapps/mobile_api/middleware.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
Middleware for Mobile APIs
|
||||
"""
|
||||
from datetime import datetime
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpResponse
|
||||
from pytz import UTC
|
||||
from mobile_api.mobile_platform import MobilePlatform
|
||||
from mobile_api.models import AppVersionConfig
|
||||
from mobile_api.utils import parsed_version
|
||||
from openedx.core.lib.mobile_utils import is_request_from_mobile_app
|
||||
import request_cache
|
||||
|
||||
|
||||
class AppVersionUpgrade(object):
|
||||
"""
|
||||
Middleware class to keep track of mobile application version being used.
|
||||
"""
|
||||
LATEST_VERSION_HEADER = 'EDX-APP-LATEST-VERSION'
|
||||
LAST_SUPPORTED_DATE_HEADER = 'EDX-APP-VERSION-LAST-SUPPORTED-DATE'
|
||||
NO_LAST_SUPPORTED_DATE = 'NO_LAST_SUPPORTED_DATE'
|
||||
NO_LATEST_VERSION = 'NO_LATEST_VERSION'
|
||||
USER_APP_VERSION = 'USER_APP_VERSION'
|
||||
REQUEST_CACHE_NAME = 'app-version-info'
|
||||
CACHE_TIMEOUT = settings.APP_UPGRADE_CACHE_TIMEOUT
|
||||
|
||||
def process_request(self, request):
|
||||
"""
|
||||
Processes request to validate app version that is making request.
|
||||
|
||||
Returns:
|
||||
Http response with status code 426 (i.e. Update Required) if request is from
|
||||
mobile native app and app version is no longer supported else returns None
|
||||
"""
|
||||
version_data = self._get_version_info(request)
|
||||
if version_data:
|
||||
last_supported_date = version_data[self.LAST_SUPPORTED_DATE_HEADER]
|
||||
if last_supported_date != self.NO_LAST_SUPPORTED_DATE:
|
||||
if datetime.now().replace(tzinfo=UTC) > last_supported_date:
|
||||
return HttpResponse(status=426) # Http status 426; Update Required
|
||||
|
||||
def process_response(self, __, response):
|
||||
"""
|
||||
If request is from mobile native app, then add version related info to response headers.
|
||||
|
||||
Returns:
|
||||
Http response: with additional headers;
|
||||
1. EDX-APP-LATEST-VERSION; if user app version < latest available version
|
||||
2. EDX-APP-VERSION-LAST-SUPPORTED-DATE; if user app version < min supported version and
|
||||
timestamp < expiry of that version
|
||||
"""
|
||||
request_cache_dict = request_cache.get_cache(self.REQUEST_CACHE_NAME)
|
||||
if request_cache_dict:
|
||||
last_supported_date = request_cache_dict[self.LAST_SUPPORTED_DATE_HEADER]
|
||||
if last_supported_date != self.NO_LAST_SUPPORTED_DATE:
|
||||
response[self.LAST_SUPPORTED_DATE_HEADER] = last_supported_date.isoformat()
|
||||
latest_version = request_cache_dict[self.LATEST_VERSION_HEADER]
|
||||
user_app_version = request_cache_dict[self.USER_APP_VERSION]
|
||||
if (latest_version != self.NO_LATEST_VERSION and
|
||||
parsed_version(user_app_version) < parsed_version(latest_version)):
|
||||
response[self.LATEST_VERSION_HEADER] = latest_version
|
||||
return response
|
||||
|
||||
def _get_cache_key_name(self, field, key):
|
||||
"""
|
||||
Get key name to use to cache any property against field name and identification key.
|
||||
|
||||
Arguments:
|
||||
field (str): The property name that needs to get cached.
|
||||
key (str): Unique identification for cache key (e.g. platform_name).
|
||||
|
||||
Returns:
|
||||
string: Cache key to be used.
|
||||
"""
|
||||
return "mobile_api.app_version_upgrade.{}.{}".format(field, key)
|
||||
|
||||
def _get_version_info(self, request):
|
||||
"""
|
||||
Gets and Sets version related info in mem cache and request cache; and returns a dict of it.
|
||||
|
||||
It sets request cache data for last_supported_date and latest_version with memcached values if exists against
|
||||
user app properties else computes the values for specific platform and sets it in both memcache (for next
|
||||
server interaction from same app version/platform) and request cache
|
||||
|
||||
Returns:
|
||||
dict: Containing app version info
|
||||
"""
|
||||
user_agent = request.META.get('HTTP_USER_AGENT')
|
||||
if user_agent:
|
||||
platform = self._get_platform(request, user_agent)
|
||||
if platform:
|
||||
request_cache_dict = request_cache.get_cache(self.REQUEST_CACHE_NAME)
|
||||
request_cache_dict[self.USER_APP_VERSION] = platform.version
|
||||
last_supported_date_cache_key = self._get_cache_key_name(
|
||||
self.LAST_SUPPORTED_DATE_HEADER,
|
||||
platform.version
|
||||
)
|
||||
latest_version_cache_key = self._get_cache_key_name(self.LATEST_VERSION_HEADER, platform.NAME)
|
||||
cached_data = cache.get_many([last_supported_date_cache_key, latest_version_cache_key])
|
||||
|
||||
last_supported_date = cached_data.get(last_supported_date_cache_key)
|
||||
if not last_supported_date:
|
||||
last_supported_date = self._get_last_supported_date(platform.NAME, platform.version)
|
||||
cache.set(last_supported_date_cache_key, last_supported_date, self.CACHE_TIMEOUT)
|
||||
request_cache_dict[self.LAST_SUPPORTED_DATE_HEADER] = last_supported_date
|
||||
|
||||
latest_version = cached_data.get(latest_version_cache_key)
|
||||
if not latest_version:
|
||||
latest_version = self._get_latest_version(platform.NAME)
|
||||
cache.set(latest_version_cache_key, latest_version, self.CACHE_TIMEOUT)
|
||||
request_cache_dict[self.LATEST_VERSION_HEADER] = latest_version
|
||||
|
||||
return request_cache_dict
|
||||
|
||||
def _get_platform(self, request, user_agent):
|
||||
"""
|
||||
Determines the platform type for mobile app making the request against user_agent.
|
||||
|
||||
Returns:
|
||||
None if request app does not belong to one of the supported mobile platforms
|
||||
else returns an instance of corresponding mobile platform.
|
||||
"""
|
||||
if is_request_from_mobile_app(request):
|
||||
return MobilePlatform.get_instance(user_agent)
|
||||
|
||||
def _get_last_supported_date(self, platform_name, platform_version):
|
||||
""" Get expiry date of app version for a platform. """
|
||||
return AppVersionConfig.last_supported_date(platform_name, platform_version) or self.NO_LAST_SUPPORTED_DATE
|
||||
|
||||
def _get_latest_version(self, platform_name):
|
||||
""" Get latest app version available for platform. """
|
||||
return AppVersionConfig.latest_version(platform_name) or self.NO_LATEST_VERSION
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mobile_api', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AppVersionConfig',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('platform', models.CharField(max_length=50, choices=[(b'Android', b'Android'), (b'iOS', b'iOS')])),
|
||||
('version', models.CharField(help_text=b'Version should be in the format X.X.X.Y where X is a number and Y is alphanumeric', max_length=50)),
|
||||
('major_version', models.IntegerField()),
|
||||
('minor_version', models.IntegerField()),
|
||||
('patch_version', models.IntegerField()),
|
||||
('expire_at', models.DateTimeField(null=True, verbose_name=b'Expiry date for platform version', blank=True)),
|
||||
('enabled', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-major_version', '-minor_version', '-patch_version'],
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='appversionconfig',
|
||||
unique_together=set([('platform', 'version')]),
|
||||
),
|
||||
]
|
||||
69
lms/djangoapps/mobile_api/mobile_platform.py
Normal file
69
lms/djangoapps/mobile_api/mobile_platform.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Platform related Operations for Mobile APP
|
||||
"""
|
||||
import abc
|
||||
import re
|
||||
|
||||
|
||||
class MobilePlatform:
|
||||
"""
|
||||
MobilePlatform class creates an instance of platform based on user agent and supports platform
|
||||
related operations.
|
||||
"""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
version = None
|
||||
|
||||
def __init__(self, version):
|
||||
self.version = version
|
||||
|
||||
@classmethod
|
||||
def get_user_app_platform(cls, user_agent, user_agent_regex):
|
||||
"""
|
||||
Returns platform instance if user_agent matches with USER_AGENT_REGEX
|
||||
|
||||
Arguments:
|
||||
user_agent (str): user-agent for mobile app making the request.
|
||||
user_agent_regex (regex str): Regex for user-agent valid for any type pf mobile platform.
|
||||
|
||||
Returns:
|
||||
An instance of class passed (which would be one of the supported mobile platform
|
||||
classes i.e. PLATFORM_CLASSES) if user_agent matches regex of that class else returns None
|
||||
"""
|
||||
match = re.search(user_agent_regex, user_agent)
|
||||
if match:
|
||||
return cls(match.group('version'))
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, user_agent):
|
||||
"""
|
||||
It creates an instance of one of the supported mobile platforms (i.e. iOS, Android) by regex comparison
|
||||
of user-agent.
|
||||
|
||||
Parameters:
|
||||
user_agent: user_agent of mobile app
|
||||
|
||||
Returns:
|
||||
instance of one of the supported mobile platforms (i.e. iOS, Android)
|
||||
"""
|
||||
for subclass in PLATFORM_CLASSES.values():
|
||||
instance = subclass.get_user_app_platform(user_agent, subclass.USER_AGENT_REGEX)
|
||||
if instance:
|
||||
return instance
|
||||
|
||||
|
||||
class IOS(MobilePlatform):
|
||||
""" iOS platform """
|
||||
USER_AGENT_REGEX = (r'\((?P<version>[0-9]+.[0-9]+.[0-9]+(.[0-9a-zA-Z]*)?); OS Version [0-9.]+ '
|
||||
r'\(Build [0-9a-zA-Z]*\)\)')
|
||||
NAME = "iOS"
|
||||
|
||||
|
||||
class Android(MobilePlatform):
|
||||
""" Android platform """
|
||||
USER_AGENT_REGEX = (r'Dalvik/[.0-9]+ \(Linux; U; Android [.0-9]+; (.*) Build/[0-9a-zA-Z]*\) '
|
||||
r'(.*)/(?P<version>[0-9]+.[0-9]+.[0-9]+(.[0-9a-zA-Z]*)?)')
|
||||
NAME = "Android"
|
||||
|
||||
|
||||
# a list of all supported mobile platforms
|
||||
PLATFORM_CLASSES = {IOS.NAME: IOS, Android.NAME: Android}
|
||||
@@ -1,10 +1,10 @@
|
||||
"""
|
||||
ConfigurationModel for the mobile_api djangoapp.
|
||||
"""
|
||||
|
||||
from django.db.models.fields import TextField
|
||||
|
||||
from django.db import models
|
||||
from mobile_api import utils
|
||||
from config_models.models import ConfigurationModel
|
||||
from mobile_api.mobile_platform import PLATFORM_CLASSES
|
||||
|
||||
|
||||
class MobileApiConfig(ConfigurationModel):
|
||||
@@ -14,7 +14,7 @@ class MobileApiConfig(ConfigurationModel):
|
||||
The order in which the comma-separated list of names of profiles are given
|
||||
is in priority order.
|
||||
"""
|
||||
video_profiles = TextField(
|
||||
video_profiles = models.TextField(
|
||||
blank=True,
|
||||
help_text="A comma-separated list of names of profiles to include for videos returned from the mobile API."
|
||||
)
|
||||
@@ -25,3 +25,54 @@ class MobileApiConfig(ConfigurationModel):
|
||||
Get the list of profiles in priority order when requesting from VAL
|
||||
"""
|
||||
return [profile.strip() for profile in cls.current().video_profiles.split(",") if profile]
|
||||
|
||||
|
||||
class AppVersionConfig(models.Model):
|
||||
"""
|
||||
Configuration for mobile app versions available.
|
||||
"""
|
||||
PLATFORM_CHOICES = tuple([
|
||||
(platform, platform)
|
||||
for platform in PLATFORM_CLASSES.keys()
|
||||
])
|
||||
platform = models.CharField(max_length=50, choices=PLATFORM_CHOICES, blank=False)
|
||||
version = models.CharField(
|
||||
max_length=50,
|
||||
blank=False,
|
||||
help_text="Version should be in the format X.X.X.Y where X is a number and Y is alphanumeric"
|
||||
)
|
||||
major_version = models.IntegerField()
|
||||
minor_version = models.IntegerField()
|
||||
patch_version = models.IntegerField()
|
||||
expire_at = models.DateTimeField(null=True, blank=True, verbose_name="Expiry date for platform version")
|
||||
enabled = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('platform', 'version',)
|
||||
ordering = ['-major_version', '-minor_version', '-patch_version']
|
||||
|
||||
def __unicode__(self):
|
||||
return "{}_{}".format(self.platform, self.version)
|
||||
|
||||
@classmethod
|
||||
def latest_version(cls, platform):
|
||||
""" Returns latest supported app version for a platform. """
|
||||
latest_version_config = cls.objects.filter(platform=platform, enabled=True).first()
|
||||
if latest_version_config:
|
||||
return latest_version_config.version
|
||||
|
||||
@classmethod
|
||||
def last_supported_date(cls, platform, version):
|
||||
""" Returns date when app version will get expired for a platform """
|
||||
parsed_version = utils.parsed_version(version)
|
||||
active_configs = cls.objects.filter(platform=platform, enabled=True, expire_at__isnull=False).reverse()
|
||||
for config in active_configs:
|
||||
if utils.parsed_version(config.version) >= parsed_version:
|
||||
return config.expire_at
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" parses version into major, minor and patch versions before saving """
|
||||
self.major_version, self.minor_version, self.patch_version = utils.parsed_version(self.version)
|
||||
super(AppVersionConfig, self).save(*args, **kwargs)
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tests for mobile API utilities.
|
||||
"""
|
||||
|
||||
import ddt
|
||||
from django.test import TestCase
|
||||
from mobile_api.models import MobileApiConfig
|
||||
|
||||
from .utils import mobile_course_access, mobile_view
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestMobileAPIDecorators(TestCase):
|
||||
"""
|
||||
Basic tests for mobile api decorators to ensure they retain the docstrings.
|
||||
"""
|
||||
@ddt.data(mobile_view, mobile_course_access)
|
||||
def test_function_decorator(self, decorator):
|
||||
@decorator()
|
||||
def decorated_func():
|
||||
"""
|
||||
Test docstring of decorated function.
|
||||
"""
|
||||
pass
|
||||
|
||||
self.assertIn("Test docstring of decorated function.", decorated_func.__doc__)
|
||||
self.assertEquals(decorated_func.__name__, "decorated_func")
|
||||
self.assertTrue(decorated_func.__module__.endswith("tests"))
|
||||
|
||||
|
||||
class TestMobileApiConfig(TestCase):
|
||||
"""
|
||||
Tests MobileAPIConfig
|
||||
"""
|
||||
|
||||
def test_video_profile_list(self):
|
||||
"""Check that video_profiles config is returned in order as a list"""
|
||||
MobileApiConfig(video_profiles="mobile_low,mobile_high,youtube").save()
|
||||
video_profile_list = MobileApiConfig.get_video_profiles()
|
||||
self.assertEqual(
|
||||
video_profile_list,
|
||||
[u'mobile_low', u'mobile_high', u'youtube']
|
||||
)
|
||||
|
||||
def test_video_profile_list_with_whitespace(self):
|
||||
"""Check video_profiles config with leading and trailing whitespace"""
|
||||
MobileApiConfig(video_profiles=" mobile_low , mobile_high,youtube ").save()
|
||||
video_profile_list = MobileApiConfig.get_video_profiles()
|
||||
self.assertEqual(
|
||||
video_profile_list,
|
||||
[u'mobile_low', u'mobile_high', u'youtube']
|
||||
)
|
||||
|
||||
def test_empty_video_profile(self):
|
||||
"""Test an empty video_profile"""
|
||||
MobileApiConfig(video_profiles="").save()
|
||||
video_profile_list = MobileApiConfig.get_video_profiles()
|
||||
self.assertEqual(video_profile_list, [])
|
||||
0
lms/djangoapps/mobile_api/tests/__init__.py
Normal file
0
lms/djangoapps/mobile_api/tests/__init__.py
Normal file
28
lms/djangoapps/mobile_api/tests/test_decorator.py
Normal file
28
lms/djangoapps/mobile_api/tests/test_decorator.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tests for mobile API utilities.
|
||||
"""
|
||||
|
||||
import ddt
|
||||
from django.test import TestCase
|
||||
|
||||
from mobile_api.utils import mobile_course_access, mobile_view
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestMobileAPIDecorators(TestCase):
|
||||
"""
|
||||
Basic tests for mobile api decorators to ensure they retain the docstrings.
|
||||
"""
|
||||
@ddt.data(mobile_view, mobile_course_access)
|
||||
def test_function_decorator(self, decorator):
|
||||
@decorator()
|
||||
def decorated_func():
|
||||
"""
|
||||
Test docstring of decorated function.
|
||||
"""
|
||||
pass
|
||||
|
||||
self.assertIn("Test docstring of decorated function.", decorated_func.__doc__)
|
||||
self.assertEquals(decorated_func.__name__, "decorated_func")
|
||||
self.assertTrue(decorated_func.__module__.endswith("test_decorator"))
|
||||
156
lms/djangoapps/mobile_api/tests/test_middleware.py
Normal file
156
lms/djangoapps/mobile_api/tests/test_middleware.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
Tests for Version Based App Upgrade Middleware
|
||||
"""
|
||||
from datetime import datetime
|
||||
import ddt
|
||||
from django.core.cache import cache, caches
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.test import TestCase
|
||||
import mock
|
||||
from pytz import UTC
|
||||
from mobile_api.middleware import AppVersionUpgrade
|
||||
from mobile_api.models import AppVersionConfig
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestAppVersionUpgradeMiddleware(TestCase):
|
||||
"""
|
||||
Tests for version based app upgrade middleware
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestAppVersionUpgradeMiddleware, self).setUp()
|
||||
self.middleware = AppVersionUpgrade()
|
||||
self.set_app_version_config()
|
||||
cache.clear()
|
||||
|
||||
def set_app_version_config(self):
|
||||
""" Creates configuration data for platform versions """
|
||||
AppVersionConfig(platform="iOS", version="1.1.1", expire_at=None, enabled=True).save()
|
||||
AppVersionConfig(
|
||||
platform="iOS",
|
||||
version="2.2.2",
|
||||
expire_at=datetime(2014, 01, 01, tzinfo=UTC),
|
||||
enabled=True
|
||||
).save()
|
||||
AppVersionConfig(
|
||||
platform="iOS",
|
||||
version="4.4.4",
|
||||
expire_at=datetime(9000, 01, 01, tzinfo=UTC),
|
||||
enabled=True
|
||||
).save()
|
||||
AppVersionConfig(platform="iOS", version="6.6.6", expire_at=None, enabled=True).save()
|
||||
|
||||
AppVersionConfig(platform="Android", version="1.1.1", expire_at=None, enabled=True).save()
|
||||
AppVersionConfig(
|
||||
platform="Android",
|
||||
version="2.2.2",
|
||||
expire_at=datetime(2014, 01, 01, tzinfo=UTC),
|
||||
enabled=True
|
||||
).save()
|
||||
AppVersionConfig(
|
||||
platform="Android",
|
||||
version="4.4.4",
|
||||
expire_at=datetime(5000, 01, 01, tzinfo=UTC),
|
||||
enabled=True
|
||||
).save()
|
||||
AppVersionConfig(platform="Android", version="8.8.8", expire_at=None, enabled=True).save()
|
||||
|
||||
def process_middleware(self, user_agent, cache_get_many_calls_for_request=1):
|
||||
""" Helper function that makes calls to middle process_request and process_response """
|
||||
fake_request = HttpRequest()
|
||||
fake_request.META['HTTP_USER_AGENT'] = user_agent
|
||||
with mock.patch.object(caches['default'], 'get_many', wraps=caches['default'].get_many) as mocked_code:
|
||||
request_response = self.middleware.process_request(fake_request)
|
||||
self.assertEqual(cache_get_many_calls_for_request, mocked_code.call_count)
|
||||
with mock.patch.object(caches['default'], 'get_many', wraps=caches['default'].get_many) as mocked_code:
|
||||
processed_response = self.middleware.process_response(fake_request, request_response or HttpResponse())
|
||||
self.assertEqual(0, mocked_code.call_count)
|
||||
return request_response, processed_response
|
||||
|
||||
@ddt.data(
|
||||
("Mozilla/5.0 (Linux; Android 5.1; Nexus 5 Build/LMY47I; wv) AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Version/4.0 Chrome/47.0.2526.100 Mobile Safari/537.36 edX/org.edx.mobile/2.0.0"),
|
||||
("Mozilla/5.0 (iPhone; CPU iPhone OS 9_2 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) "
|
||||
"Mobile/13C75 edX/org.edx.mobile/2.2.1"),
|
||||
("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 "
|
||||
"Safari/537.36"),
|
||||
)
|
||||
def test_non_mobile_app_requests(self, user_agent):
|
||||
with self.assertNumQueries(0):
|
||||
request_response, processed_response = self.process_middleware(user_agent, 0)
|
||||
self.assertIsNone(request_response)
|
||||
self.assertEquals(200, processed_response.status_code)
|
||||
self.assertNotIn(AppVersionUpgrade.LATEST_VERSION_HEADER, processed_response)
|
||||
self.assertNotIn(AppVersionUpgrade.LAST_SUPPORTED_DATE_HEADER, processed_response)
|
||||
|
||||
@ddt.data(
|
||||
"edX/org.edx.mobile (6.6.6; OS Version 9.2 (Build 13C75))",
|
||||
"edX/org.edx.mobile (7.7.7; OS Version 9.2 (Build 13C75))",
|
||||
"Dalvik/2.1.0 (Linux; U; Android 5.1; Nexus 5 Build/LMY47I) edX/org.edx.mobile/8.8.8",
|
||||
"Dalvik/2.1.0 (Linux; U; Android 5.1; Nexus 5 Build/LMY47I) edX/org.edx.mobile/9.9.9",
|
||||
)
|
||||
def test_no_update(self, user_agent):
|
||||
with self.assertNumQueries(2):
|
||||
request_response, processed_response = self.process_middleware(user_agent)
|
||||
self.assertIsNone(request_response)
|
||||
self.assertEquals(200, processed_response.status_code)
|
||||
self.assertNotIn(AppVersionUpgrade.LATEST_VERSION_HEADER, processed_response)
|
||||
self.assertNotIn(AppVersionUpgrade.LAST_SUPPORTED_DATE_HEADER, processed_response)
|
||||
with self.assertNumQueries(0):
|
||||
self.process_middleware(user_agent)
|
||||
|
||||
@ddt.data(
|
||||
("edX/org.edx.mobile (5.1.1; OS Version 9.2 (Build 13C75))", "6.6.6"),
|
||||
("edX/org.edx.mobile (5.1.1.RC; OS Version 9.2 (Build 13C75))", "6.6.6"),
|
||||
("Dalvik/2.1.0 (Linux; U; Android 5.1; Nexus 5 Build/LMY47I) edX/org.edx.mobile/5.1.1", "8.8.8"),
|
||||
("Dalvik/2.1.0 (Linux; U; Android 5.1; Nexus 5 Build/LMY47I) edX/org.edx.mobile/5.1.1.RC", "8.8.8"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_new_version_available(self, user_agent, latest_version):
|
||||
with self.assertNumQueries(2):
|
||||
request_response, processed_response = self.process_middleware(user_agent)
|
||||
self.assertIsNone(request_response)
|
||||
self.assertEquals(200, processed_response.status_code)
|
||||
self.assertEqual(latest_version, processed_response[AppVersionUpgrade.LATEST_VERSION_HEADER])
|
||||
self.assertNotIn(AppVersionUpgrade.LAST_SUPPORTED_DATE_HEADER, processed_response)
|
||||
with self.assertNumQueries(0):
|
||||
self.process_middleware(user_agent)
|
||||
|
||||
@ddt.data(
|
||||
("edX/org.edx.mobile (1.0.1; OS Version 9.2 (Build 13C75))", "6.6.6"),
|
||||
("edX/org.edx.mobile (1.1.1; OS Version 9.2 (Build 13C75))", "6.6.6"),
|
||||
("edX/org.edx.mobile (2.0.5.RC; OS Version 9.2 (Build 13C75))", "6.6.6"),
|
||||
("edX/org.edx.mobile (2.2.2; OS Version 9.2 (Build 13C75))", "6.6.6"),
|
||||
("Dalvik/2.1.0 (Linux; U; Android 5.1; Nexus 5 Build/LMY47I) edX/org.edx.mobile/1.0.1", "8.8.8"),
|
||||
("Dalvik/2.1.0 (Linux; U; Android 5.1; Nexus 5 Build/LMY47I) edX/org.edx.mobile/1.1.1", "8.8.8"),
|
||||
("Dalvik/2.1.0 (Linux; U; Android 5.1; Nexus 5 Build/LMY47I) edX/org.edx.mobile/2.0.5.RC", "8.8.8"),
|
||||
("Dalvik/2.1.0 (Linux; U; Android 5.1; Nexus 5 Build/LMY47I) edX/org.edx.mobile/2.2.2", "8.8.8"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_version_update_required(self, user_agent, latest_version):
|
||||
with self.assertNumQueries(2):
|
||||
request_response, processed_response = self.process_middleware(user_agent)
|
||||
self.assertIsNotNone(request_response)
|
||||
self.assertEquals(426, processed_response.status_code)
|
||||
self.assertEqual(latest_version, processed_response[AppVersionUpgrade.LATEST_VERSION_HEADER])
|
||||
with self.assertNumQueries(0):
|
||||
self.process_middleware(user_agent)
|
||||
|
||||
@ddt.data(
|
||||
("edX/org.edx.mobile (4.4.4; OS Version 9.2 (Build 13C75))", "6.6.6", '9000-01-01T00:00:00+00:00'),
|
||||
(
|
||||
"Dalvik/2.1.0 (Linux; U; Android 5.1; Nexus 5 Build/LMY47I) edX/org.edx.mobile/4.4.4",
|
||||
"8.8.8",
|
||||
'5000-01-01T00:00:00+00:00',
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_version_update_available_with_deadline(self, user_agent, latest_version, upgrade_date):
|
||||
with self.assertNumQueries(2):
|
||||
request_response, processed_response = self.process_middleware(user_agent)
|
||||
self.assertIsNone(request_response)
|
||||
self.assertEquals(200, processed_response.status_code)
|
||||
self.assertEqual(latest_version, processed_response[AppVersionUpgrade.LATEST_VERSION_HEADER])
|
||||
self.assertEqual(upgrade_date, processed_response[AppVersionUpgrade.LAST_SUPPORTED_DATE_HEADER])
|
||||
with self.assertNumQueries(0):
|
||||
self.process_middleware(user_agent)
|
||||
46
lms/djangoapps/mobile_api/tests/test_mobile_platform.py
Normal file
46
lms/djangoapps/mobile_api/tests/test_mobile_platform.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
Tests for Platform against Mobile App Request
|
||||
"""
|
||||
import ddt
|
||||
from django.test import TestCase
|
||||
from mobile_api.mobile_platform import MobilePlatform
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestMobilePlatform(TestCase):
|
||||
"""
|
||||
Tests for platform against mobile app request
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestMobilePlatform, self).setUp()
|
||||
|
||||
@ddt.data(
|
||||
("edX/org.edx.mobile (0.1.5; OS Version 9.2 (Build 13C75))", "iOS", "0.1.5"),
|
||||
("edX/org.edx.mobile (1.01.1; OS Version 9.2 (Build 13C75))", "iOS", "1.01.1"),
|
||||
("edX/org.edx.mobile (2.2.2; OS Version 9.2 (Build 13C75))", "iOS", "2.2.2"),
|
||||
("edX/org.edx.mobile (3.3.3; OS Version 9.2 (Build 13C75))", "iOS", "3.3.3"),
|
||||
("edX/org.edx.mobile (3.3.3.test; OS Version 9.2 (Build 13C75))", "iOS", "3.3.3.test"),
|
||||
("edX/org.test-domain.mobile (0.1.5; OS Version 9.2 (Build 13C75))", "iOS", "0.1.5"),
|
||||
("Dalvik/2.1.0 (Linux; U; Android 5.1; Nexus 5 Build/LMY47I) edX/org.edx.mobile/1.1.1", "Android", "1.1.1"),
|
||||
("Dalvik/2.1.0 (Linux; U; Android 5.1; Nexus 5 Build/LMY47I) edX/org.edx.mobile/2.2.2", "Android", "2.2.2"),
|
||||
("Dalvik/2.1.0 (Linux; U; Android 5.1; Nexus 5 Build/LMY47I) edX/org.edx.mobile/3.3.3", "Android", "3.3.3"),
|
||||
("Dalvik/2.1.0 (Linux; U; Android 5.1; Nexus 5 Build/LMY47I) edX/org.edx.mobile/3.3.3.X", "Android", "3.3.3.X"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_platform_instance(self, user_agent, platform_name, version):
|
||||
platform = MobilePlatform.get_instance(user_agent)
|
||||
self.assertEqual(platform_name, platform.NAME)
|
||||
self.assertEqual(version, platform.version)
|
||||
|
||||
@ddt.data(
|
||||
("Mozilla/5.0 (Linux; Android 5.1; Nexus 5 Build/LMY47I; wv) AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Version/4.0 Chrome/47.0.2526.100 Mobile Safari/537.36 edX/org.edx.mobile/2.0.0"),
|
||||
("Mozilla/5.0 (iPhone; CPU iPhone OS 9_2 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) "
|
||||
"Mobile/13C75 edX/org.edx.mobile/2.2.1"),
|
||||
("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 "
|
||||
"Safari/537.36"),
|
||||
"edX/org.edx.mobile (0.1.5.2.; OS Version 9.2 (Build 13C75))",
|
||||
"edX/org.edx.mobile (0.1.5.2.5.1; OS Version 9.2 (Build 13C75))",
|
||||
)
|
||||
def test_non_mobile_app_requests(self, user_agent):
|
||||
self.assertIsNone(MobilePlatform.get_instance(user_agent))
|
||||
115
lms/djangoapps/mobile_api/tests/test_model.py
Normal file
115
lms/djangoapps/mobile_api/tests/test_model.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
Tests for Mobile API Configuration Models
|
||||
"""
|
||||
from datetime import datetime
|
||||
import ddt
|
||||
from django.test import TestCase
|
||||
from pytz import UTC
|
||||
from mobile_api.models import AppVersionConfig, MobileApiConfig
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestAppVersionConfigModel(TestCase):
|
||||
"""
|
||||
Tests for app version configuration model
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestAppVersionConfigModel, self).setUp()
|
||||
|
||||
def set_app_version_config(self):
|
||||
""" Creates configuration data for platform versions """
|
||||
AppVersionConfig(platform="ios", version="1.1.1", expire_at=None, enabled=True).save()
|
||||
AppVersionConfig(
|
||||
platform="ios",
|
||||
version="2.2.2",
|
||||
expire_at=datetime(2014, 01, 01, tzinfo=UTC),
|
||||
enabled=True
|
||||
).save()
|
||||
AppVersionConfig(
|
||||
platform="ios",
|
||||
version="4.1.1",
|
||||
expire_at=datetime(5000, 01, 01, tzinfo=UTC),
|
||||
enabled=False
|
||||
).save()
|
||||
AppVersionConfig(
|
||||
platform="ios",
|
||||
version="4.4.4",
|
||||
expire_at=datetime(9000, 01, 01, tzinfo=UTC),
|
||||
enabled=True
|
||||
).save()
|
||||
AppVersionConfig(platform="ios", version="6.6.6", expire_at=None, enabled=True).save()
|
||||
AppVersionConfig(platform="ios", version="8.8.8", expire_at=None, enabled=False).save()
|
||||
|
||||
AppVersionConfig(platform="android", version="1.1.1", expire_at=None, enabled=True).save()
|
||||
AppVersionConfig(
|
||||
platform="android",
|
||||
version="2.2.2",
|
||||
expire_at=datetime(2014, 01, 01, tzinfo=UTC),
|
||||
enabled=True
|
||||
).save()
|
||||
AppVersionConfig(
|
||||
platform="android",
|
||||
version="4.4.4",
|
||||
expire_at=datetime(9000, 01, 01, tzinfo=UTC),
|
||||
enabled=True
|
||||
).save()
|
||||
AppVersionConfig(platform="android", version="8.8.8", expire_at=None, enabled=True).save()
|
||||
|
||||
@ddt.data(
|
||||
('ios', '4.4.4'),
|
||||
('ios', '6.6.6'),
|
||||
("android", '4.4.4'),
|
||||
('android', '8.8.8')
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_no_configs_available(self, platform, version):
|
||||
self.assertIsNone(AppVersionConfig.latest_version(platform))
|
||||
self.assertIsNone(AppVersionConfig.last_supported_date(platform, version))
|
||||
|
||||
@ddt.data(('ios', '6.6.6'), ('android', '8.8.8'))
|
||||
@ddt.unpack
|
||||
def test_latest_version(self, platform, latest_version):
|
||||
self.set_app_version_config()
|
||||
self.assertEqual(latest_version, AppVersionConfig.latest_version(platform))
|
||||
|
||||
@ddt.data(
|
||||
('ios', '3.3.3', datetime(9000, 1, 1, tzinfo=UTC)),
|
||||
('ios', '4.4.4', datetime(9000, 1, 1, tzinfo=UTC)),
|
||||
('ios', '6.6.6', None),
|
||||
("android", '4.4.4', datetime(9000, 1, 1, tzinfo=UTC)),
|
||||
('android', '8.8.8', None)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_last_supported_date(self, platform, version, last_supported_date):
|
||||
self.set_app_version_config()
|
||||
self.assertEqual(last_supported_date, AppVersionConfig.last_supported_date(platform, version))
|
||||
|
||||
|
||||
class TestMobileApiConfig(TestCase):
|
||||
"""
|
||||
Tests MobileAPIConfig
|
||||
"""
|
||||
|
||||
def test_video_profile_list(self):
|
||||
"""Check that video_profiles config is returned in order as a list"""
|
||||
MobileApiConfig(video_profiles="mobile_low,mobile_high,youtube").save()
|
||||
video_profile_list = MobileApiConfig.get_video_profiles()
|
||||
self.assertEqual(
|
||||
video_profile_list,
|
||||
[u'mobile_low', u'mobile_high', u'youtube']
|
||||
)
|
||||
|
||||
def test_video_profile_list_with_whitespace(self):
|
||||
"""Check video_profiles config with leading and trailing whitespace"""
|
||||
MobileApiConfig(video_profiles=" mobile_low , mobile_high,youtube ").save()
|
||||
video_profile_list = MobileApiConfig.get_video_profiles()
|
||||
self.assertEqual(
|
||||
video_profile_list,
|
||||
[u'mobile_low', u'mobile_high', u'youtube']
|
||||
)
|
||||
|
||||
def test_empty_video_profile(self):
|
||||
"""Test an empty video_profile"""
|
||||
MobileApiConfig(video_profiles="").save()
|
||||
video_profile_list = MobileApiConfig.get_video_profiles()
|
||||
self.assertEqual(video_profile_list, [])
|
||||
@@ -10,16 +10,16 @@ Test utilities for mobile API tests:
|
||||
MobileCourseAccessTestMixin - tests for APIs with mobile_course_access.
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
import ddt
|
||||
from datetime import timedelta
|
||||
|
||||
from django.utils import timezone
|
||||
import ddt
|
||||
from mock import patch
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from courseware.access_response import (
|
||||
MobileAvailabilityError,
|
||||
@@ -29,10 +29,7 @@ from courseware.access_response import (
|
||||
from courseware.tests.factories import UserFactory
|
||||
from student import auth
|
||||
from student.models import CourseEnrollment
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from mobile_api.test_milestones import MobileAPIMilestonesMixin
|
||||
from mobile_api.tests.test_milestones import MobileAPIMilestonesMixin
|
||||
|
||||
|
||||
class MobileAPITestCase(ModuleStoreTestCase, APITestCase):
|
||||
|
||||
@@ -3,15 +3,18 @@ Tests for users API
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
import datetime
|
||||
|
||||
import ddt
|
||||
from mock import patch
|
||||
from nose.plugins.attrib import attr
|
||||
import pytz
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.template import defaultfilters
|
||||
from django.test import RequestFactory
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
from xmodule.course_module import DEFAULT_START_DATE
|
||||
from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory
|
||||
|
||||
from certificates.api import generate_user_certificates
|
||||
from certificates.models import CertificateStatuses
|
||||
@@ -25,13 +28,14 @@ from course_modes.models import CourseMode
|
||||
from openedx.core.lib.courses import course_image_url
|
||||
from student.models import CourseEnrollment
|
||||
from util.milestones_helpers import set_prerequisite_courses
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
from xmodule.course_module import DEFAULT_START_DATE
|
||||
from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory
|
||||
from util.testing import UrlResetMixin
|
||||
|
||||
from .. import errors
|
||||
from ..testutils import MobileAPITestCase, MobileAuthTestMixin, MobileAuthUserTestMixin, MobileCourseAccessTestMixin
|
||||
from mobile_api.testutils import (
|
||||
MobileAPITestCase,
|
||||
MobileAuthTestMixin,
|
||||
MobileAuthUserTestMixin,
|
||||
MobileCourseAccessTestMixin,
|
||||
)
|
||||
from .serializers import CourseEnrollmentSerializer
|
||||
|
||||
|
||||
|
||||
@@ -16,3 +16,8 @@ def mobile_view(is_user=False):
|
||||
Function and class decorator that abstracts the authentication and permission checks for mobile api views.
|
||||
"""
|
||||
return view_auth_classes(is_user)
|
||||
|
||||
|
||||
def parsed_version(version):
|
||||
""" Converts string X.X.X.Y to int tuple (X, X, X) """
|
||||
return tuple(map(int, (version.split(".")[:3])))
|
||||
|
||||
@@ -3,26 +3,24 @@
|
||||
Tests for video outline API
|
||||
"""
|
||||
|
||||
import ddt
|
||||
import itertools
|
||||
from nose.plugins.attrib import attr
|
||||
from uuid import uuid4
|
||||
from collections import namedtuple
|
||||
|
||||
import ddt
|
||||
from nose.plugins.attrib import attr
|
||||
from edxval import api
|
||||
from mobile_api.models import MobileApiConfig
|
||||
from xmodule.modulestore.tests.factories import ItemFactory
|
||||
from xmodule.video_module import transcripts_utils
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.partitions.partitions import Group, UserPartition
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
|
||||
from mobile_api.models import MobileApiConfig
|
||||
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
|
||||
from openedx.core.djangoapps.course_groups.models import CourseUserGroupPartitionGroup
|
||||
from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort, remove_user_from_cohort
|
||||
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
|
||||
from ..testutils import MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin
|
||||
from mobile_api.testutils import MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin
|
||||
|
||||
|
||||
class TestVideoAPITestCase(MobileAPITestCase):
|
||||
|
||||
@@ -802,3 +802,6 @@ if FEATURES.get('ENABLE_CSMH_EXTENDED'):
|
||||
INSTALLED_APPS += ('coursewarehistoryextended',)
|
||||
|
||||
API_ACCESS_MANAGER_EMAIL = ENV_TOKENS.get('API_ACCESS_MANAGER_EMAIL')
|
||||
|
||||
# Mobile App Version Upgrade config
|
||||
APP_UPGRADE_CACHE_TIMEOUT = ENV_TOKENS.get('APP_UPGRADE_CACHE_TIMEOUT', APP_UPGRADE_CACHE_TIMEOUT)
|
||||
|
||||
@@ -29,7 +29,6 @@ Longer TODO:
|
||||
# and throws spurious errors. Therefore, we disable invalid-name checking.
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
import datetime
|
||||
import imp
|
||||
import sys
|
||||
import os
|
||||
@@ -1090,6 +1089,7 @@ simplefilter('ignore')
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'request_cache.middleware.RequestCache',
|
||||
'mobile_api.middleware.AppVersionUpgrade',
|
||||
'header_control.middleware.HeaderControlMiddleware',
|
||||
'microsite_configuration.middleware.MicrositeMiddleware',
|
||||
'django_comment_client.middleware.AjaxExceptionMiddleware',
|
||||
@@ -2850,6 +2850,9 @@ MOBILE_APP_USER_AGENT_REGEXES = [
|
||||
r'edX/org.edx.mobile',
|
||||
]
|
||||
|
||||
# cache timeout in seconds for Mobile App Version Upgrade
|
||||
APP_UPGRADE_CACHE_TIMEOUT = 3600
|
||||
|
||||
# Offset for courseware.StudentModuleHistoryExtended which is used to
|
||||
# calculate the starting primary key for the underlying table. This gap
|
||||
# should be large enough that you do not generate more than N courseware.StudentModuleHistory
|
||||
|
||||
@@ -29,7 +29,7 @@ def is_request_from_mobile_app(request):
|
||||
user_agent = request.META.get('HTTP_USER_AGENT')
|
||||
if user_agent:
|
||||
for user_agent_regex in settings.MOBILE_APP_USER_AGENT_REGEXES:
|
||||
if re.match(user_agent_regex, user_agent):
|
||||
if re.search(user_agent_regex, user_agent):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user