Merge branch 'master' into sponsors

This commit is contained in:
Thomas Flummer 2019-08-06 11:49:06 +02:00
commit ecb188c55a
50 changed files with 964 additions and 129 deletions

View file

@ -0,0 +1,39 @@
document.addEventListener("DOMContentLoaded", () => {
"use strict";
const search_form = document.getElementById("search_form");
const ticket_token_input = document.getElementById("ticket_token_input");
const scan_again = document.getElementById("scan_again");
const check_in_input = document.getElementById("check_in_input");
const hand_out_badge_input = document.getElementById("hand_out_badge_input");
const check_in_form = document.getElementById("check_in_form");
search_form.onsubmit = submit;
function submit(e) {
e.preventDefault();
if (ticket_token_input.value === "#clear") {
window.location.replace(window.location.pathname);
} else if (ticket_token_input.value === "#check-in") {
check_in_input.checked = true;
check_in_form.submit();
} else if (ticket_token_input.value === "#hand-out-badge") {
hand_out_badge_input.checked = true;
check_in_form.submit();
} else if (ticket_token_input.value.length === 65) {
search_form.submit();
} else {
scan_again.removeAttribute("hidden");
}
}
document.addEventListener("keydown", event => {
if (event.key === "#") {
ticket_token_input.value = "";
ticket_token_input.focus();
}
});
});

View file

@ -13,7 +13,7 @@
Use this view to hand out badges to participants. Use the search field to search for username, email, products, order ID, ticket UUID, etc. To check in participants go to the <a href="{% url 'backoffice:ticket_checkin' camp_slug=camp.slug %}">Ticket Checkin view</a> instead. To hand out merchandise and other products go to the <a href="{% url 'backoffice:product_handout' camp_slug=camp.slug %}">Hand Out Products</a> view instead.
</div>
<div>
This table shows all (Shop|Discount|Sponsor)Tickets which are badge_handed_out=False. Tickets must be checked in before they are shown in this list.
This table shows all (Shop|Discount|Sponsor)Tickets which are badge_ticket_generated=False. Tickets must be checked in before they are shown in this list.
</div>
</div>
<br>

View file

@ -16,17 +16,14 @@
<div class="list-group">
{% if perms.camps.infoteam_permission %}
<h3>Info Team</h3>
<a href="{% url 'backoffice:product_handout' camp_slug=camp.slug %}" class="list-group-item">
<h4 class="list-group-item-heading">Hand Out Products</h4>
<p class="list-group-item-text">Use this view to mark products such as merchandise, cabins, fridges and so on as handed out.</p>
</a>
<a href="{% url 'backoffice:ticket_checkin' camp_slug=camp.slug %}" class="list-group-item">
<h4 class="list-group-item-heading">Check-In Tickets</h4>
<p class="list-group-item-text">Use this view to check-in tickets when participants arrive.</p>
</a>
<a href="{% url 'backoffice:badge_handout' camp_slug=camp.slug %}" class="list-group-item">
<h4 class="list-group-item-heading">Hand Out Badges</h4>
<p class="list-group-item-text">Use this view to mark badges as handed out.</p>
<a href="{% url 'backoffice:scan_tickets' camp_slug=camp.slug %}"
class="list-group-item">
<h4 class="list-group-item-heading">
Scan tickets
</h4>
<p class="list-group-item-text">
Use this to get scan tickets
</p>
</a>
{% endif %}

View file

@ -13,7 +13,7 @@
Use this view to hand out products to participants. Use the search field to search for username, email, products, order ID etc. To check in participants go to the <a href="{% url 'backoffice:ticket_checkin' camp_slug=camp.slug %}">Ticket Checkin view</a> instead. To hand out badges go to the <a href="{% url 'backoffice:badge_handout' camp_slug=camp.slug %}">Badge Handout view</a> instead.
</div>
<div>
This table shows all OrderProductRelations which are handed_out=False (not including unpaid, cancelled and refunded orders). The table is initally sorted by order ID but the sorting can be changed by clicking the column headlines (if javascript is enabled).
This table shows all OrderProductRelations which are ticket_generated=False (not including unpaid, cancelled and refunded orders). The table is initally sorted by order ID but the sorting can be changed by clicking the column headlines (if javascript is enabled).
</div>
</div>
<br>

View file

@ -13,7 +13,7 @@
Use this view to check in participants. Use the search field to search for username, email, products, order ID, ticket UUID, etc. To hand out badges go to the <a href="{% url 'backoffice:badge_handout' camp_slug=camp.slug %}">Badge Handout view</a> instead. To hand out other products go to the <a href="{% url 'backoffice:product_handout' camp_slug=camp.slug %}">Hand Out Products</a> view instead.
</div>
<div>
This table shows all (Shop|Discount|Sponsor)Tickets which are checked_in=False.
This table shows all (Shop|Discount|Sponsor)Tickets which are used=False.
</div>
</div>
<br>

View file

@ -0,0 +1,118 @@
{% extends 'base.html' %}
{% load static from staticfiles %}
{% load qrcode %}
{% block content %}
<form id="search_form" method="POST" action="">
{% csrf_token %}
<div class="row">
<div class="col-md-12">
<h3>Scan the ticket!</h3>
<input type="text" name="ticket_token" id="ticket_token_input" autocomplete="off" style="color: #fff; background: #fff; border: 0; height: 0; width: 0;"/>
<div id="scan_again" hidden>
Scan again!
<hr />
</div>
</div>
</div>
</form>
{% if ticket %}
<br />
<table class="table">
<tr>
<td>
<strong>Type:</strong>
<td>
{{ ticket.ticket_type }}
<tr>
<td>
<strong>Used?:</strong>
<td>
{{ ticket.used }}
{% if ticket.ticket_type.includes_badge %}
<tr>
<td>
<strong>Badge handed out?:</strong>
<td>
{{ ticket.badge_handed_out }}
{% endif %}
{% if ticket.product %}
<tr>
<td>
<strong>Product:</strong>
<td>
{{ ticket.product }}
<tr>
<td>
<strong>Order:</strong>
<td>
{{ ticket.order }}
{% endif %}
{% if ticket.sponsor %}
<tr>
<td>
<strong>Sponsor</strong>
<td>
{{ ticket.sponsor }}
{% endif %}
</table>
<hr />
<form id="check_in_form" method="POST" action="">{% csrf_token %}
<input type="checkbox"
name="check_in_ticket_id"
id="check_in_input"
value="{{ ticket.pk }}"
style="color: #fff; background: #fff; border: 0; height: 0; width: 0; opacity: 0;" />
<input type="checkbox"
name="badge_ticket_id"
id="hand_out_badge_input"
value="{{ ticket.pk }}"
style="color: #fff; background: #fff; border: 0; height: 0; width: 0; opacity: 0;" />
</form>
<div class="row">
<div class="col-md-4">
{% qr_code "clear" %}
</div>
{% if not ticket.used %}
<div class="col-md-4">
{% qr_code "check-in" %}
</div>
{% endif %}
{% if ticket.ticket_type.includes_badge and not ticket.badge_handed_out %}
<div class="col-md-4">
{% qr_code "hand-out-badge" %}
</div>
{% endif %}
</div>
{% endif %}
<script src="{% static 'js/ticket_scan.js' %}"></script>
{% endblock content %}

