From c28116455d26574f3e457bc6c72a7f760c60688e Mon Sep 17 00:00:00 2001 From: ThomasHoy Date: Wed, 19 Nov 2025 15:51:28 +0100 Subject: [PATCH 01/11] new site --- stregsystem/models.py | 276 ++++++++--- .../stregsystem/menu_user_tickets.html | 54 +++ stregsystem/views.py | 445 +++++++++++------- 3 files changed, 543 insertions(+), 232 deletions(-) create mode 100644 stregsystem/templates/stregsystem/menu_user_tickets.html diff --git a/stregsystem/models.py b/stregsystem/models.py index 1cb7fed2..1d5fc002 100644 --- a/stregsystem/models.py +++ b/stregsystem/models.py @@ -1,5 +1,6 @@ import datetime import urllib.parse +import django.enum from abc import abstractmethod from collections import Counter from email.utils import parseaddr @@ -9,10 +10,17 @@ from django.contrib.contenttypes.models import ContentType from django.core.validators import RegexValidator from django.db import models, transaction -from django.db.models import Count +from django.db.models import ( + Count, + Q, +) from django.utils import timezone -from stregsystem.caffeine import Intake, CAFFEINE_TIME_INTERVAL, current_caffeine_in_body_compound_interest +from stregsystem.caffeine import ( + Intake, + CAFFEINE_TIME_INTERVAL, + current_caffeine_in_body_compound_interest, +) from stregsystem.deprecated import deprecated from stregsystem.mail import send_payment_mail, send_welcome_mail from stregsystem.templatetags.stregsystem_extras import money @@ -122,7 +130,9 @@ def execute(self): # Check if we have enough inventory to fulfill the order for item in self.items: - if item.product.start_date is not None and (item.product.bought + item.count > item.product.quantity): + if item.product.start_date is not None and ( + item.product.bought + item.count > item.product.quantity + ): raise NoMoreInventoryError() # Take update lock on member row @@ -133,7 +143,12 @@ def execute(self): # @HACK Since we want to use the old database layout, we need to # add a sale for every item and every instance of that item for i in range(item.count): - s = Sale(member=self.member, product=item.product, room=self.room, price=item.product.price) + s = Sale( + member=self.member, + product=item.product, + room=self.room, + price=item.product.price, + ) s.save() # Bought (used above) is automatically calculated, so we don't need @@ -158,25 +173,29 @@ def get_current_year(): class Member(models.Model): # id automatisk... GENDER_CHOICES = ( - ('U', 'Unknown'), - ('M', 'Male'), - ('F', 'Female'), + ("U", "Unknown"), + ("M", "Male"), + ("F", "Female"), ) active = models.BooleanField(default=True) no_whitespace_validator = RegexValidator( # This regex checks for whitespace in the username - regex=r'^\S+$', - code='invalid_username', + regex=r"^\S+$", + code="invalid_username", ) username = models.CharField(max_length=16, validators=[no_whitespace_validator]) - year = models.CharField(max_length=4, default=get_current_year) # Put the current year as default + year = models.CharField( + max_length=4, default=get_current_year + ) # Put the current year as default firstname = models.CharField(max_length=20) # for 'firstname' lastname = models.CharField(max_length=30) # for 'lastname' gender = models.CharField(max_length=1, choices=GENDER_CHOICES) email = models.EmailField(blank=True) want_spam = models.BooleanField(default=True) # oensker vedkommende fember mails? - balance = models.IntegerField(default=0) # hvor mange oerer vedkommende har til gode + balance = models.IntegerField( + default=0 + ) # hvor mange oerer vedkommende har til gode undo_count = models.IntegerField(default=0) # for 'undos' i alt notes = models.TextField(blank=True) signup_due_paid = models.BooleanField(default=True) @@ -189,7 +208,7 @@ def balance_display(self): return money(self.balance) + " kr." balance_display.short_description = "Balance" - balance_display.admin_order_field = 'balance' + balance_display.admin_order_field = "balance" @deprecated def __unicode__(self): @@ -286,8 +305,10 @@ def calculate_alcohol_promille(self): alcohol_sales = self.sale_set.filter( timestamp__gt=calculation_start, product__alcohol_content_ml__gt=0.0 - ).order_by('timestamp') - alcohol_timeline = [(s.timestamp, s.product.alcohol_content_ml) for s in alcohol_sales] + ).order_by("timestamp") + alcohol_timeline = [ + (s.timestamp, s.product.alcohol_content_ml) for s in alcohol_sales + ] gender = Gender.UNKNOWN if self.gender == "M": @@ -315,8 +336,9 @@ def calculate_caffeine_in_body(self) -> float: [ Intake(x.timestamp, x.product.caffeine_content_mg) for x in self.sale_set.filter( - timestamp__gt=timezone.now() - CAFFEINE_TIME_INTERVAL, product__caffeine_content_mg__gt=0 - ).order_by('timestamp') + timestamp__gt=timezone.now() - CAFFEINE_TIME_INTERVAL, + product__caffeine_content_mg__gt=0, + ).order_by("timestamp") ] ) @@ -324,15 +346,19 @@ def is_leading_coffee_addict(self): coffee_category = [6] now = timezone.now() - start_of_week = now - datetime.timedelta(days=now.weekday()) - datetime.timedelta(hours=now.hour) + start_of_week = ( + now + - datetime.timedelta(days=now.weekday()) + - datetime.timedelta(hours=now.hour) + ) user_with_most_coffees_bought = ( Member.objects.filter( sale__timestamp__gt=start_of_week, sale__timestamp__lte=now, sale__product__categories__in=coffee_category, ) - .annotate(Count('sale')) - .order_by('-sale__count', 'username') + .annotate(Count("sale")) + .order_by("-sale__count", "username") .first() ) @@ -354,14 +380,16 @@ def amount_display(self): amount_display.short_description = "Amount" # XXX - django bug - kan ikke vaelge mellem desc og asc i admin, som ved normalt felt - amount_display.admin_order_field = '-amount' + amount_display.admin_order_field = "-amount" @deprecated def __unicode__(self): return self.__str__() def __str__(self): - return self.member.username + " " + str(self.timestamp) + ": " + money(self.amount) + return ( + self.member.username + " " + str(self.timestamp) + ": " + money(self.amount) + ) @transaction.atomic def save(self, mbpayment=None, *args, **kwargs): @@ -372,8 +400,12 @@ def save(self, mbpayment=None, *args, **kwargs): super(Payment, self).save(*args, **kwargs) self.member.save() if self.member.email != "" and self.amount != 0: - if '@' in parseaddr(self.member.email)[1] and self.member.want_spam: - send_payment_mail(self.member, self.amount, mbpayment.comment if mbpayment else None) + if "@" in parseaddr(self.member.email)[1] and self.member.want_spam: + send_payment_mail( + self.member, + self.amount, + mbpayment.comment if mbpayment else None, + ) def log_from_mobile_payment(self, processed_mobile_payment, admin_user: User): LogEntry.objects.log_action( @@ -382,7 +414,8 @@ def log_from_mobile_payment(self, processed_mobile_payment, admin_user: User): object_id=self.id, object_repr=str(self), action_flag=ADDITION, - change_message=f"{''}" f"MobilePayment (transaction_id: {processed_mobile_payment.transaction_id})", + change_message=f"{''}" + f"MobilePayment (transaction_id: {processed_mobile_payment.transaction_id})", ) @transaction.atomic @@ -399,16 +432,16 @@ class ApprovalModel(models.Model): class Meta: abstract = True - UNSET = 'U' - APPROVED = 'A' - IGNORED = 'I' - REJECTED = 'R' + UNSET = "U" + APPROVED = "A" + IGNORED = "I" + REJECTED = "R" STATUS_CHOICES = ( - (UNSET, 'Unset'), - (APPROVED, 'Approved'), - (IGNORED, 'Ignored'), - (REJECTED, 'Rejected'), + (UNSET, "Unset"), + (APPROVED, "Approved"), + (IGNORED, "Ignored"), + (REJECTED, "Rejected"), ) status = models.CharField(max_length=1, choices=STATUS_CHOICES, default=UNSET) @@ -508,15 +541,15 @@ def process_submitted(cls, submitted_data, admin_user: User): for row in submitted_data: # Skip rows which are set to "unset" (the default). - if row['status'] == ApprovalModel.UNSET: + if row["status"] == ApprovalModel.UNSET: continue # Skip rows which are set to "approved" without member. A Payment MUST have a Member. - if row['status'] == ApprovalModel.APPROVED and row['member'] is None: + if row["status"] == ApprovalModel.APPROVED and row["member"] is None: continue cleaned_data.append(row) # Find the id's of the remaining cleaned data. - mobile_payment_ids = [row['id'].id for row in cleaned_data] + mobile_payment_ids = [row["id"].id for row in cleaned_data] # Count how many id of the id's who are set to status "unset". database_mobile_payment_count = MobilePayment.objects.filter( id__in=mobile_payment_ids, status=ApprovalModel.UNSET @@ -526,15 +559,16 @@ def process_submitted(cls, submitted_data, admin_user: User): # get database mobilepayments matching cleaned ids and having been processed while form has been active raise PaymentToolException( MobilePayment.objects.filter( - id__in=mobile_payment_ids, status__in=(ApprovalModel.APPROVED, ApprovalModel.IGNORED) + id__in=mobile_payment_ids, + status__in=(ApprovalModel.APPROVED, ApprovalModel.IGNORED), ) ) for row in cleaned_data: - processed_mobile_payment = MobilePayment.objects.get(id=row['id'].id) + processed_mobile_payment = MobilePayment.objects.get(id=row["id"].id) # If approved, we need to create a payment and relate said payment to the mobilepayment. - processed_mobile_payment.status = row['status'] - processed_mobile_payment.member = Member.objects.get(id=row['member'].id) + processed_mobile_payment.status = row["status"] + processed_mobile_payment.member = Member.objects.get(id=row["member"].id) processed_mobile_payment.submit_processed_mobile_payment(admin_user) processed_mobile_payment.save() @@ -558,7 +592,7 @@ def __str__(self): return self.name class Meta: - verbose_name_plural = 'Categories' + verbose_name_plural = "Categories" # XXX @@ -592,15 +626,19 @@ def __unicode__(self): return self.__str__() def __str__(self): - return active_str(self.active) + " " + self.name + " (" + money(self.price) + ")" + return ( + active_str(self.active) + " " + self.name + " (" + money(self.price) + ")" + ) def save(self, *args, **kwargs): price_changed = True if self.id: try: - oldprice = self.old_prices.order_by('-changed_on')[0:1].get() + oldprice = self.old_prices.order_by("-changed_on")[0:1].get() price_changed = oldprice != self.price - except OldPrice.DoesNotExist: # der findes varer hvor der ikke er nogen "tidligere priser" + except ( + OldPrice.DoesNotExist + ): # der findes varer hvor der ikke er nogen "tidligere priser" pass super(Product, self).save(*args, **kwargs) if price_changed: @@ -612,12 +650,14 @@ def bought(self): # bought count - Jesper 27/09-2017 if self.start_date is None: return 0 - return self.sale_set.filter(timestamp__gt=date_to_midnight(self.start_date)).aggregate(bought=Count("id"))[ - "bought" - ] + return self.sale_set.filter( + timestamp__gt=date_to_midnight(self.start_date) + ).aggregate(bought=Count("id"))["bought"] def is_active(self): - expired = self.deactivate_date is not None and self.deactivate_date <= timezone.now() + expired = ( + self.deactivate_date is not None and self.deactivate_date <= timezone.now() + ) if self.start_date is not None: out_of_stock = self.quantity <= self.bought @@ -642,18 +682,33 @@ class ProductNote(models.Model): background_color = models.CharField( max_length=20, help_text="Write a valid html color (default: red)", blank="red" ) # If anyone wants to use LightGoldenRodYellow, they can - text_color = models.CharField(max_length=20, help_text="Write a valid html color (default: black)", blank="black") + text_color = models.CharField( + max_length=20, + help_text="Write a valid html color (default: black)", + blank="black", + ) start_date = models.DateField() end_date = models.DateField() comment = models.TextField(blank=True) def __str__(self): - return self.text + " (" + " | ".join(str(x.name) for x in self.products.all()) + ")" + return ( + self.text + + " (" + + " | ".join(str(x.name) for x in self.products.all()) + + ")" + ) class NamedProduct(models.Model): - name = models.CharField(max_length=50, unique=True, validators=[RegexValidator(regex=r'^[^\d:\-_][\w\-]+$')]) - product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='named_id') + name = models.CharField( + max_length=50, + unique=True, + validators=[RegexValidator(regex=r"^[^\d:\-_][\w\-]+$")], + ) + product = models.ForeignKey( + Product, on_delete=models.CASCADE, related_name="named_id" + ) def __str__(self): return self.name @@ -663,7 +718,9 @@ def map_str(self): class OldPrice(models.Model): # gamle priser, skal huskes; til regnskab/statistik? - product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='old_prices') + product = models.ForeignKey( + Product, on_delete=models.CASCADE, related_name="old_prices" + ) price = models.IntegerField() # penge, oere... changed_on = models.DateTimeField(auto_now_add=True) @@ -672,7 +729,14 @@ def __unicode__(self): return self.__str__() def __str__(self): - return self.product.name + ": " + money(self.price) + " (" + str(self.changed_on) + ")" + return ( + self.product.name + + ": " + + money(self.price) + + " (" + + str(self.changed_on) + + ")" + ) class Sale(models.Model): @@ -694,14 +758,22 @@ def price_display(self): price_display.short_description = "Price" # XXX - django bug - kan ikke vaelge mellem desc og asc i admin, som ved normalt felt - price_display.admin_order_field = 'price' + price_display.admin_order_field = "price" @deprecated def __unicode__(self): return self.__str__() def __str__(self): - return self.member.username + " " + self.product.name + " (" + money(self.price) + ") " + str(self.timestamp) + return ( + self.member.username + + " " + + self.product.name + + " (" + + money(self.price) + + ") " + + str(self.timestamp) + ) def save(self, *args, **kwargs): if self.id: @@ -742,8 +814,12 @@ class Meta: def generate_mobilepay_url(self): comment = self.member.username - query = {'phone': '90601', 'comment': comment, 'amount': "{0:.2f}".format(self.due / 100.0)} - return 'mobilepay://send?{}'.format(urllib.parse.urlencode(query)) + query = { + "phone": "90601", + "comment": comment, + "amount": "{0:.2f}".format(self.due / 100.0), + } + return "mobilepay://send?{}".format(urllib.parse.urlencode(query)) def __str__(self): return ( @@ -758,7 +834,9 @@ def complete(self, payment: MobilePayment): """ # If the user payed more than their due add it to their balance if self.due <= 0: - payment.payment = Payment.objects.create(member=self.member, amount=-self.due) + payment.payment = Payment.objects.create( + member=self.member, amount=-self.due + ) payment.save() self.member.signup_due_paid = True @@ -796,13 +874,13 @@ def process_submitted(cls, submitted_data, admin_user: User): for row in submitted_data: # Skip rows which are set to "unset" (the default). - if row['status'] == ApprovalModel.UNSET: + if row["status"] == ApprovalModel.UNSET: continue cleaned_data.append(row) # Find the id's of the remaining cleaned data. - pending_signup_ids = [row['id'].id for row in cleaned_data] + pending_signup_ids = [row["id"].id for row in cleaned_data] # Count how many id of the id's who are set to status "unset". database_approval_count = PendingSignup.objects.filter( @@ -814,21 +892,22 @@ def process_submitted(cls, submitted_data, admin_user: User): # get database mobilepayments matching cleaned ids and having been processed while form has been active raise PaymentToolException( PendingSignup.objects.filter( - id__in=pending_signup_ids, status__in=(ApprovalModel.APPROVED, ApprovalModel.IGNORED) + id__in=pending_signup_ids, + status__in=(ApprovalModel.APPROVED, ApprovalModel.IGNORED), ) ) for row in cleaned_data: - processed_signup = PendingSignup.objects.get(id=row['id'].id) + processed_signup = PendingSignup.objects.get(id=row["id"].id) - if row['status'] == ApprovalModel.APPROVED: + if row["status"] == ApprovalModel.APPROVED: processed_signup.log_approval(admin_user, "Approved") - elif row['status'] == ApprovalModel.IGNORED: + elif row["status"] == ApprovalModel.IGNORED: processed_signup.log_approval(admin_user, "Ignored") - elif row['status'] == ApprovalModel.REJECTED: + elif row["status"] == ApprovalModel.REJECTED: processed_signup.log_approval(admin_user, "Rejected") - processed_signup.status = row['status'] + processed_signup.status = row["status"] processed_signup.save() # Trigger welcome mail if sign-up is also paid. @@ -856,10 +935,73 @@ class Theme(models.Model): (SHOW, "Force show"), (HIDE, "Force hide"), ) - override = models.CharField("Override", max_length=1, choices=OVERRIDE_CHOICES, default=NONE) + override = models.CharField( + "Override", max_length=1, choices=OVERRIDE_CHOICES, default=NONE + ) class Meta: ordering = ["begin_month", "begin_day"] def __str__(self): return self.name + + +class Event(models.Model): + name = models.CharField(max_length=50) + description = models.TextField() + image = models.ImageField(upload_to="event_images/", blank=True, null=True) + + def __str__(self): + return self.name + + +class EventInstance(models.Model): + event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="instances") + name = models.CharField(max_length=50) + description = models.TextField() + image = models.ImageField(upload_to="event_instance_images/", blank=True, null=True) + capacity = models.IntegerField() + start_time = models.DateTimeField() + end_time = models.DateTimeField() + location = models.CharField(max_length=100) + + def __str__(self): + return f"{self.name} ({self.start_time} - {self.end_time})" + + def from_time_to_time_str(self): + return f"{self.start_time.strftime('%d/%m/%Y %H:%M')} - {self.end_time.strftime('%d/%m/%Y %H:%M')}" + + +class Ticket(models.Model): + event_instance = models.ForeignKey( + EventInstance, on_delete=models.CASCADE, related_name="tickets" + ) + name = models.CharField(max_length=50) + description = models.TextField() + quantity = models.IntegerField() + product = models.OneToOneField(Product, on_delete=models.CASCADE) + + def __str__(self): + return f"{self.name} for {self.event_instance.name}" + + +class TicketPurchaseState(models.TextChoices): + HELD = "held", "Held" + PURCHASED = "purchased", "Purchased" + ATTENDED = "attended", "Attended" + REFUNDED = "refunded", "Refunded" + CANCELED = "canceled", "Canceled" + + +class TicketPurchases(models.Model): + ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE) + state = models.CharField(max_length=20, choices=TicketPurchaseState.choices) + purchased_by = models.ForeignKey( + Member, on_delete=models.CASCADE, related_name="ticket_purchases" + ) + purchased_at = models.DateTimeField(auto_now_add=True) + attended = models.BooleanField(default=False) + refunded = models.BooleanField(default=False) + + def __str__(self): + return f"Ticket: {self.ticket.name}, Purchased by: {self.purchased_by.username} ({self.state})" diff --git a/stregsystem/templates/stregsystem/menu_user_tickets.html b/stregsystem/templates/stregsystem/menu_user_tickets.html new file mode 100644 index 00000000..a66af5eb --- /dev/null +++ b/stregsystem/templates/stregsystem/menu_user_tickets.html @@ -0,0 +1,54 @@ +{% extends "stregsystem/base.html" %} + +{% load stregsystem_extras %} + +{% block title %}Treoens stregsystem : User Tickets {% endblock %} + +{% block content %} + +
+

