diff --git a/bornhack/settings/base.py b/bornhack/settings/base.py
index db1ebbcc..ae2c5978 100644
--- a/bornhack/settings/base.py
+++ b/bornhack/settings/base.py
@@ -53,6 +53,7 @@ TEMPLATES = [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
+ 'camps.context_processors.current_camp',
],
},
},
diff --git a/bornhack/templates/base.html b/bornhack/templates/base.html
index 18062297..04073465 100644
--- a/bornhack/templates/base.html
+++ b/bornhack/templates/base.html
@@ -37,6 +37,9 @@
+ {% if current_camp.ticket_sale_open %}
+ - Tickets
+ {% endif %}
- Good to know
{% if user.is_authenticated %}
- Profile
diff --git a/bornhack/urls.py b/bornhack/urls.py
index d3f20bc0..2a0c113e 100644
--- a/bornhack/urls.py
+++ b/bornhack/urls.py
@@ -35,6 +35,10 @@ urlpatterns = [
r'^profile/',
include('profiles.urls', namespace='profiles')
),
+ url(
+ r'^tickets/',
+ include('tickets.urls', namespace='tickets')
+ ),
url(r'^accounts/', include('allauth.urls')),
url(r'^admin/', include(admin.site.urls)),
diff --git a/camps/context_processors.py b/camps/context_processors.py
new file mode 100644
index 00000000..01cdfacf
--- /dev/null
+++ b/camps/context_processors.py
@@ -0,0 +1,5 @@
+from .models import Camp
+
+
+def current_camp(request):
+ return {'current_camp': Camp.objects.current()}
diff --git a/camps/managers.py b/camps/managers.py
new file mode 100644
index 00000000..908f1fe4
--- /dev/null
+++ b/camps/managers.py
@@ -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
diff --git a/camps/migrations/0004_camp_ticket_sale_open.py b/camps/migrations/0004_camp_ticket_sale_open.py
new file mode 100644
index 00000000..7826fc12
--- /dev/null
+++ b/camps/migrations/0004_camp_ticket_sale_open.py
@@ -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?'),
+ ),
+ ]
diff --git a/camps/models.py b/camps/models.py
index 89e5e312..f6566225 100644
--- a/camps/models.py
+++ b/camps/models.py
@@ -4,6 +4,8 @@ from django.utils.translation import ugettext_lazy as _
from bornhack.utils import CreatedUpdatedModel, UUIDModel
+from .managers import CampQuerySet
+
class Camp(CreatedUpdatedModel, UUIDModel):
class Meta:
@@ -28,6 +30,14 @@ class Camp(CreatedUpdatedModel, UUIDModel):
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):
return _('{} {}').format(
self.name,
@@ -36,12 +46,10 @@ class Camp(CreatedUpdatedModel, UUIDModel):
def create_days(self):
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(
date=self.start + datetime.timedelta(days=day_offset)
)
- if created:
- print('{} created'.format(day))
def save(self, **kwargs):
super().save(**kwargs)
diff --git a/tickets/admin.py b/tickets/admin.py
index d3a14445..3cefc7a0 100644
--- a/tickets/admin.py
+++ b/tickets/admin.py
@@ -8,14 +8,12 @@ class TicketAdmin(admin.ModelAdmin):
list_display = [
'user',
'ticket_type',
- 'camp',
'paid',
]
list_filter = [
'paid',
'ticket_type',
- 'camp',
]
diff --git a/tickets/forms.py b/tickets/forms.py
new file mode 100644
index 00000000..82412ddb
--- /dev/null
+++ b/tickets/forms.py
@@ -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()
+ )
+
diff --git a/tickets/managers.py b/tickets/managers.py
new file mode 100644
index 00000000..89656a7b
--- /dev/null
+++ b/tickets/managers.py
@@ -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)
+ )
diff --git a/tickets/migrations/0002_auto_20160506_1602.py b/tickets/migrations/0002_auto_20160506_1602.py
new file mode 100644
index 00000000..14455c1d
--- /dev/null
+++ b/tickets/migrations/0002_auto_20160506_1602.py
@@ -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),
+ ),
+ ]
diff --git a/tickets/migrations/0003_auto_20160506_2016.py b/tickets/migrations/0003_auto_20160506_2016.py
new file mode 100644
index 00000000..ae2d5f46
--- /dev/null
+++ b/tickets/migrations/0003_auto_20160506_2016.py
@@ -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'),
+ ),
+ ]
diff --git a/tickets/models.py b/tickets/models.py
index 2edeedc6..595781a6 100644
--- a/tickets/models.py
+++ b/tickets/models.py
@@ -1,25 +1,23 @@
from django.db import models
from django.contrib.postgres.fields import DateTimeRangeField
from django.utils.translation import ugettext_lazy as _
+from django.utils import timezone
from bornhack.utils import CreatedUpdatedModel, UUIDModel
+from .managers import TicketTypeQuerySet
+
class Ticket(CreatedUpdatedModel, UUIDModel):
class Meta:
verbose_name = _('Ticket')
verbose_name_plural = _('Tickets')
- camp = models.ForeignKey(
- 'camps.Camp',
- verbose_name=_('Camp'),
- help_text=_('The camp this ticket is for.'),
- )
-
user = models.ForeignKey(
'auth.User',
verbose_name=_('User'),
help_text=_('The user this ticket belongs to.'),
+ related_name='tickets',
)
paid = models.BooleanField(
@@ -31,14 +29,20 @@ class Ticket(CreatedUpdatedModel, UUIDModel):
ticket_type = models.ForeignKey(
'tickets.TicketType',
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 Meta:
verbose_name = _('Ticket Type')
verbose_name_plural = _('Ticket Types')
+ ordering = ['available_in']
name = models.CharField(max_length=150)
@@ -53,8 +57,20 @@ class TicketType(CreatedUpdatedModel, UUIDModel):
)
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):
- return self.name
+ return '{} ({} DKK)'.format(
+ self.name,
+ self.price,
+ )
+
+ def is_available(self):
+ now = timezone.now()
+ return now in self.available_in
diff --git a/tickets/templates/tickets/buy.html b/tickets/templates/tickets/buy.html
new file mode 100644
index 00000000..f56d9e73
--- /dev/null
+++ b/tickets/templates/tickets/buy.html
@@ -0,0 +1,13 @@
+{% extends 'base.html' %}
+
+{% load bootstrap3 %}
+
+{% block content %}
+
+
+
+{% endblock %}
diff --git a/tickets/templates/tickets/detail.html b/tickets/templates/tickets/detail.html
new file mode 100644
index 00000000..135a460d
--- /dev/null
+++ b/tickets/templates/tickets/detail.html
@@ -0,0 +1,7 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+ {{ ticket }}
+
+{% endblock %}
diff --git a/tickets/templates/tickets/index.html b/tickets/templates/tickets/index.html
new file mode 100644
index 00000000..86d131a2
--- /dev/null
+++ b/tickets/templates/tickets/index.html
@@ -0,0 +1,73 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+
Tickets
+
+
+Here you can see the different ticket types, their prices and availability.
+
+
+
+
+
+
+ Description
+ |
+ Price
+ |
+ Availability
+ |
+ Buy
+
+ |
+
+{% for ticket_type in ticket_types %}
+
+
+
+ {{ ticket_type.name }}
+ |
+ {{ ticket_type.price }} DKK
+ |
+ {{ ticket_type.available_in.lower }}
+ {% if ticket_type.available_in.upper %}
+ - {{ ticket_type.available_in.upper }}
+ {% endif %}
+ |
+ {% if ticket_type.is_available %}
+ Buy
+ {% else %}
+ N/A
+ {% endif %}
+
+{% endfor %}
+
+ |
+
+
+{% if user.is_authenticated %}
+
+
Your tickets
+
+ {% for ticket in user.tickets.all %}
+
+
+ {{ ticket.ticket_type.name }}
+ |
+ {% if ticket.paid %} Paid {% else %} Not paid {% endif %}
+ {% empty %}
+ |
+
+ You don't have a ticket! Why don't buy one and join the fun?
+ {% endfor %}
+ |
+
+
Buy tickets
+
+{% else %}
+
Sign up or
+
login to buy tickets.
+{% endif %}
+
+{% endblock %}
diff --git a/tickets/urls.py b/tickets/urls.py
new file mode 100644
index 00000000..ac577231
--- /dev/null
+++ b/tickets/urls.py
@@ -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
[a-zA-Z0-9\-]+)/$',
+ TicketDetailView.as_view(),
+ name='detail'
+ ),
+ url(r'$', TicketIndexView.as_view(), name='index'),
+]
diff --git a/tickets/views.py b/tickets/views.py
index 91ea44a2..f8065af9 100644
--- a/tickets/views.py
+++ b/tickets/views.py
@@ -1,3 +1,59 @@
+from django.http import HttpResponseRedirect, Http404
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.