View file

@ -7,6 +7,9 @@ app_name = "backoffice"
urlpatterns = [
path("", BackofficeIndexView.as_view(), name="index"),
# infodesk
path(
"tickets/", include([path("", ScanTicketsView.as_view(), name="scan_tickets")])
),
path("product_handout/", ProductHandoutView.as_view(), name="product_handout"),
path("badge_handout/", BadgeHandoutView.as_view(), name="badge_handout"),
path("ticket_checkin/", TicketCheckinView.as_view(), name="ticket_checkin"),

View file

@ -1,6 +1,7 @@
import logging, os
from itertools import chain
import qrcode
from django.contrib.auth.mixins import PermissionRequiredMixin, UserPassesTestMixin
from django.contrib.auth.models import User
from django.views.generic import TemplateView, ListView, DetailView
@ -14,7 +15,7 @@ from django.conf import settings
from django.core.files import File
from camps.mixins import CampViewMixin
from shop.models import OrderProductRelation
from shop.models import OrderProductRelation, Invoice, Order
from tickets.models import ShopTicket, SponsorTicket, DiscountTicket
from profiles.models import Profile
from program.models import SpeakerProposal, EventProposal
@ -40,7 +41,7 @@ class ProductHandoutView(CampViewMixin, InfoTeamPermissionMixin, ListView):
def get_queryset(self, **kwargs):
return OrderProductRelation.objects.filter(
handed_out=False,
ticket_generated=False,
order__paid=True,
order__refunded=False,
order__cancelled=False,
@ -52,9 +53,9 @@ class BadgeHandoutView(CampViewMixin, InfoTeamPermissionMixin, ListView):
context_object_name = "tickets"
def get_queryset(self, **kwargs):
shoptickets = ShopTicket.objects.filter(badge_handed_out=False)
sponsortickets = SponsorTicket.objects.filter(badge_handed_out=False)
discounttickets = DiscountTicket.objects.filter(badge_handed_out=False)
shoptickets = ShopTicket.objects.filter(badge_ticket_generated=False)
sponsortickets = SponsorTicket.objects.filter(badge_ticket_generated=False)
discounttickets = DiscountTicket.objects.filter(badge_ticket_generated=False)
return list(chain(shoptickets, sponsortickets, discounttickets))
@ -63,9 +64,9 @@ class TicketCheckinView(CampViewMixin, InfoTeamPermissionMixin, ListView):
context_object_name = "tickets"
def get_queryset(self, **kwargs):
shoptickets = ShopTicket.objects.filter(checked_in=False)
sponsortickets = SponsorTicket.objects.filter(checked_in=False)
discounttickets = DiscountTicket.objects.filter(checked_in=False)
shoptickets = ShopTicket.objects.filter(used=False)
sponsortickets = SponsorTicket.objects.filter(used=False)
discounttickets = DiscountTicket.objects.filter(used=False)
return list(chain(shoptickets, sponsortickets, discounttickets))
@ -151,7 +152,7 @@ class MerchandiseOrdersView(CampViewMixin, OrgaTeamPermissionMixin, ListView):
return (
OrderProductRelation.objects.filter(
handed_out=False,
ticket_generated=False,
order__paid=True,
order__refunded=False,
order__cancelled=False,
@ -169,7 +170,7 @@ class MerchandiseToOrderView(CampViewMixin, OrgaTeamPermissionMixin, TemplateVie
camp_prefix = "BornHack {}".format(timezone.now().year)
order_relations = OrderProductRelation.objects.filter(
handed_out=False,
ticket_generated=False,
order__paid=True,
order__refunded=False,
order__cancelled=False,
@ -197,7 +198,7 @@ class VillageOrdersView(CampViewMixin, OrgaTeamPermissionMixin, ListView):
return (
OrderProductRelation.objects.filter(
handed_out=False,
ticket_generated=False,
order__paid=True,
order__refunded=False,
order__cancelled=False,
@ -215,7 +216,7 @@ class VillageToOrderView(CampViewMixin, OrgaTeamPermissionMixin, TemplateView):
camp_prefix = "BornHack {}".format(timezone.now().year)
order_relations = OrderProductRelation.objects.filter(
handed_out=False,
ticket_generated=False,
order__paid=True,
order__refunded=False,
order__cancelled=False,
@ -345,7 +346,6 @@ class ReimbursementCreateView(CampViewMixin, EconomyTeamPermissionMixin, CreateV
def dispatch(self, request, *args, **kwargs):
""" Get the user from kwargs """
print("inside dispatch() with method %s" % request.method)
self.reimbursement_user = get_object_or_404(User, pk=kwargs["user_id"])
# get response now so we have self.camp available below
@ -544,3 +544,73 @@ class RevenueDetailView(CampViewMixin, EconomyTeamPermissionMixin, UpdateView):
return redirect(
reverse("backoffice:revenue_list", kwargs={"camp_slug": self.camp.slug})
)
def _ticket_getter_by_token(token):
for ticket_class in [ShopTicket, SponsorTicket, DiscountTicket]:
try:
return ticket_class.objects.get(token=token), False
except ticket_class.DoesNotExist:
try:
return ticket_class.objects.get(badge_token=token), True
except ticket_class.DoesNotExist:
pass
def _ticket_getter_by_pk(pk):
for ticket_class in [ShopTicket, SponsorTicket, DiscountTicket]:
try:
return ticket_class.objects.get(pk=pk)
except ticket_class.DoesNotExist:
pass
class ScanTicketsView(TemplateView):
template_name = "tickets/scan.html"
ticket = None
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.ticket:
context["ticket"] = self.ticket
elif "ticket_token" in self.request.POST:
# Slice to get rid of the first character which is a '#'
ticket_token = self.request.POST.get("ticket_token")[1:]
ticket, is_badge = _ticket_getter_by_token(ticket_token)
if ticket:
context["ticket"] = ticket
context["is_badge"] = is_badge
else:
messages.warning(self.request, "Ticket not found!")
return context
def post(self, request, **kwargs):
if 'check_in_ticket_id' in request.POST:
self.ticket = self.check_in_ticket(request)
elif 'badge_ticket_id' in request.POST:
self.ticket = self.hand_out_badge(request)
return super().get(request, **kwargs)
def check_in_ticket(self, request):
check_in_ticket_id = request.POST.get("check_in_ticket_id")
ticket_to_check_in = _ticket_getter_by_pk(check_in_ticket_id)
ticket_to_check_in.used = True
ticket_to_check_in.save()
messages.info(request, "Ticket checked-in!")
return ticket_to_check_in
def hand_out_badge(self, request):
badge_ticket_id = request.POST.get('badge_ticket_id')
ticket_to_handout_badge_for = _ticket_getter_by_pk(badge_ticket_id)
ticket_to_handout_badge_for.badge_handed_out = True
ticket_to_handout_badge_for.save()
messages.info(request, "Badge marked as handed out!")
return ticket_to_handout_badge_for

37
src/camps/factories.py Normal file
View file

@ -0,0 +1,37 @@
import factory
from django.utils import timezone
from factory.django import DjangoModelFactory
from psycopg2._range import DateTimeTZRange
class CampFactory(DjangoModelFactory):
class Meta:
model = "camps.Camp"
read_only = False
title = factory.Faker("word")
tagline = factory.Faker("sentence")
slug = factory.Faker("slug")
shortslug = factory.Faker("slug")
buildup = factory.LazyFunction(
lambda: DateTimeTZRange(
lower=timezone.now() - timezone.timedelta(days=3),
upper=timezone.now() - timezone.timedelta(hours=1)
)
)
camp = factory.LazyFunction(
lambda: DateTimeTZRange(lower=timezone.now(), upper=timezone.now() + timezone.timedelta(days=8))
)
teardown = factory.LazyFunction(
lambda: DateTimeTZRange(
lower=timezone.now() + timezone.timedelta(days=8, hours=1),
upper=timezone.now() + timezone.timedelta(days=11),
)
)
colour = factory.Faker("hex_color")

View file

@ -10,7 +10,7 @@
</div>
<div class="panel-body">
<h2>{% if ticket.checked_in %}This ticket has been used{% else %}This ticket is unused{% endif %}</h2>
<h2>{% if ticket.used %}This ticket has been used{% else %}This ticket is unused{% endif %}</h2>
<form method="POST" class="form">
{% csrf_token %}
{% bootstrap_field form.name %}

View file

@ -23,9 +23,13 @@
Checked in
<th>
Actions
<tbody>
{% for ticket in tickets %}
{% ifchanged ticket.ticket_type.camp %}
<tr>
<td colspan="5">
<strong>{{ ticket.ticket_type.camp }}</strong>
{% endifchanged %}
<tr>
<td>
{% if ticket.name %}
@ -36,9 +40,10 @@
<td>
{{ ticket.product.name }}
<td>
{% if ticket.ticket_type.single_ticket_per_product %}{{ ticket.get_orp.quantity }} &times; {% endif %}
{{ ticket.product.price|currency }}
<td>
{% if ticket.checked_in %}
{% if ticket.used %}
Yes
{% else %}
Not yet

View file

@ -218,6 +218,7 @@ class EventProposalForm(forms.ModelForm):
"slides_url",
"submission_notes",
"track",
"use_provided_speaker_laptop",
]
def clean_duration(self):
@ -262,6 +263,15 @@ class EventProposalForm(forms.ModelForm):
# initialise form
super().__init__(*args, **kwargs)
TALK = "Talk"
LIGHTNING_TALK = "Lightning Talk"
DEBATE = "Debate"
MUSIC_ACT = "Music Act"
RECREATIONAL_EVENT = "Recreational Event"
WORKSHOP = "Workshop"
SLACKING_OFF = "Slacking Off"
MEETUP = "Meetup"
# disable the empty_label for the track select box
self.fields["track"].empty_label = None
self.fields["track"].queryset = EventTrack.objects.filter(camp=camp)
@ -269,11 +279,15 @@ class EventProposalForm(forms.ModelForm):
# make sure video_recording checkbox defaults to checked
self.fields["allow_video_recording"].initial = True
if not (eventtype.name == "Talk" or eventtype.name == "Lightning Talk"):
if eventtype.name not in [TALK, LIGHTNING_TALK]:
# Only talk or lightning talk should show the slides_url field
del self.fields["slides_url"]
if eventtype.name == "Debate":
if not eventtype.name == LIGHTNING_TALK:
# Only lightning talks submissions will have to choose whether to use provided speaker laptop
del self.fields["use_provided_speaker_laptop"]
if eventtype.name == DEBATE:
# fix label and help_text for the title field
self.fields["title"].label = "Title of debate"
self.fields["title"].help_text = "The title of this debate"
@ -293,7 +307,7 @@ class EventProposalForm(forms.ModelForm):
"placeholder"
] = "Debate Duration (minutes)"
elif eventtype.name == "Music Act":
elif eventtype.name == MUSIC_ACT:
# fix label and help_text for the title field
self.fields["title"].label = "Title of music act"
self.fields["title"].help_text = "The title of this music act/concert/set."
@ -314,7 +328,7 @@ class EventProposalForm(forms.ModelForm):
# better placeholder text for duration field
self.fields["duration"].widget.attrs["placeholder"] = "Duration (minutes)"
elif eventtype.name == "Recreational Event":
elif eventtype.name == RECREATIONAL_EVENT:
# fix label and help_text for the title field
self.fields["title"].label = "Event Title"
self.fields["title"].help_text = "The title of this recreational event"
@ -338,7 +352,7 @@ class EventProposalForm(forms.ModelForm):
self.fields["duration"].label = "Event Duration"
self.fields["duration"].widget.attrs["placeholder"] = "Duration (minutes)"
elif eventtype.name == "Talk" or eventtype.name == "Lightning Talk":
elif eventtype.name in [TALK, LIGHTNING_TALK]:
# fix label and help_text for the title field
self.fields["title"].label = "Title of Talk"
self.fields["title"].help_text = "The title of this talk/presentation."
@ -355,7 +369,7 @@ class EventProposalForm(forms.ModelForm):
"submission_notes"
].help_text = "Private notes regarding this talk. Only visible to yourself and the BornHack organisers."
if self.fields.get("slides_url") and eventtype.name == "Lightning Talk":
if self.fields.get("slides_url") and eventtype.name == LIGHTNING_TALK:
self.fields[
"slides_url"
].help_text += " You will only get assigned a slot if you have provided slides (a title slide is enough if you don't use slides for the talk). You can add an URL later if need be."
@ -363,7 +377,7 @@ class EventProposalForm(forms.ModelForm):
# no duration for talks
del self.fields["duration"]
elif eventtype.name == "Workshop":
elif eventtype.name == WORKSHOP:
# fix label and help_text for the title field
self.fields["title"].label = "Workshop Title"
self.fields["title"].help_text = "The title of this workshop."
@ -389,7 +403,7 @@ class EventProposalForm(forms.ModelForm):
"duration"
].help_text = "How much time (in minutes) should we set aside for this workshop? Please keep it between 60 and 180 minutes (1-3 hours)."
elif eventtype.name == "Slacking Off":
elif eventtype.name == SLACKING_OFF:
# fix label and help_text for the title field
self.fields["title"].label = "Event Title"
self.fields["title"].help_text = "The title of this recreational event."
@ -415,7 +429,7 @@ class EventProposalForm(forms.ModelForm):
"duration"
].help_text = "How much time (in minutes) should we set aside for this event? Please keep it between 60 and 180 minutes (1-3 hours)."
elif eventtype.name == "Meetup":
elif eventtype.name == MEETUP:
# fix label and help_text for the title field
self.fields["title"].label = "Meetup Title"
self.fields["title"].help_text = "The title of this meetup."

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.3 on 2019-07-31 10:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('program', '0072_auto_20190616_1746'),
]
operations = [
migrations.AddField(
model_name='eventproposal',
name='use_provided_speaker_laptop',
field=models.BooleanField(default=False, help_text='Will you be using the provided speaker laptop?'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.3 on 2019-08-01 07:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('program', '0073_eventproposal_use_provided_speaker_laptop'),
]
operations = [
migrations.AlterField(
model_name='eventproposal',
name='use_provided_speaker_laptop',
field=models.BooleanField(default=True, help_text='Will you be using the provided speaker laptop?'),
),
]

View file

@ -340,6 +340,10 @@ class EventProposal(UserSubmittedModel):
blank=True,
)
use_provided_speaker_laptop = models.BooleanField(
help_text="Will you be using the provided speaker laptop?", default=True
)
@property
def camp(self):
return self.track.camp

View file

@ -9,7 +9,7 @@ Call for Participation | {{ block.super }}
{% if not camp.call_for_participation_open %}
<div class="alert alert-danger">
<strong>Note!</strong> This Call for Particilation is not open.
<strong>Note!</strong> This Call for Participation is not open.
</div>
{% endif %}

View file

@ -1,6 +1,6 @@
Hello!<br>
<br>
Event "{{ proposal.name }}" for {{ proposal.camp }} was just updated!
Event "{{ proposal.title }}" for {{ proposal.camp }} was just updated!
<br>
<br>
More info <a href="https://bornhack.dk/admin/program/eventproposal/{{ proposal.uuid }}/change/">here</a>.

View file

@ -1,6 +1,6 @@
Hello!
Event "{{ proposal.name }}" for {{ proposal.camp }} was just updated!
Event "{{ proposal.title }}" for {{ proposal.camp }} was just updated!
More info: https://bornhack.dk/admin/program/eventproposal/{{ proposal.uuid }}/change/

View file

@ -4,7 +4,7 @@
{% if not camp.call_for_participation_open %}
<div class="alert alert-danger">
<strong>Note!</strong> This Call for Particilation is not open.
<strong>Note!</strong> This Call for Participation is not open.
</div>
{% endif %}

View file

@ -6,6 +6,9 @@
<strong>ID</strong>: {{ eventproposal.uuid }}<br />
<strong>Status</strong>: {{ eventproposal.proposal_status }}<br />
<strong>Duration</strong>: {{ eventproposal.duration|default:"Not defined" }}<br />
{% if eventproposal.event_type.name == "Lightning Talk" %}
<strong>Use provided laptop?</strong>: {{ eventproposal.use_provided_speaker_laptop }}<br />
{% endif %}
</div>
</div>

View file

@ -10,7 +10,7 @@ Proposals | {{ block.super }}
{% include 'includes/event_proposal_type_select.html' %}
{% else %}
<div class="alert alert-danger">
<strong>Note!</strong> This Call for Particilation is not open.
<strong>Note!</strong> This Call for Participation is not open.
</div>
{% endif %}

View file

@ -4,7 +4,7 @@
{% if not camp.call_for_participation_open %}
<div class="alert alert-danger">
<strong>Note!</strong> This Call for Particilation is not open.
<strong>Note!</strong> This Call for Participation is not open.
</div>
{% endif %}

View file

@ -2,5 +2,5 @@
django-debug-toolbar==2.0
factory_boy==2.12.0
coverage==4.5.3
coverage==4.5.4
codecov==2.0.15

View file

@ -1,4 +1,4 @@
Django==2.2.3
Django==2.2.4
channels==2.2.0
channels-redis==2.3.2
@ -23,7 +23,7 @@ docopt==0.6.2
future==0.17.1
html5lib==1.0.1
icalendar==4.0.3
ipython==7.6.1
ipython==7.7.0
irc3==1.1.2
oauthlib==3.0.2
olefile==0.46

View file

@ -101,6 +101,8 @@ class ProductAdmin(admin.ModelAdmin):
available_to,
]
list_editable = ["ticket_type"]
list_filter = ["category", "ticket_type"]
search_fields = ["name"]
@ -142,6 +144,7 @@ class OrderAdmin(admin.ModelAdmin):
"mark_order_as_paid",
"mark_order_as_refunded",
"mark_order_as_cancelled",
"create_tickets",
]
def mark_order_as_paid(self, request, queryset):
@ -162,6 +165,12 @@ class OrderAdmin(admin.ModelAdmin):
mark_order_as_cancelled.description = "Mark order(s) as cancelled"
def create_tickets(self, request, queryset):
for order in queryset.filter(paid=True):
order.create_tickets(request)
create_tickets.description = "Create tickets for order(s) (paid only)"
def get_user_email(obj):
return obj.order.user.email

View file

@ -30,6 +30,7 @@ class ProductFactory(DjangoModelFactory):
lower=timezone.now(), upper=timezone.now() + timezone.timedelta(31)
)
)
ticket_type = factory.SubFactory("tickets.factories.TicketTypeFactory")
class OrderFactory(DjangoModelFactory):
@ -46,4 +47,4 @@ class OrderProductRelationFactory(DjangoModelFactory):
product = factory.SubFactory(ProductFactory)
order = factory.SubFactory(OrderFactory)
quantity = 1
handed_out = False
ticket_generated = False

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.2 on 2019-07-18 18:51
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('shop', '0058_order_pdf'),
]
operations = [
migrations.RenameField(
model_name='orderproductrelation',
old_name='handed_out',
new_name='ticket_generated',
),
]

