PK!77lorikeet/__init__.pydefault_app_config = 'lorikeet.apps.LorikeetAppConfig' PK!wU$$lorikeet/api_serializers.pyfrom itertools import chain from time import time from django.core.urlresolvers import reverse from rest_framework import fields, serializers from . import models class LineItemSerializerRegistry: """Registers serializers with their associated models. This is used instead of discovery or a metaclass-based registry as making sure the classes to be registered actually get imported can be fragile and non-obvious to debug. The registry instance is available at ``lorikeet.api_serializers.registry``. """ def __init__(self): self.line_items = {} self.payment_methods = {} self.delivery_addresses = {} def register(self, model, serializer): """Associate ``model`` with ``serializer``.""" print(model.__mro__) if issubclass(model, models.LineItem): self.line_items[model.__name__] = serializer elif issubclass(model, models.PaymentMethod): self.payment_methods[model.__name__] = serializer elif issubclass(model, models.DeliveryAddress): self.delivery_addresses[model.__name__] = serializer else: raise ValueError("model must be a subclass of " "LineItem, PaymentMethod or DeliveryAddress") def get_serializer_class(self, instance): if isinstance(instance, models.LineItem): return self.line_items[instance.__class__.__name__] if isinstance(instance, models.PaymentMethod): return self.payment_methods[instance.__class__.__name__] if isinstance(instance, models.DeliveryAddress): return self.delivery_addresses[instance.__class__.__name__] raise ValueError("instance must be an instance of a " "LineItem, PaymentMethod or DeliveryAddress subclass") def get_serializer(self, instance): return self.get_serializer_class(instance)(instance) registry = LineItemSerializerRegistry() class WritableSerializerMethodField(fields.SerializerMethodField): def __init__(self, write_serializer, method_name=None, **kwargs): self.method_name = method_name self.write_serializer = write_serializer kwargs['source'] = '*' super(fields.SerializerMethodField, self).__init__(**kwargs) def to_internal_value(self, representation): return {self.field_name: self.write_serializer.to_representation(representation)} class PrimaryKeyModelSerializer(serializers.ModelSerializer): """A serializer that accepts the primary key of an object as input. When read from, this serializer works exactly the same as ModelSerializer. When written to, it accepts a valid primary key of an existing instance of the same model. It can't be used to add or edit model instances. This is provided as a convenience, for the common use case of a :class:`~lorikeet.models.LineItem` subclass that has a foreign key to a product model; see the :doc:`Getting Started Guide ` for a usage example. """ def get_queryset(self): """Returns a queryset which the model instance is retrieved from. By default, returns ``self.Meta.model.objects.all()``. """ return self.Meta.model.objects.all() def to_internal_value(self, repr): return self.get_queryset().get(pk=repr) class RegistryRelatedField(fields.Field): def to_representation(self, instance): return registry.get_serializer(instance).data class RegistryRelatedWithMetadataSerializer(serializers.Serializer): type = fields.SerializerMethodField() data = fields.SerializerMethodField() def get_type(self, instance): return instance.__class__.__name__ def get_data(self, instance): return RegistryRelatedField().to_representation(instance) class LineItemMetadataSerializer(RegistryRelatedWithMetadataSerializer): data = WritableSerializerMethodField(fields.DictField()) total = fields.SerializerMethodField() url = fields.SerializerMethodField() def get_total(self, instance): return str(instance.get_total()) def get_url(self, instance): return reverse('lorikeet:cart-item', kwargs={'id': instance.id}) def update(self, instance, validated_data): ser = registry.get_serializer(instance) return ser.update(instance, validated_data['data']) class DeliveryAddressSerializer(RegistryRelatedWithMetadataSerializer): selected = WritableSerializerMethodField(fields.BooleanField()) url = fields.SerializerMethodField() def get_selected(self, instance): return instance.id == self.context['cart'].delivery_address_id def get_url(self, instance): return reverse('lorikeet:address', kwargs={'id': instance.id}) def update(self, instance, validated_data): if validated_data['selected']: cart = self.context['cart'] cart.delivery_address = instance cart.save() return instance class PaymentMethodSerializer(RegistryRelatedWithMetadataSerializer): selected = WritableSerializerMethodField(fields.BooleanField()) url = fields.SerializerMethodField() def get_selected(self, instance): return instance.id == self.context['cart'].payment_method_id def update(self, instance, validated_data): if validated_data['selected']: cart = self.context['cart'] cart.payment_method = instance cart.save() return instance def get_url(self, instance): return reverse('lorikeet:payment-method', kwargs={'id': instance.id}) class SubclassListSerializer(serializers.ListSerializer): def to_representation(self, instance, *args, **kwargs): instance = instance.select_subclasses() return super().to_representation(instance, *args, **kwargs) class CartSerializer(serializers.ModelSerializer): items = SubclassListSerializer(child=LineItemMetadataSerializer()) new_item_url = fields.SerializerMethodField() delivery_addresses = fields.SerializerMethodField() new_address_url = fields.SerializerMethodField() payment_methods = fields.SerializerMethodField() new_payment_method_url = fields.SerializerMethodField() grand_total = fields.DecimalField( max_digits=7, decimal_places=2, source='get_grand_total') is_complete = fields.SerializerMethodField() incomplete_reasons = fields.SerializerMethodField() is_authenticated = fields.SerializerMethodField() checkout_url = fields.SerializerMethodField() generated_at = fields.SerializerMethodField() email = fields.EmailField() def get_new_item_url(self, _): return reverse('lorikeet:add-to-cart') def get_new_address_url(self, _): return reverse('lorikeet:new-address') def get_delivery_addresses(self, cart): selected = cart.delivery_address_subclass the_set = [] if cart.user: the_set = cart.user.delivery_addresses.filter( active=True).select_subclasses() if selected is not None and selected not in the_set: the_set = chain(the_set, [selected]) return DeliveryAddressSerializer(instance=the_set, many=True, context={'cart': cart}).data def get_new_payment_method_url(self, _): return reverse('lorikeet:new-payment-method') def get_payment_methods(self, cart): the_set = [] selected = cart.payment_method_subclass if cart.user: the_set = cart.user.paymentmethod_set.filter( active=True).select_subclasses() if selected is not None and selected not in the_set: the_set = chain(the_set, [selected]) return PaymentMethodSerializer(instance=the_set, many=True, context={'cart': cart}).data def get_generated_at(self, cart): return time() def get_is_complete(self, cart): return cart.is_complete() def get_incomplete_reasons(self, cart): return cart.errors.to_json() def get_is_authenticated(self, cart): return cart.user_id is not None def get_checkout_url(self, _): return reverse('lorikeet:checkout') class Meta: model = models.Cart fields = ('items', 'new_item_url', 'delivery_addresses', 'new_address_url', 'payment_methods', 'new_payment_method_url', 'grand_total', 'generated_at', 'is_complete', 'incomplete_reasons', 'checkout_url', 'is_authenticated', 'email') class CartUpdateSerializer(serializers.ModelSerializer): """Serializer for updating the cart; used only for email field.""" class Meta: model = models.Cart fields = ('email',) class LineItemSerializer(serializers.ModelSerializer): """Base serializer for LineItem subclasses.""" def __init__(self, instance=None, *args, **kwargs): if 'cart' in kwargs: self.cart = kwargs.pop('cart') elif instance is not None: self.cart = instance.cart else: raise TypeError("Either instance or cart arguments must be " "provided to {}".format(self.__class__.__name__)) return super().__init__(instance, *args, **kwargs) def create(self, validated_data): validated_data['cart'] = self.cart return super().create(validated_data) PK!1""lorikeet/api_views.pyfrom logging import getLogger from django.db.transaction import atomic from django.http import Http404 from django.utils.module_loading import import_string from rest_framework.generics import CreateAPIView, RetrieveUpdateDestroyAPIView from rest_framework.response import Response from rest_framework.views import APIView from . import api_serializers, exceptions, models, settings, signals logger = getLogger(__name__) class CartView(APIView): def get(self, request, format=None): cart = request.get_cart() data = api_serializers.CartSerializer( cart, context={'request': self.request}).data return Response(data) def patch(self, request, format=None): cart = request.get_cart() ser = api_serializers.CartUpdateSerializer(instance=cart, data=request.data, partial=True) ser.is_valid(raise_exception=True) ser.save() return self.get(request, format) class CartItemView(RetrieveUpdateDestroyAPIView): def get_object(self): cart = self.request.get_cart() try: return cart.items.get_subclass(id=self.kwargs['id']) except models.LineItem.DoesNotExist: raise Http404() def get_serializer(self, instance, *args, **kwargs): return api_serializers.LineItemMetadataSerializer( instance, context={'cart': self.request.get_cart()}, *args, **kwargs) class AddToCartView(CreateAPIView): def get_serializer(self, data, *args, **kwargs): ser_class = api_serializers.registry.line_items[data['type']] return ser_class(data=data['data'], cart=self.request.get_cart(), *args, **kwargs) class NewAddressView(CreateAPIView): def get_serializer(self, data, *args, **kwargs): ser_class = api_serializers.registry.delivery_addresses[data['type']] return ser_class(data=data['data'], *args, **kwargs) def perform_create(self, serializer): if self.request.user.is_authenticated(): serializer.validated_data['user'] = self.request.user super().perform_create(serializer) cart = self.request.get_cart() cart.delivery_address = serializer.instance cart.save() class NewPaymentMethodView(CreateAPIView): def get_serializer(self, data, *args, **kwargs): ser_class = api_serializers.registry.payment_methods[data['type']] return ser_class(data=data['data'], context={'request': self.request}, *args, **kwargs) def perform_create(self, serializer): if self.request.user.is_authenticated(): serializer.validated_data['user'] = self.request.user super().perform_create(serializer) cart = self.request.get_cart() cart.payment_method = serializer.instance cart.save() class PaymentMethodView(RetrieveUpdateDestroyAPIView): def get_object(self): try: assert self.request.user.is_authenticated() return models.PaymentMethod.objects.get_subclass( user=self.request.user, id=self.kwargs['id'], active=True, ) except (AssertionError, models.PaymentMethod.DoesNotExist): cart = self.request.get_cart() if int(self.kwargs['id']) == cart.payment_method_id: return cart.payment_method_subclass raise Http404() def perform_destroy(self, instance): instance.active = False instance.save() cart = self.request.get_cart() if cart.payment_method_id == instance.id: cart.payment_method = None cart.save() def get_serializer(self, instance, *args, **kwargs): return api_serializers.PaymentMethodSerializer( instance, context={'cart': self.request.get_cart()}, *args, **kwargs) class DeliveryAddressView(RetrieveUpdateDestroyAPIView): def get_object(self): try: assert self.request.user.is_authenticated() return models.DeliveryAddress.objects.get_subclass( user=self.request.user, id=self.kwargs['id'], active=True, ) except (AssertionError, models.DeliveryAddress.DoesNotExist): cart = self.request.get_cart() if int(self.kwargs['id']) == cart.delivery_address_id: return cart.delivery_address_subclass raise Http404() def perform_destroy(self, instance): instance.active = False instance.save() cart = self.request.get_cart() if cart.delivery_address_id == instance.id: cart.delivery_address = None cart.save() def get_serializer(self, instance, *args, **kwargs): return api_serializers.DeliveryAddressSerializer( instance, context={'cart': self.request.get_cart()}, *args, **kwargs) class CheckoutView(APIView): def post(self, request, format=None): try: with atomic(): # Prepare the order object cart = request.get_cart() grand_total = cart.get_grand_total() order = models.Order.objects.create(user=cart.user, grand_total=grand_total, guest_email=cart.email) # Get an invoice ID if required if settings.LORIKEET_INVOICE_ID_GENERATOR is not None: generator = import_string( settings.LORIKEET_INVOICE_ID_GENERATOR) order.custom_invoice_id = generator() # Check the cart is ready to be checked out cart.is_complete(raise_exc=True, for_checkout=True) # copy items onto order, also calculate grand total for item in cart.items.select_subclasses().all(): total = item.get_total() item.total_when_charged = total item.order = order item.cart = None item._new_order = True item.save() item.prepare_for_checkout() # copy delivery address over order.delivery_address = cart.delivery_address # make payment and attach it to order order.payment = cart.payment_method_subclass.make_payment( order, grand_total) print(order.payment) if not isinstance(order.payment, models.Payment): raise TypeError( "{}.make_payment() returned {!r}, not a Payment " "subclass".format( cart.payment_method.__class__.__name__, order.payment, ) ) order.save() except exceptions.PaymentError as e: return Response({ 'reason': 'payment', 'payment_method': cart.payment_method_subclass.__class__.__name__, 'info': e.info, }, status=422) except exceptions.IncompleteCartErrorSet as e: return Response({ 'reason': 'incomplete', 'info': e.to_json(), }, status=422) else: response_body = { 'id': order.id, 'url': order.get_absolute_url(token=True), } # Fire checkout signal signal_res = signals.order_checked_out.send_robust( sender=models.Order, order=order, request=self.request) for handler, result in signal_res: if isinstance(result, Exception): logger.error("Exception in handler %s: %r", handler, result, exc_info=(result.__class__, result, result.__traceback__)) elif isinstance(result, dict): logger.debug("Got result from handler %r: %r", handler, result) response_body.update(result) elif result is None: pass else: logger.warning("Unexpected return type in handler %s: %r", handler, result) return Response(response_body, status=200) PK!N7Mlorikeet/api_views_test.pyfrom json import loads import pytest from shop import models as smodels from shop import factories @pytest.mark.django_db def test_empty_cart(client): resp = client.get('/_cart/') data = loads(resp.content.decode('utf-8')) generated_at = data.pop('generated_at') assert type(generated_at) == float assert data == { 'items': [], 'new_item_url': '/_cart/new/', 'delivery_addresses': [], 'new_address_url': '/_cart/new-address/', 'payment_methods': [], 'new_payment_method_url': '/_cart/new-payment-method/', 'grand_total': '0.00', 'incomplete_reasons': [ { 'code': 'not_set', 'field': 'delivery_address', 'message': 'A delivery address is required.', }, { 'code': 'not_set', 'field': 'payment_method', 'message': 'A payment method is required.', }, { 'code': 'empty', 'field': 'items', 'message': 'There are no items in the cart.', }, { 'code': 'not_set', 'field': 'email', 'message': 'An email address is required.', }, ], 'is_complete': False, 'checkout_url': '/_cart/checkout/', 'is_authenticated': False, 'email': None, } @pytest.mark.django_db def test_empty_cart_logged_in(admin_client): resp = admin_client.get('/_cart/') data = loads(resp.content.decode('utf-8')) generated_at = data.pop('generated_at') assert type(generated_at) == float assert data == { 'items': [], 'new_item_url': '/_cart/new/', 'delivery_addresses': [], 'new_address_url': '/_cart/new-address/', 'payment_methods': [], 'new_payment_method_url': '/_cart/new-payment-method/', 'grand_total': '0.00', 'incomplete_reasons': [ { 'code': 'not_set', 'field': 'delivery_address', 'message': 'A delivery address is required.', }, { 'code': 'not_set', 'field': 'payment_method', 'message': 'A payment method is required.', }, { 'code': 'empty', 'field': 'items', 'message': 'There are no items in the cart.', }, ], 'is_complete': False, 'checkout_url': '/_cart/checkout/', 'is_authenticated': True, 'email': None, } @pytest.mark.django_db def test_cart_contents(client, cart): # set up cart contents i = factories.MyLineItemFactory(cart=cart) # add some more line items not attached to the cart factories.MyLineItemFactory() factories.MyLineItemFactory() resp = client.get('/_cart/') data = loads(resp.content.decode('utf-8')) assert data['items'] == [{ 'type': 'MyLineItem', 'url': '/_cart/{}/'.format(i.id), 'data': { 'product': { 'id': i.product.id, 'name': i.product.name, 'unit_price': str(i.product.unit_price), }, 'quantity': i.quantity, }, 'total': str(i.product.unit_price * i.quantity), }] assert data['grand_total'] == str(i.product.unit_price * i.quantity) @pytest.mark.django_db def test_cart_delivery_addresses(client, cart): # set up cart contents cart.delivery_address = factories.AustralianDeliveryAddressFactory() cart.save() # Add another address not attached to the card factories.AustralianDeliveryAddressFactory() resp = client.get('/_cart/') data = loads(resp.content.decode('utf-8')) assert data['delivery_addresses'] == [{ 'type': 'AustralianDeliveryAddress', 'selected': True, 'data': { 'addressee': cart.delivery_address.addressee, 'address': cart.delivery_address.address, 'suburb': cart.delivery_address.suburb, 'state': cart.delivery_address.state, 'postcode': cart.delivery_address.postcode, }, 'url': '/_cart/address/{}/'.format(cart.delivery_address_id), }] @pytest.mark.django_db def test_cart_delivery_addresses_logged_in(admin_user, admin_client, admin_cart): # set up cart contents admin_cart.delivery_address = factories.AustralianDeliveryAddressFactory( addressee="Active Address", user=admin_user) admin_cart.save() other_addr = factories.AustralianDeliveryAddressFactory( user=admin_user, addressee="Inactive Address") # Add another address not attached to the card factories.AustralianDeliveryAddressFactory() factories.AustralianDeliveryAddressFactory( user=admin_user, active=False, addressee="Disabled Address") resp = admin_client.get('/_cart/') data = loads(resp.content.decode('utf-8')) assert data['delivery_addresses'] == [ { 'type': 'AustralianDeliveryAddress', 'selected': True, 'data': { 'addressee': admin_cart.delivery_address.addressee, 'address': admin_cart.delivery_address.address, 'suburb': admin_cart.delivery_address.suburb, 'state': admin_cart.delivery_address.state, 'postcode': admin_cart.delivery_address.postcode, }, 'url': '/_cart/address/{}/'.format(admin_cart.delivery_address_id), }, { 'type': 'AustralianDeliveryAddress', 'selected': False, 'data': { 'addressee': other_addr.addressee, 'address': other_addr.address, 'suburb': other_addr.suburb, 'state': other_addr.state, 'postcode': other_addr.postcode, }, 'url': '/_cart/address/{}/'.format(other_addr.id), }, ] @pytest.mark.django_db def test_cart_payment_methods(client, cart): # set up cart contents cart.payment_method = smodels.PipeCard.objects.create(card_id='Visa4242') cart.save() # add a payment method not attached to the card smodels.PipeCard.objects.create(card_id='Mastercard4242') resp = client.get('/_cart/') data = loads(resp.content.decode('utf-8')) assert data['payment_methods'] == [{ 'type': 'PipeCard', 'selected': True, 'data': { 'brand': 'Visa', 'last4': '4242', }, 'url': '/_cart/payment-method/{}/'.format(cart.payment_method_id) }] @pytest.mark.django_db def test_cart_payment_methods_logged_in(admin_user, admin_client, admin_cart): # set up cart contents admin_cart.payment_method = smodels.PipeCard.objects.create( card_id='Visa4242') admin_cart.save() other = smodels.PipeCard.objects.create( card_id='Discover4242', user=admin_user) # add a payment method not attached to the card smodels.PipeCard.objects.create(card_id='Mastercard4242') smodels.PipeCard.objects.create( card_id='Amex4242', user=admin_user, active=False) resp = admin_client.get('/_cart/') data = loads(resp.content.decode('utf-8')) # we don't care about the order assert data['payment_methods'] == [ { 'type': 'PipeCard', 'selected': False, 'data': { 'brand': 'Discover', 'last4': '4242', }, 'url': '/_cart/payment-method/{}/'.format(other.id) }, { 'type': 'PipeCard', 'selected': True, 'data': { 'brand': 'Visa', 'last4': '4242', }, 'url': '/_cart/payment-method/{}/'.format(admin_cart.payment_method_id) }, ] PK!z%lorikeet/api_views_test_cart_items.pyfrom json import dumps, loads import pytest from shop import models as smodels from shop import factories @pytest.mark.django_db def test_add_item_to_cart(client): p = factories.ProductFactory() resp = client.post('/_cart/new/', dumps({ 'type': "MyLineItem", 'data': {'product': p.id, 'quantity': 2}, }), content_type='application/json') assert resp.status_code == 201 assert smodels.MyLineItem.objects.count() == 1 @pytest.mark.django_db def test_view_cart_item(client, cart): i = factories.MyLineItemFactory(cart=cart) resp = client.get('/_cart/{}/'.format(i.id)) data = loads(resp.content.decode('utf-8')) assert data == { 'type': "MyLineItem", 'data': { 'product': { 'id': i.product.id, 'name': i.product.name, 'unit_price': str(i.product.unit_price), }, 'quantity': i.quantity, }, 'total': str(i.quantity * i.product.unit_price), 'url': '/_cart/{}/'.format(i.id), } @pytest.mark.django_db def test_cannot_view_cart_item_not_in_cart(client): i = factories.MyLineItemFactory() resp = client.get('/_cart/{}/'.format(i.id)) assert resp.status_code == 404 assert smodels.MyLineItem.objects.count() == 1 @pytest.mark.django_db def test_remove_cart_item(client, cart): i = factories.MyLineItemFactory(cart=cart) resp = client.delete('/_cart/{}/'.format(i.id)) assert resp.status_code == 204 assert smodels.MyLineItem.objects.count() == 0 @pytest.mark.django_db def test_cannot_remove_cart_item_not_in_cart(client): i = factories.MyLineItemFactory() resp = client.delete('/_cart/{}/'.format(i.id)) assert resp.status_code == 404 assert smodels.MyLineItem.objects.count() == 1 @pytest.mark.django_db def test_change_cart_item(client, cart): i = factories.MyLineItemFactory(cart=cart) new_qty = i.quantity + 1 resp = client.patch('/_cart/{}/'.format(i.id), dumps({ 'data': {'quantity': new_qty}, }), content_type='application/json') print(resp.content) assert resp.status_code == 200 assert smodels.MyLineItem.objects.get(id=i.id).quantity == new_qty PK!>MY Y #lorikeet/api_views_test_checkout.pyfrom json import dumps, loads import pytest from shop import models as smodels from . import models @pytest.mark.django_db def test_checkout(client, filled_cart): expected_total = filled_cart.get_grand_total() resp = client.post('/_cart/checkout/', dumps({}), content_type='application/json') assert resp.status_code == 200 assert loads(resp.content.decode('utf-8')) == { 'id': 1, 'url': None, } filled_cart.refresh_from_db() assert filled_cart.items.count() == 0 assert models.Order.objects.count() == 1 assert models.Order.objects.first().grand_total == expected_total for item in models.Order.objects.first().items.all(): assert item.total_when_charged @pytest.mark.django_db def test_cart_incomplete(client, cart): resp = client.post('/_cart/checkout/', dumps({}), content_type='application/json') assert resp.status_code == 422 assert loads(resp.content.decode('utf-8')) == { 'reason': 'incomplete', 'info': [ { 'code': 'not_set', 'field': 'delivery_address', 'message': 'A delivery address is required.', }, { 'code': 'not_set', 'field': 'payment_method', 'message': 'A payment method is required.', }, { 'code': 'empty', 'field': 'items', 'message': 'There are no items in the cart.', }, { 'code': 'not_set', 'field': 'email', 'message': 'An email address is required.', }, ], } assert models.Order.objects.count() == 0 @pytest.mark.django_db def test_payment_failed(client, filled_cart): filled_cart.payment_method = smodels.PipeCard.objects.create( card_id="Visa4949") filled_cart.save() resp = client.post('/_cart/checkout/', dumps({}), content_type='application/json') assert resp.status_code == 422 assert loads(resp.content.decode('utf-8')) == { 'reason': 'payment', 'payment_method': 'PipeCard', 'info': 'Insufficient funds', } filled_cart.refresh_from_db() assert filled_cart.items.count() == 2 assert models.Order.objects.count() == 0 PK!HH-lorikeet/api_views_test_delivery_addresses.pyfrom json import dumps, loads import pytest from shop import models as smodels from shop import factories @pytest.mark.django_db def test_add_delivery_address(client, cart): resp = client.post('/_cart/new-address/', dumps({ 'type': "AustralianDeliveryAddress", 'data': { 'addressee': 'Adam Brenecki', 'address': 'Commercial Motor Vehicles Pty Ltd\n' 'Level 1, 290 Wright Street', 'suburb': 'Adelaide', 'state': 'SA', 'postcode': '5000', }, }), content_type='application/json') assert resp.status_code == 201 assert smodels.AustralianDeliveryAddress.objects.count() == 1 cart.refresh_from_db() assert cart.delivery_address is not None @pytest.mark.django_db def test_add_delivery_address_logged_in(admin_user, admin_client, admin_cart): resp = admin_client.post('/_cart/new-address/', dumps({ 'type': "AustralianDeliveryAddress", 'data': { 'addressee': 'Adam Brenecki', 'address': 'Commercial Motor Vehicles Pty Ltd\n' 'Level 1, 290 Wright Street', 'suburb': 'Adelaide', 'state': 'SA', 'postcode': '5000', }, }), content_type='application/json') assert resp.status_code == 201 assert smodels.AustralianDeliveryAddress.objects.count() == 1 assert smodels.AustralianDeliveryAddress.objects.first().user == admin_user admin_cart.refresh_from_db() assert admin_cart.delivery_address is not None @pytest.mark.django_db def test_view_delivery_address(client, cart): cart.delivery_address = factories.AustralianDeliveryAddressFactory() cart.save() url = '/_cart/address/{}/'.format(cart.delivery_address_id) resp = client.get(url) data = loads(resp.content.decode('utf-8')) assert data == { 'type': "AustralianDeliveryAddress", 'data': { 'addressee': cart.delivery_address.addressee, 'address': cart.delivery_address.address, 'suburb': cart.delivery_address.suburb, 'state': cart.delivery_address.state, 'postcode': cart.delivery_address.postcode, }, 'selected': True, 'url': url, } @pytest.mark.django_db def test_view_owned_unselected_delivery_address(admin_user, admin_client): addr = factories.AustralianDeliveryAddressFactory(user=admin_user) url = '/_cart/address/{}/'.format(addr.id) resp = admin_client.get(url) data = loads(resp.content.decode('utf-8')) assert data == { 'type': "AustralianDeliveryAddress", 'data': { 'addressee': addr.addressee, 'address': addr.address, 'suburb': addr.suburb, 'state': addr.state, 'postcode': addr.postcode, }, 'selected': False, 'url': url, } @pytest.mark.django_db def test_view_unowned_delivery_address(admin_user, client): addr = factories.AustralianDeliveryAddressFactory(user=admin_user) url = '/_cart/address/{}/'.format(addr.id) resp = client.get(url) assert resp.status_code == 404 @pytest.mark.django_db def test_view_inactive_delivery_address(admin_user, admin_client): addr = factories.AustralianDeliveryAddressFactory(user=admin_user, active=False) url = '/_cart/address/{}/'.format(addr.id) resp = admin_client.get(url) assert resp.status_code == 404 @pytest.mark.django_db def test_select_delivery_address(admin_user, admin_client, admin_cart): addr = factories.AustralianDeliveryAddressFactory(user=admin_user) url = '/_cart/address/{}/'.format(addr.id) resp = admin_client.patch(url, dumps({'selected': True}), content_type='application/json') assert resp.status_code == 200 admin_cart.refresh_from_db() assert admin_cart.delivery_address_id == addr.id @pytest.mark.django_db def test_select_inactive_delivery_address(admin_user, admin_client, admin_cart): addr = factories.AustralianDeliveryAddressFactory(user=admin_user, active=False) url = '/_cart/address/{}/'.format(addr.id) resp = admin_client.patch(url, dumps({'selected': True}), content_type='application/json') assert resp.status_code == 404 admin_cart.refresh_from_db() assert admin_cart.delivery_address_id != addr.id @pytest.mark.django_db def test_delete_delivery_address(client, cart): addr = factories.AustralianDeliveryAddressFactory() cart.delivery_address = addr cart.save() url = '/_cart/address/{}/'.format(addr.id) resp = client.delete(url) assert resp.status_code == 204 cart.refresh_from_db() assert cart.delivery_address is None addr.refresh_from_db() assert not addr.active PK!3))lorikeet/api_views_test_payment_method.pyfrom json import dumps, loads import pytest from shop import models as smodels @pytest.mark.django_db def test_add_payment_method(client, cart): resp = client.post('/_cart/new-payment-method/', dumps({ 'type': "PipeCard", 'data': { 'card_token': 'Lvfn4242', }, }), content_type='application/json') assert resp.status_code == 201 assert smodels.PipeCard.objects.count() == 1 cart.refresh_from_db() assert cart.payment_method is not None @pytest.mark.django_db def test_add_payment_method_logged_in(admin_user, admin_client, admin_cart): resp = admin_client.post('/_cart/new-payment-method/', dumps({ 'type': "PipeCard", 'data': { 'card_token': 'Lvfn4242', }, }), content_type='application/json') assert resp.status_code == 201 assert smodels.PipeCard.objects.count() == 1 assert smodels.PipeCard.objects.first().user == admin_user admin_cart.refresh_from_db() assert admin_cart.payment_method is not None @pytest.mark.django_db def test_view_payment_method(client, cart): cart.payment_method = smodels.PipeCard.objects.create(card_id='Visa4242') cart.save() url = '/_cart/payment-method/{}/'.format(cart.payment_method_id) resp = client.get(url) data = loads(resp.content.decode('utf-8')) assert data == { 'type': 'PipeCard', 'selected': True, 'data': { 'brand': 'Visa', 'last4': '4242', }, 'url': url, } @pytest.mark.django_db def test_view_owned_unselected_payment_method(admin_user, admin_client): pm = smodels.PipeCard.objects.create(card_id='Visa4242', user=admin_user) url = '/_cart/payment-method/{}/'.format(pm.id) resp = admin_client.get(url) data = loads(resp.content.decode('utf-8')) assert data == { 'type': 'PipeCard', 'selected': False, 'data': { 'brand': 'Visa', 'last4': '4242', }, 'url': url, } @pytest.mark.django_db def test_view_unowned_payment_method(admin_user, client): pm = smodels.PipeCard.objects.create(card_id='Visa4242', user=admin_user) url = '/_cart/payment-method/{}/'.format(pm.id) resp = client.get(url) assert resp.status_code == 404 @pytest.mark.django_db def test_view_inactive_payment_method(admin_user, admin_client): pm = smodels.PipeCard.objects.create(card_id='Visa4242', user=admin_user, active=False) url = '/_cart/payment-method/{}/'.format(pm.id) resp = admin_client.get(url) assert resp.status_code == 404 @pytest.mark.django_db def test_select_payment_method(admin_user, admin_client, admin_cart): pm = smodels.PipeCard.objects.create(card_id='Visa4242', user=admin_user) url = '/_cart/payment-method/{}/'.format(pm.id) resp = admin_client.patch(url, dumps({'selected': True}), content_type='application/json') assert resp.status_code == 200 admin_cart.refresh_from_db() assert admin_cart.payment_method_id == pm.id @pytest.mark.django_db def test_select_inactive_payment_method(admin_user, admin_client, admin_cart): pm = smodels.PipeCard.objects.create(card_id='Visa4242', user=admin_user, active=False) url = '/_cart/payment-method/{}/'.format(pm.id) resp = admin_client.patch(url, dumps({'selected': True}), content_type='application/json') assert resp.status_code == 404 admin_cart.refresh_from_db() assert admin_cart.payment_method_id != pm.id @pytest.mark.django_db def test_delete_payment_method(client, cart): pm = smodels.PipeCard.objects.create(card_id='Visa4242') cart.payment_method = pm cart.save() url = '/_cart/payment-method/{}/'.format(cart.payment_method_id) resp = client.delete(url) assert resp.status_code == 204 cart.refresh_from_db() assert cart.payment_method is None pm.refresh_from_db() assert not pm.active PK!>Rlorikeet/apps.pyfrom django.apps import AppConfig class LorikeetAppConfig(AppConfig): name = 'lorikeet' verbose_name = 'Lorikeet' def ready(self): from . import signal_handlers # noqa PK!f%lorikeet/cart_checkers.pyfrom .exceptions import IncompleteCartError def delivery_address_required(cart): """Checks that a delivery address is set on the cart.""" if cart.delivery_address is None: raise IncompleteCartError('not_set', 'A delivery address is required.', 'delivery_address') def payment_method_required(cart): """Checks that a payment method is set on the cart.""" if cart.payment_method is None: raise IncompleteCartError('not_set', 'A payment method is required.', 'payment_method') def cart_not_empty(cart): """Checks that a payment method is set on the cart.""" if not cart.items.exists(): raise IncompleteCartError('empty', 'There are no items in the cart.', 'items') def email_address_if_anonymous(cart): """Checks an email address is set if the user isn't logged in.""" if not cart.user and not cart.email: raise IncompleteCartError('not_set', 'An email address is required.', 'email') PK!M lorikeet/conftest.pyimport pytest from faker import Faker from shop import models as smodels from shop import factories from . import models def fill_cart(cart): factories.MyLineItemFactory(cart=cart) factories.MyLineItemFactory(cart=cart) cart.delivery_address = factories.AustralianDeliveryAddressFactory() cart.payment_method = smodels.PipeCard.objects.create(card_id="Visa4242") if not cart.user: cart.email = Faker().safe_email() cart.save() @pytest.fixture def cart(client): cart = models.Cart.objects.create() session = client.session session['cart_id'] = cart.id session.save() return cart @pytest.fixture def filled_cart(cart): fill_cart(cart) return cart @pytest.fixture def admin_cart(admin_user): cart = models.Cart.objects.create(user=admin_user) return cart @pytest.fixture def filled_admin_cart(admin_cart): fill_cart(admin_cart) return admin_cart PK!VG lorikeet/exceptions.pyclass PaymentError(Exception): """Represents an error accepting payment on a PaymentMethod. :param info: A JSON-serializable object containing details of the problem, to be passed to the client. """ def __init__(self, info=None): self.info = info class IncompleteCartError(Exception): """Represents a reason that a cart is not ready for checkout. Similar to a Django ``ValidationError``, but not used to reject a change based on submitted data. :param code: A consistent, non-localised string to identify the specific error. :type code: str :param message: A human-readable message that explains the error. :type message: str :param field: The field that the error relates to. This should match one of the fields in the cart's serialized representation, or be set to ``None`` if a specific field does not apply. :type field: str, NoneType """ def __init__(self, code, message, field=None): self.code = code self.message = message self.field = field def to_json(self): """Returns the error in a JSON-serializable form. :rtype: dict """ return { 'code': self.code, 'message': self.message, 'field': self.field, } def __key(self): return (self.code, self.message, self.field) def __hash__(self): return hash(self.__key()) def __eq__(self, other): return self.__key() == other.__key() class IncompleteCartErrorSet(IncompleteCartError): """Represents a set of multiple reasons a cart is not ready for checkout. You can raise this exception instead of :class:`~lorikeet.exceptions.IncompleteCartError` if you would like to provide multiple errors at once. This class is iterable. :param errors: All of the errors that apply. :type errors: Iterable[IncompleteCartError] """ def __init__(self, errors=()): self.errors = list(errors) def to_json(self): """Returns the list of errors in a JSON-serializable form. :rtype: dict """ return [x.to_json() for x in self.errors] def add(self, error): """Add a new error to the set. :param error: The error to add. If an :class:`~lorikeet.exceptions.IncompleteCartErrorSet` instance is passed, it will be merged into this one. :type error: IncompleteCartError, IncompleteCartErrorSet """ if isinstance(error, IncompleteCartErrorSet): self.errors += error.errors else: self.errors.append(error) def __bool__(self): return bool(self.errors) def __iter__(self): return iter(self.errors) PK!iZZlorikeet/exceptions_test.pyfrom . import exceptions def test_cart_error_set_combination(): es = exceptions.IncompleteCartErrorSet() a = exceptions.IncompleteCartError('a', 'a') b = exceptions.IncompleteCartError('b', 'b') es.add(a) es.add(b) assert es.errors == [a, b] def test_cart_error_set_flattens(): es = exceptions.IncompleteCartErrorSet() a = exceptions.IncompleteCartError('a', 'a') b = exceptions.IncompleteCartError('b', 'b') c = exceptions.IncompleteCartError('c', 'c') es.add(a) es.add(exceptions.IncompleteCartErrorSet((b, c))) assert es.errors == [a, b, c] PK!lorikeet/extras/__init__.pyPK!OPP)lorikeet/extras/email_invoice/__init__.pydefault_app_config = 'lorikeet.extras.email_invoice.apps.EmailInvoiceAppConfig' PK!7%lorikeet/extras/email_invoice/apps.pyfrom django.apps import AppConfig class EmailInvoiceAppConfig(AppConfig): name = 'lorikeet.extras.email_invoice' def ready(self): from . import signal_handlers # noqa PK!vv)lorikeet/extras/email_invoice/settings.pyfrom django.conf import settings as s class Settings: _defaults = { 'subject': "Your order ID {order.invoice_id}", 'from_address': 'orders@example.com', 'template_html': None, 'template_text': None, 'copy_address': None, } def __getattr__(self, attr): if attr not in self._defaults: raise KeyError(attr) return getattr(s, 'LORIKEET_EMAIL_INVOICE_' + attr.upper(), self._defaults[attr]) @property def copy_address(self): return getattr(s, 'LORIKEET_EMAIL_INVOICE_COPY_ADDRESS', None) settings = Settings() __all__ = ['settings'] PK!&Σ{0lorikeet/extras/email_invoice/signal_handlers.pyfrom logging import getLogger from django.core.mail import send_mail from django.dispatch import receiver from django.template.loader import render_to_string from lorikeet.signals import order_checked_out from premailer import transform from . import textify from .settings import settings logger = getLogger(__name__) @receiver(order_checked_out) def send_email_invoice(sender, order, request, **kwargs): subject = settings.subject.format(order=order) recipient = order.user.email if order.user else order.guest_email if recipient is None: return {'invoice_email': None} ctx = { 'order': order, 'order_url': request.build_absolute_uri(order.get_absolute_url(token=True)), } mail_kwargs = {} if settings.template_html: html = render_to_string(settings.template_html, ctx) html = transform(html, base_url=request.build_absolute_uri()) mail_kwargs['html_message'] = html if settings.template_text: text = render_to_string(settings.template_text, ctx) mail_kwargs['message'] = text elif settings.template_html: mail_kwargs['message'] = textify.transform(html) else: raise ValueError("No HTML or text template set") logger.debug('Sending an invoice email to %s', recipient) send_mail( subject=subject, from_email=settings.from_address, recipient_list=[recipient], **mail_kwargs ) if settings.copy_address: send_mail( subject=subject, from_email=settings.from_address, recipient_list=[settings.copy_address], **mail_kwargs ) return {'invoice_email': recipient} PK!.?##5lorikeet/extras/email_invoice/signal_handlers_test.pyimport pytest from lorikeet import models @pytest.mark.django_db def test_checkout(settings, client, filled_cart, mailoutbox): settings.INSTALLED_APPS += ['lorikeet.extras.email_invoice'] client.post('/_cart/checkout/', content_type='application/json') assert len(mailoutbox) == 1 order = models.Order.objects.first() m = mailoutbox[0] assert m.subject == 'Your order ID {}'.format(order.invoice_id) assert 'Invoice ID: {}'.format(order.invoice_id) in m.body assert m.from_email == 'orders@example.com' assert list(m.to) == [order.guest_email] @pytest.mark.django_db def test_checkout_copy(settings, client, filled_cart, mailoutbox): settings.LORIKEET_EMAIL_INVOICE_COPY_ADDRESS = 'invcopy@example.com' if 'lorikeet.extras.email_invoice' not in settings.INSTALLED_APPS: settings.INSTALLED_APPS += ['lorikeet.extras.email_invoice'] client.post('/_cart/checkout/', content_type='application/json') assert len(mailoutbox) == 2 order = models.Order.objects.first() m = mailoutbox[1] assert m.subject == 'Your order ID {}'.format(order.invoice_id) assert 'Invoice ID: {}'.format(order.invoice_id) in m.body assert m.from_email == 'orders@example.com' assert list(m.to) == ['invcopy@example.com'] PK!y(lorikeet/extras/email_invoice/textify.pyfrom html import parser BLOCK_ELEMENTS = ('address', 'article', 'aside', 'blockquote', 'br', 'canvas', 'dd', 'div', 'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'li', 'main', 'nav', 'noscript', 'ol', 'output', 'p', 'pre', 'section', 'table', 'tfoot', 'ul', 'video') class TextifyParser(parser.HTMLParser): element_stack = [] def __init__(self, transformer): super().__init__() self.transformer = transformer self.element_stack = [('DOCUMENT', (), [])] def handle_starttag(self, tag, attrs): attr_dict = {k: v for (k, v) in attrs} self.element_stack.append((tag, attr_dict, [])) def handle_endtag(self, tag): last_element = self.element_stack.pop() transformed_element = self.transformer(*last_element) self.handle_data(transformed_element) def handle_data(self, data): self.element_stack[-1][2].append(data) def get_result(self): return self.element_stack[0][2] def get_string_result(self): return ''.join(self.get_result()) def transformer(name, attrs, body_list): body = ''.join(body_list) if name in ('h1', 'h2', 'h3', 'h4', 'h5', 'h6'): return body + '\n' + '=' * len(body) + '\n' if name == 'a' and 'href' in attrs: return body + ' [' + attrs['href'] + ']' if name in BLOCK_ELEMENTS: return body + '\n' else: return body def transform(input): parser = TextifyParser(transformer) parser.feed(input) return parser.get_string_result() PK!&lorikeet/extras/starshipit/__init__.pyPK!su5lorikeet/extras/starshipit/migrations/0001_initial.py# -*- coding: utf-8 -*- # Generated by Django 1.9.9 on 2017-04-04 03:25 from __future__ import unicode_literals from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): initial = True dependencies = [ ('lorikeet', '0013_auto_20170307_1512'), ] operations = [ migrations.CreateModel( name='StarShipItOrder', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('order', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='starshipit_order', to='lorikeet.Order')), ], ), ] PK!1lorikeet/extras/starshipit/migrations/__init__.pyPK!Rp$lorikeet/extras/starshipit/models.pyimport requests from django.conf import settings from django.core.cache import cache from django.db import models from lorikeet.models import Order CACHE_KEY_TPL = "au.com.cmv.open-source.lorikeet.starshipit.order-status.{}" class StarShipItOrder(models.Model): order = models.OneToOneField(Order, related_name="starshipit_order") @property def status(self): """Get the status of this order. Returns a dictionary in the format returned by the `StarShipIt API `_. Note that the values returned might not match the documentation. Values returned from this function are cached for 5 minutes. """ invoice_id = self.order.invoice_id cache_key = CACHE_KEY_TPL.format(invoice_id) cached_status = cache.get(cache_key) if cached_status is not None: return cached_status status = requests.get('https://api2.starshipit.com/tracking', params={ 'apikey': settings.STARSHIPIT_API_KEY, 'OrderNumber': self.order.invoice_id, 'format': 'json', }).json() cache.set(cache_key, status, 300) return status PK!ܾj>>$lorikeet/extras/starshipit/submit.pyimport requests from django.conf import settings from lorikeet import models as lorikeet_models from . import models def submit_orders(): orders = lorikeet_models.Order.objects.filter(starshipit_order=None) if not orders: return blob = get_orders_blob(orders) requests.post('https://api2.starshipit.com/orders?format=json', json=blob).raise_for_status() for order in orders: models.StarShipItOrder.objects.create(order=order) def get_orders_blob(orders): return { 'ApiKey': settings.STARSHIPIT_API_KEY, 'OrdersList': [get_order_blob(x) for x in orders], } def get_order_blob(order): destination = starshipit_repr(order.delivery_address_subclass) if 'Email' not in destination: destination['Email'] = order.email return { 'OrderNumber': order.invoice_id, 'Destination': destination, 'Items': [starshipit_repr(x) for x in order.items.select_subclasses()], } def starshipit_repr(thing): if hasattr(thing, 'starshipit_repr'): return thing.starshipit_repr() klass = thing.__class__.__name__ if hasattr(settings, 'STARSHIPIT_REPR') and klass in settings.STARSHIPIT_REPR: return settings.STARSHIPIT_REPR[klass](thing) raise TypeError("Could not serialize %r for StarShipIT", thing) PK!eo#lorikeet/extras/starshipit/tasks.pyfrom celery import shared_task from .submit import submit_orders as do_submit_orders @shared_task def submit_orders(): do_submit_orders() PK!ʷ"lorikeet/extras/stripe/__init__.pyfrom django.apps import AppConfig from django.conf import settings import stripe class StripeAppConfig(AppConfig): name = 'lorikeet.extras.stripe' verbose_name = "Lorikeet Stripe" def ready(self): stripe.api_key = settings.STRIPE_API_KEY from . import models, api_serializers from lorikeet.api_serializers import registry registry.register(models.StripeCard, api_serializers.StripeCardSerializer) default_app_config = 'lorikeet.extras.stripe.StripeAppConfig' PK!蝚)lorikeet/extras/stripe/api_serializers.pyfrom logging import getLogger import stripe from rest_framework import exceptions, fields, serializers from . import models logger = getLogger(__name__) class StripeAPIException(exceptions.APIException): status_code = 422 def __init__(self, info): super().__init__() self.detail = { 'reason': 'stripe', 'info': info, } class StripeCardSerializer(serializers.ModelSerializer): token = fields.CharField(max_length=30, write_only=True) brand = fields.SerializerMethodField() last4 = fields.SerializerMethodField() class Meta: model = models.StripeCard fields = ('token', 'reusable', 'brand', 'last4') def get_brand(self, object): return object.data['brand'] def get_last4(self, object): return object.data['last4'] def create(self, validated_data): request = self.context['request'] if validated_data['reusable']: logger.debug("Creating a reusable card record") customer = None try: if request.user.is_authenticated(): first_card = models.StripeCard.objects.filter( user=request.user, customer_id__isnull=False, ).order_by('id').first() if first_card is not None: customer = stripe.Customer.retrieve( first_card.customer_id) if customer is None: customer = stripe.Customer.create() card = customer.sources.create(source=validated_data['token']) except stripe.error.CardError as e: raise StripeAPIException(e.json_body['error']) validated_data['card_id'] = card['id'] validated_data['customer_id'] = customer['id'] else: validated_data['token_id'] = validated_data['token'] del validated_data['token'] return super().create(validated_data) PK!G1QQ1lorikeet/extras/stripe/migrations/0001_initial.py# -*- coding: utf-8 -*- # Generated by Django 1.9.9 on 2017-01-04 06:06 from __future__ import unicode_literals from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): initial = True dependencies = [ ('lorikeet', '0004_auto_20161220_1431'), ] operations = [ migrations.CreateModel( name='StripeCard', fields=[ ('paymentmethod_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='lorikeet.PaymentMethod')), ('card_token', models.CharField(max_length=30)), ('customer_token', models.CharField(max_length=30)), ], bases=('lorikeet.paymentmethod',), ), ] PK!B  7lorikeet/extras/stripe/migrations/0002_stripepayment.py# -*- coding: utf-8 -*- # Generated by Django 1.9.9 on 2017-01-18 03:12 from __future__ import unicode_literals from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ ('lorikeet', '0008_auto_20170117_0453'), ('stripe', '0001_initial'), ] operations = [ migrations.CreateModel( name='StripePayment', fields=[ ('payment_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='lorikeet.Payment')), ('charge_id', models.CharField(max_length=30)), ], bases=('lorikeet.payment',), ), ] PK!b8II<lorikeet/extras/stripe/migrations/0003_auto_20170502_1043.py# -*- coding: utf-8 -*- # Generated by Django 1.9.9 on 2017-05-02 01:13 from __future__ import unicode_literals from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('stripe', '0002_stripepayment'), ] operations = [ migrations.RenameField( model_name='stripecard', old_name='card_token', new_name='card_id', ), migrations.RenameField( model_name='stripecard', old_name='customer_token', new_name='customer_id', ), ] PK!7Rl  <lorikeet/extras/stripe/migrations/0004_auto_20170502_1228.py# -*- coding: utf-8 -*- # Generated by Django 1.9.9 on 2017-05-02 02:58 from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('stripe', '0003_auto_20170502_1043'), ] operations = [ migrations.AddField( model_name='stripecard', name='reusable', field=models.BooleanField(default=True), preserve_default=False, ), migrations.AddField( model_name='stripecard', name='token_id', field=models.CharField(blank=True, max_length=30, null=True), ), migrations.AlterField( model_name='stripecard', name='card_id', field=models.CharField(blank=True, max_length=30, null=True), ), migrations.AlterField( model_name='stripecard', name='customer_id', field=models.CharField(blank=True, max_length=30, null=True), ), ] PK!-lorikeet/extras/stripe/migrations/__init__.pyPK!D1 lorikeet/extras/stripe/models.pyimport stripe from django.core.cache import cache from django.core.exceptions import ValidationError from django.db import models from lorikeet.exceptions import PaymentError from lorikeet.models import Payment, PaymentMethod class StripeCard(PaymentMethod): token_id = models.CharField(max_length=30, blank=True, null=True) card_id = models.CharField(max_length=30, blank=True, null=True) # We store a customer token per-card rather than per-user, because we might # have a user that adds a card, then logs in afterwards, and Stripe doesn't # let us move tokens between cards. If the user _is_ logged in when creating # the card, though, we try to reuse an existing Stripe customer that belongs # to them. customer_id = models.CharField(max_length=30, blank=True, null=True) reusable = models.BooleanField() def clean(self): super().clean() errors = {} if self.reusable: if self.token_id is not None: errors['token_id'] = ValidationError( "token_id cannot be set if save is true") if not self.card_id: errors['card_id'] = ValidationError( "card_id must be set if save is true") if not self.customer_id: errors['customer_id'] = ValidationError( "customer_id must be set if save is true") else: if not self.token_id: errors['token_id'] = ValidationError( "token_id must be set if save is false") if self.card_id is not None: errors['card_id'] = ValidationError( "card_id cannot be set if save is false") if self.customer_id is not None: errors['customer_id'] = ValidationError( "customer_id cannot be set if save is false") if errors: raise ValidationError(errors) @property def data(self): """Returns the corresponding customer object from the Stripe API""" cache_key = 'w4yl.apps.stripe:card_data:{}'.format( self.card_id or self.token_id) card = cache.get(cache_key) if card is None: if self.reusable: customer = stripe.Customer.retrieve(self.customer_id) card = customer.sources.retrieve(self.card_id) else: token = stripe.Token.retrieve(self.token_id) card = token['card'] cache.set(cache_key, card, 3600) return card def make_payment(self, order, amount): if not self.reusable: self.active = False self.save() try: chg = stripe.Charge.create( amount=int(amount * 100), currency='AUD', customer=self.customer_id, source=self.card_id or self.token_id, description='Lorikeet Order {}'.format(order.invoice_id), metadata={ 'is_lorikeet_order': True, 'lorikeet_order_id': order.id, 'invoice_id': order.invoice_id, 'user_id': order.user_id, 'email': order.email, } ) except stripe.error.CardError as e: raise PaymentError(e.json_body['error']) else: return StripePayment.objects.create(method=self, charge_id=chg['id']) class StripePayment(Payment): charge_id = models.CharField(max_length=30) PK!0!||lorikeet/extras/stripe/tests.pyfrom datetime import date from json import dumps, loads import pytest import stripe from django.conf import settings from . import models pytestmark = pytest.mark.skipif( not settings.STRIPE_API_KEY or not settings.STRIPE_API_KEY.startswith('sk_test_'), reason="A Stripe test API key is required to run tests") @pytest.fixture def card_id(): return stripe.Token.create(card={ 'number': '4242424242424242', 'exp_month': 12, 'exp_year': date.today().year + 1, 'cvc': '123', })['id'] @pytest.fixture def card_id_no_charge(): return stripe.Token.create(card={ 'number': '4000000000000341', 'exp_month': 12, 'exp_year': date.today().year + 1, 'cvc': '123', })['id'] def create_card_obj(card_id, reusable=True): if reusable: customer = stripe.Customer.create() card = customer.sources.create(source=card_id) return models.StripeCard.objects.create(customer_id=customer['id'], card_id=card['id'], reusable=True) else: return models.StripeCard.objects.create(token_id=card_id, reusable=False) @pytest.fixture def card_obj(card_id): return create_card_obj(card_id) @pytest.fixture def card_obj_single_use(card_id): return create_card_obj(card_id, reusable=False) @pytest.fixture def card_obj_no_charge(card_id_no_charge): return create_card_obj(card_id_no_charge) @pytest.mark.django_db def test_add_stripe_card(client, cart, card_id): resp = client.post('/_cart/new-payment-method/', dumps({ 'type': "StripeCard", 'data': { 'token': card_id, 'reusable': True, }, }), content_type='application/json') assert resp.status_code == 201 assert models.StripeCard.objects.count() == 1 card_obj = models.StripeCard.objects.first() assert card_obj.token_id is None assert card_obj.card_id assert card_obj.customer_id assert card_obj.data['last4'] == '4242' assert card_obj.data['brand'] == "Visa" @pytest.mark.django_db def test_add_stripe_card_single_use(client, cart, card_id): resp = client.post('/_cart/new-payment-method/', dumps({ 'type': "StripeCard", 'data': { 'token': card_id, 'reusable': False, }, }), content_type='application/json') assert resp.status_code == 201 assert models.StripeCard.objects.count() == 1 card_obj = models.StripeCard.objects.first() assert card_obj.token_id assert card_obj.card_id is None assert card_obj.customer_id is None assert card_obj.data['last4'] == '4242' assert card_obj.data['brand'] == "Visa" @pytest.mark.django_db def test_checkout_with_stripe(client, filled_cart, card_obj): filled_cart.payment_method = card_obj filled_cart.save() resp = client.post('/_cart/checkout/', dumps({}), content_type='application/json') assert resp.status_code == 200 assert models.StripePayment.objects.count() == 1 card_obj.refresh_from_db() assert card_obj.active @pytest.mark.django_db def test_checkout_with_stripe_single_use(client, filled_cart, card_obj_single_use): filled_cart.payment_method = card_obj_single_use filled_cart.save() resp = client.post('/_cart/checkout/', dumps({}), content_type='application/json') assert resp.status_code == 200 assert models.StripePayment.objects.count() == 1 card_obj_single_use.refresh_from_db() assert not card_obj_single_use.active @pytest.mark.django_db def test_checkout_with_stripe_charge_fails(client, filled_cart, card_obj_no_charge): filled_cart.payment_method = card_obj_no_charge filled_cart.save() resp = client.post('/_cart/checkout/', dumps({}), content_type='application/json') assert resp.status_code == 422 data = loads(resp.content.decode('utf8')) assert data['info']['type'] == "card_error" assert data['info']['code'] == "card_declined" assert data['info']['message'] == "Your card was declined." PK!&1 1 lorikeet/generic_views.pyimport logging from django.contrib.auth.mixins import LoginRequiredMixin from django.core.signing import BadSignature from django.http import Http404, HttpResponseRedirect from django.views.generic import DetailView, ListView from . import models from .settings import order_url_signer logger = logging.getLogger(__name__) ORDER_SESSION_KEY_TEMPLATE = 'lorikeet-order-{}-access' class OrderDetailView(DetailView): """A view that displays a single order.""" model = models.Order def get(self, request, *args, **kwargs): # Remove the order access token from the URL before we ever display # a page, to avoid leaking it to third-party services; see also # https://robots.thoughtbot.com/is-your-site-leaking-password-reset-links # This token is not quite as high-value as a password reset token, # but we should probably do the right thing anyway. if 'token' in request.GET: new_query = request.GET.copy() token = new_query.pop('token') try: order_id = order_url_signer.unsign(token[0]) except BadSignature: # A 404 page with the token in the URL could still be dangerous # (in case the token is mostly correct but not entirely), so # redirect to the non-tokenized URL w/o setting the session # so that it 404s there instead. logger.debug("Bad signature: %r", token[0]) else: session_key = ORDER_SESSION_KEY_TEMPLATE.format(order_id) request.session[session_key] = True if new_query: return HttpResponseRedirect('{}?{}'.format( request.path, new_query.urlencode() )) else: return HttpResponseRedirect(request.path) return super().get(request, *args, **kwargs) def get_object(self): the_id = self.kwargs['id'] session_key = ORDER_SESSION_KEY_TEMPLATE.format(the_id) if session_key in self.request.session: return models.Order.objects.get(id=the_id) elif self.request.user.is_authenticated(): return models.Order.objects.get( user=self.request.user, id=the_id, ) else: raise Http404() class OrderListView(LoginRequiredMixin, ListView): """A view that displays a list of orders belonging to a user.""" def get_queryset(self): return models.Order.objects.filter( user=self.request.user).order_by('-id') PK!Zlorikeet/middleware.pyfrom django.middleware.csrf import get_token from .models import Cart from .settings import LORIKEET_SET_CSRFTOKEN_EVERYWHERE def cart_getter_factory(request): def get_cart(): if hasattr(request, '_cart'): return request._cart cart = None if request.user.is_authenticated(): cart, _ = Cart.objects.get_or_create(user=request.user) elif 'cart_id' in request.session: try: cart = Cart.objects.get(id=request.session['cart_id']) except Cart.DoesNotExist: pass if cart is None: # user is definitely not logged in, might be a new user with no cart, # or might be a user with a stale cart that got deleted cart = Cart.objects.create() request.session['cart_id'] = cart.id # save the cart on the request over the top of this function, # so we don't have to look it up again request._cart = cart # perform some sanity checks on the cart assert bool(cart.user) == request.user.is_authenticated() cart_dirty = False if cart.payment_method is not None and not cart.payment_method.active: cart.payment_method = None cart_dirty = True if cart.delivery_address is not None and not cart.delivery_address.active: cart.delivery_address = None cart_dirty = True if cart_dirty: cart.save() return cart return get_cart class CartMiddleware: def process_request(self, request): request.get_cart = cart_getter_factory(request) if LORIKEET_SET_CSRFTOKEN_EVERYWHERE: get_token(request) # Forces setting the CSRF cookie PK!P† #lorikeet/migrations/0001_initial.py# -*- coding: utf-8 -*- # Generated by Django 1.9.9 on 2016-12-13 01:24 from __future__ import unicode_literals from django.conf import settings from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): initial = True dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( name='Cart', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], ), migrations.CreateModel( name='DeliveryAddress', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], ), migrations.CreateModel( name='LineItem', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='lorikeet.Cart')), ], ), migrations.CreateModel( name='Order', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('custom_invoice_id', models.CharField(max_length=255)), ('guest_email', models.EmailField(blank=True, max_length=254)), ('delivery_address', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lorikeet.DeliveryAddress')), ], ), migrations.CreateModel( name='Payment', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], ), migrations.CreateModel( name='PaymentMethod', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], ), migrations.AddField( model_name='payment', name='method', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lorikeet.PaymentMethod'), ), migrations.AddField( model_name='order', name='payment', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lorikeet.Payment'), ), migrations.AddField( model_name='order', name='user', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), ), migrations.AddField( model_name='lineitem', name='order', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='lorikeet.Order'), ), ] PK!, ;44.lorikeet/migrations/0002_auto_20161213_1158.py# -*- coding: utf-8 -*- # Generated by Django 1.9.9 on 2016-12-13 01:28 from __future__ import unicode_literals from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ ('lorikeet', '0001_initial'), ] operations = [ migrations.AlterField( model_name='lineitem', name='cart', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='items', to='lorikeet.Cart'), ), migrations.AlterField( model_name='lineitem', name='order', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='items', to='lorikeet.Order'), ), ] PK!nɛ.lorikeet/migrations/0003_auto_20161220_1313.py# -*- coding: utf-8 -*- # Generated by Django 1.9.9 on 2016-12-20 02:43 from __future__ import unicode_literals from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('lorikeet', '0002_auto_20161213_1158'), ] operations = [ migrations.AlterModelOptions( name='lineitem', options={'ordering': ('id',)}, ), ] PK!Dݑvv.lorikeet/migrations/0004_auto_20161220_1431.py# -*- coding: utf-8 -*- # Generated by Django 1.9.9 on 2016-12-20 04:01 from __future__ import unicode_literals from django.conf import settings from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ ('lorikeet', '0003_auto_20161220_1313'), ] operations = [ migrations.AddField( model_name='cart', name='delivery_address', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='lorikeet.DeliveryAddress'), ), migrations.AlterField( model_name='deliveryaddress', name='user', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='delivery_addresses', to=settings.AUTH_USER_MODEL), ), ] PK!I;;/lorikeet/migrations/0005_cart_payment_method.py# -*- coding: utf-8 -*- # Generated by Django 1.9.9 on 2017-01-04 22:44 from __future__ import unicode_literals from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ ('lorikeet', '0004_auto_20161220_1431'), ] operations = [ migrations.AddField( model_name='cart', name='payment_method', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='lorikeet.PaymentMethod'), ), ] PK!.0lorikeet/migrations/0006_paymentmethod_active.py# -*- coding: utf-8 -*- # Generated by Django 1.9.9 on 2017-01-12 03:15 from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('lorikeet', '0005_cart_payment_method'), ] operations = [ migrations.AddField( model_name='paymentmethod', name='active', field=models.BooleanField(default=True), ), ] PK!2lorikeet/migrations/0007_deliveryaddress_active.py# -*- coding: utf-8 -*- # Generated by Django 1.9.9 on 2017-01-17 00:41 from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('lorikeet', '0006_paymentmethod_active'), ] operations = [ migrations.AddField( model_name='deliveryaddress', name='active', field=models.BooleanField(default=True), ), ] PK!]X~j,,.lorikeet/migrations/0008_auto_20170117_0453.py# -*- coding: utf-8 -*- # Generated by Django 1.9.9 on 2017-01-17 04:53 from __future__ import unicode_literals from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ ('lorikeet', '0007_deliveryaddress_active'), ] operations = [ migrations.AlterField( model_name='order', name='delivery_address', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='lorikeet.DeliveryAddress'), ), migrations.AlterField( model_name='order', name='payment', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='lorikeet.Payment'), ), ] PK!@r(Sgg.lorikeet/migrations/0009_auto_20170306_1150.py# -*- coding: utf-8 -*- # Generated by Django 1.9.9 on 2017-03-06 01:20 from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('lorikeet', '0008_auto_20170117_0453'), ] operations = [ migrations.AlterField( model_name='order', name='custom_invoice_id', field=models.CharField(blank=True, default=None, max_length=255, null=True), ), migrations.RunSQL([""" UPDATE lorikeet_order SET custom_invoice_id=NULL WHERE custom_invoice_id='' """]), migrations.AlterField( model_name='order', name='custom_invoice_id', field=models.CharField(blank=True, default=None, max_length=255, null=True, unique=True), ), ] PK!uй-lorikeet/migrations/0010_order_grand_total.py# -*- coding: utf-8 -*- # Generated by Django 1.9.9 on 2017-03-06 01:32 from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('lorikeet', '0009_auto_20170306_1150'), ] operations = [ migrations.AddField( model_name='order', name='grand_total', field=models.DecimalField(decimal_places=2, default=0, max_digits=7), preserve_default=False, ), ] PK!>GG7lorikeet/migrations/0011_lineitem_total_when_charged.py# -*- coding: utf-8 -*- # Generated by Django 1.9.9 on 2017-03-06 01:41 from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('lorikeet', '0010_order_grand_total'), ] operations = [ migrations.AddField( model_name='lineitem', name='total_when_charged', field=models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True), ), # migrations.RunSQL([""" # ALTER TABLE lorikeet_lineitem # ADD CONSTRAINT total_when_charged_only_set_on_order CHECK ( # (order_id IS NULL AND total_when_charged IS NULL) # OR (order_id IS NOT NULL AND total_when_charged IS NOT NULL) # ) # """], []) ] PK! &lorikeet/migrations/0012_cart_email.py# -*- coding: utf-8 -*- # Generated by Django 1.9.9 on 2017-03-07 01:13 from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('lorikeet', '0011_lineitem_total_when_charged'), ] operations = [ migrations.AddField( model_name='cart', name='email', field=models.EmailField(blank=True, max_length=254, null=True), ), ] PK!j .lorikeet/migrations/0013_auto_20170307_1512.py# -*- coding: utf-8 -*- # Generated by Django 1.9.9 on 2017-03-07 04:42 from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('lorikeet', '0012_cart_email'), ] operations = [ migrations.AlterField( model_name='order', name='guest_email', field=models.EmailField(blank=True, max_length=254, null=True), ), ] PK!}.lorikeet/migrations/0014_order_purchased_on.py# -*- coding: utf-8 -*- # Generated by Django 1.9.9 on 2017-04-10 04:16 from __future__ import unicode_literals from django.db import migrations, models import django.utils.timezone class Migration(migrations.Migration): dependencies = [ ('lorikeet', '0013_auto_20170307_1512'), ] operations = [ migrations.AddField( model_name='order', name='purchased_on', field=models.DateTimeField(default=django.utils.timezone.now), ), ] PK!lorikeet/migrations/__init__.pyPK!"7 $,$,lorikeet/models.pyfrom django.conf import settings from django.core.urlresolvers import reverse from django.db import models from django.utils.module_loading import import_string from django.utils.timezone import now from model_utils.managers import InheritanceManager from . import settings as lorikeet_settings from . import exceptions class Cart(models.Model): """An in-progress shopping cart. Carts are associated with the user for an authenticated request, or with the session otherwise; in either case it can be accessed on ``request.cart``. """ user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True) email = models.EmailField(blank=True, null=True) delivery_address = models.ForeignKey( 'lorikeet.DeliveryAddress', blank=True, null=True) payment_method = models.ForeignKey( 'lorikeet.PaymentMethod', blank=True, null=True) def get_grand_total(self): """Calculate the grand total for this cart.""" return sum(x.get_total() for x in self.items.select_subclasses().all()) @property def delivery_address_subclass(self): """Get the delivery address instance selected for this cart. Returns an instance of one of the registered :class:`~lorikeet.models.DeliveryAddress` subclasses. """ if self.delivery_address_id is not None: return DeliveryAddress.objects.get_subclass( id=self.delivery_address_id) @property def payment_method_subclass(self): """Get the payment method instance selected for this cart. Returns an instance of one of the registered :class:`~lorikeet.models.PaymentMethod` subclasses. """ if self.payment_method_id is not None: return PaymentMethod.objects.get_subclass( id=self.payment_method_id) def is_complete(self, raise_exc=False, for_checkout=False): """Determine if this cart is able to be checked out. If this function returns ``False``, the ``.errors`` attribute will be set to a :class:`~lorikeet.exceptions.IncompleteCartErrorSet` containing all of the reasons the cart cannot be checked out. :param raise_exc: If ``True`` and there are errors, raise the resulting :class:`~lorikeet.exceptions.IncompleteCartErrorSet` instead of just returning ``False``. :type raise_exc: bool :return: Whether this cart can be checked out. :rtype: bool """ # Use the .errors attribute to effectively memoize this function if not hasattr(self, 'errors'): self.errors = exceptions.IncompleteCartErrorSet() for checker in lorikeet_settings.LORIKEET_CART_COMPLETE_CHECKERS: checker_func = import_string(checker) try: checker_func(self) except exceptions.IncompleteCartError as e: self.errors.add(e) for item in self.items.all().select_subclasses(): try: item.check_complete(for_checkout) except exceptions.IncompleteCartError as e: self.errors.add(e) if raise_exc and self.errors: raise self.errors return not bool(self.errors) class Order(models.Model): """A completed, paid order. """ custom_invoice_id = models.CharField( max_length=255, blank=True, null=True, default=None, unique=True) user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True) guest_email = models.EmailField(blank=True, null=True) payment = models.ForeignKey('lorikeet.Payment', blank=True, null=True) delivery_address = models.ForeignKey( 'lorikeet.DeliveryAddress', blank=True, null=True) grand_total = models.DecimalField(max_digits=7, decimal_places=2) purchased_on = models.DateTimeField(default=now) @property def email(self): return self.user.email if self.user is not None else self.guest_email @property def invoice_id(self): """The ID of the invoice. If custom_invoice_id is set, it will be returned. Otherwise, the PK of the order object will be returned. """ return self.custom_invoice_id or self.id @property def delivery_address_subclass(self): """Get the delivery address instance selected for this cart. Returns an instance of one of the registered :class:`~lorikeet.models.DeliveryAddress` subclasses. """ if self.delivery_address_id is not None: return DeliveryAddress.objects.get_subclass( id=self.delivery_address_id) @property def payment_method_subclass(self): """Get the delivery address instance selected for this cart. Returns an instance of one of the registered :class:`~lorikeet.models.DeliveryAddress` subclasses. """ return PaymentMethod.objects.get_subclass( id=self.payment.method_id) @property def payment_subclass(self): """Get the payment method instance selected for this cart. Returns an instance of one of the registered :class:`~lorikeet.models.PaymentMethod` subclasses. """ return Payment.objects.get_subclass( id=self.payment_id) def get_absolute_url(self, token=False): """Get the absolute URL of an order details view. :param token: If true, include in the URL a token that allows unauthenticated access to the detail view. :type token: bool See the documentation for the ``LORIKEET_ORDER_DETAIL_VIEW`` setting. """ if lorikeet_settings.LORIKEET_ORDER_DETAIL_VIEW: url = reverse(lorikeet_settings.LORIKEET_ORDER_DETAIL_VIEW, kwargs={'id': self.id}) if token: url = "{}?token={}".format( url, lorikeet_settings.order_url_signer.sign(str(self.id))) return url class PaymentMethod(models.Model): """A payment method, like a credit card or bank details. This model doesn't do anything by itself; you'll need to subclass it as described in the :doc:`Getting Started Guide `. """ user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True) active = models.BooleanField(default=True) objects = InheritanceManager() def make_payment(self, order, amount): raise NotImplementedError("Provide a make_payment method in your " "PaymentMethod subclass {}.".format( self.__class__.__name__)) def assign_to_user(self, user): self.user = user class Payment(models.Model): method = models.ForeignKey(PaymentMethod) class DeliveryAddress(models.Model): """An address that an order can be delivered to. This model doesn't do anything by itself; you'll need to subclass it as described in the :doc:`Getting Started Guide `. """ user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name='delivery_addresses') active = models.BooleanField(default=True) objects = InheritanceManager() class LineItem(models.Model): """An individual item that is either in a shopping cart or on an order. This model doesn't do anything by itself; you'll need to subclass it as described in the :doc:`Getting Started Guide `. """ cart = models.ForeignKey(Cart, related_name='items', blank=True, null=True) order = models.ForeignKey( Order, related_name='items', blank=True, null=True) total_when_charged = models.DecimalField(max_digits=7, decimal_places=2, blank=True, null=True) objects = InheritanceManager() class Meta: # Because IDs auto increment, ordering by ID has the same effect as # ordering by date added, but we don't have to store the date ordering = ('id',) @property def total(self): """The total cost for this line item. Returns the total actually charged to the customer if this item is attached to an :class:`~lorikeet.models.Order`, or calls :func:`~lorikeet.models.LineItem.get_total` otherwise. """ if self.order_id: return self.total_when_charged return self.get_total() def get_total(self): """Returns the total amount to charge on this LineItem. By default this raises ``NotImplemented``; subclasses of this class need to override this. If you want to know the total for this line item from your own code, use the :func:`~lorikeet.models.LineItem.total` property rather than calling this function. """ raise NotImplemented("Provide a get_total method in your LineItem " "subclass {}.".format(self.__class__.__name__)) def save(self, *args, **kwargs): if self.order is not None and not getattr(self, '_new_order'): raise ValueError("Cannot modify a cart item attached to an order.") return super().save(*args, **kwargs) def check_complete(self, for_checkout=False): """Checks that this line item is ready to be checked out. This method should raise :class:`~lorikeet.exceptions.IncompleteCartError` if the line item is not ready to be checked out (e.g. there is insufficient stock in inventory to fulfil this line item). By default it does nothing. :param for_checkout: Set to ``True`` when the cart is about to be checked out. See the documentation for :meth:`prepare_for_checkout` for more details. is going to be called within the current transaction, so you should use things like `select_for_update `_. :type for_checkout: bool """ pass def prepare_for_checkout(self): """Prepare this line item for checkout. This is called in the checkout process, shortly before the payment method is charged, within a database transaction that will be rolled back if payment is unsuccessful. This function shouldn't fail. (If it does, the transaction will be rolled back and the payment won't be processed so nothing disastrous will happen, but the user will get a 500 error which you probably don't want.) The :meth:`check_complete` method is guaranteed to be called shortly before this method, within the same transaction, and with the ``for_checkout`` parameter set to ``True``. Any checks you need to perform to ensure checkout will succeed should be performed there, and when ``for_checkout`` is true there you should ensure that those checks remain valid for the remainder of the database transaction (e.g. using `select_for_update `_). """ pass PK!uu(lorikeet/models_test_completion_check.pyimport pytest from .exceptions import IncompleteCartErrorSet @pytest.mark.django_db def test_default_check_succeeds_cart_full(filled_cart): result = filled_cart.is_complete() assert not filled_cart.errors assert result @pytest.mark.django_db def test_default_check_fails_cart_empty(cart): result = cart.is_complete() assert cart.errors assert not result assert cart.errors.to_json() == [ { 'code': 'not_set', 'field': 'delivery_address', 'message': 'A delivery address is required.', }, { 'code': 'not_set', 'field': 'payment_method', 'message': 'A payment method is required.', }, { 'code': 'empty', 'field': 'items', 'message': 'There are no items in the cart.', }, { 'code': 'not_set', 'field': 'email', 'message': 'An email address is required.', }, ] @pytest.mark.django_db def test_raise_exc(cart): with pytest.raises(IncompleteCartErrorSet): cart.is_complete(raise_exc=True) PK!Rlorikeet/settings.pyfrom django.conf import settings from django.core.signing import Signer LORIKEET_CART_COMPLETE_CHECKERS = getattr( settings, 'LORIKEET_CART_COMPLETE_CHECKERS', [ 'lorikeet.cart_checkers.delivery_address_required', 'lorikeet.cart_checkers.payment_method_required', 'lorikeet.cart_checkers.cart_not_empty', 'lorikeet.cart_checkers.email_address_if_anonymous', ] ) LORIKEET_ORDER_DETAIL_VIEW = getattr( settings, 'LORIKEET_ORDER_DETAIL_VIEW', None ) LORIKEET_SET_CSRFTOKEN_EVERYWHERE = getattr( settings, 'LORIKEET_SET_CSRFTOKEN_EVERYWHERE', True ) LORIKEET_INVOICE_ID_GENERATOR = getattr( settings, 'LORIKEET_INVOICE_ID_GENERATOR', None ) order_url_signer = Signer( salt='au.com.cmv.open-source.lorikeet.order-url-signer') PK!7SSlorikeet/signal_handlers.pyfrom django.contrib.auth.signals import user_logged_in from django.dispatch import receiver from . import models @receiver(user_logged_in) def merge_carts(sender, user, request, **kwargs): # Try to find the session's cart. If there isn't one, we return; # there's nothing to merge. if 'cart_id' in request.session: try: session_cart = models.Cart.objects.get( id=request.session['cart_id']) except models.Cart.DoesNotExist: return else: return # Try to find the user's cart. try: user_cart = models.Cart.objects.get(user=user) except models.Cart.DoesNotExist: # User doesn't have a cart but the session does, so just assign # that one to the user. session_cart.user = user session_cart.save() request._cart = session_cart else: # Now we have to merge things properly. for item in session_cart.items.all(): # TODO: Some sort of check to see if items can be combined. item.cart = user_cart item.save() if session_cart.delivery_address_id: addr = session_cart.delivery_address addr.user = user addr.save() user_cart.delivery_address = addr if session_cart.payment_method_id: method = session_cart.payment_method method.user = user method.save() user_cart.payment_method = method user_cart.save() session_cart.delete() del request.session['cart_id'] request._cart = user_cart PK!M˲} } lorikeet/signal_handlers_test.pyfrom django.contrib.sessions.middleware import SessionMiddleware from . import signal_handlers as handlers from . import models def with_session(request): """Annotate a request object with a session""" middleware = SessionMiddleware() middleware.process_request(request) request.session.save() return request def test_merge_carts_both_empty(admin_user, rf): request = with_session(rf.post('/')) handlers.merge_carts(sender=admin_user.__class__, user=admin_user, request=request) assert not models.Cart.objects.count() def test_merge_carts_user_only(admin_user, admin_cart, rf): request = with_session(rf.post('/')) handlers.merge_carts(sender=admin_user.__class__, user=admin_user, request=request) assert models.Cart.objects.count() == 1 def test_merge_carts_session_only(admin_user, cart, rf): request = with_session(rf.post('/')) request.session['cart_id'] = cart.id handlers.merge_carts(sender=admin_user.__class__, user=admin_user, request=request) assert models.Cart.objects.count() == 1 cart.refresh_from_db() assert cart.user == admin_user def test_merge_empty_carts(admin_user, admin_cart, cart, rf): request = with_session(rf.post('/')) request.session['cart_id'] = cart.id handlers.merge_carts(sender=admin_user.__class__, user=admin_user, request=request) assert models.Cart.objects.count() == 1 admin_cart.refresh_from_db() assert admin_cart.id assert 'cart_id' not in request.session def test_merge_filled_carts(admin_user, filled_admin_cart, filled_cart, rf): request = with_session(rf.post('/')) request.session['cart_id'] = filled_cart.id user_cart_item_count = filled_admin_cart.items.count() session_cart_item_count = filled_cart.items.count() session_address_id = filled_cart.delivery_address_id session_payment_id = filled_cart.payment_method_id handlers.merge_carts(sender=admin_user.__class__, user=admin_user, request=request) assert models.Cart.objects.count() == 1 filled_admin_cart.refresh_from_db() assert filled_admin_cart.id assert 'cart_id' not in request.session assert filled_admin_cart.items.count() == (user_cart_item_count + session_cart_item_count) assert filled_admin_cart.delivery_address_id == session_address_id assert filled_admin_cart.payment_method_id == session_payment_id PK!ghddlorikeet/signals.pyfrom django.dispatch import Signal order_checked_out = Signal(providing_args=['order', 'request']) PK!!lorikeet/templatetags/__init__.pyPK!wu;;!lorikeet/templatetags/lorikeet.pyfrom json import dumps from django import template from .. import api_serializers register = template.Library() @register.simple_tag(takes_context=True) def lorikeet_cart(context): """Returns the current state of the user's cart. Returns a JSON string of the same shape as a response from :http:get:`/_cart/`. Requires that the current request be in the template's context. """ cart = context['request'].get_cart() data = api_serializers.CartSerializer( cart, context={'request': context['request']}).data return dumps(data) PK! 'lorikeet/urls.pyfrom django.conf.urls import url from . import api_views urlpatterns = [ url(r'^$', api_views.CartView.as_view(), name='cart'), url(r'^(?P\d+)/$', api_views.CartItemView.as_view(), name='cart-item'), url(r'^new/$', api_views.AddToCartView.as_view(), name='add-to-cart'), url(r'^address/(?P\d+)/$', api_views.DeliveryAddressView.as_view(), name='address'), url(r'^new-address/$', api_views.NewAddressView.as_view(), name='new-address'), url(r'^payment-method/(?P\d+)/$', api_views.PaymentMethodView.as_view(), name='payment-method'), url(r'^new-payment-method/$', api_views.NewPaymentMethodView.as_view(), name='new-payment-method'), url(r'^checkout/$', api_views.CheckoutView.as_view(), name='checkout'), ] PK!'p>66 lorikeet-0.1.7.dist-info/LICENSECopyright 2016-2017 Commercial Motor Vehicles Pty Ltd Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!H_zTTlorikeet-0.1.7.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]n0H*J>mlcAPK!H-A!lorikeet-0.1.7.dist-info/METADATAWKr6%e3bV@MUpUt 1|_W#3F#+?/,^sP j3;Y"y^Qm2dQvtMܰWu֜RɆSzҚz _ٶզ\@u[XGoUOeJBh|9VEϴ=OtU*P voO]LuvOɛZy7rCj[g︐gD_l) uH ɷK규O<?$spjE_櫷>(+p-;V"c؂et黗g'٣Nz}DN<;qu1gv%x?>'G=r0V//OA}O\& ˏC )^:j$b^-4ې( Aj@yTT e 4Qv.Y"arPtGedAAumk"sXngOt;yYJFKk>,~ں+Y,]7=ʩ #~|}逍ATn]\kHE9WB gvi-Yun@54~T21oB/j#c֝Pf"ݼ4 }E0.MYm1PvdzƇT$gQ+ }T "c%)(e>.#Ÿr)vX]W{*tlys8ꭘYhg"y\L{6 r j˱q6"0%xwf )7X&`* |vHH q'K_.GqӜJxށ-k'u$q~c%g!A#&=b-J rHB0-*]X 󲫕-f)~@7o#SB$qnJ(`bPK!Hԕ$ alorikeet-0.1.7.dist-info/RECORDIHֆ[ |V@T i\{yk2x(6ͣ*=fFIQ70,M2!(#th#\Wߤ^_Vz\IkQ04!L!2p I3ƵQ$ݪZ l^[ j;Q,܅#G]B Ft|;7k`/#uǀ.*DhIz7h?s쐨c3t43 z{c4 nIta}r_9Ĺ8\ZUݑG88n' 3NèHQ;{~QEl4imXİUip/ygbyt4@R.ʨOpEMW'әWISvXOm9V2;qKO`WV/ѼVl|nvʤJ?.ÕAQ!lt P>J͓vёewܒN%J/`dMB gYkhEMՇv&V$cm;Q YsArBp!5qeA+P Ҷ:gvΝ驟^UdW҂h6zGtPr3ֹDb > FNoW d}ڭ4Oܠ&y~`);=@nX}^|%7gl,S;:`߈wQߧUtBZQ V;\@sH_4K*,>XTkq~Թӛx5GqGЯ&ΞoT;Mn:ԏK;O  C뗯}^)۽w#{>*SqP?7KS, ~%i~3Ǯ4ny'a;ii+ǦWs{&McSgW)iLpJb"+N+޺;^*$V`mdySNqXaB~J2][)De>x N>sB ٚ1Zy8X6S](v @t6_֤ΞsKfAlg۴o)?&|xI=Ϻ!pV͘vb\v0Af"I@QHj]gj=G}\٢͇UNDO~_g A0 BX&_5C_-c3jQXgBo/CE[l\v +"-lۇ>*#% j^ԲF ɾr?W$l.Ę\Y#46>|}_j̪@{$&H"u-wߗUڂ$@G`N!s+g7"€vE!W\ g۹΋Qi j3Njp9CC>[K5&6:6ZoV^_4Pj\qIݲ^Ƒmlw7 Ҽ6kA$oh[;P1=JtP0!9?}^^{hq)Z]g en{qqA 2ć N|LfWm'WT`}5l~rrRwg\]ީBVP]-~ I>ˌnj=^/q_R{}Sl=zXhѤ'V,XU !}."v(VVt79֫m7nO#Sa&cAm}]w$5d q̤RHȇ3LSZ# {)&+Zkzٲa4^QDQK֐DsҮj$])I W!kmYEދ#߄=2ٽ,A6Q7 "℥Ta{l% ȿPK!77lorikeet/__init__.pyPK!wU$$ilorikeet/api_serializers.pyPK!1""%lorikeet/api_views.pyPK!N7MGlorikeet/api_views_test.pyPK!z%+glorikeet/api_views_test_cart_items.pyPK!>MY Y #plorikeet/api_views_test_checkout.pyPK!HH-ylorikeet/api_views_test_delivery_addresses.pyPK!3))Jlorikeet/api_views_test_payment_method.pyPK!>R{lorikeet/apps.pyPK!f%ilorikeet/cart_checkers.pyPK!M nlorikeet/conftest.pyPK!VG Blorikeet/exceptions.pyPK!iZZWlorikeet/exceptions_test.pyPK!lorikeet/extras/__init__.pyPK!OPP)#lorikeet/extras/email_invoice/__init__.pyPK!7%lorikeet/extras/email_invoice/apps.pyPK!vv)lorikeet/extras/email_invoice/settings.pyPK!&Σ{0ulorikeet/extras/email_invoice/signal_handlers.pyPK!.?##5ilorikeet/extras/email_invoice/signal_handlers_test.pyPK!y(lorikeet/extras/email_invoice/textify.pyPK!&lorikeet/extras/starshipit/__init__.pyPK!su5 lorikeet/extras/starshipit/migrations/0001_initial.pyPK!18lorikeet/extras/starshipit/migrations/__init__.pyPK!Rp$lorikeet/extras/starshipit/models.pyPK!ܾj>>$lorikeet/extras/starshipit/submit.pyPK!eo#lorikeet/extras/starshipit/tasks.pyPK!ʷ"lorikeet/extras/stripe/__init__.pyPK!蝚)@lorikeet/extras/stripe/api_serializers.pyPK!G1QQ1|lorikeet/extras/stripe/migrations/0001_initial.pyPK!B  7lorikeet/extras/stripe/migrations/0002_stripepayment.pyPK!b8II<}lorikeet/extras/stripe/migrations/0003_auto_20170502_1043.pyPK!7Rl  < lorikeet/extras/stripe/migrations/0004_auto_20170502_1228.pyPK!-lorikeet/extras/stripe/migrations/__init__.pyPK!D1 lorikeet/extras/stripe/models.pyPK!0!||-lorikeet/extras/stripe/tests.pyPK!&1 1 lorikeet/generic_views.pyPK!ZNlorikeet/middleware.pyPK!P† #]%lorikeet/migrations/0001_initial.pyPK!, ;44.$3lorikeet/migrations/0002_auto_20161213_1158.pyPK!nɛ.6lorikeet/migrations/0003_auto_20161220_1313.pyPK!Dݑvv.8lorikeet/migrations/0004_auto_20161220_1431.pyPK!I;;/M<lorikeet/migrations/0005_cart_payment_method.pyPK!.0>lorikeet/migrations/0006_paymentmethod_active.pyPK!2@lorikeet/migrations/0007_deliveryaddress_active.pyPK!]X~j,,. Clorikeet/migrations/0008_auto_20170117_0453.pyPK!@r(Sgg.Florikeet/migrations/0009_auto_20170306_1150.pyPK!uй-7Jlorikeet/migrations/0010_order_grand_total.pyPK!>GG7Llorikeet/migrations/0011_lineitem_total_when_charged.pyPK! &&Plorikeet/migrations/0012_cart_email.pyPK!j .JRlorikeet/migrations/0013_auto_20170307_1512.pyPK!}.nTlorikeet/migrations/0014_order_purchased_on.pyPK!Vlorikeet/migrations/__init__.pyPK!"7 $,$,Vlorikeet/models.pyPK!uu(Florikeet/models_test_completion_check.pyPK!Rlorikeet/settings.pyPK!7SSAlorikeet/signal_handlers.pyPK!M˲} } ͑lorikeet/signal_handlers_test.pyPK!ghddlorikeet/signals.pyPK!!lorikeet/templatetags/__init__.pyPK!wu;;!\lorikeet/templatetags/lorikeet.pyPK! '֟lorikeet/urls.pyPK!'p>66 lorikeet-0.1.7.dist-info/LICENSEPK!H_zTTlorikeet-0.1.7.dist-info/WHEELPK!H-A!!lorikeet-0.1.7.dist-info/METADATAPK!Hԕ$ aplorikeet-0.1.7.dist-info/RECORDPKAAѺ