Enable In-App purchases on edx-mobile app (#31512)

* feat: added mobile skus in course mode

* fix: changed api

---------

Co-authored-by: jawad-khan <jawadkhan444@gmail.com>
Co-authored-by: Robert Raposa <rraposa@edx.org>
This commit is contained in:
Moeez Zahid
2023-02-10 16:16:37 +05:00
committed by GitHub
parent 4abb6b51cb
commit c8af0f607f
7 changed files with 115 additions and 12 deletions

View File

@@ -191,6 +191,8 @@ class CourseModeAdmin(admin.ModelAdmin):
'_expiration_datetime',
'verification_deadline',
'sku',
'android_sku',
'ios_sku',
'bulk_sku'
)
@@ -203,6 +205,8 @@ class CourseModeAdmin(admin.ModelAdmin):
'min_price',
'expiration_datetime_custom',
'sku',
'android_sku',
'ios_sku',
'bulk_sku'
)

View File

@@ -0,0 +1,33 @@
# Generated by Django 3.2.16 on 2023-02-07 12:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course_modes', '0013_auto_20200115_2022'),
]
operations = [
migrations.AddField(
model_name='coursemode',
name='android_sku',
field=models.CharField(blank=True, help_text='OPTIONAL: This is the Android SKU registered on play store for this mode of the course. Leave this blank if the course has not yet been migrated to the ecommerce service.', max_length=255, null=True, verbose_name='Android SKU'),
),
migrations.AddField(
model_name='coursemode',
name='ios_sku',
field=models.CharField(blank=True, help_text='OPTIONAL: This is the iOS SKU registered on app store for this mode of the course. Leave this blank if the course has not yet been migrated to the ecommerce service.', max_length=255, null=True, verbose_name='iOS SKU'),
),
migrations.AddField(
model_name='historicalcoursemode',
name='android_sku',
field=models.CharField(blank=True, help_text='OPTIONAL: This is the Android SKU registered on play store for this mode of the course. Leave this blank if the course has not yet been migrated to the ecommerce service.', max_length=255, null=True, verbose_name='Android SKU'),
),
migrations.AddField(
model_name='historicalcoursemode',
name='ios_sku',
field=models.CharField(blank=True, help_text='OPTIONAL: This is the iOS SKU registered on app store for this mode of the course. Leave this blank if the course has not yet been migrated to the ecommerce service.', max_length=255, null=True, verbose_name='iOS SKU'),
),
]

View File

@@ -35,6 +35,8 @@ Mode = namedtuple('Mode',
'expiration_datetime',
'description',
'sku',
'android_sku',
'ios_sku',
'bulk_sku',
])
@@ -114,6 +116,30 @@ class CourseMode(models.Model):
)
)
# Optional Android SKU for integration with mobile and the ecommerce service
android_sku = models.CharField(
max_length=255,
null=True,
blank=True,
verbose_name="Android SKU",
help_text=_(
"OPTIONAL: This is the Android SKU registered on play store for this mode of the course. "
"Leave this blank if the course has not yet been migrated to the ecommerce service."
)
)
# Optional iOS SKU for integration with mobile and the ecommerce service
ios_sku = models.CharField(
max_length=255,
null=True,
blank=True,
verbose_name="iOS SKU",
help_text=_(
"OPTIONAL: This is the iOS SKU registered on app store for this mode of the course. "
"Leave this blank if the course has not yet been migrated to the ecommerce service."
)
)
# Optional bulk order SKU for integration with the ecommerce service
bulk_sku = models.CharField(
max_length=255,
@@ -150,6 +176,8 @@ class CourseMode(models.Model):
settings.COURSE_MODE_DEFAULTS['expiration_datetime'],
settings.COURSE_MODE_DEFAULTS['description'],
settings.COURSE_MODE_DEFAULTS['sku'],
settings.COURSE_MODE_DEFAULTS['android_sku'],
settings.COURSE_MODE_DEFAULTS['ios_sku'],
settings.COURSE_MODE_DEFAULTS['bulk_sku'],
)
DEFAULT_MODE_SLUG = settings.COURSE_MODE_DEFAULTS['slug']
@@ -817,6 +845,8 @@ class CourseMode(models.Model):
self.expiration_datetime,
self.description,
self.sku,
self.android_sku,
self.ios_sku,
self.bulk_sku
)

View File