View file

@ -208,6 +208,7 @@ class Order(CreatedUpdatedModel):
return str(reverse_lazy("shop:order_detail", kwargs={"pk": self.pk}))
def create_tickets(self, request=None):
tickets = []
for order_product in self.orderproductrelation_set.all():
# if this is a Ticket product?
if order_product.product.ticket_type:
@ -216,6 +217,30 @@ class Order(CreatedUpdatedModel):
ticket_type=order_product.product.ticket_type,
)
if order_product.product.ticket_type.single_ticket_per_product:
# This ticket type is one where we only create one ticket
ticket, created = self.shoptickets.get_or_create(**query_kwargs)
if created:
msg = (
"Created ticket for product %s on order %s (quantity: %s)"
% (
order_product.product,
order_product.order.pk,
order_product.quantity,
)
)
tickets.append(ticket)
else:
msg = "Ticket already created for product %s on order %s" % (
order_product.product,
order_product.order.pk,
)
if request:
messages.success(request, msg)
else:
# We should create a number of tickets equal to OrderProductRelation quantity
already_created_tickets = self.shoptickets.filter(
**query_kwargs
).count()
@ -228,7 +253,8 @@ class Order(CreatedUpdatedModel):
for _ in range(
0, (order_product.quantity - already_created_tickets)
):
self.shoptickets.create(**query_kwargs)
ticket = self.shoptickets.create(**query_kwargs)
tickets.append(ticket)
msg = "Created %s tickets of type: %s" % (
order_product.quantity,
@ -236,13 +262,13 @@ class Order(CreatedUpdatedModel):
)
if request:
messages.success(request, msg)
else:
print(msg)
# and mark the OPR as handed_out=True
order_product.handed_out = True
# and mark the OPR as ticket_generated=True
order_product.ticket_generated = True
order_product.save()
return tickets
def mark_as_paid(self, request=None):
self.paid = True
self.open = None
@ -289,35 +315,36 @@ class Order(CreatedUpdatedModel):
self.open = None
self.save()
def is_not_handed_out(self):
if self.orderproductrelation_set.filter(handed_out=True).count() == 0:
def is_not_ticket_generated(self):
if self.orderproductrelation_set.filter(ticket_generated=True).count() == 0:
return True
else:
return False
def is_partially_handed_out(self):
def is_partially_ticket_generated(self):
if (
self.orderproductrelation_set.filter(handed_out=True).count() != 0
and self.orderproductrelation_set.filter(handed_out=False).count() != 0
self.orderproductrelation_set.filter(ticket_generated=True).count() != 0
and self.orderproductrelation_set.filter(ticket_generated=False).count()
!= 0
):
# some products are handed out, others are not
return True
else:
return False
def is_fully_handed_out(self):
if self.orderproductrelation_set.filter(handed_out=False).count() == 0:
def is_fully_ticket_generated(self):
if self.orderproductrelation_set.filter(ticket_generated=False).count() == 0:
return True
else:
return False
@property
def handed_out_status(self):
if self.is_not_handed_out():
def ticket_generated_status(self):
if self.is_not_ticket_generated():
return "no"
elif self.is_partially_handed_out():
elif self.is_partially_ticket_generated():
return "partially"
elif self.is_fully_handed_out():
elif self.is_fully_ticket_generated():
return "fully"
else:
return False
@ -466,14 +493,14 @@ class OrderProductRelation(CreatedUpdatedModel):
order = models.ForeignKey("shop.Order", on_delete=models.PROTECT)
product = models.ForeignKey("shop.Product", on_delete=models.PROTECT)
quantity = models.PositiveIntegerField()
handed_out = models.BooleanField(default=False)
ticket_generated = models.BooleanField(default=False)
@property
def total(self):
return Decimal(self.product.price * self.quantity)
def clean(self):
if self.handed_out and not self.order.paid:
if self.ticket_generated and not self.order.paid:
raise ValidationError(
"Product can not be handed out when order is not paid."
)

