Skip to content

Commit

Permalink
Merge pull request #216 from jrief/add-data-to-extra-price-fields
Browse files Browse the repository at this point in the history
Added ``data`` as optional JSONField to extra price models.
  • Loading branch information
chrisglass committed Mar 11, 2013
2 parents c87e7c6 + 1745e1b commit 7e094c4
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 14 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Expand Up @@ -5,6 +5,9 @@ Version NEXT
* Support for Django 1.2 dropped.
* Product model now has property ``can_be_added_to_cart`` which is checked before adding the product to cart

* Cart modifiers can add an optional ``data`` field beside ``label`` and ``value``
for both, the ExtraOrderPriceField and the ExtraOrderItemPriceField model.
This extra ``data`` field can contain anything serializable as JSON.

Version 0.1.2
=============
Expand Down
3 changes: 2 additions & 1 deletion docs/getting-started.rst
Expand Up @@ -22,7 +22,8 @@ django-cbv if you're using 1.3.
pip install django-cbv # only if using django<1.3
pip install south
pip install django-shop
pip install jsonfield

.. highlight:: python

3. Go to your settings.py and configure your DB like the following, or anything
Expand Down
6 changes: 3 additions & 3 deletions setup.py
Expand Up @@ -28,10 +28,10 @@
'Django>=1.3',
'django-classy-tags>=0.3.3',
'django-polymorphic>=0.2',
'south>=0.7.2'
'south>=0.7.2',
'jsonfield>=0.9.6'
],
packages=find_packages(exclude=["example", "example.*"]),
include_package_data=True,
zip_safe = False,
zip_safe=False,
)

