Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/djangoapps/course_modes/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ class CourseModeAdmin(admin.ModelAdmin):
'mode_slug',
'mode_display_name',
'min_price',
'price',
'currency',
'_expiration_datetime',
'verification_deadline',
Expand All @@ -202,6 +203,7 @@ class CourseModeAdmin(admin.ModelAdmin):
'course',
'mode_slug',
'min_price',
'price',
'expiration_datetime_custom',
'sku',
'bulk_sku'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 2.2.18 on 2021-02-04 16:59

from decimal import Decimal
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('course_modes', '0013_auto_20200115_2022'),
]

operations = [
migrations.AddField(
model_name='coursemode',
name='price',
field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=30, verbose_name='Price'),
),
migrations.AddField(
model_name='coursemodesarchive',
name='price',
field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=30),
),
migrations.AddField(
model_name='historicalcoursemode',
name='price',
field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=30, verbose_name='Price'),
),
]
10 changes: 10 additions & 0 deletions common/djangoapps/course_modes/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from collections import defaultdict, namedtuple
from datetime import timedelta
from decimal import Decimal

import inspect # lint-amnesty, pylint: disable=unused-import
import logging
Expand Down Expand Up @@ -34,6 +35,7 @@
'slug',
'name',
'min_price',
'price',
'suggested_prices',
'currency',
'expiration_datetime',
Expand Down Expand Up @@ -71,6 +73,9 @@ class CourseMode(models.Model):
# really just the price of the course.
min_price = models.IntegerField(default=0, verbose_name=_("Price"))

# This price is the same as min_price, but this is saved as a Decimal to accomodate courses that have a price with decimals (ie. 49.99) for propper display and consistency with ecommerce db, since min_price is currently an integer. This will eventually become the only price column in this model.
price = models.DecimalField(default=Decimal('0.00'), verbose_name=_("Price"), max_digits=30, decimal_places=2)

# the currency these prices are in, using lower case ISO currency codes
currency = models.CharField(default=u"usd", max_length=8)

Expand Down Expand Up @@ -146,6 +151,7 @@ class CourseMode(models.Model):
settings.COURSE_MODE_DEFAULTS['slug'],
settings.COURSE_MODE_DEFAULTS['name'],
settings.COURSE_MODE_DEFAULTS['min_price'],
settings.COURSE_MODE_DEFAULTS['price'],
settings.COURSE_MODE_DEFAULTS['suggested_prices'],
settings.COURSE_MODE_DEFAULTS['currency'],
settings.COURSE_MODE_DEFAULTS['expiration_datetime'],
Expand Down Expand Up @@ -801,6 +807,7 @@ def to_tuple(self):
self.mode_slug,
self.mode_display_name,
self.min_price,
self.price,
self.suggested_prices,
self.currency,
self.expiration_datetime,
Expand Down Expand Up @@ -907,6 +914,9 @@ class Meta(object):
# minimum price in USD that we would like to charge for this mode of the course
min_price = models.IntegerField(default=0)

# This price is the same as min_price, but this is saved as a Decimal to accomodate courses that have a price with decimals (ie. 49.99) for propper display and consistency with ecommerce db, since min_price is currently an integer. This will eventually become the only price column in this model.
price = models.DecimalField(default=Decimal('0.00'), max_digits=30, decimal_places=2)

# the suggested prices for this mode
suggested_prices = models.CharField(max_length=255, blank=True, default=u'',
validators=[validate_comma_separated_integer_list])
Expand Down
1 change: 1 addition & 0 deletions common/djangoapps/course_modes/rest_api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class CourseModeSerializer(serializers.Serializer):
mode_slug = serializers.CharField()
mode_display_name = serializers.CharField()
min_price = serializers.IntegerField(required=False)
price = serializers.DecimalField(required=False, max_digits=30, decimal_places=2)
currency = serializers.CharField()
expiration_datetime = serializers.DateTimeField(required=False)
expiration_datetime_is_explicit = serializers.BooleanField(required=False)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import unittest

