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ęć

3
.gitignore vendored
Wyświetl plik

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

Wyświetl plik

@ -5,7 +5,7 @@ language: python
python:
- "3.5"
env:
env:
- TOX_ENV=py35-django-18
- TOX_ENV=py34-django-18
- TOX_ENV=py33-django-18
@ -13,12 +13,14 @@ 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
# command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors
install:
install:
- . $HOME/.nvm/nvm.sh
- nvm install stable
- nvm use stable

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

@ -3,18 +3,18 @@
Checkout
========
Longclaw provides a simple, single checkout view.
Longclaw provides a simple, single checkout view.
The URL for the checkout is ``'checkout/'``.
After a successful checkout, it is redirected to ``checkout/success/``.
To implement the checkout, simply provide ``'longclawcheckout/checkout.html'`` and
``'longclawcheckout/success.html'`` templates. (Empty templates will have been created if
``'longclawcheckout/success.html'`` templates. (Empty templates will have been created if
you ran the longclaw CLI to start your project)
There are three forms provided in the checkout view:
:checkout_form:
Captures the email address and optionally the shipping option for the checkout.
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
:shipping_form:
@ -24,13 +24,13 @@ There are three forms provided in the checkout view:
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
Generally, you may need to use a little javascript to optionally render the form if the user selects
'different billing address'.
Shipping Options and Javascript
--------------------------------
The shipping option dropdown has no options by default - this is because it is dependent on the shipping country.
The shipping option dropdown has no options by default - this is because it is dependent on the shipping country.
The checkout form includes the necessary javascript to do this - you just need to include it on the page.
You will typically also need to include your chosen payment gateways' client javascript:
@ -46,7 +46,7 @@ You will typically also need to include your chosen payment gateways' client jav
<script type="text/javascript">
initShippingOption('{% longclaw_api_url_prefix %}');
</script>
The first half uses the ``gateway_client_js`` template tag to load all the payment gateway javascript. There may be one or more.
The second half has three parts to it:
@ -68,8 +68,10 @@ It is up to you to render a payment form and then pass the token in the POST dat
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.
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).
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,16 +34,182 @@ 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.
must still configure it.
The basic client payment flow with Braintree is as follows:
1. The client requests a braintree token. Longclaw provides an API endpoint to generate tokens using the braintree SDK
2. The client gathers payment details and turns this into a `payment method nonce` by interacting with the braintree server.
Paypal Express Checkout will take care of this entirely.
3. The client submits the `payment method nonce` to the server to capture the payment. Longclaw provides an API endpoint for all payment captures.
3. The client submits the `payment method nonce` to the server to capture the payment. Longclaw provides an API endpoint for all payment captures.
We therefore have three things we need to do in our client-side javascript:
@ -51,9 +224,9 @@ We therefore have three things we need to do in our client-side javascript:
}
})
2. Following this, configure the paypal express checkout functionality. This actually has two steps.
We must first create a braintree `client` using our new token. We then use this to create a braintree
`paypal` instance.
2. Following this, configure the paypal express checkout functionality. This actually has two steps.
We must first create a braintree `client` using our new token. We then use this to create a braintree
`paypal` instance.
.. code-block:: javascript
@ -70,20 +243,20 @@ We therefore have three things we need to do in our client-side javascript:
if (err) {
console.log("handle error creating paypal");
return;
}
}
console.log("Paypal instance": paypalInstance);
});
});
});
3. Once paypal has created the `nonce` for the entered payment details, we must submit this
to our server so longclaw can capture the payment.
3. Once paypal has created the `nonce` for the entered payment details, we must submit this
to our server so longclaw can capture the payment.
To do this, we must have a button which we want to use to launch the paypal express checkout window.
We 'attach' the paypal instance we just created to the button like so:
.. code-block:: javascript
paypalButton.addEventListener(
'click',
'click',
function (){
paypalInstance.tokenize({
flow: 'checkout',
@ -161,7 +334,7 @@ We can make all these nested API calls simpler if we use ES6 Promises and the fe
}
// This is where we actually setup paypal
export function setupBraintreePaypal(totalAmount,
export function setupBraintreePaypal(totalAmount,
paypalButton,
shippingAddress,
shippingRate,
@ -173,7 +346,7 @@ We can make all these nested API calls simpler if we use ES6 Promises and the fe
return getToken()
.then(data => braintreeClientCreate(data.token))
.then(client => braintreePaypalCreate(client))
.then(paypalInstance => paypalButton.addEventListener('click',
.then(paypalInstance => paypalButton.addEventListener('click',
function (){
paypalInstance.tokenize({
flow: 'checkout',
@ -206,7 +379,7 @@ We can make all these nested API calls simpler if we use ES6 Promises and the fe
let contentType = 'application/json';
const headers = {
Accept: 'application/json, application/json, application/coreapi+json',
};
if (!form) headers['Content-Type'] = contentType;
const csrf = JsCookie.get('csrftoken');
@ -239,4 +412,4 @@ We can make all these nested API calls simpler if we use ES6 Promises and the fe
}
The total amount, shipping address, shipping rate and email address of the customer are passed into the setup function;
it is up to the front end developer to create the necessary forms to gather these.
it is up to the front end developer to create the necessary forms to gather these.

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
@ -48,18 +47,25 @@ Now we have a django project which looks like this::
requirements.txt
The ``home`` and ``search`` folders are default folders used in Wagtail projects. Users of Wagtail
will be familiar with these.
The ``products`` folder contains a skeleton model for our product `variants` which we will come to later.
will be familiar with these.
The ``products`` folder contains a skeleton model for our product `variants` which we will come to later.
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

@ -6,7 +6,7 @@ Managing the Catalogue
Creating the Product Index
--------------------------
Wagtails' ``Page`` models are organized in a tree structure. All our ``Product`` pages will therefore
need a parent. This is provided by the ``ProductIndex`` model.
need a parent. This is provided by the ``ProductIndex`` model.
.. note::
Read more about Wagtail pages in the `Wagtail docs <http://docs.wagtail.io/en/v1.9/topics/pages.html>`_
@ -25,7 +25,7 @@ We can now add ``Product`` models as children of ``ProductIndex``. Only pages of
Adding a Product
----------------
Under the explorer homepage, we should now see our newly created ``ProductIndex``. We can select ``Add child page`` to add our first
Under the explorer homepage, we should now see our newly created ``ProductIndex``. We can select ``Add child page`` to add our first
``Product``. The ``Product`` model is fairly minimal. It has:
- A title
@ -40,10 +40,10 @@ Customising Variants
--------------------
The ``ProductVariant`` model is where we can customise the attributes of our model. Running ``longclaw start``
provided a ``products`` with a minimal implementation of a custom ``ProductVariant`` model.
provided a ``products`` with a minimal implementation of a custom ``ProductVariant`` model.
We can further customise this now by opening ``my_shop/products/models.py`` in a text editor.
``ProductVariant`` inherits from ``ProductVariantBase`` which provides the ``price``, ``ref`` and ``slug`` fields.
``ProductVariant`` inherits from ``ProductVariantBase`` which provides the ``price``, ``ref`` and ``slug`` fields.
The ``ref`` field is intended to be used as a short description or sub-title to help distinguish a particular variant.
The ``slug`` field is autogenerated from the ``ref`` and the parent ``Product`` title.
@ -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 = (
@ -59,7 +59,7 @@ remove the ``description`` field as we dont have any real need for it at the mom
(2, 'Vinyl'),
)
music_format = models.IntegerField(max_length=3, choices=_MUSIC_FORMAT_CHOICES)
music_format = models.IntegerField(max_length=3, choices=_MUSIC_FORMAT_CHOICES)
After making and running migrations, we can now select the format for each variant:
@ -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