Commit 453092d1 by Clinton Blackburn

Updated to Django Oscar 1.2.2

ECOM-7095
parent e208386e
...@@ -180,7 +180,7 @@ class CouponMixin(object): ...@@ -180,7 +180,7 @@ class CouponMixin(object):
def setUp(self): def setUp(self):
super(CouponMixin, self).setUp() super(CouponMixin, self).setUp()
self.category = factories.CategoryFactory() self.category = factories.CategoryFactory(path='1000')
# Force the creation of a coupon ProductClass # Force the creation of a coupon ProductClass
self.coupon_product_class # pylint: disable=pointless-statement self.coupon_product_class # pylint: disable=pointless-statement
......
...@@ -5,8 +5,6 @@ from oscar.core.loading import get_class ...@@ -5,8 +5,6 @@ from oscar.core.loading import get_class
class BasketApplication(app.BasketApplication): class BasketApplication(app.BasketApplication):
add_voucher_view = get_class('basket.views', 'VoucherAddMessagesView')
remove_voucher_view = get_class('basket.views', 'VoucherRemoveMessagesView')
single_item_view = get_class('basket.views', 'BasketSingleItemView') single_item_view = get_class('basket.views', 'BasketSingleItemView')
summary_view = get_class('basket.views', 'BasketSummaryView') summary_view = get_class('basket.views', 'BasketSummaryView')
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('basket', '0007_auto_20160907_2040'),
]
operations = [
migrations.AlterModelOptions(
name='line',
options={'ordering': ['date_created', 'pk'], 'verbose_name': 'Basket line', 'verbose_name_plural': 'Basket lines'},
),
]
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import datetime
import logging import logging
from datetime import datetime
import dateutil.parser import dateutil.parser
import waffle import waffle
from django.http import HttpResponseBadRequest, HttpResponseRedirect from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext as _
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from oscar.apps.basket.views import * # pylint: disable=wildcard-import, unused-wildcard-import from oscar.apps.basket.views import * # pylint: disable=wildcard-import, unused-wildcard-import
from oscar.core.utils import redirect_to_referrer from oscar.apps.basket.views import VoucherAddView as BaseVoucherAddView, VoucherRemoveView as BaseVoucherRemoveView
from requests.exceptions import ConnectionError, Timeout from requests.exceptions import ConnectionError, Timeout
from slumber.exceptions import SlumberBaseException from slumber.exceptions import SlumberBaseException
...@@ -25,7 +25,6 @@ from ecommerce.extensions.partner.shortcuts import get_partner_for_site ...@@ -25,7 +25,6 @@ from ecommerce.extensions.partner.shortcuts import get_partner_for_site
from ecommerce.extensions.payment.constants import CLIENT_SIDE_CHECKOUT_FLAG_NAME from ecommerce.extensions.payment.constants import CLIENT_SIDE_CHECKOUT_FLAG_NAME
from ecommerce.extensions.payment.forms import PaymentForm from ecommerce.extensions.payment.forms import PaymentForm
Benefit = get_model('offer', 'Benefit') Benefit = get_model('offer', 'Benefit')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
StockRecord = get_model('partner', 'StockRecord') StockRecord = get_model('partner', 'StockRecord')
...@@ -319,60 +318,85 @@ class BasketSummaryView(BasketView): ...@@ -319,60 +318,85 @@ class BasketSummaryView(BasketView):
return context return context
class VoucherAddMessagesView(VoucherAddView): class VoucherAddView(BaseVoucherAddView): # pylint: disable=function-redefined
""" def apply_voucher_to_basket(self, voucher):
View that applies a voucher to basket. code = voucher.code
We change default messages oscar returns. if voucher.is_expired():
""" messages.error(
self.request,
_("Coupon code '{code}' has expired.").format(code=code)
)
return
if not voucher.is_active():
messages.error(
self.request,
_("Coupon code '{code}' is not active.").format(code=code))
return
is_available, message = voucher.is_available_to_user(self.request.user)
if not is_available:
if voucher.usage == Voucher.SINGLE_USE:
message = _("Coupon code '{code}' has already been redeemed.").format(code=code)
messages.error(self.request, message)
return
self.request.basket.vouchers.add(voucher)
# Raise signal
self.add_signal.send(sender=self, basket=self.request.basket, voucher=voucher)
# Recalculate discounts to see if the voucher gives any
Applicator().apply(self.request.basket, self.request.user,
self.request)
discounts_after = self.request.basket.offer_applications
# Look for discounts from this new voucher
found_discount = False
for discount in discounts_after:
if discount['voucher'] and discount['voucher'] == voucher:
found_discount = True
break
if not found_discount:
messages.warning(
self.request,
_('Your basket does not qualify for a coupon code discount.'))
self.request.basket.vouchers.remove(voucher)
else:
messages.info(
self.request,
_("Coupon code '{code}' added to basket.").format(code=code)
)
def form_valid(self, form): def form_valid(self, form):
super(VoucherAddMessagesView, self).form_valid(form)
code = form.cleaned_data['code'] code = form.cleaned_data['code']
if not self.request.basket.id:
for msg in list(messages.get_messages(self.request)): return redirect_to_referrer(self.request, 'basket:summary')
if msg.message == _("No voucher found with code '{code}'").format(code=code): if self.request.basket.contains_voucher(code):
messages.error(
self.request,
_("You have already added coupon code '{code}' to your basket.").format(code=code)
)
else:
try:
voucher = self.voucher_model._default_manager.get(code=code) # pylint: disable=protected-access
except self.voucher_model.DoesNotExist:
messages.error( messages.error(
self.request, self.request,
_("Coupon code '{code}' does not exist.").format(code=code) _("Coupon code '{code}' does not exist.").format(code=code)
) )
elif msg.message == _("You have already added the '{code}' voucher to your basket").format(code=code):
messages.error(
self.request,
_("You have already added coupon code '{code}' to your basket.").format(code=code)
)
elif msg.message == _("The '{code}' voucher has expired").format(code=code):
messages.error(
self.request,
_("Coupon code '{code}' has expired.").format(code=code)
)
elif msg.message == _("Voucher '{code}' added to basket").format(code=code):
messages.info(
self.request,
_("Coupon code '{code}' added to basket.").format(code=code)
)
elif msg.message == _("Your basket does not qualify for a voucher discount"):
messages.warning(
self.request,
_("Your basket does not qualify for a coupon code discount.")
)
elif msg.message == _("This voucher has already been used"):
messages.error(
self.request,
_("Coupon code '{code}' has already been redeemed.").format(code=code)
)
else: else:
messages.error( self.apply_voucher_to_basket(voucher)
self.request,
_("Coupon code '{code}' is invalid.").format(code=code)
)
return redirect_to_referrer(self.request, 'basket:summary') return redirect_to_referrer(self.request, 'basket:summary')
class VoucherRemoveMessagesView(VoucherRemoveView): class VoucherRemoveView(BaseVoucherRemoveView): # pylint: disable=function-redefined
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
# This will fix the bug in Django Oscar # TODO Remove this once https://github.com/django-oscar/django-oscar/pull/2241 is merged.
# Expected Primary Key to be integer, but it's Unicode instead # This prevents an issue that arises when the user applies a voucher, opens the basket page in
# another window/tab, and attempts to remove the voucher on both screens. Under this scenario the
# second attempt to remove the voucher will raise an error.
kwargs['pk'] = int(kwargs['pk']) kwargs['pk'] = int(kwargs['pk'])
return super(VoucherRemoveMessagesView, self).post(request, *args, **kwargs) return super(VoucherRemoveView, self).post(request, *args, **kwargs)
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.core.validators
import oscar.core.validators
class Migration(migrations.Migration):
dependencies = [
('catalogue', '0020_auto_20161025_1446'),
]
operations = [
migrations.AlterField(
model_name='productattribute',
name='code',
field=models.SlugField(max_length=128, verbose_name='Code', validators=[django.core.validators.RegexValidator(regex=b'^[a-zA-Z_][0-9a-zA-Z_]*$', message="Code can only contain the letters a-z, A-Z, digits, and underscores, and can't start with a digit."), oscar.core.validators.non_python_keyword]),
),
migrations.AlterUniqueTogether(
name='attributeoption',
unique_together=set([('group', 'option')]),
),
]
from __future__ import unicode_literals from __future__ import unicode_literals
import logging import logging
from oscar.core.loading import get_model from oscar.core.loading import get_model
from oscar.test import factories from oscar.test import factories
from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.core.constants import ENROLLMENT_CODE_PRODUCT_CLASS_NAME from ecommerce.core.constants import ENROLLMENT_CODE_PRODUCT_CLASS_NAME
from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.tests.factories import PartnerFactory from ecommerce.tests.factories import PartnerFactory
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -22,13 +23,19 @@ class CourseCatalogTestMixin(object): ...@@ -22,13 +23,19 @@ class CourseCatalogTestMixin(object):
The setup method guarantees the requisite product class, partner, and category will be in place. This is especially The setup method guarantees the requisite product class, partner, and category will be in place. This is especially
useful when running tests without database migrations (which normally create these objects). useful when running tests without database migrations (which normally create these objects).
""" """
def setUp(self): def setUp(self):
super(CourseCatalogTestMixin, self).setUp() super(CourseCatalogTestMixin, self).setUp()
# Force the creation of a seat ProductClass # Force the creation of a seat ProductClass
self.seat_product_class # pylint: disable=pointless-statement self.seat_product_class # pylint: disable=pointless-statement
self.enrollment_code_product_class # pylint: disable=pointless-statement self.enrollment_code_product_class # pylint: disable=pointless-statement
self.category, _created = Category.objects.get_or_create(name='Seats', defaults={'depth': 1})
category_name = 'Seats'
try:
self.category = Category.objects.get(name=category_name)
except Category.DoesNotExist:
self.category = factories.CategoryFactory(name=category_name)
def create_course_and_seat( def create_course_and_seat(
self, course_id=None, seat_type='verified', id_verification=False, price=10, partner=None self, course_id=None, seat_type='verified', id_verification=False, price=10, partner=None
......
from auth_backends.urls import auth_urlpatterns
from django.conf.urls import url, include from django.conf.urls import url, include
from oscar.apps.dashboard import app from oscar.apps.dashboard import app
from oscar.core.loading import get_class from oscar.core.loading import get_class
from ecommerce.core.views import LogoutView
# NOTE: This should match AUTH_URLS in ecommerce/urls.py. These are duplicated here because Oscar's
# dashboard templates, strangely, reference dashboard:login and dashboard:logout instead of login and logout.
AUTH_URLS = [url(r'^logout/$', LogoutView.as_view(), name='logout'), ] + auth_urlpatterns
class DashboardApplication(app.DashboardApplication): class DashboardApplication(app.DashboardApplication):
index_view = get_class('dashboard.views', 'ExtendedIndexView') index_view = get_class('dashboard.views', 'ExtendedIndexView')
...@@ -25,6 +32,7 @@ class DashboardApplication(app.DashboardApplication): ...@@ -25,6 +32,7 @@ class DashboardApplication(app.DashboardApplication):
url(r'^shipping/', include(self.shipping_app.urls)), url(r'^shipping/', include(self.shipping_app.urls)),
url(r'^refunds/', include(self.refunds_app.urls)), url(r'^refunds/', include(self.refunds_app.urls)),
] ]
urls += AUTH_URLS
return self.post_process_urls(urls) return self.post_process_urls(urls)
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import oscar.models.fields
class Migration(migrations.Migration):
dependencies = [
('offer', '0009_range_enterprise_customer'),
]
operations = [
migrations.AlterField(
model_name='benefit',
name='proxy_class',
field=oscar.models.fields.NullCharField(default=None, max_length=255, verbose_name='Custom class'),
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0011_auto_20161025_1446'),
]
operations = [
migrations.AlterModelOptions(
name='line',
options={'ordering': ['pk'], 'verbose_name': 'Order Line', 'verbose_name_plural': 'Order Lines'},
),
]
...@@ -5,6 +5,7 @@ from oscar.test.factories import * # pylint:disable=wildcard-import,unused-wild ...@@ -5,6 +5,7 @@ from oscar.test.factories import * # pylint:disable=wildcard-import,unused-wild
Benefit = get_model('offer', 'Benefit') Benefit = get_model('offer', 'Benefit')
Catalog = get_model('catalogue', 'Catalog') Catalog = get_model('catalogue', 'Catalog')
Default = get_class('partner.strategy', 'Default')
Voucher = get_model('voucher', 'Voucher') Voucher = get_model('voucher', 'Voucher')
OrderNumberGenerator = get_class('order.utils', 'OrderNumberGenerator') OrderNumberGenerator = get_class('order.utils', 'OrderNumberGenerator')
...@@ -17,7 +18,7 @@ def create_order(number=None, basket=None, user=None, shipping_address=None, # ...@@ -17,7 +18,7 @@ def create_order(number=None, basket=None, user=None, shipping_address=None, #
""" """
if not basket: if not basket:
basket = Basket.objects.create() basket = Basket.objects.create()
basket.strategy = strategy.Default() basket.strategy = Default()
product = create_product() product = create_product()
create_stockrecord( create_stockrecord(
product, num_in_stock=10, price_excl_tax=D('10.00')) product, num_in_stock=10, price_excl_tax=D('10.00'))
...@@ -50,13 +51,16 @@ def create_order(number=None, basket=None, user=None, shipping_address=None, # ...@@ -50,13 +51,16 @@ def create_order(number=None, basket=None, user=None, shipping_address=None, #
def prepare_voucher(code='COUPONTEST', _range=None, start_datetime=None, end_datetime=None, benefit_value=100, def prepare_voucher(code='COUPONTEST', _range=None, start_datetime=None, end_datetime=None, benefit_value=100,
benefit_type=Benefit.PERCENTAGE, usage=Voucher.SINGLE_USE, max_usage=None): benefit_type=Benefit.PERCENTAGE, usage=Voucher.SINGLE_USE, max_usage=None):
""" Helper function to create a voucher and add an offer to it that contains a product. """ """ Helper function to create a voucher and add an offer to it that contains a product. """
# NOTE (CCB): We use empty categories here to avoid unique-constraint issues that occur when we use
# ProductCategoryFactory in conjunction with pre-created Category objects.
if _range is None: if _range is None:
product = ProductFactory(title='Test product') product = ProductFactory(categories=[])
_range = RangeFactory(products=[product, ]) _range = RangeFactory(products=[product, ])
elif _range.num_products() > 0: elif _range.num_products() > 0:
product = _range.all_products()[0] product = _range.all_products()[0]
else: else:
product = ProductFactory(title='Test product') product = ProductFactory(categories=[])
if start_datetime is None: if start_datetime is None:
start_datetime = now() - datetime.timedelta(days=1) start_datetime = now() - datetime.timedelta(days=1)
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
{# Translation support for JavaScript strings. #} {# Translation support for JavaScript strings. #}
<script type="text/javascript" src="{% url 'django.views.i18n.javascript_catalog' %}"></script> <script type="text/javascript" src="{% url 'django.views.i18n.javascript_catalog' %}"></script>
{% compress js %} {% compress js %}
<script src="{% static 'bower_components/js-cookie/src/js.cookie.js' %}" type="text/javascript"></script> <script src="{% static 'bower_components/js-cookie/src/js.cookie.js' %}" type="text/javascript"></script>
<script src="{% static 'vendor-extensions/oscar/js/add_message.js' %}" type="text/javascript"></script> <script src="{% static 'vendor-extensions/oscar/js/add_message.js' %}" type="text/javascript"></script>
...@@ -27,11 +27,9 @@ ...@@ -27,11 +27,9 @@
<ul class="breadcrumb"> <ul class="breadcrumb">
<li> <li>
<a href="{% url 'dashboard:index' %}">{% trans "Dashboard" %}</a> <a href="{% url 'dashboard:index' %}">{% trans "Dashboard" %}</a>
<span class="divider">/</span>
</li> </li>
<li> <li>
<a href="{% url 'dashboard:order-list' %}">{% trans "Orders" %}</a> <a href="{% url 'dashboard:order-list' %}">{% trans "Orders" %}</a>
<span class="divider">/</span>
</li> </li>
<li class="active">#{{ order.number }}</li> <li class="active">#{{ order.number }}</li>
</ul> </ul>
......
...@@ -28,7 +28,6 @@ ...@@ -28,7 +28,6 @@
<ul class="breadcrumb"> <ul class="breadcrumb">
<li> <li>
<a href="{% url 'dashboard:index' %}">{% trans "Dashboard" %}</a> <a href="{% url 'dashboard:index' %}">{% trans "Dashboard" %}</a>
<span class="divider">/</span>
</li> </li>
<li class="active">{% trans "Orders" %}</li> <li class="active">{% trans "Orders" %}</li>
</ul> </ul>
......
...@@ -27,11 +27,9 @@ ...@@ -27,11 +27,9 @@
<ul class="breadcrumb"> <ul class="breadcrumb">
<li> <li>
<a href="{% url 'dashboard:index' %}">{% trans "Dashboard" %}</a> <a href="{% url 'dashboard:index' %}">{% trans "Dashboard" %}</a>
<span class="divider">/</span>
</li> </li>
<li> <li>
<a href="{% url 'dashboard:refunds:list' %}">{% trans "Refunds" %}</a> <a href="{% url 'dashboard:refunds:list' %}">{% trans "Refunds" %}</a>
<span class="divider">/</span>
</li> </li>
<li class="active">#{{ refund.id }}</li> <li class="active">#{{ refund.id }}</li>
</ul> </ul>
......
...@@ -28,7 +28,6 @@ ...@@ -28,7 +28,6 @@
<ul class="breadcrumb"> <ul class="breadcrumb">
<li> <li>
<a href="{% url 'dashboard:index' %}">{% trans "Dashboard" %}</a> <a href="{% url 'dashboard:index' %}">{% trans "Dashboard" %}</a>
<span class="divider">/</span>
</li> </li>
<li class="active">{% trans "Refunds" %}</li> <li class="active">{% trans "Refunds" %}</li>
</ul> </ul>
......
...@@ -18,11 +18,9 @@ ...@@ -18,11 +18,9 @@
<ul class="breadcrumb"> <ul class="breadcrumb">
<li> <li>
<a href="{% url 'dashboard:index' %}">{% trans "Dashboard" %}</a> <a href="{% url 'dashboard:index' %}">{% trans "Dashboard" %}</a>
<span class="divider">/</span>
</li> </li>
<li> <li>
<a href="{% url 'dashboard:users-index' %}">{% trans "Customers" %}</a> <a href="{% url 'dashboard:users-index' %}">{% trans "Customers" %}</a>
<span class="divider">/</span>
</li> </li>
<li class="active">{{ customer.username }}</li> <li class="active">{{ customer.username }}</li>
</ul> </ul>
......
...@@ -11,11 +11,11 @@ from django.contrib.auth import get_user_model ...@@ -11,11 +11,11 @@ from django.contrib.auth import get_user_model
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core.cache import cache from django.core.cache import cache
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test.client import RequestFactory
from django.utils.timezone import now from django.utils.timezone import now
from mock import patch from mock import patch
from oscar.core.loading import get_class, get_model from oscar.core.loading import get_class, get_model
from oscar.test import factories from oscar.test import factories
from oscar.test.utils import RequestFactory
from social.apps.django_app.default.models import UserSocialAuth from social.apps.django_app.default.models import UserSocialAuth
from threadlocals.threadlocals import set_thread_variable from threadlocals.threadlocals import set_thread_variable
......
...@@ -6,7 +6,7 @@ django-crispy-forms==1.6.1 ...@@ -6,7 +6,7 @@ django-crispy-forms==1.6.1
django_extensions==1.5.5 django_extensions==1.5.5
django-filter==0.11.0 django-filter==0.11.0
django-libsass==0.5 django-libsass==0.5
django-oscar==1.1.1 django-oscar==1.2.2
django-rest-swagger[reST]==0.3.10 django-rest-swagger[reST]==0.3.10
django-simple-history==1.8.2 django-simple-history==1.8.2
django-solo==1.1.2 django-solo==1.1.2
......
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