Commit 86e954f6 by wajeeha-khalid

version-based mobile app upgrade

parent a52cafcc
......@@ -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 (
......
"""
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
# -*- 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')]),
),
]
"""
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}
"""
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)
......@@ -5,9 +5,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
from mobile_api.utils import mobile_course_access, mobile_view
@ddt.ddt
......@@ -26,34 +25,4 @@ class TestMobileAPIDecorators(TestCase):
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, [])
self.assertTrue(decorated_func.__module__.endswith("test_decorator"))
"""
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)
"""
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))
"""
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
......
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