A lot of ticketing stuff.
This commit is contained in:
parent
e258db6546
commit
869f5a8fe9
|
@ -53,6 +53,7 @@ TEMPLATES = [
|
||||||
'django.template.context_processors.request',
|
'django.template.context_processors.request',
|
||||||
'django.contrib.auth.context_processors.auth',
|
'django.contrib.auth.context_processors.auth',
|
||||||
'django.contrib.messages.context_processors.messages',
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
'camps.context_processors.current_camp',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -37,6 +37,9 @@
|
||||||
|
|
||||||
<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 %}
|
||||||
|
<li><a href="{% url 'tickets:index' %}">Tickets</a></li>
|
||||||
|
{% 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 %}
|
||||||
<li><a href="{% url 'profiles:detail' %}">Profile</a></li>
|
<li><a href="{% url 'profiles:detail' %}">Profile</a></li>
|
||||||
|
|
|
@ -35,6 +35,10 @@ urlpatterns = [
|
||||||
r'^profile/',
|
r'^profile/',
|
||||||
include('profiles.urls', namespace='profiles')
|
include('profiles.urls', namespace='profiles')
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
r'^tickets/',
|
||||||
|
include('tickets.urls', namespace='tickets')
|
||||||
|
),
|
||||||
url(r'^accounts/', include('allauth.urls')),
|
url(r'^accounts/', include('allauth.urls')),
|
||||||
|
|
||||||
url(r'^admin/', include(admin.site.urls)),
|
url(r'^admin/', include(admin.site.urls)),
|
||||||
|
|
5
camps/context_processors.py
Normal file
5
camps/context_processors.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from .models import Camp
|
||||||
|
|
||||||
|
|
||||||
|
def current_camp(request):
|
||||||
|
return {'current_camp': Camp.objects.current()}
|
10
camps/managers.py
Normal file
10
camps/managers.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db.models import QuerySet
|
||||||
|
|
||||||
|
|
||||||
|
class CampQuerySet(QuerySet):
|
||||||
|
def current(self):
|
||||||
|
now = timezone.now()
|
||||||
|
if self.filter(start__year=now.year).exists():
|
||||||
|
return self.get(start__year=now.year)
|
||||||
|
return None
|
20
camps/migrations/0004_camp_ticket_sale_open.py
Normal file
20
camps/migrations/0004_camp_ticket_sale_open.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.5 on 2016-05-06 20:16
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('camps', '0003_auto_20160422_2019'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='camp',
|
||||||
|
name='ticket_sale_open',
|
||||||
|
field=models.BooleanField(default=False, help_text='Whether tickets are for sale or not.', verbose_name='Ticket sale open?'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -4,6 +4,8 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from bornhack.utils import CreatedUpdatedModel, UUIDModel
|
from bornhack.utils import CreatedUpdatedModel, UUIDModel
|
||||||
|
|
||||||
|
from .managers import CampQuerySet
|
||||||
|
|
||||||
|
|
||||||
class Camp(CreatedUpdatedModel, UUIDModel):
|
class Camp(CreatedUpdatedModel, UUIDModel):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -28,6 +30,14 @@ class Camp(CreatedUpdatedModel, UUIDModel):
|
||||||
unique=True,
|
unique=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ticket_sale_open = models.BooleanField(
|
||||||
|
verbose_name=_('Ticket sale open?'),
|
||||||
|
help_text=_('Whether tickets are for sale or not.'),
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = CampQuerySet.as_manager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return _('{} {}').format(
|
return _('{} {}').format(
|
||||||
self.name,
|
self.name,
|
||||||
|
@ -36,12 +46,10 @@ class Camp(CreatedUpdatedModel, UUIDModel):
|
||||||
|
|
||||||
def create_days(self):
|
def create_days(self):
|
||||||
delta = self.end - self.start
|
delta = self.end - self.start
|
||||||
for day_offset in range(1, delta.days + 1):
|
for day_offset in range(0, delta.days + 1):
|
||||||
day, created = self.days.get_or_create(
|
day, created = self.days.get_or_create(
|
||||||
date=self.start + datetime.timedelta(days=day_offset)
|
date=self.start + datetime.timedelta(days=day_offset)
|
||||||
)
|
)
|
||||||
if created:
|
|
||||||
print('{} created'.format(day))
|
|
||||||
|
|
||||||
def save(self, **kwargs):
|
def save(self, **kwargs):
|
||||||
super().save(**kwargs)
|
super().save(**kwargs)
|
||||||
|
|
|
@ -8,14 +8,12 @@ class TicketAdmin(admin.ModelAdmin):
|
||||||
list_display = [
|
list_display = [
|
||||||
'user',
|
'user',
|
||||||
'ticket_type',
|
'ticket_type',
|
||||||
'camp',
|
|
||||||
'paid',
|
'paid',
|
||||||
]
|
]
|
||||||
|
|
||||||
list_filter = [
|
list_filter = [
|
||||||
'paid',
|
'paid',
|
||||||
'ticket_type',
|
'ticket_type',
|
||||||
'camp',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
16
tickets/forms.py
Normal file
16
tickets/forms.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from django import forms
|
||||||
|
from .models import Ticket, TicketType
|
||||||
|
|
||||||
|
|
||||||
|
class TicketForm(forms.ModelForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Ticket
|
||||||
|
fields = [
|
||||||
|
'ticket_type',
|
||||||
|
]
|
||||||
|
|
||||||
|
ticket_type = forms.ModelChoiceField(
|
||||||
|
queryset=TicketType.objects.available()
|
||||||
|
)
|
||||||
|
|
13
tickets/managers.py
Normal file
13
tickets/managers.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
from psycopg2.extras import DateTimeTZRange
|
||||||
|
|
||||||
|
from django.db.models import QuerySet
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
class TicketTypeQuerySet(QuerySet):
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
now = timezone.now()
|
||||||
|
return self.filter(
|
||||||
|
available_in__contains=DateTimeTZRange(now, None)
|
||||||
|
)
|
36
tickets/migrations/0002_auto_20160506_1602.py
Normal file
36
tickets/migrations/0002_auto_20160506_1602.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# -*- 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),
|
||||||
|
),
|
||||||
|
]
|
31
tickets/migrations/0003_auto_20160506_2016.py
Normal file
31
tickets/migrations/0003_auto_20160506_2016.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# -*- 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,25 +1,23 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.postgres.fields import DateTimeRangeField
|
from django.contrib.postgres.fields import DateTimeRangeField
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from bornhack.utils import CreatedUpdatedModel, UUIDModel
|
from bornhack.utils import CreatedUpdatedModel, UUIDModel
|
||||||
|
|
||||||
|
from .managers import TicketTypeQuerySet
|
||||||
|
|
||||||
|
|
||||||
class Ticket(CreatedUpdatedModel, UUIDModel):
|
class Ticket(CreatedUpdatedModel, UUIDModel):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _('Ticket')
|
verbose_name = _('Ticket')
|
||||||
verbose_name_plural = _('Tickets')
|
verbose_name_plural = _('Tickets')
|
||||||
|
|
||||||
camp = models.ForeignKey(
|
|
||||||
'camps.Camp',
|
|
||||||
verbose_name=_('Camp'),
|
|
||||||
help_text=_('The camp this ticket is for.'),
|
|
||||||
)
|
|
||||||
|
|
||||||
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 ticket belongs to.'),
|
||||||
|
related_name='tickets',
|
||||||
)
|
)
|
||||||
|
|
||||||
paid = models.BooleanField(
|
paid = models.BooleanField(
|
||||||
|
@ -31,14 +29,20 @@ class Ticket(CreatedUpdatedModel, UUIDModel):
|
||||||
ticket_type = models.ForeignKey(
|
ticket_type = models.ForeignKey(
|
||||||
'tickets.TicketType',
|
'tickets.TicketType',
|
||||||
verbose_name=_('Ticket type'),
|
verbose_name=_('Ticket type'),
|
||||||
help_text=_('The type of the ticket.'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '{} ({})'.format(
|
||||||
|
self.ticket_type.name,
|
||||||
|
self.ticket_type.camp
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TicketType(CreatedUpdatedModel, UUIDModel):
|
class TicketType(CreatedUpdatedModel, UUIDModel):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _('Ticket Type')
|
verbose_name = _('Ticket Type')
|
||||||
verbose_name_plural = _('Ticket Types')
|
verbose_name_plural = _('Ticket Types')
|
||||||
|
ordering = ['available_in']
|
||||||
|
|
||||||
name = models.CharField(max_length=150)
|
name = models.CharField(max_length=150)
|
||||||
|
|
||||||
|
@ -53,8 +57,20 @@ class TicketType(CreatedUpdatedModel, UUIDModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
available_in = DateTimeRangeField(
|
available_in = DateTimeRangeField(
|
||||||
help_text=_('Which period is this ticket available for purchase?')
|
help_text=_(
|
||||||
|
'Which period is this ticket available for purchase? | '
|
||||||
|
'(Format: YYYY-MM-DD HH:MM) | Only one of start/end is required'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
objects = TicketTypeQuerySet.as_manager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return '{} ({} DKK)'.format(
|
||||||
|
self.name,
|
||||||
|
self.price,
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_available(self):
|
||||||
|
now = timezone.now()
|
||||||
|
return now in self.available_in
|
||||||
|
|
13
tickets/templates/tickets/buy.html
Normal file
13
tickets/templates/tickets/buy.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
{% bootstrap_button "Buy" button_type="submit" button_class="btn-primary" %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
7
tickets/templates/tickets/detail.html
Normal file
7
tickets/templates/tickets/detail.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{{ ticket }}
|
||||||
|
|
||||||
|
{% endblock %}
|
73
tickets/templates/tickets/index.html
Normal file
73
tickets/templates/tickets/index.html
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h2>Tickets</h2>
|
||||||
|
|
||||||
|
<p class="lead">
|
||||||
|
Here you can see the different ticket types, their prices and availability.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table class="table table-bordered table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
Description
|
||||||
|
<th>
|
||||||
|
Price
|
||||||
|
<th>
|
||||||
|
Availability
|
||||||
|
<th>
|
||||||
|
Buy
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
{% for ticket_type in ticket_types %}
|
||||||
|
|
||||||
|
<tr {% if not ticket_type.is_available %}style="color: lightgrey"{%endif%}>
|
||||||
|
<td>
|
||||||
|
{{ ticket_type.name }}
|
||||||
|
<td>
|
||||||
|
{{ ticket_type.price }} DKK
|
||||||
|
<td>
|
||||||
|
{{ ticket_type.available_in.lower }}
|
||||||
|
{% if ticket_type.available_in.upper %}
|
||||||
|
- {{ ticket_type.available_in.upper }}
|
||||||
|
{% endif %}
|
||||||
|
<td>
|
||||||
|
{% if ticket_type.is_available %}
|
||||||
|
<a href="{% url 'tickets:buy' %}?ticket_type={{ ticket_type.pk }}">Buy</a>
|
||||||
|
{% else %}
|
||||||
|
N/A
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<hr />
|
||||||
|
<h3>Your tickets</h3>
|
||||||
|
<table class="table table-hover">
|
||||||
|
{% for ticket in user.tickets.all %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{ ticket.ticket_type.name }}
|
||||||
|
<td>
|
||||||
|
{% if ticket.paid %} Paid {% else %} Not paid {% endif %}
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan=3>
|
||||||
|
You don't have a ticket! Why don't buy one and join the fun?
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<a href="{% url 'tickets:buy' %}" class="btn btn-success">Buy tickets</a>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<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.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
13
tickets/urls.py
Normal file
13
tickets/urls.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from .views import BuyTicketView, TicketIndexView, TicketDetailView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'buy/$', BuyTicketView.as_view(), name='buy'),
|
||||||
|
url(
|
||||||
|
r'detail/(?P<pk>[a-zA-Z0-9\-]+)/$',
|
||||||
|
TicketDetailView.as_view(),
|
||||||
|
name='detail'
|
||||||
|
),
|
||||||
|
url(r'$', TicketIndexView.as_view(), name='index'),
|
||||||
|
]
|
|
@ -1,3 +1,59 @@
|
||||||
|
from django.http import HttpResponseRedirect, Http404
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
from django.views.generic import CreateView, TemplateView, DetailView
|
||||||
|
from django.core.urlresolvers import reverse_lazy
|
||||||
|
|
||||||
|
from camps.models import Camp
|
||||||
|
|
||||||
|
from .models import Ticket, TicketType
|
||||||
|
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().dispatch(*args, **kwargs)
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
|
|
||||||
|
class TicketIndexView(CampTicketSaleCheck, TemplateView):
|
||||||
|
template_name = "tickets/index.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['ticket_types'] = TicketType.objects.all()
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class TicketDetailView(CampTicketSaleCheck, DetailView):
|
||||||
|
model = Ticket
|
||||||
|
template_name = 'tickets/detail.html'
|
||||||
|
context_object_name = 'ticket'
|
||||||
|
|
||||||
|
|
||||||
|
class BuyTicketView(CampTicketSaleCheck, CreateView):
|
||||||
|
model = Ticket
|
||||||
|
template_name = "tickets/buy.html"
|
||||||
|
form_class = TicketForm
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super().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()
|
||||||
|
return HttpResponseRedirect(
|
||||||
|
reverse_lazy('tickets:detail', kwargs={
|
||||||
|
'pk': str(instance.pk)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
# Create your views here.
|
|
||||||
|
|
Loading…
Reference in a new issue