import ddt
from decimal import Decimal
from django.conf import settings
from django.urls import reverse
from opaque_keys.edx.keys import CourseKey
Expand Down Expand Up @@ -134,6 +135,7 @@ def test_list_course_modes_happy_path(self):
'mode_slug': 'audit',
'mode_display_name': 'Audit',
'min_price': 0,
'price': Decimal('0.00'),
'currency': 'usd',
'expiration_datetime': None,
'expiration_datetime_is_explicit': False,
Expand All @@ -146,6 +148,7 @@ def test_list_course_modes_happy_path(self):
'mode_slug': 'verified',
'mode_display_name': 'Verified',
'min_price': 25,
'price': Decimal('25.00'),
'currency': 'usd',
'expiration_datetime': None,
'expiration_datetime_is_explicit': False,
Expand Down Expand Up @@ -286,6 +289,7 @@ def test_retrieve_course_mode_happy_path(self):
'mode_slug': 'audit',
'mode_display_name': 'Audit',
'min_price': 0,
'price': Decimal('0.00'),
'currency': 'usd',
'expiration_datetime': None,
'expiration_datetime_is_explicit': False,
Expand Down
1 change: 1 addition & 0 deletions common/djangoapps/course_modes/rest_api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class CourseModesView(CourseModesMixin, ListCreateAPIView):
* mode_display_name: The verbose name for the course mode.
* min_price: The minimum price for which a user can
enroll in this mode.
* price: The minimum price for which a user can enroll in this mode, as a decimal. This will eventually replace min_price.
* currency: The currency of the listed prices.
* expiration_datetime: The date and time after which
users cannot enroll in the course in this mode (not required for POST).
Expand Down
2 changes: 2 additions & 0 deletions common/djangoapps/course_modes/tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def test_expiration_timezone(self):
'mode_slug': 'verified',
'mode_display_name': 'verified',
'min_price': 10,
"price": 10.00,
'currency': 'usd',
'_expiration_datetime_0': expiration.date(), # due to django admin datetime widget passing as separate vals
'_expiration_datetime_1': expiration.time(),
Expand Down Expand Up @@ -208,6 +209,7 @@ def _admin_form(self, mode, upgrade_deadline=None):
"_expiration_datetime": upgrade_deadline,
"currency": "usd",
"min_price": 10,
"price": 10.00,
}, instance=course_mode)

def _set_form_verification_deadline(self, form, deadline):
Expand Down
24 changes: 14 additions & 10 deletions common/djangoapps/course_modes/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import itertools
from datetime import timedelta
from decimal import Decimal

import ddt
from django.core.exceptions import ValidationError
Expand Down Expand Up @@ -51,6 +52,7 @@ def create_mode(
mode_slug,
mode_name,
min_price=0,
price=Decimal('0.00'),
suggested_prices='',
currency='usd',
expiration_datetime=None,
Expand All @@ -63,14 +65,15 @@ def create_mode(
mode_display_name=mode_name,
mode_slug=mode_slug,
min_price=min_price,
price=price,
suggested_prices=suggested_prices,
currency=currency,
_expiration_datetime=expiration_datetime,
)

def test_save(self):
""" Verify currency is always lowercase. """
cm, __ = self.create_mode('honor', 'honor', 0, '', 'USD')
cm, __ = self.create_mode('honor', 'honor', 0, 0.00, '', 'USD')
self.assertEqual(cm.currency, 'usd')

cm.currency = 'GHS'
Expand All @@ -92,7 +95,7 @@ def test_nodes_for_course_single(self):

self.create_mode('verified', 'Verified Certificate', 10)
modes = CourseMode.modes_for_course(self.course_key)
mode = Mode(u'verified', u'Verified Certificate', 10, '', 'usd', None, None, None, None)
mode = Mode(u'verified', u'Verified Certificate', 10, 10.00, '', 'usd', None, None, None, None)
self.assertEqual([mode], modes)

modes_dict = CourseMode.modes_for_course_dict(self.course_key)
Expand All @@ -104,11 +107,11 @@ def test_modes_for_course_multiple(self):
"""
Finding the modes when there's multiple modes
"""
mode1 = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None, None)
mode2 = Mode(u'verified', u'Verified Certificate', 10, '', 'usd', None, None, None, None)
mode1 = Mode(u'honor', u'Honor Code Certificate', 0, 0.00, '', 'usd', None, None, None, None)
mode2 = Mode(u'verified', u'Verified Certificate', 10, 10.00, '', 'usd', 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)
self.create_mode(mode.slug, mode.name, mode.min_price, mode.price, mode.suggested_prices)