View file

@ -25,13 +25,13 @@
<tbody>
{% for order in orders %}
{% if order.products.exists %}
<tr {% if not order.open and order.paid and order.is_fully_handed_out %}style="color: lightgrey"{% endif %}>
<tr {% if not order.open and order.paid and order.is_fully_ticket_generated %}style="color: lightgrey"{% endif %}>
<td>{{ order.id }}</td>
<td>{{ order.get_number_of_items }}</td>
<td>{{ order.total|currency }}</td>
<td class="text-center">{{ order.open|truefalseicon }}</td>
<td class="text-center">{{ order.paid|truefalseicon }}</td>
<td class="text-center">{{ order.handed_out_status }}</td>
<td class="text-center">{{ order.ticket_generated_status }}</td>
<td>
{% if order.paid %}
{% if order.invoice.pdf %}

View file

@ -4,6 +4,8 @@ from django.utils import timezone
from psycopg2.extras import DateTimeTZRange
from shop.forms import OrderProductRelationForm
from tickets.factories import TicketTypeFactory
from tickets.models import ShopTicket
from utils.factories import UserFactory
from .factories import ProductFactory, OrderProductRelationFactory, OrderFactory
@ -371,3 +373,27 @@ class TestOrderListView(TestCase):
path = reverse("shop:order_list")
response = self.client.get(path)
self.assertEquals(response.status_code, 200)
class TestTicketCreation(TestCase):
def test_multiple_tickets_created(self):
user = UserFactory()
ticket_type = TicketTypeFactory(single_ticket_per_product=False)
product = ProductFactory(ticket_type=ticket_type)
order = OrderFactory(user=user)
OrderProductRelationFactory(order=order, product=product, quantity=5)
order.mark_as_paid()
self.assertEquals(
ShopTicket.objects.filter(product=product, order=order).count(), 5
)
def test_single_ticket_created(self):
user = UserFactory()
ticket_type = TicketTypeFactory(single_ticket_per_product=True)
product = ProductFactory(ticket_type=ticket_type)
order = OrderFactory(user=user)
OrderProductRelationFactory(order=order, product=product, quantity=5)
order.mark_as_paid()
self.assertEquals(
ShopTicket.objects.filter(product=product, order=order).count(), 1
)

