Permalink
Browse files

added documentation / tests / runtests.sh

  • Loading branch information...
1 parent ba2aca5 commit 9dfb098aa914cc4ec61d240960011c619cc8dc8e @mbrochh mbrochh committed Jun 17, 2011
View
91 README.rst
@@ -2,32 +2,105 @@
django SHOP - Simple Variations
================================
-This app's purpose is to provide a way to quickly create product variations for most simple cases.
+This app's purpose is to provide a way to quickly create product variations for
+most simple cases.
-It considers variations as a {label: value} entry in the cart modifiers, so it is perfect for things like
-differently priced colors, or build-your-own computers for example.
+It considers variations as a {label: value} entry in the cart modifiers, so it
+is perfect for things like differently priced colors, or build-your-own
+computers for example.
Installation
============
This requires django SHOP to work (https://github.com/chrisglass/django-shop)
-* Add the app to your installed app
-* Add `shop_simplevariations.cart_modifier.ProductOptionsModifier` to your `SHOP_CART_MODIFIERS` setting.
+* Add the app to your INSTALLED_APPS in your settings.py
+* Add `shop_simplevariations.cart_modifier.ProductOptionsModifier` to your
+ `SHOP_CART_MODIFIERS` setting.
+* Add `(r'^shop/cart/', include(simplevariations_urls)),` to your `urls.py`
+ just before `(r'^shop/', include(shop_urls)),`
+
+Your urls.py should look like this:
+
+::
+
+ from django.conf.urls.defaults import *
+ from django.contrib import admin
+
+ from shop import urls as shop_urls
+ from shop_simplevariations import urls as simplevariations_urls
+
+
+ admin.autodiscover()
+
+
+ urlpatterns = patterns('',
+ (r'^admin/', include(admin.site.urls)),
+ (r'^shop/cart/', include(simplevariations_urls)),
+ (r'^shop/', include(shop_urls)),
+ )
Usage
-======
+=====
* Create an Option group in the admin view
* Bind it to a product
* Add options and the corresponding price to the group.
-* When a `CartItemOption` object is linked to a `CartItem`, the option's value will be added to the CartItem's price
- and a corresponding extra field willbe added to the Cart/Order.
+* When a `CartItemOption` object is linked to a `CartItem`, the option's value
+ will be added to the CartItem's price and a corresponding extra field willbe
+ added to the Cart/Order.
+* Override django-shop's `product_detail.html` template and add selection
+ elements so that your users can select variations.
+
+
+The product_detail.html template
+================================
+The simple `product_detail.html` that ships with the shop doesn't take
+variations into consideration.
+
+Therefore you need to override the template. django-shop-simplevariations
+ships with two templatetags that help creating drop down lists so that a
+customer can actually chose variation.
+
+First make sure to load the simplecariation templatetags:
+
+::
+
+ {% load simplevariation_tags %}
+ <h1>Product detail:</h1>
+ ...
+
+Next create the drop down lists of OptionsGroups and Options:
+
+::
+ <form method="post" action="{% url cart %}">{% csrf_token %}
+ {% with option_groups=object|get_option_groups %}
+ {% if option_groups %}
+ <div>
+ <h2>Variations:</h2>
+ {% for option_group in option_groups %}
+ <label for="add_item_option_group_{{ option_group.id }}">{{ option_group.name }}</label>
+ {% with options=option_group|get_options %}
+ <select name="add_item_option_group_{{ option_group.id }}">
+ {% for option in options %}
+ <option value="{{ option.id }}">{{ option.name }}</option>
+ {% endfor %}
+ </select>
+ {% endwith %}
+ {% endfor %}
+ </div>
+ {% endif %}
+ {% endwith %}
+ <input type="hidden" name="add_item_id" value="{{object.id}}">
+ <input type="hidden" name="add_item_quantity" value="1">
+ <input type="submit" value="Add to cart">
+ </form>
Contributing
============
Feel free to fork this project on github, send pull requests...
-development discussion happends on the django SHOP mailing list (django-shop@googlegroups.com)
+development discussion happends on the django SHOP mailing list
+(django-shop@googlegroups.com)
View
61 runtests.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+
+args=("$@")
+num_args=${#args[@]}
+index=0
+
+suite='shop_simplevariations'
+coverage=false
+documentation=false
+ci=false
+
+while [ "$index" -lt "$num_args" ]
+do
+ case "${args[$index]}" in
+ "--with-docs")
+ documentation=true
+ ;;
+ "--with-coverage")
+ coverage=true
+ ;;
+ "--ci")
+ ci=true
+ ;;
+ *)
+ suite="shop.${args[$index]}"
+ esac
+let "index = $index + 1"
+done
+if [ $ci == true ]; then
+ pushd .
+ cd tests/testapp
+ coverage run manage.py test $suite
+ coverage xml
+ popd
+
+elif [ $coverage == true ]; then
+ pushd .
+ cd tests/testapp
+ coverage run manage.py test $suite
+ coverage html
+ #x-www-browser htmlcov/index.html
+ popd
+
+else
+
+ # the default case...
+ pushd .
+ cd tests/testapp
+ python manage.py test $suite
+ popd
+
+fi
+
+if [ $documentation == true ]; then
+ pushd .
+ cd docs/
+ make html
+ x-www-browser _build/html/index.html
+ popd
+fi
+
View
2 shop_simplevariations/templatetags/simplevariation_tags.py
@@ -1,7 +1,5 @@
from django import template
-from ..models import CartItemOption
-
register = template.Library()
View
3 shop_simplevariations/tests/__init__.py
@@ -0,0 +1,3 @@
+from .cart_modifier import *
+from .simplevariation_tags import *
+from .views import *
View
69 ...simplevariations/tests/product_options.py → shop_simplevariations/tests/cart_modifier.py
@@ -9,20 +9,21 @@
from shop.tests.utils.context_managers import SettingsOverride
from shop_simplevariations.models import OptionGroup, Option, CartItemOption
+
class ProductOptionsTestCase(TestCase):
-
+
PRODUCT_PRICE = Decimal('100')
- AWESOME_OPTION_PRICE = Decimal('50') # The price of awesome?
+ AWESOME_OPTION_PRICE = Decimal('42') # The price of awesome?
TEN_PERCENT = Decimal(10) / Decimal(100)
-
- def create_fixtures(self):
+
+ def create_fixtures(self, quantity=1):
cart_modifiers_pool.USE_CACHE=False
-
- self.user = User.objects.create(username="test",
+
+ self.user = User.objects.create(username="test",
email="test@example.com",
first_name="Test",
last_name = "Toto")
-
+
self.product = Product()
self.product.name = "TestPrduct"
self.product.slug = "TestPrduct"
@@ -31,55 +32,75 @@ def create_fixtures(self):
self.product.active = True
self.product.unit_price = self.PRODUCT_PRICE
self.product.save()
-
+
self.ogroup = OptionGroup()
self.ogroup.product = self.product
self.ogroup.name = 'Test group'
self.ogroup.save()
-
+
self.option = Option()
self.option.group = self.ogroup
self.option.name = "Awesome"
self.option.price = self.AWESOME_OPTION_PRICE
self.option.save()
-
+
self.cart = Cart()
self.cart.user = self.user
self.cart.save()
-
+
self.cartitem = CartItem()
self.cartitem.cart = self.cart
- self.cartitem.quantity = 1
+ self.cartitem.quantity = quantity
self.cartitem.product = self.product
self.cartitem.save()
-
+
def test_01_no_options_yield_normal_price(self):
self.create_fixtures()
- MODIFIERS = ['shop.cart.modifiers.product_options.ProductOptionsModifier']
+ MODIFIERS = ['shop_simplevariations.cart_modifier.ProductOptionsModifier']
with SettingsOverride(SHOP_CART_MODIFIERS=MODIFIERS):
#No need to add a product there is already on in the fixtures
self.cart.update()
self.cart.save()
- sub_should_be = 1*self.PRODUCT_PRICE
- total_should_be = sub_should_be
-
+ sub_should_be = 1*self.PRODUCT_PRICE
+ total_should_be = sub_should_be
+
self.assertEqual(self.cart.subtotal_price, sub_should_be)
self.assertEqual(self.cart.total_price, total_should_be)
-
+
def test_02_awesome_option_increases_price_by_its_value(self):
self.create_fixtures()
-
+
c_item_option = CartItemOption()
c_item_option.option = self.option
c_item_option.cartitem = self.cartitem
c_item_option.save()
-
- MODIFIERS = ['shop.cart.modifiers.product_options.ProductOptionsModifier']
+
+ MODIFIERS = ['shop_simplevariations.cart_modifier.ProductOptionsModifier']
with SettingsOverride(SHOP_CART_MODIFIERS=MODIFIERS):
self.cart.update()
self.cart.save()
sub_should_be = (1*self.PRODUCT_PRICE) + (1*self.AWESOME_OPTION_PRICE)
- total_should_be = sub_should_be
-
+ total_should_be = sub_should_be
+
self.assertEqual(self.cart.subtotal_price, sub_should_be)
- self.assertEqual(self.cart.total_price, total_should_be)
+ self.assertEqual(self.cart.total_price, total_should_be)
+
+ def test03_quantity_should_be_used_by_modifier(self):
+ quantity = 2
+ self.create_fixtures(quantity=quantity)
+
+ c_item_option = CartItemOption()
+ c_item_option.option = self.option
+ c_item_option.cartitem = self.cartitem
+ c_item_option.save()
+
+ MODIFIERS = ['shop_simplevariations.cart_modifier.ProductOptionsModifier']
+ with SettingsOverride(SHOP_CART_MODIFIERS=MODIFIERS):
+ self.cart.update()
+ self.cart.save()
+ sub_should_be = (quantity*self.PRODUCT_PRICE) \
+ + (quantity*self.AWESOME_OPTION_PRICE)
+ total_should_be = sub_should_be
+
+ self.assertEqual(self.cart.subtotal_price, sub_should_be)
+ self.assertEqual(self.cart.total_price, total_should_be)
View
27 shop_simplevariations/tests/simplevariation_tags.py
@@ -0,0 +1,27 @@
+#-*- coding: utf-8 -*-
+"""Tests for templatetags of the shop_simplevariation application."""
+from django.test.testcases import TestCase
+
+from shop.models.productmodel import Product
+
+from ..models import OptionGroup
+from ..templatetags.simplevariation_tags import get_options, get_option_groups
+from .test_utils import create_fixtures
+
+
+class GetOptionGroupsTestCase(TestCase):
+ """Tests for the get_option_groups templatetag."""
+ def test01_should_return_all_option_groups_for_a_product(self):
+ create_fixtures(options=True)
+ product = Product.objects.all()[0]
+ option_groups = get_option_groups(product)
+ self.assertEqual(len(option_groups), 1)
+
+
+class GetOptionsTestCase(TestCase):
+ """Tests for the get_options templatetag."""
+ def test01_should_return_all_options_for_an_option_group(self):
+ create_fixtures(options=True)
+ option_group = OptionGroup.objects.all()[0]
+ options = get_options(option_group)
+ self.assertEqual(len(options), 2)
View
18 shop_simplevariations/tests/test_utils.py
@@ -0,0 +1,18 @@
+"""Commonly used methods for shop_simplevariation tests."""
+from shop.models.productmodel import Product
+
+from ..models import CartItemOption, Option, OptionGroup
+
+
+def create_fixtures(options=False):
+ product = Product( name='product 1', slug='product-1', active=True,
+ unit_price=43)
+ product.save()
+ if not options:
+ return
+ option_group = OptionGroup(name='option group 1', slug='option-group-1')
+ option_group.save()
+ option_group.products.add(product)
+
+ Option.objects.create(name='option 1', price='42', group=option_group)
+ Option.objects.create(name='option 2', price='84', group=option_group)
View
52 shop_simplevariations/tests/views.py
@@ -0,0 +1,52 @@
+#-*- coding: utf-8 -*-
+"""Tests for view classes of the shop_simplevariation application."""
+from django.core.urlresolvers import reverse
+from django.test.testcases import TestCase
+
+from shop.models.cartmodel import CartItem
+
+from .test_utils import create_fixtures
+
+
+class SimplevariationCartDetailsTestCase(TestCase):
+ """Tests for the SimplevariationCartDetails view class."""
+
+ def test01_cart_is_callable(self):
+ resp = self.client.get(reverse('cart'))
+ self.assertEqual(resp.status_code, 200)
+
+ def test02_post_adds_new_cart_item(self):
+ create_fixtures()
+ data = {
+ 'add_item_id': '1',
+ 'add_item_quantity': '2',
+ 'Add to cart': '',
+ }
+ resp = self.client.post(reverse('cart_item_add'), data=data)
+ self.assertEqual(len(CartItem.objects.all()), 1)
+
+ def test03_post_adds_different_cart_items_if_different_variations(self):
+ create_fixtures(options=True)
+ data = {
+ 'add_item_id': '1',
+ 'add_item_quantity': '2',
+ 'Add to cart': '',
+ 'add_item_option_group_1': '1',
+ }
+ resp = self.client.post(reverse('cart_item_add'), data=data)
+ data.update({'add_item_option_group_1': '2'})
+ resp = self.client.post(reverse('cart_item_add'), data=data)
+ self.assertEqual(len(CartItem.objects.all()), 2)
+
+ def test04_post_adds_to_same_cart_item_if_same_variations(self):
+ create_fixtures(options=True)
+ data = {
+ 'add_item_id': '1',
+ 'add_item_quantity': '2',
+ 'Add to cart': '',
+ 'add_item_option_group_1': '1',
+ }
+ resp = self.client.post(reverse('cart_item_add'), data=data)
+ resp = self.client.post(reverse('cart_item_add'), data=data)
+ self.assertEqual(len(CartItem.objects.all()), 1)
+ self.assertEqual(CartItem.objects.all()[0].quantity, 4)
View
1 shop_simplevariations/views.py
@@ -37,6 +37,7 @@ def post(self, *args, **kwargs):
if len(cartitemoptions) == len(option_ids):
found_cartitem_id = cartitem.id
merge = True
+ break
#if we found a CartItem object that has the same options, we need
#to select this one instead of just any CartItem that belongs to this
View
12 tests/testapp/.coveragerc
@@ -0,0 +1,12 @@
+[run]
+branch = True
+source =
+ shop
+ shop.addressmodel
+omit =
+ ../../*migrations*
+ ../../*tests*
+ /usr/share/*
+[report]
+precision = 2
+
View
0 tests/testapp/__init__.py
No changes.
View
13 tests/testapp/manage.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+import os, sys
+from django.core.management import execute_manager
+sys.path.insert(0, os.path.abspath('./../../'))
+try:
+ import settings # Assumed to be in the same directory.
+except ImportError:
+ import sys
+ sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ execute_manager(settings)
View
0 tests/testapp/project/__init__.py
No changes.
View
26 tests/testapp/project/models.py
@@ -0,0 +1,26 @@
+from django.db import models
+
+from shop.models.productmodel import Product
+from shop.util.fields import CurrencyField
+
+
+class BookProduct(Product):
+ isbn = models.CharField(max_length=255)
+ number_of_pages = models.IntegerField()
+
+
+class CompactDiscProduct(Product):
+ number_of_tracks = models.IntegerField()
+
+
+class BaseProduct(models.Model):
+ unit_price = CurrencyField()
+
+
+class ProductVariation(Product):
+ baseproduct = models.ForeignKey(BaseProduct)
+
+ def get_price(self):
+ return self.baseproduct.unit_price
+
+
View
29 tests/testapp/project/templates/project/bookproduct_detail.html
@@ -0,0 +1,29 @@
+<h1>Book Product detail:</h1>
+{{object.name}}<br />
+{{object.slug}}<br />
+{{object.short_description}}<br />
+{{object.long_description}}<br />
+{{object.active}}<br />
+
+{{object.date_added}}<br />
+{{object.last_modified}}<br />
+
+{{object.unit_price}}<br />
+
+{% if object.category %}
+{{object.category.name}}
+{% else %}
+(Product is at root category)
+{% endif %}
+<br />
+
+<h2>Book specifics:</h2><br />
+ISBN: {{object.isbn}}<br />
+Pages: {{object.number_of_pages}}<br />
+
+
+<form method="post" action="{% url cart %}">{% csrf_token %}
+<input type="hidden" name="add_item_id" value="{{object.id}}">
+<input type="hidden" name="add_item_quantity" value="1">
+<input type="submit" value="Add to cart">
+</form>
View
23 tests/testapp/project/tests.py
@@ -0,0 +1,23 @@
+"""
+This file demonstrates two different styles of tests (one doctest and one
+unittest). These will both pass when you run "manage.py test".
+
+Replace these with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+class SimpleTest(TestCase):
+ def test_basic_addition(self):
+ """
+ Tests that 1 + 1 always equals 2.
+ """
+ self.failUnlessEqual(1 + 1, 2)
+
+__test__ = {"doctest": """
+Another way to test that 1 + 1 is equal to 2.
+
+>>> 1 + 1 == 2
+True
+"""}
+
View
1 tests/testapp/project/views.py
@@ -0,0 +1 @@
+# Create your views here.
View
113 tests/testapp/settings.py
@@ -0,0 +1,113 @@
+# Django settings for example project.
+
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+ ('Christopher Glass', 'tribaal@gmail.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+ 'NAME': 'test.sqlite', # Or path to database file if using sqlite3.
+ 'USER': '', # Not used with sqlite3.
+ 'PASSWORD': '', # Not used with sqlite3.
+ 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
+ 'PORT': '', # Set to empty string for default. Not used with sqlite3.
+ }
+}
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# On Unix systems, a value of None will cause Django to use the same
+# timezone as the operating system.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'Europe/Zurich'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# If you set this to False, Django will not format dates, numbers and
+# calendars according to the current locale
+USE_L10N = True
+
+# Absolute path to the directory that holds media.
+# Example: "/home/media/media.lawrence.com/"
+MEDIA_ROOT = ''
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash if there is a path component (optional in other cases).
+# Examples: "http://media.lawrence.com", "http://example.com/media/"
+MEDIA_URL = ''
+
+# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
+# trailing slash.
+# Examples: "http://foo.com/media/", "/media/".
+ADMIN_MEDIA_PREFIX = '/media/'
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = 'h2%uf!luks79rw^4!5%q#v2znc87g_)@^jf1og!04@&&tsf7*9'
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.app_directories.Loader',
+# 'django.template.loaders.eggs.Loader',
+)
+
+MIDDLEWARE_CLASSES = [
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+]
+
+import django
+if django.VERSION[0] < 1 or django.VERSION[1] <3:
+ MIDDLEWARE_CLASSES.append('cbv.middleware.DeferredRenderingMiddleware')
+
+ROOT_URLCONF = 'testapp.urls'
+
+TEMPLATE_DIRS = (
+ # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+)
+
+INSTALLED_APPS = (
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'django.contrib.messages',
+ # Uncomment the next line to enable the admin:
+ 'django.contrib.admin',
+ # Uncomment the next line to enable admin documentation:
+ 'django.contrib.admindocs',
+ 'polymorphic', # We need polymorphic installed for the shop
+ 'shop', # The django SHOP application
+ 'shop.addressmodel',
+ 'shop_simplevariations',
+ 'project', # the test project application
+)
+
+# The shop settings:
+SHOP_CART_MODIFIERS= ['shop.cart.modifiers.rebate_modifiers.BulkRebateModifier']
+SHOP_SHIPPING_BACKENDS=['shop.shipping.backends.flat_rate.FlatRateShipping']
+
+# Shop module settings
+SHOP_SHIPPING_FLAT_RATE = '10' # That's just for the flat rate shipping backend
View
19 tests/testapp/urls.py
@@ -0,0 +1,19 @@
+from django.conf.urls.defaults import *
+
+# Uncomment the next two lines to enable the admin:
+from django.contrib import admin
+admin.autodiscover()
+
+from shop_simplevariations import urls as simplevariations_urls
+
+
+urlpatterns = patterns('',
+ # Example:
+ #(r'^example/', include('example.foo.urls')),
+ # Uncomment the admin/doc line below to enable admin documentation:
+ (r'^admin/doc/', include('django.contrib.admindocs.urls')),
+ # Uncomment the next line to enable the admin:
+ (r'^admin/', include(admin.site.urls)),
+ (r'^shop/cart/', include(simplevariations_urls)),
+ (r'^shop/', include('shop.urls')),
+)

0 comments on commit 9dfb098

Please sign in to comment.