Getting there!

This commit is contained in:
Víðir Valberg Guðmundsson 2016-05-16 00:09:00 +02:00
parent 326e9d70cd
commit c45a3aa8c9
13 changed files with 410 additions and 162 deletions

View file

@ -1,7 +1,7 @@
def current_order(request): def current_order(request):
if request.user.is_authenticated(): if request.user.is_authenticated():
order = None order = None
orders = request.user.orders.filter(finalized=False) orders = request.user.orders.filter(open__isnull=False)
if orders: if orders:
order = orders[0] order = orders[0]

View file

@ -2,10 +2,6 @@ from django import forms
from .models import Order from .models import Order
class CheckoutForm(forms.Form):
# no fields here, just three submit buttons
pass
class AddToOrderForm(forms.Form): class AddToOrderForm(forms.Form):
quantity = forms.IntegerField() quantity = forms.IntegerField()

View file

@ -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'),
),
]

View file

@ -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,
),
]

View file

@ -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)
]

View file

@ -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')]),
),
]

View file

@ -1,6 +1,7 @@
from django.db import models from django.db import models
from django.db.models.aggregates import Sum from django.db.models.aggregates import Sum
from django.contrib.postgres.fields import DateTimeRangeField, JSONField 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.translation import ugettext_lazy as _
from django.utils import timezone from django.utils import timezone
from bornhack.utils import CreatedUpdatedModel, UUIDModel from bornhack.utils import CreatedUpdatedModel, UUIDModel
@ -9,6 +10,9 @@ from .managers import ProductQuerySet
class Order(CreatedUpdatedModel): class Order(CreatedUpdatedModel):
class Meta:
unique_together = ('user', 'open')
products = models.ManyToManyField( products = models.ManyToManyField(
'shop.Product', 'shop.Product',
through='shop.OrderProductRelation' through='shop.OrderProductRelation'
@ -27,10 +31,10 @@ class Order(CreatedUpdatedModel):
default=False, default=False,
) )
finalized = models.BooleanField( open = models.NullBooleanField(
verbose_name=_('Finalized?'), verbose_name=_('Open?'),
help_text=_('Whether this order has been finalized.'), help_text=_('Whether this order is open or not. "None" means closed.'),
default=False, default=True,
) )
camp = models.ForeignKey( camp = models.ForeignKey(
@ -44,6 +48,12 @@ class Order(CreatedUpdatedModel):
BANK_TRANSFER = 'bank_transfer' BANK_TRANSFER = 'bank_transfer'
PAYMENT_METHODS = [ PAYMENT_METHODS = [
CREDIT_CARD,
BLOCKCHAIN,
BANK_TRANSFER,
]
PAYMENT_METHOD_CHOICES = [
(CREDIT_CARD, 'Credit card'), (CREDIT_CARD, 'Credit card'),
(BLOCKCHAIN, 'Blockchain'), (BLOCKCHAIN, 'Blockchain'),
(BANK_TRANSFER, 'Bank transfer'), (BANK_TRANSFER, 'Bank transfer'),
@ -51,7 +61,7 @@ class Order(CreatedUpdatedModel):
payment_method = models.CharField( payment_method = models.CharField(
max_length=50, max_length=50,
choices=PAYMENT_METHODS, choices=PAYMENT_METHOD_CHOICES,
default=BLOCKCHAIN default=BLOCKCHAIN
) )
@ -67,10 +77,15 @@ class ProductCategory(CreatedUpdatedModel, UUIDModel):
verbose_name_plural = 'Product categories' verbose_name_plural = 'Product categories'
name = models.CharField(max_length=150) name = models.CharField(max_length=150)
slug = models.SlugField()
def __str__(self): def __str__(self):
return self.name return self.name
def save(self, **kwargs):
self.slug = slugify(self.name)
super(ProductCategory, self).save(**kwargs)
class Product(CreatedUpdatedModel, UUIDModel): class Product(CreatedUpdatedModel, UUIDModel):
class Meta: class Meta:
@ -78,9 +93,13 @@ class Product(CreatedUpdatedModel, UUIDModel):
verbose_name_plural = 'Products' verbose_name_plural = 'Products'
ordering = ['available_in'] ordering = ['available_in']
category = models.ForeignKey('shop.ProductCategory') category = models.ForeignKey(
'shop.ProductCategory',
related_name='products'
)
name = models.CharField(max_length=150) name = models.CharField(max_length=150)
slug = models.SlugField()
price = models.IntegerField( price = models.IntegerField(
help_text=_('Price of the product (in DKK).') help_text=_('Price of the product (in DKK).')
@ -103,6 +122,10 @@ class Product(CreatedUpdatedModel, UUIDModel):
self.price, self.price,
) )
def save(self, **kwargs):
self.slug = slugify(self.name)
super(Product, self).save(**kwargs)
def is_available(self): def is_available(self):
now = timezone.now() now = timezone.now()
return now in self.available_in return now in self.available_in
@ -114,6 +137,10 @@ class OrderProductRelation(models.Model):
quantity = models.PositiveIntegerField() quantity = models.PositiveIntegerField()
handed_out = models.BooleanField(default=False) handed_out = models.BooleanField(default=False)
@property
def total(self):
return self.product.price * self.quantity
class EpayCallback(CreatedUpdatedModel, UUIDModel): class EpayCallback(CreatedUpdatedModel, UUIDModel):
class Meta: class Meta:

View file

@ -1,7 +1,48 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load bootstrap3 %}
{% block content %} {% block content %}
<b>details for order {{ order.id }}</b> <h1>Order #{{ order.id }}</h1>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>
Name
<th>
Quantity
<th>
Price
<th>
Total
<tbody>
{% for order_product in order.orderproductrelation_set.all %}
<tr>
<td>
{{ order_product.product.name }}
<td>
{{ order_product.quantity }}
<td>
{{ order_product.product.price }}
<td>
{{ order_product.total }}
{% endfor %}
{# TODO: Add total + VAT info #}
</table>
{% if order.open %}
<form method="POST">
{% csrf_token %}
{% bootstrap_button "Credit card" button_type="submit" button_class="btn-primary" name="payment_method" value="credit_card" %}
{% bootstrap_button "Blockchain" button_type="submit" button_class="btn-primary" name="payment_method" value="blockchain" %}
{% bootstrap_button "Bank transfer" button_type="submit" button_class="btn-primary" name="payment_method" value="bank_transfer" %}
</form>
{% endif %}
{% endblock %} {% endblock %}

View file

@ -0,0 +1,36 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
{% block content %}
<h3>Orders</h3>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Order ID</th>
<th>Open?</th>
<th>Paid?</th>
<th>Delivered?</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr {% if order.finalized and order.paid %}style="color: lightgreen"{% endif %}>
<td>{{ order.id }}</td>
<td>{{ order.finalized }}</td>
<td>{{ order.paid }}</td>
<td>?</td>
<td>
{% 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 %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View file

@ -7,6 +7,7 @@
<form method="POST"> <form method="POST">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %}
{% bootstrap_button "Add to order" button_type="submit" button_class="btn-primary" %} {% bootstrap_button "Add to order" button_type="submit" button_class="btn-primary" %}
</form> </form>
{% endblock %} {% endblock %}

View file

@ -1,73 +1,61 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% block content %} {% block content %}
<h2>Shop</h2> <div class="row">
<div class="col-md-12">
Categories:
{% for category in categories %}
<a href="{% url 'shop:index' %}?category={{category.slug}}"
class="label label-{% if category.slug == current_category %}primary{% else %}default{% endif %}">
{{category}}
</a>&nbsp;
{% endfor %}
</div>
</div>
<hr />
<div class="row">
{% for product in products %}
<a href="{% url 'shop:product_detail' slug=product.slug %}">
<div class="col-sm-4 col-lg-4 col-md-4">
<div class="thumbnail">
<img src="http://placehold.it/320x200" alt="">
<div class="caption">
<h5>{{ product.category.name }}</h5>
<h4>
{{ product.name }}
</h4>
<h4>
Price: {{ product.price }} DKK
</h4>
<p>
{{ product.description }}
</p>
<small>
<strong>Availability:</strong><br/>
{{ product.available_in.lower|date:"Y-m-d H:i" }}
{% if product.available_in.upper %}
- {{ product.available_in.upper|date:"Y-m-d H:i" }}
{% endif %}
</small>
</div>
</div>
</div>
</a>
{% endfor %}
</div>
<p class="lead">
Here you can see your orders and available products in the shop.
</p>
<h3>Orders</h3>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Order ID</th>
<th>Open?</th>
<th>Paid?</th>
<th>Delivered?</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr {% if order.finalized and order.paid %}style="color: lightgreen"{% endif %}>
<td>{{ order.id }}</td>
<td>{{ order.finalized }}</td>
<td>{{ order.paid }}</td>
<td>?</td>
<td>
{% 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 %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<h3>Products</h3>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Description</th>
<th>Price</th>
<th>Availability</th>
<th>Buy</th>
</tr>
</thead>
<tbody>
{% for product in product_list %}
<tr {% if not product.is_available %}style="color: lightgrey"{%endif%}>
<td>{{ product.name }}</td>
<td>{{ product.price }} DKK</td>
<td>{{ product.available_in.lower }}
{% if product.available_in.upper %}
- {{ product.available_in.upper }}
{% endif %}
</td>
<td>
{% 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 %}
</td>
</tr>
</tbody>
{% endfor %}
</table> </table>
{% endblock %} {% endblock %}

View file

@ -13,7 +13,8 @@ urlpatterns = [
#name='epay_callback' #name='epay_callback'
#), #),
url(r'^$', ShopIndexView.as_view(), name='index'), url(r'^$', ShopIndexView.as_view(), name='index'),
url(r'products/(?P<pk>[a-zA-Z0-9\-]+)/$', ProductDetailView.as_view(), name='product_detail'), url(r'products/(?P<slug>[-_\w+]+)/$', ProductDetailView.as_view(), name='product_detail'),
url(r'orders/$', OrderListView.as_view(), name='order_list'),
url(r'orders/(?P<pk>[0-9]+)/$', OrderDetailView.as_view(), name='order_detail'), url(r'orders/(?P<pk>[0-9]+)/$', OrderDetailView.as_view(), name='order_detail'),
url(r'orders/(?P<pk>[0-9]+)/checkout/$', CheckoutView.as_view(), name='checkout'), # url(r'orders/(?P<pk>[0-9]+)/checkout/$', CheckoutView.as_view(), name='checkout'),
] ]

View file

@ -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.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse
from django.contrib import messages 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 shop.models import (
from .forms import CheckoutForm, AddToOrderForm Order,
Product,
OrderProductRelation,
ProductCategory,
)
from .forms import AddToOrderForm
class ShopIndexView(ListView): class ShopIndexView(ListView):
model = Product model = Product
template_name = "shop_index.html" template_name = "shop_index.html"
context_object_name = 'products'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(ShopIndexView, self).get_context_data(**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) context['orders'] = Order.objects.filter(user=self.request.user)
return context 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): 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 model = Order
template_name = 'checkout.html' template_name = 'order_detail.html'
form_class = CheckoutForm
context_object_name = 'order' context_object_name = 'order'
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@ -79,44 +69,97 @@ class CheckoutView(LoginRequiredMixin, FormView):
messages.error(request, 'This order contains no products!') messages.error(request, 'This order contains no products!')
return HttpResponseRedirect('shop:order_detail') 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): def form_valid(self, form):
### mark order as finalized and redirect user to payment product = self.get_object()
form.instance.finalized=True quantity = form.cleaned_data.get('quantity'),
### set payment_method based on submit button used # do we have an open order?
if 'credit_card' in form.data: try:
form.instance.payment_method=='credit_card' order = Order.objects.get(
elif 'blockchain' in form.data: user=self.request.user,
form.instance.payment_method=='blockchain' finalized=False
elif 'bank_transfer' in form.data: )
form.instance.payment_method=='bank_transfer' 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: else:
### unknown submit button order.products.add(product)
messages.error(request, 'Unknown submit button :(')
return reverse('shop:checkout', kwargs={'orderid': self.get_object.id})
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): def get_success_url(self):
if self.get_object.payment_method == 'credit_card': return reverse_lazy(
return reverse('shop:epay_form', kwargs={'orderid': self.get_object.id}) 'shop:product_detail',
elif self.get_object.payment_method == 'blockchain': kwargs={'slug': self.get_object().slug}
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})
class CoinifyRedirectView(TemplateView): class CoinifyRedirectView(TemplateView):
template_name = 'coinify_redirect.html' template_name = 'coinify_redirect.html'
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
### validate a few things # validate a few things
self.order = Order.objects.get(pk=kwargs.get('order_id')) self.order = Order.objects.get(pk=kwargs.get('order_id'))
if self.order.user != request.user: if self.order.user != request.user:
raise Http404("Order not found") raise Http404("Order not found")
@ -139,25 +182,37 @@ class CoinifyRedirectView(TemplateView):
order = Order.objects.get(pk=kwargs.get('order_id')) order = Order.objects.get(pk=kwargs.get('order_id'))
context = super(CoinifyRedirectView, self).get_context_data(**kwargs) context = super(CoinifyRedirectView, self).get_context_data(**kwargs)
context['order'] = order context['order'] = order
### Initiate coinify API and create invoice # Initiate coinify API and create invoice
coinifyapi = CoinifyAPI(settings.COINIFY_API_KEY, settings.COINIFY_API_SECRET) coinifyapi = CoinifyAPI(
settings.COINIFY_API_KEY,
settings.COINIFY_API_SECRET
)
response = coinifyapi.invoice_create( response = coinifyapi.invoice_create(
amount, amount,
currency, currency,
plugin_name='BornHack 2016 webshop', plugin_name='BornHack 2016 webshop',
plugin_version='1.0', plugin_version='1.0',
description='BornHack 2016 order id #%s' % order.id, description='BornHack 2016 order id #%s' % order.id,
callback_url=reverse('shop:coinfy_callback', kwargs={'orderid': order.id}), callback_url=reverse(
return_url=reverse('shop:order_paid', kwargs={'orderid': order.id}), 'shop:coinfy_callback',
kwargs={'orderid': order.id}
),
return_url=reverse(
'shop:order_paid',
kwargs={'orderid': order.id}
),
) )
if not response['success']: if not response['success']:
api_error = response['error'] 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'] 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 context['invoice'] = invoice
return context return context