View file

@ -53,11 +53,14 @@ Team: {{ team.name }} | {{ block.super }}
</li>
{% endif %}
{% if request.user in team.members.all %}
<li {% if view.active_menu == "guide" %}class="active"{% endif %}>
<a href="{% url "teams:guide" camp_slug=team.camp.slug team_slug=team.slug %}">
Team guide
</a>
</li>
{% endif %}
</ul>

View file

@ -1,22 +1,28 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView, DetailView
from django.views.generic import DetailView
from camps.mixins import CampViewMixin
from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin
from ..models import Team
from ..models import Team, TeamMember
class TeamGuideView(LoginRequiredMixin, CampViewMixin, DetailView):
class TeamGuideView(LoginRequiredMixin, CampViewMixin, UserPassesTestMixin, DetailView):
template_name = "team_guide.html"
context_object_name = "team"
model = Team
slug_url_kwarg = "team_slug"
active_menu = "guide"
def get_queryset(self):
qs = CampViewMixin.get_queryset(self)
qs.filter(teammember__approved=True, teammember__user=self.request.user)
return qs
def test_func(self):
# Make sure that the user is an approved member of the team
try:
TeamMember.objects.get(
user=self.request.user, team=self.get_object(), approved=True
)
except TeamMember.DoesNotExist:
return False
else:
return True
class TeamGuidePrintView(TeamGuideView):