9 changes: 7 additions & 2 deletions shop/cart/cart_modifiers_base.py
Expand Up @@ -113,15 +113,20 @@ def get_extra_cart_item_price_field(self, cart_item):
a tuple. The decimal should be the amount that should get added to the
current subtotal. It can be a negative value.
An optional third tuple element can be used to store extra data of any
kind, which must be serializable as JSON.
In case your modifier is based on the current price (for example in
order to compute value added tax for this cart item only) your
override can access that price via ``cart_item.current_total``.
A tax modifier would do something like this:
>>> return ('taxes', Decimal(9))
>>> return ('taxes', Decimal(9), {'rate': Decimal(10), 'identifier': 'V.A.T.'})
Note that the third element in this tuple is optional.
And a rebate modifier would do something along the lines of:
>>> return ('rebate', Decimal(-9))
>>> return ('rebate', Decimal(-9), {'rate': Decimal(3), 'identifier': 'Discount'})
Note that the third element in this tuple is optional.
More examples can be found in shop.cart.modifiers.*
"""
Expand Down
150 changes: 150 additions & 0 deletions shop/migrations/0012_auto__add_field_extraorderpricefield_data.py
@@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models


class Migration(SchemaMigration):

def forwards(self, orm):
# Adding field 'ExtraOrderPriceField.data'
db.add_column('shop_extraorderpricefield', 'data',
self.gf('jsonfield.fields.JSONField')(null=True, blank=True),
keep_default=False)

# Adding field 'ExtraOrderItemPriceField.data'
db.add_column('shop_extraorderitempricefield', 'data',
self.gf('jsonfield.fields.JSONField')(null=True, blank=True),
keep_default=False)


def backwards(self, orm):
# Deleting field 'ExtraOrderPriceField.data'
db.delete_column('shop_extraorderpricefield', 'data')

# Deleting field 'ExtraOrderItemPriceField.data'
db.delete_column('shop_extraorderitempricefield', 'data')


models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'shop.cart': {
'Meta': {'object_name': 'Cart'},
'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'last_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'null': 'True', 'blank': 'True'})
},
'shop.cartitem': {
'Meta': {'object_name': 'CartItem'},
'cart': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'items'", 'to': "orm['shop.Cart']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shop.Product']"}),
'quantity': ('django.db.models.fields.IntegerField', [], {})
},
'shop.extraorderitempricefield': {
'Meta': {'object_name': 'ExtraOrderItemPriceField'},
'data': ('jsonfield.fields.JSONField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order_item': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shop.OrderItem']"}),
'value': ('django.db.models.fields.DecimalField', [], {'default': "'0.0'", 'max_digits': '30', 'decimal_places': '2'})
},
'shop.extraorderpricefield': {
'Meta': {'object_name': 'ExtraOrderPriceField'},
'data': ('jsonfield.fields.JSONField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_shipping': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shop.Order']"}),
'value': ('django.db.models.fields.DecimalField', [], {'default': "'0.0'", 'max_digits': '30', 'decimal_places': '2'})
},
'shop.order': {
'Meta': {'object_name': 'Order'},
'billing_address_text': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'cart_pk': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'order_subtotal': ('django.db.models.fields.DecimalField', [], {'default': "'0.0'", 'max_digits': '30', 'decimal_places': '2'}),
'order_total': ('django.db.models.fields.DecimalField', [], {'default': "'0.0'", 'max_digits': '30', 'decimal_places': '2'}),
'shipping_address_text': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'status': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
'shop.orderextrainfo': {
'Meta': {'object_name': 'OrderExtraInfo'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'order': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'extra_info'", 'to': "orm['shop.Order']"}),
'text': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'shop.orderitem': {
'Meta': {'object_name': 'OrderItem'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'line_subtotal': ('django.db.models.fields.DecimalField', [], {'default': "'0.0'", 'max_digits': '30', 'decimal_places': '2'}),
'line_total': ('django.db.models.fields.DecimalField', [], {'default': "'0.0'", 'max_digits': '30', 'decimal_places': '2'}),
'order': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'items'", 'to': "orm['shop.Order']"}),
'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shop.Product']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
'product_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'product_reference': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'quantity': ('django.db.models.fields.IntegerField', [], {}),
'unit_price': ('django.db.models.fields.DecimalField', [], {'default': "'0.0'", 'max_digits': '30', 'decimal_places': '2'})
},
'shop.orderpayment': {
'Meta': {'object_name': 'OrderPayment'},
'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.0'", 'max_digits': '30', 'decimal_places': '2'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shop.Order']"}),
'payment_method': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'transaction_id': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'shop.product': {
'Meta': {'object_name': 'Product'},
'active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'date_added': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'polymorphic_ctype': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'polymorphic_shop.product_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'}),
'unit_price': ('django.db.models.fields.DecimalField', [], {'default': "'0.0'", 'max_digits': '30', 'decimal_places': '2'})
}
}

complete_apps = ['shop']
3 changes: 3 additions & 0 deletions shop/models/ordermodel.py
Expand Up @@ -4,6 +4,7 @@
from django.db import models
from django.db.models.signals import pre_delete
from django.utils.translation import ugettext_lazy as _
from jsonfield.fields import JSONField
from shop.models.productmodel import Product
from shop.util.fields import CurrencyField
from shop.util.loader import load_class
Expand Down Expand Up @@ -59,6 +60,7 @@ class ExtraOrderPriceField(models.Model):
order = models.ForeignKey(Order, verbose_name=_('Order'))
label = models.CharField(max_length=255, verbose_name=_('Label'))
value = CurrencyField(verbose_name=_('Amount'))
data = JSONField(null=True, blank=True, verbose_name=_('Serialized extra data'))
# Does this represent shipping costs?
is_shipping = models.BooleanField(default=False, editable=False,
verbose_name=_('Is shipping'))
Expand All @@ -77,6 +79,7 @@ class ExtraOrderItemPriceField(models.Model):
order_item = models.ForeignKey(OrderItem, verbose_name=_('Order item'))
label = models.CharField(max_length=255, verbose_name=_('Label'))
value = CurrencyField(verbose_name=_('Amount'))
data = JSONField(null=True, blank=True, verbose_name=_('Serialized extra data'))

class Meta(object):
app_label = 'shop'
Expand Down
16 changes: 10 additions & 6 deletions shop/models_bases/managers.py
Expand Up @@ -124,11 +124,13 @@ def create_from_cart(self, cart, state=None):
order.save()

# Let's serialize all the extra price arguments in DB
for label, value in cart.extra_price_fields:
for field in cart.extra_price_fields:
eoi = ExtraOrderPriceField()
eoi.order = order
eoi.label = unicode(label)
eoi.value = value
eoi.label = unicode(field[0])
eoi.value = field[1]
if len(field) == 3:
eoi.data = field[2]
eoi.save()

# There, now move on to the order items.
Expand All @@ -146,12 +148,14 @@ def create_from_cart(self, cart, state=None):
order_item.line_subtotal = item.line_subtotal
order_item.save()
# For each order item, we save the extra_price_fields to DB
for label, value in item.extra_price_fields:
for field in item.extra_price_fields:
eoi = ExtraOrderItemPriceField()
eoi.order_item = order_item
# Force unicode, in case it has àö...
eoi.label = unicode(label)
eoi.value = value
eoi.label = unicode(field[0])
eoi.value = field[1]
if len(field) == 3:
eoi.data = field[2]
eoi.save()

processing.send(self.model, order=order, cart=cart)
Expand Down
56 changes: 54 additions & 2 deletions shop/tests/order.py
Expand Up @@ -4,10 +4,11 @@
from django.contrib.auth.models import User
from django.test.testcases import TestCase
from shop.cart.modifiers_pool import cart_modifiers_pool
from shop.cart.cart_modifiers_base import BaseCartModifier
from shop.models.cartmodel import Cart, CartItem
from shop.addressmodel.models import Address, Country
from shop.models.ordermodel import Order, OrderItem, ExtraOrderPriceField, \
OrderPayment
from shop.models.ordermodel import Order, OrderItem, OrderPayment, \
ExtraOrderPriceField, ExtraOrderItemPriceField
from shop.models.productmodel import Product
from shop.tests.util import Mock
from shop.tests.utils.context_managers import SettingsOverride
Expand Down Expand Up @@ -128,6 +129,33 @@ def test_is_paid_works(self):
self.assertEqual(ret, False)


class MockCartModifierWithNothing(BaseCartModifier):
def get_extra_cart_price_field(self, cart):
return ('Total', Decimal(10))

def get_extra_cart_item_price_field(self, cart_item):
return ('Item', Decimal(1))


class MockCartModifierWithSimpleString(BaseCartModifier):
stdstr = 'plain ASCII'
unicodestr = u'unicode ÄÖÜäöüáàéèêóòñ'

def get_extra_cart_price_field(self, cart):
return ('Total', Decimal(10), str(self.stdstr))

def get_extra_cart_item_price_field(self, cart_item):
return ('Item', Decimal(1), self.unicodestr)


class MockCartModifierWithDictionaries(BaseCartModifier):
def get_extra_cart_price_field(self, cart):
return ('Total', Decimal(10), [{'rate': Decimal(9.8)}, {'discount': Decimal(0.2)}])

def get_extra_cart_item_price_field(self, cart_item):
return ('Item', Decimal(1), {'rate': Decimal(9.8), 'discount': Decimal(0.2)})


class OrderConversionTestCase(TestCase):

PRODUCT_PRICE = Decimal('100')
Expand Down Expand Up @@ -276,6 +304,30 @@ def test_order_addresses_match_user_preferences(self):
self.assertEqual(o.shipping_address_text, self.address.as_text())
self.assertEqual(o.billing_address_text, self.address2.as_text())

def test_create_order_with_extra_data_in_cart_modifier(self):
MODIFIERS = [
'shop.tests.order.MockCartModifierWithNothing',
'shop.tests.order.MockCartModifierWithSimpleString',
'shop.tests.order.MockCartModifierWithDictionaries'
]

with SettingsOverride(SHOP_CART_MODIFIERS=MODIFIERS):
self.cart.add_product(self.product)
self.cart.update()
self.cart.save()
order = Order.objects.create_from_cart(self.cart,)
extra_order_fields = ExtraOrderPriceField.objects.filter(order=order)
self.assertEqual(len(extra_order_fields), 3)
self.assertEqual(extra_order_fields[0].data, None)
self.assertEqual(extra_order_fields[1].data, MockCartModifierWithSimpleString.stdstr)
self.assertEqual(Decimal(extra_order_fields[2].data[0].get('rate')), Decimal(9.8))

extra_order_fields = ExtraOrderItemPriceField.objects.filter(order_item__order=order)
self.assertEqual(len(extra_order_fields), 3)
self.assertEqual(extra_order_fields[0].data, None)
self.assertEqual(extra_order_fields[1].data, MockCartModifierWithSimpleString.unicodestr)
self.assertEqual(Decimal(extra_order_fields[2].data.get('discount')), Decimal(0.2))

def test_create_order_respects_product_specific_get_price_method(self):
if SKIP_BASEPRODUCT_TEST:
return
Expand Down

0 comments on commit 7e094c4

Please sign in to comment.