Fixes #50, #59, #60, #65
pull/43/merge
James Ramm 2017-05-24 11:12:36 +01:00 zatwierdzone przez GitHub
rodzic c58c7f303e
commit 3de6f48124
42 zmienionych plików z 3124 dodań i 344 usunięć

Wyświetl plik

@ -29,4 +29,5 @@ install:
script: tox -e $TOX_ENV
after_success:
- coverage xml
- codecov -e TOX_ENV

Wyświetl plik

@ -5,6 +5,9 @@ Longclaw
.. image:: https://badge.fury.io/py/longclaw.svg
:target: https://badge.fury.io/py/longclaw
.. image:: https://codecov.io/gh/JamesRamm/longclaw/branch/master/graph/badge.svg
:target: https://codecov.io/gh/JamesRamm/longclaw
.. image:: https://travis-ci.org/JamesRamm/longclaw.svg?branch=master
:target: https://travis-ci.org/JamesRamm/longclaw

2
codecov.yml 100644
Wyświetl plik

@ -0,0 +1,2 @@
codecov:
token: c4e276fe-1d69-49e9-bbbe-fabaf4890222

Wyświetl plik

@ -2,179 +2,40 @@
Checkout
========
The Longclaw checkout process consists of three steps:
- Capturing the payment
- Collecting details about the customer - their email and shipping address.
- Creating a new ``Order`` to store this information.
Longclaw provides a simple, single checkout view.
The URL for the checkout is ``'checkout/'``.
After a successful checkout, it is redirected to ``checkout/success/``.
Longclaw currently provides a web API for the checkout process which captures this information in one step.
It is left to the user to design the front end template and javascript. In the front end, you should:
To implement the checkout, simply provide ``'longclawcheckout/checkout.html'`` and
``'longclawcheckout/success.html'`` templates. (Empty templates will have been created if
you ran the longclaw CLI to start your project)
- Collect the customer email
- Collect the shipping and billing address
- Payment capture by tokenizing the payment method (e.g. credit card) or payment itself
- (Optionally) calculate the shipping costs
- Submit all the information to the server in an AJAX POST request.
There are three forms provided in the checkout view:
The first two are relatively simple to achieve. Longclaw provides some utilities to help with the rest.
:checkout_form:
Captures the email address and optionally the shipping option for the checkout.
Also captures a boolean indicating whether a different billing address should be used
Payment Capture
===============
With Longclaw you can either tokenize the customers payment method (e.g. credit card) and
send this to the server for the payment to be captured, or you can use a service such as paypal
express checkout, which captures the payment directly and returns a token representing the transaction
id. You would then submit this token to your server.
:shipping_form:
Captures shipping information.
The second option is often easiest to integrate since the user is redirected to the 3rd party site for payment.
(This is increasingly done via a modal popup rather than a redirect, which makes the user experience smoother).
The first option offers tightest integration with the look and feel of your site, but invariable involves more
front end work and validation.
Tokenizing the Payment
+++++++++++++++++++++++
To capture the payment with a 3rd party service, you will include some external javascript on your page
and often designate a button or ``div`` to initialise the popup/redirect. You will also specify a submit
handler to receive the token representing the transaction.
For example, the braintree javascript client allows express checkout using Paypal. Full details of how
to setup are `here <https://developers.braintreepayments.com/guides/paypal/checkout-with-paypal/javascript/v3>`_.
Other providers such as Stripe offer similar services.
Once you have received this token, you should submit it, along with the shipping address, billing address,
email and shipping rate to the ``api/checkout/prepaid/`` endpoint.
.. note:: The ``api/`` prefix can be configured in your django settings under ``API_URL_PREFIX``.
For example, if you want to distinguish the longclaw API from your own, you could set ``API_URL_PREFIX="api/longclaw/"``
The checkout url would then be ``api/longclaw/checkout/prepaid/``
The JSON request data would look like:
.. code-block:: json
{
transaction_id: "...",
shipping_rate: 0.0,
email: "john@smith.com",
address: {
shipping_name: "john smith",
shipping_address_line_1": "...",
shipping_address_city: "",
shipping_address_zip: "",
shipping_address_country: "",
billing_name: "john smith",
billing_address_line_1: "...",
billing_address_city: "",
billing_address_zip: "",
billing_address_country: "",
}
}
transaction_id
The token returned from e.g. paypal
When using this method, you do not need to define the ``PAYMENT_GATEWAY`` setting.
Tokenizing the Payment method
+++++++++++++++++++++++++++++
Alternatively, you can pass the payment method for Longclaw to manually capture the payment.
Longclaw expects the payment details (i.e. credit card) to be passed as some kind of token in
a POST request to ``api/checkout/``.
Longclaw will then use the payment gateway defined by the ``PAYMENT_GATEWAY`` setting to capture
the payment.
To create the initial token representing the customers payment information, you may be able to use
the ``api/checkout/token/`` endpoint, passing the card information in the request data. This is dependent
upon the backend and it may be preferable to use client javascript libraries provided by your payment
gateway (e.g. ``stripe.js`` or ``braintree-web`` ) to generate a token.
Once the token is generated, the request data to send to ``api/checkout/`` is very similar to that for
``api/checkout/prepaid/``:
.. code-block:: json
{
token: "...",
shipping_rate: 0.0,
email: "john@smith.com",
address: {
shipping_name: "john smith",
shipping_address_line_1: "...",
shipping_address_city: "",
shipping_address_zip: "",
shipping_address_country: "",
billing_name: "john smith",
billing_address_line_1: "...",
billing_address_city: "",
billing_address_zip: "",
billing_address_country: "",
}
}
token
The token for customer details. The key name is dependent on the backend ("token" for stripe, "payment_method_nonce" for braintree)
shipping_rate
Number or string representation of a number (will be cast to float). The shipping costs
email
The customers' email
.. note:: The ``"token"`` key is dependent upon the payment backend and may be named differently.
Both ``api/checkout/`` and ``api/checkout/prepaid/`` return a 201 response with ``order_id`` in the JSON data.
If the payment fails, ``api/checkout/`` will return a 400 response with ``order_id`` and ``message`` in the JSON data.
Calculating Shipping Costs
==========================
You will have noticed the need to send ``shipping_rate`` with the checkout. If you are using Longclaws' shipping
settings, you can easily calculate the shipping cost either in python or by using the ``api/shipping/cost/`` endpoint.
Python example:
.. code-block:: python
from longclaw.longclawshipping import utils
from longclaw.longclawsettings.models import LongclawSettings
country_code = "GB" # ISO 2-letter country code for a configured shipping rate
option = "standard" # Name of shipping rate configured through longclaw admin (only used if more than one shipping rate exists for the given country)
settings = LongclawSettings.for_site(request.site)
try:
data = utils.get_shipping_cost(country_code, option, settings)
except InvalidShippingRate:
# More than 1 shipping rate for the country exists,
# but the supplied option doesnt match any
pass
except InvalidShippingCountry:
# A shipping rate for this country does not exist and ``default_shipping_enabled``
# is set to ``False`` in the longclaw admin settings
Javascript example:
.. code-block:: javascript
fetch(
"api/shipping/cost/",
{
method: "POST",
headers: {
Accept: 'application/json, application/json, application/coreapi+json',
"Content-Type": 'application/json"
},
credentials: "include",
body: JSON.stringify({
country_code: "GB",
shipping_rate_name: "standard"
})
}
).then(response => {...})
:billing_form:
A second address form for capturing alternate billing information. If you do not submit this form
(e.g. by not rendering it on the template), the billing and shipping addresses are assumed to be the same.
Generally, you may need to use a little javascript to optionally render the form if the user selects
'different billing address'.
Payment forms
-------------
It is up to you to render a payment form and then pass the token in the POST data.
Normally, the payment gateway chosen will have a javascript integration to render a form for you
and tokenize the payment method (e.g. braintrees 'hosted fields')
Longclaws' payment gateways provide some helpful utilities to load client javascript and generate tokens.
Loading ``longclawcheckout_tags`` in your template will allow you to retrieve the gateways' javascript libraries
as script tags (``{% gateway_client_js %}`` and generate a client token (``{% gateway_token %}``).
A little javascript is then required to setup your form and ask the gateway to tokenize the payment method for you.
You should then add this token to the request POST data (e.g. with a hidden input field).

Wyświetl plik

@ -0,0 +1,175 @@
.. checkout:
The Checkout API
================
The checkout API allows you to create more complex javascript-based checkout flows by providing some
simple endpoints for capturing payments and orders.
In the front end, you should:
- Collect the customer email
- Collect the shipping and billing address
- Payment capture by tokenizing the payment method (e.g. credit card) or payment itself
- (Optionally) calculate the shipping costs
- Submit all the information to the server in an AJAX POST request.
The first two are relatively simple to achieve. Longclaw provides some utilities to help with the rest.
Payment Capture
===============
With Longclaw you can either tokenize the customers payment method (e.g. credit card) and
send this to the server for the payment to be captured, or you can use a service such as paypal
express checkout, which captures the payment directly and returns a token representing the transaction
id. You would then submit this token to your server.
The second option is often easiest to integrate since the user is redirected to the 3rd party site for payment.
(This is increasingly done via a modal popup rather than a redirect, which makes the user experience smoother).
The first option offers tightest integration with the look and feel of your site, but invariable involves more
front end work and validation.
Tokenizing the Payment
+++++++++++++++++++++++
To capture the payment with a 3rd party service, you will include some external javascript on your page
and often designate a button or ``div`` to initialise the popup/redirect. You will also specify a submit
handler to receive the token representing the transaction.
For example, the braintree javascript client allows express checkout using Paypal. Full details of how
to setup are `here <https://developers.braintreepayments.com/guides/paypal/checkout-with-paypal/javascript/v3>`_.
Other providers such as Stripe offer similar services.
Once you have received this token, you should submit it, along with the shipping address, billing address,
email and shipping rate to the ``api/checkout/prepaid/`` endpoint.
.. note:: The ``api/`` prefix can be configured in your django settings under ``API_URL_PREFIX``.
For example, if you want to distinguish the longclaw API from your own, you could set ``API_URL_PREFIX="api/longclaw/"``
The checkout url would then be ``api/longclaw/checkout/prepaid/``
The JSON request data would look like:
.. code-block:: json
{
transaction_id: "...",
shipping_rate: 0.0,
email: "john@smith.com",
address: {
shipping_name: "john smith",
shipping_address_line_1": "...",
shipping_address_city: "",
shipping_address_zip: "",
shipping_address_country: "",
billing_name: "john smith",
billing_address_line_1: "...",
billing_address_city: "",
billing_address_zip: "",
billing_address_country: "",
}
}
transaction_id
The token returned from e.g. paypal
When using this method, you do not need to define the ``PAYMENT_GATEWAY`` setting.
Tokenizing the Payment method
+++++++++++++++++++++++++++++
Alternatively, you can pass the payment method for Longclaw to manually capture the payment.
Longclaw expects the payment details (i.e. credit card) to be passed as some kind of token in
a POST request to ``api/checkout/``.
Longclaw will then use the payment gateway defined by the ``PAYMENT_GATEWAY`` setting to capture
the payment.
To create the initial token representing the customers payment information, you may be able to use
the ``api/checkout/token/`` endpoint, passing the card information in the request data. This is dependent
upon the backend and it may be preferable to use client javascript libraries provided by your payment
gateway (e.g. ``stripe.js`` or ``braintree-web`` ) to generate a token.
Once the token is generated, the request data to send to ``api/checkout/`` is very similar to that for
``api/checkout/prepaid/``:
.. code-block:: json
{
token: "...",
shipping_rate: 0.0,
email: "john@smith.com",
address: {
shipping_name: "john smith",
shipping_address_line_1: "...",
shipping_address_city: "",
shipping_address_zip: "",
shipping_address_country: "",
billing_name: "john smith",
billing_address_line_1: "...",
billing_address_city: "",
billing_address_zip: "",
billing_address_country: "",
}
}
token
The token for customer details. The key name is dependent on the backend ("token" for stripe, "payment_method_nonce" for braintree)
shipping_rate
Number or string representation of a number (will be cast to float). The shipping costs
email
The customers' email
.. note:: The ``"token"`` key is dependent upon the payment backend and may be named differently.
Both ``api/checkout/`` and ``api/checkout/prepaid/`` return a 201 response with ``order_id`` in the JSON data.
If the payment fails, ``api/checkout/`` will return a 400 response with ``order_id`` and ``message`` in the JSON data.
Calculating Shipping Costs
==========================
You will have noticed the need to send ``shipping_rate`` with the checkout. If you are using Longclaws' shipping
settings, you can easily calculate the shipping cost either in python or by using the ``api/shipping/cost/`` endpoint.
Python example:
.. code-block:: python
from longclaw.longclawshipping import utils
from longclaw.longclawsettings.models import LongclawSettings
country_code = "GB" # ISO 2-letter country code for a configured shipping rate
option = "standard" # Name of shipping rate configured through longclaw admin (only used if more than one shipping rate exists for the given country)
settings = LongclawSettings.for_site(request.site)
try:
data = utils.get_shipping_cost(country_code, option, settings)
except InvalidShippingRate:
# More than 1 shipping rate for the country exists,
# but the supplied option doesnt match any
pass
except InvalidShippingCountry:
# A shipping rate for this country does not exist and ``default_shipping_enabled``
# is set to ``False`` in the longclaw admin settings
Javascript example:
.. code-block:: javascript
fetch(
"api/shipping/cost/",
{
method: "POST",
headers: {
Accept: 'application/json, application/json, application/coreapi+json',
"Content-Type": 'application/json"
},
credentials: "include",
body: JSON.stringify({
country_code: "GB",
shipping_rate_name: "standard"
})
}
).then(response => {...})

Wyświetl plik

@ -10,6 +10,7 @@ Usage Guide
products
basket
checkout
checkout_api
shipping
orders
payments

Wyświetl plik

@ -0,0 +1,12 @@
from django import template
from longclaw.longclawbasket.utils import get_basket_items
register = template.Library()
@register.simple_tag(takes_context=True)
def basket(context):
'''
Return the BasketItems in the current basket
'''
items, _ = get_basket_items(context["request"])
return items

Wyświetl plik

@ -2,16 +2,13 @@
Shipping logic and payment capture API
'''
from django.utils import timezone
from django.utils.module_loading import import_string
from django.db import transaction
from rest_framework.decorators import api_view, permission_classes
from rest_framework import permissions, status
from rest_framework.response import Response
from longclaw.longclawbasket.utils import get_basket_items, destroy_basket
from longclaw.longclawcheckout.utils import PaymentError, create_order
from longclaw import settings
gateway = import_string(settings.PAYMENT_GATEWAY)()
from longclaw.longclawbasket.utils import destroy_basket
from longclaw.longclawcheckout.utils import create_order, GATEWAY
from longclaw.longclawcheckout.errors import PaymentError
@api_view(['GET'])
@permission_classes([permissions.AllowAny])
@ -20,7 +17,7 @@ def create_token(request):
payment backend. Some payment backends (e.g. braintree) support creating a payment
token, which should be imported from the backend as 'get_token'
'''
token = gateway.get_token(request)
token = GATEWAY.get_token(request)
return Response({'token': token}, status=status.HTTP_200_OK)
@transaction.atomic
@ -36,23 +33,19 @@ def create_order_with_token(request):
# Get the request data
try:
address = request.data['address']
postage = float(request.data['shipping_rate'])
shipping_option = request.data.get('shipping_option', None)
email = request.data['email']
transaction_id = request.data['transaction_id']
except KeyError:
return Response(data={"message": "Missing parameters from request data"},
status=status.HTTP_400_BAD_REQUEST)
# Get the contents of the basket
items, _ = get_basket_items(request)
# Create the order
ip_address = request.data.get('ip', '0.0.0.0')
order = create_order(
items,
address,
email,
postage,
ip_address
request,
addresses=address,
shipping_option=shipping_option
)
order.payment_date = timezone.now()
@ -85,41 +78,22 @@ def capture_payment(request):
billing_address_country
'email': Email address of the customer
'ip': IP address of the customer
'shipping': The shipping rate (in the sites' currency)
'''
# Get the contents of the basket
items, _ = get_basket_items(request)
# Compute basket total
total = 0
for item in items:
total += item.total()
# Create the order
# get request data
address = request.data['address']
postage = float(request.data['shipping_rate'])
email = request.data['email']
ip_address = request.data.get('ip', '0.0.0.0')
order = create_order(
items,
address,
email,
postage,
ip_address
)
email = request.data.get('email', None)
shipping_option = request.data.get('shipping_option', None)
# Capture the payment
try:
desc = 'Payment from {} for order id #{}'.format(request.data['email'], order.id)
transaction_id = gateway.create_payment(request,
float(total) + postage,
description=desc)
order.payment_date = timezone.now()
order.transaction_id = transaction_id
# Once the order has been successfully taken, we can empty the basket
destroy_basket(request)
order = create_order(
email,
request,
addresses=address,
shipping_option=shipping_option,
capture_payment=True
)
response = Response(data={"order_id": order.id},
status=status.HTTP_201_CREATED)
except PaymentError as err:

Wyświetl plik

@ -0,0 +1,4 @@
class PaymentError(Exception):
def __init__(self, message):
self.message = str(message)

Wyświetl plik

@ -0,0 +1,9 @@
from django import forms
class CheckoutForm(forms.Form):
'''
Captures extra info required for checkout
'''
email = forms.EmailField()
shipping_option = forms.ChoiceField(required=False)
different_billing_address = forms.BooleanField(required=False)

Wyświetl plik

@ -1,6 +1,6 @@
from longclaw.longclawcheckout.utils import PaymentError
from longclaw.longclawcheckout.errors import PaymentError
class BasePayment():
class BasePayment(object):
'''
Provides the interface for payment backends and
can function as a dummy backend for testing.
@ -13,20 +13,28 @@ class BasePayment():
Can be used for testing - to simulate a failed payment/error,
pass `error: true` in the request data.
'''
err = request.data.get("error", False)
err = request.POST.get("error", False)
if err:
raise PaymentError("Dummy error requested")
return 'fake_transaction_id'
def get_token(self, request):
def get_token(self, request=None):
'''
Dummy function for generating a client token through
a payment gateway. Most (all?) gateways have a flow which
involves requesting a token from the server (usually to
tokenize the payment method) and then passing that token
to another api endpoint to create the payment.
involves requesting a token from the server to initialise
a client.
This function should be overriden in child classes
'''
return 'dummy_token'
def client_js(self):
'''
Return any client javascript library paths required
by the payment integration.
Should return an iterable of JS paths which can
be used in <script> tags
'''
return ('http://dummy.js', 'dummy.js')

Wyświetl plik

@ -1,7 +1,7 @@
import braintree
from longclaw import settings
from longclaw.longclawsettings.models import LongclawSettings
from longclaw.longclawcheckout.utils import PaymentError
from longclaw.longclawcheckout.errors import PaymentError
from longclaw.longclawcheckout.gateways import BasePayment
class BraintreePayment(BasePayment):
@ -9,13 +9,17 @@ class BraintreePayment(BasePayment):
Create a payment using Braintree
'''
def __init__(self):
braintree.Configuration.configure(braintree.Environment.Sandbox,
if settings.BRAINTREE_SANDBOX:
env = braintree.Environment.Sandbox
else:
env = braintree.Environment.Production
braintree.Configuration.configure(env,
merchant_id=settings.BRAINTREE_MERCHANT_ID,
public_key=settings.BRAINTREE_PUBLIC_KEY,
private_key=settings.BRAINTREE_PRIVATE_KEY)
def create_payment(self, request, amount):
nonce = request.data['payment_method_nonce']
def create_payment(self, request, amount, description=''):
nonce = request.POST.get('payment_method_nonce')
result = braintree.Transaction.sale({
"amount": str(amount),
"payment_method_nonce": nonce,
@ -27,9 +31,15 @@ class BraintreePayment(BasePayment):
raise PaymentError(result)
return result.transaction.id
def get_token(self, request):
# Generate client token for the dropin ui
return braintree.ClientToken.generate({})
def get_token(self, request=None):
# Generate client token
return braintree.ClientToken.generate()
def client_js(self):
return (
"https://js.braintreegateway.com/web/3.15.0/js/client.min.js",
"https://js.braintreegateway.com/web/3.15.0/js/hosted-fields.min.js"
)
class PaypalVZeroPayment(BasePayment):
'''
@ -40,7 +50,7 @@ class PaypalVZeroPayment(BasePayment):
def create_payment(self, request, amount, description=''):
longclaw_settings = LongclawSettings.for_site(request.site)
nonce = request.data['payment_method_nonce']
nonce = request.POST.get('payment_method_nonce')
result = self.gateway.transaction.sale({
"amount": str(amount),
"payment_method_nonce": nonce,
@ -57,3 +67,10 @@ class PaypalVZeroPayment(BasePayment):
def get_token(self, request):
return self.gateway.client_token.generate()
def client_js(self):
return (
"https://www.paypalobjects.com/api/checkout.js",
"https://js.braintreegateway.com/web/3.15.0/js/client.min.js",
"https://js.braintreegateway.com/web/3.15.0/js/paypal-checkout.min.js"
)

Wyświetl plik

@ -2,7 +2,7 @@ import math
import stripe
from longclaw.settings import STRIPE_SECRET
from longclaw.longclawsettings.models import LongclawSettings
from longclaw.longclawcheckout.utils import PaymentError
from longclaw.longclawcheckout.errors import PaymentError
from longclaw.longclawcheckout.gateways import BasePayment
@ -13,14 +13,14 @@ class StripePayment(BasePayment):
def __init__(self):
stripe.api_key = STRIPE_SECRET
def create_payment(self, request, amount):
def create_payment(self, request, amount, description=''):
try:
currency = LongclawSettings.for_site(request.site).currency
charge = stripe.Charge.create(
amount=int(math.ceil(amount * 100)), # Amount in pence
currency=currency.lower(),
source=request.data['token'],
description="Payment from"
description=description
)
return charge.id
except stripe.error.CardError as error:

Wyświetl plik

@ -0,0 +1,28 @@
from django import template
from longclaw.longclawcheckout.utils import GATEWAY
register = template.Library()
@register.simple_tag
def gateway_client_js():
'''
Template tag which provides a `script` tag for each javascript item
required by the payment gateway
'''
javascripts = GATEWAY.client_js()
if isinstance(javascripts, (tuple, list)):
tags = []
for js in javascripts:
tags.append('<script type="text/javascript" src="{}"></script>'.format(js))
return tags
else:
raise TypeError(
'function client_js of {} must return a list or tuple'.format(GATEWAY.__name__))
@register.simple_tag
def gateway_token():
'''
Provide a client token from the chosen gateway
'''
return GATEWAY.get_token()

Wyświetl plik

@ -1,12 +1,22 @@
from django.test import TestCase
from django.test.client import RequestFactory
from longclaw.tests.utils import LongclawTestCase, BasketItemFactory
from django.contrib.sites.models import Site
try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
from longclaw.tests.utils import LongclawTestCase, AddressFactory, BasketItemFactory, CountryFactory, OrderFactory
from longclaw.longclawcheckout.utils import create_order
from longclaw.longclawcheckout.forms import CheckoutForm
from longclaw.longclawcheckout.views import CheckoutView
from longclaw.longclawbasket.utils import basket_id
class CheckoutTest(LongclawTestCase):
class CheckoutApiTest(LongclawTestCase):
def setUp(self):
self.addresses = {
'shipping_name': '',
'shipping_address_line1': '',
@ -20,16 +30,14 @@ class CheckoutTest(LongclawTestCase):
'billing_address_country': ''
}
self.email = "test@test.com"
request = RequestFactory().get('/')
request.session = {}
self.basket_id = basket_id(request)
self.request = RequestFactory().get('/')
self.request.session = {}
self.basket_id = basket_id(self.request)
def test_create_order(self):
shipping_rate = 0
basket_items = [BasketItemFactory(basket_id=self.basket_id),
BasketItemFactory(basket_id=self.basket_id)]
order = create_order(basket_items, self.addresses,
self.email, shipping_rate)
BasketItemFactory(basket_id=self.basket_id),
BasketItemFactory(basket_id=self.basket_id)
order = create_order(self.email, self.request, self.addresses)
self.assertIsNotNone(order)
self.assertEqual(self.email, order.email)
self.assertEqual(order.items.count(), 2)
@ -42,8 +50,7 @@ class CheckoutTest(LongclawTestCase):
BasketItemFactory(basket_id=self.basket_id)
data = {
'address': self.addresses,
'email': self.email,
'shipping_rate': 0
'email': self.email
}
self.post_test(data, 'longclaw_checkout', format='json')
@ -55,7 +62,6 @@ class CheckoutTest(LongclawTestCase):
data = {
'address': self.addresses,
'email': self.email,
'shipping_rate': 3.95,
'transaction_id': 'blahblah'
}
self.post_test(data, 'longclaw_checkout_prepaid', format='json')
@ -65,3 +71,111 @@ class CheckoutTest(LongclawTestCase):
Test api endpoint checkout/token/
"""
self.get_test('longclaw_checkout_token')
class CheckoutTest(TestCase):
def test_checkout_form(self):
'''
Test we can create the form without a shipping option
'''
data = {
'email': 'test@test.com',
'different_billing_address': False
}
form = CheckoutForm(data=data)
self.assertTrue(form.is_valid(), form.errors.as_json())
def test_invalid_checkout_form(self):
'''
Test making an invalid form
'''
form = CheckoutForm({
'email': ''
})
self.assertFalse(form.is_valid())
def test_get_checkout(self):
'''
Test the checkout GET view
'''
request = RequestFactory().get(reverse('longclaw_checkout_view'))
response = CheckoutView.as_view()(request)
self.assertEqual(response.status_code, 200)
def test_post_checkout(self):
'''
Test correctly posting to the checkout view
'''
country = CountryFactory()
request = RequestFactory().post(
reverse('longclaw_checkout_view'),
{
'shipping-name': 'bob',
'shipping-line_1': 'blah blah',
'shipping-postcode': 'ytxx 23x',
'shipping-city': 'London',
'shipping-country': country.pk,
'email': 'test@test.com'
}
)
request.session = {}
bid = basket_id(request)
BasketItemFactory(basket_id=bid)
response = CheckoutView.as_view()(request)
self.assertEqual(response.status_code, 302)
def test_post_checkout_billing(self):
'''
Test using an alternate shipping
address in the checkout view
'''
country = CountryFactory()
request = RequestFactory().post(
reverse('longclaw_checkout_view'),
{
'shipping-name': 'bob',
'shipping-line_1': 'blah blah',
'shipping-postcode': 'ytxx 23x',
'shipping-city': 'London',
'shipping-country': country.pk,
'billing-name': 'john',
'billing-line_1': 'somewhere',
'billing-postcode': 'lmewrewr',
'billing-city': 'London',
'billing-country': country.pk,
'email': 'test@test.com',
'different_billing_address': True
}
)
request.session = {}
bid = basket_id(request)
BasketItemFactory(basket_id=bid)
response = CheckoutView.as_view()(request)
self.assertEqual(response.status_code, 302)
def test_post_checkout_invalid(self):
'''
Test posting an invalid form.
This should return a 200 response - rerendering
the form page with the errors
'''
request = RequestFactory().post(
reverse('longclaw_checkout_view')
)
request.session = {}
bid = basket_id(request)
BasketItemFactory(basket_id=bid)
response = CheckoutView.as_view()(request)
self.assertEqual(response.status_code, 200)
def test_checkout_success(self):
'''
Test the checkout success view
'''
address = AddressFactory()
order = OrderFactory(shipping_address=address, billing_address=address)
response = self.client.get(reverse('longclaw_checkout_success', kwargs={'pk': order.id}))
self.assertEqual(response.status_code, 200)

Wyświetl plik

@ -1,5 +1,5 @@
from django.conf.urls import url
from longclaw.longclawcheckout import api
from longclaw.longclawcheckout import api, views
from longclaw.settings import API_URL_PREFIX
urlpatterns = [
@ -11,5 +11,11 @@ urlpatterns = [
name='longclaw_checkout_prepaid'),
url(API_URL_PREFIX + r'checkout/token/$',
api.create_token,
name='longclaw_checkout_token')
]
name='longclaw_checkout_token'),
url(r'checkout/$',
views.CheckoutView.as_view(),
name='longclaw_checkout_view'),
url(r'checkout/success/(?P<pk>[0-9]+)/$',
views.checkout_success,
name='longclaw_checkout_success')
]

Wyświetl plik

@ -1,41 +1,76 @@
from django.utils.module_loading import import_string
from django.utils import timezone
from ipware.ip import get_real_ip
from longclaw.longclawbasket.utils import get_basket_items, destroy_basket
from longclaw.longclawshipping.utils import get_shipping_cost
from longclaw.longclaworders.models import Order, OrderItem
from longclaw.longclawshipping.models import Address
from longclaw.longclawsettings.models import LongclawSettings
from longclaw import settings
class PaymentError(Exception):
def __init__(self, message):
self.message = str(message)
GATEWAY = import_string(settings.PAYMENT_GATEWAY)()
def create_order(basket_items,
addresses,
email,
shipping_rate,
ip_address='0.0.0.0'):
def create_order(email,
request,
addresses=None,
shipping_address=None,
billing_address=None,
shipping_option=None,
capture_payment=False):
'''
Create an order from a basket and customer infomation
'''
if isinstance(addresses, dict):
shipping_address, _ = Address.objects.get_or_create(name=addresses['shipping_name'],
basket_items, _ = get_basket_items(request)
if addresses:
# Longclaw < 0.2 used 'shipping_name', longclaw > 0.2 uses a consistent
# prefix (shipping_address_xxxx)
try:
shipping_name = addresses['shipping_name']
except KeyError:
shipping_name = addresses['shipping_address_name']
shipping_country = addresses['shipping_address_country']
if not shipping_country:
shipping_country = None
shipping_address, _ = Address.objects.get_or_create(name=shipping_name,
line_1=addresses[
'shipping_address_line1'],
city=addresses[
'shipping_address_city'],
postcode=addresses[
'shipping_address_zip'],
country=addresses[
'shipping_address_country'])
country=shipping_country)
shipping_address.save()
billing_address, _ = Address.objects.get_or_create(name=addresses['billing_name'],
try:
billing_name = addresses['billing_name']
except KeyError:
billing_name = addresses['billing_address_name']
billing_country = addresses['shipping_address_country']
if not billing_country:
billing_country = None
billing_address, _ = Address.objects.get_or_create(name=billing_name,
line_1=addresses[
'billing_address_line1'],
city=addresses[
'billing_address_city'],
postcode=addresses[
'billing_address_zip'],
country=addresses[
'billing_address_country'])
country=billing_country)
billing_address.save()
else:
shipping_country = shipping_address.country
ip_address = get_real_ip(request)
if shipping_country and shipping_option:
site_settings = LongclawSettings.for_site(request.site)
shipping_rate = get_shipping_cost(
shipping_address.country.pk,
shipping_option,
site_settings)
else:
shipping_rate = 0
order = Order(
email=email,
ip_address=ip_address,
@ -44,8 +79,11 @@ def create_order(basket_items,
shipping_rate=shipping_rate
)
order.save()
# Create the order items
# Create the order items & compute total
total = 0
for item in basket_items:
total += item.total()
order_item = OrderItem(
product=item.variant,
quantity=item.quantity,
@ -53,4 +91,13 @@ def create_order(basket_items,
)
order_item.save()
if capture_payment:
desc = 'Payment from {} for order id #{}'.format(email, order.id)
transaction_id = GATEWAY.create_payment(request,
float(total) + shipping_rate,
description=desc)
order.payment_date = timezone.now()
order.transaction_id = transaction_id
# Once the order has been successfully taken, we can empty the basket
destroy_basket(request)
return order

Wyświetl plik

@ -1,3 +1,80 @@
from django.shortcuts import render
from django.shortcuts import render, get_object_or_404
from django.views.generic import TemplateView
from django.views.decorators.http import require_GET
from django.http import HttpResponseRedirect
# Create your views here.
try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
from longclaw.longclawshipping.forms import AddressForm
from longclaw.longclawcheckout.forms import CheckoutForm
from longclaw.longclawcheckout.utils import create_order
from longclaw.longclawbasket.utils import get_basket_items
from longclaw.longclaworders.models import Order
@require_GET
def checkout_success(request, pk):
order = get_object_or_404(Order, id=pk)
return render(request, "longclawcheckout/success.html", {'order': order})
class CheckoutView(TemplateView):
template_name = "longclawcheckout/checkout.html"
checkout_form = CheckoutForm
shipping_address_form = AddressForm
billing_address_form = AddressForm
def get_context_data(self, **kwargs):
context = super(CheckoutView, self).get_context_data(**kwargs)
items, _ = get_basket_items(self.request)
total_price = sum(item.total() for item in items)
site = getattr(self.request, 'site', None)
context['checkout_form'] = self.checkout_form(
self.request.POST or None)
context['shipping_form'] = self.shipping_address_form(
self.request.POST or None,
prefix='shipping',
site=site)
context['billing_form'] = self.billing_address_form(
self.request.POST or None,
prefix='billing',
site=site)
context['basket'] = items
context['total_price'] = total_price
return context
def post(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
checkout_form = context['checkout_form']
shipping_form = context['shipping_form']
all_ok = checkout_form.is_valid() and shipping_form.is_valid()
if all_ok:
email = checkout_form.cleaned_data['email']
shipping_option = checkout_form.cleaned_data.get(
'shipping_option', None)
shipping_address = shipping_form.save()
if checkout_form.cleaned_data['different_billing_address']:
billing_form = context['billing_form']
all_ok = billing_form.is_valid()
if all_ok:
billing_address = billing_form.save()
else:
billing_address = shipping_address
if all_ok:
order = create_order(
email,
request,
shipping_address=shipping_address,
billing_address=billing_address,
shipping_option=shipping_option,
capture_payment=True
)
return HttpResponseRedirect(reverse(
'longclaw_checkout_success',
kwargs={'pk': order.id}))
return super(CheckoutView, self).render_to_response(context)

Wyświetl plik

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-16 16:29
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('longclaworders', '0007_auto_20170313_0846'),
]
operations = [
migrations.AlterField(
model_name='order',
name='shipping_address',
field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='orders_shipping_address', to='longclawshipping.Address'),
),
]

Wyświetl plik

@ -24,10 +24,12 @@ class Order(models.Model):
ip_address = models.GenericIPAddressField(blank=True, null=True)
# shipping info
shipping_address = models.ForeignKey(Address, related_name="orders_shipping_address")
shipping_address = models.ForeignKey(
Address, blank=True, related_name="orders_shipping_address")
# billing info
billing_address = models.ForeignKey(Address, blank=True, related_name="orders_billing_address")
billing_address = models.ForeignKey(
Address, blank=True, related_name="orders_billing_address")
shipping_rate = models.DecimalField(max_digits=12,
decimal_places=2,

Wyświetl plik

@ -46,6 +46,5 @@ def shipping_cost(request):
def shipping_countries(request):
''' Get all shipping countries
'''
queryset = models.ShippingRate.objects.all()
country_data = [(c.name, c.code) for obj in queryset for c in obj.countries]
return Response(data=country_data, status=status.HTTP_200_OK)
queryset = models.Country.exclude(shippingrate=None)
return Response(data=queryset, status=status.HTTP_200_OK)

Wyświetl plik

@ -1,38 +0,0 @@
from longclaw.longclawsettings.models import LongclawSettings
from longclaw.longclawshipping.models import ShippingRate
from django_countries import countries, fields
class CountryChoices(object):
'''
Helper class which returns a list of available countries based on
the selected shipping options.
If default_shipping_enabled is ``True`` in the longclaw settings, then
all possible countries are returned. Otherwise only countries for
which a ``ShippingRate`` has been declared are returned.
'''
def __init__(self, **kwargs):
request = kwargs.get('request', None)
self._all_countries = True
if request:
settings = LongclawSettings.for_site(request.site)
self._all_countries = settings.default_shipping_enabled
def __call__(self, *args, **kwargs):
if self._all_countries:
return countries
else:
return ShippingRate.objects.values_list('countries').distinct()
class ShippingCountryField(fields.CountryField):
'''
Country choice field whose choices are constrained by the
configured shipping options.
'''
def __init__(self, *args, **kwargs):
kwargs.update({
'countries': CountryChoices(**kwargs)
})
super(ShippingCountryField, self).__init__(*args, **kwargs)

Wyświetl plik

@ -0,0 +1,25 @@
from django.forms import ModelForm, ModelChoiceField
from longclaw.longclawsettings.models import LongclawSettings
from longclaw.longclawshipping.models import Address, Country
class AddressForm(ModelForm):
class Meta:
model = Address
fields = ['name', 'line_1', 'line_2', 'city', 'postcode', 'country']
def __init__(self, *args, **kwargs):
site = kwargs.pop('site', None)
super(AddressForm, self).__init__(*args, **kwargs)
# Edit the country field to only contain
# countries specified for shipping
all_countries = True
if site:
settings = LongclawSettings.for_site(site)
all_countries = settings.default_shipping_enabled
if all_countries:
queryset = Country.objects.all()
else:
queryset = Country.objects.exclude(shippingrate=None)
self.fields['country'] = ModelChoiceField(queryset)

Wyświetl plik

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-16 16:29
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('longclawshipping', '0002_auto_20170410_1620'),
]
operations = [
migrations.CreateModel(
name='Country',
fields=[
('iso', models.CharField(max_length=2, primary_key=True, serialize=False)),
('name_official', models.CharField(max_length=128)),
('name', models.CharField(max_length=128)),
('sort_priority', models.PositiveIntegerField(default=0)),
],
options={
'verbose_name_plural': 'Countries',
'ordering': ('-sort_priority', 'name'),
},
),
migrations.RemoveField(
model_name='shippingrate',
name='countries',
),
migrations.AddField(
model_name='shippingrate',
name='countries',
field=models.ManyToManyField(to='longclawshipping.Country'),
),
]

Wyświetl plik

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-18 10:26
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('longclawshipping', '0003_auto_20170516_1629'),
]
operations = [
migrations.AlterField(
model_name='address',
name='country',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='longclawshipping.Country'),
),
]

Wyświetl plik

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-18 10:58
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('longclawshipping', '0004_auto_20170518_0526'),
]
operations = [
migrations.AlterField(
model_name='address',
name='country',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='longclawshipping.Country'),
),
]

Wyświetl plik

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-21 08:31
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('longclawshipping', '0005_auto_20170518_0558'),
]
operations = [
migrations.AlterField(
model_name='shippingrate',
name='name',
field=models.CharField(help_text='Unique name to refer to this shipping rate by', max_length=32, unique=True),
),
]

Wyświetl plik

@ -3,7 +3,6 @@ from django.utils.encoding import python_2_unicode_compatible
from wagtail.wagtailadmin.edit_handlers import FieldPanel
from wagtail.wagtailsnippets.models import register_snippet
from django_countries.fields import CountryField
@register_snippet
@python_2_unicode_compatible
@ -13,7 +12,7 @@ class Address(models.Model):
line_2 = models.CharField(max_length=128, blank=True)
city = models.CharField(max_length=64)
postcode = models.CharField(max_length=10)
country = CountryField()
country = models.ForeignKey('longclawshipping.Country', blank=True, null=True)
panels = [
FieldPanel('name'),
@ -33,11 +32,15 @@ class ShippingRate(models.Model):
An individual shipping rate. This can be applied to
multiple countries.
'''
name = models.CharField(max_length=32, unique=True)
name = models.CharField(
max_length=32,
unique=True,
help_text="Unique name to refer to this shipping rate by"
)
rate = models.DecimalField(max_digits=12, decimal_places=2)
carrier = models.CharField(max_length=64)
description = models.CharField(max_length=128)
countries = CountryField(multiple=True)
countries = models.ManyToManyField('longclawshipping.Country')
panels = [
FieldPanel('name'),
@ -49,3 +52,33 @@ class ShippingRate(models.Model):
def __str__(self):
return self.name
@python_2_unicode_compatible
class Country(models.Model):
"""
International Organization for Standardization (ISO) 3166-1 Country list
Instance Variables:
iso -- ISO 3166-1 alpha-2
name -- Official country names (in all caps) used by the ISO 3166
display_name -- Country names in title format
sort_priority -- field that allows for customizing the default ordering
0 is the default value, and the higher the value the closer to the
beginning of the list it will be. An example use case would be you will
primarily have addresses for one country, so you want that particular
country to be the first option in an html dropdown box. To do this, you
would simply change the value in the json file or alter
country_grabber.py's priority dictionary and run it to regenerate
the json
"""
iso = models.CharField(max_length=2, primary_key=True)
name_official = models.CharField(max_length=128)
name = models.CharField(max_length=128)
sort_priority = models.PositiveIntegerField(default=0)
class Meta:
verbose_name_plural = 'Countries'
ordering = ('-sort_priority', 'name',)
def __str__(self):
''' Return the display form of the country name'''
return self.name

Wyświetl plik

@ -1,19 +1,19 @@
from rest_framework import serializers
from django_countries.serializer_fields import CountryField
from longclaw.longclawshipping.models import Address, ShippingRate
from longclaw.longclawshipping.models import Address, ShippingRate, Country
class AddressSerializer(serializers.ModelSerializer):
country = CountryField()
country = serializers.PrimaryKeyRelatedField(queryset=Country.objects.all())
class Meta:
model = Address
fields = "__all__"
class ShippingRateSerializer(serializers.ModelSerializer):
class Meta:
model = ShippingRate
fields = "__all__"
class CountrySerializer(serializers.ModelSerializer):
class Meta:
model = Country
fields = "__all__"

Wyświetl plik

@ -1,7 +1,11 @@
from longclaw.tests.utils import LongclawTestCase
from django.test import TestCase
from django.forms.models import model_to_dict
from longclaw.tests.utils import LongclawTestCase, AddressFactory, CountryFactory
from longclaw.longclawshipping.forms import AddressForm
class AddressTest(LongclawTestCase):
def setUp(self):
self.country = CountryFactory()
def test_create_address(self):
"""
Test creating an address object via the api
@ -11,6 +15,16 @@ class AddressTest(LongclawTestCase):
'line_1': 'Bobstreet',
'city': 'Bobsville',
'postcode': 'BOB22 2BO',
'country': 'UK'
'country': self.country.pk
}
self.post_test(data, 'longclaw_address_list')
class AddressFormTest(TestCase):
def setUp(self):
self.address = AddressFactory()
def test_address_form(self):
form = AddressForm(data=model_to_dict(self.address))
self.assertTrue(form.is_valid(), form.errors.as_json())

Wyświetl plik

@ -7,12 +7,15 @@ class InvalidShippingRate(Exception):
class InvalidShippingCountry(Exception):
pass
def get_shipping_cost(country_code, option, settings):
def get_shipping_cost(country_code, name, settings):
"""
Return the shipping cost for a given country code and shipping option (shipping rate name)
"""
try:
qrs = models.ShippingRate.objects.filter(countries__contains=country_code)
qrs = models.ShippingRate.objects.filter(countries__in=[country_code])
try:
if qrs.count() > 1:
shipping_rate = qrs.filter(name=option)[0]
shipping_rate = qrs.filter(name=name)[0]
else:
shipping_rate = qrs[0]
return {

Wyświetl plik

@ -0,0 +1 @@
{% templatetag openblock %} extends "base.html" {% templatetag closeblock %}

Wyświetl plik

@ -0,0 +1 @@
{% templatetag openblock %} extends "base.html" {% templatetag closeblock %}

Wyświetl plik

@ -23,6 +23,7 @@ STRIPE_PUBLISHABLE = getattr(settings, 'STRIPE_PUBLISHABLE', '')
STRIPE_SECRET = getattr(settings, 'STRIPE_SECRET', '')
# Only required if using Braintree as the payment gateway
BRAINTREE_SANDBOX = getattr(settings, 'BRAINTREE_SANDBOX', False)
BRAINTREE_MERCHANT_ID = getattr(settings, 'BRAINTREE_MERCHANT_ID', '')
BRAINTREE_PUBLIC_KEY = getattr(settings, 'BRAINTREE_PUBLIC_KEY', '')
BRAINTREE_PRIVATE_KEY = getattr(settings, 'BRAINTREE_PRIVATE_KEY', '')

Wyświetl plik

@ -1,27 +1,27 @@
# -*- coding: utf-8
from __future__ import unicode_literals, absolute_import
import os
import django
DEBUG = True
USE_TZ = True
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk"
SECRET_KEY = 'kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk'
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ":memory:",
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
ROOT_URLCONF = "tests.urls"
ROOT_URLCONF = 'longclaw.tests.urls'
INSTALLED_APPS = [
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sites",
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sites',
'django.contrib.sessions',
'wagtail.wagtailforms',
@ -42,24 +42,39 @@ INSTALLED_APPS = [
'taggit',
'rest_framework',
'django_extensions',
'django_countries',
"longclaw.longclawsettings",
"longclaw.longclawshipping",
"longclaw.longclawproducts",
"longclaw.longclaworders",
"longclaw.longclawcheckout",
"longclaw.longclawbasket",
"longclaw.tests.products"
'longclaw.longclawsettings',
'longclaw.longclawshipping',
'longclaw.longclawproducts',
'longclaw.longclaworders',
'longclaw.longclawcheckout',
'longclaw.longclawbasket',
'longclaw.tests.products',
]
SITE_ID = 1
if django.VERSION >= (1, 10):
MIDDLEWARE = ()
else:
MIDDLEWARE_CLASSES = ()
MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'wagtail.wagtailcore.middleware.SiteMiddleware',
'wagtail.wagtailredirects.middleware.RedirectMiddleware',
]
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(os.path.dirname(__file__), 'templates'),
],
'APP_DIRS': True,
},
]
if django.VERSION >= (1, 10):
MIDDLEWARE = MIDDLEWARE
else:
MIDDLEWARE_CLASSES = MIDDLEWARE
PRODUCT_VARIANT_MODEL = 'products.ProductVariant'

Wyświetl plik

@ -8,8 +8,55 @@ from wagtail_factories import PageFactory
from longclaw.longclawproducts.models import Product
from longclaw.longclawbasket.models import BasketItem
from longclaw.longclaworders.models import Order, OrderItem
from longclaw.longclawshipping.models import Address, Country, ShippingRate
from longclaw.utils import ProductVariant
class OrderFactory(factory.django.DjangoModelFactory):
class Meta:
model = Order
class CountryFactory(factory.django.DjangoModelFactory):
class Meta:
model = Country
iso = factory.Faker('pystr', max_chars=2, min_chars=2)
name_official = factory.Faker('text', max_nb_chars=128)
name = factory.Faker('text', max_nb_chars=128)
class AddressFactory(factory.django.DjangoModelFactory):
class Meta:
model = Address
name = factory.Faker('text', max_nb_chars=64)
line_1 = factory.Faker('text', max_nb_chars=128)
line_2 = factory.Faker('text', max_nb_chars=128)
city = factory.Faker('text', max_nb_chars=64)
postcode = factory.Faker('text', max_nb_chars=10)
country = factory.SubFactory(CountryFactory)
class ShippingRateFactory(factory.django.DjangoModelFactory):
class Meta:
model = ShippingRate
name = factory.Faker('text', max_nb_chars=32)
rate = 1.0
carrier = 'Royal Mail'
description = 'Test'
@factory.post_generation
def countries(self, create, extracted, **kwargs):
if not create:
# Simple build, do nothing.
return
if extracted:
# A list of countries were passed in, use them
for country in extracted:
self.countries.add(country)
class ProductFactory(PageFactory):
''' Create a random Product
'''

Wyświetl plik

@ -1,5 +1,6 @@
django-countries>=4.1
django-extensions==1.7.5
django-ipware>=1.1.6
djangorestframework==3.5.4
wagtail>=1.7

Wyświetl plik

@ -90,7 +90,8 @@ setup(
'wagtail>=1.7',
'django-countries>=4.3',
'django-extensions>=1.7.5',
'djangorestframework>=3.5.4'
'djangorestframework>=3.5.4',
'django-ipware>=1.1.6'
],
license="MIT",
zip_safe=False,