View file

@ -1,11 +1,13 @@
from django.contrib import admin
from shop.models import OrderProductRelation
from .models import TicketType, SponsorTicket, DiscountTicket, ShopTicket
class BaseTicketAdmin(admin.ModelAdmin):
actions = ["generate_pdf"]
exclude = ["qrcode_base64"]
readonly_fields = ['token', 'badge_token']
def generate_pdf(self, request, queryset):
for ticket in queryset.all():
@ -23,9 +25,9 @@ class TicketTypeAdmin(admin.ModelAdmin):
@admin.register(SponsorTicket)
class SponsorTicketAdmin(BaseTicketAdmin):
list_display = ["pk", "ticket_type", "sponsor", "checked_in"]
list_display = ["pk", "ticket_type", "sponsor", "used"]
list_filter = ["ticket_type__camp", "checked_in", "ticket_type", "sponsor"]
list_filter = ["ticket_type__camp", "used", "ticket_type", "sponsor"]
search_fields = ["pk", "sponsor__name"]
@ -50,13 +52,25 @@ class ShopTicketAdmin(BaseTicketAdmin):
"ticket_type",
"order",
"product",
"checked_in",
"used",
"product_quantity",
]
list_filter = ["ticket_type__camp", "checked_in", "ticket_type", "order", "product"]
list_filter = ["ticket_type__camp", "used", "ticket_type", "order", "product"]
search_fields = ["uuid", "order__id", "order__user__email", "name", "email"]
def product_quantity(self, ticket):
orp = OrderProductRelation.objects.get(
product=ticket.product, order=ticket.order
)
return (
str(orp.quantity) if ticket.ticket_type.single_ticket_per_product else "1"
)
product_quantity.short_description = "Quantity"
class ShopTicketInline(admin.TabularInline):
model = ShopTicket

18
src/tickets/factories.py Normal file
View file

@ -0,0 +1,18 @@
import factory
from factory.django import DjangoModelFactory
class TicketTypeFactory(DjangoModelFactory):
class Meta:
model = "tickets.TicketType"
name = factory.Faker("sentence")
camp = factory.SubFactory("camps.factories.CampFactory")
class ShopTicketFactory(DjangoModelFactory):
class Meta:
model = "tickets.ShopTicket"
ticket_type = factory.SubFactory(TicketTypeFactory)

View file

@ -0,0 +1,28 @@
# Generated by Django 2.2.2 on 2019-06-16 15:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tickets', '0005_auto_20180318_0906'),
]
operations = [
migrations.AddField(
model_name='discountticket',
name='token',
field=models.CharField(max_length=64, null=True),
),
migrations.AddField(
model_name='shopticket',
name='token',
field=models.CharField(max_length=64, null=True),
),
migrations.AddField(
model_name='sponsorticket',
name='token',
field=models.CharField(max_length=64, null=True),
),
]

View file

@ -0,0 +1,29 @@
# Generated by Django 2.2.2 on 2019-07-18 18:52
from django.conf import settings
from django.db import migrations
from tickets.models import create_ticket_token
def save_tokens(apps, schema_editor):
ShopTicket = apps.get_model("tickets", "ShopTicket")
SponsorTicket = apps.get_model("tickets", "SponsorTicket")
DiscountTicket = apps.get_model("tickets", "DiscountTicket")
for model in (ShopTicket, SponsorTicket, DiscountTicket):
for ticket in model.objects.all():
token = create_ticket_token(
"{_id}{secret_key}".format(
_id=ticket.uuid, secret_key=settings.SECRET_KEY
).encode("utf-8")
)
ticket.token = token
ticket.save()
class Migration(migrations.Migration):
dependencies = [("tickets", "0006_auto_20190616_1746")]
operations = [migrations.RunPython(save_tokens)]

View file

