Merge pull request #150 from bornhack/tickets

Tickets
This commit is contained in:
Stephan Telling 2017-08-17 18:51:56 +02:00 committed by GitHub
commit e828bc9c4a
16 changed files with 365 additions and 16 deletions

View file

@ -42,6 +42,7 @@ INSTALLED_APPS = [
'ircbot',
'teams',
'people',
'tickets',
'allauth',
'allauth.account',

View file

@ -10,6 +10,7 @@ admin.site.register(models.CoinifyAPIRequest)
admin.site.register(models.Invoice)
admin.site.register(models.CreditNote)
@admin.register(models.CustomOrder)
class CustomOrderAdmin(admin.ModelAdmin):
list_display = [
@ -25,6 +26,7 @@ class CustomOrderAdmin(admin.ModelAdmin):
'paid',
]
@admin.register(models.ProductCategory)
class ProductCategoryAdmin(admin.ModelAdmin):
list_display = [
@ -37,6 +39,7 @@ class ProductAdmin(admin.ModelAdmin):
list_display = [
'name',
'category',
'ticket_type',
'price',
'available_in',
]
@ -115,7 +118,6 @@ class TicketModelAdmin(admin.ModelAdmin):
list_filter = ['product', 'checked_in']
actions = ['mark_as_arrived']
def mark_as_arrived(self, request, queryset):

View file

@ -1,5 +1,5 @@
from django.core.files import File
from shop.pdf import generate_pdf_letter
from utils.pdf import generate_pdf_letter
from shop.email import add_invoice_email, add_creditnote_email
from shop.models import Order, CustomOrder, Invoice, CreditNote
import logging

View file

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-08-17 14:22
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tickets', '0001_initial'),
('shop', '0047_auto_20170522_1942'),
]
operations = [
migrations.AddField(
model_name='product',
name='ticket_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tickets.TicketType'),
),
]

View file

@ -1,21 +1,28 @@
import io
import logging
import hashlib
import base64
import qrcode
from django.conf import settings
from django.db import models
from django.db.models.aggregates import Sum
from django.contrib import messages
from django.contrib.postgres.fields import DateTimeRangeField, JSONField
from django.http import HttpResponse
from django.utils.text import slugify
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.core.urlresolvers import reverse_lazy
from utils.models import UUIDModel, CreatedUpdatedModel
from .managers import ProductQuerySet, OrderQuerySet
import hashlib, io, base64, qrcode
from decimal import Decimal
from datetime import timedelta
from unidecode import unidecode
from django.utils.dateparse import parse_datetime
from django.utils import timezone
from utils.models import UUIDModel, CreatedUpdatedModel
from tickets.models import ShopTicket
from .managers import ProductQuerySet, OrderQuerySet
logger = logging.getLogger("bornhack.%s" % __name__)
class CustomOrder(CreatedUpdatedModel):
@ -77,7 +84,7 @@ class Order(CreatedUpdatedModel):
CREDIT_CARD = 'credit_card'
BLOCKCHAIN = 'blockchain'
BANK_TRANSFER = 'bank_transfer'
CASH = 'cash'
CASH = 'cash'
PAYMENT_METHODS = [
CREDIT_CARD,
@ -115,7 +122,6 @@ class Order(CreatedUpdatedModel):
blank=True,
)
objects = OrderQuerySet.as_manager()
def __str__(self):
@ -176,7 +182,8 @@ class Order(CreatedUpdatedModel):
for order_product in self.orderproductrelation_set.all():
if order_product.product.category.name == "Tickets":
for _ in range(0, order_product.quantity):
ticket = Ticket(
ticket = ShopTicket(
ticket_type=order_product.product.ticket_type,
order=self,
product=order_product.product,
)
@ -291,6 +298,12 @@ class Product(CreatedUpdatedModel, UUIDModel):
)
)
ticket_type = models.ForeignKey(
'tickets.TicketType',
null=True,
blank=True
)
objects = ProductQuerySet.as_manager()
def __str__(self):
@ -514,4 +527,3 @@ class Ticket(CreatedUpdatedModel, UUIDModel):
def get_absolute_url(self):
return str(reverse_lazy('shop:ticket_detail', kwargs={'pk': self.pk}))

0
src/tickets/__init__.py Normal file
View file

38
src/tickets/admin.py Normal file
View file

