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
from course_discovery.apps.core.utils import serialize_datetime
......@@ -21,7 +18,7 @@ class TestSerializeSeatForEcommerceApi:
seat = SeatFactory(type=Seat.AUDIT)
actual = serialize_seat_for_ecommerce_api(seat)
expected = {
'expires': serialize_datetime(seat.upgrade_deadline),
'expires': serialize_datetime(seat.calculated_upgrade_deadline),
'price': str(seat.price),
'product_class': 'Seat',
'attribute_values': [
......@@ -38,14 +35,6 @@ class TestSerializeSeatForEcommerceApi:
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))
def test_serialize_seat_for_ecommerce_api_with_id_verification(self, 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.publisher.models import 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 {
'expires': serialize_datetime(upgrade_deadline),
'expires': serialize_datetime(seat.calculated_upgrade_deadline),
'price': str(seat.price),
'product_class': 'Seat',
'attribute_values': [
......
import datetime
import json
import random
import mock
import responses
from django.test import override_settings
from django.urls import reverse
from rest_framework.test import APITestCase
......@@ -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.tests.factories import CourseRunFactory, SeatFactory
PUBLISHER_UPGRADE_DEADLINE_DAYS = random.randint(1, 21)
class CourseRunViewSetTests(APITestCase):
def test_without_authentication(self):
......@@ -172,11 +177,44 @@ class CourseRunViewSetTests(APITestCase):
'currency': currency,
}
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,
price=professional_seat.price,
**common_seat_kwargs)
DiscoverySeat.objects.get(type=DiscoverySeat.VERIFIED, upgrade_deadline=verified_seat.upgrade_deadline,
price=verified_seat.price, **common_seat_kwargs)
DiscoverySeat.objects.get(
type=DiscoverySeat.PROFESSIONAL,
upgrade_deadline__isnull=True,
price=professional_seat.price,
**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):
self.client.force_login(StaffUserFactory())
......
......@@ -148,7 +148,7 @@ class CourseRunViewSet(viewsets.GenericViewSet):
currency=seat.currency,
defaults={
'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
from urllib.parse import urljoin
import waffle
from django.conf import settings
from django.contrib.auth.models import Group
from django.db import models
from django.urls import reverse
......@@ -350,7 +352,7 @@ class CourseRun(TimeStampedModel, ChangedByMixin):
@property
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:
return history_user.get_full_name() or history_user.username
......@@ -419,7 +421,6 @@ class CourseRun(TimeStampedModel, ChangedByMixin):
class Seat(TimeStampedModel, ChangedByMixin):
""" Seat model. """
HONOR = 'honor'
AUDIT = 'audit'
VERIFIED = 'verified'
......@@ -432,7 +433,7 @@ class Seat(TimeStampedModel, ChangedByMixin):
(AUDIT, _('Audit')),
(VERIFIED, _('Verified')),
(PROFESSIONAL, _('Professional (with ID verification)')),
(NO_ID_PROFESSIONAL, _('Professional (no ID verifiation)')),
(NO_ID_PROFESSIONAL, _('Professional (no ID verification)')),
(CREDIT, _('Credit')),
)
......@@ -467,6 +468,19 @@ class Seat(TimeStampedModel, ChangedByMixin):
(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):
""" Record additional metadata about a user. """
......
# pylint: disable=no-member
import datetime
import random
import ddt
import pytest
from django.db import IntegrityError
from django.test import TestCase
from django.urls import reverse
......@@ -353,21 +357,28 @@ class CourseTests(TestCase):
self.assertEqual(self.course.course_title, course_run.title_override)
class SeatTests(TestCase):
""" Tests for the publisher `Seat` model. """
def setUp(self):
super(SeatTests, self).setUp()
self.seat = factories.SeatFactory()
@pytest.mark.django_db
class TestSeatModel:
def test_str(self):
""" Verify casting an instance to a string returns a string containing the course title and seat type. """
self.assertEqual(
str(self.seat),
'{course}: {type}'.format(
course=self.seat.course_run.course.title, type=self.seat.type
)
)
seat = factories.SeatFactory()
assert str(seat) == '{course}: {type}'.format(course=seat.course_run.course.title, type=seat.type)
@pytest.mark.parametrize(
'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):
......
......@@ -7,14 +7,14 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
#: apps/api/filters.py
#, python-brace-format
......@@ -887,7 +887,7 @@ msgid "Professional (with ID verification)"
msgstr ""
#: apps/publisher/models.py
msgid "Professional (no ID verifiation)"
msgid "Professional (no ID verification)"
msgstr ""
#: apps/publisher/models.py
......
......@@ -7,14 +7,14 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: apps/api/filters.py
......@@ -1039,9 +1039,9 @@ msgstr ""
"Pröféssïönäl (wïth ÌD vérïfïçätïön) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#"
#: apps/publisher/models.py
msgid "Professional (no ID verifiation)"
msgid "Professional (no ID verification)"
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
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