Commits

Anonymous committed 9b89ce3

initial checkin

Comments (0)

Files changed (15)

+pyc$
+~$

Empty file added.

catalogue/admin.py

+from django.contrib import admin
+from gnocchi_catalogue import models
+from gnocchi_tools.admin import InlineAttributeAdmin
+
+class InlinePriceBreakAdmin( admin.TabularInline ):
+    model = models.PriceBreak
+
+class InlineVariantAdmin( admin.TabularInline ):
+    model = models.ProductVariant
+
+class InlineProductImageAdmin( admin.TabularInline ):
+    model = models.ProductImage
+
+class ProductAdmin( admin.ModelAdmin ):
+    list_display = ( 'title', 'price', 'short_description', 'get_codes', )
+    list_filter = ( 'price', )
+    filter_horizontal = ( 'related_products', )
+    inlines = [
+        InlineVariantAdmin,
+        InlineProductImageAdmin,
+        InlinePriceBreakAdmin,
+        InlineAttributeAdmin,
+    ]
+    def get_codes(self, obj):
+        return u', '.join(obj.productvariant_set.values_list('sku', flat=True ))
+    get_codes.short_description = 'Codes'
+
+
+admin.site.register( models.Product, ProductAdmin )

catalogue/cart.py

+
+from session_cart import cart
+
+class CartItem(cart.CartItem):
+    '''cart item customised for our products'''
+    def unit_price(self):
+        return self.item.product.get_price(self.quantity)
+
+    def total(self):
+        return self.item.product.get_price(self.quantity) * self.quantity
Add a comment to this file

catalogue/management/__init__.py

Empty file added.

Add a comment to this file

catalogue/management/commands/__init__.py

Empty file added.

catalogue/management/commands/sync_product_images.py

+from django.core.management.base import NoArgsCommand
+from gnocchi_catalogue.models import Product, ProductImage
+from django.conf import settings
+from glob import glob
+import os.path
+
+class Command(NoArgsCommand):
+    help = "Ensure an Image entry exists for all files in images/ directory"
+
+    def handle_noargs(self, **options ):
+        root_path = os.path.join( settings.MEDIA_ROOT, 'images/products/' )
+
+        pfx = len(settings.MEDIA_ROOT)
+        for product in Product.objects.all():
+            print "Product: %s" % product
+            prod_path = os.path.join( root_path, str(product.id), '*' )
+            print "Searching: %s" % prod_path
+            for filename in glob( prod_path ):
+                filename = filename[pfx:] # strip media path
+                print "Found: %s" % filename
+                if not product.productimage_set.filter( image=filename ).count():
+                    print "[%s] Adding Image: %s" % ( product, filename, ),
+                    try:
+                        ProductImage.objects.create(
+                            product=product,
+                            image=filename,
+                            order=1
+                        )
+                    except:
+                        print " Failed!"
+                    else:
+                        print " Done!"
+        print "Done."

catalogue/models.py

+from django.db import models
+from django.core.urlresolvers import reverse
+from django.core.exceptions import ValidationError
+from django.contrib.contenttypes import generic
+from django.utils.translation import ugettext_lazy as _
+
+from datetime import datetime
+from decimal import Decimal
+
+from taggit.managers import TaggableManager
+
+from gnocchi_tools.attr import AttrHelper
+
+class Product(models.Model, AttrHelper):
+    title = models.CharField(max_length=200, verbose_name=_('Title'))
+    short_description = models.CharField(verbose_name=_('Short Description'),
+        max_length=1024, blank=True)
+    description = models.TextField(verbose_name=_('Description'))
+    price = models.DecimalField(verbose_name=_('Price'), max_digits=12,
+        decimal_places=2)
+    shipping = models.DecimalField(verbose_name=_('Shipping Cost'),
+        max_digits=12, decimal_places=2, default='0.0')
+    attributes = generic.GenericRelation('tools.Attribute')
+
+    tags = TaggableManager()
+
+    related_products = models.ManyToManyField('self', blank=True,
+        verbose_name=_('Related Products'))
+
+    def __unicode__(self):
+        return self.title
+    def get_price(self, quantity):
+        try:
+            return self.pricebreak_set.filter(quantity__lte=quantity)[0].price
+        except (PriceBreak.DoesNotExist, IndexError):
+            return self.price
+    def get_absolute_url(self):
+        return reverse('product_detail', args=[self.pk])
+
+def product_image_name(instance, filename):
+    return 'images/products/%s/%s' % (instance.product.pk, filename,)
+
+class ProductImage(models.Model):
+    product = models.ForeignKey('Product', related_name='images')
+    order = models.IntegerField(default=1)
+    image = models.ImageField(upload_to=product_image_name)
+
+    class Meta:
+        ordering = ('-order',)
+
+class ProductVariant(models.Model):
+    product = models.ForeignKey('Product')
+    sku = models.CharField(max_length=256, verbose_name=_("Order Code"))
+    description = models.CharField(max_length=1024, blank=True,
+        verbose_name=_('Description'))
+
+    def __unicode__(self):
+        return u'%s (%s)' % (self.product.short_description, self.description,)
+
+    def get_absolute_url(self):
+        return self.product.get_absolute_url()
+
+class PriceBreak( models.Model ):
+    product = models.ForeignKey('Product')
+    quantity = models.PositiveIntegerField(verbose_name=_('Quantity'))
+    price = models.DecimalField(max_digits=12, decimal_places=2,
+        verbose_name=_('Price'))
+    class Meta:
+        ordering = ( 'quantity', )

catalogue/search_indexes.py