@ -0,0 +1,38 @@
from django.contrib import admin
from .models import (
TicketType,
SponsorTicket,
DiscountTicket,
ShopTicket
)
class BaseTicketAdmin(admin.ModelAdmin):
actions = ['generate_pdf']
exclude = ['qrcode_base64']
def generate_pdf(self, request, queryset):
for ticket in queryset.all():
ticket.generate_pdf()
generate_pdf.description = 'Generate PDF for the ticket'
@admin.register(TicketType)
class TicketTypeAdmin(admin.ModelAdmin):
pass
@admin.register(SponsorTicket)
class SponsorTicketAdmin(BaseTicketAdmin):
pass
@admin.register(DiscountTicket)
class DiscountTicketAdmin(BaseTicketAdmin):
pass
@admin.register(ShopTicket)
class ShopTicketAdmin(BaseTicketAdmin):
pass

5
src/tickets/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class TicketsConfig(AppConfig):
name = 'tickets'

View file

@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-08-17 14:22
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
('shop', '0047_auto_20170522_1942'),
('camps', '0022_camp_colour'),
('sponsors', '0006_auto_20170715_1110'),
]
operations = [
migrations.CreateModel(
name='BaseTicket',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('qrcode_base64', models.TextField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='TicketType',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('name', models.TextField()),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='DiscountTicket',
fields=[
('baseticket_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='tickets.BaseTicket')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('price', models.IntegerField(help_text='Price of the discounted ticket (in DKK, including VAT).')),
],
options={
'abstract': False,
},
bases=('tickets.baseticket', models.Model),
),
migrations.CreateModel(
name='ShopTicket',
fields=[
('baseticket_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='tickets.BaseTicket')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('name', models.CharField(blank=True, help_text='Name of the person this ticket belongs to. This can be different from the buying user.', max_length=100, null=True)),
('email', models.EmailField(blank=True, max_length=254, null=True)),
('checked_in', models.BooleanField(default=False)),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shoptickets', to='shop.Order')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.Product')),
],
options={
'abstract': False,
},
bases=('tickets.baseticket', models.Model),
),
migrations.CreateModel(
name='SponsorTicket',
fields=[
('baseticket_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='tickets.BaseTicket')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('sponsor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sponsors.Sponsor')),
],
options={
'abstract': False,
},
bases=('tickets.baseticket', models.Model),
),
migrations.AddField(
model_name='baseticket',
name='camp',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='camps.Camp'),
),
migrations.AddField(
model_name='baseticket',
name='ticket_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tickets.TicketType'),
),
]

View file

128
src/tickets/models.py Normal file
View file

@ -0,0 +1,128 @@
import io
import logging
import hashlib
import base64
import qrcode
from django.db import models
from django.conf import settings
from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from utils.models import (
UUIDModel,
CreatedUpdatedModel
)
from utils.pdf import generate_pdf_letter
logger = logging.getLogger("bornhack.%s" % __name__)
# TicketType can be full week, one day. etc.
class TicketType(CreatedUpdatedModel, UUIDModel):
name = models.TextField()
def __str__(self):
return '{}'.format(self.name)
class BaseTicket(models.Model):
qrcode_base64 = models.TextField(null=True, blank=True)
ticket_type = models.ForeignKey('TicketType')
camp = models.ForeignKey('camps.Camp')
def save(self, **kwargs):
super(BaseTicket, self).save(**kwargs)
self.qrcode_base64 = self.get_qr_code()
super(BaseTicket, self).save(**kwargs)
def _get_token(self):
return hashlib.sha256(
'{_id}{secret_key}'.format(
_id=self.pk,
secret_key=settings.SECRET_KEY,
).encode('utf-8')
).hexdigest()
def get_qr_code(self):
qr = qrcode.make(
self._get_token(),
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_H
).resize((250, 250))
file_like = io.BytesIO()
qr.save(file_like, format='png')
qrcode_base64 = base64.b64encode(file_like.getvalue())
return qrcode_base64
def get_qr_code_url(self):
return 'data:image/png;base64,{}'.format(self.qrcode_base64)
def generate_pdf(self):
generate_pdf_letter(
filename='ticket_{}.pdf'.format(self.pk),
formatdict={'ticket': self},
template='pdf/ticket.html'
)
class SponsorTicket(BaseTicket, CreatedUpdatedModel, UUIDModel):
sponsor = models.ForeignKey('sponsors.Sponsor')
def __str__(self):
return 'SponsorTicket: {}'.format(self.pk)
class DiscountTicket(BaseTicket, CreatedUpdatedModel, UUIDModel):
price = models.IntegerField(
help_text=_('Price of the discounted ticket (in DKK, including VAT).')
)
def __str__(self):
return 'DiscountTicket: {}'.format(self.pk)
class ShopTicket(BaseTicket, CreatedUpdatedModel, UUIDModel):
order = models.ForeignKey('shop.Order', related_name='shoptickets')
product = models.ForeignKey('shop.Product')
name = models.CharField(
max_length=100,
help_text=(
'Name of the person this ticket belongs to. '
'This can be different from the buying user.'
),
null=True,
blank=True,
)
email = models.EmailField(
null=True,
blank=True,
)
checked_in = models.BooleanField(default=False)
# overwrite the _get_token method because old tickets use the user_id
def _get_token(self):
return hashlib.sha256(
'{_id}{user_id}{secret_key}'.format(
_id=self.pk,
user_id=self.order.user.pk,
secret_key=settings.SECRET_KEY,
).encode('utf-8')
).hexdigest()
def __str__(self):
return 'Ticket {user} {product}'.format(
user=self.order.user,
product=self.product
)
def save(self, **kwargs):
super(ShopTicket, self).save(**kwargs)
self.qrcode_base64 = self.get_qr_code()
super(ShopTicket, self).save(**kwargs)
def get_absolute_url(self):
return str(reverse_lazy('shop:ticket_detail', kwargs={'pk': self.pk}))

