From 5911d2042b34c7794c9e5ea9bc72a9211b6edb49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Wed, 17 Jul 2019 22:02:47 +0200 Subject: [PATCH 1/6] Initial work on a more lean infodesk. --- src/backoffice/templates/index.html | 19 ++-- .../templates/user/order_all_actions.html | 13 +++ src/backoffice/templates/user/search.html | 90 +++++++++++++++++++ src/backoffice/urls.py | 3 + src/backoffice/views.py | 31 ++++++- .../migrations/0006_auto_20190616_1746.py | 28 ++++++ src/tickets/models.py | 7 +- src/utils/templatetags/qrcode.py | 23 +++++ 8 files changed, 200 insertions(+), 14 deletions(-) create mode 100644 src/backoffice/templates/user/order_all_actions.html create mode 100644 src/backoffice/templates/user/search.html create mode 100644 src/tickets/migrations/0006_auto_20190616_1746.py create mode 100644 src/utils/templatetags/qrcode.py diff --git a/src/backoffice/templates/index.html b/src/backoffice/templates/index.html index d6764c7a..19f97455 100644 --- a/src/backoffice/templates/index.html +++ b/src/backoffice/templates/index.html @@ -16,17 +16,14 @@
{% if perms.camps.infoteam_permission %}

Info Team

- -

Hand Out Products

-

Use this view to mark products such as merchandise, cabins, fridges and so on as handed out.

-
- -

Check-In Tickets

-

Use this view to check-in tickets when participants arrive.

-
- -

Hand Out Badges

-

Use this view to mark badges as handed out.

+
+

+ User stuff +

+

+ Use this to get everything related to a user +

{% endif %} diff --git a/src/backoffice/templates/user/order_all_actions.html b/src/backoffice/templates/user/order_all_actions.html new file mode 100644 index 00000000..8ccb85f6 --- /dev/null +++ b/src/backoffice/templates/user/order_all_actions.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} +{% load static from staticfiles %} + +{% block content %} + +Orders + +Tickets (incl. badges) + +Merchandise + +{% endblock content %} + diff --git a/src/backoffice/templates/user/search.html b/src/backoffice/templates/user/search.html new file mode 100644 index 00000000..7c75d026 --- /dev/null +++ b/src/backoffice/templates/user/search.html @@ -0,0 +1,90 @@ +{% extends 'base.html' %} +{% load static from staticfiles %} +{% load qrcode %} + +{% block content %} + +
+ {% csrf_token %} + +
+ +
+

Scan the ticket!

