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

View file

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

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.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:

View file

@ -1,7 +1,48 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
{% 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 %}

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">
{% csrf_token %}
{% bootstrap_form form %}
{% bootstrap_button "Add to order" button_type="submit" button_class="btn-primary" %}
</form>
{% endblock %}

View file

@ -1,73 +1,61 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
{% 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>
<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>
<hr />
<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 }}
<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 }}
- {{ product.available_in.upper|date:"Y-m-d H:i" }}
{% 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 %}
</small>
</div>
</div>
</div>
</a>
{% endfor %}
</div>
</table>
{% endblock %}

View file

@ -13,7 +13,8 @@ urlpatterns = [
#name='epay_callback'
#),
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]+)/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.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")
@ -140,24 +183,36 @@ class CoinifyRedirectView(TemplateView):
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