0
src/tickets/pdf.py Normal file
View file

View file

@ -0,0 +1,32 @@
{% load static from staticfiles %}
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<table style="width:100%;">
<tr>
<td style="width: 75%;">&nbsp;</td>
<td>
<h3>
{{ ticket.created|date:"b jS, Y" }}<br>
</h3>
</td>
</tr>
</table>
<br>
<h2>{{ ticket.camp.title }} Ticket</h2>
<h3>Type: {{ ticket.ticket_type }}</h3>
{% if ticket.name %}
<h3>Participant: {{ ticket.name }}</h3>
<br>
{% elif ticket.order.user.email %}
<h3>Participant: {{ ticket.order.user.email }}</h3>
<br>
{% elif ticket.sponsor %}
<h3>Sponsor: {{ ticket.sponsor.name }} </h3>
<img src="{{ ticket.sponsor.logo }}"></img>
{% endif %}
<center>
<img src="{{ ticket.get_qr_code_url }}"></img>
<p>Ticket #{{ ticket.pk }}</p>
</center>

3
src/tickets/views.py Normal file
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View file

@ -48,6 +48,7 @@ class Command(BaseCommand):
timezone.datetime(2016, 9, 4, 12, 0, tzinfo=timezone.utc),
timezone.datetime(2016, 9, 6, 12, 0, tzinfo=timezone.utc),
),
colour='#000000',
)
camp2017 = Camp.objects.create(
@ -66,6 +67,7 @@ class Command(BaseCommand):
timezone.datetime(2017, 9, 4, 12, 0, tzinfo=timezone.utc),
timezone.datetime(2017, 9, 6, 12, 0, tzinfo=timezone.utc),
),
colour='#000000',
)
camp2018 = Camp.objects.create(
@ -84,6 +86,7 @@ class Command(BaseCommand):
timezone.datetime(2018, 9, 4, 12, 0, tzinfo=timezone.utc),
timezone.datetime(2018, 9, 6, 12, 0, tzinfo=timezone.utc),
),
colour='#000000',
)
self.output("Creating users...")

View file

@ -1,11 +1,12 @@
import logging
import io
import os
from django.contrib.auth.models import AnonymousUser
from wkhtmltopdf.views import PDFTemplateResponse
from PyPDF2 import PdfFileWriter, PdfFileReader
from django.test.client import RequestFactory
from django.conf import settings
import logging
import io
import os
logger = logging.getLogger("bornhack.%s" % __name__)
@ -36,7 +37,14 @@ def generate_pdf_letter(filename, template, formatdict):
# get watermark from watermark file
watermark = PdfFileReader(
open(os.path.join(settings.STATICFILES_DIRS[0], 'pdf', settings.PDF_LETTERHEAD_FILENAME), 'rb')
open(
os.path.join(
settings.STATICFILES_DIRS[0],
'pdf',
settings.PDF_LETTERHEAD_FILENAME
),
'rb'
)
)
# add the watermark to all pages
@ -60,4 +68,3 @@ def generate_pdf_letter(filename, template, formatdict):
returnfile = io.BytesIO()
finalpdf.write(returnfile)
return returnfile