@@ -95,7 +95,7 @@ class CourseModeModelTest(TestCase):
self.create_mode('verified', 'Verified Certificate', 10)
modes = CourseMode.modes_for_course(self.course_key)
mode = Mode('verified', 'Verified Certificate', 10, '', 'usd', None, None, None, None)
mode = Mode('verified', 'Verified Certificate', 10, '', 'usd', None, None, None, None, None, None)
assert [mode] == modes
modes_dict = CourseMode.modes_for_course_dict(self.course_key)
@@ -106,8 +106,8 @@ class CourseModeModelTest(TestCase):
"""
Finding the modes when there's multiple modes
"""
mode1 = Mode('honor', 'Honor Code Certificate', 0, '', 'usd', None, None, None, None)
mode2 = Mode('verified', 'Verified Certificate', 10, '', 'usd', None, None, None, None)
mode1 = Mode('honor', 'Honor Code Certificate', 0, '', 'usd', None, None, None, None, None, None)
mode2 = Mode('verified', 'Verified Certificate', 10, '', 'usd', None, None, None, None, None, None)
set_modes = [mode1, mode2]
for mode in set_modes:
self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices)
@@ -126,14 +126,14 @@ class CourseModeModelTest(TestCase):
assert 0 == CourseMode.min_course_price_for_currency(self.course_key, 'usd')
# with mode with other currency, should get 0
mode = Mode('audit', 'Audit', 30, '', 'eur', None, None, None, None)
mode = Mode('audit', 'Audit', 30, '', 'eur', None, None, None, None, None, None)
self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices, mode.currency)
assert 0 == CourseMode.min_course_price_for_currency(self.course_key, 'usd')
# create some modes
mode1 = Mode('honor', 'Honor Code Certificate', 10, '', 'usd', None, None, None, None)
mode2 = Mode('verified', 'Verified Certificate', 20, '', 'usd', None, None, None, None)
mode3 = Mode('honor', 'Honor Code Certificate', 80, '', 'cny', None, None, None, None)
mode1 = Mode('honor', 'Honor Code Certificate', 10, '', 'usd', None, None, None, None, None, None)
mode2 = Mode('verified', 'Verified Certificate', 20, '', 'usd', None, None, None, None, None, None)
mode3 = Mode('honor', 'Honor Code Certificate', 80, '', 'cny', None, None, None, None, None, None)
set_modes = [mode1, mode2, mode3]
for mode in set_modes:
self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices, mode.currency)
@@ -148,7 +148,7 @@ class CourseModeModelTest(TestCase):
modes = CourseMode.modes_for_course(self.course_key)
assert [CourseMode.DEFAULT_MODE] == modes
mode1 = Mode('honor', 'Honor Code Certificate', 0, '', 'usd', None, None, None, None)
mode1 = Mode('honor', 'Honor Code Certificate', 0, '', 'usd', None, None, None, None, None, None)
self.create_mode(mode1.slug, mode1.name, mode1.min_price, mode1.suggested_prices)
modes = CourseMode.modes_for_course(self.course_key)
assert [mode1] == modes
@@ -165,6 +165,8 @@ class CourseModeModelTest(TestCase):
expiration_datetime,
None,
None,
None,
None,
None
)
modes = CourseMode.modes_for_course(self.course_key)

View File

@@ -407,7 +407,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
assert response.status_code == 200
expected_mode = [Mode('honor', 'Honor Code Certificate', 0, '', 'usd', None, None, None, None)]
expected_mode = [Mode('honor', 'Honor Code Certificate', 0, '', 'usd', None, None, None, None, None, None)]
course_mode = CourseMode.modes_for_course(self.course.id)
assert course_mode == expected_mode
@@ -441,6 +441,8 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
None,
None,
None,
None,
None,
None
)
]
@@ -466,8 +468,8 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
url = reverse('create_mode', args=[str(self.course.id)])
self.client.get(url, parameters)
honor_mode = Mode('honor', 'Honor Code Certificate', 0, '', 'usd', None, None, None, None)
verified_mode = Mode('verified', 'Verified Certificate', 10, '10,20', 'usd', None, None, None, None)
honor_mode = Mode('honor', 'Honor Code Certificate', 0, '', 'usd', None, None, None, None, None, None)
verified_mode = Mode('verified', 'Verified Certificate', 10, '10,20', 'usd', None, None, None, None, None, None)
expected_modes = [honor_mode, verified_mode]
course_modes = CourseMode.modes_for_course(self.course.id)

View File

@@ -6,6 +6,7 @@ Serializer for user API
from rest_framework import serializers
from rest_framework.reverse import reverse
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollment, User
from common.djangoapps.util.course import get_encoded_course_sharing_utm_params, get_link_for_about_page
from lms.djangoapps.certificates.api import certificate_downloadable_status
@@ -91,6 +92,7 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
course = CourseOverviewField(source="course_overview", read_only=True)
certificate = serializers.SerializerMethodField()
audit_access_expires = serializers.SerializerMethodField()
course_modes = serializers.SerializerMethodField()
def get_audit_access_expires(self, model):
"""
@@ -110,9 +112,22 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
else:
return {}
def get_course_modes(self, obj):
"""
Retrieve course modes associated with the course.
"""
course_modes = CourseMode.modes_for_course(
obj.course.id,
only_selectable=False
)
return [
ModeSerializer(mode).data
for mode in course_modes
]
class Meta:
model = CourseEnrollment
fields = ('audit_access_expires', 'created', 'mode', 'is_active', 'course', 'certificate')
fields = ('audit_access_expires', 'created', 'mode', 'is_active', 'course', 'certificate', 'course_modes')
lookup_field = 'username'
@@ -150,3 +165,18 @@ class UserSerializer(serializers.ModelSerializer):
lookup_field = 'username'
# For disambiguating within the drf-yasg swagger schema
ref_name = 'mobile_api.User'
class ModeSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serializes a course's 'Mode' tuples
Returns a serialized representation of the modes available for course enrollment. The course
modes models are designed to return a tuple instead of the model object itself. This serializer
handles the given tuple.
"""
slug = serializers.CharField(max_length=100)
sku = serializers.CharField()
android_sku = serializers.CharField()
ios_sku = serializers.CharField()

View File

@@ -1409,10 +1409,12 @@ WIKI_ENABLED = True
###
COURSE_MODE_DEFAULTS = {
'android_sku': None,
'bulk_sku': None,
'currency': 'usd',
'description': None,
'expiration_datetime': None,
'ios_sku': None,
'min_price': 0,
'name': _('Audit'),
'sku': None,