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:
@@ -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'
|
||||
)
|
||||
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user