+ + + + +
+ +
+ +
+ +{% if ticket %} + {{ ticket }}
+
+ Checked in?: {{ ticket.checked_in }} + +
+
{% csrf_token %} + +
+ +
+
+ {% qr_code "clear" %} +
+ +
+ {% qr_code "check-in" %} +
+ +{% endif %} + + + + + +{% endblock content %} + diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py index aa5165f9..a892343d 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -7,6 +7,9 @@ app_name = "backoffice" urlpatterns = [ path("", BackofficeIndexView.as_view(), name="index"), # infodesk + path( + "user/", include([path("", SearchForUser.as_view(), name="user_interaction")]) + ), 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"), diff --git a/src/backoffice/views.py b/src/backoffice/views.py index 76e121b4..07cfbba2 100644 --- a/src/backoffice/views.py +++ b/src/backoffice/views.py @@ -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 @@ -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,30 @@ class RevenueDetailView(CampViewMixin, EconomyTeamPermissionMixin, UpdateView): return redirect( 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.checked_in = 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 diff --git a/src/tickets/migrations/0006_auto_20190616_1746.py b/src/tickets/migrations/0006_auto_20190616_1746.py new file mode 100644 index 00000000..f2fac385 --- /dev/null +++ b/src/tickets/migrations/0006_auto_20190616_1746.py @@ -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), + ), + ] diff --git a/src/tickets/models.py b/src/tickets/models.py index 1f2e6cd7..5cd9312e 100644 --- a/src/tickets/models.py +++ b/src/tickets/models.py @@ -26,6 +26,7 @@ class BaseTicket(CampRelatedModel, UUIDModel): ticket_type = models.ForeignKey("TicketType", on_delete=models.PROTECT) checked_in = models.BooleanField(default=False) badge_handed_out = models.BooleanField(default=False) + token = models.CharField(max_length=64) class Meta: abstract = True @@ -34,10 +35,14 @@ class BaseTicket(CampRelatedModel, UUIDModel): def camp(self): return self.ticket_type.camp + def save(self, **kwargs): + self.token = self._get_token() + super().save(**kwargs) + def _get_token(self): return hashlib.sha256( "{_id}{secret_key}".format( - _id=self.pk, secret_key=settings.SECRET_KEY + _id=self.uuid, secret_key=settings.SECRET_KEY ).encode("utf-8") ).hexdigest() diff --git a/src/utils/templatetags/qrcode.py b/src/utils/templatetags/qrcode.py new file mode 100644 index 00000000..d09b8e5c --- /dev/null +++ b/src/utils/templatetags/qrcode.py @@ -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( + "
" + '' + "
{}
" + "
".format(data.decode(), value) + ) From b1b810f165c5ea6c8ad61b6650fab17655572871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Thu, 18 Jul 2019 21:04:49 +0200 Subject: [PATCH 2/6] Rename handed_out field on ORP to ticket_generated. Rename checked_in on tickets to used. Add save token migration. --- src/backoffice/templates/ticket_checkin.html | 2 +- src/backoffice/templates/user/search.html | 2 +- src/backoffice/views.py | 8 ++-- .../templates/tickets/ticket_detail.html | 2 +- .../templates/tickets/ticket_list.html | 2 +- .../migrations/0059_auto_20190718_2051.py | 18 ++++++++ src/shop/models.py | 6 +-- src/tickets/admin.py | 8 ++-- .../migrations/0007_save_token_to_db.py | 33 ++++++++++++++ .../migrations/0008_auto_20190718_2055.py | 43 +++++++++++++++++++ src/tickets/models.py | 2 +- src/tickets/templates/pdf/ticket.html | 4 +- 12 files changed, 112 insertions(+), 18 deletions(-) create mode 100644 src/shop/migrations/0059_auto_20190718_2051.py create mode 100644 src/tickets/migrations/0007_save_token_to_db.py create mode 100644 src/tickets/migrations/0008_auto_20190718_2055.py diff --git a/src/backoffice/templates/ticket_checkin.html b/src/backoffice/templates/ticket_checkin.html index b35181ef..85344325 100644 --- a/src/backoffice/templates/ticket_checkin.html +++ b/src/backoffice/templates/ticket_checkin.html @@ -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 Badge Handout view instead. To hand out other products go to the Hand Out Products view instead.
- 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.

diff --git a/src/backoffice/templates/user/search.html b/src/backoffice/templates/user/search.html index 7c75d026..37afbdbd 100644 --- a/src/backoffice/templates/user/search.html +++ b/src/backoffice/templates/user/search.html @@ -24,7 +24,7 @@ {% if ticket %} {{ ticket }}

- Checked in?: {{ ticket.checked_in }} + Used?: {{ ticket.used }}
{% csrf_token %} diff --git a/src/backoffice/views.py b/src/backoffice/views.py index 07cfbba2..e66c98d6 100644 --- a/src/backoffice/views.py +++ b/src/backoffice/views.py @@ -64,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)) @@ -553,7 +553,7 @@ class SearchForUser(TemplateView): 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.checked_in = True + ticket_to_check_in.used = True ticket_to_check_in.save() messages.info(request, "Ticket checked-in!") diff --git a/src/profiles/templates/tickets/ticket_detail.html b/src/profiles/templates/tickets/ticket_detail.html index 16d24de7..a234a668 100644 --- a/src/profiles/templates/tickets/ticket_detail.html +++ b/src/profiles/templates/tickets/ticket_detail.html @@ -10,7 +10,7 @@
-

{% if ticket.checked_in %}This ticket has been used{% else %}This ticket is unused{% endif %}

+

{% if ticket.used %}This ticket has been used{% else %}This ticket is unused{% endif %}

