Skip to content

Commit e3b279d

Browse files
authored
Pretix Products Parsing (#35)
An example implementation of parsing pretix data
1 parent 1c05562 commit e3b279d

File tree

8 files changed

+501
-2
lines changed

8 files changed

+501
-2
lines changed

intbot/core/analysis/products.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""
2+
Parse Products from PretixData for further joins and analysis in other places.
3+
"""
4+
5+
from decimal import Decimal
6+
from typing import ClassVar, Iterable
7+
8+
import polars as pl
9+
from core.models import PretixData
10+
from pydantic import BaseModel, model_validator
11+
12+
13+
class LocalisedFieldsMixin:
14+
# Marking as ClassVar here is important. It doens't work without it :)
15+
_localised_fields: ClassVar[Iterable[str]] = ()
16+
17+
@model_validator(mode="before")
18+
@classmethod
19+
def extract(cls, values):
20+
for field in cls._localised_fields:
21+
if isinstance(values[field], dict) and "en" in values[field]:
22+
values[field] = values[field]["en"]
23+
continue
24+
25+
return values
26+
27+
28+
class ProductVariation(LocalisedFieldsMixin, BaseModel):
29+
id: int
30+
value: str
31+
description: str | dict
32+
price: Decimal
33+
34+
_localised_fields = ["value", "description"]
35+
36+
37+
class Product(LocalisedFieldsMixin, BaseModel):
38+
id: int
39+
name: str
40+
description: str | dict
41+
variations: list[ProductVariation]
42+
default_price: Decimal | None = None
43+
44+
_localised_fields = ["name", "description"]
45+
46+
47+
class FlatProductDescription(BaseModel):
48+
product_id: int
49+
variation_id: int | None
50+
product_name: str
51+
type: str
52+
variant: str
53+
price: Decimal | None
54+
55+
56+
def get_latest_products_data() -> PretixData:
57+
qs = PretixData.objects.filter(resource=PretixData.PretixResources.products)
58+
return qs.latest("created_at")
59+
60+
61+
def parse_latest_products_to_objects(pretix_data: PretixData) -> list[Product]:
62+
data = pretix_data.content
63+
products = [Product.model_validate(entry) for entry in data]
64+
return products
65+
66+
67+
def flat_product_data(products: list[Product]) -> pl.DataFrame:
68+
"""
69+
Returns a polars data frame with flat description of available Products.
70+
Products hold nested `ProductVariation`s; flatten them so every variation
71+
(or base product) becomes one row in a DataFrame.
72+
"""
73+
rows = []
74+
for p in products:
75+
if p.variations:
76+
for v in p.variations:
77+
if "Late" in v.value:
78+
type_ = "Late"
79+
name = v.value.replace("Late", "").strip()
80+
else:
81+
type_ = "Regular"
82+
name = v.value
83+
84+
if "Combined" in name:
85+
name = "Combined"
86+
87+
rows.append(
88+
FlatProductDescription(
89+
product_id=p.id,
90+
variation_id=v.id,
91+
product_name=p.name,
92+
type=type_,
93+
variant=name,
94+
price=v.price,
95+
)
96+
)
97+
else:
98+
rows.append(
99+
FlatProductDescription(
100+
product_id=p.id,
101+
variation_id=None,
102+
product_name=p.name,
103+
variant=p.name,
104+
type="Late" if "Late" in p.name else "Regular",
105+
price=p.default_price,
106+
)
107+
)
108+
109+
return pl.DataFrame(rows)
110+
111+
112+
def latest_flat_product_data() -> pl.DataFrame:
113+
"""
114+
Thin wrapper on getting latest information from the database, and
115+
converting into a polars data frame
116+
"""
117+
pretix_data = get_latest_products_data()
118+
products = parse_latest_products_to_objects(pretix_data)
119+
return flat_product_data(products)

intbot/core/bot/main.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import discord
2+
from asgiref.sync import sync_to_async
3+
from core.analysis.products import latest_flat_product_data
24
from core.models import DiscordMessage, InboxItem
35
from discord.ext import commands, tasks
46
from django.conf import settings
@@ -199,6 +201,25 @@ async def until(ctx):
199201
await ctx.send(f"{delta.days} days left until the conference")
200202

201203

204+
@bot.command()
205+
@commands.has_role("EP2025")
206+
async def products(ctx):
207+
"""
208+
Returns pretix Products but only if called by a EP2025 volunteer on a test channel.
209+
210+
This is an example of implementation of a command that has some basic
211+
access control (in this case works only on certain channels).
212+
"""
213+
214+
if ctx.channel.id != settings.DISCORD_TEST_CHANNEL_ID:
215+
await ctx.send("This command only works on certain channels")
216+
return
217+
218+
data = await sync_to_async(latest_flat_product_data)()
219+
220+
await ctx.send(f"```{str(data)}```")
221+
222+
202223
def run_bot():
203224
bot_token = settings.DISCORD_BOT_TOKEN
204225
bot.run(bot_token)

intbot/core/views.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,39 @@
1+
from core.analysis.products import latest_flat_product_data
12
from django.conf import settings
3+
from django.contrib.auth.decorators import login_required
24
from django.template.response import TemplateResponse
35
from django.utils import timezone
46

57

68
def days_until(request):
79
delta = settings.CONFERENCE_START - timezone.now()
810

9-
return TemplateResponse(request, "days_until.html", {"days_until": delta.days})
11+
return TemplateResponse(
12+
request,
13+
"days_until.html",
14+
{
15+
"days_until": delta.days,
16+
},
17+
)
18+
19+
20+
@login_required
21+
def products(request):
22+
"""
23+
For now this is just an example of the implementation.
24+
25+
Table with products is not that useful, but it's the easiest one to
26+
implement first as proof of concept
27+
"""
28+
df = latest_flat_product_data()
29+
columns = df.columns
30+
rows = df.rows()
31+
32+
return TemplateResponse(
33+
request,
34+
"table.html",
35+
{
36+
"columns": columns,
37+
"rows": rows,
38+
},
39+
)

intbot/intbot/urls.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
internal_webhook_endpoint,
55
zammad_webhook_endpoint,
66
)
7-
from core.views import days_until
7+
from core.views import days_until, products
88
from django.contrib import admin
99
from django.urls import path
1010

@@ -17,4 +17,5 @@
1717
path("webhook/zammad/", zammad_webhook_endpoint),
1818
# Public Pages
1919
path("days-until/", days_until),
20+
path("products/", products),
2021
]

intbot/templates/table.html

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
<html>
3+
<body>
4+
<style type="text/css">
5+
body { background-color: #ffe; color: #151f38; font-family: sans-serif;}
6+
h1 { font-size: 3em; text-align: center; margin-top: 3em; }
7+
thead { background: #aaa; }
8+
table { font-family: monospace; width: 100%}
9+
table td { padding: .5em 1em; }
10+
table tr:nth-child(2n) td { background: #eee; }
11+
table tr:hover td { background: #fee; }
12+
</style>
13+
<div style="margin: 3em">
14+
15+
<h1>Flat Products Table</h1>
16+
<p style="text-align: center">This is not particularly useful now, but it's here as a proof of concept :)</p>
17+
18+
<table>
19+
<thead>
20+
{% for column in columns %}
21+
<td>{{ column }}</td>
22+
{% endfor %}
23+
</thead>
24+
25+
{% for row in rows %}
26+
<tr>
27+
{% for cell in row %}
28+
<td>{{ cell }}</td>
29+
{% endfor %}
30+
</tr>
31+
{% endfor %}
32+
</table>
33+
34+
</div>
35+
</body>
36+
</html>

0 commit comments

Comments
 (0)