├── .gitignore
├── LICENSE
├── README.md
├── __init__.py
├── admin.py
├── commands.py
├── fixtures.py
├── functions.py
├── info.py
├── main.py
├── models.py
├── pipelines
├── __init__.py
├── base.py
└── shipping.py
├── processors
├── __init__.py
├── base.py
└── pagseguro_processor.py
├── tasks.py
├── template_filters.py
├── templates
└── cart
│ ├── cart.html
│ ├── cart
│ └── complete_information.html
│ ├── cart_base.html
│ ├── confirmation.html
│ ├── dummy.html
│ ├── empty_cart.html
│ ├── history.html
│ ├── pipeline_error.html
│ ├── simple_confirmation.html
│ └── test.html
└── views.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 |
3 | # C extensions
4 | *.so
5 |
6 | # Packages
7 | *.egg
8 | *.egg-info
9 | dist
10 | build
11 | eggs
12 | parts
13 | bin
14 | var
15 | sdist
16 | develop-eggs
17 | .installed.cfg
18 | lib
19 | lib64
20 | __pycache__
21 |
22 | # Installer logs
23 | pip-log.txt
24 |
25 | # Unit test / coverage reports
26 | .coverage
27 | .tox
28 | nosetests.xml
29 |
30 | # Translations
31 | *.mo
32 |
33 | # Mr Developer
34 | .mr.developer.cfg
35 | .project
36 | .pydevproject
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013 pythonic open source network
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Quokka Cart | Version 0.1.0dev
2 |
3 | A Shopping cart for [Quokka CMS](http://www.quokkaproject.org)
4 |
5 |
6 |
7 |
8 |
9 |
10 | Features
11 | =============
12 |
13 | ### What Quokka-cart does:
14 |
15 | - Generic shopping cart management (manages itenms, prices, quantities)
16 | - Executes **pipelines** (functions to dinamycally configure the checkout)
17 | - Executes a decoupled **processor** for checkout
18 | - Expose urls for checkout, history and receive carrier notifications
19 | - Expose simple API to manage the cart (/additem, /setitem, /removeitem etc..)
20 |
21 | ### What Quokka-cart does not:
22 |
23 | - A complete e-commerce solution (for that take a look at Quokka-commerce which uses quokka-cart)
24 |
25 |
26 | How to install
27 | ===============
28 |
29 | Go to your quokka modules folder and clone it.
30 |
31 | ```bash
32 | $ cd quokka/quokka/modules
33 | $ git clone https://github.com/pythonhub/quokka-cart.git cart
34 | $ ls
35 | __init__.py accounts media posts cart ...
36 | ```
37 |
38 | Now you have **cart** folder as a Quokka module, restart your server and it will be available in yout application and in admin under "Cart" menu.
39 |
40 |
41 | Product
42 | =======
43 |
44 | Cart
45 | ====
46 |
47 | Checkout Pipeline
48 | ========
49 |
50 | Processor
51 | =========
52 |
53 |
54 | - http://github.com/pythonhub/quokka-cart
55 | - by Bruno Rocha
56 |
57 |
58 | [](https://bitdeli.com/free "Bitdeli Badge")
59 |
60 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quokkaproject/quokka-cart/d119e98c05c688e4da24861ceef6b2c57d59cc6f/__init__.py
--------------------------------------------------------------------------------
/admin.py:
--------------------------------------------------------------------------------
1 | # coding : utf -8
2 | # from flask.ext.htmlbuilder import html
3 | # from flask.ext.admin.babel import lazy_gettext
4 | from quokka import admin
5 | from quokka.modules.posts.admin import PostAdmin
6 | from quokka.core.admin.models import ModelAdmin
7 | from quokka.utils.translation import _, _l
8 | from quokka.core.widgets import TextEditor, PrepopulatedText
9 | from .models import Cart, Processor
10 |
11 |
12 | class ProductAdmin(PostAdmin):
13 | column_list = ('title', 'slug', 'channel',
14 | 'unity_value', 'weight',
15 | 'published', 'created_at',
16 | 'available_at', 'view_on_site')
17 | column_searchable_list = ['title', 'summary', 'description']
18 | form_columns = [
19 | 'title', 'slug', 'channel', 'related_channels', 'summary',
20 | 'description', 'unity_value', 'weight', 'dimensions', 'extra_value',
21 | 'published', 'show_on_channel',
22 | 'available_at', 'available_until',
23 | 'tags', 'contents', 'values', 'template_type'
24 | ]
25 | form_widget_args = {
26 | 'description': {
27 | 'rows': 20,
28 | 'cols': 20,
29 | 'class': 'text_editor',
30 | 'style': "margin: 0px; width: 725px; height: 360px;"
31 | },
32 | 'summary': {
33 | 'style': 'width: 400px; height: 100px;'
34 | },
35 | 'title': {'style': 'width: 400px'},
36 | 'slug': {'style': 'width: 400px'},
37 | }
38 |
39 |
40 | class CartAdmin(ModelAdmin):
41 | roles_accepted = ('admin', 'editor')
42 | column_filters = ('status', 'created_at', 'total', 'tax',
43 | 'reference_code', 'transaction_code')
44 | column_searchable_list = ('transaction_code', 'checkout_code',
45 | 'reference_code', 'search_helper')
46 | column_list = ("belongs_to", 'total', 'tax', 'status', 'created_at',
47 | 'processor',
48 | "reference_code", 'items', 'published')
49 | form_columns = ('created_at', 'belongs_to', 'processor', 'status',
50 | 'total', 'extra_costs', 'reference_code', 'checkout_code',
51 | 'sender_data', 'shipping_data', 'tax', 'shipping_cost',
52 | 'transaction_code',
53 | # 'requires_login',
54 | # 'continue_shopping_url', 'pipeline', 'config',
55 | # 'items',
56 | 'payment', 'published')
57 |
58 | form_subdocuments = {
59 | 'items': {
60 | 'form_subdocuments': {
61 | None: {
62 | 'form_columns': ['uid', 'title', 'description',
63 | 'link', 'quantity', 'unity_value',
64 | 'total_value', 'weight', 'dimensions',
65 | 'extra_value']
66 | }
67 | }
68 | }
69 | }
70 |
71 | column_formatters = {
72 | 'created_at': ModelAdmin.formatters.get('datetime'),
73 | 'available_at': ModelAdmin.formatters.get('datetime'),
74 | 'items': ModelAdmin.formatters.get('ul'),
75 | 'status': ModelAdmin.formatters.get('status'),
76 | 'reference_code': ModelAdmin.formatters.get('get_url')
77 | }
78 |
79 | column_formatters_args = {
80 | 'ul': {
81 | 'items': {
82 | 'placeholder': u"{item.title} - {item.total_value}",
83 | 'style': "min-width:200px;max-width:300px;"
84 | }
85 | },
86 | 'status': {
87 | 'status': {
88 | 'labels': {
89 | 'confirmed': 'success',
90 | 'checked_out': 'warning',
91 | 'cancelled': 'important',
92 | 'completed': 'success'
93 | },
94 | 'style': 'min-height:18px;'
95 | }
96 | },
97 | 'get_url': {
98 | 'reference_code': {
99 | 'attribute': 'reference',
100 | 'method': 'get_admin_url'
101 | }
102 | }
103 | }
104 |
105 | def after_model_change(self, form, model, is_created):
106 | if not is_created and model.reference:
107 | model.reference.published = model.published
108 | if model.tax:
109 | model.set_reference_tax(float(model.tax))
110 | model.reference.save()
111 |
112 |
113 | class ProcessorAdmin(ModelAdmin):
114 | roles_accepted = ('admin', 'developer')
115 | column_list = ('identifier', 'title', 'module', 'published')
116 | form_args = {
117 | "description": {"widget": TextEditor()},
118 | "identifier": {"widget": PrepopulatedText(master='title')}
119 | }
120 | form_columns = ('title', 'identifier', 'description', 'module',
121 | 'requires', 'image', 'link', 'config', 'pipeline',
122 | 'published')
123 | form_ajax_refs = {
124 | 'image': {'fields': ['title', 'long_slug', 'summary']}
125 | }
126 |
127 | form_widget_args = {
128 | 'config': {'cols': 40, 'rows': 10, 'style': 'width:500px;'}
129 | }
130 |
131 | admin.register(Cart, CartAdmin, category=_("Cart"), name=_l("Cart"))
132 | admin.register(Processor, ProcessorAdmin, category=_("Cart"),
133 | name=_l("Processor"))
134 |
--------------------------------------------------------------------------------
/commands.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | import logging
4 |
5 | from flask.ext.script import Command, Option
6 | from .models import Cart
7 |
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 |
12 | class ListCart(Command):
13 | "prints a list of carts"
14 |
15 | command_name = 'list_carts'
16 |
17 | option_list = (
18 | Option('--title', '-t', dest='title'),
19 | )
20 |
21 | def run(self, title=None):
22 |
23 | carts = Cart.objects
24 | if title:
25 | carts = carts(title=title)
26 |
27 | for cart in carts:
28 | logger.info('Cart: {}'.format(cart))
29 |
--------------------------------------------------------------------------------
/fixtures.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
--------------------------------------------------------------------------------
/functions.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | from flask import session
4 | from .models import Cart
5 |
6 |
7 | def get_current_cart(*args, **kwargs):
8 | if session.get('cart_id'):
9 | return Cart.get_cart(*args, **kwargs)
10 |
--------------------------------------------------------------------------------
/info.py:
--------------------------------------------------------------------------------
1 | author = "Bruno Rocha "
2 | link = "http://github.com/pythonhub/quokka-cart"
3 | version = "0.1.0"
4 | image = ""
5 | requirements_apt = []
6 | requirements = ["pagseguro"]
7 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | from quokka.core.app import QuokkaModule
4 | from .views import CartView, SetItemView, RemoveItemView, SetProcessorView, \
5 | CheckoutView, HistoryView, ConfirmationView, NotificationView
6 | from .functions import get_current_cart
7 |
8 | module = QuokkaModule("cart", __name__,
9 | template_folder="templates", static_folder="static")
10 |
11 | # template globals
12 | module.add_app_template_global(get_current_cart)
13 |
14 |
15 | # urls
16 | module.add_url_rule('/cart/', view_func=CartView.as_view('cart'))
17 | module.add_url_rule('/cart/setitem/', view_func=SetItemView.as_view('setitem'))
18 | module.add_url_rule('/cart/removeitem/',
19 | view_func=RemoveItemView.as_view('removeitem'))
20 | module.add_url_rule('/cart/setprocessor/',
21 | view_func=SetProcessorView.as_view('setprocessor'))
22 | module.add_url_rule('/cart/checkout/',
23 | view_func=CheckoutView.as_view('checkout'))
24 | module.add_url_rule('/cart/history/', view_func=HistoryView.as_view('history'))
25 | module.add_url_rule('/cart/confirmation//',
26 | view_func=ConfirmationView.as_view('confirmation'))
27 | module.add_url_rule('/cart/notification//',
28 | view_func=NotificationView.as_view('notification'))
29 |
30 | """
31 | Every url accepts ajax requests, and so do not redirect anything.
32 | in ajax request it will return JSON as response
33 | /cart
34 | - if there is a cart, but it is checked_out, create a new one (if has item)
35 | - show a link to cart history if any
36 | - show the cart to the user
37 | - context is "cart"
38 | - renders cart/cart.html template
39 | - list items and has form for quantity and extra info
40 | - different things can be done via api ex: config shipping
41 | /cart/setitem
42 | - receives a POST with item information
43 | - if "uid" is present ans item exists it will be updated else created
44 | - "product" reference is passed as an "id" and converted to a reference
45 | - receive quantity, weight etc..
46 | - if "next" is present redirect to there else redirect to "/cart"
47 | /cart/removeitem
48 | - receives a POST with item_id or product_id
49 | - use 'next' to redirect or '/cart'
50 | /cart/setprocessor
51 | - receives a POST with processor identifier or id
52 | /cart/setstatus
53 | - user can set only from abandoned to pending
54 | - if there is a current 'pending' cart it will be set to 'abandoned'
55 | - admin can set to any status
56 | /cart/configure
57 | - requires logged in user as cart owner
58 | - receive a POST
59 | - "property" can be shipping_data, sender_data, extra_costs, shipping_cost
60 | /cart/checkout
61 | - if cart.requires_login:
62 | if user is not logged in it will redirect user to login page
63 | - Do the checkout process if the cart is "pending"
64 | else clean cart from session and redirect to history
65 | - if the processor has a pipeline in record or in class it will executed
66 | - record: pipeline
67 | - class: pre_pipeline, pos_pipeline
68 | - every method in pipeline must act like a view
69 | and it will have self.cart, self.config and flask request and session
70 | to deal
71 | - they should return a redirect, a rendered template, or self.continue()
72 | which will continue to the next method in the pipeline and set state
73 | to jump to a specific method in pipeline you can do continue('method')
74 | - if None is returned or pipeline is empty/iterated,
75 | so the processor.process() method will be executed
76 | /cart/history
77 | - show the cart history for the current user
78 | - context is carts
79 | - user must be logged in
80 | """
81 |
--------------------------------------------------------------------------------
/models.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | import datetime
4 | import logging
5 | import sys
6 |
7 | from werkzeug.utils import import_string
8 | from flask import session, current_app
9 |
10 | from quokka.utils.translation import _l
11 | from quokka.utils import get_current_user, lazy_str_setting
12 | from quokka.core.templates import render_template
13 | from quokka.core.db import db
14 | from quokka.core.models.signature import (
15 | Publishable, Ordered, Dated
16 | )
17 | from quokka.core.models.content import Content
18 | from quokka.modules.media.models import Image
19 |
20 |
21 | if sys.version_info.major == 3:
22 | from functools import reduce
23 |
24 | logger = logging.getLogger()
25 |
26 |
27 | class BaseProductReference(object):
28 | def get_title(self):
29 | return getattr(self, 'title', None)
30 |
31 | def get_description(self):
32 | return getattr(self, 'description', None)
33 |
34 | def get_unity_value(self):
35 | return getattr(self, 'unity_value', None)
36 |
37 | def get_weight(self):
38 | return getattr(self, 'weight', None)
39 |
40 | def get_dimensions(self):
41 | return getattr(self, 'dimensions', None)
42 |
43 | def get_summary(self):
44 | summary = getattr(self, 'summary', None)
45 | if not summary:
46 | try:
47 | return self.get_description()[:255]
48 | except:
49 | pass
50 | return summary
51 |
52 | def get_extra_value(self):
53 | return getattr(self, 'extra_value', None)
54 |
55 | def get_uid(self):
56 | return str(self.id)
57 |
58 | def set_status(self, *args, **kwargs):
59 | pass
60 |
61 | def remove_item(self, *args, **kwargs):
62 | pass
63 |
64 |
65 | class BaseProduct(BaseProductReference, Content):
66 | description = db.StringField(required=True)
67 | unity_value = db.FloatField()
68 | weight = db.FloatField()
69 | dimensions = db.StringField()
70 | extra_value = db.FloatField()
71 |
72 | meta = {
73 | 'allow_inheritance': True
74 | }
75 |
76 |
77 | class Item(Ordered, Dated, db.EmbeddedDocument):
78 | product = db.ReferenceField(Content)
79 | reference = db.GenericReferenceField() # customized product
80 | """
81 | Must implement all the BaseProduct methods/ its optional
82 | if None, "product" will be considered
83 | """
84 | uid = db.StringField()
85 | title = db.StringField(required=True, max_length=255)
86 | description = db.StringField(required=True)
87 | link = db.StringField()
88 | quantity = db.FloatField(default=1)
89 | unity_value = db.FloatField(required=True)
90 | total_value = db.FloatField()
91 | weight = db.FloatField()
92 | dimensions = db.StringField()
93 | extra_value = db.FloatField()
94 | allowed_to_set = db.ListField(db.StringField(), default=['quantity'])
95 | pipeline = db.ListField(db.StringField(), default=[])
96 |
97 | def set_status(self, status, *args, **kwargs):
98 | kwargs['item'] = self
99 | if self.reference and hasattr(self.reference, 'set_status'):
100 | self.reference.set_status(status, *args, **kwargs)
101 | if self.product and hasattr(self.product, 'set_status'):
102 | self.product.set_status(status, *args, **kwargs)
103 |
104 | def get_main_image_url(self, thumb=False, default=None):
105 | try:
106 | return self.product.get_main_image_url(thumb, default)
107 | except:
108 | return None
109 |
110 | @classmethod
111 | def normalize(cls, kwargs):
112 | new = {}
113 | for k, v in kwargs.items():
114 | field = cls._fields.get(k)
115 | if not field:
116 | continue
117 | new[k] = field.to_python(v)
118 | return new
119 |
120 | def __unicode__(self):
121 | return u"{i.title} - {i.total_value}".format(i=self)
122 |
123 | def get_uid(self):
124 | try:
125 | return self.product.get_uid()
126 | except:
127 | return self.uid
128 |
129 | @property
130 | def unity_plus_extra(self):
131 | return float(self.unity_value or 0) + float(self.extra_value or 0)
132 |
133 | @property
134 | def total(self):
135 | self.clean()
136 | self.total_value = self.unity_plus_extra * float(self.quantity or 1)
137 | return self.total_value
138 |
139 | def clean(self):
140 | mapping = [
141 | ('title', 'get_title'),
142 | ('description', 'get_description'),
143 | ('link', 'get_absolute_url'),
144 | ('unity_value', 'get_unity_value'),
145 | ('weight', 'get_weight'),
146 | ('dimensions', 'get_dimensions'),
147 | ('extra_value', 'get_extra_value'),
148 | ('uid', 'get_uid'),
149 | ]
150 |
151 | references = [self.reference, self.product]
152 |
153 | for ref in references:
154 | if not ref:
155 | continue
156 | for attr, method in mapping:
157 | current = getattr(self, attr, None)
158 | if current is not None:
159 | continue
160 | setattr(self, attr, getattr(ref, method, lambda: None)())
161 |
162 |
163 | class Payment(db.EmbeddedDocument):
164 | uid = db.StringField()
165 | payment_system = db.StringField()
166 | method = db.StringField()
167 | value = db.FloatField()
168 | extra_value = db.FloatField()
169 | date = db.DateTimeField()
170 | confirmed_at = db.DateTimeField()
171 | status = db.StringField()
172 |
173 |
174 | class Processor(Publishable, db.DynamicDocument):
175 | identifier = db.StringField(max_length=100, unique=True)
176 | module = db.StringField(max_length=255)
177 | requires = db.ListField(db.StringField(max_length=255))
178 | description = db.StringField()
179 | title = db.StringField()
180 | image = db.ReferenceField(Image, reverse_delete_rule=db.NULLIFY)
181 | link = db.StringField(max_length=255)
182 | config = db.DictField(default=lambda: {})
183 | pipeline = db.ListField(db.StringField(max_length=255), default=[])
184 |
185 | def import_processor(self):
186 | return import_string(self.module)
187 |
188 | def get_instance(self, *args, **kwargs):
189 | if 'config' not in kwargs:
190 | kwargs['config'] = self.config
191 | kwargs['_record'] = self
192 | return self.import_processor()(*args, **kwargs)
193 |
194 | def clean(self, *args, **kwargs):
195 | for item in (self.requires or []):
196 | import_string(item)
197 | super(Processor, self).clean(*args, **kwargs)
198 |
199 | def __unicode__(self):
200 | return self.identifier
201 |
202 | @classmethod
203 | def get_instance_by_identifier(cls, identifier, cart=None):
204 | processor = cls.objects.get(identifier=identifier)
205 | return processor.get_instance(cart=cart)
206 |
207 | @classmethod
208 | def get_default_processor(cls):
209 | default = lazy_str_setting(
210 | 'CART_DEFAULT_PROCESSOR',
211 | default={
212 | 'module': 'quokka.modules.cart.processors.Dummy',
213 | 'identifier': 'dummy',
214 | 'published': True,
215 | 'title': "Test"
216 | }
217 | )
218 |
219 | try:
220 | return cls.objects.get(identifier=default['identifier'])
221 | except:
222 | return cls.objects.create(**default)
223 |
224 | def save(self, *args, **kwargs):
225 | self.import_processor()
226 | super(Processor, self).save(*args, **kwargs)
227 |
228 |
229 | class Cart(Publishable, db.DynamicDocument):
230 | STATUS = (
231 | ("pending", _l("Pending")), # not checked out
232 | ("checked_out", _l("Checked out")), # not confirmed (payment)
233 | ("analysing", _l("Analysing")), # Analysing payment
234 | ("confirmed", _l("Confirmed")), # Payment confirmed
235 | ("completed", _l("Completed")), # Payment completed (money released)
236 | ("refunding", _l("Refunding")), # Buyer asks refund
237 | ("refunded", _l("Refunded")), # Money refunded to buyer
238 | ("cancelled", _l("Cancelled")), # Cancelled without processing
239 | ("abandoned", _l("Abandoned")), # Long time no update
240 | )
241 | reference = db.GenericReferenceField()
242 | """reference must implement set_status(**kwargs) method
243 | arguments: status(str), value(float), date, uid(str), msg(str)
244 | and extra(dict).
245 | Also reference must implement get_uid() which will return
246 | the unique identifier for this transaction"""
247 |
248 | belongs_to = db.ReferenceField('User',
249 | # default=get_current_user,
250 | reverse_delete_rule=db.NULLIFY)
251 | items = db.ListField(db.EmbeddedDocumentField(Item))
252 | payment = db.ListField(db.EmbeddedDocumentField(Payment))
253 | status = db.StringField(choices=STATUS, default='pending')
254 | total = db.FloatField(default=0)
255 | extra_costs = db.DictField(default=lambda: {})
256 | sender_data = db.DictField(default=lambda: {})
257 | shipping_data = db.DictField(default=lambda: {})
258 | shipping_cost = db.FloatField(default=0)
259 | tax = db.FloatField(default=0)
260 | processor = db.ReferenceField(Processor,
261 | default=Processor.get_default_processor,
262 | reverse_delete_rule=db.NULLIFY)
263 | reference_code = db.StringField() # Reference code for filtering
264 | checkout_code = db.StringField() # The UID for transaction checkout
265 | transaction_code = db.StringField() # The UID for transaction
266 | requires_login = db.BooleanField(default=True)
267 | continue_shopping_url = db.StringField(
268 | default=lambda: current_app.config.get(
269 | 'CART_CONTINUE_SHOPPING_URL', '/'
270 | )
271 | )
272 | pipeline = db.ListField(db.StringField(), default=[])
273 | log = db.ListField(db.StringField(), default=[])
274 | config = db.DictField(default=lambda: {})
275 |
276 | search_helper = db.StringField()
277 |
278 | meta = {
279 | 'ordering': ['-created_at']
280 | }
281 |
282 | def send_response(self, response, identifier):
283 | if self.reference and hasattr(self.reference, 'get_response'):
284 | self.reference.get_response(response, identifier)
285 |
286 | for item in self.items:
287 | if hasattr(item, 'get_response'):
288 | item.get_response(response, identifier)
289 |
290 | def set_tax(self, tax, save=False):
291 | """
292 | set tax and send to references
293 | """
294 | try:
295 | tax = float(tax)
296 | self.tax = tax
297 | self.set_reference_tax(tax)
298 | except Exception as e:
299 | self.addlog("impossible to set tax: %s" % str(e))
300 |
301 | def set_status(self, status, save=False):
302 | """
303 | THis method will be called by the processor
304 | which will pass a valid status as in STATUS
305 | so, this method will dispatch the STATUS to
306 | all the items and also the 'reference' if set
307 | """
308 | if self.status != status:
309 | self.status = status
310 |
311 | self.set_reference_statuses(status)
312 |
313 | if save:
314 | self.save()
315 |
316 | def set_reference_statuses(self, status):
317 | if self.reference and hasattr(self.reference, 'set_status'):
318 | self.reference.set_status(status, cart=self)
319 |
320 | for item in self.items:
321 | item.set_status(status, cart=self)
322 |
323 | def set_reference_tax(self, tax):
324 | if self.reference and hasattr(self.reference, 'set_tax'):
325 | self.reference.set_tax(tax)
326 |
327 | for item in self.items:
328 | if hasattr(item, 'set_tax'):
329 | item.set_tax(tax)
330 |
331 | def addlog(self, msg, save=True):
332 | try:
333 | self.log.append(u"{0},{1}".format(datetime.datetime.now(), msg))
334 | logger.debug(msg)
335 | save and self.save()
336 | except UnicodeDecodeError as e:
337 | logger.info(msg)
338 | logger.error(str(e))
339 |
340 | @property
341 | def uid(self):
342 | return self.get_uid()
343 |
344 | def get_uid(self):
345 | try:
346 | return self.reference.get_uid() or str(self.id)
347 | except Exception:
348 | self.addlog("Using self.id as reference", save=False)
349 | return str(self.id)
350 |
351 | def __unicode__(self):
352 | return u"{o.uid} - {o.processor.identifier}".format(o=self)
353 |
354 | def get_extra_costs(self):
355 | if self.extra_costs:
356 | return sum(self.extra_costs.values())
357 |
358 | @classmethod
359 | def get_cart(cls, no_dereference=False, save=True):
360 | """
361 | get or create a new cart related to the session
362 | if there is a current logged in user it will be set
363 | else it will be set during the checkout.
364 | """
365 | session.permanent = current_app.config.get(
366 | "CART_PERMANENT_SESSION", True)
367 | try:
368 | cart = cls.objects(id=session.get('cart_id'), status='pending')
369 |
370 | if not cart:
371 | raise cls.DoesNotExist('A pending cart not found')
372 |
373 | if no_dereference:
374 | cart = cart.no_dereference()
375 |
376 | cart = cart.first()
377 |
378 | save and cart.save()
379 |
380 | except (cls.DoesNotExist, db.ValidationError):
381 | cart = cls(status="pending")
382 | cart.save()
383 | session['cart_id'] = str(cart.id)
384 | session.pop('cart_pipeline_index', None)
385 | session.pop('cart_pipeline_args', None)
386 |
387 | return cart
388 |
389 | def assign(self):
390 | self.belongs_to = self.belongs_to or get_current_user()
391 |
392 | def save(self, *args, **kwargs):
393 | self.total = sum([item.total for item in self.items])
394 | self.assign()
395 | self.reference_code = self.get_uid()
396 | self.search_helper = self.get_search_helper()
397 | if not self.id:
398 | self.published = True
399 | super(Cart, self).save(*args, **kwargs)
400 | self.set_reference_statuses(self.status)
401 |
402 | def get_search_helper(self):
403 | if not self.belongs_to:
404 | return ""
405 | user = self.belongs_to
406 | return " ".join([
407 | user.name or "",
408 | user.email or ""
409 | ])
410 |
411 | def get_item(self, uid):
412 | # MongoEngine/mongoengine#503
413 | return self.items.get(uid=uid)
414 |
415 | def set_item(self, **kwargs):
416 | if 'product' in kwargs:
417 | if not isinstance(kwargs['product'], Content):
418 | try:
419 | kwargs['product'] = Content.objects.get(
420 | id=kwargs['product'])
421 | except Content.DoesNotExist:
422 | kwargs['product'] = None
423 |
424 | uid = kwargs.get(
425 | 'uid',
426 | kwargs['product'].get_uid() if kwargs.get('product') else None
427 | )
428 |
429 | if not uid:
430 | self.addlog("Cannot add item without an uid %s" % kwargs)
431 | return
432 |
433 | item = self.get_item(uid)
434 |
435 | kwargs = Item.normalize(kwargs)
436 |
437 | if not item:
438 | # items should only be added if there is a product (for safety)
439 | if not kwargs.get('product'):
440 | self.addlog("there is no product to add item")
441 | return
442 | allowed = ['product', 'quantity']
443 | item = self.items.create(
444 | **{k: v for k, v in kwargs.items() if k in allowed}
445 | )
446 | self.addlog("New item created %s" % item, save=False)
447 | else:
448 | # update only allowed attributes
449 | item = self.items.update(
450 | {k: v for k, v in kwargs.items() if k in item.allowed_to_set},
451 | uid=item.uid
452 | )
453 | self.addlog("Item updated %s" % item, save=False)
454 |
455 | if int(kwargs.get('quantity', "1")) == 0:
456 | self.addlog("quantity is 0, removed %s" % kwargs, save=False)
457 | self.remove_item(**kwargs)
458 |
459 | self.save()
460 | self.reload()
461 | return item
462 |
463 | def remove_item(self, **kwargs):
464 | deleted = self.items.delete(**kwargs)
465 | if self.reference and hasattr(self.reference, 'remove_item'):
466 | self.reference.remove_item(**kwargs)
467 | return deleted
468 |
469 | def checkout(self, processor=None, *args, **kwargs):
470 | self.set_processor(processor)
471 | processor_instance = self.processor.get_instance(self, *args, **kwargs)
472 | if processor_instance.validate():
473 | response = processor_instance.process()
474 | self.status = 'checked_out'
475 | self.save()
476 | session.pop('cart_id', None)
477 | return response
478 | else:
479 | self.addlog("Cart did not validate")
480 | raise Exception("Cart did not validate") # todo: specialize this
481 |
482 | def get_items_pipeline(self):
483 | if not self.items:
484 | return []
485 |
486 | return reduce(
487 | lambda x, y: x + y, [item.pipeline for item in self.items]
488 | )
489 |
490 | def build_pipeline(self):
491 | items = ['quokka.modules.cart.pipelines:StartPipeline']
492 | items.extend(current_app.config.get('CART_PIPELINE', []))
493 | items.extend(self.get_items_pipeline())
494 | items.extend(self.pipeline)
495 | items.extend(self.processor and self.processor.pipeline or [])
496 | return items
497 |
498 | def process_pipeline(self):
499 | if not self.items:
500 | return render_template('cart/empty_cart.html',
501 | url=self.continue_shopping_url)
502 |
503 | pipelines = self.build_pipeline()
504 | index = session.get('cart_pipeline_index', 0)
505 | pipeline = import_string(pipelines[index])
506 | return pipeline(self, pipelines, index)._preprocess()
507 |
508 | def set_processor(self, processor=None):
509 | if not self.processor:
510 | self.processor = Processor.get_default_processor()
511 | self.save()
512 |
513 | if not processor:
514 | return
515 |
516 | if isinstance(processor, Processor):
517 | self.processor = processor
518 | self.save()
519 | return
520 |
521 | try:
522 | self.processor = Processor.objects.get(id=processor)
523 | except:
524 | self.processor = Processor.objects.get(identifier=processor)
525 |
526 | self.save()
527 |
528 | def get_available_processors(self):
529 | return Processor.objects(published=True)
530 |
--------------------------------------------------------------------------------
/pipelines/__init__.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | from .base import *
4 |
--------------------------------------------------------------------------------
/pipelines/base.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | from flask import session, request
3 | from werkzeug.utils import import_string
4 | from quokka.core.templates import render_template
5 | from quokka.utils import get_current_user
6 |
7 |
8 | class PipelineOverflow(Exception):
9 | pass
10 |
11 |
12 | class CartPipeline(object):
13 |
14 | def __init__(self, cart, pipeline, index=0):
15 | self.cart = cart # Cart object
16 | self.pipeline = pipeline # pipeline ordered list []
17 | self.index = index # current index in pipeline index
18 | self.update_args()
19 |
20 | def update_args(self):
21 | self.args = session.get('cart_pipeline_args', {})
22 | self.args.update(request.form.copy())
23 | session['cart_pipeline_args'] = self.args.copy()
24 |
25 | def del_sessions(self):
26 | if session.get('cart_pipeline_index'):
27 | del session['cart_pipeline_index']
28 | if session.get('cart_pipeline_args'):
29 | del session['cart_pipeline_args']
30 |
31 | def render(self, *args, **kwargs):
32 | return render_template(*args, **kwargs)
33 |
34 | def _preprocess(self):
35 | try:
36 | ret = self.process() # the only overridable method
37 | if not ret:
38 | ret = self.go()
39 | if isinstance(ret, CartPipeline):
40 | session['cart_pipeline_index'] = ret.index
41 | return ret._preprocess()
42 | else:
43 | session['cart_pipeline_index'] = self.index
44 | return ret
45 | except PipelineOverflow as e:
46 | ret = self.cart.checkout()
47 | self.del_sessions()
48 | return ret
49 | except Exception as e:
50 | self.del_sessions()
51 | self.cart.addlog(
52 | u"{e} {p.index} {p} cart: {p.cart.id}".format(p=self, e=e)
53 | )
54 | return render_template('cart/pipeline_error.html',
55 | pipeline=self,
56 | error=e)
57 |
58 | def process(self):
59 | return NotImplementedError("Should be implemented")
60 |
61 | def go(self, index=None, name=None):
62 | index = index or self.index + 1
63 | try:
64 | pipeline = import_string(self.pipeline[index])
65 | except IndexError:
66 | raise PipelineOverflow("pipeline overflow at %s" % index)
67 |
68 | if not issubclass(pipeline, CartPipeline):
69 | raise ValueError("Pipelines should be subclass of CartPipeline")
70 |
71 | return pipeline(self.cart, self.pipeline, index)
72 |
73 |
74 | class CartItemPipeline(CartPipeline):
75 | pass
76 |
77 |
78 | class CartProcessorPipeline(CartPipeline):
79 | pass
80 |
81 |
82 | class StartPipeline(CartPipeline):
83 | """
84 | This is the first pipeline executed upon cart checkout
85 | it only checks if user has email and name
86 | """
87 | def process(self):
88 | self.cart.addlog("StartPipeline")
89 | user = get_current_user()
90 | if not all([user.name, user.email]):
91 | confirm = request.form.get('cart_complete_information')
92 |
93 | name = request.form.get("name") or user.name or ""
94 | email = request.form.get("email") or user.email
95 |
96 | valid_name = len(name.split()) > 1
97 |
98 | if not confirm or not valid_name:
99 | return self.render('cart/complete_information.html',
100 | valid_name=valid_name,
101 | name=name)
102 |
103 | user.name = name
104 | user.email = email
105 | user.save()
106 |
107 | return self.go()
108 |
109 |
110 | class TestPipeline(CartPipeline):
111 | def process(self):
112 | self.cart.addlog("TestPipeline")
113 | if not session.get('completed') == 3:
114 | session['completed'] = 3
115 | return render_template('cart/test.html', cart=self.cart)
116 | return self.go()
117 |
--------------------------------------------------------------------------------
/pipelines/shipping.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quokkaproject/quokka-cart/d119e98c05c688e4da24861ceef6b2c57d59cc6f/pipelines/shipping.py
--------------------------------------------------------------------------------
/processors/__init__.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | import logging
3 | from .base import BaseProcessor
4 | from quokka.core.templates import render_template
5 |
6 | logger = logging.getLogger()
7 |
8 |
9 | class Dummy(BaseProcessor):
10 | def validate(self, *args, **kwargs):
11 | items = self.cart.items
12 | logger.info(items)
13 | return True
14 |
15 | def process(self, *args, **kwargs):
16 | logger.info("Cheking out %s" % self.cart.id)
17 | self.cart.addlog("Dummy processor %s" % self.cart.id)
18 | return render_template('cart/dummy.html', cart=self.cart)
19 |
--------------------------------------------------------------------------------
/processors/base.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 |
4 | class BaseProcessor(object):
5 | def __init__(self, cart, *args, **kwargs):
6 | self.cart = cart
7 | self.config = kwargs.get('config', {})
8 | self._record = kwargs.get('_record')
9 |
10 | def validate(self, *args, **kwargs):
11 | raise NotImplementedError()
12 |
13 | def process(self, *args, **kwargs):
14 | raise NotImplementedError()
15 |
16 | def notification(self):
17 | return "notification"
18 |
19 | def confirmation(self):
20 | return "confirmation"
21 |
--------------------------------------------------------------------------------
/processors/pagseguro_processor.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | import logging
3 | from flask import redirect, request
4 | from pagseguro import PagSeguro
5 | from quokka.core.templates import render_template
6 | from .base import BaseProcessor
7 | from ..models import Cart
8 |
9 | logger = logging.getLogger()
10 |
11 |
12 | class PagSeguroProcessor(BaseProcessor):
13 |
14 | STATUS_MAP = {
15 | "1": "checked_out",
16 | "2": "analysing",
17 | "3": "confirmed",
18 | "4": "completed",
19 | "5": "refunding",
20 | "6": "refunded",
21 | "7": "cancelled"
22 | }
23 |
24 | def __init__(self, cart, *args, **kwargs):
25 | self.cart = cart
26 | self.config = kwargs.get('config')
27 | self._record = kwargs.get('_record')
28 | if not isinstance(self.config, dict):
29 | raise ValueError("Config must be a dict")
30 | email = self.config.get('email')
31 | token = self.config.get('token')
32 | self.pg = PagSeguro(email=email, token=token)
33 | self.cart and self.cart.addlog(
34 | "PagSeguro initialized {}".format(self.__dict__)
35 | )
36 |
37 | def validate(self, *args, **kwargs):
38 | self.pg.sender = self.cart.sender_data
39 | self.pg.shipping = self.cart.shipping_data
40 | self.pg.reference = self.cart.get_uid()
41 | extra_costs = self.cart.get_extra_costs()
42 | if extra_costs:
43 | self.pg.extra_amount = "%.2f" % extra_costs
44 |
45 | self.pg.items = [
46 | {
47 | "id": item.get_uid(),
48 | "description": item.title[:100],
49 | "amount": "%.2f" % item.unity_plus_extra,
50 | "weight": item.weight,
51 | "quantity": int(item.quantity or 1)
52 | }
53 | for item in self.cart.items if item.total >= 0
54 | ]
55 |
56 | if hasattr(self.cart, 'redirect_url'):
57 | self.pg.redirect_url = self.cart.redirect_url
58 | else:
59 | self.pg.redirect_url = self.config.get('redirect_url')
60 |
61 | if hasattr(self.cart, 'notification_url'):
62 | self.pg.notification_url = self.cart.notification_url
63 | else:
64 | self.pg.notification_url = self.config.get('notification_url')
65 |
66 | self.cart.addlog("pagSeguro validated {}".format(self.pg.data))
67 | return True # all data is valid
68 |
69 | def process(self, *args, **kwargs):
70 | kwargs.update(self._record.config)
71 | kwargs.update(self.cart.config)
72 | response = self.pg.checkout(**kwargs)
73 | self.cart.addlog(
74 | (
75 | "lib checkout data:{pg.data}\n"
76 | " code:{r.code} url:{r.payment_url}\n"
77 | " errors: {r.errors}\n"
78 | " xml: {r.xml}\n"
79 | ).format(
80 | pg=self.pg, r=response
81 | )
82 | )
83 | if not response.errors:
84 | self.cart.checkout_code = response.code
85 | self.cart.addlog("PagSeguro processed! {}".format(response.code))
86 | return redirect(response.payment_url)
87 | else:
88 | self.cart.addlog(
89 | 'PagSeguro error processing {}'.format(
90 | response.errors
91 | )
92 | )
93 | return render_template("cart/checkout_error.html",
94 | response=response, cart=self.cart)
95 |
96 | def notification(self):
97 | code = request.form.get('notificationCode')
98 | if not code:
99 | return "notification code not found"
100 |
101 | response = self.pg.check_notification(code)
102 | reference = getattr(response, 'reference', None)
103 | if not reference:
104 | return "reference not found"
105 |
106 | prefix = self.pg.config.get('REFERENCE_PREFIX', '') or ''
107 | prefix = prefix.replace('%s', '')
108 |
109 | status = getattr(response, 'status', None)
110 | transaction_code = getattr(response, 'code', None)
111 |
112 | # get grossAmount to populate a payment with methods
113 | try:
114 | ref = reference.replace(prefix, '')
115 | qs = Cart.objects.filter(
116 | reference_code=ref
117 | ) or Cart.objects.filter(id=ref)
118 |
119 | if not qs:
120 | return "Cart not found"
121 |
122 | self.cart = qs[0]
123 |
124 | self.cart.set_status(
125 | self.STATUS_MAP.get(str(status), self.cart.status)
126 | )
127 |
128 | if transaction_code:
129 | self.cart.transaction_code = transaction_code
130 |
131 | msg = "Status changed to: %s" % self.cart.status
132 | self.cart.addlog(msg)
133 |
134 | fee_amount = getattr(response, 'feeAmount', None)
135 | if fee_amount:
136 | self.cart.set_tax(fee_amount)
137 | msg = "Tax set to: %s" % fee_amount
138 | self.cart.addlog(msg)
139 |
140 | # send response to reference and products
141 | self.cart.send_response(response, 'pagseguro')
142 |
143 | return msg
144 | except Exception as e:
145 | msg = "Error in notification: {} - {}".format(reference, e)
146 | logger.error(msg)
147 | return msg
148 |
149 | def confirmation(self): # redirect_url
150 | context = {}
151 | transaction_param = self.config.get(
152 | 'transaction_param',
153 | self.pg.config.get('TRANSACTION_PARAM', 'transaction_id')
154 | )
155 | transaction_code = request.args.get(transaction_param)
156 | if transaction_code:
157 | context['transaction_code'] = transaction_code
158 | response = self.pg.check_transaction(transaction_code)
159 | logger.debug(response.xml)
160 | reference = getattr(response, 'reference', None)
161 | if not reference:
162 | logger.error("no reference found")
163 | return render_template('cart/simple_confirmation.html',
164 | **context)
165 | prefix = self.pg.config.get('REFERENCE_PREFIX', '') or ''
166 | prefix = prefix.replace('%s', '')
167 |
168 | status = getattr(response, 'status', None)
169 |
170 | # get grossAmount to populate a payment with methods
171 | try:
172 | # self.cart = Cart.objects.get(
173 | # reference_code=reference.replace(PREFIX, '')
174 | # )
175 | ref = reference.replace(prefix, '')
176 | qs = Cart.objects.filter(
177 | reference_code=ref
178 | ) or Cart.objects.filter(id=ref)
179 |
180 | if not qs:
181 | return "Cart not found"
182 |
183 | self.cart = qs[0]
184 |
185 | self.cart.set_status(
186 | self.STATUS_MAP.get(str(status), self.cart.status)
187 | )
188 |
189 | self.cart.transaction_code = transaction_code
190 | msg = "Status changed to: %s" % self.cart.status
191 | self.cart.addlog(msg)
192 | context['cart'] = self.cart
193 | logger.info("Cart updated")
194 |
195 | fee_amount = getattr(response, 'feeAmount', None)
196 | if fee_amount:
197 | self.cart.set_tax(fee_amount)
198 | msg = "Tax set to: %s" % fee_amount
199 | self.cart.addlog(msg)
200 |
201 | # send response to reference and products
202 | self.cart.send_response(response, 'pagseguro')
203 |
204 | return render_template('cart/confirmation.html', **context)
205 | except Exception as e:
206 | msg = "Error in confirmation: {} - {}".format(reference, e)
207 | logger.error(msg)
208 | return render_template('cart/simple_confirmation.html', **context)
209 |
--------------------------------------------------------------------------------
/tasks.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | import logging
4 |
5 | from quokka import create_celery_app
6 |
7 | celery = create_celery_app()
8 |
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | @celery.task
14 | def cart_task():
15 | logger.info("Doing something async...")
16 |
--------------------------------------------------------------------------------
/template_filters.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quokkaproject/quokka-cart/d119e98c05c688e4da24861ceef6b2c57d59cc6f/template_filters.py
--------------------------------------------------------------------------------
/templates/cart/cart.html:
--------------------------------------------------------------------------------
1 | {% extends theme('cart/cart_base.html') %}
2 | {% block js_footer %}
3 |
11 | {% endblock %}
12 |
13 | {% block content %}
14 |
15 |
16 |
17 |
18 | Product |
19 | Unity value |
20 | Extra value |
21 | Quantity |
22 | Total |
23 | Actions |
24 |
25 |
26 |
27 | {% for item in cart.items %}
28 |
29 |
30 | {{ item.title }}
31 | {{ item.description|truncate(140) }}
32 | |
33 | $ {{"%.2f" % item.unity_value}} |
34 | $ {{"%.2f" % item.extra_value}} |
35 |
36 |
40 | |
41 | $ {{"%.2f" % item.total}} |
42 |
43 |
47 | |
48 |
49 | {% endfor %}
50 |
51 | |
52 | $ {{"%.2f" % cart.items|sum(attribute="extra_value")}} |
53 | {{ cart.items|sum(attribute="quantity")|int }} |
54 | $ {{ "%.2f" % cart.total }} |
55 |
56 |
57 |
58 |
59 |
80 |
81 | {% if current_user.is_authenticated() %}
82 |
Purchase history
83 | {% endif %}
84 |
85 | {% endblock %}
86 |
--------------------------------------------------------------------------------
/templates/cart/cart/complete_information.html:
--------------------------------------------------------------------------------
1 | {% extends theme('cart/cart_base.html') %}
2 | {% block content %}
3 | Implements a form
4 |
5 | confirm, name and email
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/templates/cart/cart_base.html:
--------------------------------------------------------------------------------
1 | {% extends theme("base.html") %}
2 |
3 | {% block page_header %}
4 |
5 |
8 |
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/templates/cart/confirmation.html:
--------------------------------------------------------------------------------
1 | {% extends theme('cart/cart_base.html') %}
2 | {% block content %}
3 |
4 |
5 |
6 |
7 | Product |
8 | Unity value |
9 | Extra value |
10 | Quantity |
11 | Total |
12 |
13 |
14 |
15 | {% for item in cart.items %}
16 |
17 |
18 | {{ item.title }}
19 | {{ item.description|truncate(140) }}
20 | |
21 | $ {{"%.2f" % item.unity_value}} |
22 | $ {{"%.2f" % item.extra_value}} |
23 |
24 | {{item.quantity|int}}
25 | |
26 | $ {{"%.2f" % item.total}} |
27 |
28 |
29 | {% endfor %}
30 |
31 | |
32 | $ {{"%.2f" % cart.items|sum(attribute="extra_value")}} |
33 | {{ cart.items|sum(attribute="quantity")|int }} |
34 | $ {{ "%.2f" % cart.total }} |
35 |
36 |
37 |
38 |
39 | {{ transaction_code }}
40 |
41 |
Continue shopping
42 | {% if current_user.is_authenticated() %}
43 |
Purchase history
44 | {% endif %}
45 |
46 | {% endblock %}
47 |
--------------------------------------------------------------------------------
/templates/cart/dummy.html:
--------------------------------------------------------------------------------
1 | {% extends theme('cart/cart_base.html') %}
2 |
3 | {% block content %}
4 |
5 | Dummy processed
6 |
7 | {% for item in cart.log %}
8 | {{item}}
9 | {% endfor %}
10 |
11 |
12 | {% endblock %}
--------------------------------------------------------------------------------
/templates/cart/empty_cart.html:
--------------------------------------------------------------------------------
1 | {% extends theme('cart/cart_base.html') %}
2 |
3 | {% block content %}
4 | Your cart is empty
5 | Continue shopping
6 | {% if current_user.is_authenticated() %}
7 | Purchase history
8 | {% endif %}
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/templates/cart/history.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends theme("base.html") %}
3 |
4 | {% block page_header %}
5 |
6 |
9 |
10 | {% endblock %}
11 |
12 | {% block content %}
13 |
14 |
15 |
Cart
16 |
17 |
18 |
19 | Reference |
20 | Status |
21 | Items |
22 | Amount |
23 | Checkout code |
24 | Created |
25 |
26 |
27 |
28 | {% for cart in carts %}
29 |
30 | {{cart.get_uid()}} |
31 | {{cart.status}} |
32 |
33 | Items: {{cart.items.count()}}
34 |
35 | {% for item in cart.items %}
36 | - {{ item }}
37 | {% endfor %}
38 |
39 | |
40 | $ {{ "%.2f" % cart.total}} |
41 | {{cart.checkout_code}} |
42 | {{cart.created_at}} |
43 |
44 | {% endfor %}
45 |
46 |
47 |
48 | {% endblock %}
49 |
--------------------------------------------------------------------------------
/templates/cart/pipeline_error.html:
--------------------------------------------------------------------------------
1 | {% extends theme('cart/cart_base.html') %}
2 |
3 | {% block content %}
4 |
5 | There was an error processing your cart
6 | Developers have been informed about this error
7 |
8 | erro: {{ e }}
9 |
10 | cart: {{ pipeline.cart }}
11 |
12 |
13 |
14 | {% endblock %}
--------------------------------------------------------------------------------
/templates/cart/simple_confirmation.html:
--------------------------------------------------------------------------------
1 | {% extends theme('cart/cart_base.html') %}
2 | {% block content %}
3 | Thanks for your purchase!
4 | {{ transaction_code }}
5 |
6 | {% endblock %}
--------------------------------------------------------------------------------
/templates/cart/test.html:
--------------------------------------------------------------------------------
1 | {% extends theme('cart/cart_base.html') %}
2 |
3 | {% block content %}
4 |
5 |
20 |
21 | {% endblock %}
22 |
--------------------------------------------------------------------------------
/views.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | import json
4 | from flask import request, jsonify, redirect, url_for, session, current_app
5 | from flask.views import View, MethodView
6 | from quokka.core.templates import render_template
7 | from quokka.utils import get_current_user
8 | from flask.ext.security import current_user
9 | from flask.ext.security.utils import url_for_security
10 | from .models import Cart, Processor
11 |
12 | import logging
13 | logger = logging.getLogger()
14 |
15 |
16 | class BaseView(MethodView):
17 |
18 | requires_login = False
19 |
20 | def needs_login(self, **kwargs):
21 | if not current_user.is_authenticated():
22 | next = kwargs.get('next', request.values.get('next', '/cart'))
23 | return redirect(url_for_security('login', next=next))
24 |
25 | def get(self):
26 | # by default redirects to /cart on get
27 | return self.redirect()
28 |
29 | def as_json(self, **kwargs):
30 | format = request.args.get('format')
31 | if request.is_xhr or format == 'json':
32 | for k, v in kwargs.items():
33 | if hasattr(v, 'to_json'):
34 | kwargs[k] = json.loads(v.to_json())
35 | try:
36 | return jsonify(kwargs)
37 | except:
38 | return jsonify({"result": str(kwargs)})
39 |
40 | def render(self, *args, **kwargs):
41 | return self.as_json(**kwargs) or render_template(*args, **kwargs)
42 |
43 | def redirect(self, *args, **kwargs):
44 | next = request.values.get('next', '/cart')
45 | return self.as_json(**kwargs) or redirect(next)
46 |
47 |
48 | class CartView(BaseView):
49 |
50 | def get(self):
51 | if not session.get('cart_id'):
52 | return self.render(
53 | 'cart/empty_cart.html',
54 | url=current_app.config.get('CART_CONTINUE_URL', '/')
55 | )
56 |
57 | cart = Cart.get_cart()
58 | context = {"cart": cart}
59 |
60 | if cart.items:
61 | template = 'cart/cart.html'
62 | else:
63 | template = 'cart/empty_cart.html'
64 | context['url'] = cart.continue_shopping_url
65 |
66 | return self.render(template, **context)
67 |
68 |
69 | class SetItemView(BaseView):
70 | def post(self):
71 | cart = Cart.get_cart()
72 | params = {k: v for k, v in request.form.items() if not k == "next"}
73 | item = cart.set_item(**params)
74 | return self.redirect(item=item)
75 |
76 |
77 | class RemoveItemView(BaseView):
78 | def post(self):
79 | cart = Cart.get_cart()
80 | params = {k: v for k, v in request.form.items() if not k == "next"}
81 | item = cart.remove_item(**params)
82 | return self.redirect(item=item)
83 |
84 |
85 | class SetProcessorView(BaseView):
86 | def post(self):
87 | cart = Cart.get_cart()
88 | processor = request.form.get('processor')
89 | cart.set_processor(processor)
90 | return self.redirect(processor=cart.processor.identifier)
91 |
92 |
93 | class CheckoutView(BaseView):
94 | def post(self):
95 | cart = Cart.get_cart()
96 | return (cart.requires_login and self.needs_login()) \
97 | or cart.process_pipeline()
98 |
99 |
100 | class HistoryView(BaseView):
101 | def get(self):
102 | context = {
103 | "carts": Cart.objects(belongs_to=get_current_user())
104 | }
105 | return self.needs_login(
106 | next=url_for('quokka.modules.cart.history')
107 | ) or self.render(
108 | 'cart/history.html', **context
109 | )
110 |
111 |
112 | class ProcessorView(View):
113 | methods = ['GET', 'POST']
114 |
115 | def get_processor(self, identifier):
116 | return Processor.get_instance_by_identifier(identifier)
117 |
118 |
119 | class NotificationView(ProcessorView):
120 | def dispatch_request(self, identifier):
121 | processor = self.get_processor(identifier)
122 | return processor.notification()
123 |
124 |
125 | class ConfirmationView(ProcessorView):
126 | def dispatch_request(self, identifier):
127 | processor = self.get_processor(identifier)
128 | return processor.confirmation()
129 |
--------------------------------------------------------------------------------