Add to basket template tag & docs (#82)

* fixes #61

* Update travis

* Update changelog for 0.2

* Add alex as contributor

* Docs update

* tests in longclawcore package

* Remove eggs dir

* update ignores

* Add last entry to changelog

* fixes #79
pull/153/head 0.2.0
James Ramm 2017-08-25 08:33:47 +01:00 zatwierdzone przez GitHub
rodzic 7e67fcc9cd
commit bcec8915c1
15 zmienionych plików z 334 dodań i 62 usunięć

1
.gitignore vendored
Wyświetl plik

@ -53,3 +53,4 @@ docs/_build
webpack-stats.json
*bundle.js*
.eggs/

Wyświetl plik

@ -13,6 +13,8 @@ env:
- TOX_ENV=py35-django-19
- TOX_ENV=py34-django-19
- TOX_ENV=py27-django-19
- TOX_ENV=py35-django-110
- TOX_ENV=py35-django-111
matrix:
fast_finish: true

Wyświetl plik

@ -10,4 +10,5 @@ Development Lead
Contributors
------------
None yet. Why not be the first?
* Alex (https://github.com/alexfromvl)

Wyświetl plik

@ -6,6 +6,16 @@ History
0.2.0 (In Development)
++++++++++++++++++++++
* Added a template tag for easy 'Add To Basket' buttons
* Added a template tag for shipping rates
* Created a client side Javascript library for the REST API
* We built basic views for Checkout and Basket
* Added template tags to help simplify integration with payment backends
* Basic checkout template in the project_template
* Bug fixes around payment gateway integrations
* Created a standard address form
* Pushed test coverage past 80%
0.1.1 (2017-04-14)
+++++++++++++++++++

Wyświetl plik

@ -73,3 +73,5 @@ Loading ``longclawcheckout_tags`` in your template will allow you to retrieve th
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).
For in-depth info on integration, see the walkthrough.

Wyświetl plik

@ -1,10 +1,10 @@
.. checkout-walkthrough:
Checkout with Paypal Express
Checkout with Braintree
============================
Longclaw offers integration with a few payment gateways and it is also fairly easy to integrate your own.
For this tutorial, we will use Paypal Express Checkout to process payments.
For this tutorial, we will use Braintree to process payments.
Settings and Dependencies
-------------------------
@ -13,10 +13,17 @@ The payment gateway to use must be set in the settings file:
.. code-block:: python
PAYMENT_GATEWAY = 'longclaw.longclawcheckout.gateways.braintree.PaypalVZeroPayment'
PAYMENT_GATEWAY = 'longclaw.longclawcheckout.gateways.braintree.BraintreePayment'
The ``PaypalVZeroPayment`` class will allow us to take payments using Paypal Express Checkout and is dependent on the
braintree SDK.
We also need to define settings for access tokens;
.. code-block:: python
BRAINTREE_SANDBOX = False
BRAINTREE_MERCHANT_ID = os.environ['BRAINTREE_MERCHANT_ID']
BRAINTREE_PUBLIC_KEY = os.environ['BRAINTREE_PUBLIC_KEY']
BRAINTREE_PRIVATE_KEY = os.environ['BRAINTREE_PRIVATE_KEY']
We will need to install this SDK as it is not an explicit dependency of longclaw::
@ -27,6 +34,172 @@ That is all we need to do to configure our backend!
Front end integration
---------------------
We will first show how to setup a checkout page using the Checkout view provided by longclaw.
The code shown here is very similar to the implementation of the checkout page here: `Ramshackle Audio<https://github.com/JamesRamm/ramshacklerecording>`_
First, we should load some templatetags which will help us:
.. code-block:: django
{% load longclawcheckout_tags longclawcore_tags %}
As an aside - you may wish to display the items in the basket on our checkout page. The basket items queryset is available as ``basket``
in the views' context.
Next, we need to setup the forms to gather customer information. There are 2 forms in the context. We will
display and submit them as a single form. Here is an example layout:
.. code-block:: django
<form action="." method="post" id="checkout-form">
{% csrf_token %}
{% for field in shipping_form %}
{% if field.is_hidden %}
{{ field }}
{% else %}
{% if field.errors %}
<div class="field error">
{% else %}
<div class="field">
{% endif %}
<label>{{ field.label_tag }}</label>
{{ field }}
{% if field.help_text %}
<p class="help">{{ field.help_text|safe }}</p>
{% endif %}
<div class="ui error message">
<p>{{ field.errors }}</p>
</div>
</div>
{% endif %}
{% endfor %}
{% for field in checkout_form %}
<!-- purposefully ignoring different billing address option to simplify -->
{% if field.name == 'different_billing_address' %}
{% else %}
{% if field.errors %}
<div class="field error">
{% else %}
<div class="field">
{% endif %}
<label>{{ field.label_tag }}</label>
{{ field }}
<div class="ui error message">
{% for error in field.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
You may wish to layout the form differently. We have purposefully ignored the ``different_billing_address`` field
since the Braintree dropin-ui will collect a billing postcode anyway, for its' own security checks.
Before we close our `<form>` element, there are 3 further items to add:
.. code-block:: django
<!-- hidden field for submitting the token back to the server. Name will vary depending on integration-->
<input type="hidden" id="payment_method_nonce" name="payment_method_nonce"></input>
<h4 class="ui dividing header">Payment Details</h4>
<div id="dropin-container"></div>
<input type="submit" id="submit-button" value="Place Order" class="ui button submit" />
</form>
We add a hidden field. This field will contain a token (string of characters) given by braintree which represents the payment method.
Most payment gateways require something like this, although the name of the field will change between backends.
We then add an empty div with the id ``dropin-container``. This will contain the Braintree Dropin UI.
We could manually create the fields (using e.g. Hosted Fields for braintree or Elements for stripe) for payment forms, however
most integrations offer some sort of 'dropin' which are increasingly customisable. For most purposes, this will suffice.
Finally, we add a submit button.
The Javascript
***************
OK, so now we have hidden elements, empty containers....we need to get this stuff populated!
Each payment gateway integration provides the necessary javascript libraries to interact with the gateway.
They are made available via a template tag.
Add them like this:
.. code-block:: django
<!--Load any client javascript provided by the payment gateway.
I have chosen braintree as my gateway so the template tag below
should give me a list of script tags which load the braintree
SDK's
-->
{% gateway_client_js as scripts %}
{% for js in scripts %}
{{ js|safe }}
{% endfor %}
<!--Finally add the media from the checkout form.-->
{{ checkout_form.media }}
The checkout form also provides a little javascript to initialise shipping options (when the user selects a shipping country).
Finally, we need to add a little of our own javascript to create the braintree dropin:
.. code-block:: django
<script type="text/javascript">
//Initialize shipping options - this function is from the
//checkout form media.
initShippingOption('{% longclaw_api_url_prefix %}');
// Initialize the braintree dropin.
// The gateway token below is taken from the template tag provided by
// longclaw. This is calculated depending on the chosen
// PAYMENT_GATEWAY in the user settings.py
var button = document.querySelector('#submit-button');
braintree.dropin.create({
authorization: "{% gateway_token %}",
container: '#dropin-container'
}, function (createErr, instance) {
button.addEventListener('click', function (event) {
event.preventDefault();
if (instance){
instance.requestPaymentMethod(function (err, payload) {
// Submit payload.nonce to your server
if (err) {
// TODO: Handle this error
console.log(err);
}
else {
$('#payment_method_nonce').val(payload.nonce);
document.getElementById("checkout-form").submit();
}
});
}
});
});
</script>
Two things are happening in the above code. First, we initialise the shipping options. Note we are using a template tag
to pass the longclaw API url prefix, since this is customisable in your settings.py
Secondly, we initialise the braintree dropin. Again, we use a template tag to get a token for the gateway.
All payment backends provide the ``gateway_token`` template tag, although it is not always necessary.
You may wish to only show the braintree payment form if the user has anything in their basket. In which case you might qualify
the above javascript with ``{% if basket.count > 0 %}`` in your template.
As you can see, setting up the checkout is one of the most involved aspects of creating your store. We have worked to simplify this
for v0.2, but welcome any suggestions on how to make it easier!
If you wish to forego the templatetags & forms (e.g. if making a fully React-based frontend), read on. Otherwise, that is the end of the tutorial!
Javascript-Only integration
----------------------------
Below is a walkthrough of integrating a payment gateway (PayPal) without the aid of templatetags etc..
There is a fair amount of work to do to setup the front end when using any payment gateway. Paypal
Express minimises this for us by taking charge of collecting and tokenizing payment data, although we
must still configure it.

Wyświetl plik

@ -20,8 +20,7 @@ Install Longclaw into it:
(my_project) $ pip install longclaw
We also need to install the client library for our payment gateway integration. We are going to
use Paypal as our payment gateway in this walkthrough. To make things easy, we will use Paypal
Express Checkout. For this we can use the Braintree SDK:
use Braintree as our payment gateway in this walkthrough.
.. code-block:: bash
@ -54,12 +53,19 @@ The ``products`` folder contains a skeleton model for our product `variants` whi
Before proceeding, we need to setup our ``settings`` file, in ``my_shop/settings/base.py``.
We need to configure which payment gateway we are using. Change the entry for ``PAYMENT_GATEWAY`` from
``'longclaw.longclawcheckout.gateways.BasePayment'`` to ``'longclaw.longclawcheckout.gateways.PaypalVZero'``
``'longclaw.longclawcheckout.gateways.BasePayment'`` to ``'longclaw.longclawcheckout.gateways.braintree.BraintreePayment'``
We also need to set the access token. The setting for this is ``VZERO_ACCESS_TOKEN``. Paypal access tokens
are termed something like ``access_token$sandbox`` followed by a sequence of characters. As we have different
access tokens for sandbox and live accounts, we will set ``VZERO_ACCESS_TOKEN`` in ``my_shop/settings/dev.py``
for the sandbox account and ``my_shop/settings/production.py`` for the live account.
We also need to set the access tokens for the braintree backend. Add the following settings:
.. codeblock:: python
BRAINTREE_SANDBOX = False
BRAINTREE_MERCHANT_ID = os.environ['BRAINTREE_MERCHANT_ID']
BRAINTREE_PUBLIC_KEY = os.environ['BRAINTREE_PUBLIC_KEY']
BRAINTREE_PRIVATE_KEY = os.environ['BRAINTREE_PRIVATE_KEY']
For development/testing, you will probably want to set ``BRAINTREE_SANDBOX`` to ``True``. The above settings assume that
you have set environment variables on your OS with the access tokens.
.. note: Don't forget that Longclaw is a Wagtail project. You may need to configure additional settings
for wagtail.

Wyświetl plik

@ -51,7 +51,7 @@ The ``slug`` field is autogenerated from the ``ref`` and the parent ``Product``
As we are creating a music shop, we are going to add a ``music_format`` field to the model. We will also
remove the ``description`` field as we dont have any real need for it at the moment:
.. code:: python
.. code-block:: python
class ProductVariant(ProductVariantBase):
_MUSIC_FORMAT_CHOICES = (
@ -84,12 +84,25 @@ For a more complete template, take a look at the `demo project <https://github.c
Adding Products to the Basket
-----------------------------
An important detail of the product template is providing the ability to add or remove a product to the basket.
This is done by making AJAX calls to the longclaw API.
Longclaw offers a helpful template tag to create an ``Add To Basket`` button for your products.
In your template, load the longclawbasket tags::
In the product template, we would like to provide a means to select a variant and add it to the basket.
For t-shirts, our variants are going to represent different sizes, so we would like a single ``Add`` button
and a drop down of sizes.
.. code-block:: django
{% load longclawbasket_tags %}
You can now use the tag to render a button for each product variant:
.. code-block:: django
{% add_to_basket_btn variant.id btn_text="Add To Basket" btn_class="btn btn-default" %}
If you wish to create a button manually, you can handle the click event by making an AJAX call to the longclaw API.
Situations where you would prefer this over the tempaltetag might be to support non-button elements, such as
dropdown buttons, or for React-based frontends.
Here is an example with a single button whose 'variant id' will change depending on the selection in a dropdown box.
We can acheive the drop down like this:
.. code-block:: django
@ -127,4 +140,5 @@ We can then write a jquery function to handle the click event:
This is a basic example of integrating with the basket. You will likely need to incorporate more
complex designs such as displaying a count of items in the basket, allowing the user to increase/decrease
quantity and so on. The :ref:`basket API <basket>` allows all such interactions and all front end design decisions such as these are left up to the developer
quantity and so on. The :ref:`basket API <basket>` allows all such interactions and all front end design decisions such as these are left up to the developer.
It is worthwhile looking at the longclaw demo source code to see how e.g. a basket & item count in the page header is implemented.

Wyświetl plik

@ -0,0 +1,18 @@
{% load longclawcore_tags %}
<button id="btn-add-to-basket" class="{{btn_class}}" data-variant-id="{{variant_id}}">
{{btn_text}}
</button>
{% longclaw_vendors_bundle %}
{% longclaw_client_bundle %}
<script type="text/javascript">
var btn = document.getElementById('btn-add-to-basket');
btn.addEventListener("click", function (e) {
longclawclient.basketList.post({
prefix: "{% longclaw_api_url_prefix %}",
data: {
variant_id: e.target.dataset.variantId
}
});
});
</script>

Wyświetl plik

@ -10,3 +10,14 @@ def basket(context):
'''
items, _ = get_basket_items(context["request"])
return items
@register.inclusion_tag('longclawbasket/add_to_basket.html')
def add_to_basket_btn(variant_id, btn_class="btn btn-default", btn_text="Add To Basket"):
'''Button to add an item to the basket
'''
return {
'btn_class': btn_class,
'variant_id': variant_id,
'btn_text': btn_text
}

Wyświetl plik

@ -4,6 +4,7 @@ from django.core.urlresolvers import reverse
from longclaw.tests.utils import LongclawTestCase, BasketItemFactory, ProductVariantFactory
from longclaw.longclawbasket.utils import basket_id
from longclaw.longclawbasket.templatetags import longclawbasket_tags
class BasketTest(LongclawTestCase):
@ -51,6 +52,13 @@ class BasketTest(LongclawTestCase):
self.assertEqual(response.status_code, 400)
def test_add_to_cart_btn(self):
'''Test the add to cart tag responds
'''
result = longclawbasket_tags.add_to_basket_btn(1)
self.assertIsNotNone(result)
class BasketModelTest(TestCase):
def setUp(self):

Wyświetl plik

@ -1,3 +1 @@
from django.db import models
# Create your models here.

Wyświetl plik

@ -1,3 +1,27 @@
import os
from django.test import TestCase
from django.contrib.staticfiles import finders
# Create your tests here.
from longclaw import settings
from longclaw.longclawcore.templatetags import longclawcore_tags
class TagTests(TestCase):
def _test_static_file(self, pth):
result = finders.find(pth)
print(result)
self.assertTrue(result)
def test_vendors_bundle(self):
ctx = longclawcore_tags.longclaw_vendors_bundle()
self._test_static_file(ctx['path'])
def test_client_bundle(self):
ctx = longclawcore_tags.longclaw_client_bundle()
self._test_static_file(ctx['path'])
def test_api_url_prefix(self):
self.assertEqual(
settings.API_URL_PREFIX,
longclawcore_tags.longclaw_api_url_prefix()
)

Wyświetl plik

@ -45,6 +45,7 @@ INSTALLED_APPS = [
'rest_framework',
'django_extensions',
'longclaw.longclawcore',
'longclaw.longclawsettings',
'longclaw.longclawshipping',
'longclaw.longclawproducts',

13
tox.ini
Wyświetl plik

@ -1,20 +1,23 @@
[tox]
envlist =
{py27,py33,py34,py35}-django-18
{py27,py34,py35}-django-19
{py27,py34,py35}-django-110
{py27,py33,py34,py35,py36}-django-18
{py27,py34,py35,py36}-django-19
{py27,py34,py35,py36}-django-110
{py27,py34,py35,py36}-django-111
[testenv]
setenv =
PYTHONPATH = {toxinidir}:{toxinidir}/longclaw
commands = coverage run --source longclaw runtests.py
coverage xml --omit=*/apps.py,*/migrations/*,*/__init__.py,*/gateways/braintree.py,*/gateways/stripe.py
coverage xml --omit=*/apps.py,*/migrations/*,*/__init__.py,*/gateways/braintree.py,*/gateways/stripe.py,*/bin/longclaw.py
deps =
django-18: Django>=1.8,<1.9
django-19: Django>=1.9,<1.10
django-110: Django>=1.10
django-110: Django>=1.10,<1.11
django-111: Django>=1.11
-r{toxinidir}/requirements_test.txt
basepython =
py36: python3.6
py35: python3.5
py34: python3.4
py33: python3.3