modes = CourseMode.modes_for_course(self.course_key)
self.assertEqual(modes, set_modes)
Expand All @@ -124,12 +127,12 @@ def test_min_course_price_for_currency(self):
self.assertEqual(0, CourseMode.min_course_price_for_currency(self.course_key, 'usd'))

# create some modes
mode1 = Mode(u'honor', u'Honor Code Certificate', 10, '', 'usd', None, None, None, None)
mode2 = Mode(u'verified', u'Verified Certificate', 20, '', 'usd', None, None, None, None)
mode3 = Mode(u'honor', u'Honor Code Certificate', 80, '', 'cny', None, None, None, None)
mode1 = Mode(u'honor', u'Honor Code Certificate', 10, 10.00, '', 'usd', None, None, None, None)
mode2 = Mode(u'verified', u'Verified Certificate', 20, 20.00, '', 'usd', None, None, None, None)
mode3 = Mode(u'honor', u'Honor Code Certificate', 80, 80.00, '', 'cny', 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)
self.create_mode(mode.slug, mode.name, mode.min_price, mode.price, mode.suggested_prices, mode.currency)

self.assertEqual(10, CourseMode.min_course_price_for_currency(self.course_key, 'usd'))
self.assertEqual(80, CourseMode.min_course_price_for_currency(self.course_key, 'cny'))
Expand All @@ -141,7 +144,7 @@ def test_modes_for_course_expired(self):
modes = CourseMode.modes_for_course(self.course_key)
self.assertEqual([CourseMode.DEFAULT_MODE], modes)