@ -0,0 +1,43 @@
# Generated by Django 2.2.2 on 2019-07-18 18:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tickets', '0007_save_token_to_db'),
]
operations = [
migrations.RenameField(
model_name='discountticket',
old_name='checked_in',
new_name='used',
),
migrations.RenameField(
model_name='shopticket',
old_name='checked_in',
new_name='used',
),
migrations.RenameField(
model_name='sponsorticket',
old_name='checked_in',
new_name='used',
),
migrations.AlterField(
model_name='discountticket',
name='token',
field=models.CharField(max_length=64),
),
migrations.AlterField(
model_name='shopticket',
name='token',
field=models.CharField(max_length=64),
),
migrations.AlterField(
model_name='sponsorticket',
name='token',
field=models.CharField(max_length=64),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.3 on 2019-07-23 20:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tickets', '0008_auto_20190718_2055'),
]
operations = [
migrations.AddField(
model_name='tickettype',
name='includes_badge',
field=models.BooleanField(default=False),
),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 2.2.3 on 2019-07-24 18:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tickets', '0009_tickettype_includes_badge'),
]
operations = [
migrations.AddField(
model_name='discountticket',
name='badge_token',
field=models.CharField(max_length=64, null=True),
),
migrations.AddField(
model_name='shopticket',
name='badge_token',
field=models.CharField(max_length=64, null=True),
),
migrations.AddField(
model_name='sponsorticket',
name='badge_token',
field=models.CharField(max_length=64, null=True),
),
]

View file

@ -0,0 +1,29 @@
# Generated by Django 2.2.3 on 2019-07-24 18:37
from django.conf import settings
from django.db import migrations
from tickets.models import create_ticket_token
def save_badge_tokens(apps, schema_editor):
ShopTicket = apps.get_model("tickets", "ShopTicket")
SponsorTicket = apps.get_model("tickets", "SponsorTicket")
DiscountTicket = apps.get_model("tickets", "DiscountTicket")
for model in (ShopTicket, SponsorTicket, DiscountTicket):
for ticket in model.objects.all():
badge_token = create_ticket_token(
"{_id}{secret_key}-badge".format(
_id=ticket.uuid, secret_key=settings.SECRET_KEY
).encode("utf-8")
)
ticket.badge_token = badge_token
ticket.save()
class Migration(migrations.Migration):
dependencies = [("tickets", "0010_auto_20190724_2037")]
operations = [migrations.RunPython(save_badge_tokens)]

View file

@ -0,0 +1,28 @@
# Generated by Django 2.2.3 on 2019-07-24 18:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tickets', '0011_save_badge_token_to_db'),
]
operations = [
migrations.AlterField(
model_name='discountticket',
name='badge_token',
field=models.CharField(max_length=64),
),
migrations.AlterField(
model_name='shopticket',
name='badge_token',
field=models.CharField(max_length=64),
),
migrations.AlterField(
model_name='sponsorticket',
name='badge_token',
field=models.CharField(max_length=64),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.3 on 2019-07-30 20:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tickets', '0012_auto_20190724_2037'),
]
operations = [
migrations.AddField(
model_name='tickettype',
name='single_ticket_per_product',
field=models.BooleanField(default=False, help_text='Only create one ticket for a product/order pair no matter the quantity. Useful for products which are bought in larger quantity (ie. village chairs)'),
),
]

View file

@ -0,0 +1,43 @@
# Generated by Django 2.2.3 on 2019-08-03 20:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tickets', '0013_tickettype_single_ticket_per_product'),
]
operations = [
migrations.AlterField(
model_name='discountticket',
name='badge_token',
field=models.CharField(blank=True, max_length=64),
),
migrations.AlterField(
model_name='discountticket',
name='token',
field=models.CharField(blank=True, max_length=64),
),
migrations.AlterField(
model_name='shopticket',
name='badge_token',
field=models.CharField(blank=True, max_length=64),
),
migrations.AlterField(
model_name='shopticket',
name='token',
field=models.CharField(blank=True, max_length=64),
),
migrations.AlterField(
model_name='sponsorticket',
name='badge_token',
field=models.CharField(blank=True, max_length=64),
),
migrations.AlterField(
model_name='sponsorticket',
name='token',
field=models.CharField(blank=True, max_length=64),
),
]

View file