{{member.firstname}} {{member.lastname}} ({{member.email}})

+ +

Tilbage til produktmenu

+ +

Billetter relateret til dig

+ + + + + + + + + + {% autoescape off %} + {% for ticket_assignment in ticket_assignments %} + + + + + + + + {% empty %} + + + + {% endfor %} + {% endautoescape %} +
EventTicket NavnBeskrivelseKøbt afTildelt til
{{ ticket_assignment.ticket.event_instance.name }}{{ ticket_assignment.ticket.name }}{{ ticket_assignment.ticket.description }}{{ ticket_assignment.assigned_by_member }}{{ ticket_assignment.assigned_to_name }}
Ingen billetter releateret til dig.
+ + + + + +{% endblock %} + diff --git a/stregsystem/views.py b/stregsystem/views.py index 0468daca..40716262 100644 --- a/stregsystem/views.py +++ b/stregsystem/views.py @@ -18,7 +18,11 @@ from django.core import management from django.db.models import Q, Count, Sum from django.forms import modelformset_factory -from django.http import HttpResponsePermanentRedirect, HttpResponseBadRequest, JsonResponse +from django.http import ( + HttpResponsePermanentRedirect, + HttpResponseBadRequest, + JsonResponse, +) from django.shortcuts import get_object_or_404, render, redirect from django.utils import timezone from django.views.decorators.csrf import csrf_exempt @@ -43,6 +47,7 @@ NamedProduct, ApprovalModel, ProductNote, + TicketAssignment, ) from stregsystem.templatetags.stregsystem_extras import money from stregsystem.utils import ( @@ -58,7 +63,14 @@ from .booze import ballmer_peak from .caffeine import caffeine_mg_to_coffee_cups -from .forms import PaymentToolForm, QRPaymentForm, PurchaseForm, SignupForm, RankingDateForm, SignupToolForm +from .forms import ( + PaymentToolForm, + QRPaymentForm, + PurchaseForm, + SignupForm, + RankingDateForm, + SignupToolForm, +) from .management.commands.autopayment import submit_filled_mobilepayments from .purchase_heatmap import ( prepare_heatmap_template_context, @@ -68,13 +80,19 @@ def __get_news(): try: current_time = timezone.now() - return News.objects.filter(stop_date__gte=current_time, pub_date__lte=current_time).order_by('?').first() + return ( + News.objects.filter(stop_date__gte=current_time, pub_date__lte=current_time) + .order_by("?") + .first() + ) except News.DoesNotExist: return None def __get_productlist(room_id): - return make_active_productlist_query(Product.objects).filter(make_room_specific_query(room_id)) + return make_active_productlist_query(Product.objects).filter( + make_room_specific_query(room_id) + ) def __get_active_notes_for_product(product): @@ -87,7 +105,7 @@ def __get_active_notes_for_product(product): def roomindex(request): - return HttpResponsePermanentRedirect('/1/') + return HttpResponsePermanentRedirect("/1/") # room_list = Room.objects.all().order_by('name', 'description') @@ -96,64 +114,68 @@ def roomindex(request): def index(request, room_id): room = get_object_or_404(Room, pk=int(room_id)) - ProductNotePair = namedtuple('ProductNotePair', 'product note') + ProductNotePair = namedtuple("ProductNotePair", "product note") product_note_pair_list = [ - ProductNotePair(product, __get_active_notes_for_product(product)) for product in __get_productlist(room_id) + ProductNotePair(product, __get_active_notes_for_product(product)) + for product in __get_productlist(room_id) ] news = __get_news() - return render(request, 'stregsystem/index.html', locals()) + return render(request, "stregsystem/index.html", locals()) def _pre_process(buy_string): - items = buy_string.split(' ') + items = buy_string.split(" ") _items = [items[0]] for item in items[1:]: if type(item) is not int: - _item = NamedProduct.objects.filter(name=item.split(':')[0].lower() if ':' in item else item) + _item = NamedProduct.objects.filter( + name=item.split(":")[0].lower() if ":" in item else item + ) if _item: - item = item.replace(item.split(':')[0], str(_item.get().product.pk)) + item = item.replace(item.split(":")[0], str(_item.get().product.pk)) _items.append(str(item)) - return ' '.join(_items) + return " ".join(_items) def sale(request, room_id): room = get_object_or_404(Room, pk=room_id) news = __get_news() product_list = __get_productlist(room_id) - ProductNotePair = namedtuple('ProductNotePair', 'product note') + ProductNotePair = namedtuple("ProductNotePair", "product note") product_note_pair_list = [ - ProductNotePair(product, __get_active_notes_for_product(product)) for product in __get_productlist(room_id) + ProductNotePair(product, __get_active_notes_for_product(product)) + for product in __get_productlist(room_id) ] - buy_string = request.POST['quickbuy'].strip() + buy_string = request.POST["quickbuy"].strip() # Handle empty line if buy_string == "": - return render(request, 'stregsystem/index.html', locals()) + return render(request, "stregsystem/index.html", locals()) # Extract username and product ids try: username, bought_ids = parser.parse(_pre_process(buy_string)) except parser.QuickBuyError as err: values = { - 'correct': err.parsed_part, - 'incorrect': err.failed_part, - 'error_ptr': '~' * (len(err.parsed_part)) + '^', - 'error_msg': ' ' * (len(err.parsed_part) - 4) + 'Fejl her', - 'room': room, + "correct": err.parsed_part, + "incorrect": err.failed_part, + "error_ptr": "~" * (len(err.parsed_part)) + "^", + "error_msg": " " * (len(err.parsed_part) - 4) + "Fejl her", + "room": room, } - return render(request, 'stregsystem/error_invalidquickbuy.html', values) + return render(request, "stregsystem/error_invalidquickbuy.html", values) # Fetch member from DB try: member = Member.objects.get(username__iexact=username, active=True) except Member.DoesNotExist: - return render(request, 'stregsystem/error_usernotfound.html', locals()) + return render(request, "stregsystem/error_usernotfound.html", locals()) if not member.signup_due_paid: - return render(request, 'stregsystem/error_signupdue.html', locals()) + return render(request, "stregsystem/error_signupdue.html", locals()) if not member.signup_approved(): - return render(request, 'stregsystem/error_signup_not_approved.html', locals()) + return render(request, "stregsystem/error_signup_not_approved.html", locals()) if len(bought_ids): return quicksale(request, room, member, bought_ids) @@ -165,8 +187,12 @@ def _multibuy_hint(now, member): # Get a timestamp to fetch sales for the member for the last 60 sec earliest_recent_purchase = now - datetime.timedelta(seconds=60) # get the sales with this timestamp - recent_purchases = Sale.objects.filter(member=member, timestamp__gt=earliest_recent_purchase) - number_of_recent_distinct_purchases = recent_purchases.values('timestamp').distinct().count() + recent_purchases = Sale.objects.filter( + member=member, timestamp__gt=earliest_recent_purchase + ) + number_of_recent_distinct_purchases = ( + recent_purchases.values("timestamp").distinct().count() + ) # add hint for multibuy if number_of_recent_distinct_purchases > 1: @@ -176,7 +202,7 @@ def _multibuy_hint(now, member): sale_dict[str(sale.product.id)] = 1 else: sale_dict[str(sale.product.id)] = sale_dict[str(sale.product.id)] + 1 - sale_hints = ["{}".format(member.username)] + sale_hints = ['{}'.format(member.username)] if all(sale_count == 1 for sale_count in sale_dict.values()): return (False, None) for key in sale_dict: @@ -184,7 +210,7 @@ def _multibuy_hint(now, member): sale_hints.append("{}:{}".format(key, sale_dict[key])) else: sale_hints.append(key) - return (True, ' '.join(sale_hints)) + return (True, " ".join(sale_hints)) return (False, None) @@ -192,25 +218,34 @@ def _multibuy_hint(now, member): def quicksale(request, room, member: Member, bought_ids): news = __get_news() product_list = __get_productlist(room.id) - ProductNotePair = namedtuple('ProductNotePair', 'product note') + ProductNotePair = namedtuple("ProductNotePair", "product note") product_note_pair_list = [ - ProductNotePair(product, __get_active_notes_for_product(product)) for product in __get_productlist(room.id) + ProductNotePair(product, __get_active_notes_for_product(product)) + for product in __get_productlist(room.id) ] now = timezone.now() # Retrieve products and construct transaction products: List[Product] = [] - msg, status, result = __append_bought_ids_to_product_list(products, bought_ids, now, room) + msg, status, result = __append_bought_ids_to_product_list( + products, bought_ids, now, room + ) if status == 400: - return render(request, 'stregsystem/error_productdoesntexist.html', {'failedProduct': result, 'room': room}) + return render( + request, + "stregsystem/error_productdoesntexist.html", + {"failedProduct": result, "room": room}, + ) order = Order.from_products(member=member, products=products, room=room) msg, status, result = __execute_order(order) - if 'Out of stock' in msg: - return render(request, 'stregsystem/error_stregforbud.html', locals()) - elif 'Stregforbud' in msg: - return render(request, 'stregsystem/error_stregforbud.html', locals(), status=402) + if "Out of stock" in msg: + return render(request, "stregsystem/error_stregforbud.html", locals()) + elif "Stregforbud" in msg: + return render( + request, "stregsystem/error_stregforbud.html", locals(), status=402 + ) ( promille, @@ -230,15 +265,16 @@ def quicksale(request, room, member: Member, bought_ids): products = Counter([str(product.name) for product in products]).most_common() - return render(request, 'stregsystem/index_sale.html', locals()) + return render(request, "stregsystem/index_sale.html", locals()) def usermenu(request, room, member, bought, from_sale=False): negative_balance = member.balance < 0 product_list = __get_productlist(room.id) - ProductNotePair = namedtuple('ProductNotePair', 'product note') + ProductNotePair = namedtuple("ProductNotePair", "product note") product_note_pair_list = [ - ProductNotePair(product, __get_active_notes_for_product(product)) for product in __get_productlist(room.id) + ProductNotePair(product, __get_active_notes_for_product(product)) + for product in __get_productlist(room.id) ] news = __get_news() promille = member.calculate_alcohol_promille() @@ -255,12 +291,14 @@ def usermenu(request, room, member, bought, from_sale=False): give_multibuy_hint, sale_hints = _multibuy_hint(timezone.now(), member) give_multibuy_hint = give_multibuy_hint and from_sale - heatmap_context = prepare_heatmap_template_context(member, 12, datetime.date.today()) + heatmap_context = prepare_heatmap_template_context( + member, 12, datetime.date.today() + ) if member.has_stregforbud(): - return render(request, 'stregsystem/error_stregforbud.html', locals()) + return render(request, "stregsystem/error_stregforbud.html", locals()) else: - return render(request, 'stregsystem/menu.html', {**locals(), **heatmap_context}) + return render(request, "stregsystem/menu.html", {**locals(), **heatmap_context}) def menu_userinfo(request, room_id, member_id): @@ -269,25 +307,25 @@ def menu_userinfo(request, room_id, member_id): member = Member.objects.get(pk=member_id, active=True) if not member.signup_due_paid: - return render(request, 'stregsystem/error_signupdue.html', locals()) + return render(request, "stregsystem/error_signupdue.html", locals()) if not member.signup_approved(): - return render(request, 'stregsystem/error_signup_not_approved.html', locals()) + return render(request, "stregsystem/error_signup_not_approved.html", locals()) stats = Sale.objects.filter(member_id=member_id).aggregate( - total_amount=Sum('price'), total_purchases=Count('timestamp') + total_amount=Sum("price"), total_purchases=Count("timestamp") ) - last_sale_list = member.sale_set.order_by('-timestamp')[:10] + last_sale_list = member.sale_set.order_by("-timestamp")[:10] try: - last_payment = member.payment_set.order_by('-timestamp')[0] + last_payment = member.payment_set.order_by("-timestamp")[0] except IndexError: last_payment = None negative_balance = member.balance < 0 stregforbud = member.has_stregforbud() - return render(request, 'stregsystem/menu_userinfo.html', locals()) + return render(request, "stregsystem/menu_userinfo.html", locals()) def send_userdata(request, room_id, member_id): @@ -297,10 +335,10 @@ def send_userdata(request, room_id, member_id): member = Member.objects.get(pk=member_id, active=True) if not member.signup_due_paid: - return render(request, 'stregsystem/error_signupdue.html', locals()) + return render(request, "stregsystem/error_signupdue.html", locals()) if not member.signup_approved(): - return render(request, 'stregsystem/error_signup_not_approved.html', locals()) + return render(request, "stregsystem/error_signup_not_approved.html", locals()) mail_sent = send_userdata_mail(member) sent_time = data_sent[member.id] @@ -316,15 +354,15 @@ def menu_userpay(request, room_id, member_id): member = Member.objects.get(pk=member_id, active=True) if not member.signup_due_paid: - return render(request, 'stregsystem/error_signupdue.html', locals()) + return render(request, "stregsystem/error_signupdue.html", locals()) if not member.signup_approved(): - return render(request, 'stregsystem/error_signup_not_approved.html', locals()) + return render(request, "stregsystem/error_signup_not_approved.html", locals()) amounts = {100, 200} try: - last_payment = member.payment_set.order_by('-timestamp')[0] + last_payment = member.payment_set.order_by("-timestamp")[0] amounts.add(last_payment.amount / 100.0) except IndexError: last_payment = None @@ -335,7 +373,7 @@ def menu_userpay(request, room_id, member_id): amounts = sorted(amounts) - return render(request, 'stregsystem/menu_userpay.html', locals()) + return render(request, "stregsystem/menu_userpay.html", locals()) def menu_userrank(request, room_id, member_id): @@ -345,16 +383,20 @@ def menu_userrank(request, room_id, member_id): member = Member.objects.get(pk=member_id, active=True) if not member.signup_due_paid: - return render(request, 'stregsystem/error_signupdue.html', locals()) + return render(request, "stregsystem/error_signupdue.html", locals()) if not member.signup_approved(): - return render(request, 'stregsystem/error_signup_not_approved.html', locals()) + return render(request, "stregsystem/error_signup_not_approved.html", locals()) def ranking(category_ids, from_d, to_d): qs = ( - Member.objects.filter(sale__product__in=category_ids, sale__timestamp__gt=from_d, sale__timestamp__lte=to_d) - .annotate(Count('sale')) - .order_by('-sale__count', 'username') + Member.objects.filter( + sale__product__in=category_ids, + sale__timestamp__gt=from_d, + sale__timestamp__lte=to_d, + ) + .annotate(Count("sale")) + .order_by("-sale__count", "username") ) if member not in qs: return 0, qs.count() @@ -362,9 +404,9 @@ def ranking(category_ids, from_d, to_d): def get_product_ids_for_category(category) -> list: return list( - Product.objects.filter(categories__exact=Category.objects.get(name__exact=category)).values_list( - 'id', flat=True - ) + Product.objects.filter( + categories__exact=Category.objects.get(name__exact=category) + ).values_list("id", flat=True) ) def category_per_uni_day(category_ids, from_d, to_d): @@ -377,7 +419,9 @@ def category_per_uni_day(category_ids, from_d, to_d): if member not in qs: return 0 else: - return "{:.2f}".format(qs.count() / ((to_d - from_d).days * 162.14 / 365)) # university workdays in 2021 + return "{:.2f}".format( + qs.count() / ((to_d - from_d).days * 162.14 / 365) + ) # university workdays in 2021 def sale_count_for_product(category_ids, from_d, to_d): qs = Sale.objects.filter( @@ -390,19 +434,19 @@ def sale_count_for_product(category_ids, from_d, to_d): # let user know when they first purchased a product member_first_purchase = "Ikke endnu, køb en limfjordsporter!" - first_purchase = Sale.objects.filter(member=member_id).order_by('-timestamp') + first_purchase = Sale.objects.filter(member=member_id).order_by("-timestamp") if first_purchase.exists(): member_first_purchase = first_purchase.last().timestamp form = RankingDateForm() - if request.method == "POST" and request.POST['custom-range']: + if request.method == "POST" and request.POST["custom-range"]: form = RankingDateForm(request.POST) if form.is_valid(): - from_date = form.cleaned_data['from_date'] - to_date = form.cleaned_data['to_date'] + datetime.timedelta(days=1) + from_date = form.cleaned_data["from_date"] + to_date = form.cleaned_data["to_date"] + datetime.timedelta(days=1) else: # setup initial dates for form and results - form = RankingDateForm(initial={'from_date': from_date, 'to_date': to_date}) + form = RankingDateForm(initial={"from_date": from_date, "to_date": to_date}) # get prod_ids for each category as dict {cat: [key1, key2])}, then flatten list of singleton # dicts into one dict, lastly calculate member_id rating and units/weekday for category_ids @@ -414,12 +458,26 @@ def sale_count_for_product(category_ids, from_d, to_d): ) for key, category_ids in { k: v - for x in map(lambda x: {x: get_product_ids_for_category(x)}, list(Category.objects.all())) + for x in map( + lambda x: {x: get_product_ids_for_category(x)}, + list(Category.objects.all()), + ) for k, v in x.items() }.items() } - return render(request, 'stregsystem/menu_userrank.html', locals()) + return render(request, "stregsystem/menu_userrank.html", locals()) + + +def menu_user_tickets(request, room_id, member_id): + room = Room.objects.get(pk=room_id) + member = Member.objects.get(pk=member_id, active=True) + + ticket_assignments = TicketAssignment.get_related_assignments_for_member( + member + ).order_by("-assigned_at") + + return render(request, "stregsystem/menu_user_tickets.html", locals()) def menu_sale(request, room_id, member_id, product_id=None): @@ -428,13 +486,13 @@ def menu_sale(request, room_id, member_id, product_id=None): member = Member.objects.get(pk=member_id, active=True) if not member.signup_due_paid: - return render(request, 'stregsystem/error_signupdue.html', locals()) + return render(request, "stregsystem/error_signupdue.html", locals()) if not member.signup_approved(): - return render(request, 'stregsystem/error_signup_not_approved.html', locals()) + return render(request, "stregsystem/error_signup_not_approved.html", locals()) product = None - if request.method == 'POST': + if request.method == "POST": purchase = PurchaseForm(request.POST) if not purchase.is_valid(): return HttpResponseBadRequest( @@ -443,10 +501,11 @@ def menu_sale(request, room_id, member_id, product_id=None): try: product = Product.objects.get( - Q(pk=purchase.cleaned_data['product_id']), + Q(pk=purchase.cleaned_data["product_id"]), Q(active=True), Q(rooms__id=room_id) | Q(rooms=None), - Q(deactivate_date__gte=timezone.now()) | Q(deactivate_date__isnull=True), + Q(deactivate_date__gte=timezone.now()) + | Q(deactivate_date__isnull=True), ) order = Order.from_products(member=member, room=room, products=(product,)) @@ -456,10 +515,10 @@ def menu_sale(request, room_id, member_id, product_id=None): except Product.DoesNotExist: pass except StregForbudError: - return render(request, 'stregsystem/error_stregforbud.html', locals()) + return render(request, "stregsystem/error_stregforbud.html", locals()) except NoMoreInventoryError: # @INCOMPLETE this should render with a different template - return render(request, 'stregsystem/error_stregforbud.html', locals()) + return render(request, "stregsystem/error_stregforbud.html", locals()) # Refresh member, to get new amount member = Member.objects.get(pk=member_id, active=True) @@ -470,7 +529,9 @@ def menu_sale(request, room_id, member_id, product_id=None): @permission_required("stregsystem.import_batch_payments") def batch_payment(request): PaymentFormSet = forms.modelformset_factory( - Payment, fields=("member", "amount"), widgets={"member": forms.Select(attrs={"class": "select2"})} + Payment, + fields=("member", "amount"), + widgets={"member": forms.Select(attrs={"class": "select2"})}, ) if request.method == "POST": formset = PaymentFormSet(request.POST, request.FILES) @@ -511,42 +572,55 @@ def batch_payment(request): ) -def approval_tool_context(request, approval_formset_factory, approval_queryset, approval_model: Type[ApprovalModel]): +def approval_tool_context( + request, + approval_formset_factory, + approval_queryset, + approval_model: Type[ApprovalModel], +): data = dict() if request.method == "GET": - data['formset'] = approval_formset_factory(queryset=approval_queryset) - elif request.method == "POST" and request.POST['action'] == "Submit pre-matched entries": + data["formset"] = approval_formset_factory(queryset=approval_queryset) + elif ( + request.method == "POST" + and request.POST["action"] == "Submit pre-matched entries" + ): count = submit_filled_mobilepayments(request.user) - data['submitted_count'] = count - data['formset'] = approval_formset_factory(queryset=approval_queryset) - elif request.method == "POST" and request.POST['action'] == "Submit": + data["submitted_count"] = count + data["formset"] = approval_formset_factory(queryset=approval_queryset) + elif request.method == "POST" and request.POST["action"] == "Submit": form = approval_formset_factory(request.POST) if form.is_valid(): try: # Do custom validation on form to avoid race conditions with autopayment - count = approval_model.process_submitted(form.cleaned_data, request.user) - data['submitted_count'] = count + count = approval_model.process_submitted( + form.cleaned_data, request.user + ) + data["submitted_count"] = count except PaymentToolException as e: - data['error_count'] = e.inconsistent_mbpayments_count - data['error_transaction_ids'] = e.inconsistent_transaction_ids + data["error_count"] = e.inconsistent_mbpayments_count + data["error_transaction_ids"] = e.inconsistent_transaction_ids # refresh form after submission - data['formset'] = approval_formset_factory(queryset=approval_queryset) + data["formset"] = approval_formset_factory(queryset=approval_queryset) else: # update form with errors - data['formset'] = form - elif request.method == "POST" and request.POST['action'] == "Import via MobilePay API": + data["formset"] = form + elif ( + request.method == "POST" + and request.POST["action"] == "Import via MobilePay API" + ): before_count = MobilePayment.objects.count() - management.call_command('importmobilepaypayments') + management.call_command("importmobilepaypayments") count = MobilePayment.objects.count() - before_count - data['api'] = f"Successfully imported {count} MobilePay transactions" - data['formset'] = approval_formset_factory(queryset=approval_queryset) + data["api"] = f"Successfully imported {count} MobilePay transactions" + data["formset"] = approval_formset_factory(queryset=approval_queryset) else: - data['formset'] = approval_formset_factory(queryset=approval_queryset) + data["formset"] = approval_formset_factory(queryset=approval_queryset) return data @@ -554,23 +628,33 @@ def approval_tool_context(request, approval_formset_factory, approval_queryset, @staff_member_required() @permission_required("stregsystem.mobilepaytool_access") def payment_tool(request): - paytool_form_set = modelformset_factory(MobilePayment, form=PaymentToolForm, extra=0) + paytool_form_set = modelformset_factory( + MobilePayment, form=PaymentToolForm, extra=0 + ) - data = approval_tool_context(request, paytool_form_set, make_unprocessed_mobilepayment_query(), MobilePayment) + data = approval_tool_context( + request, paytool_form_set, make_unprocessed_mobilepayment_query(), MobilePayment + ) if bool(data): pass - elif request.method == "POST" and 'csv_file' in request.FILES and request.POST['action'] == "Import MobilePay CSV": + elif ( + request.method == "POST" + and "csv_file" in request.FILES + and request.POST["action"] == "Import MobilePay CSV" + ): # Prepare uploaded CSV to be read - csv_file = request.FILES['csv_file'] + csv_file = request.FILES["csv_file"] csv_file.seek(0) - data['imports'], data['duplicates'] = parse_csv_and_create_mobile_payments( - str(csv_file.read().decode('utf-8')).splitlines() + data["imports"], data["duplicates"] = parse_csv_and_create_mobile_payments( + str(csv_file.read().decode("utf-8")).splitlines() ) # refresh form after submission - data['formset'] = paytool_form_set(queryset=make_unprocessed_mobilepayment_query()) + data["formset"] = paytool_form_set( + queryset=make_unprocessed_mobilepayment_query() + ) return render(request, "admin/stregsystem/approval_tools/payment_tool.html", data) @@ -578,16 +662,23 @@ def payment_tool(request): @staff_member_required() @permission_required("stregsystem.signuptool_access") def signup_tool(request): - signuptool_form_set = modelformset_factory(PendingSignup, form=SignupToolForm, extra=0) + signuptool_form_set = modelformset_factory( + PendingSignup, form=SignupToolForm, extra=0 + ) - data = approval_tool_context(request, signuptool_form_set, make_unprocessed_signups_query(), PendingSignup) + data = approval_tool_context( + request, signuptool_form_set, make_unprocessed_signups_query(), PendingSignup + ) if bool(data): pass - elif request.method == "POST" and request.POST['action'] == "Process transactions for sign-ups": + elif ( + request.method == "POST" + and request.POST["action"] == "Process transactions for sign-ups" + ): # TODO: Make changes here # management.call_command('autosignup') - data['formset'] = signuptool_form_set(queryset=make_unprocessed_signups_query()) + data["formset"] = signuptool_form_set(queryset=make_unprocessed_signups_query()) return render(request, "admin/stregsystem/approval_tools/signup_tool.html", data) @@ -600,8 +691,8 @@ def get_payment_qr(request): if not form.is_valid(): return HttpResponseBadRequest("Invalid input for MobilePay QR code generation") - username = form.cleaned_data.get('username') - amount = form.cleaned_data.get('amount') + username = form.cleaned_data.get("username") + amount = form.cleaned_data.get("amount") return qr_code(mobilepay_launch_uri(username, amount)) @@ -611,23 +702,28 @@ def signup(request): form = SignupForm(request.POST) if is_post else SignupForm() if is_post and form.is_valid(): - if Member.objects.filter(username=form.cleaned_data.get('username')).all().count() > 0: + if ( + Member.objects.filter(username=form.cleaned_data.get("username")) + .all() + .count() + > 0 + ): form.add_error("username", "Brugernavn allerede i brug") return render(request, "stregsystem/signup.html", locals()) member = Member.objects.create( - username=form.cleaned_data.get('username'), - firstname=form.cleaned_data.get('firstname'), - lastname=form.cleaned_data.get('lastname'), - email=form.cleaned_data.get('email'), - notes=form.cleaned_data.get('notes'), - gender=form.cleaned_data.get('gender'), + username=form.cleaned_data.get("username"), + firstname=form.cleaned_data.get("firstname"), + lastname=form.cleaned_data.get("lastname"), + email=form.cleaned_data.get("email"), + notes=form.cleaned_data.get("notes"), + gender=form.cleaned_data.get("gender"), signup_due_paid=False, ) signup_request = PendingSignup(member=member, due=200 * 100) signup_request.save() - return redirect('signup_status', signup_id=signup_request.id) + return redirect("signup_status", signup_id=signup_request.id) return render(request, "stregsystem/signup.html", locals()) @@ -636,21 +732,21 @@ def signup_status(request, signup_id): try: pending_signup = PendingSignup.objects.get(pk=signup_id) except PendingSignup.DoesNotExist: - return redirect('signup') + return redirect("signup") mobilepay_url = pending_signup.generate_mobilepay_url() qr = io.BytesIO() qrcode.make(mobilepay_url, image_factory=qrcode.image.svg.SvgPathFillImage).save(qr) - mobilepay_qr_svg = qr.getvalue().decode('utf-8').splitlines()[1] + mobilepay_qr_svg = qr.getvalue().decode("utf-8").splitlines()[1] qr.close() return render(request, "stregsystem/signup_status.html", locals()) def get_active_items(request): - room_id = request.GET.get('room_id') or None + room_id = request.GET.get("room_id") or None if room_id is None: return HttpResponseBadRequest("Parameter missing: room_id") @@ -664,12 +760,12 @@ def get_active_items(request): # TODO: Check whether room exists items = __get_productlist(room_id) - items_dict = {item.id: {'name': item.name, 'price': item.price} for item in items} - return JsonResponse(items_dict, json_dumps_params={'ensure_ascii': False}) + items_dict = {item.id: {"name": item.name, "price": item.price} for item in items} + return JsonResponse(items_dict, json_dumps_params={"ensure_ascii": False}) def get_member_active(request): - member_id = request.GET.get('member_id') or None + member_id = request.GET.get("member_id") or None if member_id is None: return HttpResponseBadRequest("Parameter missing: member_id") elif not member_id.isdigit(): @@ -680,11 +776,11 @@ def get_member_active(request): except Member.DoesNotExist: return HttpResponseBadRequest("Member not found") - return JsonResponse({'active': member.active}) + return JsonResponse({"active": member.active}) def get_member_id(request): - username = request.GET.get('username') or None + username = request.GET.get("username") or None if username is None: return HttpResponseBadRequest("Parameter missing: username") @@ -693,20 +789,23 @@ def get_member_id(request): except Member.DoesNotExist: return HttpResponseBadRequest("Member not found") - return JsonResponse({'member_id': member.id}) + return JsonResponse({"member_id": member.id}) def get_product_category_mappings(request): return JsonResponse( { - p.id: [{'category_id': cat.id, 'category_name': cat.name} for cat in p.categories.all()] + p.id: [ + {"category_id": cat.id, "category_name": cat.name} + for cat in p.categories.all() + ] for p in Product.objects.all() } ) def get_member_sales(request): - member_id = request.GET.get('member_id') or None + member_id = request.GET.get("member_id") or None if member_id is None: return HttpResponseBadRequest("Parameter missing: member_id") elif not member_id.isdigit(): @@ -717,15 +816,26 @@ def get_member_sales(request): except Member.DoesNotExist: return HttpResponseBadRequest("Member not found") - count = 10 if request.GET.get('count') is None else int(request.GET.get('count') or 10) - sales = Sale.objects.filter(member=member).order_by('-timestamp')[:count] + count = ( + 10 if request.GET.get("count") is None else int(request.GET.get("count") or 10) + ) + sales = Sale.objects.filter(member=member).order_by("-timestamp")[:count] return JsonResponse( - {'sales': [{'timestamp': s.timestamp, 'product': s.product.name, 'price': s.product.price} for s in sales]} + { + "sales": [ + { + "timestamp": s.timestamp, + "product": s.product.name, + "price": s.product.price, + } + for s in sales + ] + } ) def get_member_balance(request): - member_id = request.GET.get('member_id') or None + member_id = request.GET.get("member_id") or None if member_id is None: return HttpResponseBadRequest("Parameter missing: member_id") elif not member_id.isdigit(): @@ -736,11 +846,11 @@ def get_member_balance(request): except Member.DoesNotExist: return HttpResponseBadRequest("Member not found") - return JsonResponse({'balance': member.balance}) + return JsonResponse({"balance": member.balance}) def get_member_info(request): - member_id = str(request.GET.get('member_id')) or None + member_id = str(request.GET.get("member_id")) or None if member_id is None: return HttpResponseBadRequest("Parameter missing: member_id") elif not member_id.isdigit(): @@ -753,11 +863,11 @@ def get_member_info(request): return JsonResponse( { - 'balance': member.balance, - 'username': member.username, - 'active': member.active, - 'name': f'{member.firstname} {member.lastname}', - 'signup_due_paid': member.signup_due_paid, + "balance": member.balance, + "username": member.username, + "active": member.active, + "name": f"{member.firstname} {member.lastname}", + "signup_due_paid": member.signup_due_paid, } ) @@ -772,7 +882,7 @@ def find_user_from_id(user_id: int): def get_named_products(request): items = NamedProduct.objects.all() items_dict = {item.name: item.product.id for item in items} - return JsonResponse(items_dict, json_dumps_params={'ensure_ascii': False}) + return JsonResponse(items_dict, json_dumps_params={"ensure_ascii": False}) @csrf_exempt @@ -781,9 +891,9 @@ def api_sale(request): return HttpResponseBadRequest() else: data = json.loads(request.body) - buy_string = str(data['buystring']).strip() - room = str(data['room']) or None - member_id = str(data['member_id']) or None + buy_string = str(data["buystring"]).strip() + room = str(data["room"]) or None + member_id = str(data["member_id"]) or None if room is None: return HttpResponseBadRequest("Parameter missing: room") @@ -816,7 +926,7 @@ def api_sale(request): return HttpResponseBadRequest("Username does not match member_id") if not buy_string.startswith(member.username): - buy_string = f'{member.username} {buy_string}' + buy_string = f"{member.username} {buy_string}" try: room = Room.objects.get(pk=room) @@ -824,7 +934,8 @@ def api_sale(request): return HttpResponseBadRequest("Parameter invalid: room") msg, status, ret_obj = api_quicksale(request, room, member, bought_ids) return JsonResponse( - {'status': status, 'msg': msg, 'values': ret_obj}, json_dumps_params={'ensure_ascii': False} + {"status": status, "msg": msg, "values": ret_obj}, + json_dumps_params={"ensure_ascii": False}, ) @@ -834,7 +945,9 @@ def api_quicksale(request, room, member: Member, bought_ids): # Retrieve products and construct transaction products: List[Product] = [] - msg, status, result = __append_bought_ids_to_product_list(products, bought_ids, now, room) + msg, status, result = __append_bought_ids_to_product_list( + products, bought_ids, now, room + ) if status == 400: return msg, status, result @@ -864,25 +977,25 @@ def api_quicksale(request, room, member: Member, bought_ids): "OK", 200 if len(bought_ids) > 0 else 201, { - 'order': { - 'room': order.room.id, - 'member': order.member.id, - 'created_on': order.created_on, - 'items': bought_ids, + "order": { + "room": order.room.id, + "member": order.member.id, + "created_on": order.created_on, + "items": bought_ids, }, - 'promille': promille, - 'is_ballmer_peaking': is_ballmer_peaking, - 'bp_minutes': bp_minutes, - 'bp_seconds': bp_seconds, - 'caffeine': caffeine, - 'cups': cups, - 'product_contains_caffeine': product_contains_caffeine, - 'is_coffee_master': is_coffee_master, - 'cost': cost(), - 'give_multibuy_hint': give_multibuy_hint, - 'sale_hints': sale_hints, - 'member_has_low_balance': member_has_low_balance, - 'member_balance': member_balance, + "promille": promille, + "is_ballmer_peaking": is_ballmer_peaking, + "bp_minutes": bp_minutes, + "bp_seconds": bp_seconds, + "caffeine": caffeine, + "cups": cups, + "product_contains_caffeine": product_contains_caffeine, + "is_coffee_master": is_coffee_master, + "cost": cost(), + "give_multibuy_hint": give_multibuy_hint, + "sale_hints": sale_hints, + "member_has_low_balance": member_has_low_balance, + "member_balance": member_balance, }, ) @@ -919,7 +1032,9 @@ def __set_local_values(member, room, products, order, now): caffeine = member.calculate_caffeine_in_body() cups = caffeine_mg_to_coffee_cups(caffeine) - product_contains_caffeine = any(product.caffeine_content_mg > 0 for product in products) + product_contains_caffeine = any( + product.caffeine_content_mg > 0 for product in products + ) is_coffee_master = member.is_leading_coffee_addict() cost = order.total From b55de77892c8b833993bee88f34e4c8010696965 Mon Sep 17 00:00:00 2001 From: ThomasHoy Date: Fri, 21 Nov 2025 16:11:56 +0100 Subject: [PATCH 02/11] Change models, make migration, add url --- ...nt_eventinstance_ticket_ticketpurchases.py | 136 ++++++++++++++++++ stregsystem/models.py | 34 +++-- .../stregsystem/menu_user_tickets.html | 2 +- stregsystem/urls.py | 1 + stregsystem/views.py | 8 +- 5 files changed, 162 insertions(+), 19 deletions(-) create mode 100644 stregsystem/migrations/0023_event_eventinstance_ticket_ticketpurchases.py diff --git a/stregsystem/migrations/0023_event_eventinstance_ticket_ticketpurchases.py b/stregsystem/migrations/0023_event_eventinstance_ticket_ticketpurchases.py new file mode 100644 index 00000000..40f7bedf --- /dev/null +++ b/stregsystem/migrations/0023_event_eventinstance_ticket_ticketpurchases.py @@ -0,0 +1,136 @@ +# Generated by Django 4.1.13 on 2025-11-21 15:04 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("stregsystem", "0022_productnote"), + ] + + operations = [ + migrations.CreateModel( + name="Event", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=50)), + ("description", models.TextField()), + ( + "image", + models.ImageField(blank=True, null=True, upload_to="event_images/"), + ), + ], + ), + migrations.CreateModel( + name="EventInstance", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=50)), + ("description", models.TextField()), + ( + "image", + models.ImageField( + blank=True, null=True, upload_to="event_instance_images/" + ), + ), + ("capacity", models.IntegerField()), + ("start_time", models.DateTimeField()), + ("end_time", models.DateTimeField()), + ("location", models.CharField(max_length=100)), + ( + "event", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="instances", + to="stregsystem.event", + ), + ), + ], + ), + migrations.CreateModel( + name="Ticket", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=50)), + ("description", models.TextField()), + ("quantity", models.IntegerField()), + ( + "event_instance", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tickets", + to="stregsystem.eventinstance", + ), + ), + ( + "product", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tickets", + to="stregsystem.product", + ), + ), + ], + ), + migrations.CreateModel( + name="TicketPurchases", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("purchased_at", models.DateTimeField(auto_now_add=True)), + ("attended", models.BooleanField(default=False)), + ("refunded", models.BooleanField(default=False)), + ("refunded_at", models.DateTimeField(blank=True, null=True)), + ("on_stand_by", models.BooleanField(default=False)), + ( + "purchased_by", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="ticket_purchases", + to="stregsystem.member", + ), + ), + ( + "ticket", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="purchases", + to="stregsystem.ticket", + ), + ), + ], + ), + ] diff --git a/stregsystem/models.py b/stregsystem/models.py index 1d5fc002..ea277aa3 100644 --- a/stregsystem/models.py +++ b/stregsystem/models.py @@ -1,6 +1,5 @@ import datetime import urllib.parse -import django.enum from abc import abstractmethod from collections import Counter from email.utils import parseaddr @@ -968,7 +967,7 @@ class EventInstance(models.Model): def __str__(self): return f"{self.name} ({self.start_time} - {self.end_time})" - def from_time_to_time_str(self): + def from_start_to_end_time_str(self): return f"{self.start_time.strftime('%d/%m/%Y %H:%M')} - {self.end_time.strftime('%d/%m/%Y %H:%M')}" @@ -979,29 +978,36 @@ class Ticket(models.Model): name = models.CharField(max_length=50) description = models.TextField() quantity = models.IntegerField() - product = models.OneToOneField(Product, on_delete=models.CASCADE) + product = models.ForeignKey( + Product, on_delete=models.CASCADE, related_name="tickets" + ) def __str__(self): return f"{self.name} for {self.event_instance.name}" -class TicketPurchaseState(models.TextChoices): - HELD = "held", "Held" - PURCHASED = "purchased", "Purchased" - ATTENDED = "attended", "Attended" - REFUNDED = "refunded", "Refunded" - CANCELED = "canceled", "Canceled" - - class TicketPurchases(models.Model): - ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE) - state = models.CharField(max_length=20, choices=TicketPurchaseState.choices) + ticket = models.ForeignKey( + Ticket, on_delete=models.CASCADE, related_name="purchases" + ) purchased_by = models.ForeignKey( Member, on_delete=models.CASCADE, related_name="ticket_purchases" ) purchased_at = models.DateTimeField(auto_now_add=True) + attended = models.BooleanField(default=False) + refunded = models.BooleanField(default=False) + refunded_at = models.DateTimeField(null=True, blank=True) + + on_stand_by = models.BooleanField(default=False) + + @staticmethod + def get_member_purchases(member: Member): + return TicketPurchases.objects.filter(purchased_by=member) def __str__(self): - return f"Ticket: {self.ticket.name}, Purchased by: {self.purchased_by.username} ({self.state})" + str = f"Ticket: {self.ticket.name}, Purchased by: {self.purchased_by.username}, Refunded: {self.refunded}" + if not self.refunded and self.on_stand_by: + str += " (On stand-by)" + return str diff --git a/stregsystem/templates/stregsystem/menu_user_tickets.html b/stregsystem/templates/stregsystem/menu_user_tickets.html index a66af5eb..38b4ead1 100644 --- a/stregsystem/templates/stregsystem/menu_user_tickets.html +++ b/stregsystem/templates/stregsystem/menu_user_tickets.html @@ -22,7 +22,7 @@

Billetter relateret til dig

Tildelt til {% autoescape off %} - {% for ticket_assignment in ticket_assignments %} + {% for ticket_assignment in ticket_purchases %} {{ ticket_assignment.ticket.event_instance.name }} {{ ticket_assignment.ticket.name }} diff --git a/stregsystem/urls.py b/stregsystem/urls.py index 9fbbe9bb..035a3fae 100644 --- a/stregsystem/urls.py +++ b/stregsystem/urls.py @@ -32,6 +32,7 @@ re_path(r'^(?P\d+)/user/(?P\d+)/$', views.menu_userinfo, name="userinfo"), re_path(r'^(?P\d+)/user/(?P\d+)/pay$', views.menu_userpay, name="userpay"), re_path(r'^(?P\d+)/user/(?P\d+)/rank$', views.menu_userrank, name="userrank"), + re_path(r'^(?P\d+)/user/(?P\d+)/tickets$', views.menu_user_tickets, name="user_tickets"), re_path(r'^(?P\d+)/send_csv_mail/(?P\d+)/$', views.send_userdata, name="send_userdata"), re_path(r'^api/member/payment/qr$', views.get_payment_qr, name="api_payment_qr"), re_path(r'^api/member/active$', views.get_member_active, name="api_member_active"), diff --git a/stregsystem/views.py b/stregsystem/views.py index 40716262..ba83a527 100644 --- a/stregsystem/views.py +++ b/stregsystem/views.py @@ -47,7 +47,7 @@ NamedProduct, ApprovalModel, ProductNote, - TicketAssignment, + TicketPurchases, ) from stregsystem.templatetags.stregsystem_extras import money from stregsystem.utils import ( @@ -473,9 +473,9 @@ def menu_user_tickets(request, room_id, member_id): room = Room.objects.get(pk=room_id) member = Member.objects.get(pk=member_id, active=True) - ticket_assignments = TicketAssignment.get_related_assignments_for_member( - member - ).order_by("-assigned_at") + ticket_purchases = TicketPurchases.get_member_purchases(member).order_by( + "-purchased_at" + ) return render(request, "stregsystem/menu_user_tickets.html", locals()) From 04616b4d81514050c15e5d82b3946a79195433d3 Mon Sep 17 00:00:00 2001 From: ThomasHoy Date: Fri, 21 Nov 2025 18:13:10 +0100 Subject: [PATCH 03/11] changes to the models, ticket site and admin site --- stregsystem/admin.py | 20 +++++ stregsystem/models.py | 77 +++++++++++++++++++ .../stregsystem/menu_user_tickets.html | 41 ++++++---- stregsystem/views.py | 12 +++ 4 files changed, 134 insertions(+), 16 deletions(-) diff --git a/stregsystem/admin.py b/stregsystem/admin.py index 8b4620d7..6b5ce293 100644 --- a/stregsystem/admin.py +++ b/stregsystem/admin.py @@ -18,6 +18,10 @@ PendingSignup, Theme, ProductNote, + Event, + EventInstance, + Ticket, + TicketPurchases, ) from stregsystem.templatetags.stregsystem_extras import money from stregsystem.utils import make_active_productlist_query, make_inactive_productlist_query @@ -372,6 +376,18 @@ class ProductNoteAdmin(admin.ModelAdmin): actions = [toggle_active_selected_products] +class EventAdmin(admin.ModelAdmin): + pass + +class EventInstanceAdmin(admin.ModelAdmin): + pass + +class TicketAdmin(admin.ModelAdmin): + pass + +class TicketPurchasesAdmin(admin.ModelAdmin): + pass + admin.site.register(LogEntry, LogEntryAdmin) admin.site.register(Sale, SaleAdmin) @@ -386,3 +402,7 @@ class ProductNoteAdmin(admin.ModelAdmin): admin.site.register(PendingSignup) admin.site.register(Theme, ThemeAdmin) admin.site.register(ProductNote, ProductNoteAdmin) +admin.site.register(Event, EventAdmin) +admin.site.register(EventInstance, EventInstanceAdmin) +admin.site.register(Ticket, TicketAdmin) +admin.site.register(TicketPurchases, TicketPurchasesAdmin) diff --git a/stregsystem/models.py b/stregsystem/models.py index 42275ee4..f338a914 100644 --- a/stregsystem/models.py +++ b/stregsystem/models.py @@ -864,3 +864,80 @@ class Meta: def __str__(self): return self.name + +class Event(models.Model): + name = models.CharField(max_length=50) + description = models.TextField() + image = models.ImageField(upload_to="event_images/", blank=True, null=True) + + def __str__(self): + return self.name + + +class EventInstance(models.Model): + event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="instances", null=False, blank=False) + name_overwrite = models.CharField(max_length=50, blank=True) + description_overwrite = models.TextField(blank=True) + image_overwrite = models.ImageField(upload_to="event_instance_images/", blank=True, null=True) + capacity = models.IntegerField(null=False, blank=False) + start_time = models.DateTimeField(null=False, blank=False) + end_time = models.DateTimeField(null=False, blank=False) + location = models.CharField(max_length=100, null=False, blank=False) + + def __str__(self): + return f"{self.name_overwrite} ({self.start_time} - {self.end_time})" + + def from_start_to_end_time_str(self): + return f"{self.start_time.strftime('%d/%m/%Y %H:%M')} - {self.end_time.strftime('%d/%m/%Y %H:%M')}" + + +class Ticket(models.Model): + event_instance = models.ForeignKey( + EventInstance, on_delete=models.CASCADE, related_name="tickets" + ) + name = models.CharField(max_length=50) + description = models.TextField() + quantity = models.IntegerField() + product = models.ForeignKey( + Product, on_delete=models.CASCADE, related_name="tickets" + ) + + def __str__(self): + return f"{self.name} for {self.event_instance.name_overwrite}" + + +class TicketPurchases(models.Model): + ticket = models.ForeignKey( + Ticket, on_delete=models.CASCADE, related_name="purchases" + ) + purchased_by = models.ForeignKey( + Member, on_delete=models.CASCADE, related_name="ticket_purchases" + ) + purchased_at = models.DateTimeField(auto_now_add=True) + + attended = models.BooleanField(default=False) + + refunded = models.BooleanField(default=False) + refunded_at = models.DateTimeField(null=True, blank=True) + + on_stand_by = models.BooleanField(default=False) + + @staticmethod + def get_member_purchases(member: Member): + return TicketPurchases.objects.filter(purchased_by=member) + + @staticmethod + def get_bool_pretty(value: bool) -> str: + return "Ja" if value else "Nej" + + def get_refunded_pretty(self) -> str: + return self.get_bool_pretty(self.refunded) + + def get_stand_by_pretty(self) -> str: + return self.get_bool_pretty(self.on_stand_by) + + def __str__(self): + str = f"Billet: {self.ticket.name}, Købt af: {self.purchased_by.username}, Refunderet: {self.refunded}" + if not self.refunded and self.on_stand_by: + str += " (På stand-by)" + return str \ No newline at end of file diff --git a/stregsystem/templates/stregsystem/menu_user_tickets.html b/stregsystem/templates/stregsystem/menu_user_tickets.html index 38b4ead1..87b5fb3b 100644 --- a/stregsystem/templates/stregsystem/menu_user_tickets.html +++ b/stregsystem/templates/stregsystem/menu_user_tickets.html @@ -16,22 +16,22 @@