{% csrf_token %} {% bootstrap_field form.name %} diff --git a/src/profiles/templates/tickets/ticket_list.html b/src/profiles/templates/tickets/ticket_list.html index 95aae6f1..c00a9a79 100644 --- a/src/profiles/templates/tickets/ticket_list.html +++ b/src/profiles/templates/tickets/ticket_list.html @@ -38,7 +38,7 @@ {{ ticket.product.price|currency }} - {% if ticket.checked_in %} + {% if ticket.used %} Yes {% else %} Not yet diff --git a/src/shop/migrations/0059_auto_20190718_2051.py b/src/shop/migrations/0059_auto_20190718_2051.py new file mode 100644 index 00000000..8d423deb --- /dev/null +++ b/src/shop/migrations/0059_auto_20190718_2051.py @@ -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', + ), + ] diff --git a/src/shop/models.py b/src/shop/models.py index d8f1480a..36fd3d2d 100644 --- a/src/shop/models.py +++ b/src/shop/models.py @@ -239,8 +239,8 @@ class Order(CreatedUpdatedModel): 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() def mark_as_paid(self, request=None): @@ -466,7 +466,7 @@ 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): diff --git a/src/tickets/admin.py b/src/tickets/admin.py index 45a28a25..478bfca6 100644 --- a/src/tickets/admin.py +++ b/src/tickets/admin.py @@ -23,9 +23,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,10 +50,10 @@ class ShopTicketAdmin(BaseTicketAdmin): "ticket_type", "order", "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"] diff --git a/src/tickets/migrations/0007_save_token_to_db.py b/src/tickets/migrations/0007_save_token_to_db.py new file mode 100644 index 00000000..aba23f66 --- /dev/null +++ b/src/tickets/migrations/0007_save_token_to_db.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.2 on 2019-07-18 18:52 +import hashlib + +from django.conf import settings +from django.db import migrations + + +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 = hashlib.sha256( + "{_id}{secret_key}".format( + _id=ticket.uuid, secret_key=settings.SECRET_KEY + ).encode("utf-8") + ).hexdigest() + ticket.token = token + ticket.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0006_auto_20190616_1746'), + ] + + operations = [ + migrations.RunPython(save_tokens) + ] diff --git a/src/tickets/migrations/0008_auto_20190718_2055.py b/src/tickets/migrations/0008_auto_20190718_2055.py new file mode 100644 index 00000000..3462307b --- /dev/null +++ b/src/tickets/migrations/0008_auto_20190718_2055.py @@ -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), + ), + ] diff --git a/src/tickets/models.py b/src/tickets/models.py index 5cd9312e..49ffeab7 100644 --- a/src/tickets/models.py +++ b/src/tickets/models.py @@ -24,7 +24,7 @@ class TicketType(CampRelatedModel, UUIDModel): 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) diff --git a/src/tickets/templates/pdf/ticket.html b/src/tickets/templates/pdf/ticket.html index 7623325e..d8e3eaa8 100644 --- a/src/tickets/templates/pdf/ticket.html +++ b/src/tickets/templates/pdf/ticket.html @@ -25,8 +25,8 @@

Sponsor: {{ ticket.sponsor.name }}

{% endif %} -{% if ticket.checked_in %} -

This ticket has been checked in.

+{% if ticket.used %} +

This ticket has been used.

{% endif %}
From cad3b21fc446d69dd99475fc8bfe736d89d84333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Thu, 18 Jul 2019 21:20:29 +0200 Subject: [PATCH 3/6] handed_out -> ticket_generated --- src/backoffice/templates/badge_handout.html | 2 +- src/backoffice/templates/product_handout.html | 2 +- src/backoffice/views.py | 16 ++++++------- src/shop/factories.py | 2 +- src/shop/models.py | 24 +++++++++---------- src/shop/templates/order_list.html | 4 ++-- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/backoffice/templates/badge_handout.html b/src/backoffice/templates/badge_handout.html index 722f9795..c92ed64b 100644 --- a/src/backoffice/templates/badge_handout.html +++ b/src/backoffice/templates/badge_handout.html @@ -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 Ticket Checkin view instead. To hand out merchandise and other products go to the Hand Out Products view instead.
- 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.

diff --git a/src/backoffice/templates/product_handout.html b/src/backoffice/templates/product_handout.html index 677ff974..a8db0054 100644 --- a/src/backoffice/templates/product_handout.html +++ b/src/backoffice/templates/product_handout.html @@ -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 Ticket Checkin view instead. To hand out badges go to the Badge Handout view instead.
- 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).