+
+from haystack.indexes import SearchIndex, CharField
+from haystack import site
+from gnocchi_catalogue.models import Product
+
+class ProductIndex(SearchIndex):
+    text = CharField(document=True, use_template=True)
+    title = CharField(model_attr='title')
+
+site.register(Product, ProductIndex)

catalogue/templates/search/indexes/catalogue/product_text.txt

+{{ object.title }}
+{{ object.short_description }}
+{{ object.description }}
+{% for var in object.productvariant_set.all %}
+{{ var.sku }} {{ var.description }}
+{% endfor %}
+${{ object.price }}
Add a comment to this file

catalogue/templatetags/__init__.py

Empty file added.

catalogue/templatetags/catalogue.py

+from django import template
+from gnocchi_catalogue import models
+
+register = template.Library()
+
+@register.filter
+def products_with_tags(values):
+    qset = models.Product.objects.all()
+    for tag in values.split():
+        qset = qset.filter(tags__tag__name=tag.strip())
+    return qset

catalogue/urls.py

+from django.conf.urls.defaults import patterns, url
+
+urlpatterns = patterns('gnocchi_catalogue.views',
+    url(r'^cart/update/$', 'cart_update', name='cart_update'),
+    url(r'^cart/remove/$', 'cart_remove', name='cart_remove'),
+    url(r'^cart/empty/$', 'cart_empty', name='cart_empty'),
+    url(r'^cart/add/$', 'cart_add', name='cart_add'),
+    url(r'^cart/$', 'cart_details', name='cart_details'),
+
+    url(r'^view/(?P<object_id>\d+)/$', 'product_detail', name='product_detail'),
+    url(r'^products/(?P<tags>.+)/$', 'product_list'),
+    url(r'^$', 'product_list', name='product_list'),
+)

catalogue/views.py

+'''Catalogue views'''
+from django.views.generic.list_detail import object_list, object_detail
+from django.views.generic.simple import direct_to_template
+from django.http import HttpResponseRedirect
+from django.core.urlresolvers import reverse
+from django.db.models import Q
+from django.conf import settings
+
+from gnocchi_catalogue import models
+
+PAGE_SIZE = int(getattr(settings, 'CATALOGUE_PAGE_SIZE', 15))
+
+##
+## Product Views
+##
+
+
+def product_detail(request, object_id):
+    '''Render details of the current product'''
+    return object_detail(request, object_id=object_id,
+        template_object_name='product',
+        queryset=models.Product.objects.all(),
+    )
+
+def product_list(request, tags =None):
+    '''Show a list of products'''
+    qset = models.Product.objects.all()
+    # Filter ...
+    if tags is None:
+        tags = request.GET.getlist('tags')
+    elif isinstance(tags, basestring):
+        tags = tags.split('/')
+    if tags:
+        qset = qset.filter(tags__in=tags)
+
+    if 'search' in request.REQUEST:
+        search = request.REQUEST['search']
+        qset = qset.filter(Q(description__icontains=search) | \
+            Q(short_description__icontains=search)
+        )
+
+    options = dict(
+        template_object_name='product',
+        queryset=qset,
+        allow_empty=True,
+        extra_context={
+            'tag_usage': models.Product.tags.most_common(),
+        }
+    )
+
+    try:
+        page_size = int(request.GET['paginate'])
+    except (KeyError, ValueError):
+        page_size = PAGE_SIZE
+    if page_size:
+        options['paginate_by'] = page_size
+
+    return object_list(request, **options)
+
+##
+## Cart views
+##
+
+def cart_or_next(view):
+    '''Decorator to redirect response to 'next' or the cart'''
+    def inner(request, *a, **kw):
+        '''Inner wrapper for cart_or_next'''
+        view(request, *a, **kw)
+        if 'next' in request.REQUEST:
+            url = request.REQUEST['next']
+        else:
+            url = reverse('cart_details')
+        return HttpResponseRedirect(url)
+    return inner
+
+@cart_or_next
+def cart_add(request):
+    '''Add an item to the cart'''
+    product_id = request.REQUEST['product_id']
+    product = models.ProductVariant.objects.get(id=product_id)
+    quantity = int(request.REQUEST.get('quantity', 1))
+    request.cart.add_item(product, quantity)
+
+def cart_details(request):
+    '''View the current cart'''
+    return direct_to_template(request, 'catalogue/cart_list.html')
+
+@cart_or_next
+def cart_update(request):
+    '''Update quantities in cart'''
+    # List of item pkeys to remove
+    remove_list = set(int(x) for x in request.POST.getlist('remove'))
+    remove = set()
+    for item in request.cart:
+        if item.item.pk in remove:
+            remove.add(item)
+        else:
+            val = int(request.POST['item_%d' % item.item.pk])
+            if val:
+                item.quantity = val
+            else:
+                remove.add(item)
+    # Remove items
+    for item in remove:
+        request.cart.remove(item)
+
+@cart_or_next
+def cart_remove(request):
+    '''Remove an item from the cart'''
+    item = request.cart._get(request.REQUEST['product_id'])
+    try:
+        request.cart.remove(item)
+    except ValueError:
+        pass
+
+@cart_or_next
+def cart_empty(request):
+    '''Empty the cart'''
+    request.cart.empty()
+from setuptools import setup, find_packages
+
+setup( name='gnocchi-catalogue',
+	version='1.0',
+	description='A simple Product Catalogue for Gnocchi',
+	author='Curtis Maloney',
+	author_email='curtis@tinbrain.net',
+	url='http://bitbucket.org/funkybob/gnocci-catalogue/',
+	keywords=['django', 'e-commerce',],
+	packages=find_packages(),
+	zip_safe=False,
+	install_requires=[
+		'gnocchi-tools>=1.0',
+		'session-cart>=1.0',
+	]
+)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.