Billetter relateret til dig

- + - - + + {% autoescape off %} - {% for ticket_assignment in ticket_purchases %} - - - - - - + {% for ticket_purchase in purchase_page %} + + + + + + {% empty %} - + {% endfor %} @@ -39,13 +39,22 @@

Billetter relateret til dig

EventTicket NavnBillet Navn BeskrivelseKøbt afTildelt tilPå Stand ByRefunderet
{{ ticket_assignment.ticket.event_instance.name }}{{ ticket_assignment.ticket.name }}{{ ticket_assignment.ticket.description }}{{ ticket_assignment.assigned_by_member }}{{ ticket_assignment.assigned_to_name }}
{{ ticket_purchase.ticket.event_instance.name }}{{ ticket_purchase.ticket.name }}{{ ticket_purchase.ticket.description }}{{ ticket_purchase.get_stand_by_pretty }}{{ ticket_purchase.get_refunded_pretty }}
Ingen billetter releateret til dig.
diff --git a/stregsystem/views.py b/stregsystem/views.py index ebac6898..33ba3dd6 100644 --- a/stregsystem/views.py +++ b/stregsystem/views.py @@ -44,6 +44,7 @@ NamedProduct, ApprovalModel, ProductNote, + TicketPurchases, ) from stregsystem.templatetags.stregsystem_extras import money from stregsystem.utils import ( @@ -424,6 +425,17 @@ def sale_count_for_product(category_ids, from_d, to_d): return render(request, 'stregsystem/menu_userrank.html', locals()) +def menu_user_tickets(request, room_id, member_id): + room = Room.objects.get(pk=room_id) + member = Member.objects.get(pk=member_id, active=True) + + all_ticket_purchases_current_member = TicketPurchases.get_member_purchases(member).order_by("-purchased_at") + purchase_paginator = Paginator(all_ticket_purchases_current_member, 5) + purchase_page_number = request.GET.get('purchase_table_index', 1) + purchase_page = purchase_paginator.get_page(purchase_page_number) + + return render(request, "stregsystem/menu_user_tickets.html", locals()) + def menu_sale(request, room_id, member_id, product_id=None): room = Room.objects.get(pk=room_id) From 71435dade0a38407bd30a985c87201279fba14f4 Mon Sep 17 00:00:00 2001 From: ThomasHoy Date: Fri, 21 Nov 2025 18:20:25 +0100 Subject: [PATCH 04/11] new migration --- .../0023_event_eventinstance_ticket_ticketpurchases.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stregsystem/migrations/0023_event_eventinstance_ticket_ticketpurchases.py b/stregsystem/migrations/0023_event_eventinstance_ticket_ticketpurchases.py index 40f7bedf..5195e6d9 100644 --- a/stregsystem/migrations/0023_event_eventinstance_ticket_ticketpurchases.py +++ b/stregsystem/migrations/0023_event_eventinstance_ticket_ticketpurchases.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.13 on 2025-11-21 15:04 +# Generated by Django 4.1.13 on 2025-11-21 17:19 from django.db import migrations, models import django.db.models.deletion @@ -43,10 +43,10 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("name", models.CharField(max_length=50)), - ("description", models.TextField()), + ("name_overwrite", models.CharField(blank=True, max_length=50)), + ("description_overwrite", models.TextField(blank=True)), ( - "image", + "image_overwrite", models.ImageField( blank=True, null=True, upload_to="event_instance_images/" ), From ec6880a7cb4b53ddf269bdb2e041284e483b8901 Mon Sep 17 00:00:00 2001 From: ThomasHoy Date: Fri, 21 Nov 2025 21:01:43 +0100 Subject: [PATCH 05/11] model changes --- stregsystem/models.py | 35 ++++++++++++++--------------------- stregsystem/utils.py | 3 +++ 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/stregsystem/models.py b/stregsystem/models.py index f338a914..90ef0487 100644 --- a/stregsystem/models.py +++ b/stregsystem/models.py @@ -21,6 +21,7 @@ make_processed_mobilepayment_query, make_unprocessed_member_filled_mobilepayment_query, PaymentToolException, + get_bool_pretty, ) @@ -888,7 +889,7 @@ def __str__(self): return f"{self.name_overwrite} ({self.start_time} - {self.end_time})" def from_start_to_end_time_str(self): - return f"{self.start_time.strftime('%d/%m/%Y %H:%M')} - {self.end_time.strftime('%d/%m/%Y %H:%M')}" + return f"Fra {self.start_time.strftime('%d/%m/%Y %H:%M')} - til {self.end_time.strftime('%d/%m/%Y %H:%M')}" class Ticket(models.Model): @@ -906,7 +907,14 @@ def __str__(self): return f"{self.name} for {self.event_instance.name_overwrite}" -class TicketPurchases(models.Model): +class TicketPurchaseStatus(models.TextChoices): + ISSUED = "ISSUED", "Issued" + ADMIN_ISSUED = "ADMIN_ISSUED", "Issued by Admin" + STAND_BY = "STAND_BY", "On Stand-by" + REFUNDED = "REFUNDED", "Refunded" + + +class TicketRecord(models.Model): ticket = models.ForeignKey( Ticket, on_delete=models.CASCADE, related_name="purchases" ) @@ -914,30 +922,15 @@ class TicketPurchases(models.Model): Member, on_delete=models.CASCADE, related_name="ticket_purchases" ) purchased_at = models.DateTimeField(auto_now_add=True) + status = models.CharField(max_length=20, choices=TicketPurchaseStatus.choices, default=TicketPurchaseStatus.ISSUED) attended = models.BooleanField(default=False) - refunded = models.BooleanField(default=False) - refunded_at = models.DateTimeField(null=True, blank=True) - - on_stand_by = models.BooleanField(default=False) + refunded_at = models.DateTimeField(blank=True) @staticmethod def get_member_purchases(member: Member): - return TicketPurchases.objects.filter(purchased_by=member) - - @staticmethod - def get_bool_pretty(value: bool) -> str: - return "Ja" if value else "Nej" - - def get_refunded_pretty(self) -> str: - return self.get_bool_pretty(self.refunded) - - def get_stand_by_pretty(self) -> str: - return self.get_bool_pretty(self.on_stand_by) + return TicketRecord.objects.filter(purchased_by=member) def __str__(self): - str = f"Billet: {self.ticket.name}, Købt af: {self.purchased_by.username}, Refunderet: {self.refunded}" - if not self.refunded and self.on_stand_by: - str += " (På stand-by)" - return str \ No newline at end of file + return f"{self.purchased_by.username}'s billet: {self.ticket.name}, Status: {self.status}" \ No newline at end of file diff --git a/stregsystem/utils.py b/stregsystem/utils.py index baafca0e..fbf51fd4 100644 --- a/stregsystem/utils.py +++ b/stregsystem/utils.py @@ -210,3 +210,6 @@ def rows_to_csv(rows) -> str: # Converting elements in rows to strings to ensure it can be written to the file object csv.writer(file).writerows([[str(item) for item in row] for row in rows]) return file.data + +def get_bool_pretty(value: bool) -> str: + return "Ja" if value else "Nej" \ No newline at end of file From 6e2f8146bac43d95aa10828b57d803c14d0e30b8 Mon Sep 17 00:00:00 2001 From: ThomasHoy Date: Fri, 21 Nov 2025 21:17:26 +0100 Subject: [PATCH 06/11] fix --- stregsystem/admin.py | 6 +++--- stregsystem/models.py | 2 +- stregsystem/views.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/stregsystem/admin.py b/stregsystem/admin.py index 6b5ce293..52951a7b 100644 --- a/stregsystem/admin.py +++ b/stregsystem/admin.py @@ -21,7 +21,7 @@ Event, EventInstance, Ticket, - TicketPurchases, + TicketRecord, ) from stregsystem.templatetags.stregsystem_extras import money from stregsystem.utils import make_active_productlist_query, make_inactive_productlist_query @@ -385,7 +385,7 @@ class EventInstanceAdmin(admin.ModelAdmin): class TicketAdmin(admin.ModelAdmin): pass -class TicketPurchasesAdmin(admin.ModelAdmin): +class TicketRecordAdmin(admin.ModelAdmin): pass @@ -405,4 +405,4 @@ class TicketPurchasesAdmin(admin.ModelAdmin): admin.site.register(Event, EventAdmin) admin.site.register(EventInstance, EventInstanceAdmin) admin.site.register(Ticket, TicketAdmin) -admin.site.register(TicketPurchases, TicketPurchasesAdmin) +admin.site.register(TicketRecord, TicketRecordAdmin) diff --git a/stregsystem/models.py b/stregsystem/models.py index 90ef0487..06806c21 100644 --- a/stregsystem/models.py +++ b/stregsystem/models.py @@ -919,7 +919,7 @@ class TicketRecord(models.Model): Ticket, on_delete=models.CASCADE, related_name="purchases" ) purchased_by = models.ForeignKey( - Member, on_delete=models.CASCADE, related_name="ticket_purchases" + Member, on_delete=models.CASCADE, related_name="tickets" ) purchased_at = models.DateTimeField(auto_now_add=True) status = models.CharField(max_length=20, choices=TicketPurchaseStatus.choices, default=TicketPurchaseStatus.ISSUED) diff --git a/stregsystem/views.py b/stregsystem/views.py index 33ba3dd6..d7d7200c 100644 --- a/stregsystem/views.py +++ b/stregsystem/views.py @@ -44,7 +44,7 @@ NamedProduct, ApprovalModel, ProductNote, - TicketPurchases, + TicketRecord, ) from stregsystem.templatetags.stregsystem_extras import money from stregsystem.utils import ( @@ -429,7 +429,7 @@ def menu_user_tickets(request, room_id, member_id): room = Room.objects.get(pk=room_id) member = Member.objects.get(pk=member_id, active=True) - all_ticket_purchases_current_member = TicketPurchases.get_member_purchases(member).order_by("-purchased_at") + all_ticket_purchases_current_member = TicketRecord.get_member_purchases(member).order_by("-purchased_at") purchase_paginator = Paginator(all_ticket_purchases_current_member, 5) purchase_page_number = request.GET.get('purchase_table_index', 1) purchase_page = purchase_paginator.get_page(purchase_page_number) From 9749fdcfe7d25a1116fd85fe8ff841ce66cc53a5 Mon Sep 17 00:00:00 2001 From: ThomasHoy Date: Tue, 25 Nov 2025 17:39:12 +0100 Subject: [PATCH 07/11] new migration --- ...event_eventinstance_ticket_ticketrecord.py | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 stregsystem/migrations/0023_event_eventinstance_ticket_ticketrecord.py diff --git a/stregsystem/migrations/0023_event_eventinstance_ticket_ticketrecord.py b/stregsystem/migrations/0023_event_eventinstance_ticket_ticketrecord.py new file mode 100644 index 00000000..2652d0c9 --- /dev/null +++ b/stregsystem/migrations/0023_event_eventinstance_ticket_ticketrecord.py @@ -0,0 +1,147 @@ +# Generated by Django 4.1.13 on 2025-11-25 16:37 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("stregsystem", "0022_productnote"), + ] + + operations = [ + migrations.CreateModel( + name="Event", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=50)), + ("description", models.TextField()), + ( + "image", + models.ImageField(blank=True, null=True, upload_to="event_images/"), + ), + ], + ), + migrations.CreateModel( + name="EventInstance", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name_overwrite", models.CharField(blank=True, max_length=50)), + ("description_overwrite", models.TextField(blank=True)), + ( + "image_overwrite", + models.ImageField( + blank=True, null=True, upload_to="event_instance_images/" + ), + ), + ("capacity", models.IntegerField()), + ("start_time", models.DateTimeField()), + ("end_time", models.DateTimeField()), + ("location", models.CharField(max_length=100)), + ( + "event", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="instances", + to="stregsystem.event", + ), + ), + ], + ), + migrations.CreateModel( + name="Ticket", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=50)), + ("description", models.TextField()), + ("quantity", models.IntegerField()), + ( + "event_instance", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tickets", + to="stregsystem.eventinstance", + ), + ), + ( + "product", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tickets", + to="stregsystem.product", + ), + ), + ], + ), + migrations.CreateModel( + name="TicketRecord", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("purchased_at", models.DateTimeField(auto_now_add=True)), + ( + "status", + models.CharField( + choices=[ + ("ISSUED", "Issued"), + ("ADMIN_ISSUED", "Issued by Admin"), + ("STAND_BY", "On Stand-by"), + ("REFUNDED", "Refunded"), + ], + default="ISSUED", + max_length=20, + ), + ), + ("attended", models.BooleanField(default=False)), + ("refunded_at", models.DateTimeField(blank=True)), + ( + "purchased_by", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tickets", + to="stregsystem.member", + ), + ), + ( + "ticket", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="purchases", + to="stregsystem.ticket", + ), + ), + ], + ), + ] From 9d89a4df584e117b16dfcbe313399ed129e63481 Mon Sep 17 00:00:00 2001 From: ThomasHoy Date: Tue, 25 Nov 2025 17:43:32 +0100 Subject: [PATCH 08/11] forgot to remove --- ...nt_eventinstance_ticket_ticketpurchases.py | 136 ------------------ 1 file changed, 136 deletions(-) delete mode 100644 stregsystem/migrations/0023_event_eventinstance_ticket_ticketpurchases.py diff --git a/stregsystem/migrations/0023_event_eventinstance_ticket_ticketpurchases.py b/stregsystem/migrations/0023_event_eventinstance_ticket_ticketpurchases.py deleted file mode 100644 index 5195e6d9..00000000 --- a/stregsystem/migrations/0023_event_eventinstance_ticket_ticketpurchases.py +++ /dev/null @@ -1,136 +0,0 @@ -# Generated by Django 4.1.13 on 2025-11-21 17:19 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("stregsystem", "0022_productnote"), - ] - - operations = [ - migrations.CreateModel( - name="Event", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=50)), - ("description", models.TextField()), - ( - "image", - models.ImageField(blank=True, null=True, upload_to="event_images/"), - ), - ], - ), - migrations.CreateModel( - name="EventInstance", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name_overwrite", models.CharField(blank=True, max_length=50)), - ("description_overwrite", models.TextField(blank=True)), - ( - "image_overwrite", - models.ImageField( - blank=True, null=True, upload_to="event_instance_images/" - ), - ), - ("capacity", models.IntegerField()), - ("start_time", models.DateTimeField()), - ("end_time", models.DateTimeField()), - ("location", models.CharField(max_length=100)), - ( - "event", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="instances", - to="stregsystem.event", - ), - ), - ], - ), - migrations.CreateModel( - name="Ticket", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=50)), - ("description", models.TextField()), - ("quantity", models.IntegerField()), - ( - "event_instance", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="tickets", - to="stregsystem.eventinstance", - ), - ), - ( - "product", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="tickets", - to="stregsystem.product", - ), - ), - ], - ), - migrations.CreateModel( - name="TicketPurchases", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("purchased_at", models.DateTimeField(auto_now_add=True)), - ("attended", models.BooleanField(default=False)), - ("refunded", models.BooleanField(default=False)), - ("refunded_at", models.DateTimeField(blank=True, null=True)), - ("on_stand_by", models.BooleanField(default=False)), - ( - "purchased_by", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="ticket_purchases", - to="stregsystem.member", - ), - ), - ( - "ticket", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="purchases", - to="stregsystem.ticket", - ), - ), - ], - ), - ] From 4d5a42bdd115457438c6d88d37bf583aea725306 Mon Sep 17 00:00:00 2001 From: Kresten Laust Date: Thu, 8 Jan 2026 11:39:12 +0100 Subject: [PATCH 09/11] Black --- stregsystem/admin.py | 4 ++++ stregsystem/models.py | 21 +++++++-------------- stregsystem/utils.py | 3 ++- stregsystem/views.py | 1 + 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/stregsystem/admin.py b/stregsystem/admin.py index 52951a7b..990da7ea 100644 --- a/stregsystem/admin.py +++ b/stregsystem/admin.py @@ -376,15 +376,19 @@ class ProductNoteAdmin(admin.ModelAdmin): actions = [toggle_active_selected_products] + class EventAdmin(admin.ModelAdmin): pass + class EventInstanceAdmin(admin.ModelAdmin): pass + class TicketAdmin(admin.ModelAdmin): pass + class TicketRecordAdmin(admin.ModelAdmin): pass diff --git a/stregsystem/models.py b/stregsystem/models.py index 06806c21..3c66b8bd 100644 --- a/stregsystem/models.py +++ b/stregsystem/models.py @@ -866,6 +866,7 @@ class Meta: def __str__(self): return self.name + class Event(models.Model): name = models.CharField(max_length=50) description = models.TextField() @@ -893,15 +894,11 @@ def from_start_to_end_time_str(self): class Ticket(models.Model): - event_instance = models.ForeignKey( - EventInstance, on_delete=models.CASCADE, related_name="tickets" - ) + event_instance = models.ForeignKey(EventInstance, on_delete=models.CASCADE, related_name="tickets") name = models.CharField(max_length=50) description = models.TextField() quantity = models.IntegerField() - product = models.ForeignKey( - Product, on_delete=models.CASCADE, related_name="tickets" - ) + product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="tickets") def __str__(self): return f"{self.name} for {self.event_instance.name_overwrite}" @@ -915,12 +912,8 @@ class TicketPurchaseStatus(models.TextChoices): class TicketRecord(models.Model): - ticket = models.ForeignKey( - Ticket, on_delete=models.CASCADE, related_name="purchases" - ) - purchased_by = models.ForeignKey( - Member, on_delete=models.CASCADE, related_name="tickets" - ) + ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name="purchases") + purchased_by = models.ForeignKey(Member, on_delete=models.CASCADE, related_name="tickets") purchased_at = models.DateTimeField(auto_now_add=True) status = models.CharField(max_length=20, choices=TicketPurchaseStatus.choices, default=TicketPurchaseStatus.ISSUED) @@ -930,7 +923,7 @@ class TicketRecord(models.Model): @staticmethod def get_member_purchases(member: Member): - return TicketRecord.objects.filter(purchased_by=member) + return TicketRecord.objects.filter(purchased_by=member) def __str__(self): - return f"{self.purchased_by.username}'s billet: {self.ticket.name}, Status: {self.status}" \ No newline at end of file + return f"{self.purchased_by.username}'s billet: {self.ticket.name}, Status: {self.status}" diff --git a/stregsystem/utils.py b/stregsystem/utils.py index fbf51fd4..0dd96596 100644 --- a/stregsystem/utils.py +++ b/stregsystem/utils.py @@ -211,5 +211,6 @@ def rows_to_csv(rows) -> str: csv.writer(file).writerows([[str(item) for item in row] for row in rows]) return file.data + def get_bool_pretty(value: bool) -> str: - return "Ja" if value else "Nej" \ No newline at end of file + return "Ja" if value else "Nej" diff --git a/stregsystem/views.py b/stregsystem/views.py index d7d7200c..5db6719b 100644 --- a/stregsystem/views.py +++ b/stregsystem/views.py @@ -425,6 +425,7 @@ def sale_count_for_product(category_ids, from_d, to_d): return render(request, 'stregsystem/menu_userrank.html', locals()) + def menu_user_tickets(request, room_id, member_id): room = Room.objects.get(pk=room_id) member = Member.objects.get(pk=member_id, active=True) From 5e4a3c90233f0bad4714d68f8d0a626905868aba Mon Sep 17 00:00:00 2001 From: ThomasBow Date: Sun, 11 Jan 2026 00:47:10 +0100 Subject: [PATCH 10/11] new ticket record model relations --- db.sqlite3.bak | Bin 0 -> 372736 bytes ...event_eventinstance_ticket_ticketrecord.py | 35 ++++++++++--- stregsystem/models.py | 47 +++++++++++++++++- stregsystem/templates/stregsystem/menu.html | 2 + stregsystem/views.py | 2 +- 5 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 db.sqlite3.bak diff --git a/db.sqlite3.bak b/db.sqlite3.bak new file mode 100644 index 0000000000000000000000000000000000000000..5d91ee383540b43af89e6956a0e50108e173980f GIT binary patch literal 372736 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 46b9fe7 (new ticket record model relations) + + @staticmethod + def is_product_a_ticket(product: Product) -> tuple[bool, Optional[Ticket]]: + ticket = Ticket.objects.filter(product=product) + if ticket.exists(): + return True, ticket.first() + else: + return False, None def __str__(self): return f"{self.name} for {self.event_instance.name_overwrite}" @@ -912,18 +934,41 @@ class TicketPurchaseStatus(models.TextChoices): class TicketRecord(models.Model): +<<<<<<< HEAD ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name="purchases") purchased_by = models.ForeignKey(Member, on_delete=models.CASCADE, related_name="tickets") purchased_at = models.DateTimeField(auto_now_add=True) +======= + ticket = models.ForeignKey( + Ticket, on_delete=models.CASCADE, related_name="purchases" + ) + sale = models.OneToOneField( + Sale, on_delete=models.CASCADE, related_name="ticket_record" + ) +>>>>>>> 46b9fe7 (new ticket record model relations) status = models.CharField(max_length=20, choices=TicketPurchaseStatus.choices, default=TicketPurchaseStatus.ISSUED) - attended = models.BooleanField(default=False) + attended = models.BooleanField(null=True, blank=True) refunded_at = models.DateTimeField(blank=True) + refunded_by_admin = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="admin_refunded_tickets", null=True, blank=True + ) + + issued_by_admin = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="admin_issued_tickets", null=True, blank=True + ) @staticmethod def get_member_purchases(member: Member): +<<<<<<< HEAD return TicketRecord.objects.filter(purchased_by=member) def __str__(self): return f"{self.purchased_by.username}'s billet: {self.ticket.name}, Status: {self.status}" +======= + return TicketRecord.objects.filter(sale__member=member) + + def __str__(self): + return f"{self.sale.member.username}'s billet: {self.ticket.name}, Status: {self.status}" +>>>>>>> 46b9fe7 (new ticket record model relations) diff --git a/stregsystem/templates/stregsystem/menu.html b/stregsystem/templates/stregsystem/menu.html index 809f1986..986079b8 100644 --- a/stregsystem/templates/stregsystem/menu.html +++ b/stregsystem/templates/stregsystem/menu.html @@ -35,6 +35,8 @@

Du Bruger Info Indsæt penge Rangliste + Billetter + {% comment %} Fortryd køb {% endcomment %} diff --git a/stregsystem/views.py b/stregsystem/views.py index 5db6719b..5417c7a6 100644 --- a/stregsystem/views.py +++ b/stregsystem/views.py @@ -430,7 +430,7 @@ def menu_user_tickets(request, room_id, member_id): room = Room.objects.get(pk=room_id) member = Member.objects.get(pk=member_id, active=True) - all_ticket_purchases_current_member = TicketRecord.get_member_purchases(member).order_by("-purchased_at") + all_ticket_purchases_current_member = TicketRecord.get_member_purchases(member).order_by("sale__timestamp") purchase_paginator = Paginator(all_ticket_purchases_current_member, 5) purchase_page_number = request.GET.get('purchase_table_index', 1) purchase_page = purchase_paginator.get_page(purchase_page_number) From 222071e6c563a30f22b35a2d15dbb844d30fd163 Mon Sep 17 00:00:00 2001 From: ThomasBow Date: Sun, 11 Jan 2026 00:58:49 +0100 Subject: [PATCH 11/11] black and missing from previous --- stregsystem/models.py | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/stregsystem/models.py b/stregsystem/models.py index c024456e..be9951af 100644 --- a/stregsystem/models.py +++ b/stregsystem/models.py @@ -906,13 +906,7 @@ class Ticket(models.Model): name = models.CharField(max_length=50) description = models.TextField() quantity = models.IntegerField() -<<<<<<< HEAD - product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="tickets") -======= - product = models.OneToOneField( - Product, on_delete=models.CASCADE, related_name="tickets" - ) ->>>>>>> 46b9fe7 (new ticket record model relations) + product = models.OneToOneField(Product, on_delete=models.CASCADE, related_name="tickets") @staticmethod def is_product_a_ticket(product: Product) -> tuple[bool, Optional[Ticket]]: @@ -934,18 +928,8 @@ class TicketPurchaseStatus(models.TextChoices): class TicketRecord(models.Model): -<<<<<<< HEAD ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name="purchases") - purchased_by = models.ForeignKey(Member, on_delete=models.CASCADE, related_name="tickets") - purchased_at = models.DateTimeField(auto_now_add=True) -======= - ticket = models.ForeignKey( - Ticket, on_delete=models.CASCADE, related_name="purchases" - ) - sale = models.OneToOneField( - Sale, on_delete=models.CASCADE, related_name="ticket_record" - ) ->>>>>>> 46b9fe7 (new ticket record model relations) + sale = models.OneToOneField(Sale, on_delete=models.CASCADE, related_name="ticket_record") status = models.CharField(max_length=20, choices=TicketPurchaseStatus.choices, default=TicketPurchaseStatus.ISSUED) attended = models.BooleanField(null=True, blank=True) @@ -961,14 +945,7 @@ class TicketRecord(models.Model): @staticmethod def get_member_purchases(member: Member): -<<<<<<< HEAD - return TicketRecord.objects.filter(purchased_by=member) - - def __str__(self): - return f"{self.purchased_by.username}'s billet: {self.ticket.name}, Status: {self.status}" -======= return TicketRecord.objects.filter(sale__member=member) def __str__(self): return f"{self.sale.member.username}'s billet: {self.ticket.name}, Status: {self.status}" ->>>>>>> 46b9fe7 (new ticket record model relations)