Commit 80f51474 by Uman Shahzad Committed by Douglas Hall

Administration UI for Enterprise Offers.

parent 8779027b
......@@ -41,11 +41,11 @@
exclude: ['js/common']
},
{
name: 'js/pages/program_offer_list_page',
name: 'js/pages/offer_list_page',
exclude: ['js/common']
},
{
name: 'js/pages/program_offer_form_page',
name: 'js/pages/offer_form_page',
exclude: ['js/common']
},
{
......
......@@ -268,10 +268,10 @@ class CouponOfferViewTests(ApiMockMixin, CouponMixin, DiscoveryTestMixin, Enterp
)
@ddt.data(
('', 'If you have concerns about sharing your data, please contact your administrator at TestShib.'),
('', 'If you have concerns about sharing your data, please contact your administrator at BigEnterprise.'),
(
'contact@example.com',
'If you have concerns about sharing your data, please contact your administrator at TestShib at '
'If you have concerns about sharing your data, please contact your administrator at BigEnterprise at '
'contact@example.com.',
),
)
......@@ -282,7 +282,6 @@ class CouponOfferViewTests(ApiMockMixin, CouponMixin, DiscoveryTestMixin, Enterp
self.mock_access_token_response()
self.mock_specific_enterprise_customer_api(
ENTERPRISE_CUSTOMER,
name='TestShib',
contact_email=contact_email
)
base_url = self.prepare_url_for_credit_seat(enterprise_customer=ENTERPRISE_CUSTOMER)
......@@ -551,7 +550,7 @@ class CouponRedeemViewTests(CouponMixin, DiscoveryTestMixin, LmsApiMockMixin, En
@httpretty.activate
def test_enterprise_customer_successful_redemption_message(self):
""" Verify the info message appears on successful redemption. """
expected_message = '<i class="fa fa-info-circle"></i> A discount has been applied, courtesy of TestShib.'
expected_message = '<i class="fa fa-info-circle"></i> A discount has been applied, courtesy of BigEnterprise.'
# Setting benefit value to a low amount to ensure the basket is not free,
# and calls to the checkout page do not redirect away from the checkout page.
......
......@@ -11,7 +11,6 @@ from slumber.exceptions import SlumberHttpBaseException
from ecommerce.core.utils import get_cache_key
logger = logging.getLogger(__name__)
......@@ -88,7 +87,7 @@ def fetch_enterprise_learner_data(site, user):
{
"enterprise_customer": {
"uuid": "cf246b88-d5f6-4908-a522-fc307e0b0c59",
"name": "TestShib",
"name": "BigEnterprise",
"catalog": 2,
"active": true,
"site": {
......
from django.utils.translation import ugettext_lazy as _
from oscar.core.loading import get_model
from ecommerce.enterprise.benefits import EnterpriseAbsoluteDiscountBenefit, EnterprisePercentageDiscountBenefit
Benefit = get_model('offer', 'Benefit')
BENEFIT_MAP = {
Benefit.FIXED: EnterpriseAbsoluteDiscountBenefit,
Benefit.PERCENTAGE: EnterprisePercentageDiscountBenefit,
}
BENEFIT_TYPE_CHOICES = (
(Benefit.PERCENTAGE, _('Percentage')),
(Benefit.FIXED, _('Absolute')),
)
# Waffle switch used to enable/disable Enterprise offers.
ENTERPRISE_OFFERS_SWITCH = 'enable_enterprise_offers'
# -*- coding: utf-8 -*-
# TODO: Refactor this to consolidate it with `ecommerce.programs.forms`.
from django import forms
from django.forms.utils import ErrorList
from django.utils.translation import ugettext_lazy as _
from oscar.core.loading import get_model
from ecommerce.enterprise.conditions import EnterpriseCustomerCondition
from ecommerce.enterprise.constants import BENEFIT_MAP, BENEFIT_TYPE_CHOICES
from ecommerce.enterprise.utils import get_enterprise_customer
from ecommerce.programs.custom import class_path, create_condition
Benefit = get_model('offer', 'Benefit')
Condition = get_model('offer', 'Condition')
ConditionalOffer = get_model('offer', 'ConditionalOffer')
Range = get_model('offer', 'Range')
class EnterpriseOfferForm(forms.ModelForm):
enterprise_customer_uuid = forms.UUIDField(required=True, label=_('Enterprise Customer UUID'))
enterprise_customer_catalog_uuid = forms.UUIDField(required=False, label=_('Enterprise Customer Catalog UUID'))
benefit_type = forms.ChoiceField(choices=BENEFIT_TYPE_CHOICES, label=_('Discount Type'))
benefit_value = forms.DecimalField(
required=True, decimal_places=2, max_digits=12, min_value=0, label=_('Discount Value')
)
class Meta(object):
model = ConditionalOffer
fields = [
'enterprise_customer_uuid', 'enterprise_customer_catalog_uuid', 'start_datetime', 'end_datetime',
'benefit_type', 'benefit_value'
]
help_texts = {
'end_datetime': '',
}
labels = {
'start_datetime': _('Start Date'),
'end_datetime': _('End Date'),
}
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=ErrorList,
label_suffix=None, empty_permitted=False, instance=None, request=None):
initial = initial or {}
self.request = request
if instance:
initial.update({
'enterprise_customer_uuid': instance.condition.enterprise_customer_uuid,
'enterprise_customer_catalog_uuid': instance.condition.enterprise_customer_catalog_uuid,
'benefit_type': instance.benefit.proxy().benefit_class_type,
'benefit_value': instance.benefit.value,
})
super(EnterpriseOfferForm, self).__init__(data, files, auto_id, prefix, initial, error_class, label_suffix,
empty_permitted, instance)
date_ui_class = {'class': 'add-pikaday'}
self.fields['start_datetime'].widget.attrs.update(date_ui_class)
self.fields['end_datetime'].widget.attrs.update(date_ui_class)
def clean(self):
cleaned_data = super(EnterpriseOfferForm, self).clean()
start_datetime = cleaned_data.get('start_datetime')
end_datetime = cleaned_data.get('end_datetime')
enterprise_customer_uuid = cleaned_data.get('enterprise_customer_uuid')
enterprise_customer_catalog_uuid = cleaned_data.get('enterprise_customer_catalog_uuid')
if not self.instance.pk and enterprise_customer_uuid and enterprise_customer_catalog_uuid:
enterprise_offer_exists = ConditionalOffer.objects.filter(
offer_type=ConditionalOffer.SITE,
condition__enterprise_customer_uuid=enterprise_customer_uuid,
condition__enterprise_customer_catalog_uuid=enterprise_customer_catalog_uuid,
).exists()
if enterprise_offer_exists:
for field in ['enterprise_customer_uuid', 'enterprise_customer_catalog_uuid']:
self.add_error(field, _('An offer already exists for this Enterprise & Catalog combination.'))
if cleaned_data['benefit_type'] == Benefit.PERCENTAGE and cleaned_data.get('benefit_value') > 100:
self.add_error('benefit_value', _('Percentage discounts cannot be greater than 100%.'))
if end_datetime and not start_datetime:
self.add_error('start_datetime', _('A start date must be specified when specifying an end date.'))
if start_datetime and end_datetime and start_datetime > end_datetime:
self.add_error('start_datetime', _('The start date must occur before the end date.'))
return cleaned_data
def save(self, commit=True):
enterprise_customer_uuid = self.cleaned_data['enterprise_customer_uuid']
enterprise_customer_catalog_uuid = self.cleaned_data['enterprise_customer_catalog_uuid']
site = self.request.site
enterprise_customer = get_enterprise_customer(site, enterprise_customer_uuid)
enterprise_customer_name = enterprise_customer['name']
self.instance.name = _(u'Discount provided by {enterprise_customer_name}.'.format(
enterprise_customer_name=enterprise_customer_name
))
self.instance.status = ConditionalOffer.OPEN
self.instance.offer_type = ConditionalOffer.SITE
self.instance.max_basket_applications = 1
self.instance.site = site
self.instance.priority = 10 # This will ensure that Enterprise Offers are applied before Program Offers.
if commit:
benefit = getattr(self.instance, 'benefit', Benefit())
benefit.proxy_class = class_path(BENEFIT_MAP[self.cleaned_data['benefit_type']])
benefit.value = self.cleaned_data['benefit_value']
benefit.save()
self.instance.benefit = benefit
if hasattr(self.instance, 'condition'):
self.instance.condition.enterprise_customer_uuid = enterprise_customer_uuid
self.instance.condition.enterprise_customer_name = enterprise_customer_name
self.instance.condition.enterprise_customer_catalog_uuid = enterprise_customer_catalog_uuid
self.instance.condition.save()
else:
self.instance.condition = create_condition(
EnterpriseCustomerCondition,
enterprise_customer_uuid=enterprise_customer_uuid,
enterprise_customer_name=enterprise_customer_name,
enterprise_customer_catalog_uuid=enterprise_customer_catalog_uuid,
)
return super(EnterpriseOfferForm, self).save(commit)
{% extends 'edx/base.html' %}
{% load crispy_forms_tags %}
{% load i18n %}
{% load staticfiles %}
{% block title %}
{% if editing %}
{% blocktrans trimmed with enterprise_customer_name=enterprise_customer.name %}
Edit Enterprise Offer: {{ enterprise_customer_name }}
{% endblocktrans %}
{% else %}
{% trans "Create Enterprise Offer" %}
{% endif %}
{% endblock %}
{% block stylesheets %}
<link rel="stylesheet" href="{% static 'bower_components/pikaday/css/pikaday.css' %}" type="text/x-scss">
{% endblock %}
{% block navbar %}
{% include "edx/partials/_staff_navbar.html" %}
{% include "edx/partials/_administration_menu.html" %}
{% endblock navbar %}
{% block content %}
<div class="container">
<ol class="breadcrumb">
<li><a href="{% url 'enterprise:offers:list' %}">{% trans "Enterprise Offers" %}</a></li>
{% if editing %}
<li>{{ enterprise_customer.name }}</li>
<li>{% trans "Edit" %}</li>
{% else %}
<li>{% trans "Create" %}</li>
{% endif %}
</ol>
{% include 'partials/alert_messages.html' %}
<div class="page-header">
<h1 class="hd-1 emphasized">
{% if editing %}
{% trans "Edit Enterprise Offer" %}
{% else %}
{% trans "Create Enterprise Offer" %}
{% endif %}
</h1>
</div>
<form id="offerForm" method="post">
{% csrf_token %}
{{ form|crispy }}
<div class="form-actions">
<input type="submit" class="btn btn-primary" value="{% if editing %}
{% trans "Save Changes" %}
{% else %}
{% trans "Create Enterprise Offer" %}
{% endif %}">
<a class="btn btn-default" href="{% url 'enterprise:offers:list' %}">{% trans "Cancel" %}</a>
</div>
</form>
</div>
{% endblock %}
{% block footer %}
<footer class="footer">
<div class="container">
<div class="row">
<div class="col-xs-12 text-right">
<em>{% blocktrans %}{{ platform_name }} Enterprise Offer Administration Tool{% endblocktrans %}</em>
</div>
</div>
</div>
</footer>
{% endblock footer %}
{% block javascript %}
<script src="{% static 'js/pages/offer_form_page.js' %}"></script>
{% endblock %}
{% extends 'edx/base.html' %}
{% load i18n %}
{% load offer_tags %}
{% load staticfiles %}
{% block title %}{% trans "Enterprise Offers" %}{% endblock %}
{% block navbar %}
{% include "edx/partials/_staff_navbar.html" %}
{% include "edx/partials/_administration_menu.html" %}
{% endblock navbar %}
{% block content %}
<div class="container">
<div class="page-header">
<h1 class="hd-1 emphasized">
{% trans "Enterprise Offers" %}
<div class="pull-right">
<a href="{% url 'enterprise:offers:new' %}"
class="btn btn-primary btn-small">{% trans "Create Enterprise Offer" %}</a>
</div>
</h1>
</div>
<table id="offerTable" class="copy copy-base table table-striped table-bordered" cellspacing="0">
<caption class="sr-only">{% trans "Current enterprise offers" %}</caption>
<thead>
<tr>
<th>{% trans 'Enterprise Customer Name' %}</th>
<th>{% trans 'Enterprise Customer UUID' %}</th>
<th>{% trans 'Enterprise Customer Catalog UUID' %}</th>
<th>{% trans 'Type' %}</th>
<th>{% trans 'Value' %}</th>
<th>{% trans 'Start' %}</th>
<th>{% trans 'End' %}</th>
</tr>
</thead>
<tbody>
{% for offer in object_list %}
<tr>
<td>
<a href="{% url 'enterprise:offers:edit' pk=offer.pk %}">{{ offer.condition.enterprise_customer_name }}</a>
</td>
<td>{{ offer.condition.enterprise_customer_uuid }}</td>
<td>{{ offer.condition.enterprise_customer_catalog_uuid }}</td>
<td>{{ offer.benefit|benefit_type|capfirst }}</td>
<td>{{ offer.benefit.value }}</td>
<td>{{ offer.start_datetime|default_if_none:'--' }}</td>
<td>{{ offer.end_datetime|default_if_none:'--' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% block footer %}
<footer class="footer">
<div class="container">
<div class="row">
<div class="col-xs-12 text-right">
<em>{% blocktrans %}{{ platform_name }} Enterprise Offer Administration Tool{% endblocktrans %}</em>
</div>
</div>
</div>
</footer>
{% endblock footer %}
{% block javascript %}
<script src="{% static 'js/pages/offer_list_page.js' %}"></script>
{% endblock %}
......@@ -67,6 +67,7 @@ class EnterpriseServiceMockMixin(object):
}
enterprise_customer_api_response_json = json.dumps(enterprise_customer_api_response)
self.mock_access_token_response()
httpretty.register_uri(
method=httpretty.GET,
uri=self.ENTERPRISE_CUSTOMER_URL,
......@@ -74,12 +75,12 @@ class EnterpriseServiceMockMixin(object):
content_type='application/json'
)
def mock_specific_enterprise_customer_api(self, uuid, name='TestShib', contact_email='', consent_enabled=True):
def mock_specific_enterprise_customer_api(self, uuid, name='BigEnterprise', contact_email='', consent_enabled=True):
"""
Helper function to register the enterprise customer API endpoint.
"""
enterprise_customer_api_response = {
'uuid': uuid,
'uuid': str(uuid),
'name': name,
'catalog': 0,
'active': True,
......@@ -103,6 +104,7 @@ class EnterpriseServiceMockMixin(object):
}
enterprise_customer_api_response_json = json.dumps(enterprise_customer_api_response)
self.mock_access_token_response()
httpretty.register_uri(
method=httpretty.GET,
uri='{}{}/'.format(self.ENTERPRISE_CUSTOMER_URL, uuid),
......@@ -119,6 +121,7 @@ class EnterpriseServiceMockMixin(object):
}
enterprise_customer_api_response_json = json.dumps(enterprise_customer_api_response)
self.mock_access_token_response()
httpretty.register_uri(
method=httpretty.GET,
uri='{}{}/'.format(self.ENTERPRISE_CUSTOMER_URL, uuid),
......@@ -149,7 +152,7 @@ class EnterpriseServiceMockMixin(object):
'id': learner_id,
'enterprise_customer': {
'uuid': enterprise_customer_uuid,
'name': 'TestShib',
'name': 'BigEnterprise',
'catalog': catalog_id,
'active': True,
'site': {
......@@ -197,6 +200,7 @@ class EnterpriseServiceMockMixin(object):
}
enterprise_learner_api_response_json = json.dumps(enterprise_learner_api_response)
self.mock_access_token_response()
httpretty.register_uri(
method=httpretty.GET,
uri=self.ENTERPRISE_LEARNER_URL,
......@@ -214,6 +218,7 @@ class EnterpriseServiceMockMixin(object):
}
enterprise_learner_api_response_json = json.dumps(enterprise_learner_api_response)
self.mock_access_token_response()
httpretty.register_uri(
method=httpretty.POST,
uri=self.ENTERPRISE_LEARNER_URL,
......@@ -237,6 +242,7 @@ class EnterpriseServiceMockMixin(object):
}
enterprise_learner_api_response_json = json.dumps(enterprise_learner_api_response)
self.mock_access_token_response()
httpretty.register_uri(
method=httpretty.GET,
uri=self.ENTERPRISE_LEARNER_URL,
......@@ -258,7 +264,7 @@ class EnterpriseServiceMockMixin(object):
'invalid-unexpected-key': {
'enterprise_customer': {
'uuid': 'cf246b88-d5f6-4908-a522-fc307e0b0c59',
'name': 'TestShib',
'name': 'BigEnterprise',
'catalog': 1,
'active': True,
'site': {
......@@ -281,6 +287,7 @@ class EnterpriseServiceMockMixin(object):
}
enterprise_learner_api_response_json = json.dumps(enterprise_learner_api_response)
self.mock_access_token_response()
httpretty.register_uri(
method=httpretty.GET,
uri=self.ENTERPRISE_LEARNER_URL,
......@@ -302,7 +309,7 @@ class EnterpriseServiceMockMixin(object):
{
'enterprise_customer': {
'uuid': 'cf246b88-d5f6-4908-a522-fc307e0b0c59',
'name': 'TestShib',
'name': 'BigEnterprise',
'catalog': 1,
'active': True,
'site': {
......@@ -324,6 +331,7 @@ class EnterpriseServiceMockMixin(object):
}
enterprise_learner_api_response_json = json.dumps(enterprise_learner_api_response)
self.mock_access_token_response()
httpretty.register_uri(
method=httpretty.GET,
uri=self.ENTERPRISE_LEARNER_URL,
......@@ -347,6 +355,7 @@ class EnterpriseServiceMockMixin(object):
Helper function to register enterprise learner API endpoint for a
failure.
"""
self.mock_access_token_response()
httpretty.register_uri(
method=httpretty.GET,
uri=self.ENTERPRISE_LEARNER_URL,
......@@ -357,6 +366,7 @@ class EnterpriseServiceMockMixin(object):
"""
Helper function to return 500 error while accessing learner entitlements api endpoint.
"""
self.mock_access_token_response()
httpretty.register_uri(
method=httpretty.GET,
uri='{base_url}{learner_id}/entitlements/'.format(
......@@ -381,6 +391,7 @@ class EnterpriseServiceMockMixin(object):
}
learner_entitlements_json = json.dumps(enterprise_learner_entitlements_api_response)
self.mock_access_token_response()
httpretty.register_uri(
method=httpretty.GET,
uri='{base_url}{learner_id}/entitlements/'.format(
......@@ -426,6 +437,7 @@ class EnterpriseServiceMockMixin(object):
}
enterprise_enrollment_api_response_json = json.dumps(enterprise_enrollment_api_response)
self.mock_access_token_response()
httpretty.register_uri(
method=httpretty.GET,
uri=self.ENTERPRISE_COURSE_ENROLLMENT_URL,
......@@ -452,6 +464,8 @@ class EnterpriseServiceMockMixin(object):
'consent_required': required,
'exists': exists,
}
self.mock_access_token_response()
httpretty.register_uri(
method=method,
uri=self.site.siteconfiguration.build_lms_url('/consent/api/v1/data_sharing_consent'),
......
# -*- coding: utf-8 -*-
import uuid
import httpretty
from oscar.core.loading import get_model
from ecommerce.enterprise.constants import BENEFIT_MAP
from ecommerce.enterprise.forms import EnterpriseOfferForm
from ecommerce.enterprise.tests.mixins import EnterpriseServiceMockMixin
from ecommerce.extensions.test import factories
from ecommerce.programs.custom import class_path
from ecommerce.tests.testcases import TestCase
Benefit = get_model('offer', 'Benefit')
ConditionalOffer = get_model('offer', 'ConditionalOffer')
class EnterpriseOfferFormTests(EnterpriseServiceMockMixin, TestCase):
def generate_data(self, **kwargs):
data = {
'enterprise_customer_uuid': uuid.uuid4(),
'enterprise_customer_name': 'BigEnterprise',
'enterprise_customer_catalog_uuid': uuid.uuid4(),
'benefit_type': Benefit.PERCENTAGE,
'benefit_value': 22,
}
data.update(**kwargs)
return data
def assert_enterprise_offer_conditions(self, offer, enterprise_customer_uuid, enterprise_customer_name,
enterprise_customer_catalog_uuid, expected_benefit_value,
expected_benefit_type, expected_name):
""" Assert the given offer's parameters match the expected values. """
self.assertEqual(str(offer.name), expected_name)
self.assertEqual(offer.offer_type, ConditionalOffer.SITE)
self.assertEqual(offer.status, ConditionalOffer.OPEN)
self.assertEqual(offer.max_basket_applications, 1)
self.assertEqual(offer.site, self.site)
self.assertEqual(offer.condition.enterprise_customer_uuid, enterprise_customer_uuid)
self.assertEqual(offer.condition.enterprise_customer_name, enterprise_customer_name)
self.assertEqual(offer.condition.enterprise_customer_catalog_uuid, enterprise_customer_catalog_uuid)
self.assertEqual(offer.benefit.proxy_class, class_path(BENEFIT_MAP[expected_benefit_type]))
self.assertEqual(offer.benefit.value, expected_benefit_value)
def assert_form_errors(self, data, expected_errors):
""" Assert that form validation fails with the expected errors. """
form = EnterpriseOfferForm(data=data)
self.assertFalse(form.is_valid())
self.assertEqual(form.errors, expected_errors)
def test_init(self):
""" The constructor should pull initial data from the passed-in instance. """
enterprise_offer = factories.EnterpriseOfferFactory()
form = EnterpriseOfferForm(instance=enterprise_offer)
self.assertEqual(
form['enterprise_customer_uuid'].value(),
enterprise_offer.condition.enterprise_customer_uuid.hex
)
self.assertEqual(
form['enterprise_customer_catalog_uuid'].value(),
enterprise_offer.condition.enterprise_customer_catalog_uuid.hex
)
self.assertEqual(form['benefit_type'].value(), enterprise_offer.benefit.proxy().benefit_class_type)
self.assertEqual(form['benefit_value'].value(), enterprise_offer.benefit.value)
def test_clean_percentage(self):
""" If a percentage benefit type is specified, the benefit value must never be greater than 100. """
data = self.generate_data(benefit_type=Benefit.PERCENTAGE, benefit_value=101)
self.assert_form_errors(data, {'benefit_value': ['Percentage discounts cannot be greater than 100%.']})
def test_clean_with_missing_start_date(self):
""" If an end date is specified, a start date must also be specified. """
data = self.generate_data(end_datetime='2017-01-01 00:00:00')
self.assert_form_errors(
data,
{'start_datetime': ['A start date must be specified when specifying an end date.']}
)
def test_clean_with_invalid_date_ordering(self):
""" The start date must always occur before the end date. """
data = self.generate_data(start_datetime='2017-01-02 00:00:00', end_datetime='2017-01-01 00:00:00')
self.assert_form_errors(data, {'start_datetime': ['The start date must occur before the end date.']})
def test_clean_with_conflicting_enterprise_customer_and_catalog_uuids(self):
""" If an offer already exists for the given Enterprise and Catalog, an error should be raised. """
offer = factories.EnterpriseOfferFactory()
data = self.generate_data(
enterprise_customer_uuid=offer.condition.enterprise_customer_uuid,
enterprise_customer_name=offer.condition.enterprise_customer_name,
enterprise_customer_catalog_uuid=offer.condition.enterprise_customer_catalog_uuid,
)
self.assert_form_errors(
data,
{
'enterprise_customer_uuid': [
'An offer already exists for this Enterprise & Catalog combination.'
],
'enterprise_customer_catalog_uuid': [
'An offer already exists for this Enterprise & Catalog combination.'
]
}
)
@httpretty.activate
def test_save_create(self):
""" A new ConditionalOffer, Benefit, and Condition should be created. """
data = self.generate_data()
self.mock_specific_enterprise_customer_api(data['enterprise_customer_uuid'])
form = EnterpriseOfferForm(request=self.request, data=data)
form.is_valid()
offer = form.save()
self.assert_enterprise_offer_conditions(
offer,
data['enterprise_customer_uuid'],
data['enterprise_customer_name'],
data['enterprise_customer_catalog_uuid'],
data['benefit_value'],
data['benefit_type'],
'Discount provided by {}.'.format(data['enterprise_customer_name']),
)
@httpretty.activate
def test_save_create_special_char_title(self):
""" When the Enterprise's name is international, new objects should still be created."""
enterprise_customer_uuid = uuid.uuid4()
data = self.generate_data(
enterprise_customer_uuid=enterprise_customer_uuid,
enterprise_customer_name=u'Sp\xe1nish Enterprise',
)
self.mock_specific_enterprise_customer_api(data['enterprise_customer_uuid'], name=u'Sp\xe1nish Enterprise')
form = EnterpriseOfferForm(request=self.request, data=data)
form.is_valid()
offer = form.save()
self.assert_enterprise_offer_conditions(
offer,
data['enterprise_customer_uuid'],
data['enterprise_customer_name'],
data['enterprise_customer_catalog_uuid'],
data['benefit_value'],
data['benefit_type'],
'Discount provided by Spánish Enterprise.'
)
@httpretty.activate
def test_save_edit(self):
""" Previously-created ConditionalOffer, Benefit, and Condition instances should be updated. """
offer = factories.EnterpriseOfferFactory()
data = self.generate_data(
enterprise_customer_uuid=offer.condition.enterprise_customer_uuid,
benefit_type=Benefit.FIXED
)
self.mock_specific_enterprise_customer_api(data['enterprise_customer_uuid'])
form = EnterpriseOfferForm(request=self.request, data=data, instance=offer)
form.is_valid()
form.save()
offer.refresh_from_db()
self.assert_enterprise_offer_conditions(
offer,
data['enterprise_customer_uuid'],
data['enterprise_customer_name'],
data['enterprise_customer_catalog_uuid'],
data['benefit_value'],
data['benefit_type'],
'Discount provided by {}.'.format(data['enterprise_customer_name']),
)
@httpretty.activate
def test_save_without_commit(self):
""" No data should be persisted to the database if the commit kwarg is set to False. """
data = self.generate_data()
form = EnterpriseOfferForm(request=self.request, data=data)
self.mock_specific_enterprise_customer_api(data['enterprise_customer_uuid'])
form.is_valid()
instance = form.save(commit=False)
self.assertIsNone(instance.pk)
self.assertFalse(hasattr(instance, 'benefit'))
self.assertFalse(hasattr(instance, 'condition'))
@httpretty.activate
def test_save_offer_name(self):
""" If a request object is sent, the offer name should include the enterprise name. """
data = self.generate_data()
self.mock_specific_enterprise_customer_api(data['enterprise_customer_uuid'])
form = EnterpriseOfferForm(request=self.request, data=data)
form.is_valid()
offer = form.save()
self.assert_enterprise_offer_conditions(
offer,
data['enterprise_customer_uuid'],
data['enterprise_customer_name'],
data['enterprise_customer_catalog_uuid'],
data['benefit_value'],
data['benefit_type'],
'Discount provided by {}.'.format(data['enterprise_customer_name']),
)
def test_create_when_conditional_offer_with_uuid_exists(self):
"""
An Enterprise Offer can be created if a conditional offer with different type and same UUIDs already exists.
"""
data = self.generate_data()
factories.EnterpriseOfferFactory(
condition__enterprise_customer_uuid=data['enterprise_customer_uuid'],
condition__enterprise_customer_name=data['enterprise_customer_name'],
condition__enterprise_customer_catalog_uuid=data['enterprise_customer_catalog_uuid'],
offer_type=ConditionalOffer.VOUCHER,
)
form = EnterpriseOfferForm(request=self.request, data=data)
self.assertTrue(form.is_valid())
......@@ -2,6 +2,7 @@ import httpretty
import mock
from django.conf import settings
from django.template import Context, Template
from oscar.core.loading import get_model
from ecommerce.core.tests import toggle_switch
from ecommerce.coupons.tests.mixins import CouponMixin
......@@ -9,6 +10,7 @@ from ecommerce.enterprise.exceptions import EnterpriseDoesNotExist
from ecommerce.enterprise.tests.mixins import EnterpriseServiceMockMixin
from ecommerce.tests.testcases import TestCase
Benefit = get_model('offer', 'Benefit')
TEST_ENTERPRISE_CUSTOMER_UUID = 'cf246b88-d5f6-4908-a522-fc307e0b0c59'
......
import uuid
import httpretty
from django.core.urlresolvers import reverse
from oscar.core.loading import get_model
from ecommerce.enterprise.benefits import EnterprisePercentageDiscountBenefit
from ecommerce.enterprise.tests.mixins import EnterpriseServiceMockMixin
from ecommerce.extensions.test import factories
from ecommerce.programs.custom import class_path
from ecommerce.tests.testcases import TestCase, ViewTestMixin
Benefit = get_model('offer', 'Benefit')
ConditionalOffer = get_model('offer', 'ConditionalOffer')
class EnterpriseOfferListViewTests(EnterpriseServiceMockMixin, ViewTestMixin, TestCase):
path = reverse('enterprise:offers:list')
def setUp(self):
super(EnterpriseOfferListViewTests, self).setUp()
httpretty.enable()
self.mock_access_token_response()
def tearDown(self):
super(EnterpriseOfferListViewTests, self).tearDown()
httpretty.disable()
httpretty.reset()
def test_get(self):
""" The context should contain a list of enterprise offers. """
# These should be ignored since their associated Condition objects do NOT have an Enterprise Customer UUID.
factories.ConditionalOfferFactory.create_batch(3)
enterprise_offers = factories.EnterpriseOfferFactory.create_batch(4, site=self.site)
for offer in enterprise_offers:
self.mock_specific_enterprise_customer_api(offer.condition.enterprise_customer_uuid)
response = self.assert_get_response_status(200)
self.assertEqual(list(response.context['object_list']), enterprise_offers)
# The page should load even if the Enterprise API is inaccessible
httpretty.disable()
response = self.assert_get_response_status(200)
self.assertEqual(list(response.context['object_list']), enterprise_offers)
def test_get_queryset(self):
""" Should return only Conditional Offers with Site offer type. """
# Conditional Offer should contain a condition with enterprise customer uuid set in order to be returned
site_conditional_offer = factories.EnterpriseOfferFactory(site=self.site)
# Conditional Offer with null Site or non-matching Site should not be returned
null_site_offer = factories.EnterpriseOfferFactory()
different_site_offer = factories.EnterpriseOfferFactory(site=factories.SiteConfigurationFactory().site)
enterprise_offers = [
site_conditional_offer,
factories.EnterpriseOfferFactory(offer_type=ConditionalOffer.VOUCHER),
factories.ConditionalOfferFactory(offer_type=ConditionalOffer.SITE),
null_site_offer,
different_site_offer
]
for offer in enterprise_offers:
self.mock_specific_enterprise_customer_api(offer.condition.enterprise_customer_uuid)
response = self.client.get(self.path)
self.assertEqual(list(response.context['object_list']), [site_conditional_offer])
class EnterpriseOfferUpdateViewTests(EnterpriseServiceMockMixin, ViewTestMixin, TestCase):
def setUp(self):
super(EnterpriseOfferUpdateViewTests, self).setUp()
self.enterprise_offer = factories.EnterpriseOfferFactory(site=self.site)
self.path = reverse('enterprise:offers:edit', kwargs={'pk': self.enterprise_offer.pk})
# NOTE: We activate httpretty here so that we don't have to decorate every test method.
httpretty.enable()
self.mock_specific_enterprise_customer_api(self.enterprise_offer.condition.enterprise_customer_uuid)
def tearDown(self):
super(EnterpriseOfferUpdateViewTests, self).tearDown()
httpretty.disable()
httpretty.reset()
def test_get(self):
""" The context should contain the enterprise offer. """
response = self.assert_get_response_status(200)
self.assertEqual(response.context['object'], self.enterprise_offer)
# The page should load even if the Enterprise API is inaccessible
httpretty.disable()
response = self.assert_get_response_status(200)
self.assertEqual(response.context['object'], self.enterprise_offer)
def test_post(self):
""" The enterprise offer should be updated. """
data = {
'enterprise_customer_uuid': self.enterprise_offer.condition.enterprise_customer_uuid,
'enterprise_customer_catalog_uuid': self.enterprise_offer.condition.enterprise_customer_catalog_uuid,
'benefit_type': self.enterprise_offer.benefit.proxy().benefit_class_type,
'benefit_value': self.enterprise_offer.benefit.value,
}
response = self.client.post(self.path, data, follow=False)
self.assertRedirects(response, self.path)
@httpretty.activate
class EnterpriseOfferCreateViewTests(EnterpriseServiceMockMixin, ViewTestMixin, TestCase):
path = reverse('enterprise:offers:new')
def test_post(self):
""" A new enterprise offer should be created. """
expected_ec_uuid = uuid.uuid4()
expected_ec_catalog_uuid = uuid.uuid4()
self.mock_specific_enterprise_customer_api(expected_ec_uuid)
expected_benefit_value = 10
data = {
'enterprise_customer_uuid': expected_ec_uuid,
'enterprise_customer_catalog_uuid': expected_ec_catalog_uuid,
'benefit_type': Benefit.PERCENTAGE,
'benefit_value': expected_benefit_value,
}
response = self.client.post(self.path, data, follow=False)
enterprise_offer = ConditionalOffer.objects.get()
self.assertRedirects(response, reverse('enterprise:offers:edit', kwargs={'pk': enterprise_offer.pk}))
self.assertIsNone(enterprise_offer.start_datetime)
self.assertIsNone(enterprise_offer.end_datetime)
self.assertEqual(enterprise_offer.condition.enterprise_customer_uuid, expected_ec_uuid)
self.assertEqual(enterprise_offer.condition.enterprise_customer_catalog_uuid, expected_ec_catalog_uuid)
self.assertEqual(enterprise_offer.benefit.type, '')
self.assertEqual(enterprise_offer.benefit.value, expected_benefit_value)
self.assertEqual(enterprise_offer.benefit.proxy_class, class_path(EnterprisePercentageDiscountBenefit))
from django.conf.urls import include, url
from ecommerce.enterprise import views
OFFER_URLS = [
url(r'^$', views.EnterpriseOfferListView.as_view(), name='list'),
url(r'^new/$', views.EnterpriseOfferCreateView.as_view(), name='new'),
url(r'^(?P<pk>[\d]+)/edit/$', views.EnterpriseOfferUpdateView.as_view(), name='edit'),
]
urlpatterns = [
url(r'^offers/', include(OFFER_URLS, namespace='offers')),
]
......@@ -13,7 +13,8 @@ from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from edx_rest_api_client.client import EdxRestApiClient
from oscar.core.loading import get_model
from slumber.exceptions import HttpNotFoundError
from requests.exceptions import ConnectionError, Timeout
from slumber.exceptions import SlumberHttpBaseException
from ecommerce.core.utils import traverse_pagination
from ecommerce.enterprise.exceptions import EnterpriseDoesNotExist
......@@ -62,7 +63,7 @@ def get_enterprise_customer(site, uuid):
try:
response = client.get()
except HttpNotFoundError:
except (ConnectionError, SlumberHttpBaseException, Timeout):
return None
return {
'name': response['name'],
......
# TODO: Refactor this to consolidate it with `ecommerce.programs.views`.
import logging
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from django.views.generic import CreateView, ListView, UpdateView
from oscar.core.loading import get_model
from ecommerce.core.views import StaffOnlyMixin
from ecommerce.enterprise.forms import EnterpriseOfferForm
from ecommerce.enterprise.utils import get_enterprise_customer
Benefit = get_model('offer', 'Benefit')
ConditionalOffer = get_model('offer', 'ConditionalOffer')
logger = logging.getLogger(__name__)
class EnterpriseOfferViewMixin(StaffOnlyMixin):
model = ConditionalOffer
def get_context_data(self, **kwargs):
context = super(EnterpriseOfferViewMixin, self).get_context_data(**kwargs)
context['admin'] = 'enterprise_offers'
return context
def get_queryset(self):
return super(EnterpriseOfferViewMixin, self).get_queryset().filter(
site=self.request.site.id,
condition__enterprise_customer_uuid__isnull=False,
offer_type=ConditionalOffer.SITE
)
class EnterpriseOfferProcessFormViewMixin(EnterpriseOfferViewMixin):
form_class = EnterpriseOfferForm
success_message = _('Enterprise offer updated!')
def get_form_kwargs(self):
kwargs = super(EnterpriseOfferProcessFormViewMixin, self).get_form_kwargs()
kwargs.update({'request': self.request})
return kwargs
def get_context_data(self, **kwargs):
context = super(EnterpriseOfferProcessFormViewMixin, self).get_context_data(**kwargs)
context.update({
'editing': False,
})
return context
def get_success_url(self):
messages.add_message(self.request, messages.SUCCESS, self.success_message)
return reverse('enterprise:offers:edit', kwargs={'pk': self.object.pk})
class EnterpriseOfferCreateView(EnterpriseOfferProcessFormViewMixin, CreateView):
initial = {
'benefit_type': Benefit.PERCENTAGE,
}
success_message = _('Enterprise offer created!')
template_name = 'enterprise/enterpriseoffer_form.html'
class EnterpriseOfferUpdateView(EnterpriseOfferProcessFormViewMixin, UpdateView):
template_name = 'enterprise/enterpriseoffer_form.html'
def get_context_data(self, **kwargs):
context = super(EnterpriseOfferUpdateView, self).get_context_data(**kwargs)
context.update({
'editing': True,
'enterprise_customer': get_enterprise_customer(
self.request.site,
self.object.condition.enterprise_customer_uuid
)
})
return context
class EnterpriseOfferListView(EnterpriseOfferViewMixin, ListView):
template_name = 'enterprise/enterpriseoffer_list.html'
# NOTE (CCB): These functions are copied from oscar.apps.offer.custom due to a bug
# detailed at https://github.com/django-oscar/django-oscar/issues/2345. This file
# should be removed after the fix for the bug is released.
# TODO: Issue above is fixed; we need to upgrade to django-oscar==1.5 and this can be removed.
# (https://github.com/django-oscar/django-oscar/commit/38367f9ca854cd21eaf19a174f24b59a0e65cf79)
from oscar.core.loading import get_model
Condition = get_model('offer', 'Condition')
......
......@@ -47,7 +47,7 @@
</h1>
</div>
<form id="programOfferForm" method="post">
<form id="offerForm" method="post">
{% csrf_token %}
{{ form|crispy }}
......@@ -78,5 +78,5 @@
{% block javascript %}
<script src="{% static 'js/pages/program_offer_form_page.js' %}"></script>
<script src="{% static 'js/pages/offer_form_page.js' %}"></script>
{% endblock %}
......@@ -22,7 +22,7 @@
</h1>
</div>
<table id="programOfferTable" class="copy copy-base table table-striped table-bordered" cellspacing="0">
<table id="offerTable" class="copy copy-base table table-striped table-bordered" cellspacing="0">
<caption class="sr-only">{% trans "Current program offers" %}</caption>
<thead>
<tr>
......@@ -63,5 +63,5 @@
{% endblock footer %}
{% block javascript %}
<script src="{% static 'js/pages/program_offer_list_page.js' %}"></script>
<script src="{% static 'js/pages/offer_list_page.js' %}"></script>
{% endblock %}
import uuid
import httpretty
from django.conf import settings
from django.core.urlresolvers import reverse
from oscar.core.loading import get_model
......@@ -9,45 +8,12 @@ from ecommerce.extensions.test import factories
from ecommerce.programs.benefits import PercentageDiscountBenefitWithoutRange
from ecommerce.programs.custom import class_path
from ecommerce.programs.tests.mixins import ProgramTestMixin
from ecommerce.tests.testcases import CacheMixin, TestCase
from ecommerce.tests.testcases import TestCase, ViewTestMixin
Benefit = get_model('offer', 'Benefit')
ConditionalOffer = get_model('offer', 'ConditionalOffer')
class ViewTestMixin(CacheMixin):
path = None
def setUp(self):
super(ViewTestMixin, self).setUp()
user = self.create_user(is_staff=True)
self.client.login(username=user.username, password=self.password)
def assert_get_response_status(self, status_code):
""" Asserts the HTTP status of a GET responses matches the expected status. """
response = self.client.get(self.path)
self.assertEqual(response.status_code, status_code)
return response
def test_login_required(self):
""" Users are required to login before accessing the view. """
self.client.logout()
response = self.assert_get_response_status(302)
self.assertIn(settings.LOGIN_URL, response.url)
def test_staff_only(self):
""" The view should only be accessible to staff. """
self.client.logout()
user = self.create_user(is_staff=False)
self.client.login(username=user.username, password=self.password)
self.assert_get_response_status(404)
user.is_staff = True
user.save()
self.assert_get_response_status(200)
class ProgramOfferListViewTests(ProgramTestMixin, ViewTestMixin, TestCase):
path = reverse('programs:offers:list')
......
......@@ -6,7 +6,7 @@ require([
'use strict';
$(function() {
$('#programOfferForm').find('.add-pikaday').each(function() {
$('#offerForm').find('.add-pikaday').each(function() {
new Pikaday({
field: this,
format: 'YYYY-MM-DD HH:mm:ss',
......
......@@ -6,7 +6,7 @@ require([
'use strict';
$(function() {
$('#programOfferTable').DataTable({
$('#offerTable').DataTable({
paging: true
});
});
......
......@@ -7,5 +7,8 @@
<a {% if admin == "program_offers" %}class="selected"{% endif %} href="{% url 'programs:offers:list' %}">
{% trans "Program Offers" %}
</a>
<a {% if admin == "enterprise_offers" %}class="selected"{% endif %} href="{% url 'enterprise:offers:list' %}">
{% trans "Enterprise Offers" %}
</a>
</div>
</nav>
from django.conf import settings
from django.core.cache import cache
from django.test import LiveServerTestCase as DjangoLiveServerTestCase
from django.test import TestCase as DjangoTestCase
......@@ -16,6 +17,39 @@ class CacheMixin(object):
super(CacheMixin, self).tearDown()
class ViewTestMixin(CacheMixin):
path = None
def setUp(self):
super(ViewTestMixin, self).setUp()
user = self.create_user(is_staff=True)
self.client.login(username=user.username, password=self.password)
def assert_get_response_status(self, status_code):
""" Asserts the HTTP status of a GET responses matches the expected status. """
response = self.client.get(self.path)
self.assertEqual(response.status_code, status_code)
return response
def test_login_required(self):
""" Users are required to login before accessing the view. """
self.client.logout()
response = self.assert_get_response_status(302)
self.assertIn(settings.LOGIN_URL, response.url)
def test_staff_only(self):
""" The view should only be accessible to staff. """
self.client.logout()
user = self.create_user(is_staff=False)
self.client.login(username=user.username, password=self.password)
self.assert_get_response_status(404)
user.is_staff = True
user.save()
self.assert_get_response_status(200)
class TestCase(TestServerUrlMixin, UserMixin, SiteMixin, CacheMixin, DjangoTestCase):
"""
Base test case for ecommerce tests.
......
......@@ -62,6 +62,7 @@ urlpatterns = AUTH_URLS + WELL_KNOWN_URLS + [
url(r'^i18n/', include('django.conf.urls.i18n')),
url(r'^jsi18n/$', JavaScriptCatalog.as_view(packages=['courses']), name='javascript-catalog'),
url(r'^programs/', include('ecommerce.programs.urls', namespace='programs')),
url(r'^enterprise/', include('ecommerce.enterprise.urls', namespace='enterprise')),
]
# Install Oscar extension URLs
......
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