mode1 = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None, None)
mode1 = Mode(u'honor', u'Honor Code Certificate', 0, 0.00, '', 'usd', 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)
self.assertEqual([mode1], modes)
Expand All @@ -153,6 +156,7 @@ def test_modes_for_course_expired(self):
u'verified',
u'Verified Certificate',
10,
10.00,
'',
'usd',
expiration_datetime,
Expand Down
19 changes: 11 additions & 8 deletions common/djangoapps/course_modes/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
"""


import decimal
import unittest
from datetime import datetime, timedelta
from decimal import Decimal

import ddt
import freezegun
Expand Down Expand Up @@ -366,7 +366,7 @@ def test_remember_donation_for_course(self):
self.assertIn(six.text_type(self.course.id), self.client.session['donation_for_course'])

actual_amount = self.client.session['donation_for_course'][six.text_type(self.course.id)]
expected_amount = decimal.Decimal(self.POST_PARAMS_FOR_COURSE_MODE['verified']['contribution'])
expected_amount = Decimal(self.POST_PARAMS_FOR_COURSE_MODE['verified']['contribution'])
self.assertEqual(actual_amount, expected_amount)

def test_successful_default_enrollment(self):
Expand Down Expand Up @@ -410,22 +410,23 @@ def test_default_mode_creation(self):

self.assertEqual(response.status_code, 200)

expected_mode = [Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None, None)]
expected_mode = [Mode(u'honor', u'Honor Code Certificate', 0, Decimal('0.00'), '', 'usd', None, None, None, None)]
course_mode = CourseMode.modes_for_course(self.course.id)

self.assertEqual(course_mode, expected_mode)

@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@ddt.data(
(u'verified', u'Verified Certificate', 10, '10,20,30', 'usd'),
(u'professional', u'Professional Education', 100, '100,200', 'usd'),
(u'verified', u'Verified Certificate', 10, Decimal('10.00'), '10,20,30', 'usd'),
(u'professional', u'Professional Education', 100, Decimal('100.00'), '100,200', 'usd'),
)
@ddt.unpack
def test_verified_mode_creation(self, mode_slug, mode_display_name, min_price, suggested_prices, currency):
def test_verified_mode_creation(self, mode_slug, mode_display_name, min_price, price, suggested_prices, currency):
parameters = {}
parameters['mode_slug'] = mode_slug
parameters['mode_display_name'] = mode_display_name
parameters['min_price'] = min_price
parameters['price'] = price
parameters['suggested_prices'] = suggested_prices
parameters['currency'] = currency

Expand All @@ -439,6 +440,7 @@ def test_verified_mode_creation(self, mode_slug, mode_display_name, min_price, s
mode_slug,
mode_display_name,
min_price,
price,
suggested_prices,
currency,
None,
Expand All @@ -463,14 +465,15 @@ def test_multiple_mode_creation(self):
parameters['mode_slug'] = u'verified'
parameters['mode_display_name'] = u'Verified Certificate'
parameters['min_price'] = 10
parameters['price'] = Decimal('10.00')
parameters['suggested_prices'] = '10,20'

# Create a verified mode
url = reverse('create_mode', args=[six.text_type(self.course.id)])
self.client.get(url, parameters)

honor_mode = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None, None)
verified_mode = Mode(u'verified', u'Verified Certificate', 10, '10,20', 'usd', None, None, None, None)
honor_mode = Mode(u'honor', u'Honor Code Certificate', 0, Decimal('0.00'), '', 'usd', None, None, None, None)
verified_mode = Mode(u'verified', u'Verified Certificate', 10, Decimal('10.00'), '10,20', 'usd', None, None, None, None)
expected_modes = [honor_mode, verified_mode]
course_modes = CourseMode.modes_for_course(self.course.id)

Expand Down
1 change: 1 addition & 0 deletions lms/djangoapps/commerce/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class CourseModeSerializer(serializers.ModelSerializer):
""" CourseMode serializer. """
name = serializers.CharField(source='mode_slug')
price = serializers.IntegerField(source='min_price')
price_decimal = serializers.DecimalField(source='min_price', max_digits=30, decimal_places=2)
expires = serializers.DateTimeField(
source='expiration_datetime',
required=False,
Expand Down
8 changes: 8 additions & 0 deletions lms/djangoapps/commerce/api/v1/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import itertools
import json
from datetime import datetime, timedelta
from decimal import Decimal

import ddt
import pytz
Expand Down Expand Up @@ -63,6 +64,7 @@ def _serialize_course_mode(cls, course_mode):
u'name': course_mode.mode_slug,
u'currency': course_mode.currency.lower(),
u'price': course_mode.min_price,
u'price_decimal': course_mode.min_price,
u'sku': course_mode.sku,
u'bulk_sku': course_mode.bulk_sku,
u'expires': cls._serialize_datetime(course_mode.expiration_datetime),
Expand Down Expand Up @@ -157,6 +159,7 @@ def _get_update_response_and_expected_data(self, mode_expiration, verification_d
expected_course_mode = CourseMode(
mode_slug=u'verified',
min_price=200,
price=Decimal('200.00'),
currency=u'USD',
sku=u'ABC123',
bulk_sku=u'BULK-ABC123',
Expand Down Expand Up @@ -228,6 +231,7 @@ def test_update_remove_verification_deadline(self):
verified_mode = CourseMode(
mode_slug=u'verified',
min_price=200,
price=Decimal('200.00'),
currency=u'USD',
sku=u'ABC123',
bulk_sku=u'BULK-ABC123',
Expand All @@ -253,6 +257,7 @@ def test_update_verification_deadline_left_alone(self):
verified_mode = CourseMode(
mode_slug=u'verified',
min_price=200,
price=Decimal('200.00'),
currency=u'USD',
sku=u'ABC123',
bulk_sku=u'BULK-ABC123',
Expand Down Expand Up @@ -298,6 +303,7 @@ def test_update_overwrite(self):
course_id=self.course.id,
mode_slug=u'masters',
min_price=10000,
price=Decimal('10000.00'),
currency=u'USD',
sku=u'DEF456',
bulk_sku=u'BULK-DEF456'
Expand All @@ -306,6 +312,7 @@ def test_update_overwrite(self):
course_id=self.course.id,
mode_slug=u'credit',
min_price=500,
price=Decimal('500.00'),
currency=u'USD',
sku=u'ABC123',
bulk_sku=u'BULK-ABC123'
Expand Down Expand Up @@ -341,6 +348,7 @@ def test_update_professional_expiration(self, mode_slug, expiration_datetime_nam
CourseMode(
mode_slug=mode_slug,
min_price=500,
price=Decimal('500.00'),
currency=u'USD',
sku=u'ABC123',
bulk_sku=u'BULK-ABC123',
Expand Down
Loading