diff --git a/src/backoffice/views.py b/src/backoffice/views.py index e66c98d6..08a4a4cd 100644 --- a/src/backoffice/views.py +++ b/src/backoffice/views.py @@ -41,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, @@ -53,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)) @@ -152,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, @@ -170,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, @@ -198,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, @@ -216,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, diff --git a/src/shop/factories.py b/src/shop/factories.py index 3c65c43f..6bc42936 100644 --- a/src/shop/factories.py +++ b/src/shop/factories.py @@ -46,4 +46,4 @@ class OrderProductRelationFactory(DjangoModelFactory): product = factory.SubFactory(ProductFactory) order = factory.SubFactory(OrderFactory) quantity = 1 - handed_out = False + ticket_generated = False diff --git a/src/shop/models.py b/src/shop/models.py index 36fd3d2d..07091cf4 100644 --- a/src/shop/models.py +++ b/src/shop/models.py @@ -289,35 +289,35 @@ 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(tic=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 @@ -473,7 +473,7 @@ class OrderProductRelation(CreatedUpdatedModel): 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." ) diff --git a/src/shop/templates/order_list.html b/src/shop/templates/order_list.html index 54802925..1c0f5507 100644 --- a/src/shop/templates/order_list.html +++ b/src/shop/templates/order_list.html @@ -25,13 +25,13 @@ {% for order in orders %} {% if order.products.exists %} - + {{ order.id }} {{ order.get_number_of_items }} {{ order.total|currency }} {{ order.open|truefalseicon }} {{ order.paid|truefalseicon }} - {{ order.handed_out_status }} + {{ order.ticket_generated_status }} {% if order.paid %} {% if order.invoice.pdf %} From b7feb96c9b0bd1ff066da4c58be997958e4dd12c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Wed, 24 Jul 2019 21:08:36 +0200 Subject: [PATCH 4/6] Adding a badge token to tickets that grant that. --- src/shop/models.py | 3 +- .../migrations/0007_save_token_to_db.py | 22 ++++------ .../0009_tickettype_includes_badge.py | 18 ++++++++ .../migrations/0010_auto_20190724_2037.py | 28 ++++++++++++ .../migrations/0011_save_badge_token_to_db.py | 29 ++++++++++++ .../migrations/0012_auto_20190724_2037.py | 28 ++++++++++++ src/tickets/models.py | 44 +++++++++++++------ src/tickets/templates/pdf/ticket.html | 30 ++++++++++++- src/utils/pdf.py | 9 ++-- 9 files changed, 178 insertions(+), 33 deletions(-) create mode 100644 src/tickets/migrations/0009_tickettype_includes_badge.py create mode 100644 src/tickets/migrations/0010_auto_20190724_2037.py create mode 100644 src/tickets/migrations/0011_save_badge_token_to_db.py create mode 100644 src/tickets/migrations/0012_auto_20190724_2037.py diff --git a/src/shop/models.py b/src/shop/models.py index 07091cf4..cda98870 100644 --- a/src/shop/models.py +++ b/src/shop/models.py @@ -298,7 +298,8 @@ class Order(CreatedUpdatedModel): def is_partially_ticket_generated(self): if ( self.orderproductrelation_set.filter(ticket_generated=True).count() != 0 - and self.orderproductrelation_set.filter(ticket_generated=False).count() != 0 + and self.orderproductrelation_set.filter(ticket_generated=False).count() + != 0 ): # some products are handed out, others are not return True diff --git a/src/tickets/migrations/0007_save_token_to_db.py b/src/tickets/migrations/0007_save_token_to_db.py index aba23f66..cef68d1e 100644 --- a/src/tickets/migrations/0007_save_token_to_db.py +++ b/src/tickets/migrations/0007_save_token_to_db.py @@ -1,33 +1,29 @@ # Generated by Django 2.2.2 on 2019-07-18 18:52 -import hashlib - 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') + 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 = hashlib.sha256( + token = create_ticket_token( "{_id}{secret_key}".format( _id=ticket.uuid, secret_key=settings.SECRET_KEY ).encode("utf-8") - ).hexdigest() + ) ticket.token = token ticket.save() class Migration(migrations.Migration): - dependencies = [ - ('tickets', '0006_auto_20190616_1746'), - ] + dependencies = [("tickets", "0006_auto_20190616_1746")] - operations = [ - migrations.RunPython(save_tokens) - ] + operations = [migrations.RunPython(save_tokens)] diff --git a/src/tickets/migrations/0009_tickettype_includes_badge.py b/src/tickets/migrations/0009_tickettype_includes_badge.py new file mode 100644 index 00000000..82a5da1b --- /dev/null +++ b/src/tickets/migrations/0009_tickettype_includes_badge.py @@ -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), + ), + ] diff --git a/src/tickets/migrations/0010_auto_20190724_2037.py b/src/tickets/migrations/0010_auto_20190724_2037.py new file mode 100644 index 00000000..414dfbd5 --- /dev/null +++ b/src/tickets/migrations/0010_auto_20190724_2037.py @@ -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), + ), + ] diff --git a/src/tickets/migrations/0011_save_badge_token_to_db.py b/src/tickets/migrations/0011_save_badge_token_to_db.py new file mode 100644 index 00000000..dbc0883d --- /dev/null +++ b/src/tickets/migrations/0011_save_badge_token_to_db.py @@ -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)] diff --git a/src/tickets/migrations/0012_auto_20190724_2037.py b/src/tickets/migrations/0012_auto_20190724_2037.py new file mode 100644 index 00000000..ae3ce0db --- /dev/null +++ b/src/tickets/migrations/0012_auto_20190724_2037.py @@ -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), + ), + ] diff --git a/src/tickets/models.py b/src/tickets/models.py index 49ffeab7..36b4d969 100644 --- a/src/tickets/models.py +++ b/src/tickets/models.py @@ -17,16 +17,32 @@ 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) 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) used = 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: abstract = True @@ -37,29 +53,31 @@ class BaseTicket(CampRelatedModel, UUIDModel): def save(self, **kwargs): self.token = self._get_token() + self.badge_token = self._get_token() super().save(**kwargs) def _get_token(self): - return hashlib.sha256( + return create_ticket_token( "{_id}{secret_key}".format( _id=self.uuid, secret_key=settings.SECRET_KEY ).encode("utf-8") - ).hexdigest() + ) - 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_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): diff --git a/src/tickets/templates/pdf/ticket.html b/src/tickets/templates/pdf/ticket.html index d8e3eaa8..57ed3957 100644 --- a/src/tickets/templates/pdf/ticket.html +++ b/src/tickets/templates/pdf/ticket.html @@ -1,6 +1,5 @@ {% load static from staticfiles %} - @@ -25,11 +24,38 @@

Sponsor: {{ ticket.sponsor.name }}

{% endif %} + {% if ticket.used %}

This ticket has been used.

{% endif %}
-

Ticket #{{ ticket.pk }}

+

{{ ticket.token }}

+ +{% if ticket.ticket_type.includes_badge %} +
+ +
 
+ + + + +
  +

+ {{ ticket.created|date:"b jS, Y" }}
+

+
+
+ +

Badge voucher

+ +
+ +

{{ ticket.badge_token }}

+
+ +{% endif %} + + diff --git a/src/utils/pdf.py b/src/utils/pdf.py index 345aa03a..f9d43079 100644 --- a/src/utils/pdf.py +++ b/src/utils/pdf.py @@ -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 - From 5d3cfe62108000826c86ddf90a7b00f5a7284d89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Mon, 29 Jul 2019 21:16:48 +0200 Subject: [PATCH 5/6] Order tickets in account page. --- src/profiles/templates/tickets/ticket_list.html | 6 +++++- src/tickets/views.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/profiles/templates/tickets/ticket_list.html b/src/profiles/templates/tickets/ticket_list.html index c00a9a79..ee63ccd8 100644 --- a/src/profiles/templates/tickets/ticket_list.html +++ b/src/profiles/templates/tickets/ticket_list.html @@ -23,9 +23,13 @@ Checked in Actions - {% for ticket in tickets %} + {% ifchanged ticket.ticket_type.camp %} + + + {{ ticket.ticket_type.camp }} + {% endifchanged %} {% if ticket.name %} diff --git a/src/tickets/views.py b/src/tickets/views.py index 7641664d..5a41f47b 100644 --- a/src/tickets/views.py +++ b/src/tickets/views.py @@ -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): From 70984ab40fa5e73dd2e9a5b3964f75b63a259cda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Mon, 29 Jul 2019 21:26:31 +0200 Subject: [PATCH 6/6] Remove unused template. --- .../templates/user/order_all_actions.html | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 src/backoffice/templates/user/order_all_actions.html diff --git a/src/backoffice/templates/user/order_all_actions.html b/src/backoffice/templates/user/order_all_actions.html deleted file mode 100644 index 8ccb85f6..00000000 --- a/src/backoffice/templates/user/order_all_actions.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends 'base.html' %} -{% load static from staticfiles %} - -{% block content %} - -Orders - -Tickets (incl. badges) - -Merchandise - -{% endblock content %} -