@ -5,6 +5,8 @@ import qrcode
from django.conf import settings
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from shop.models import OrderProductRelation
from utils.models import UUIDModel, CampRelatedModel
from utils.pdf import generate_pdf_letter
from django.db import models
@ -17,15 +19,39 @@ logger = logging.getLogger("bornhack.%s" % __name__)
class TicketType(CampRelatedModel, UUIDModel):
name = models.TextField()
camp = models.ForeignKey("camps.Camp", on_delete=models.PROTECT)
includes_badge = models.BooleanField(default=False)
single_ticket_per_product = models.BooleanField(
default=False,
help_text=(
"Only create one ticket for a product/order pair no matter the quantity. "
"Useful for products which are bought in larger quantity (ie. village chairs)"
),
)
def __str__(self):
return "{} ({})".format(self.name, self.camp.title)
def create_ticket_token(string):
return hashlib.sha256(string).hexdigest()
def qr_code_base64(token):
qr = qrcode.make(
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
class BaseTicket(CampRelatedModel, UUIDModel):
ticket_type = models.ForeignKey("TicketType", on_delete=models.PROTECT)
checked_in = models.BooleanField(default=False)
used = models.BooleanField(default=False)
badge_handed_out = models.BooleanField(default=False)
token = models.CharField(max_length=64, blank=True)
badge_token = models.CharField(max_length=64, blank=True)
class Meta:
abstract = True
@ -34,33 +60,45 @@ class BaseTicket(CampRelatedModel, UUIDModel):
def camp(self):
return self.ticket_type.camp
def _get_token(self):
return hashlib.sha256(
"{_id}{secret_key}".format(
_id=self.pk, secret_key=settings.SECRET_KEY
).encode("utf-8")
).hexdigest()
def save(self, **kwargs):
self.token = self._get_token()
self.badge_token = self._get_badge_token()
super().save(**kwargs)
def get_qr_code_base64(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_token(self):
return create_ticket_token(
"{_id}{secret_key}".format(
_id=self.uuid, secret_key=settings.SECRET_KEY
).encode("utf-8")
)
def _get_badge_token(self):
return create_ticket_token(
"{_id}{secret_key}-badge".format(
_id=self.uuid, secret_key=settings.SECRET_KEY
).encode("utf-8")
)
def get_qr_code_url(self):
return "data:image/png;base64,{}".format(
self.get_qr_code_base64().decode("utf-8")
qr_code_base64(self._get_token()).decode("utf-8")
)
def get_qr_badge_code_url(self):
return "data:image/png;base64,{}".format(
qr_code_base64(self._get_badge_token()).decode("utf-8")
)
def generate_pdf(self):
formatdict = {"ticket": self}
if self.ticket_type.single_ticket_per_product and self.shortname == "shop":
orp = self.get_orp()
formatdict["quantity"] = orp.quantity
return generate_pdf_letter(
filename="{}_ticket_{}.pdf".format(self.shortname, self.pk),
formatdict={"ticket": self},
formatdict=formatdict,
template="pdf/ticket.html",
)
@ -126,3 +164,6 @@ class ShopTicket(BaseTicket):
@property
def shortname(self):
return "shop"
def get_orp(self):
return OrderProductRelation.objects.get(product=self.product, order=self.order)

View file

@ -1,6 +1,5 @@
{% 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>
@ -14,6 +13,10 @@
<br>
<h2>{{ ticket.ticket_type.camp.title }} Ticket</h2>
<h3>Type: {{ ticket.ticket_type.name }}</h3>
<h3>Product: {{ ticket.product.name }}</h3>
{% if quantity %}
<h3>Quantity: {{ quantity }}</h3>
{% endif %}
{% if ticket.name %}
<h3>Participant: {{ ticket.name }}</h3>
@ -23,13 +26,40 @@
<br>
{% elif ticket.sponsor %}
<h3>Sponsor: {{ ticket.sponsor.name }} </h3>
<img src="{% static 'img/sponsors/' %}{{ sponsor.logo_filename }}"></img>
<img src="{% static 'img/sponsors/' %}{{ ticket.sponsor.logo_filename }}" />
{% endif %}
{% if ticket.checked_in %}
<h2>This ticket has been checked in.</h2>
{% if ticket.used %}
<h2>This ticket has been used.</h2>
{% endif %}
<center>
<img src="{{ ticket.get_qr_code_url }}"></img>
<p>Ticket #{{ ticket.pk }}</p>
<p>{{ ticket.token }}</p>
</center>
{% if ticket.ticket_type.includes_badge %}
<div style="display:block; clear:both; page-break-after:always;"></div>
<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>Badge voucher</h2>
<center>
<img src="{{ ticket.get_qr_badge_code_url }}"></img>
<p>{{ ticket.badge_token }}</p>
</center>
{% endif %}

25
src/tickets/tests.py Normal file
View file

@ -0,0 +1,25 @@
from django.test import TestCase
from shop.factories import OrderProductRelationFactory
from .factories import TicketTypeFactory
from .models import ShopTicket
class TicketTests(TestCase):
def test_correct_token_and_badge_token_are_different(self):
ticket_type = TicketTypeFactory()
orp = OrderProductRelationFactory()
shop_ticket = ShopTicket.objects.create(
ticket_type=ticket_type,
product=orp.product,
order=orp.order,
)
self.assertNotEqual(shop_ticket.token, shop_ticket.badge_token)
self.assertEqual(shop_ticket.token, shop_ticket._get_token())
self.assertEqual(shop_ticket.badge_token, shop_ticket._get_badge_token())

View file

@ -19,7 +19,7 @@ class ShopTicketListView(LoginRequiredMixin, ListView):
def get_queryset(self):
tickets = super(ShopTicketListView, self).get_queryset()
user = self.request.user
return tickets.filter(order__user=user)
return tickets.filter(order__user=user).order_by("ticket_type__camp")
class ShopTicketDownloadView(LoginRequiredMixin, SingleObjectMixin, View):

View file

@ -1399,7 +1399,8 @@ class Command(BaseCommand):
camp=camp,
user=users[1],
seats=2,
location="From Copenhagen",
from_location="Copenhagen",
to_location="BornHack",
when=timezone.datetime(year, 8, 27, 12, 0, tzinfo=timezone.utc),
description="I have space for two people and a little bit of luggage",
)
@ -1407,7 +1408,8 @@ class Command(BaseCommand):
camp=camp,
user=users[1],
seats=2,
location="To Copenhagen",
from_location="BornHack",
to_location="Copenhagen",
when=timezone.datetime(year, 9, 4, 12, 0, tzinfo=timezone.utc),
description="I have space for two people and a little bit of luggage",
)
@ -1415,7 +1417,8 @@ class Command(BaseCommand):
camp=camp,
user=users[4],
seats=1,
location="From Aarhus",
from_location="Aarhus",
to_location="BornHack",
when=timezone.datetime(year, 8, 27, 12, 0, tzinfo=timezone.utc),
description="I need a ride and have a large backpack",
)

View file

@ -12,7 +12,9 @@ logger = logging.getLogger("bornhack.%s" % __name__)
def generate_pdf_letter(filename, template, formatdict):
logger.debug("Generating PDF with filename %s and template %s" % (filename, template))
logger.debug(
"Generating PDF with filename %s and template %s" % (filename, template)
)
# conjure up a fake request for PDFTemplateResponse
request = RequestFactory().get("/")
@ -47,9 +49,9 @@ def generate_pdf_letter(filename, template, formatdict):
# add the watermark to all pages
for pagenum in range(pdfreader.getNumPages()):
page = watermark.getPage(0)
page = pdfreader.getPage(pagenum)
try:
page.mergePage(pdfreader.getPage(pagenum))
page.mergePage(watermark.getPage(0))
except ValueError:
# watermark pdf might be broken?
return False
@ -65,4 +67,3 @@ def generate_pdf_letter(filename, template, formatdict):
returnfile = io.BytesIO()
finalpdf.write(returnfile)
return returnfile

View file

@ -0,0 +1,23 @@
import base64
import io
import qrcode
from django import template
from django.utils.safestring import mark_safe
register = template.Library()
@register.simple_tag
def qr_code(value):
stream = io.BytesIO()
img = qrcode.make("#" + value, box_size=7)
img.save(stream, "PNG")
data = base64.b64encode(stream.getvalue())
return mark_safe(
"<figure style='text-align: center;'>"
'<img src="data:image/png;base64,{}" alt="">'
"<figcaption style='text-align: center;'>{}</figcaption>"
"</figure>".format(data.decode(), value)
)