From 3de6f481240073bbfbf49e0473e062b23d45c06c Mon Sep 17 00:00:00 2001 From: James Ramm Date: Wed, 24 May 2017 11:12:36 +0100 Subject: [PATCH] Checkout view (#68) Fixes #50, #59, #60, #65 --- .travis.yml | 1 + README.rst | 3 + codecov.yml | 2 + docs/usage/checkout.rst | 193 +- docs/usage/checkout_api.rst | 175 ++ docs/usage/index.rst | 1 + .../longclawbasket/templatetags/__init__.py | 0 .../templatetags/longclawbasket_tags.py | 12 + longclaw/longclawcheckout/api.py | 62 +- longclaw/longclawcheckout/errors.py | 4 + longclaw/longclawcheckout/forms.py | 9 + longclaw/longclawcheckout/gateways/base.py | 22 +- .../longclawcheckout/gateways/braintree.py | 33 +- longclaw/longclawcheckout/gateways/stripe.py | 6 +- .../longclawcheckout/templatetags/__init__.py | 0 .../templatetags/longclawcheckout_tags.py | 28 + longclaw/longclawcheckout/tests.py | 140 +- longclaw/longclawcheckout/urls.py | 12 +- longclaw/longclawcheckout/utils.py | 81 +- longclaw/longclawcheckout/views.py | 81 +- .../migrations/0008_auto_20170516_1629.py | 21 + longclaw/longclaworders/models.py | 6 +- longclaw/longclawshipping/api.py | 5 +- longclaw/longclawshipping/fields.py | 38 - .../fixtures/longclawshipping_initial.json | 2216 +++++++++++++++++ longclaw/longclawshipping/forms.py | 25 + .../migrations/0003_auto_20170516_1629.py | 37 + .../migrations/0004_auto_20170518_0526.py | 21 + .../migrations/0005_auto_20170518_0558.py | 21 + .../migrations/0006_auto_20170521_0831.py | 20 + longclaw/longclawshipping/models.py | 41 +- longclaw/longclawshipping/serializers.py | 12 +- longclaw/longclawshipping/tests.py | 20 +- longclaw/longclawshipping/utils.py | 9 +- .../templates/longclawcheckout/checkout.html | 1 + .../templates/longclawcheckout/success.html | 1 + longclaw/settings.py | 1 + longclaw/tests/settings.py | 57 +- .../templates/longclawcheckout/success.html | 0 longclaw/tests/utils.py | 47 + requirements.txt | 1 + setup.py | 3 +- 42 files changed, 3124 insertions(+), 344 deletions(-) create mode 100644 codecov.yml create mode 100644 docs/usage/checkout_api.rst create mode 100644 longclaw/longclawbasket/templatetags/__init__.py create mode 100644 longclaw/longclawbasket/templatetags/longclawbasket_tags.py create mode 100644 longclaw/longclawcheckout/errors.py create mode 100644 longclaw/longclawcheckout/templatetags/__init__.py create mode 100644 longclaw/longclawcheckout/templatetags/longclawcheckout_tags.py create mode 100644 longclaw/longclaworders/migrations/0008_auto_20170516_1629.py delete mode 100644 longclaw/longclawshipping/fields.py create mode 100644 longclaw/longclawshipping/fixtures/longclawshipping_initial.json create mode 100644 longclaw/longclawshipping/forms.py create mode 100644 longclaw/longclawshipping/migrations/0003_auto_20170516_1629.py create mode 100644 longclaw/longclawshipping/migrations/0004_auto_20170518_0526.py create mode 100644 longclaw/longclawshipping/migrations/0005_auto_20170518_0558.py create mode 100644 longclaw/longclawshipping/migrations/0006_auto_20170521_0831.py create mode 100644 longclaw/project_template/project_name/templates/longclawcheckout/checkout.html create mode 100644 longclaw/project_template/project_name/templates/longclawcheckout/success.html create mode 100644 longclaw/tests/templates/longclawcheckout/success.html diff --git a/.travis.yml b/.travis.yml index 30f66c8..b08d866 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,4 +29,5 @@ install: script: tox -e $TOX_ENV after_success: + - coverage xml - codecov -e TOX_ENV diff --git a/README.rst b/README.rst index 85390a6..8950e36 100644 --- a/README.rst +++ b/README.rst @@ -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 diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..764d31a --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +codecov: + token: c4e276fe-1d69-49e9-bbbe-fabaf4890222 \ No newline at end of file diff --git a/docs/usage/checkout.rst b/docs/usage/checkout.rst index dac18c1..e450af7 100644 --- a/docs/usage/checkout.rst +++ b/docs/usage/checkout.rst @@ -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 `_. -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). diff --git a/docs/usage/checkout_api.rst b/docs/usage/checkout_api.rst new file mode 100644 index 0000000..700f151 --- /dev/null +++ b/docs/usage/checkout_api.rst @@ -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 `_. +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 => {...}) + + + + diff --git a/docs/usage/index.rst b/docs/usage/index.rst index ca2d1a8..28ea885 100644 --- a/docs/usage/index.rst +++ b/docs/usage/index.rst @@ -10,6 +10,7 @@ Usage Guide products basket checkout + checkout_api shipping orders payments diff --git a/longclaw/longclawbasket/templatetags/__init__.py b/longclaw/longclawbasket/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/longclaw/longclawbasket/templatetags/longclawbasket_tags.py b/longclaw/longclawbasket/templatetags/longclawbasket_tags.py new file mode 100644 index 0000000..0d8791a --- /dev/null +++ b/longclaw/longclawbasket/templatetags/longclawbasket_tags.py @@ -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 diff --git a/longclaw/longclawcheckout/api.py b/longclaw/longclawcheckout/api.py index f361af9..dea40e4 100644 --- a/longclaw/longclawcheckout/api.py +++ b/longclaw/longclawcheckout/api.py @@ -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: diff --git a/longclaw/longclawcheckout/errors.py b/longclaw/longclawcheckout/errors.py new file mode 100644 index 0000000..274338a --- /dev/null +++ b/longclaw/longclawcheckout/errors.py @@ -0,0 +1,4 @@ + +class PaymentError(Exception): + def __init__(self, message): + self.message = str(message) diff --git a/longclaw/longclawcheckout/forms.py b/longclaw/longclawcheckout/forms.py index e69de29..23a8a56 100644 --- a/longclaw/longclawcheckout/forms.py +++ b/longclaw/longclawcheckout/forms.py @@ -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) diff --git a/longclaw/longclawcheckout/gateways/base.py b/longclaw/longclawcheckout/gateways/base.py index 7ea4f0e..a5b16fb 100644 --- a/longclaw/longclawcheckout/gateways/base.py +++ b/longclaw/longclawcheckout/gateways/base.py @@ -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 '.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() diff --git a/longclaw/longclawcheckout/tests.py b/longclaw/longclawcheckout/tests.py index dacdf7d..45efb67 100644 --- a/longclaw/longclawcheckout/tests.py +++ b/longclaw/longclawcheckout/tests.py @@ -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) + + diff --git a/longclaw/longclawcheckout/urls.py b/longclaw/longclawcheckout/urls.py index 25ea21d..08c5005 100644 --- a/longclaw/longclawcheckout/urls.py +++ b/longclaw/longclawcheckout/urls.py @@ -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') -] \ No newline at end of file + name='longclaw_checkout_token'), + url(r'checkout/$', + views.CheckoutView.as_view(), + name='longclaw_checkout_view'), + url(r'checkout/success/(?P[0-9]+)/$', + views.checkout_success, + name='longclaw_checkout_success') +] diff --git a/longclaw/longclawcheckout/utils.py b/longclaw/longclawcheckout/utils.py index 754795b..fda5d35 100644 --- a/longclaw/longclawcheckout/utils.py +++ b/longclaw/longclawcheckout/utils.py @@ -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 diff --git a/longclaw/longclawcheckout/views.py b/longclaw/longclawcheckout/views.py index 91ea44a..319472d 100644 --- a/longclaw/longclawcheckout/views.py +++ b/longclaw/longclawcheckout/views.py @@ -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) diff --git a/longclaw/longclaworders/migrations/0008_auto_20170516_1629.py b/longclaw/longclaworders/migrations/0008_auto_20170516_1629.py new file mode 100644 index 0000000..43c0111 --- /dev/null +++ b/longclaw/longclaworders/migrations/0008_auto_20170516_1629.py @@ -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'), + ), + ] diff --git a/longclaw/longclaworders/models.py b/longclaw/longclaworders/models.py index 4a2b327..31b3557 100644 --- a/longclaw/longclaworders/models.py +++ b/longclaw/longclaworders/models.py @@ -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, diff --git a/longclaw/longclawshipping/api.py b/longclaw/longclawshipping/api.py index 2f35766..e4502a0 100644 --- a/longclaw/longclawshipping/api.py +++ b/longclaw/longclawshipping/api.py @@ -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) diff --git a/longclaw/longclawshipping/fields.py b/longclaw/longclawshipping/fields.py deleted file mode 100644 index 8359ccc..0000000 --- a/longclaw/longclawshipping/fields.py +++ /dev/null @@ -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) - diff --git a/longclaw/longclawshipping/fixtures/longclawshipping_initial.json b/longclaw/longclawshipping/fixtures/longclawshipping_initial.json new file mode 100644 index 0000000..62a9ede --- /dev/null +++ b/longclaw/longclawshipping/fixtures/longclawshipping_initial.json @@ -0,0 +1,2216 @@ +[ + { + "pk": "AF", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "AFGHANISTAN", + "name": "Afghanistan" + } + }, + { + "pk": "AX", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "\u00c5LAND ISLANDS", + "name": "\u00c5Land Islands" + } + }, + { + "pk": "AL", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ALBANIA", + "name": "Albania" + } + }, + { + "pk": "DZ", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ALGERIA", + "name": "Algeria" + } + }, + { + "pk": "AS", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "AMERICAN SAMOA", + "name": "American Samoa" + } + }, + { + "pk": "AD", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ANDORRA", + "name": "Andorra" + } + }, + { + "pk": "AO", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ANGOLA", + "name": "Angola" + } + }, + { + "pk": "AI", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ANGUILLA", + "name": "Anguilla" + } + }, + { + "pk": "AQ", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ANTARCTICA", + "name": "Antarctica" + } + }, + { + "pk": "AG", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ANTIGUA AND BARBUDA", + "name": "Antigua And Barbuda" + } + }, + { + "pk": "AR", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ARGENTINA", + "name": "Argentina" + } + }, + { + "pk": "AM", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ARMENIA", + "name": "Armenia" + } + }, + { + "pk": "AW", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ARUBA", + "name": "Aruba" + } + }, + { + "pk": "AU", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "AUSTRALIA", + "name": "Australia" + } + }, + { + "pk": "AT", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "AUSTRIA", + "name": "Austria" + } + }, + { + "pk": "AZ", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "AZERBAIJAN", + "name": "Azerbaijan" + } + }, + { + "pk": "BS", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BAHAMAS", + "name": "Bahamas" + } + }, + { + "pk": "BH", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BAHRAIN", + "name": "Bahrain" + } + }, + { + "pk": "BD", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BANGLADESH", + "name": "Bangladesh" + } + }, + { + "pk": "BB", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BARBADOS", + "name": "Barbados" + } + }, + { + "pk": "BY", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BELARUS", + "name": "Belarus" + } + }, + { + "pk": "BE", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BELGIUM", + "name": "Belgium" + } + }, + { + "pk": "BZ", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BELIZE", + "name": "Belize" + } + }, + { + "pk": "BJ", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BENIN", + "name": "Benin" + } + }, + { + "pk": "BM", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BERMUDA", + "name": "Bermuda" + } + }, + { + "pk": "BT", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BHUTAN", + "name": "Bhutan" + } + }, + { + "pk": "BO", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BOLIVIA, PLURINATIONAL STATE OF", + "name": "Bolivia, Plurinational State Of" + } + }, + { + "pk": "BA", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BOSNIA AND HERZEGOVINA", + "name": "Bosnia And Herzegovina" + } + }, + { + "pk": "BW", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BOTSWANA", + "name": "Botswana" + } + }, + { + "pk": "BV", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BOUVET ISLAND", + "name": "Bouvet Island" + } + }, + { + "pk": "BR", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BRAZIL", + "name": "Brazil" + } + }, + { + "pk": "IO", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BRITISH INDIAN OCEAN TERRITORY", + "name": "British Indian Ocean Territory" + } + }, + { + "pk": "BN", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BRUNEI DARUSSALAM", + "name": "Brunei Darussalam" + } + }, + { + "pk": "BG", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BULGARIA", + "name": "Bulgaria" + } + }, + { + "pk": "BF", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BURKINA FASO", + "name": "Burkina Faso" + } + }, + { + "pk": "BI", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "BURUNDI", + "name": "Burundi" + } + }, + { + "pk": "KH", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "CAMBODIA", + "name": "Cambodia" + } + }, + { + "pk": "CM", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "CAMEROON", + "name": "Cameroon" + } + }, + { + "pk": "CA", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "CANADA", + "name": "Canada" + } + }, + { + "pk": "CV", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "CAPE VERDE", + "name": "Cape Verde" + } + }, + { + "pk": "KY", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "CAYMAN ISLANDS", + "name": "Cayman Islands" + } + }, + { + "pk": "CF", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "CENTRAL AFRICAN REPUBLIC", + "name": "Central African Republic" + } + }, + { + "pk": "TD", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "CHAD", + "name": "Chad" + } + }, + { + "pk": "CL", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "CHILE", + "name": "Chile" + } + }, + { + "pk": "CN", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "CHINA", + "name": "China" + } + }, + { + "pk": "CX", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "CHRISTMAS ISLAND", + "name": "Christmas Island" + } + }, + { + "pk": "CC", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "COCOS (KEELING) ISLANDS", + "name": "Cocos (Keeling) Islands" + } + }, + { + "pk": "CO", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "COLOMBIA", + "name": "Colombia" + } + }, + { + "pk": "KM", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "COMOROS", + "name": "Comoros" + } + }, + { + "pk": "CG", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "CONGO", + "name": "Congo" + } + }, + { + "pk": "CD", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "CONGO, THE DEMOCRATIC REPUBLIC OF THE", + "name": "Congo, The Democratic Republic Of The" + } + }, + { + "pk": "CK", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "COOK ISLANDS", + "name": "Cook Islands" + } + }, + { + "pk": "CR", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "COSTA RICA", + "name": "Costa Rica" + } + }, + { + "pk": "CI", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "C\u00d4TE D'IVOIRE", + "name": "C\u00d4Te D'Ivoire" + } + }, + { + "pk": "HR", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "CROATIA", + "name": "Croatia" + } + }, + { + "pk": "CU", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "CUBA", + "name": "Cuba" + } + }, + { + "pk": "CY", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "CYPRUS", + "name": "Cyprus" + } + }, + { + "pk": "CZ", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "CZECH REPUBLIC", + "name": "Czech Republic" + } + }, + { + "pk": "DK", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "DENMARK", + "name": "Denmark" + } + }, + { + "pk": "DJ", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "DJIBOUTI", + "name": "Djibouti" + } + }, + { + "pk": "DM", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "DOMINICA", + "name": "Dominica" + } + }, + { + "pk": "DO", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "DOMINICAN REPUBLIC", + "name": "Dominican Republic" + } + }, + { + "pk": "EC", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ECUADOR", + "name": "Ecuador" + } + }, + { + "pk": "EG", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "EGYPT", + "name": "Egypt" + } + }, + { + "pk": "SV", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "EL SALVADOR", + "name": "El Salvador" + } + }, + { + "pk": "GQ", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "EQUATORIAL GUINEA", + "name": "Equatorial Guinea" + } + }, + { + "pk": "ER", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ERITREA", + "name": "Eritrea" + } + }, + { + "pk": "EE", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ESTONIA", + "name": "Estonia" + } + }, + { + "pk": "ET", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ETHIOPIA", + "name": "Ethiopia" + } + }, + { + "pk": "FK", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "FALKLAND ISLANDS (MALVINAS)", + "name": "Falkland Islands (Malvinas)" + } + }, + { + "pk": "FO", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "FAROE ISLANDS", + "name": "Faroe Islands" + } + }, + { + "pk": "FJ", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "FIJI", + "name": "Fiji" + } + }, + { + "pk": "FI", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "FINLAND", + "name": "Finland" + } + }, + { + "pk": "FR", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "FRANCE", + "name": "France" + } + }, + { + "pk": "GF", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "FRENCH GUIANA", + "name": "French Guiana" + } + }, + { + "pk": "PF", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "FRENCH POLYNESIA", + "name": "French Polynesia" + } + }, + { + "pk": "TF", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "FRENCH SOUTHERN TERRITORIES", + "name": "French Southern Territories" + } + }, + { + "pk": "GA", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "GABON", + "name": "Gabon" + } + }, + { + "pk": "GM", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "GAMBIA", + "name": "Gambia" + } + }, + { + "pk": "GE", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "GEORGIA", + "name": "Georgia" + } + }, + { + "pk": "DE", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "GERMANY", + "name": "Germany" + } + }, + { + "pk": "GH", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "GHANA", + "name": "Ghana" + } + }, + { + "pk": "GI", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "GIBRALTAR", + "name": "Gibraltar" + } + }, + { + "pk": "GR", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "GREECE", + "name": "Greece" + } + }, + { + "pk": "GL", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "GREENLAND", + "name": "Greenland" + } + }, + { + "pk": "GD", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "GRENADA", + "name": "Grenada" + } + }, + { + "pk": "GP", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "GUADELOUPE", + "name": "Guadeloupe" + } + }, + { + "pk": "GU", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "GUAM", + "name": "Guam" + } + }, + { + "pk": "GT", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "GUATEMALA", + "name": "Guatemala" + } + }, + { + "pk": "GG", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "GUERNSEY", + "name": "Guernsey" + } + }, + { + "pk": "GN", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "GUINEA", + "name": "Guinea" + } + }, + { + "pk": "GW", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "GUINEA-BISSAU", + "name": "Guinea-Bissau" + } + }, + { + "pk": "GY", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "GUYANA", + "name": "Guyana" + } + }, + { + "pk": "HT", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "HAITI", + "name": "Haiti" + } + }, + { + "pk": "HM", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "HEARD ISLAND AND MCDONALD ISLANDS", + "name": "Heard Island And Mcdonald Islands" + } + }, + { + "pk": "VA", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "HOLY SEE (VATICAN CITY STATE)", + "name": "Holy See (Vatican City State)" + } + }, + { + "pk": "HN", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "HONDURAS", + "name": "Honduras" + } + }, + { + "pk": "HK", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "HONG KONG", + "name": "Hong Kong" + } + }, + { + "pk": "HU", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "HUNGARY", + "name": "Hungary" + } + }, + { + "pk": "IS", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ICELAND", + "name": "Iceland" + } + }, + { + "pk": "IN", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "INDIA", + "name": "India" + } + }, + { + "pk": "ID", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "INDONESIA", + "name": "Indonesia" + } + }, + { + "pk": "IR", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "IRAN, ISLAMIC REPUBLIC OF", + "name": "Iran, Islamic Republic Of" + } + }, + { + "pk": "IQ", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "IRAQ", + "name": "Iraq" + } + }, + { + "pk": "IE", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "IRELAND", + "name": "Ireland" + } + }, + { + "pk": "IM", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ISLE OF MAN", + "name": "Isle Of Man" + } + }, + { + "pk": "IL", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ISRAEL", + "name": "Israel" + } + }, + { + "pk": "IT", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ITALY", + "name": "Italy" + } + }, + { + "pk": "JM", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "JAMAICA", + "name": "Jamaica" + } + }, + { + "pk": "JP", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "JAPAN", + "name": "Japan" + } + }, + { + "pk": "JE", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "JERSEY", + "name": "Jersey" + } + }, + { + "pk": "JO", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "JORDAN", + "name": "Jordan" + } + }, + { + "pk": "KZ", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "KAZAKHSTAN", + "name": "Kazakhstan" + } + }, + { + "pk": "KE", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "KENYA", + "name": "Kenya" + } + }, + { + "pk": "KI", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "KIRIBATI", + "name": "Kiribati" + } + }, + { + "pk": "KP", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF", + "name": "Korea, Democratic People'S Republic Of" + } + }, + { + "pk": "KR", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "KOREA, REPUBLIC OF", + "name": "Korea, Republic Of" + } + }, + { + "pk": "KW", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "KUWAIT", + "name": "Kuwait" + } + }, + { + "pk": "KG", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "KYRGYZSTAN", + "name": "Kyrgyzstan" + } + }, + { + "pk": "LA", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "LAO PEOPLE'S DEMOCRATIC REPUBLIC", + "name": "Lao People'S Democratic Republic" + } + }, + { + "pk": "LV", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "LATVIA", + "name": "Latvia" + } + }, + { + "pk": "LB", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "LEBANON", + "name": "Lebanon" + } + }, + { + "pk": "LS", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "LESOTHO", + "name": "Lesotho" + } + }, + { + "pk": "LR", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "LIBERIA", + "name": "Liberia" + } + }, + { + "pk": "LY", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "LIBYAN ARAB JAMAHIRIYA", + "name": "Libyan Arab Jamahiriya" + } + }, + { + "pk": "LI", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "LIECHTENSTEIN", + "name": "Liechtenstein" + } + }, + { + "pk": "LT", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "LITHUANIA", + "name": "Lithuania" + } + }, + { + "pk": "LU", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "LUXEMBOURG", + "name": "Luxembourg" + } + }, + { + "pk": "MO", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MACAO", + "name": "Macao" + } + }, + { + "pk": "MK", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF", + "name": "Macedonia, The Former Yugoslav Republic Of" + } + }, + { + "pk": "MG", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MADAGASCAR", + "name": "Madagascar" + } + }, + { + "pk": "MW", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MALAWI", + "name": "Malawi" + } + }, + { + "pk": "MY", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MALAYSIA", + "name": "Malaysia" + } + }, + { + "pk": "MV", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MALDIVES", + "name": "Maldives" + } + }, + { + "pk": "ML", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MALI", + "name": "Mali" + } + }, + { + "pk": "MT", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MALTA", + "name": "Malta" + } + }, + { + "pk": "MH", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MARSHALL ISLANDS", + "name": "Marshall Islands" + } + }, + { + "pk": "MQ", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MARTINIQUE", + "name": "Martinique" + } + }, + { + "pk": "MR", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MAURITANIA", + "name": "Mauritania" + } + }, + { + "pk": "MU", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MAURITIUS", + "name": "Mauritius" + } + }, + { + "pk": "YT", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MAYOTTE", + "name": "Mayotte" + } + }, + { + "pk": "MX", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MEXICO", + "name": "Mexico" + } + }, + { + "pk": "FM", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MICRONESIA, FEDERATED STATES OF", + "name": "Micronesia, Federated States Of" + } + }, + { + "pk": "MD", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MOLDOVA, REPUBLIC OF", + "name": "Moldova, Republic Of" + } + }, + { + "pk": "MC", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MONACO", + "name": "Monaco" + } + }, + { + "pk": "MN", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MONGOLIA", + "name": "Mongolia" + } + }, + { + "pk": "ME", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MONTENEGRO", + "name": "Montenegro" + } + }, + { + "pk": "MS", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MONTSERRAT", + "name": "Montserrat" + } + }, + { + "pk": "MA", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MOROCCO", + "name": "Morocco" + } + }, + { + "pk": "MZ", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MOZAMBIQUE", + "name": "Mozambique" + } + }, + { + "pk": "MM", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "MYANMAR", + "name": "Myanmar" + } + }, + { + "pk": "NA", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "NAMIBIA", + "name": "Namibia" + } + }, + { + "pk": "NR", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "NAURU", + "name": "Nauru" + } + }, + { + "pk": "NP", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "NEPAL", + "name": "Nepal" + } + }, + { + "pk": "NL", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "NETHERLANDS", + "name": "Netherlands" + } + }, + { + "pk": "AN", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "NETHERLANDS ANTILLES", + "name": "Netherlands Antilles" + } + }, + { + "pk": "NC", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "NEW CALEDONIA", + "name": "New Caledonia" + } + }, + { + "pk": "NZ", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "NEW ZEALAND", + "name": "New Zealand" + } + }, + { + "pk": "NI", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "NICARAGUA", + "name": "Nicaragua" + } + }, + { + "pk": "NE", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "NIGER", + "name": "Niger" + } + }, + { + "pk": "NG", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "NIGERIA", + "name": "Nigeria" + } + }, + { + "pk": "NU", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "NIUE", + "name": "Niue" + } + }, + { + "pk": "NF", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "NORFOLK ISLAND", + "name": "Norfolk Island" + } + }, + { + "pk": "MP", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "NORTHERN MARIANA ISLANDS", + "name": "Northern Mariana Islands" + } + }, + { + "pk": "NO", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "NORWAY", + "name": "Norway" + } + }, + { + "pk": "OM", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "OMAN", + "name": "Oman" + } + }, + { + "pk": "PK", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "PAKISTAN", + "name": "Pakistan" + } + }, + { + "pk": "PW", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "PALAU", + "name": "Palau" + } + }, + { + "pk": "PS", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "PALESTINIAN TERRITORY, OCCUPIED", + "name": "Palestinian Territory, Occupied" + } + }, + { + "pk": "PA", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "PANAMA", + "name": "Panama" + } + }, + { + "pk": "PG", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "PAPUA NEW GUINEA", + "name": "Papua New Guinea" + } + }, + { + "pk": "PY", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "PARAGUAY", + "name": "Paraguay" + } + }, + { + "pk": "PE", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "PERU", + "name": "Peru" + } + }, + { + "pk": "PH", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "PHILIPPINES", + "name": "Philippines" + } + }, + { + "pk": "PN", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "PITCAIRN", + "name": "Pitcairn" + } + }, + { + "pk": "PL", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "POLAND", + "name": "Poland" + } + }, + { + "pk": "PT", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "PORTUGAL", + "name": "Portugal" + } + }, + { + "pk": "PR", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "PUERTO RICO", + "name": "Puerto Rico" + } + }, + { + "pk": "QA", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "QATAR", + "name": "Qatar" + } + }, + { + "pk": "RE", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "R\u00c9UNION", + "name": "R\u00c9Union" + } + }, + { + "pk": "RO", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ROMANIA", + "name": "Romania" + } + }, + { + "pk": "RU", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "RUSSIAN FEDERATION", + "name": "Russian Federation" + } + }, + { + "pk": "RW", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "RWANDA", + "name": "Rwanda" + } + }, + { + "pk": "BL", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SAINT BARTH\u00c9LEMY", + "name": "Saint Barth\u00c9Lemy" + } + }, + { + "pk": "SH", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SAINT HELENA, ASCENSION AND TRISTAN DA CUNHA", + "name": "Saint Helena, Ascension And Tristan Da Cunha" + } + }, + { + "pk": "KN", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SAINT KITTS AND NEVIS", + "name": "Saint Kitts And Nevis" + } + }, + { + "pk": "LC", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SAINT LUCIA", + "name": "Saint Lucia" + } + }, + { + "pk": "MF", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SAINT MARTIN", + "name": "Saint Martin" + } + }, + { + "pk": "PM", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SAINT PIERRE AND MIQUELON", + "name": "Saint Pierre And Miquelon" + } + }, + { + "pk": "VC", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SAINT VINCENT AND THE GRENADINES", + "name": "Saint Vincent And The Grenadines" + } + }, + { + "pk": "WS", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SAMOA", + "name": "Samoa" + } + }, + { + "pk": "SM", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SAN MARINO", + "name": "San Marino" + } + }, + { + "pk": "ST", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SAO TOME AND PRINCIPE", + "name": "Sao Tome And Principe" + } + }, + { + "pk": "SA", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SAUDI ARABIA", + "name": "Saudi Arabia" + } + }, + { + "pk": "SN", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SENEGAL", + "name": "Senegal" + } + }, + { + "pk": "RS", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SERBIA", + "name": "Serbia" + } + }, + { + "pk": "SC", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SEYCHELLES", + "name": "Seychelles" + } + }, + { + "pk": "SL", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SIERRA LEONE", + "name": "Sierra Leone" + } + }, + { + "pk": "SG", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SINGAPORE", + "name": "Singapore" + } + }, + { + "pk": "SK", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SLOVAKIA", + "name": "Slovakia" + } + }, + { + "pk": "SI", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SLOVENIA", + "name": "Slovenia" + } + }, + { + "pk": "SB", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SOLOMON ISLANDS", + "name": "Solomon Islands" + } + }, + { + "pk": "SO", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SOMALIA", + "name": "Somalia" + } + }, + { + "pk": "ZA", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SOUTH AFRICA", + "name": "South Africa" + } + }, + { + "pk": "GS", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS", + "name": "South Georgia And The South Sandwich Islands" + } + }, + { + "pk": "ES", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SPAIN", + "name": "Spain" + } + }, + { + "pk": "LK", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SRI LANKA", + "name": "Sri Lanka" + } + }, + { + "pk": "SD", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SUDAN", + "name": "Sudan" + } + }, + { + "pk": "SR", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SURINAME", + "name": "Suriname" + } + }, + { + "pk": "SJ", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SVALBARD AND JAN MAYEN", + "name": "Svalbard And Jan Mayen" + } + }, + { + "pk": "SZ", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SWAZILAND", + "name": "Swaziland" + } + }, + { + "pk": "SE", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SWEDEN", + "name": "Sweden" + } + }, + { + "pk": "CH", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SWITZERLAND", + "name": "Switzerland" + } + }, + { + "pk": "SY", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "SYRIAN ARAB REPUBLIC", + "name": "Syrian Arab Republic" + } + }, + { + "pk": "TW", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "TAIWAN, PROVINCE OF CHINA", + "name": "Taiwan, Province Of China" + } + }, + { + "pk": "TJ", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "TAJIKISTAN", + "name": "Tajikistan" + } + }, + { + "pk": "TZ", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "TANZANIA, UNITED REPUBLIC OF", + "name": "Tanzania, United Republic Of" + } + }, + { + "pk": "TH", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "THAILAND", + "name": "Thailand" + } + }, + { + "pk": "TL", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "TIMOR-LESTE", + "name": "Timor-Leste" + } + }, + { + "pk": "TG", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "TOGO", + "name": "Togo" + } + }, + { + "pk": "TK", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "TOKELAU", + "name": "Tokelau" + } + }, + { + "pk": "TO", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "TONGA", + "name": "Tonga" + } + }, + { + "pk": "TT", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "TRINIDAD AND TOBAGO", + "name": "Trinidad And Tobago" + } + }, + { + "pk": "TN", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "TUNISIA", + "name": "Tunisia" + } + }, + { + "pk": "TR", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "TURKEY", + "name": "Turkey" + } + }, + { + "pk": "TM", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "TURKMENISTAN", + "name": "Turkmenistan" + } + }, + { + "pk": "TC", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "TURKS AND CAICOS ISLANDS", + "name": "Turks And Caicos Islands" + } + }, + { + "pk": "TV", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "TUVALU", + "name": "Tuvalu" + } + }, + { + "pk": "UG", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "UGANDA", + "name": "Uganda" + } + }, + { + "pk": "UA", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "UKRAINE", + "name": "Ukraine" + } + }, + { + "pk": "AE", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "UNITED ARAB EMIRATES", + "name": "United Arab Emirates" + } + }, + { + "pk": "GB", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "UNITED KINGDOM", + "name": "United Kingdom" + } + }, + { + "pk": "US", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "UNITED STATES", + "name": "United States" + } + }, + { + "pk": "UM", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "UNITED STATES MINOR OUTLYING ISLANDS", + "name": "United States Minor Outlying Islands" + } + }, + { + "pk": "UY", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "URUGUAY", + "name": "Uruguay" + } + }, + { + "pk": "UZ", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "UZBEKISTAN", + "name": "Uzbekistan" + } + }, + { + "pk": "VU", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "VANUATU", + "name": "Vanuatu" + } + }, + { + "pk": "VE", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "VENEZUELA, BOLIVARIAN REPUBLIC OF", + "name": "Venezuela, Bolivarian Republic Of" + } + }, + { + "pk": "VN", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "VIET NAM", + "name": "Viet Nam" + } + }, + { + "pk": "VG", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "VIRGIN ISLANDS, BRITISH", + "name": "Virgin Islands, British" + } + }, + { + "pk": "VI", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "VIRGIN ISLANDS, U.S.", + "name": "Virgin Islands, U.S." + } + }, + { + "pk": "WF", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "WALLIS AND FUTUNA", + "name": "Wallis And Futuna" + } + }, + { + "pk": "EH", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "WESTERN SAHARA", + "name": "Western Sahara" + } + }, + { + "pk": "YE", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "YEMEN", + "name": "Yemen" + } + }, + { + "pk": "ZM", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ZAMBIA", + "name": "Zambia" + } + }, + { + "pk": "ZW", + "model": "longclawshipping.country", + "fields": { + "sort_priority": 0, + "name_official": "ZIMBABWE", + "name": "Zimbabwe" + } + } +] \ No newline at end of file diff --git a/longclaw/longclawshipping/forms.py b/longclaw/longclawshipping/forms.py new file mode 100644 index 0000000..705a449 --- /dev/null +++ b/longclaw/longclawshipping/forms.py @@ -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) + diff --git a/longclaw/longclawshipping/migrations/0003_auto_20170516_1629.py b/longclaw/longclawshipping/migrations/0003_auto_20170516_1629.py new file mode 100644 index 0000000..de05732 --- /dev/null +++ b/longclaw/longclawshipping/migrations/0003_auto_20170516_1629.py @@ -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'), + ), + ] diff --git a/longclaw/longclawshipping/migrations/0004_auto_20170518_0526.py b/longclaw/longclawshipping/migrations/0004_auto_20170518_0526.py new file mode 100644 index 0000000..94cc4cf --- /dev/null +++ b/longclaw/longclawshipping/migrations/0004_auto_20170518_0526.py @@ -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'), + ), + ] diff --git a/longclaw/longclawshipping/migrations/0005_auto_20170518_0558.py b/longclaw/longclawshipping/migrations/0005_auto_20170518_0558.py new file mode 100644 index 0000000..f61d2d0 --- /dev/null +++ b/longclaw/longclawshipping/migrations/0005_auto_20170518_0558.py @@ -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'), + ), + ] diff --git a/longclaw/longclawshipping/migrations/0006_auto_20170521_0831.py b/longclaw/longclawshipping/migrations/0006_auto_20170521_0831.py new file mode 100644 index 0000000..bf9eae7 --- /dev/null +++ b/longclaw/longclawshipping/migrations/0006_auto_20170521_0831.py @@ -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), + ), + ] diff --git a/longclaw/longclawshipping/models.py b/longclaw/longclawshipping/models.py index 238641f..b3f8a02 100644 --- a/longclaw/longclawshipping/models.py +++ b/longclaw/longclawshipping/models.py @@ -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 diff --git a/longclaw/longclawshipping/serializers.py b/longclaw/longclawshipping/serializers.py index dd3dee2..fec8405 100644 --- a/longclaw/longclawshipping/serializers.py +++ b/longclaw/longclawshipping/serializers.py @@ -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__" - \ No newline at end of file +class CountrySerializer(serializers.ModelSerializer): + class Meta: + model = Country + fields = "__all__" diff --git a/longclaw/longclawshipping/tests.py b/longclaw/longclawshipping/tests.py index 9607fad..8001202 100644 --- a/longclaw/longclawshipping/tests.py +++ b/longclaw/longclawshipping/tests.py @@ -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()) diff --git a/longclaw/longclawshipping/utils.py b/longclaw/longclawshipping/utils.py index 16ecea5..13418c8 100644 --- a/longclaw/longclawshipping/utils.py +++ b/longclaw/longclawshipping/utils.py @@ -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 { diff --git a/longclaw/project_template/project_name/templates/longclawcheckout/checkout.html b/longclaw/project_template/project_name/templates/longclawcheckout/checkout.html new file mode 100644 index 0000000..ddac449 --- /dev/null +++ b/longclaw/project_template/project_name/templates/longclawcheckout/checkout.html @@ -0,0 +1 @@ +{% templatetag openblock %} extends "base.html" {% templatetag closeblock %} diff --git a/longclaw/project_template/project_name/templates/longclawcheckout/success.html b/longclaw/project_template/project_name/templates/longclawcheckout/success.html new file mode 100644 index 0000000..ddac449 --- /dev/null +++ b/longclaw/project_template/project_name/templates/longclawcheckout/success.html @@ -0,0 +1 @@ +{% templatetag openblock %} extends "base.html" {% templatetag closeblock %} diff --git a/longclaw/settings.py b/longclaw/settings.py index 90ae7bc..e837f9f 100644 --- a/longclaw/settings.py +++ b/longclaw/settings.py @@ -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', '') diff --git a/longclaw/tests/settings.py b/longclaw/tests/settings.py index 9d94e65..4436e2c 100644 --- a/longclaw/tests/settings.py +++ b/longclaw/tests/settings.py @@ -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' diff --git a/longclaw/tests/templates/longclawcheckout/success.html b/longclaw/tests/templates/longclawcheckout/success.html new file mode 100644 index 0000000..e69de29 diff --git a/longclaw/tests/utils.py b/longclaw/tests/utils.py index 9c7bd06..d8a3ad5 100644 --- a/longclaw/tests/utils.py +++ b/longclaw/tests/utils.py @@ -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 ''' diff --git a/requirements.txt b/requirements.txt index 830a045..ac68932 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/setup.py b/setup.py index 688bd61..6a04fc1 100755 --- a/setup.py +++ b/setup.py @@ -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,