We don't only operate on tickets - renaming and refactoring accordingly
This commit is contained in:
parent
b00b3f3156
commit
5cfcf13e7d
|
@ -26,7 +26,7 @@ INSTALLED_APPS = [
|
||||||
|
|
||||||
'profiles',
|
'profiles',
|
||||||
'camps',
|
'camps',
|
||||||
'tickets',
|
'shop',
|
||||||
|
|
||||||
'allauth',
|
'allauth',
|
||||||
'allauth.account',
|
'allauth.account',
|
||||||
|
|
|
@ -37,8 +37,8 @@
|
||||||
|
|
||||||
<div id="navbar" class="navbar-collapse collapse">
|
<div id="navbar" class="navbar-collapse collapse">
|
||||||
<ul class="nav navbar-nav">
|
<ul class="nav navbar-nav">
|
||||||
{% if current_camp.ticket_sale_open %}
|
{% if current_camp.shop_open %}
|
||||||
<li><a href="{% url 'tickets:index' %}">Tickets</a></li>
|
<li><a href="{% url 'shop:index' %}">Shop</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{% url 'good-to-know' %}">Good to know</a></li>
|
<li><a href="{% url 'good-to-know' %}">Good to know</a></li>
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
|
|
|
@ -36,8 +36,8 @@ urlpatterns = [
|
||||||
include('profiles.urls', namespace='profiles')
|
include('profiles.urls', namespace='profiles')
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r'^tickets/',
|
r'^shop/',
|
||||||
include('tickets.urls', namespace='tickets')
|
include('shop.urls', namespace='shop')
|
||||||
),
|
),
|
||||||
url(r'^accounts/', include('allauth.urls')),
|
url(r'^accounts/', include('allauth.urls')),
|
||||||
|
|
||||||
|
|
24
camps/migrations/0005_auto_20160510_2011.py
Normal file
24
camps/migrations/0005_auto_20160510_2011.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.6 on 2016-05-10 20:11
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('camps', '0004_camp_ticket_sale_open'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='camp',
|
||||||
|
name='ticket_sale_open',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='camp',
|
||||||
|
name='shop_open',
|
||||||
|
field=models.BooleanField(default=False, help_text='Whether the shop is open or not.', verbose_name='Shop open?'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -30,9 +30,9 @@ class Camp(CreatedUpdatedModel, UUIDModel):
|
||||||
unique=True,
|
unique=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
ticket_sale_open = models.BooleanField(
|
shop_open = models.BooleanField(
|
||||||
verbose_name=_('Ticket sale open?'),
|
verbose_name=_('Shop open?'),
|
||||||
help_text=_('Whether tickets are for sale or not.'),
|
help_text=_('Whether the shop is open or not.'),
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
46
shop/admin.py
Normal file
46
shop/admin.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import Order, ProductCategory, Product, OrderProductRelation
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ProductCategory)
|
||||||
|
class ProductCategoryAdmin(admin.ModelAdmin):
|
||||||
|
list_display = [
|
||||||
|
'name',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Product)
|
||||||
|
class ProductAdmin(admin.ModelAdmin):
|
||||||
|
list_display = [
|
||||||
|
'name',
|
||||||
|
'category',
|
||||||
|
'price',
|
||||||
|
'available_in',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ProductInline(admin.TabularInline):
|
||||||
|
model = OrderProductRelation
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Order)
|
||||||
|
class OrderAdmin(admin.ModelAdmin):
|
||||||
|
list_display = [
|
||||||
|
'user',
|
||||||
|
'camp',
|
||||||
|
'payment_method',
|
||||||
|
'paid',
|
||||||
|
]
|
||||||
|
|
||||||
|
list_filter = [
|
||||||
|
'user',
|
||||||
|
'camp',
|
||||||
|
'payment_method',
|
||||||
|
'paid',
|
||||||
|
]
|
||||||
|
|
||||||
|
exclude = ['products']
|
||||||
|
|
||||||
|
inlines = [ProductInline]
|
||||||
|
|
5
shop/apps.py
Normal file
5
shop/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ShopConfig(AppConfig):
|
||||||
|
name = 'shop'
|
218
shop/epayintegration.py
Normal file
218
shop/epayintegration.py
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
### epay callback function
|
||||||
|
@require_safe
|
||||||
|
def api_epay_callback(request):
|
||||||
|
### save GET payload
|
||||||
|
callback = EpayCallback.objects.create(
|
||||||
|
json_payload=json.dumps(request.GET),
|
||||||
|
callback_time=timezone.now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
### find order
|
||||||
|
if 'orderid' in request.GET:
|
||||||
|
### find order type
|
||||||
|
if request.GET['orderid'][0] == "M":
|
||||||
|
### find epay order
|
||||||
|
try:
|
||||||
|
order = EpayOrder.objects.get(id=request.GET['orderid'][1:])
|
||||||
|
except EpayOrder.DoesNotExist:
|
||||||
|
print "epay callback - epayorder %s not found" % request.GET['orderid']
|
||||||
|
return HttpResponse("Not OK")
|
||||||
|
|
||||||
|
### check hash here
|
||||||
|
if 'hash' not in request.GET:
|
||||||
|
print "epay callback - missing epay hash"
|
||||||
|
return HttpResponse("Not OK")
|
||||||
|
|
||||||
|
### this does not work sometimes, ordering is off maybe?
|
||||||
|
hashstring = ''
|
||||||
|
qs = request.META['QUERY_STRING']
|
||||||
|
print "querystring is %s" % qs
|
||||||
|
for kv in qs.split("&"):
|
||||||
|
print "hashstring is now %s" % hashstring
|
||||||
|
if kv.split("=")[0] != "hash":
|
||||||
|
hashstring += kv.split("=")[1]
|
||||||
|
print "hashstring is now %s" % hashstring
|
||||||
|
hashstring += settings.EPAY_MD5_SECRET
|
||||||
|
epayhash = hashlib.md5(hashstring).hexdigest()
|
||||||
|
if epayhash != request.GET['hash']:
|
||||||
|
print "epay callback - wrong epay hash"
|
||||||
|
return HttpResponse("Not OK")
|
||||||
|
|
||||||
|
### save callback in epayorder
|
||||||
|
order.epay_callback = callback
|
||||||
|
if 'txnid' in request.GET:
|
||||||
|
order.epay_txnid=request.GET['txnid']
|
||||||
|
if 'amount' in request.GET:
|
||||||
|
order.epay_amount=int(request.GET['amount'])/100
|
||||||
|
if 'fraud' in request.GET:
|
||||||
|
if request.GET['fraud'] == '1':
|
||||||
|
order.epay_fraud=True
|
||||||
|
else:
|
||||||
|
order.epay_fraud=False
|
||||||
|
|
||||||
|
### delay epayorder depending on user level
|
||||||
|
if settings.ACCOUNT_LEVEL_SETTINGS[str(order.user.profile.account_level)]['MOBILEPAY_DELAY_DAYS'] > 0:
|
||||||
|
order.btc_processing_delayed = True
|
||||||
|
|
||||||
|
### all ok?
|
||||||
|
if order.epay_amount == order.xxx_amount and not order.epay_fraud:
|
||||||
|
order.epay_payment_ok=True
|
||||||
|
|
||||||
|
### save
|
||||||
|
order.save()
|
||||||
|
else:
|
||||||
|
print "epay callback - order %s not recognized" % request.GET['orderid']
|
||||||
|
return HttpResponse("Not OK")
|
||||||
|
return HttpResponse("OK")
|
||||||
|
|
||||||
|
|
||||||
|
### epay order function
|
||||||
|
@login_required
|
||||||
|
def epay_order(request):
|
||||||
|
if 'HTTP_X_FORWARDED_FOR' in request.META:
|
||||||
|
ip = request.META['HTTP_X_FORWARDED_FOR']
|
||||||
|
else:
|
||||||
|
ip = request.META['REMOTE_ADDR']
|
||||||
|
|
||||||
|
country = GeoIP().country(ip)["country_code"]
|
||||||
|
if country not in settings.PERMITTED_EPAY_COUNTRIES:
|
||||||
|
return render(request, 'epay_sepa_only.html')
|
||||||
|
|
||||||
|
### get provision percent
|
||||||
|
provision = get_provision_percent(request.user, "epayorder")
|
||||||
|
|
||||||
|
### convert BTC price to DKK
|
||||||
|
btc_usd_price = CurrencyPrice.objects.get(item='BTCUSD').price
|
||||||
|
btc_dkk_price = ConvertCurrency(btc_usd_price, 'USD', 'DKK')
|
||||||
|
|
||||||
|
### add provision
|
||||||
|
btc_dkk_price_with_provision = btc_dkk_price * provision
|
||||||
|
|
||||||
|
### instantiate form
|
||||||
|
form = EpayOrderForm(request.POST or None, request=request, initial={
|
||||||
|
'currency': request.user.profile.preferred_currency,
|
||||||
|
'btc_address': request.user.profile.btc_address,
|
||||||
|
})
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
### Check exhange rates
|
||||||
|
check_if_exchange_rates_are_too_old()
|
||||||
|
|
||||||
|
### save order
|
||||||
|
try:
|
||||||
|
### save epay order with commit=False
|
||||||
|
epayorder = form.save(commit=False)
|
||||||
|
except Exception as E:
|
||||||
|
print "unable to save epay order with commit=false"
|
||||||
|
return render(request, 'epay_order_fail.html', {
|
||||||
|
'message': _('Unable to save epay order. Please try again, and please contact us if the problem persists.')
|
||||||
|
})
|
||||||
|
|
||||||
|
### set useragent and IP
|
||||||
|
epayorder.useragent = request.META['HTTP_USER_AGENT']
|
||||||
|
epayorder.ip = ip
|
||||||
|
|
||||||
|
### create time
|
||||||
|
epayorder.create_time = timezone.now()
|
||||||
|
|
||||||
|
### set order user
|
||||||
|
epayorder.user = request.user
|
||||||
|
|
||||||
|
### save DKK amount
|
||||||
|
epayorder.dkk_amount = ConvertCurrency(epayorder.xxx_amount, epayorder.currency, 'DKK')
|
||||||
|
|
||||||
|
### set fee
|
||||||
|
epayorder.fee = settings.ACCOUNT_LEVEL_SETTINGS[str(epayorder.user.profile.account_level)]['MOBILEPAY_PROVISION']
|
||||||
|
|
||||||
|
### save epayorder
|
||||||
|
epayorder.save()
|
||||||
|
|
||||||
|
### save btc_address to profile if needed
|
||||||
|
if not request.user.profile.btc_address:
|
||||||
|
request.user.profile.btc_address = epayorder.btc_address
|
||||||
|
request.user.profile.save()
|
||||||
|
|
||||||
|
send_amqp_message("epayorder M%s created by user %s" % (epayorder.id, request.user), "epayorder.create")
|
||||||
|
return HttpResponseRedirect(reverse('epay_epay_form', kwargs={'epayorderid': epayorder.id}))
|
||||||
|
|
||||||
|
### calculate BTC price for the users native currency
|
||||||
|
btc_xxx_price = ConvertCurrency(amount=btc_usd_price, fromcurrency="USD", tocurrency=request.user.profile.preferred_currency or "EUR")
|
||||||
|
|
||||||
|
### add provision
|
||||||
|
btc_xxx_price = btc_xxx_price * provision
|
||||||
|
|
||||||
|
### find out if new orders by this user should be delayed
|
||||||
|
if settings.ACCOUNT_LEVEL_SETTINGS[str(request.user.profile.account_level)]['MOBILEPAY_DELAY_DAYS'] == 0:
|
||||||
|
skipdelay = True
|
||||||
|
else:
|
||||||
|
skipdelay = False
|
||||||
|
|
||||||
|
### delay relevant?
|
||||||
|
if settings.ACCOUNT_LEVEL_SETTINGS[str(request.user.profile.account_level)]['MOBILEPAY_DELAY_DAYS'] > 0:
|
||||||
|
nextlevel_history_delay = settings.ACCOUNT_LEVEL_SETTINGS[str(request.user.profile.account_level+1)]['BUY_HISTORY_DELAY_DAYS']
|
||||||
|
nextlevel_history_amount = settings.ACCOUNT_LEVEL_SETTINGS[str(request.user.profile.account_level+1)]['BUY_HISTORY_MINIMUM']
|
||||||
|
else:
|
||||||
|
nextlevel_history_delay = None
|
||||||
|
nextlevel_history_amount = None
|
||||||
|
|
||||||
|
### render the response
|
||||||
|
return render(request, 'epay_order.html', {
|
||||||
|
'form': form,
|
||||||
|
'btc_xxx_price': round(btc_xxx_price, 2),
|
||||||
|
'currency': request.user.profile.preferred_currency or "EUR",
|
||||||
|
'provision': round((provision-1)*100, 1),
|
||||||
|
'skipdelay': skipdelay,
|
||||||
|
'delaydays': settings.ACCOUNT_LEVEL_SETTINGS[str(request.user.profile.account_level)]['MOBILEPAY_DELAY_DAYS'],
|
||||||
|
'daily_limit': settings.ACCOUNT_LEVEL_SETTINGS[str(request.user.profile.account_level)]['MOBILEPAY_LIMIT_DAY'],
|
||||||
|
'nextlevel_history_delay': nextlevel_history_delay,
|
||||||
|
'nextlevel_history_amount': nextlevel_history_amount,
|
||||||
|
'btc_address': request.user.profile.btc_address,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
### the page which shows the epay epay form
|
||||||
|
@login_required
|
||||||
|
def epay_epay_form(request, epayorderid):
|
||||||
|
epayorder = get_object_or_404(EpayOrder, id=epayorderid, user=request.user, user_cancelled=False, epay_callback__isnull=True)
|
||||||
|
accepturl = request.build_absolute_uri(reverse('epay_thanks', kwargs={'epayorderid': epayorder.id})).replace('http://', 'https://')
|
||||||
|
description = str(request.user.id)
|
||||||
|
hashstring = settings.EPAY_MERCHANT_NUMBER+description+'11'+str(epayorder.xxx_amount*100)+str(epayorder.currency)+'M'+str(epayorder.id)+accepturl+settings.EPAY_MD5_SECRET
|
||||||
|
epayhash = hashlib.md5(hashstring).hexdigest()
|
||||||
|
return render(request, 'epay_form.html', {
|
||||||
|
'orderid': 'M%s' % epayorder.id,
|
||||||
|
'description': description,
|
||||||
|
'merchantnumber': settings.EPAY_MERCHANT_NUMBER,
|
||||||
|
'amount': epayorder.xxx_amount*100,
|
||||||
|
'currency': epayorder.currency,
|
||||||
|
'epayhash': epayhash,
|
||||||
|
'accepturl': accepturl,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
### after epay payment thanks page
|
||||||
|
@login_required
|
||||||
|
def epay_thanks(request, epayorderid):
|
||||||
|
### get order - if it is owned by this user and uncancelled
|
||||||
|
epayorder = get_object_or_404(EpayOrder, id=epayorderid, user=request.user, user_cancelled=False)
|
||||||
|
|
||||||
|
### check for querystring
|
||||||
|
if request.GET:
|
||||||
|
### update order to register thanks page view
|
||||||
|
epayorder.thanks_page_view_time=timezone.now()
|
||||||
|
epayorder.save()
|
||||||
|
|
||||||
|
### redirect to get rid of GET querystring
|
||||||
|
return HttpResponseRedirect(reverse('epay_thanks', kwargs={'epayorderid': epayorder.id}))
|
||||||
|
|
||||||
|
if epayorder.btc_processing_delayed:
|
||||||
|
minamount = ConvertLimit(settings.ACCOUNT_LEVEL_SETTINGS[str(request.user.profile.account_level+1)]['BUY_HISTORY_MINIMUM'], epayorder.currency)
|
||||||
|
delaydays = settings.ACCOUNT_LEVEL_SETTINGS[str(request.user.profile.account_level)]['MOBILEPAY_DELAY_DAYS']
|
||||||
|
else:
|
||||||
|
minamount = None
|
||||||
|
delaydays = None
|
||||||
|
|
||||||
|
return render(request, 'epay_thanks.html', {
|
||||||
|
'order': epayorder,
|
||||||
|
'delaydays': delaydays,
|
||||||
|
'minamount': minamount,
|
||||||
|
})
|
1
shop/forms.py
Normal file
1
shop/forms.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from django import forms
|
|
@ -4,7 +4,7 @@ from django.db.models import QuerySet
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
class TicketTypeQuerySet(QuerySet):
|
class ProductQuerySet(QuerySet):
|
||||||
|
|
||||||
def available(self):
|
def available(self):
|
||||||
now = timezone.now()
|
now = timezone.now()
|
127
shop/migrations/0001_initial.py
Normal file
127
shop/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.6 on 2016-05-10 20:00
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.contrib.postgres.fields.jsonb
|
||||||
|
import django.contrib.postgres.fields.ranges
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('camps', '0004_camp_ticket_sale_open'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EpayCallback',
|
||||||
|
fields=[
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated', models.DateTimeField(auto_now=True)),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('payload', django.contrib.postgres.fields.jsonb.JSONField()),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Epay Callback',
|
||||||
|
'verbose_name_plural': 'Epay Callbacks',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EpayPayment',
|
||||||
|
fields=[
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated', models.DateTimeField(auto_now=True)),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('txnid', models.IntegerField()),
|
||||||
|
('callback', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.EpayCallback')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Epay Payment',
|
||||||
|
'verbose_name_plural': 'Epay Payments',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Order',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated', models.DateTimeField(auto_now=True)),
|
||||||
|
('paid', models.BooleanField(default=False, help_text='Whether this order has been paid.', verbose_name='Paid?')),
|
||||||
|
('payment_method', models.CharField(choices=[(b'credit_card', b'Credit card'), (b'altcoin', b'Altcoin'), (b'bank_transfer', b'Bank transfer')], default=b'credit_card', max_length=50)),
|
||||||
|
('camp', models.ForeignKey(help_text='The camp this order is for.', on_delete=django.db.models.deletion.CASCADE, to='camps.Camp', verbose_name='Camp')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OrderProductRelation',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', models.PositiveIntegerField()),
|
||||||
|
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.Order')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Product',
|
||||||
|
fields=[
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated', models.DateTimeField(auto_now=True)),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=150)),
|
||||||
|
('price', models.IntegerField(help_text='Price of the ticket (in DKK).')),
|
||||||
|
('description', models.TextField()),
|
||||||
|
('available_in', django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text='Which period is this ticket available for purchase? | (Format: YYYY-MM-DD HH:MM) | Only one of start/end is required')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['available_in'],
|
||||||
|
'verbose_name': 'Product',
|
||||||
|
'verbose_name_plural': 'Products',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ProductCategory',
|
||||||
|
fields=[
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated', models.DateTimeField(auto_now=True)),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=150)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Product category',
|
||||||
|
'verbose_name_plural': 'Product categories',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='category',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.ProductCategory'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='orderproductrelation',
|
||||||
|
name='product',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.Product'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='products',
|
||||||
|
field=models.ManyToManyField(through='shop.OrderProductRelation', to='shop.Product'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(help_text='The user this order belongs to.', on_delete=django.db.models.deletion.CASCADE, related_name='orders', to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='epaypayment',
|
||||||
|
name='order',
|
||||||
|
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='shop.Order'),
|
||||||
|
),
|
||||||
|
]
|
20
shop/migrations/0002_orderproductrelation_handed_out.py
Normal file
20
shop/migrations/0002_orderproductrelation_handed_out.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.6 on 2016-05-10 20:02
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('shop', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='orderproductrelation',
|
||||||
|
name='handed_out',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
|
@ -2,34 +2,36 @@ from django.db import models
|
||||||
from django.contrib.postgres.fields import DateTimeRangeField, JSONField
|
from django.contrib.postgres.fields import DateTimeRangeField, JSONField
|
||||||
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 django.core.urlresolvers import reverse_lazy
|
|
||||||
|
|
||||||
from bornhack.utils import CreatedUpdatedModel, UUIDModel
|
from bornhack.utils import CreatedUpdatedModel, UUIDModel
|
||||||
|
|
||||||
from .managers import TicketTypeQuerySet
|
from .managers import ProductQuerySet
|
||||||
|
|
||||||
|
|
||||||
class Ticket(CreatedUpdatedModel, UUIDModel):
|
class Order(CreatedUpdatedModel):
|
||||||
class Meta:
|
|
||||||
verbose_name = _('Ticket')
|
products = models.ManyToManyField(
|
||||||
verbose_name_plural = _('Tickets')
|
'shop.Product',
|
||||||
|
through='shop.OrderProductRelation'
|
||||||
|
)
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
'auth.User',
|
'auth.User',
|
||||||
verbose_name=_('User'),
|
verbose_name=_('User'),
|
||||||
help_text=_('The user this ticket belongs to.'),
|
help_text=_('The user this order belongs to.'),
|
||||||
related_name='tickets',
|
related_name='orders',
|
||||||
)
|
)
|
||||||
|
|
||||||
paid = models.BooleanField(
|
paid = models.BooleanField(
|
||||||
verbose_name=_('Paid?'),
|
verbose_name=_('Paid?'),
|
||||||
help_text=_('Whether the user has paid.'),
|
help_text=_('Whether this order has been paid.'),
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
ticket_type = models.ForeignKey(
|
camp = models.ForeignKey(
|
||||||
'tickets.TicketType',
|
'camps.Camp',
|
||||||
verbose_name=_('Ticket type'),
|
verbose_name=_('Camp'),
|
||||||
|
help_text=_('The camp this order is for.'),
|
||||||
)
|
)
|
||||||
|
|
||||||
CREDIT_CARD = 'credit_card'
|
CREDIT_CARD = 'credit_card'
|
||||||
|
@ -48,44 +50,42 @@ class Ticket(CreatedUpdatedModel, UUIDModel):
|
||||||
default=CREDIT_CARD
|
default=CREDIT_CARD
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return '{} ({})'.format(
|
|
||||||
self.ticket_type.name,
|
|
||||||
self.ticket_type.camp
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
class ProductCategory(CreatedUpdatedModel, UUIDModel):
|
||||||
return reverse_lazy('tickets:detail', kwargs={
|
|
||||||
'pk': self.pk
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class TicketType(CreatedUpdatedModel, UUIDModel):
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _('Ticket Type')
|
verbose_name = 'Product category'
|
||||||
verbose_name_plural = _('Ticket Types')
|
verbose_name_plural = 'Product categories'
|
||||||
ordering = ['available_in']
|
|
||||||
|
|
||||||
name = models.CharField(max_length=150)
|
name = models.CharField(max_length=150)
|
||||||
|
|
||||||
camp = models.ForeignKey(
|
def __str__(self):
|
||||||
'camps.Camp',
|
return self.name
|
||||||
verbose_name=_('Camp'),
|
|
||||||
help_text=_('The camp this ticket type is for.'),
|
|
||||||
)
|
class Product(CreatedUpdatedModel, UUIDModel):
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Product'
|
||||||
|
verbose_name_plural = 'Products'
|
||||||
|
ordering = ['available_in']
|
||||||
|
|
||||||
|
category = models.ForeignKey('shop.ProductCategory')
|
||||||
|
|
||||||
|
name = models.CharField(max_length=150)
|
||||||
|
|
||||||
price = models.IntegerField(
|
price = models.IntegerField(
|
||||||
help_text=_('Price of the ticket (in DKK).')
|
help_text=_('Price of the product (in DKK).')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
description = models.TextField()
|
||||||
|
|
||||||
available_in = DateTimeRangeField(
|
available_in = DateTimeRangeField(
|
||||||
help_text=_(
|
help_text=_(
|
||||||
'Which period is this ticket available for purchase? | '
|
'Which period is this product available for purchase? | '
|
||||||
'(Format: YYYY-MM-DD HH:MM) | Only one of start/end is required'
|
'(Format: YYYY-MM-DD HH:MM) | Only one of start/end is required'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = TicketTypeQuerySet.as_manager()
|
objects = ProductQuerySet.as_manager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{} ({} DKK)'.format(
|
return '{} ({} DKK)'.format(
|
||||||
|
@ -98,6 +98,13 @@ class TicketType(CreatedUpdatedModel, UUIDModel):
|
||||||
return now in self.available_in
|
return now in self.available_in
|
||||||
|
|
||||||
|
|
||||||
|
class OrderProductRelation(models.Model):
|
||||||
|
order = models.ForeignKey('shop.Order')
|
||||||
|
product = models.ForeignKey('shop.Product')
|
||||||
|
quantity = models.PositiveIntegerField()
|
||||||
|
handed_out = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
class EpayCallback(CreatedUpdatedModel, UUIDModel):
|
class EpayCallback(CreatedUpdatedModel, UUIDModel):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'Epay Callback'
|
verbose_name = 'Epay Callback'
|
||||||
|
@ -110,6 +117,6 @@ class EpayPayment(CreatedUpdatedModel, UUIDModel):
|
||||||
verbose_name = 'Epay Payment'
|
verbose_name = 'Epay Payment'
|
||||||
verbose_name_plural = 'Epay Payments'
|
verbose_name_plural = 'Epay Payments'
|
||||||
|
|
||||||
ticket = models.OneToOneField('tickets.Ticket')
|
order = models.OneToOneField('shop.Order')
|
||||||
callback = models.ForeignKey('tickets.EpayCallback')
|
callback = models.ForeignKey('shop.EpayCallback')
|
||||||
txnid = models.IntegerField()
|
txnid = models.IntegerField()
|
|
@ -22,21 +22,21 @@ Here you can see the different ticket types, their prices and availability.
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
{% for ticket_type in ticket_types %}
|
{% for product in tickets %}
|
||||||
|
|
||||||
<tr {% if not ticket_type.is_available %}style="color: lightgrey"{%endif%}>
|
<tr {% if not product.is_available %}style="color: lightgrey"{%endif%}>
|
||||||
<td>
|
<td>
|
||||||
{{ ticket_type.name }}
|
{{ product.name }}
|
||||||
<td>
|
<td>
|
||||||
{{ ticket_type.price }} DKK
|
{{ product.price }} DKK
|
||||||
<td>
|
<td>
|
||||||
{{ ticket_type.available_in.lower }}
|
{{ product.available_in.lower }}
|
||||||
{% if ticket_type.available_in.upper %}
|
{% if product.available_in.upper %}
|
||||||
- {{ ticket_type.available_in.upper }}
|
- {{ product.available_in.upper }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
{% if ticket_type.is_available %}
|
{% if product.is_available %}
|
||||||
<a href="{% url 'tickets:order' %}?ticket_type={{ ticket_type.pk }}">
|
<a href="">
|
||||||
Order
|
Order
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -47,7 +47,7 @@ Here you can see the different ticket types, their prices and availability.
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<hr />
|
<hr />
|
||||||
<h3>Your tickets</h3>
|
<h3>Your tickets</h3>
|
||||||
|
@ -71,5 +71,6 @@ Here you can see the different ticket types, their prices and availability.
|
||||||
<a href="{% url 'account_signup' %}?next={% url 'tickets:index' %}">Sign up</a> or
|
<a href="{% url 'account_signup' %}?next={% url 'tickets:index' %}">Sign up</a> or
|
||||||
<a href="{% url 'account_login' %}?next={% url 'tickets:index' %}">login</a> to buy tickets.
|
<a href="{% url 'account_login' %}?next={% url 'tickets:index' %}">login</a> to buy tickets.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
27
shop/urls.py
Normal file
27
shop/urls.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from .views import (
|
||||||
|
ShopIndexView,
|
||||||
|
# EpayView,
|
||||||
|
# EpayCallbackView,
|
||||||
|
)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
#url(r'order/$', TicketOrderView.as_view(), name='order'),
|
||||||
|
#url(
|
||||||
|
#r'pay/credit_card/(?P<ticket_id>[a-zA-Z0-9\-]+)/$',
|
||||||
|
#EpayView.as_view(),
|
||||||
|
#name='epay_form'
|
||||||
|
#),
|
||||||
|
#url(
|
||||||
|
#r'epay_callback/',
|
||||||
|
#EpayCallbackView,
|
||||||
|
#name='epay_callback'
|
||||||
|
#),
|
||||||
|
#url(
|
||||||
|
#r'detail/(?P<pk>[a-zA-Z0-9\-]+)/$',
|
||||||
|
#TicketDetailView.as_view(),
|
||||||
|
#name='detail'
|
||||||
|
#),
|
||||||
|
url(r'$', ShopIndexView.as_view(), name='index'),
|
||||||
|
]
|
102
shop/views.py
Normal file
102
shop/views.py
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from django.http import HttpResponseRedirect, Http404
|
||||||
|
from django.views.generic import CreateView, TemplateView, DetailView, View
|
||||||
|
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 .models import Order, Product, EpayCallback, EpayPayment
|
||||||
|
|
||||||
|
|
||||||
|
class ShopIndexView(TemplateView):
|
||||||
|
template_name = "shop/index.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(ShopIndexView, self).get_context_data(**kwargs)
|
||||||
|
context['tickets'] = Product.objects.filter(category__name='Tickets')
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class ProductDetailView(LoginRequiredMixin, DetailView):
|
||||||
|
model = Product
|
||||||
|
template_name = 'product/detail.html'
|
||||||
|
context_object_name = 'product'
|
||||||
|
|
||||||
|
|
||||||
|
# class EpayView(TemplateView):
|
||||||
|
# template_name = 'tickets/epay_form.html'
|
||||||
|
#
|
||||||
|
# def get_context_data(self, **kwargs):
|
||||||
|
# ticket = Ticket.objects.get(pk=kwargs.get('ticket_id'))
|
||||||
|
# accept_url = ticket.get_absolute_url()
|
||||||
|
# amount = ticket.ticket_type.price * 100
|
||||||
|
# order_id = str(ticket.pk)
|
||||||
|
# description = str(ticket.user.pk)
|
||||||
|
#
|
||||||
|
# hashstring = (
|
||||||
|
# '{merchant_number}{description}11{amount}DKK'
|
||||||
|
# '{order_id}{accept_url}{md5_secret}'
|
||||||
|
# ).format(
|
||||||
|
# merchant_number=settings.EPAY_MERCHANT_NUMBER,
|
||||||
|
# description=description,
|
||||||
|
# amount=str(amount),
|
||||||
|
# order_id=str(order_id),
|
||||||
|
# accept_url=accept_url,
|
||||||
|
# md5_secret=settings.EPAY_MD5_SECRET,
|
||||||
|
# )
|
||||||
|
# epay_hash = hashlib.md5(hashstring).hexdigest()
|
||||||
|
#
|
||||||
|
# context = super(EpayView, self).get_context_data(**kwargs)
|
||||||
|
# context['merchant_number'] = settings.EPAY_MERCHANT_NUMBER
|
||||||
|
# context['description'] = description
|
||||||
|
# context['order_id'] = order_id
|
||||||
|
# context['accept_url'] = accept_url
|
||||||
|
# context['amount'] = amount
|
||||||
|
# context['epay_hash'] = epay_hash
|
||||||
|
# return context
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# class EpayCallbackView(View):
|
||||||
|
#
|
||||||
|
# def get(self, request, **kwargs):
|
||||||
|
#
|
||||||
|
# callback = EpayCallback.objects.create(
|
||||||
|
# payload=request.GET
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# if 'orderid' in request.GET:
|
||||||
|
# ticket = Ticket.objects.get(pk=request.GET.get('order_id'))
|
||||||
|
# query = dict(
|
||||||
|
# map(
|
||||||
|
# lambda x: tuple(x.split('=')),
|
||||||
|
# request.META['QUERY_STRING'].split('&')
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# hashstring = (
|
||||||
|
# '{merchant_number}{description}11{amount}DKK'
|
||||||
|
# '{order_id}{accept_url}{md5_secret}'
|
||||||
|
# ).format(
|
||||||
|
# merchant_number=query.get('merchantnumber'),
|
||||||
|
# description=query.get('description'),
|
||||||
|
# amount=query.get('amount'),
|
||||||
|
# order_id=query.get('orderid'),
|
||||||
|
# accept_url=query.get('accepturl'),
|
||||||
|
# md5_secret=settings.EPAY_MD5_SECRET,
|
||||||
|
# )
|
||||||
|
# epay_hash = hashlib.md5(hashstring).hexdigest()
|
||||||
|
#
|
||||||
|
# if not epay_hash == request.GET['hash']:
|
||||||
|
# return HttpResponse(status=400)
|
||||||
|
#
|
||||||
|
# EpayPayment.objects.create(
|
||||||
|
# ticket=ticket,
|
||||||
|
# callback=callback,
|
||||||
|
# txnid=request.GET['txnid'],
|
||||||
|
# )
|
||||||
|
# else:
|
||||||
|
# return HttpResponse(status=400)
|
||||||
|
#
|
||||||
|
# return HttpResponse('OK')
|
|
@ -1,29 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
from .models import Ticket, TicketType
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Ticket)
|
|
||||||
class TicketAdmin(admin.ModelAdmin):
|
|
||||||
list_display = [
|
|
||||||
'user',
|
|
||||||
'ticket_type',
|
|
||||||
'payment_method',
|
|
||||||
'paid',
|
|
||||||
]
|
|
||||||
|
|
||||||
list_filter = [
|
|
||||||
'paid',
|
|
||||||
'ticket_type',
|
|
||||||
'payment_method',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(TicketType)
|
|
||||||
class TicketTypeAdmin(admin.ModelAdmin):
|
|
||||||
list_display = [
|
|
||||||
'name',
|
|
||||||
'price',
|
|
||||||
'available_in',
|
|
||||||
'camp',
|
|
||||||
]
|
|
|
@ -1,5 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class TicketsConfig(AppConfig):
|
|
||||||
name = 'tickets'
|
|
|
@ -1,21 +0,0 @@
|
||||||
from django import forms
|
|
||||||
from .models import Ticket, TicketType
|
|
||||||
|
|
||||||
|
|
||||||
class TicketForm(forms.ModelForm):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Ticket
|
|
||||||
fields = [
|
|
||||||
'ticket_type',
|
|
||||||
'payment_method',
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
'payment_method': forms.RadioSelect()
|
|
||||||
}
|
|
||||||
|
|
||||||
ticket_type = forms.ModelChoiceField(
|
|
||||||
queryset=TicketType.objects.available()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.9.2 on 2016-04-22 20:19
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
import django.contrib.postgres.fields.ranges
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('camps', '0003_auto_20160422_2019'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Ticket',
|
|
||||||
fields=[
|
|
||||||
('created', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('updated', models.DateTimeField(auto_now=True)),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
||||||
('paid', models.BooleanField(default=False, help_text='Whether the user has paid.', verbose_name='Paid?')),
|
|
||||||
('camp', models.ForeignKey(help_text='The camp this ticket is for.', on_delete=django.db.models.deletion.CASCADE, to='camps.Camp', verbose_name='Camp')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name_plural': 'Tickets',
|
|
||||||
'verbose_name': 'Ticket',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='TicketType',
|
|
||||||
fields=[
|
|
||||||
('created', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('updated', models.DateTimeField(auto_now=True)),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
||||||
('name', models.CharField(max_length=100)),
|
|
||||||
('price', models.IntegerField(help_text='Price of the ticket (in DKK).')),
|
|
||||||
('available_in', django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text='Which period is this ticket available for purchase?')),
|
|
||||||
('camp', models.ForeignKey(help_text='The camp this ticket type is for.', on_delete=django.db.models.deletion.CASCADE, to='camps.Camp', verbose_name='Camp')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name_plural': 'Ticket Types',
|
|
||||||
'verbose_name': 'Ticket Type',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='ticket',
|
|
||||||
name='ticket_type',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tickets.TicketType'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='ticket',
|
|
||||||
name='user',
|
|
||||||
field=models.ForeignKey(help_text='The user this ticket belongs to.', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,36 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.9.5 on 2016-05-06 16:02
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import django.contrib.postgres.fields.ranges
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('tickets', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='ticket',
|
|
||||||
name='camp',
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='ticket',
|
|
||||||
name='ticket_type',
|
|
||||||
field=models.ForeignKey(help_text='The type of the ticket.', on_delete=django.db.models.deletion.CASCADE, to='tickets.TicketType', verbose_name='Ticket type'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='tickettype',
|
|
||||||
name='available_in',
|
|
||||||
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text='Which period is this ticket available for purchase? | (Format: YYYY-MM-DD HH:MM) | Only one of start/end is required'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='tickettype',
|
|
||||||
name='name',
|
|
||||||
field=models.CharField(max_length=150),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,31 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.9.5 on 2016-05-06 20:16
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('tickets', '0002_auto_20160506_1602'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='tickettype',
|
|
||||||
options={'ordering': ['available_in'], 'verbose_name': 'Ticket Type', 'verbose_name_plural': 'Ticket Types'},
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='ticket',
|
|
||||||
name='ticket_type',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tickets.TicketType', verbose_name='Ticket type'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='ticket',
|
|
||||||
name='user',
|
|
||||||
field=models.ForeignKey(help_text='The user this ticket belongs to.', on_delete=django.db.models.deletion.CASCADE, related_name='tickets', to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,20 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.9.5 on 2016-05-08 11:21
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('tickets', '0003_auto_20160506_2016'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='ticket',
|
|
||||||
name='payment_method',
|
|
||||||
field=models.CharField(choices=[('credit_card', 'Credit card'), ('altcoin', 'Altcoin'), ('bank_transfer', 'Bank transfer')], default='credit_card', max_length=50),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,29 +0,0 @@
|
||||||
from django.conf.urls import url
|
|
||||||
|
|
||||||
from .views import (
|
|
||||||
TicketIndexView,
|
|
||||||
TicketOrderView,
|
|
||||||
TicketDetailView,
|
|
||||||
EpayView,
|
|
||||||
EpayCallbackView,
|
|
||||||
)
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
url(r'order/$', TicketOrderView.as_view(), name='order'),
|
|
||||||
url(
|
|
||||||
r'pay/credit_card/(?P<ticket_id>[a-zA-Z0-9\-]+)/$',
|
|
||||||
EpayView.as_view(),
|
|
||||||
name='epay_form'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'epay_callback/',
|
|
||||||
EpayCallbackView,
|
|
||||||
name='epay_callback'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'detail/(?P<pk>[a-zA-Z0-9\-]+)/$',
|
|
||||||
TicketDetailView.as_view(),
|
|
||||||
name='detail'
|
|
||||||
),
|
|
||||||
url(r'$', TicketIndexView.as_view(), name='index'),
|
|
||||||
]
|
|
150
tickets/views.py
150
tickets/views.py
|
@ -1,150 +0,0 @@
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from django.http import HttpResponseRedirect, Http404
|
|
||||||
from django.views.generic import CreateView, TemplateView, DetailView, View
|
|
||||||
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 camps.models import Camp
|
|
||||||
|
|
||||||
from .models import Ticket, TicketType, EpayPayment, EpayCallback
|
|
||||||
from .forms import TicketForm
|
|
||||||
|
|
||||||
|
|
||||||
class CampTicketSaleCheck(object):
|
|
||||||
def dispatch(self, *args, **kwargs):
|
|
||||||
current_camp = Camp.objects.current()
|
|
||||||
if current_camp and current_camp.ticket_sale_open:
|
|
||||||
return super(CampTicketSaleCheck, self).dispatch(*args, **kwargs)
|
|
||||||
raise Http404()
|
|
||||||
|
|
||||||
|
|
||||||
class TicketIndexView(CampTicketSaleCheck, TemplateView):
|
|
||||||
template_name = "tickets/index.html"
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(TicketIndexView, self).get_context_data(**kwargs)
|
|
||||||
context['ticket_types'] = TicketType.objects.all()
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class TicketDetailView(LoginRequiredMixin, CampTicketSaleCheck, DetailView):
|
|
||||||
model = Ticket
|
|
||||||
template_name = 'tickets/detail.html'
|
|
||||||
context_object_name = 'ticket'
|
|
||||||
|
|
||||||
|
|
||||||
class TicketOrderView(LoginRequiredMixin, CampTicketSaleCheck, CreateView):
|
|
||||||
model = Ticket
|
|
||||||
template_name = "tickets/order.html"
|
|
||||||
form_class = TicketForm
|
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
|
||||||
kwargs = super(TicketOrderView, self).get_form_kwargs()
|
|
||||||
ticket_type = self.request.GET.get('ticket_type', None)
|
|
||||||
if ticket_type:
|
|
||||||
kwargs['initial'] = {
|
|
||||||
'ticket_type': ticket_type
|
|
||||||
}
|
|
||||||
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
instance = form.save(commit=False)
|
|
||||||
instance.user = self.request.user
|
|
||||||
instance.save()
|
|
||||||
|
|
||||||
if instance.payment_method == Ticket.ALTCOIN:
|
|
||||||
return HttpResponse('Altcoin')
|
|
||||||
|
|
||||||
if instance.payment_method == Ticket.CREDIT_CARD:
|
|
||||||
return HttpResponseRedirect(
|
|
||||||
reverse_lazy('tickets:epay_form', kwargs={
|
|
||||||
'ticket_id': str(instance.pk)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
return HttpResponseRedirect(
|
|
||||||
reverse_lazy('tickets:detail', kwargs={
|
|
||||||
'pk': str(instance.pk)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EpayView(TemplateView):
|
|
||||||
template_name = 'tickets/epay_form.html'
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
ticket = Ticket.objects.get(pk=kwargs.get('ticket_id'))
|
|
||||||
accept_url = ticket.get_absolute_url()
|
|
||||||
amount = ticket.ticket_type.price * 100
|
|
||||||
order_id = str(ticket.pk)
|
|
||||||
description = str(ticket.user.pk)
|
|
||||||
|
|
||||||
hashstring = (
|
|
||||||
'{merchant_number}{description}11{amount}DKK'
|
|
||||||
'{order_id}{accept_url}{md5_secret}'
|
|
||||||
).format(
|
|
||||||
merchant_number=settings.EPAY_MERCHANT_NUMBER,
|
|
||||||
description=description,
|
|
||||||
amount=str(amount),
|
|
||||||
order_id=str(order_id),
|
|
||||||
accept_url=accept_url,
|
|
||||||
md5_secret=settings.EPAY_MD5_SECRET,
|
|
||||||
)
|
|
||||||
epay_hash = hashlib.md5(hashstring).hexdigest()
|
|
||||||
|
|
||||||
context = super(EpayView, self).get_context_data(**kwargs)
|
|
||||||
context['merchant_number'] = settings.EPAY_MERCHANT_NUMBER
|
|
||||||
context['description'] = description
|
|
||||||
context['order_id'] = order_id
|
|
||||||
context['accept_url'] = accept_url
|
|
||||||
context['amount'] = amount
|
|
||||||
context['epay_hash'] = epay_hash
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class EpayCallbackView(View):
|
|
||||||
|
|
||||||
def get(self, request, **kwargs):
|
|
||||||
|
|
||||||
callback = EpayCallback.objects.create(
|
|
||||||
payload=request.GET
|
|
||||||
)
|
|
||||||
|
|
||||||
if 'orderid' in request.GET:
|
|
||||||
ticket = Ticket.objects.get(pk=request.GET.get('order_id'))
|
|
||||||
query = dict(
|
|
||||||
map(
|
|
||||||
lambda x: tuple(x.split('=')),
|
|
||||||
request.META['QUERY_STRING'].split('&')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
hashstring = (
|
|
||||||
'{merchant_number}{description}11{amount}DKK'
|
|
||||||
'{order_id}{accept_url}{md5_secret}'
|
|
||||||
).format(
|
|
||||||
merchant_number=query.get('merchantnumber'),
|
|
||||||
description=query.get('description'),
|
|
||||||
amount=query.get('amount'),
|
|
||||||
order_id=query.get('orderid'),
|
|
||||||
accept_url=query.get('accepturl'),
|
|
||||||
md5_secret=settings.EPAY_MD5_SECRET,
|
|
||||||
)
|
|
||||||
epay_hash = hashlib.md5(hashstring).hexdigest()
|
|
||||||
|
|
||||||
if not epay_hash == request.GET['hash']:
|
|
||||||
return HttpResponse(status=400)
|
|
||||||
|
|
||||||
EpayPayment.objects.create(
|
|
||||||
ticket=ticket,
|
|
||||||
callback=callback,
|
|
||||||
txnid=request.GET['txnid'],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return HttpResponse(status=400)
|
|
||||||
|
|
||||||
return HttpResponse('OK')
|
|
Loading…
Reference in a new issue