Merge pull request #361 from bornhack/backoffice_invoice_scanning
Save ticket tokens to db. Rename some fields to make using tickets more sane. Initial scanning of tickets.
This commit is contained in:
commit
2c8f04a9a0
|
@ -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.
|
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>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
|
|
|
@ -16,17 +16,14 @@
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
{% if perms.camps.infoteam_permission %}
|
{% if perms.camps.infoteam_permission %}
|
||||||
<h3>Info Team</h3>
|
<h3>Info Team</h3>
|
||||||
<a href="{% url 'backoffice:product_handout' camp_slug=camp.slug %}" class="list-group-item">
|
<a href="{% url 'backoffice:user_interaction' camp_slug=camp.slug %}"
|
||||||
<h4 class="list-group-item-heading">Hand Out Products</h4>
|
class="list-group-item">
|
||||||
<p class="list-group-item-text">Use this view to mark products such as merchandise, cabins, fridges and so on as handed out.</p>
|
<h4 class="list-group-item-heading">
|
||||||
</a>
|
User stuff
|
||||||
<a href="{% url 'backoffice:ticket_checkin' camp_slug=camp.slug %}" class="list-group-item">
|
</h4>
|
||||||
<h4 class="list-group-item-heading">Check-In Tickets</h4>
|
<p class="list-group-item-text">
|
||||||
<p class="list-group-item-text">Use this view to check-in tickets when participants arrive.</p>
|
Use this to get everything related to a user
|
||||||
</a>
|
</p>
|
||||||
<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>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
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>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
|
|
|
@ -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.
|
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>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
|
|
90
src/backoffice/templates/user/search.html
Normal file
90
src/backoffice/templates/user/search.html
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
{% 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;"/>
|
||||||
|
|
||||||
|
<span id="scan_again" hidden>Scan again!</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if ticket %}
|
||||||
|
{{ ticket }}<br />
|
||||||
|
<br />
|
||||||
|
Used?: {{ ticket.used }}
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<form id="check_in_form" method="POST" action="">{% csrf_token %}
|
||||||
|
<input type="input" name="check_in_ticket_id" id="check_in_input" value="{{ ticket.pk }}" style="color: #fff; background: #fff; border: 0; height: 0; width: 0;"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
{% qr_code "clear" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
{% qr_code "check-in" %}
|
||||||
|
</div>
|
||||||
|
</divc>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
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 check_in_form = document.getElementById('check_in_form');
|
||||||
|
|
||||||
|
search_form.onsubmit = submit;
|
||||||
|
|
||||||
|
function submit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
console.log(ticket_token_input.value);
|
||||||
|
|
||||||
|
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.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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock content %}
|
||||||
|
|
|
@ -7,6 +7,9 @@ app_name = "backoffice"
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", BackofficeIndexView.as_view(), name="index"),
|
path("", BackofficeIndexView.as_view(), name="index"),
|
||||||
# infodesk
|
# infodesk
|
||||||
|
path(
|
||||||
|
"user/", include([path("", SearchForUser.as_view(), name="user_interaction")])
|
||||||
|
),
|
||||||
path("product_handout/", ProductHandoutView.as_view(), name="product_handout"),
|
path("product_handout/", ProductHandoutView.as_view(), name="product_handout"),
|
||||||
path("badge_handout/", BadgeHandoutView.as_view(), name="badge_handout"),
|
path("badge_handout/", BadgeHandoutView.as_view(), name="badge_handout"),
|
||||||
path("ticket_checkin/", TicketCheckinView.as_view(), name="ticket_checkin"),
|
path("ticket_checkin/", TicketCheckinView.as_view(), name="ticket_checkin"),
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import logging, os
|
import logging, os
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
|
import qrcode
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin, UserPassesTestMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin, UserPassesTestMixin
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.views.generic import TemplateView, ListView, DetailView
|
from django.views.generic import TemplateView, ListView, DetailView
|
||||||
|
@ -14,7 +15,7 @@ from django.conf import settings
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
|
|
||||||
from camps.mixins import CampViewMixin
|
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 tickets.models import ShopTicket, SponsorTicket, DiscountTicket
|
||||||
from profiles.models import Profile
|
from profiles.models import Profile
|
||||||
from program.models import SpeakerProposal, EventProposal
|
from program.models import SpeakerProposal, EventProposal
|
||||||
|
@ -40,7 +41,7 @@ class ProductHandoutView(CampViewMixin, InfoTeamPermissionMixin, ListView):
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
return OrderProductRelation.objects.filter(
|
return OrderProductRelation.objects.filter(
|
||||||
handed_out=False,
|
ticket_generated=False,
|
||||||
order__paid=True,
|
order__paid=True,
|
||||||
order__refunded=False,
|
order__refunded=False,
|
||||||
order__cancelled=False,
|
order__cancelled=False,
|
||||||
|
@ -52,9 +53,9 @@ class BadgeHandoutView(CampViewMixin, InfoTeamPermissionMixin, ListView):
|
||||||
context_object_name = "tickets"
|
context_object_name = "tickets"
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
shoptickets = ShopTicket.objects.filter(badge_handed_out=False)
|
shoptickets = ShopTicket.objects.filter(badge_ticket_generated=False)
|
||||||
sponsortickets = SponsorTicket.objects.filter(badge_handed_out=False)
|
sponsortickets = SponsorTicket.objects.filter(badge_ticket_generated=False)
|
||||||
discounttickets = DiscountTicket.objects.filter(badge_handed_out=False)
|
discounttickets = DiscountTicket.objects.filter(badge_ticket_generated=False)
|
||||||
return list(chain(shoptickets, sponsortickets, discounttickets))
|
return list(chain(shoptickets, sponsortickets, discounttickets))
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,9 +64,9 @@ class TicketCheckinView(CampViewMixin, InfoTeamPermissionMixin, ListView):
|
||||||
context_object_name = "tickets"
|
context_object_name = "tickets"
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
shoptickets = ShopTicket.objects.filter(checked_in=False)
|
shoptickets = ShopTicket.objects.filter(used=False)
|
||||||
sponsortickets = SponsorTicket.objects.filter(checked_in=False)
|
sponsortickets = SponsorTicket.objects.filter(used=False)
|
||||||
discounttickets = DiscountTicket.objects.filter(checked_in=False)
|
discounttickets = DiscountTicket.objects.filter(used=False)
|
||||||
return list(chain(shoptickets, sponsortickets, discounttickets))
|
return list(chain(shoptickets, sponsortickets, discounttickets))
|
||||||
|
|
||||||
|
|
||||||
|
@ -151,7 +152,7 @@ class MerchandiseOrdersView(CampViewMixin, OrgaTeamPermissionMixin, ListView):
|
||||||
|
|
||||||
return (
|
return (
|
||||||
OrderProductRelation.objects.filter(
|
OrderProductRelation.objects.filter(
|
||||||
handed_out=False,
|
ticket_generated=False,
|
||||||
order__paid=True,
|
order__paid=True,
|
||||||
order__refunded=False,
|
order__refunded=False,
|
||||||
order__cancelled=False,
|
order__cancelled=False,
|
||||||
|
@ -169,7 +170,7 @@ class MerchandiseToOrderView(CampViewMixin, OrgaTeamPermissionMixin, TemplateVie
|
||||||
camp_prefix = "BornHack {}".format(timezone.now().year)
|
camp_prefix = "BornHack {}".format(timezone.now().year)
|
||||||
|
|
||||||
order_relations = OrderProductRelation.objects.filter(
|
order_relations = OrderProductRelation.objects.filter(
|
||||||
handed_out=False,
|
ticket_generated=False,
|
||||||
order__paid=True,
|
order__paid=True,
|
||||||
order__refunded=False,
|
order__refunded=False,
|
||||||
order__cancelled=False,
|
order__cancelled=False,
|
||||||
|
@ -197,7 +198,7 @@ class VillageOrdersView(CampViewMixin, OrgaTeamPermissionMixin, ListView):
|
||||||
|
|
||||||
return (
|
return (
|
||||||
OrderProductRelation.objects.filter(
|
OrderProductRelation.objects.filter(
|
||||||
handed_out=False,
|
ticket_generated=False,
|
||||||
order__paid=True,
|
order__paid=True,
|
||||||
order__refunded=False,
|
order__refunded=False,
|
||||||
order__cancelled=False,
|
order__cancelled=False,
|
||||||
|
@ -215,7 +216,7 @@ class VillageToOrderView(CampViewMixin, OrgaTeamPermissionMixin, TemplateView):
|
||||||
camp_prefix = "BornHack {}".format(timezone.now().year)
|
camp_prefix = "BornHack {}".format(timezone.now().year)
|
||||||
|
|
||||||
order_relations = OrderProductRelation.objects.filter(
|
order_relations = OrderProductRelation.objects.filter(
|
||||||
handed_out=False,
|
ticket_generated=False,
|
||||||
order__paid=True,
|
order__paid=True,
|
||||||
order__refunded=False,
|
order__refunded=False,
|
||||||
order__cancelled=False,
|
order__cancelled=False,
|
||||||
|
@ -345,7 +346,6 @@ class ReimbursementCreateView(CampViewMixin, EconomyTeamPermissionMixin, CreateV
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
""" Get the user from 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"])
|
self.reimbursement_user = get_object_or_404(User, pk=kwargs["user_id"])
|
||||||
|
|
||||||
# get response now so we have self.camp available below
|
# get response now so we have self.camp available below
|
||||||
|
@ -544,3 +544,30 @@ class RevenueDetailView(CampViewMixin, EconomyTeamPermissionMixin, UpdateView):
|
||||||
return redirect(
|
return redirect(
|
||||||
reverse("backoffice:revenue_list", kwargs={"camp_slug": self.camp.slug})
|
reverse("backoffice:revenue_list", kwargs={"camp_slug": self.camp.slug})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SearchForUser(TemplateView):
|
||||||
|
template_name = "user/search.html"
|
||||||
|
|
||||||
|
def post(self, request, **kwargs):
|
||||||
|
check_in_ticket_id = request.POST.get("check_in_ticket_id")
|
||||||
|
if check_in_ticket_id:
|
||||||
|
ticket_to_check_in = ShopTicket.objects.get(pk=check_in_ticket_id)
|
||||||
|
ticket_to_check_in.used = True
|
||||||
|
ticket_to_check_in.save()
|
||||||
|
messages.info(request, "Ticket checked-in!")
|
||||||
|
|
||||||
|
return super().get(request, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
ticket_token = self.request.POST.get("ticket_token")
|
||||||
|
if ticket_token:
|
||||||
|
try:
|
||||||
|
ticket = ShopTicket.objects.get(token=ticket_token[1:])
|
||||||
|
context["ticket"] = ticket
|
||||||
|
except ShopTicket.DoesNotExist:
|
||||||
|
messages.warning(self.request, "Ticket not found!")
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<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">
|
<form method="POST" class="form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% bootstrap_field form.name %}
|
{% bootstrap_field form.name %}
|
||||||
|
|
|
@ -23,9 +23,13 @@
|
||||||
Checked in
|
Checked in
|
||||||
<th>
|
<th>
|
||||||
Actions
|
Actions
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for ticket in tickets %}
|
{% for ticket in tickets %}
|
||||||
|
{% ifchanged ticket.ticket_type.camp %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5">
|
||||||
|
<strong>{{ ticket.ticket_type.camp }}</strong>
|
||||||
|
{% endifchanged %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{% if ticket.name %}
|
{% if ticket.name %}
|
||||||
|
@ -38,7 +42,7 @@
|
||||||
<td>
|
<td>
|
||||||
{{ ticket.product.price|currency }}
|
{{ ticket.product.price|currency }}
|
||||||
<td>
|
<td>
|
||||||
{% if ticket.checked_in %}
|
{% if ticket.used %}
|
||||||
Yes
|
Yes
|
||||||
{% else %}
|
{% else %}
|
||||||
Not yet
|
Not yet
|
||||||
|
|
|
@ -46,4 +46,4 @@ class OrderProductRelationFactory(DjangoModelFactory):
|
||||||
product = factory.SubFactory(ProductFactory)
|
product = factory.SubFactory(ProductFactory)
|
||||||
order = factory.SubFactory(OrderFactory)
|
order = factory.SubFactory(OrderFactory)
|
||||||
quantity = 1
|
quantity = 1
|
||||||
handed_out = False
|
ticket_generated = False
|
||||||
|
|
18
src/shop/migrations/0059_auto_20190718_2051.py
Normal file
18
src/shop/migrations/0059_auto_20190718_2051.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
|
@ -239,8 +239,8 @@ class Order(CreatedUpdatedModel):
|
||||||
else:
|
else:
|
||||||
print(msg)
|
print(msg)
|
||||||
|
|
||||||
# and mark the OPR as handed_out=True
|
# and mark the OPR as ticket_generated=True
|
||||||
order_product.handed_out = True
|
order_product.ticket_generated = True
|
||||||
order_product.save()
|
order_product.save()
|
||||||
|
|
||||||
def mark_as_paid(self, request=None):
|
def mark_as_paid(self, request=None):
|
||||||
|
@ -289,35 +289,36 @@ class Order(CreatedUpdatedModel):
|
||||||
self.open = None
|
self.open = None
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def is_not_handed_out(self):
|
def is_not_ticket_generated(self):
|
||||||
if self.orderproductrelation_set.filter(handed_out=True).count() == 0:
|
if self.orderproductrelation_set.filter(tic=True).count() == 0:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def is_partially_handed_out(self):
|
def is_partially_ticket_generated(self):
|
||||||
if (
|
if (
|
||||||
self.orderproductrelation_set.filter(handed_out=True).count() != 0
|
self.orderproductrelation_set.filter(ticket_generated=True).count() != 0
|
||||||
and self.orderproductrelation_set.filter(handed_out=False).count() != 0
|
and self.orderproductrelation_set.filter(ticket_generated=False).count()
|
||||||
|
!= 0
|
||||||
):
|
):
|
||||||
# some products are handed out, others are not
|
# some products are handed out, others are not
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def is_fully_handed_out(self):
|
def is_fully_ticket_generated(self):
|
||||||
if self.orderproductrelation_set.filter(handed_out=False).count() == 0:
|
if self.orderproductrelation_set.filter(ticket_generated=False).count() == 0:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def handed_out_status(self):
|
def ticket_generated_status(self):
|
||||||
if self.is_not_handed_out():
|
if self.is_not_ticket_generated():
|
||||||
return "no"
|
return "no"
|
||||||
elif self.is_partially_handed_out():
|
elif self.is_partially_ticket_generated():
|
||||||
return "partially"
|
return "partially"
|
||||||
elif self.is_fully_handed_out():
|
elif self.is_fully_ticket_generated():
|
||||||
return "fully"
|
return "fully"
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
@ -466,14 +467,14 @@ class OrderProductRelation(CreatedUpdatedModel):
|
||||||
order = models.ForeignKey("shop.Order", on_delete=models.PROTECT)
|
order = models.ForeignKey("shop.Order", on_delete=models.PROTECT)
|
||||||
product = models.ForeignKey("shop.Product", on_delete=models.PROTECT)
|
product = models.ForeignKey("shop.Product", on_delete=models.PROTECT)
|
||||||
quantity = models.PositiveIntegerField()
|
quantity = models.PositiveIntegerField()
|
||||||
handed_out = models.BooleanField(default=False)
|
ticket_generated = models.BooleanField(default=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total(self):
|
def total(self):
|
||||||
return Decimal(self.product.price * self.quantity)
|
return Decimal(self.product.price * self.quantity)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.handed_out and not self.order.paid:
|
if self.ticket_generated and not self.order.paid:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
"Product can not be handed out when order is not paid."
|
"Product can not be handed out when order is not paid."
|
||||||
)
|
)
|
||||||
|
|
|
@ -25,13 +25,13 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for order in orders %}
|
{% for order in orders %}
|
||||||
{% if order.products.exists %}
|
{% 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.id }}</td>
|
||||||
<td>{{ order.get_number_of_items }}</td>
|
<td>{{ order.get_number_of_items }}</td>
|
||||||
<td>{{ order.total|currency }}</td>
|
<td>{{ order.total|currency }}</td>
|
||||||
<td class="text-center">{{ order.open|truefalseicon }}</td>
|
<td class="text-center">{{ order.open|truefalseicon }}</td>
|
||||||
<td class="text-center">{{ order.paid|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>
|
<td>
|
||||||
{% if order.paid %}
|
{% if order.paid %}
|
||||||
{% if order.invoice.pdf %}
|
{% if order.invoice.pdf %}
|
||||||
|
|
|
@ -23,9 +23,9 @@ class TicketTypeAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
@admin.register(SponsorTicket)
|
@admin.register(SponsorTicket)
|
||||||
class SponsorTicketAdmin(BaseTicketAdmin):
|
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"]
|
search_fields = ["pk", "sponsor__name"]
|
||||||
|
|
||||||
|
@ -50,10 +50,10 @@ class ShopTicketAdmin(BaseTicketAdmin):
|
||||||
"ticket_type",
|
"ticket_type",
|
||||||
"order",
|
"order",
|
||||||
"product",
|
"product",
|
||||||
"checked_in",
|
"used",
|
||||||
]
|
]
|
||||||
|
|
||||||
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"]
|
search_fields = ["uuid", "order__id", "order__user__email", "name", "email"]
|
||||||
|
|
||||||
|
|
28
src/tickets/migrations/0006_auto_20190616_1746.py
Normal file
28
src/tickets/migrations/0006_auto_20190616_1746.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
29
src/tickets/migrations/0007_save_token_to_db.py
Normal file
29
src/tickets/migrations/0007_save_token_to_db.py
Normal 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)]
|
43
src/tickets/migrations/0008_auto_20190718_2055.py
Normal file
43
src/tickets/migrations/0008_auto_20190718_2055.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
18
src/tickets/migrations/0009_tickettype_includes_badge.py
Normal file
18
src/tickets/migrations/0009_tickettype_includes_badge.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
28
src/tickets/migrations/0010_auto_20190724_2037.py
Normal file
28
src/tickets/migrations/0010_auto_20190724_2037.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
29
src/tickets/migrations/0011_save_badge_token_to_db.py
Normal file
29
src/tickets/migrations/0011_save_badge_token_to_db.py
Normal 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)]
|
28
src/tickets/migrations/0012_auto_20190724_2037.py
Normal file
28
src/tickets/migrations/0012_auto_20190724_2037.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -17,15 +17,32 @@ logger = logging.getLogger("bornhack.%s" % __name__)
|
||||||
class TicketType(CampRelatedModel, UUIDModel):
|
class TicketType(CampRelatedModel, UUIDModel):
|
||||||
name = models.TextField()
|
name = models.TextField()
|
||||||
camp = models.ForeignKey("camps.Camp", on_delete=models.PROTECT)
|
camp = models.ForeignKey("camps.Camp", on_delete=models.PROTECT)
|
||||||
|
includes_badge = models.BooleanField(default=False)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{} ({})".format(self.name, self.camp.title)
|
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):
|
class BaseTicket(CampRelatedModel, UUIDModel):
|
||||||
ticket_type = models.ForeignKey("TicketType", on_delete=models.PROTECT)
|
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)
|
badge_handed_out = models.BooleanField(default=False)
|
||||||
|
token = models.CharField(max_length=64)
|
||||||
|
badge_token = models.CharField(max_length=64)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
@ -34,27 +51,33 @@ class BaseTicket(CampRelatedModel, UUIDModel):
|
||||||
def camp(self):
|
def camp(self):
|
||||||
return self.ticket_type.camp
|
return self.ticket_type.camp
|
||||||
|
|
||||||
def _get_token(self):
|
def save(self, **kwargs):
|
||||||
return hashlib.sha256(
|
self.token = self._get_token()
|
||||||
"{_id}{secret_key}".format(
|
self.badge_token = self._get_token()
|
||||||
_id=self.pk, secret_key=settings.SECRET_KEY
|
super().save(**kwargs)
|
||||||
).encode("utf-8")
|
|
||||||
).hexdigest()
|
|
||||||
|
|
||||||
def get_qr_code_base64(self):
|
def _get_token(self):
|
||||||
qr = qrcode.make(
|
return create_ticket_token(
|
||||||
self._get_token(),
|
"{_id}{secret_key}".format(
|
||||||
version=1,
|
_id=self.uuid, secret_key=settings.SECRET_KEY
|
||||||
error_correction=qrcode.constants.ERROR_CORRECT_H,
|
).encode("utf-8")
|
||||||
).resize((250, 250))
|
)
|
||||||
file_like = io.BytesIO()
|
|
||||||
qr.save(file_like, format="png")
|
def _get_badge_token(self):
|
||||||
qrcode_base64 = base64.b64encode(file_like.getvalue())
|
return create_ticket_token(
|
||||||
return qrcode_base64
|
"{_id}{secret_key}-badge".format(
|
||||||
|
_id=self.uuid, secret_key=settings.SECRET_KEY
|
||||||
|
).encode("utf-8")
|
||||||
|
)
|
||||||
|
|
||||||
def get_qr_code_url(self):
|
def get_qr_code_url(self):
|
||||||
return "data:image/png;base64,{}".format(
|
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):
|
def generate_pdf(self):
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
{% load static from staticfiles %}
|
{% load static from staticfiles %}
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
|
||||||
<table style="width:100%;">
|
<table style="width:100%;">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="width: 75%;"> </td>
|
<td style="width: 75%;"> </td>
|
||||||
|
@ -25,11 +24,38 @@
|
||||||
<h3>Sponsor: {{ ticket.sponsor.name }} </h3>
|
<h3>Sponsor: {{ ticket.sponsor.name }} </h3>
|
||||||
<img src="{% static 'img/sponsors/' %}{{ sponsor.logo_filename }}"></img>
|
<img src="{% static 'img/sponsors/' %}{{ sponsor.logo_filename }}"></img>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if ticket.checked_in %}
|
|
||||||
<h2>This ticket has been checked in.</h2>
|
{% if ticket.used %}
|
||||||
|
<h2>This ticket has been used.</h2>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<center>
|
<center>
|
||||||
<img src="{{ ticket.get_qr_code_url }}"></img>
|
<img src="{{ ticket.get_qr_code_url }}"></img>
|
||||||
<p>Ticket #{{ ticket.pk }}</p>
|
<p>{{ ticket.token }}</p>
|
||||||
</center>
|
</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%;"> </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 %}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ class ShopTicketListView(LoginRequiredMixin, ListView):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
tickets = super(ShopTicketListView, self).get_queryset()
|
tickets = super(ShopTicketListView, self).get_queryset()
|
||||||
user = self.request.user
|
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):
|
class ShopTicketDownloadView(LoginRequiredMixin, SingleObjectMixin, View):
|
||||||
|
|
|
@ -12,7 +12,9 @@ logger = logging.getLogger("bornhack.%s" % __name__)
|
||||||
|
|
||||||
|
|
||||||
def generate_pdf_letter(filename, template, formatdict):
|
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
|
# conjure up a fake request for PDFTemplateResponse
|
||||||
request = RequestFactory().get("/")
|
request = RequestFactory().get("/")
|
||||||
|
@ -47,9 +49,9 @@ def generate_pdf_letter(filename, template, formatdict):
|
||||||
|
|
||||||
# add the watermark to all pages
|
# add the watermark to all pages
|
||||||
for pagenum in range(pdfreader.getNumPages()):
|
for pagenum in range(pdfreader.getNumPages()):
|
||||||
page = watermark.getPage(0)
|
page = pdfreader.getPage(pagenum)
|
||||||
try:
|
try:
|
||||||
page.mergePage(pdfreader.getPage(pagenum))
|
page.mergePage(watermark.getPage(0))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# watermark pdf might be broken?
|
# watermark pdf might be broken?
|
||||||
return False
|
return False
|
||||||
|
@ -65,4 +67,3 @@ def generate_pdf_letter(filename, template, formatdict):
|
||||||
returnfile = io.BytesIO()
|
returnfile = io.BytesIO()
|
||||||
finalpdf.write(returnfile)
|
finalpdf.write(returnfile)
|
||||||
return returnfile
|
return returnfile
|
||||||
|
|
||||||
|
|
23
src/utils/templatetags/qrcode.py
Normal file
23
src/utils/templatetags/qrcode.py
Normal 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=5)
|
||||||
|
img.save(stream, "PNG")
|
||||||
|
data = base64.b64encode(stream.getvalue())
|
||||||
|
|
||||||
|
return mark_safe(
|
||||||
|
"<figure>"
|
||||||
|
'<img src="data:image/png;base64,{}" alt="">'
|
||||||
|
"<figcaption>{}</figcaption>"
|
||||||
|
"</figure>".format(data.decode(), value)
|
||||||
|
)
|
Loading…
Reference in a new issue