Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added db.sqlite3.bak
Binary file not shown.
24 changes: 24 additions & 0 deletions stregsystem/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
PendingSignup,
Theme,
ProductNote,
Event,
EventInstance,
Ticket,
TicketRecord,
)
from stregsystem.templatetags.stregsystem_extras import money
from stregsystem.utils import (
Expand Down Expand Up @@ -376,6 +380,22 @@ 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


admin.site.register(LogEntry, LogEntryAdmin)
admin.site.register(Sale, SaleAdmin)
admin.site.register(Member, MemberAdmin)
Expand All @@ -389,3 +409,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(TicketRecord, TicketRecordAdmin)
168 changes: 168 additions & 0 deletions stregsystem/migrations/0023_event_eventinstance_ticket_ticketrecord.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Generated by Django 4.1.13 on 2026-01-10 23:03

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("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.OneToOneField(
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",
),
),
(
"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(blank=True, null=True)),
("refunded_at", models.DateTimeField(blank=True)),
(
"issued_by_admin",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="admin_issued_tickets",
to=settings.AUTH_USER_MODEL,
),
),
(
"refunded_by_admin",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="admin_refunded_tickets",
to=settings.AUTH_USER_MODEL,
),
),
(
"sale",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="ticket_record",
to="stregsystem.sale",
),
),
(
"ticket",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="purchases",
to="stregsystem.ticket",
),
),
],
),
]
85 changes: 85 additions & 0 deletions stregsystem/models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations
import datetime
import urllib.parse
from abc import abstractmethod
from collections import Counter
from email.utils import parseaddr
from typing import Optional

from django.contrib.admin.models import LogEntry, ADDITION, CHANGE
from django.contrib.auth.models import User
Expand All @@ -21,6 +23,7 @@
make_processed_mobilepayment_query,
make_unprocessed_member_filled_mobilepayment_query,
PaymentToolException,
get_bool_pretty,
)


Expand Down Expand Up @@ -707,6 +710,12 @@ def __str__(self):
def save(self, *args, **kwargs):
if self.id:
raise RuntimeError("Updates of sales are not allowed")

is_ticket, ticket = Ticket.is_product_a_ticket(self.product)
if is_ticket:
# Create a ticket record for this sale
TicketRecord.objects.create(ticket=ticket, sale=self)

super(Sale, self).save(*args, **kwargs)

def delete(self, *args, **kwargs):
Expand Down Expand Up @@ -864,3 +873,79 @@ 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"Fra {self.start_time.strftime('%d/%m/%Y %H:%M')} - til {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, related_name="tickets")

@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}"


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")
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)

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):
return TicketRecord.objects.filter(sale__member=member)

def __str__(self):
return f"{self.sale.member.username}'s billet: {self.ticket.name}, Status: {self.status}"
2 changes: 2 additions & 0 deletions stregsystem/templates/stregsystem/menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ <h4>Du
<a href="/{{room.id}}/user/{{member.id}}">Bruger Info</a>
<a href="/{{room.id}}/user/{{member.id}}/pay">Indsæt penge</a>
<a href="/{{room.id}}/user/{{member.id}}/rank">Rangliste</a>
<a href="/{{room.id}}/user/{{member.id}}/tickets">Billetter</a>

{% comment %}
<a href="/{{room.id}}/user/{{member.id}}/undo">Fortryd køb</a>
{% endcomment %}
Expand Down
63 changes: 63 additions & 0 deletions stregsystem/templates/stregsystem/menu_user_tickets.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{% extends "stregsystem/base.html" %}

{% load stregsystem_extras %}

{% block title %}Treoens stregsystem : User Tickets {% endblock %}

{% block content %}

<main class="center">
<h3>{{member.firstname}} {{member.lastname}} ({{member.email}})</h3>

<h2><a href="/{{room.id}}/sale/{{member.id}}">Tilbage til produktmenu</a></h2>

<h4>Billetter relateret til dig</h4>

<table class="default">
<tr>
<th>Event</th>
<th>Billet Navn</th>
<th>Beskrivelse</th>
<th>På Stand By</th>
<th>Refunderet</th>
</tr>
{% autoescape off %}
{% for ticket_purchase in purchase_page %}
<tr style="text-align:center;">
<td>{{ ticket_purchase.ticket.event_instance.name }}</td>
<td>{{ ticket_purchase.ticket.name }}</td>
<td>{{ ticket_purchase.ticket.description }}</td>
<td>{{ ticket_purchase.get_stand_by_pretty }}</td>
<td>{{ ticket_purchase.get_refunded_pretty }}</td>
</tr>
{% empty %}
<tr style="text-align:center;">
<td colspan="5">Ingen billetter releateret til dig.</td>
</tr>
{% endfor %}
{% endautoescape %}
</table>

<div class="pagination">
<div>
{% if purchase_page.has_previous %}
<a href="?purchase_table_index={{ purchase_page.previous_page_number }}">Forrige</a>
{% else %}
Ingen Forrige
{% endif %}
-
{% if purchase_page.has_next %}
<a href="?purchase_table_index={{ purchase_page.next_page_number }}">Næste</a>
{% else %}
Ingen Næste
{% endif %}
</div>
<div>
Side {{ purchase_page.number }} af {{ purchase_page.paginator.num_pages }}
</div>
</div>



{% endblock %}

Loading