...
 
Commits (3)
......@@ -19,8 +19,8 @@ wheel:
stage: package
image: python:3.5
script:
- pip install wheel
- python setup.py bdist_wheel
- pip install poetry
- poetry build -f wheel
- mv dist/*.whl .
artifacts:
paths:
......
......@@ -151,6 +151,8 @@ class AddressOrPayment extends CartEntry {
* currently available to the user.
* @property {AddressOrPayment[]} payment_methods All payment methods currently
* available to the user.
* @property {CartEntry[]} adjustments All adjustments currently applied to
* the cart.
*/
/**
......@@ -173,6 +175,7 @@ class CartClient {
this.addItem = this.addItem.bind(this)
this.addAddress = this.addAddress.bind(this)
this.addPaymentMethod = this.addPaymentMethod.bind(this)
this.addAdjustment = this.addAdjustment.bind(this)
this.checkout = this.checkout.bind(this)
this.cartUrl = cartUrl
......@@ -230,6 +233,7 @@ class CartClient {
cart.payment_methods = cart.payment_methods.map(
x => new AddressOrPayment(this, x),
)
cart.adjustments = cart.adjustments.map(x => new CartEntry(this, x))
this.cart = cart
this.cartListeners.forEach(x => setImmediate(x.bind(null, this.cart)))
......@@ -286,7 +290,7 @@ class CartClient {
}
/**
* Add a delivery address to the shopping cart.
* Add a payment method to the shopping cart.
* @param {string} type - Type of PaymentMethod to create
* @param {object} data - Data that the corresponding PaymentMethod
* serializer is expecting.
......@@ -295,6 +299,16 @@ class CartClient {
return this.add(this.cart.new_payment_method_url, type, data)
}
/**
* Add an adjustment to the shopping cart.
* @param {string} type - Type of PaymentMethod to create
* @param {object} data - Data that the corresponding PaymentMethod
* serializer is expecting.
*/
addAdjustment(type, data) {
return this.add(this.cart.new_adjustment_url, type, data)
}
/**
* Set an email address for the shopping cart.
* @param {string|null} address Email address to set. Use null to clear the
......
......@@ -66,7 +66,7 @@ class WritableSerializerMethodField(fields.SerializerMethodField):
self.method_name = method_name
self.write_serializer = write_serializer
kwargs['source'] = '*'
super(fields.SerializerMethodField, self).__init__(**kwargs)
super(fields.SerializerMethodField, self).__init__(**kwargs) # noqa
def to_internal_value(self, representation):
return {self.field_name: self.write_serializer.to_representation(representation)}
......@@ -92,8 +92,8 @@ class PrimaryKeyModelSerializer(serializers.ModelSerializer):
"""
return self.Meta.model.objects.all()
def to_internal_value(self, repr):
return self.get_queryset().get(pk=repr)
def to_internal_value(self, representation):
return self.get_queryset().get(pk=representation)
class RegistryRelatedField(fields.Field):
......@@ -167,11 +167,15 @@ class PaymentMethodSerializer(RegistryRelatedWithMetadataSerializer):
class AdjustmentSerializer(RegistryRelatedWithMetadataSerializer):
total = fields.SerializerMethodField()
url = fields.SerializerMethodField()
def get_total(self, instance):
# TODO: Store subtotal so that it's only calculated once
return str(instance.get_total(instance.cart.get_subtotal()))
def get_url(self, instance):
return reverse('lorikeet:adjustment', kwargs={'id': instance.id})
class SubclassListSerializer(serializers.ListSerializer):
......@@ -183,11 +187,14 @@ class SubclassListSerializer(serializers.ListSerializer):
class CartSerializer(serializers.ModelSerializer):
items = SubclassListSerializer(child=LineItemMetadataSerializer())
new_item_url = fields.SerializerMethodField()
subtotal = fields.DecimalField(
max_digits=7, decimal_places=2, source='get_subtotal')
delivery_addresses = fields.SerializerMethodField()
new_address_url = fields.SerializerMethodField()
payment_methods = fields.SerializerMethodField()
new_payment_method_url = fields.SerializerMethodField()
adjustments = SubclassListSerializer(child=AdjustmentSerializer())
new_adjustment_url = fields.SerializerMethodField()
grand_total = fields.DecimalField(
max_digits=7, decimal_places=2, source='get_grand_total')
is_complete = fields.SerializerMethodField()
......@@ -232,6 +239,9 @@ class CartSerializer(serializers.ModelSerializer):
return PaymentMethodSerializer(instance=the_set, many=True, context={'cart': cart}).data
def get_new_adjustment_url(self, _):
return reverse('lorikeet:new-adjustment')
def get_generated_at(self, cart):
return time()
......@@ -253,7 +263,8 @@ class CartSerializer(serializers.ModelSerializer):
'new_address_url', 'payment_methods',
'new_payment_method_url', 'grand_total', 'generated_at',
'is_complete', 'incomplete_reasons', 'checkout_url',
'is_authenticated', 'email', 'adjustments')
'is_authenticated', 'email', 'adjustments',
'new_adjustment_url', 'subtotal')
class CartUpdateSerializer(serializers.ModelSerializer):
......@@ -267,7 +278,7 @@ class CartUpdateSerializer(serializers.ModelSerializer):
class LineItemSerializer(serializers.ModelSerializer):
"""Base serializer for LineItem subclasses."""
def __init__(self, instance=None, *args, **kwargs):
def __init__(self, instance=None, *args, **kwargs): # noqa
if 'cart' in kwargs:
self.cart = kwargs.pop('cart')
elif instance is not None:
......@@ -275,7 +286,7 @@ class LineItemSerializer(serializers.ModelSerializer):
else:
raise TypeError("Either instance or cart arguments must be "
"provided to {}".format(self.__class__.__name__))
return super().__init__(instance, *args, **kwargs)
super().__init__(instance, *args, **kwargs)
def create(self, validated_data):
validated_data['cart'] = self.cart
......
......@@ -11,15 +11,17 @@ def test_empty_cart(client):
resp = client.get('/_cart/')
data = loads(resp.content.decode('utf-8'))
generated_at = data.pop('generated_at')
assert type(generated_at) == float
assert isinstance(generated_at, float)
assert data == {
'items': [],
'new_item_url': '/_cart/new/',
'subtotal': '0.00',
'delivery_addresses': [],
'new_address_url': '/_cart/new-address/',
'payment_methods': [],
'new_payment_method_url': '/_cart/new-payment-method/',
'adjustments': [],
'new_adjustment_url': '/_cart/new-adjustment/',
'grand_total': '0.00',
'incomplete_reasons': [
{
......@@ -55,15 +57,17 @@ def test_empty_cart_logged_in(admin_client):
resp = admin_client.get('/_cart/')
data = loads(resp.content.decode('utf-8'))
generated_at = data.pop('generated_at')
assert type(generated_at) == float
assert isinstance(generated_at, float)
assert data == {
'items': [],
'new_item_url': '/_cart/new/',
'subtotal': '0.00',
'delivery_addresses': [],
'new_address_url': '/_cart/new-address/',
'payment_methods': [],
'new_payment_method_url': '/_cart/new-payment-method/',
'adjustments': [],
'new_adjustment_url': '/_cart/new-adjustment/',
'grand_total': '0.00',
'incomplete_reasons': [
{
......@@ -138,11 +142,11 @@ def test_cart_contents_with_adjustment(client, cart):
).quantize(Decimal('.01'), rounding=ROUND_DOWN)
assert data['adjustments'] == [{
'type': 'CartDiscount',
# 'url': '/_cart/adjustment/{}/'.format(i.id),
'url': '/_cart/adjustment/{}/'.format(i.id),
'data': {
'percentage': a.percentage,
},
'total': str(discount)
'total': str(discount),
}]
assert data['grand_total'] == str(
(i.product.unit_price * i.quantity) + discount)
......
......@@ -31,6 +31,7 @@ def test_view_adjustment(client, cart):
'percentage': a.percentage,
},
'total': '0.00',
'url': '/_cart/adjustment/{}/'.format(a.id),
}
......@@ -52,3 +53,13 @@ def test_delete_adjustment(client, cart):
assert resp.status_code == 204
with pytest.raises(smodels.CartDiscount.DoesNotExist):
a.refresh_from_db()
@pytest.mark.django_db
def test_delete_unowned_adjustment(client, other_cart):
a = sfactories.CartDiscountFactory(cart=other_cart)
url = '/_cart/adjustment/{}/'.format(a.id)
resp = client.delete(url)
assert resp.status_code == 404
a.refresh_from_db()
This diff is collapsed.
......@@ -10,6 +10,6 @@
"babel-preset-react": "^6.16.0"
},
"scripts": {
"prepublish": "babel -d . js"
"prepare": "babel -d . js"
}
}
......@@ -73,11 +73,8 @@ description = "A high-level Python Web framework that encourages rapid developme
name = "django"
optional = false
platform = "*"
python-versions = ">=3.4"
version = "2.0.7"
[package.dependencies]
pytz = "*"
python-versions = "*"
version = "1.10.8"
[[package]]
category = "main"
......@@ -260,7 +257,7 @@ version = "2.7.3"
six = ">=1.5"
[[package]]
category = "main"
category = "dev"
description = "World timezone definitions, modern and historical"
name = "pytz"
optional = false
......@@ -426,7 +423,7 @@ starshipit = ["requests"]
stripe = ["stripe"]
[metadata]
content-hash = "6a8b2513f6fa9a73de72c18d18b7bc2f2e39244a34313bfd236ce5d71bd2f55e"
content-hash = "7331857351a183f614e12cf937c3c18e8d0d41f38556644819d9864991e1f5c6"
platform = "*"
python-versions = ">=3.4"
......@@ -438,7 +435,7 @@ chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "
colorama = ["463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", "48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"]
cssselect = ["066d8bc5229af09617e24b3ca4d52f1f9092d9e061931f4184cd572885c23204", "3b5103e8789da9e936a68d993b70df732d06b8bb9a337a05ed4eb52c17ef7206"]
cssutils = ["a2fcf06467553038e98fea9cfe36af2bf14063eb147a70958cfcaa8f5786acaf", "c74dbe19c92f5052774eadb15136263548dd013250f1ed1027988e7fef125c8d"]
django = ["97886b8a13bbc33bfeba2ff133035d3eca014e2309dff2b6da0bdfc0b8656613", "e900b73beee8977c7b887d90c6c57d68af10066b9dac898e1eaf0f82313de334"]
django = ["d4ef83bd326573c00972cb9429beb396d210341a636e4b816fc9b3f505c498bb", "ffdc7e938391ae3c2ee8ff82e0b4444e4e6bb15c99d00770285233d42aaf33d6"]
django-model-utils = ["92e2ea87147447935800cbc21d79ccf678298166e345c794340d6b0e7a3307e7"]
djangorestframework = ["110afa12784ceadfb50808882689302d266785b51e3d13286744333ff6d78e60", "f995a35ae22f354d2a9a42ee6d2c059c101f826b1485ed46781677895ad25ee5"]
docutils = ["02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", "51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", "7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"]
......
......@@ -13,6 +13,7 @@ repository = "https://gitlab.com/abre/lorikeet"
[tool.poetry.dependencies]
python = ">=3.4"
django = ">=1.8,<1.11"
django-model-utils = "^2.6"
djangorestframework = "~3.5.3"
stripe = {version = "^1.44.0", optional = true}
......
from setuptools import find_packages, setup
setup(
name='django-lorikeet',
description='A simple, generic, API-only shopping cart framework for Django',
author='Adam Brenecki',
author_email='adam@brenecki.id.au',
license='MIT',
setup_requires=["setuptools_scm>=1.11.1"],
use_scm_version=True,
packages=find_packages(),
include_package_data=True,
install_requires=[
'djangorestframework>=3.5.3,<3.6',
'django-model-utils>=2.6,<3',
],
extras_require={
'stripe': [
'stripe>=1.44.0,<2',
],
'email_invoice': [
'premailer>=3.0.1,<4',
],
'starshipit': [
'requests>=2.13.0,<3',
],
'docs': [
'sphinx>=1.5.1,<1.6',
'sphinx-rtd-theme>=0.2.4,<0.3',
'sphinx-js>=1.3,<1.4',
'sphinxcontrib_httpdomain>=1.5.0,<2',
],
'dev': [
'django<=1.10.999',
'factory-boy',
'tox',
]
},
)
[tox]
skipsdist=True
envlist=py{34,35,36}-dj{18,19,110}
[testenv]
extras=
......@@ -13,7 +14,11 @@ deps=
factory-boy>=2.8.1,<2.9
stripe
premailer
commands=pytest {posargs}
whitelist_externals=sh
skip_install=true
commands=
sh -c "rm dist/*.whl && poetry build -f wheel && pip install dist/*.whl"
pytest {posargs}
passenv=STRIPE_API_KEY
[pytest]
DJANGO_SETTINGS_MODULE=testproject.settings
......