diff --git a/shop/context_processors.py b/shop/context_processors.py
index b7ed3af2..c46c6bb2 100644
--- a/shop/context_processors.py
+++ b/shop/context_processors.py
@@ -1,7 +1,7 @@
def current_order(request):
if request.user.is_authenticated():
order = None
- orders = request.user.orders.filter(finalized=False)
+ orders = request.user.orders.filter(open__isnull=False)
if orders:
order = orders[0]
diff --git a/shop/forms.py b/shop/forms.py
index 5d2e98ec..ccdb9209 100644
--- a/shop/forms.py
+++ b/shop/forms.py
@@ -2,10 +2,6 @@ from django import forms
from .models import Order
-class CheckoutForm(forms.Form):
- # no fields here, just three submit buttons
- pass
-
class AddToOrderForm(forms.Form):
quantity = forms.IntegerField()
diff --git a/shop/migrations/0004_auto_20160515_1604.py b/shop/migrations/0004_auto_20160515_1604.py
new file mode 100644
index 00000000..4ab76d62
--- /dev/null
+++ b/shop/migrations/0004_auto_20160515_1604.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.6 on 2016-05-15 16:04
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('shop', '0003_auto_20160513_0646'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='productcategory',
+ name='slug',
+ field=models.SlugField(default=''),
+ preserve_default=False,
+ ),
+ migrations.AlterField(
+ model_name='product',
+ name='category',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='shop.ProductCategory'),
+ ),
+ ]
diff --git a/shop/migrations/0005_product_slug.py b/shop/migrations/0005_product_slug.py
new file mode 100644
index 00000000..27d01c8c
--- /dev/null
+++ b/shop/migrations/0005_product_slug.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.6 on 2016-05-15 16:14
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('shop', '0004_auto_20160515_1604'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='product',
+ name='slug',
+ field=models.SlugField(default=''),
+ preserve_default=False,
+ ),
+ ]
diff --git a/shop/migrations/0006_ensure_slugs.py b/shop/migrations/0006_ensure_slugs.py
new file mode 100644
index 00000000..d7bc09fb
--- /dev/null
+++ b/shop/migrations/0006_ensure_slugs.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.6 on 2016-05-15 16:15
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+def ensure_slugs(apps, schema_editor):
+ ProductCategory = apps.get_model('shop', 'ProductCategory')
+ Product = apps.get_model('shop', 'Product')
+
+ for category in ProductCategory.objects.all():
+ category.save()
+
+ for product in Product.objects.all():
+ product.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('shop', '0005_product_slug'),
+ ]
+
+ operations = [
+ migrations.RunPython(ensure_slugs, migrations.RunPython.noop)
+ ]
diff --git a/shop/migrations/0007_auto_20160515_2157.py b/shop/migrations/0007_auto_20160515_2157.py
new file mode 100644
index 00000000..d6550bfb
--- /dev/null
+++ b/shop/migrations/0007_auto_20160515_2157.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.6 on 2016-05-15 21:57
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('shop', '0006_ensure_slugs'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='order',
+ name='open',
+ field=models.NullBooleanField(default=True, help_text='Whether this order is open or not. "None" means closed.', verbose_name='Open?'),
+ ),
+ migrations.RemoveField(
+ model_name='order',
+ name='finalized',
+ ),
+ migrations.AlterUniqueTogether(
+ name='order',
+ unique_together=set([('user', 'open')]),
+ ),
+ ]
diff --git a/shop/models.py b/shop/models.py
index 383935b6..e6af683c 100644
--- a/shop/models.py
+++ b/shop/models.py
@@ -1,6 +1,7 @@
from django.db import models
from django.db.models.aggregates import Sum
from django.contrib.postgres.fields import DateTimeRangeField, JSONField
+from django.utils.text import slugify
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from bornhack.utils import CreatedUpdatedModel, UUIDModel
@@ -9,6 +10,9 @@ from .managers import ProductQuerySet
class Order(CreatedUpdatedModel):
+ class Meta:
+ unique_together = ('user', 'open')
+
products = models.ManyToManyField(
'shop.Product',
through='shop.OrderProductRelation'
@@ -27,10 +31,10 @@ class Order(CreatedUpdatedModel):
default=False,
)
- finalized = models.BooleanField(
- verbose_name=_('Finalized?'),
- help_text=_('Whether this order has been finalized.'),
- default=False,
+ open = models.NullBooleanField(
+ verbose_name=_('Open?'),
+ help_text=_('Whether this order is open or not. "None" means closed.'),
+ default=True,
)
camp = models.ForeignKey(
@@ -44,6 +48,12 @@ class Order(CreatedUpdatedModel):
BANK_TRANSFER = 'bank_transfer'
PAYMENT_METHODS = [
+ CREDIT_CARD,
+ BLOCKCHAIN,
+ BANK_TRANSFER,
+ ]
+
+ PAYMENT_METHOD_CHOICES = [
(CREDIT_CARD, 'Credit card'),
(BLOCKCHAIN, 'Blockchain'),
(BANK_TRANSFER, 'Bank transfer'),
@@ -51,7 +61,7 @@ class Order(CreatedUpdatedModel):
payment_method = models.CharField(
max_length=50,
- choices=PAYMENT_METHODS,
+ choices=PAYMENT_METHOD_CHOICES,
default=BLOCKCHAIN
)
@@ -67,10 +77,15 @@ class ProductCategory(CreatedUpdatedModel, UUIDModel):
verbose_name_plural = 'Product categories'
name = models.CharField(max_length=150)
+ slug = models.SlugField()
def __str__(self):
return self.name
+ def save(self, **kwargs):
+ self.slug = slugify(self.name)
+ super(ProductCategory, self).save(**kwargs)
+
class Product(CreatedUpdatedModel, UUIDModel):
class Meta:
@@ -78,9 +93,13 @@ class Product(CreatedUpdatedModel, UUIDModel):
verbose_name_plural = 'Products'
ordering = ['available_in']
- category = models.ForeignKey('shop.ProductCategory')
+ category = models.ForeignKey(
+ 'shop.ProductCategory',
+ related_name='products'
+ )
name = models.CharField(max_length=150)
+ slug = models.SlugField()
price = models.IntegerField(
help_text=_('Price of the product (in DKK).')
@@ -103,6 +122,10 @@ class Product(CreatedUpdatedModel, UUIDModel):
self.price,
)
+ def save(self, **kwargs):
+ self.slug = slugify(self.name)
+ super(Product, self).save(**kwargs)
+
def is_available(self):
now = timezone.now()
return now in self.available_in
@@ -114,6 +137,10 @@ class OrderProductRelation(models.Model):
quantity = models.PositiveIntegerField()
handed_out = models.BooleanField(default=False)
+ @property
+ def total(self):
+ return self.product.price * self.quantity
+
class EpayCallback(CreatedUpdatedModel, UUIDModel):
class Meta:
diff --git a/shop/templates/order_detail.html b/shop/templates/order_detail.html
index 384462be..d4df4508 100644
--- a/shop/templates/order_detail.html
+++ b/shop/templates/order_detail.html
@@ -1,7 +1,48 @@
{% extends 'base.html' %}
+{% load bootstrap3 %}
{% block content %}
-details for order {{ order.id }}
+
Order #{{ order.id }}
+
+
+
+
+
+
+ Name
+ |
+ Quantity
+ |
+ Price
+ |
+ Total
+
+ |
+{% for order_product in order.orderproductrelation_set.all %}
+
+
+ {{ order_product.product.name }}
+ |
+ {{ order_product.quantity }}
+ |
+ {{ order_product.product.price }}
+ |
+ {{ order_product.total }}
+
+{% endfor %}
+
+{# TODO: Add total + VAT info #}
+
+ |
+
+{% if order.open %}
+
+{% endif %}
{% endblock %}
diff --git a/shop/templates/order_list.html b/shop/templates/order_list.html
new file mode 100644
index 00000000..3dc46281
--- /dev/null
+++ b/shop/templates/order_list.html
@@ -0,0 +1,36 @@
+{% extends 'base.html' %}
+{% load bootstrap3 %}
+
+{% block content %}
+Orders
+
+
+
+ Order ID |
+ Open? |
+ Paid? |
+ Delivered? |
+ Actions |
+
+
+
+{% for order in orders %}
+
+ {{ order.id }} |
+ {{ order.finalized }} |
+ {{ order.paid }} |
+ ? |
+
+ {% if order.finalized and not order.paid %}
+ {% url 'shop:order_detail' pk=order.pk as order_detail_url %}
+ {% url 'shop:order_cancel' pk=order.pk as order_cancel_url %}
+ {% bootstrap_button "Pay order" href=order_detail_url button_class="btn-primary" %}
+ {% bootstrap_button "Cancel order" href=order_cancel_url button_class="btn-primary" %}
+ {% endif %}
+ |
+
+{% endfor %}
+
+
+
+{% endblock %}
diff --git a/shop/templates/product_detail.html b/shop/templates/product_detail.html
index 7aa53ed3..7f6a7263 100644
--- a/shop/templates/product_detail.html
+++ b/shop/templates/product_detail.html
@@ -7,6 +7,7 @@
{% endblock %}
diff --git a/shop/templates/shop_index.html b/shop/templates/shop_index.html
index 6418bcc4..77b890b0 100644
--- a/shop/templates/shop_index.html
+++ b/shop/templates/shop_index.html
@@ -1,73 +1,61 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
+
{% block content %}
-Shop
+
+
+ Categories:
+ {% for category in categories %}
+
+ {{category}}
+
+ {% endfor %}
+
+
+
+
+
+
-
-Here you can see your orders and available products in the shop.
-
-Orders
-
-
-
- Order ID |
- Open? |
- Paid? |
- Delivered? |
- Actions |
-
-
-
-{% for order in orders %}
-
- {{ order.id }} |
- {{ order.finalized }} |
- {{ order.paid }} |
- ? |
-
- {% if order.finalized and not order.paid %}
- {% url 'shop:order_detail' pk=order.id as order_detail_url %}
- {% url 'shop:order_cancel' pk=order.id as order_cancel_url %}
- {% bootstrap_button "Pay order" href=order_detail_url button_class="btn-primary" %}
- {% bootstrap_button "Cancel order" href=order_cancel_url button_class="btn-primary" %}
- {% endif %}
- |
-
-{% endfor %}
-
-
-Products
-
-
-
- Description |
- Price |
- Availability |
- Buy |
-
-
-
-{% for product in product_list %}
-
- {{ product.name }} |
- {{ product.price }} DKK |
- {{ product.available_in.lower }}
- {% if product.available_in.upper %}
- - {{ product.available_in.upper }}
- {% endif %}
- |
-
- {% if product.is_available %}
- {% url 'shop:product_detail' pk=product.pk as product_detail_url %}
- {% bootstrap_button "Add to order" href=product_detail_url button_class="btn-primary" %}
- {% else %}
- N/A
- {% endif %}
- |
-
-
-{% endfor %}
{% endblock %}
diff --git a/shop/urls.py b/shop/urls.py
index 23581cac..1d3fa374 100644
--- a/shop/urls.py
+++ b/shop/urls.py
@@ -13,7 +13,8 @@ urlpatterns = [
#name='epay_callback'
#),
url(r'^$', ShopIndexView.as_view(), name='index'),
- url(r'products/(?P[a-zA-Z0-9\-]+)/$', ProductDetailView.as_view(), name='product_detail'),
+ url(r'products/(?P[-_\w+]+)/$', ProductDetailView.as_view(), name='product_detail'),
+ url(r'orders/$', OrderListView.as_view(), name='order_list'),
url(r'orders/(?P[0-9]+)/$', OrderDetailView.as_view(), name='order_detail'),
- url(r'orders/(?P[0-9]+)/checkout/$', CheckoutView.as_view(), name='checkout'),
+ # url(r'orders/(?P[0-9]+)/checkout/$', CheckoutView.as_view(), name='checkout'),
]
diff --git a/shop/views.py b/shop/views.py
index 2883d2e6..813c79cb 100644
--- a/shop/views.py
+++ b/shop/views.py
@@ -1,70 +1,60 @@
-import hashlib
-
-from django.http import HttpResponseRedirect, Http404
-from django.views.generic import CreateView, TemplateView, ListView, DetailView, View, FormView
-from django.core.urlresolvers import reverse_lazy
from django.conf import settings
-from django.contrib.auth.mixins import LoginRequiredMixin
-from django.http import HttpResponse
from django.contrib import messages
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.core.urlresolvers import reverse_lazy
+from django.db.models import Count, F
+from django.http import HttpResponseRedirect, Http404
+from django.views.generic import (
+ TemplateView,
+ ListView,
+ DetailView,
+ FormView,
+)
-from .models import Order, Product, EpayCallback, EpayPayment, OrderProductRelation
-from .forms import CheckoutForm, AddToOrderForm
+from shop.models import (
+ Order,
+ Product,
+ OrderProductRelation,
+ ProductCategory,
+)
+from .forms import AddToOrderForm
class ShopIndexView(ListView):
model = Product
template_name = "shop_index.html"
+ context_object_name = 'products'
def get_context_data(self, **kwargs):
context = super(ShopIndexView, self).get_context_data(**kwargs)
+
+ if 'category' in self.request.GET:
+ category = self.request.GET.get('category')
+ context['products'] = context['products'].filter(
+ category__slug=category
+ )
+ context['current_category'] = category
+ context['categories'] = ProductCategory.objects.annotate(
+ num_products=Count('products')
+ ).filter(
+ num_products__gt=0
+ )
+ return context
+
+
+class OrderListView(LoginRequiredMixin, ListView):
+ model = Order
+ template_name = "order_list.html"
+
+ def get_context_data(self, **kwargs):
+ context = super(OrderListView, self).get_context_data(**kwargs)
context['orders'] = Order.objects.filter(user=self.request.user)
return context
-class ProductDetailView(LoginRequiredMixin, FormView):
- model = Product
- template_name = 'product_detail.html'
- form_class = AddToOrderForm
- context_object_name = 'product'
-
- def get(self, request, *args, **kwargs):
- self.product = Product.objects.get(pk=kwargs.get('pk'))
- return self.render_to_response(self.get_context_data())
-
- def form_valid(self, form):
- ### do we have an open order?
- try:
- order = Order.objects.get(user=self.request.user, finalized=False)
- except Order.DoesNotExist:
- ### no open order - open a new one
- order = Order.objects.create(user=request.user)
-
- ### get product from kwargs
- if self.product in order.products.all():
- ### this product is already added to this order, increase count by one
- OrderProductRelation.objects.filter(product=self.product, order=order).update(quantity=F('quantity') + 1)
- else:
- order.products.add(self.product)
-
- ### done
- return super(ProductDetailView, self).form_valid(form)
-
-
class OrderDetailView(LoginRequiredMixin, DetailView):
- model = Product
- template_name = 'order_detail.html'
- context_object_name = 'order'
-
-
-class CheckoutView(LoginRequiredMixin, FormView):
- """
- Shows a summary of all products contained in an order,
- total price, VAT info, and a button to finalize order and go to payment
- """
model = Order
- template_name = 'checkout.html'
- form_class = CheckoutForm
+ template_name = 'order_detail.html'
context_object_name = 'order'
def get(self, request, *args, **kwargs):
@@ -79,44 +69,97 @@ class CheckoutView(LoginRequiredMixin, FormView):
messages.error(request, 'This order contains no products!')
return HttpResponseRedirect('shop:order_detail')
- return self.render_to_response(self.get_context_data())
+ return super(OrderDetailView, self).get(request, *args, **kwargs)
+
+ def post(self, request, *args, **kwargs):
+ # mark order as finalized and redirect user to payment
+ order = self.get_object()
+ payment_method = request.POST.get('payment_method')
+
+ if payment_method in order.PAYMENT_METHODS:
+ order.payment_method = payment_method
+ else:
+ # unknown submit button
+ messages.error(self.request, 'Unknown submit button :(')
+ return reverse_lazy(
+ 'shop:checkout',
+ kwargs={'orderid': self.get_object.id}
+ )
+
+ order.finalized = True
+
+ reverses = {
+ Order.CREDIT_CARD: reverse_lazy(
+ 'shop:epay_form',
+ kwargs={'orderid': order.id}
+ ),
+ Order.BLOCKCHAIN: reverse_lazy(
+ 'shop:coinify_pay',
+ kwargs={'orderid': order.id}
+ ),
+ Order.BANK_TRANSFER: reverse_lazy(
+ 'shop:bank_transfer',
+ kwargs={'orderid': order.id}
+ )
+ }
+
+ return HttpResponseRedirect(reverses[payment_method])
+
+
+class ProductDetailView(LoginRequiredMixin, FormView, DetailView):
+ model = Product
+ template_name = 'product_detail.html'
+ form_class = AddToOrderForm
+ context_object_name = 'product'
def form_valid(self, form):
- ### mark order as finalized and redirect user to payment
- form.instance.finalized=True
+ product = self.get_object()
+ quantity = form.cleaned_data.get('quantity'),
- ### set payment_method based on submit button used
- if 'credit_card' in form.data:
- form.instance.payment_method=='credit_card'
- elif 'blockchain' in form.data:
- form.instance.payment_method=='blockchain'
- elif 'bank_transfer' in form.data:
- form.instance.payment_method=='bank_transfer'
+ # do we have an open order?
+ try:
+ order = Order.objects.get(
+ user=self.request.user,
+ finalized=False
+ )
+ except Order.DoesNotExist:
+ # no open order - open a new one
+ order = Order.objects.create(user=self.request.user)
+
+ # get product from kwargs
+ if product in order.products.all():
+ # this product is already added to this order,
+ # increase count by quantity
+ OrderProductRelation.objects.filter(
+ product=product,
+ order=order
+ ).update(quantity=F('quantity') + quantity)
else:
- ### unknown submit button
- messages.error(request, 'Unknown submit button :(')
- return reverse('shop:checkout', kwargs={'orderid': self.get_object.id})
+ order.products.add(product)
- return super(CheckoutView, self).form_valid(form)
+ messages.info(
+ self.request,
+ '{}x {} has been added to your order.'.format(
+ quantity,
+ product.name
+ )
+ )
+
+ # done
+ return super(ProductDetailView, self).form_valid(form)
def get_success_url(self):
- if self.get_object.payment_method == 'credit_card':
- return reverse('shop:epay_form', kwargs={'orderid': self.get_object.id})
- elif self.get_object.payment_method == 'blockchain':
- return reverse('shop:coinify_pay', kwargs={'orderid': self.get_object.id})
- elif self.get_object.payment_method == 'bank_transfer':
- return reverse('shop:bank_transfer', kwargs={'orderid': self.get_object.id})
- else:
- ### unknown payment method
- messages.error(request, 'Unknown payment method :(')
- return reverse('shop:checkout', kwargs={'orderid': self.get_object.id})
+ return reverse_lazy(
+ 'shop:product_detail',
+ kwargs={'slug': self.get_object().slug}
+ )
class CoinifyRedirectView(TemplateView):
template_name = 'coinify_redirect.html'
-
+
def get(self, request, *args, **kwargs):
- ### validate a few things
+ # validate a few things
self.order = Order.objects.get(pk=kwargs.get('order_id'))
if self.order.user != request.user:
raise Http404("Order not found")
@@ -139,25 +182,37 @@ class CoinifyRedirectView(TemplateView):
order = Order.objects.get(pk=kwargs.get('order_id'))
context = super(CoinifyRedirectView, self).get_context_data(**kwargs)
context['order'] = order
-
- ### Initiate coinify API and create invoice
- coinifyapi = CoinifyAPI(settings.COINIFY_API_KEY, settings.COINIFY_API_SECRET)
+
+ # Initiate coinify API and create invoice
+ coinifyapi = CoinifyAPI(
+ settings.COINIFY_API_KEY,
+ settings.COINIFY_API_SECRET
+ )
response = coinifyapi.invoice_create(
amount,
currency,
plugin_name='BornHack 2016 webshop',
plugin_version='1.0',
description='BornHack 2016 order id #%s' % order.id,
- callback_url=reverse('shop:coinfy_callback', kwargs={'orderid': order.id}),
- return_url=reverse('shop:order_paid', kwargs={'orderid': order.id}),
+ callback_url=reverse(
+ 'shop:coinfy_callback',
+ kwargs={'orderid': order.id}
+ ),
+ return_url=reverse(
+ 'shop:order_paid',
+ kwargs={'orderid': order.id}
+ ),
)
if not response['success']:
api_error = response['error']
- print "API error: %s (%s)" % (api_error['message'], api_error['code'] )
+ print "API error: %s (%s)" % (
+ api_error['message'],
+ api_error['code']
+ )
invoice = response['data']
- ### change this to pass only needed data when we get that far
+ # change this to pass only needed data when we get that far
context['invoice'] = invoice
return context