Commit c0157d1d by Clinton Blackburn

Updated Publisher upgrade deadline logic

The upgrade deadline published to external services now follows edX business logic. Only verified seats have an upgrade deadline. If no value is set, one is calculated based on the course run's end date.

EDUCATOR-1465
parent cde5deca
import datetime
import random
import pytest import pytest
from course_discovery.apps.core.utils import serialize_datetime from course_discovery.apps.core.utils import serialize_datetime
...@@ -21,7 +18,7 @@ class TestSerializeSeatForEcommerceApi: ...@@ -21,7 +18,7 @@ class TestSerializeSeatForEcommerceApi:
seat = SeatFactory(type=Seat.AUDIT) seat = SeatFactory(type=Seat.AUDIT)
actual = serialize_seat_for_ecommerce_api(seat) actual = serialize_seat_for_ecommerce_api(seat)
expected = { expected = {
'expires': serialize_datetime(seat.upgrade_deadline), 'expires': serialize_datetime(seat.calculated_upgrade_deadline),
'price': str(seat.price), 'price': str(seat.price),
'product_class': 'Seat', 'product_class': 'Seat',
'attribute_values': [ 'attribute_values': [
...@@ -38,14 +35,6 @@ class TestSerializeSeatForEcommerceApi: ...@@ -38,14 +35,6 @@ class TestSerializeSeatForEcommerceApi:
assert actual == expected assert actual == expected
def test_serialize_seat_for_ecommerce_api_without_upgrade_deadline(self, settings):
settings.PUBLISHER_UPGRADE_DEADLINE_DAYS = random.randint(1, 21)
now = datetime.datetime.utcnow()
seat = SeatFactory(upgrade_deadline=None, course_run__end=now)
actual = serialize_seat_for_ecommerce_api(seat)
assert actual['expires'] == serialize_datetime(
now - datetime.timedelta(days=settings.PUBLISHER_UPGRADE_DEADLINE_DAYS))
@pytest.mark.parametrize('seat_type', (Seat.VERIFIED, Seat.PROFESSIONAL)) @pytest.mark.parametrize('seat_type', (Seat.VERIFIED, Seat.PROFESSIONAL))
def test_serialize_seat_for_ecommerce_api_with_id_verification(self, seat_type): def test_serialize_seat_for_ecommerce_api_with_id_verification(self, seat_type):
seat = SeatFactory(type=seat_type) seat = SeatFactory(type=seat_type)
......
import datetime
from django.conf import settings
from course_discovery.apps.core.utils import serialize_datetime from course_discovery.apps.core.utils import serialize_datetime
from course_discovery.apps.publisher.models import Seat from course_discovery.apps.publisher.models import Seat
def serialize_seat_for_ecommerce_api(seat): def serialize_seat_for_ecommerce_api(seat):
upgrade_deadline = seat.upgrade_deadline
if not upgrade_deadline:
upgrade_deadline = seat.course_run.end - datetime.timedelta(days=settings.PUBLISHER_UPGRADE_DEADLINE_DAYS)
return { return {
'expires': serialize_datetime(upgrade_deadline), 'expires': serialize_datetime(seat.calculated_upgrade_deadline),
'price': str(seat.price), 'price': str(seat.price),
'product_class': 'Seat', 'product_class': 'Seat',
'attribute_values': [ 'attribute_values': [
......
import datetime
import json import json
import random
import mock import mock
import responses import responses
from django.test import override_settings
from django.urls import reverse from django.urls import reverse
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
...@@ -17,6 +20,8 @@ from course_discovery.apps.publisher.api.v1.views import CourseRunViewSet ...@@ -17,6 +20,8 @@ from course_discovery.apps.publisher.api.v1.views import CourseRunViewSet
from course_discovery.apps.publisher.models import Seat from course_discovery.apps.publisher.models import Seat
from course_discovery.apps.publisher.tests.factories import CourseRunFactory, SeatFactory from course_discovery.apps.publisher.tests.factories import CourseRunFactory, SeatFactory
PUBLISHER_UPGRADE_DEADLINE_DAYS = random.randint(1, 21)
class CourseRunViewSetTests(APITestCase): class CourseRunViewSetTests(APITestCase):
def test_without_authentication(self): def test_without_authentication(self):
...@@ -172,11 +177,44 @@ class CourseRunViewSetTests(APITestCase): ...@@ -172,11 +177,44 @@ class CourseRunViewSetTests(APITestCase):
'currency': currency, 'currency': currency,
} }
DiscoverySeat.objects.get(type=DiscoverySeat.AUDIT, upgrade_deadline__isnull=True, **common_seat_kwargs) DiscoverySeat.objects.get(type=DiscoverySeat.AUDIT, upgrade_deadline__isnull=True, **common_seat_kwargs)
DiscoverySeat.objects.get(type=DiscoverySeat.PROFESSIONAL, upgrade_deadline=professional_seat.upgrade_deadline, DiscoverySeat.objects.get(
price=professional_seat.price, type=DiscoverySeat.PROFESSIONAL,
**common_seat_kwargs) upgrade_deadline__isnull=True,
DiscoverySeat.objects.get(type=DiscoverySeat.VERIFIED, upgrade_deadline=verified_seat.upgrade_deadline, price=professional_seat.price,
price=verified_seat.price, **common_seat_kwargs) **common_seat_kwargs
)
DiscoverySeat.objects.get(
type=DiscoverySeat.VERIFIED,
upgrade_deadline=verified_seat.upgrade_deadline,
price=verified_seat.price,
**common_seat_kwargs
)
# pylint: disable=unused-argument,too-many-statements
@responses.activate
@override_settings(PUBLISHER_UPGRADE_DEADLINE_DAYS=PUBLISHER_UPGRADE_DEADLINE_DAYS)
@mock.patch.object(Partner, 'access_token', return_value='JWT fake')
def test_publish_seat_without_upgrade_deadline(self, mock_access_token):
publisher_course_run = self._create_course_run_for_publication()
verified_seat = SeatFactory(type=Seat.VERIFIED, course_run=publisher_course_run, upgrade_deadline=None)
partner = publisher_course_run.course.organizations.first().partner
self._set_test_client_domain_and_login(partner)
self._mock_studio_api_success(publisher_course_run)
self._mock_ecommerce_api(publisher_course_run)
url = reverse('publisher:api:v1:course_run-publish', kwargs={'pk': publisher_course_run.pk})
response = self.client.post(url, {})
assert response.status_code == 200
discovery_course_run = CourseRun.objects.get(key=publisher_course_run.lms_course_id)
DiscoverySeat.objects.get(
type=DiscoverySeat.VERIFIED,
upgrade_deadline=publisher_course_run.end - datetime.timedelta(days=PUBLISHER_UPGRADE_DEADLINE_DAYS),
price=verified_seat.price,
course_run=discovery_course_run
)
def test_publish_missing_course_run(self): def test_publish_missing_course_run(self):
self.client.force_login(StaffUserFactory()) self.client.force_login(StaffUserFactory())
......
...@@ -148,7 +148,7 @@ class CourseRunViewSet(viewsets.GenericViewSet): ...@@ -148,7 +148,7 @@ class CourseRunViewSet(viewsets.GenericViewSet):
currency=seat.currency, currency=seat.currency,
defaults={ defaults={
'price': seat.price, 'price': seat.price,
'upgrade_deadline': seat.upgrade_deadline, 'upgrade_deadline': seat.calculated_upgrade_deadline,
} }
) )
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-10-04 05:21
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('publisher', '0059_auto_20170928_0425'),
]
operations = [
migrations.AlterField(
model_name='historicalseat',
name='type',
field=models.CharField(choices=[('honor', 'Honor'), ('audit', 'Audit'), ('verified', 'Verified'), ('professional', 'Professional (with ID verification)'), ('no-id-professional', 'Professional (no ID verification)'), ('credit', 'Credit')], max_length=63, verbose_name='Seat type'),
),
migrations.AlterField(
model_name='seat',
name='type',
field=models.CharField(choices=[('honor', 'Honor'), ('audit', 'Audit'), ('verified', 'Verified'), ('professional', 'Professional (with ID verification)'), ('no-id-professional', 'Professional (no ID verification)'), ('credit', 'Credit')], max_length=63, verbose_name='Seat type'),
),
]
import datetime
import logging import logging
from urllib.parse import urljoin from urllib.parse import urljoin
import waffle import waffle
from django.conf import settings
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
...@@ -350,7 +352,7 @@ class CourseRun(TimeStampedModel, ChangedByMixin): ...@@ -350,7 +352,7 @@ class CourseRun(TimeStampedModel, ChangedByMixin):
@property @property
def created_by(self): def created_by(self):
history_user = self.history.order_by('history_date').first().history_user # pylint: disable=no-member history_user = self.history.order_by('history_date').first().history_user # pylint: disable=no-member
if history_user: if history_user:
return history_user.get_full_name() or history_user.username return history_user.get_full_name() or history_user.username
...@@ -419,7 +421,6 @@ class CourseRun(TimeStampedModel, ChangedByMixin): ...@@ -419,7 +421,6 @@ class CourseRun(TimeStampedModel, ChangedByMixin):
class Seat(TimeStampedModel, ChangedByMixin): class Seat(TimeStampedModel, ChangedByMixin):
""" Seat model. """
HONOR = 'honor' HONOR = 'honor'
AUDIT = 'audit' AUDIT = 'audit'
VERIFIED = 'verified' VERIFIED = 'verified'
...@@ -432,7 +433,7 @@ class Seat(TimeStampedModel, ChangedByMixin): ...@@ -432,7 +433,7 @@ class Seat(TimeStampedModel, ChangedByMixin):
(AUDIT, _('Audit')), (AUDIT, _('Audit')),
(VERIFIED, _('Verified')), (VERIFIED, _('Verified')),
(PROFESSIONAL, _('Professional (with ID verification)')), (PROFESSIONAL, _('Professional (with ID verification)')),
(NO_ID_PROFESSIONAL, _('Professional (no ID verifiation)')), (NO_ID_PROFESSIONAL, _('Professional (no ID verification)')),
(CREDIT, _('Credit')), (CREDIT, _('Credit')),
) )
...@@ -467,6 +468,19 @@ class Seat(TimeStampedModel, ChangedByMixin): ...@@ -467,6 +468,19 @@ class Seat(TimeStampedModel, ChangedByMixin):
(self.type == self.CREDIT and self.credit_price > 0 and self.price > 0) (self.type == self.CREDIT and self.credit_price > 0 and self.price > 0)
) )
@property
def calculated_upgrade_deadline(self):
""" Returns upgraded deadline calculated using edX business logic.
Only verified seats have upgrade deadlines. If the instance does not have an upgrade deadline set, the value
will be calculated based on the related course run's end date.
"""
if self.type == self.VERIFIED:
return self.upgrade_deadline or (
self.course_run.end - datetime.timedelta(days=settings.PUBLISHER_UPGRADE_DEADLINE_DAYS))
return None
class UserAttributes(TimeStampedModel): class UserAttributes(TimeStampedModel):
""" Record additional metadata about a user. """ """ Record additional metadata about a user. """
......
# pylint: disable=no-member # pylint: disable=no-member
import datetime
import random
import ddt import ddt
import pytest
from django.db import IntegrityError from django.db import IntegrityError
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
...@@ -353,21 +357,28 @@ class CourseTests(TestCase): ...@@ -353,21 +357,28 @@ class CourseTests(TestCase):
self.assertEqual(self.course.course_title, course_run.title_override) self.assertEqual(self.course.course_title, course_run.title_override)
class SeatTests(TestCase): @pytest.mark.django_db
""" Tests for the publisher `Seat` model. """ class TestSeatModel:
def setUp(self):
super(SeatTests, self).setUp()
self.seat = factories.SeatFactory()
def test_str(self): def test_str(self):
""" Verify casting an instance to a string returns a string containing the course title and seat type. """ seat = factories.SeatFactory()
self.assertEqual( assert str(seat) == '{course}: {type}'.format(course=seat.course_run.course.title, type=seat.type)
str(self.seat),
'{course}: {type}'.format( @pytest.mark.parametrize(
course=self.seat.course_run.course.title, type=self.seat.type 'seat_type', [choice[0] for choice in Seat.SEAT_TYPE_CHOICES if choice[0] != Seat.VERIFIED])
) def test_calculated_upgrade_deadline_with_nonverified_seat(self, seat_type):
) seat = factories.SeatFactory(type=seat_type)
assert seat.calculated_upgrade_deadline is None
def test_calculated_upgrade_deadline_with_verified_seat(self, settings):
settings.PUBLISHER_UPGRADE_DEADLINE_DAYS = random.randint(1, 21)
now = datetime.datetime.utcnow()
seat = factories.SeatFactory(type=Seat.VERIFIED, upgrade_deadline=None, course_run__end=now)
expected = now - datetime.timedelta(days=settings.PUBLISHER_UPGRADE_DEADLINE_DAYS)
assert seat.calculated_upgrade_deadline == expected
seat = factories.SeatFactory(type=Seat.VERIFIED)
assert seat.calculated_upgrade_deadline is not None
assert seat.calculated_upgrade_deadline == seat.upgrade_deadline
class UserAttributeTests(TestCase): class UserAttributeTests(TestCase):
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-28 12:52-0400\n" "POT-Creation-Date: 2017-10-04 01:21-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
#: apps/api/filters.py #: apps/api/filters.py
#, python-brace-format #, python-brace-format
...@@ -887,7 +887,7 @@ msgid "Professional (with ID verification)" ...@@ -887,7 +887,7 @@ msgid "Professional (with ID verification)"
msgstr "" msgstr ""
#: apps/publisher/models.py #: apps/publisher/models.py
msgid "Professional (no ID verifiation)" msgid "Professional (no ID verification)"
msgstr "" msgstr ""
#: apps/publisher/models.py #: apps/publisher/models.py
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-28 12:52-0400\n" "POT-Creation-Date: 2017-10-04 01:21-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: apps/api/filters.py #: apps/api/filters.py
...@@ -1039,9 +1039,9 @@ msgstr "" ...@@ -1039,9 +1039,9 @@ msgstr ""
"Pröféssïönäl (wïth ÌD vérïfïçätïön) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#" "Pröféssïönäl (wïth ÌD vérïfïçätïön) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#"
#: apps/publisher/models.py #: apps/publisher/models.py
msgid "Professional (no ID verifiation)" msgid "Professional (no ID verification)"
msgstr "" msgstr ""
"Pröféssïönäl (nö ÌD vérïfïätïön) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тє#" "Pröféssïönäl (nö ÌD vérïfïçätïön) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тє#"
#: apps/publisher/models.py #: apps/publisher/models.py
msgid "Organization Role" msgid "Organization Role"
......
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