Skip to content

Commit 7d8fa23

Browse files
committed
parse Submissions data from pretalx
Example of the implementation, including pushing reports and basic charts to both web and discord
1 parent e3b279d commit 7d8fa23

File tree

10 files changed

+669
-4
lines changed

10 files changed

+669
-4
lines changed

intbot/core/analysis/submissions.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""
2+
Basic analysis of Pretalx Submissions data
3+
"""
4+
5+
from datetime import datetime
6+
from typing import ClassVar, Iterable
7+
8+
import plotly.express as px
9+
import polars as pl
10+
from core.models import PretalxData
11+
from pydantic import BaseModel, model_validator
12+
13+
14+
class LocalisedFieldsMixin:
15+
# Marking as ClassVar here is important. It doens't work without it :)
16+
_localised_fields: ClassVar[Iterable[str]] = ()
17+
18+
@model_validator(mode="before")
19+
@classmethod
20+
def extract(cls, values):
21+
for field in cls._localised_fields:
22+
if isinstance(values[field], dict) and "en" in values[field]:
23+
values[field] = values[field]["en"]
24+
continue
25+
26+
return values
27+
28+
29+
class Submission(LocalisedFieldsMixin, BaseModel):
30+
code: str
31+
title: str
32+
submission_type: str | dict
33+
track: str | None
34+
state: str
35+
abstract: str
36+
duration: int
37+
created: datetime
38+
level: str = ""
39+
outline: str | None = None
40+
event: str = "ep2025"
41+
42+
_localised_fields = ["submission_type", "track"]
43+
44+
class Questions:
45+
level = "Expected audience expertise"
46+
outline = "Outline"
47+
48+
@model_validator(mode="before")
49+
def extract_answers(cls, values):
50+
# Some things are available as answers to questions and we can extract
51+
# them here
52+
# But using .get since this should be optional for creating objects
53+
# manually
54+
for answer in values.get("answers", ""):
55+
if answer["submission"] is not None and cls.question_is(
56+
answer, cls.Questions.level
57+
):
58+
values["level"] = answer["answer"]
59+
60+
if answer["submission"] is not None and cls.question_is(
61+
answer, cls.Questions.outline
62+
):
63+
values["outline"] = answer["answer"]
64+
65+
return values
66+
67+
@staticmethod
68+
def question_is(answer: dict, question: str) -> bool:
69+
return answer.get("question", {}).get("question", {}).get("en") == question
70+
71+
72+
def get_latest_submissions_data() -> PretalxData:
73+
qs = PretalxData.objects.filter(resource=PretalxData.PretalxResources.submissions)
74+
return qs.latest("created_at")
75+
76+
77+
def parse_latest_submissions_to_objects(pretalx_data: PretalxData) -> list[Submission]:
78+
data = pretalx_data.content
79+
# NOTE: add event as context here
80+
submissions = [Submission.model_validate(entry) for entry in data]
81+
return submissions
82+
83+
84+
def flat_submissions_data(submissions: list[Submission]) -> pl.DataFrame:
85+
"""
86+
Returns a polars data frame with flat description of Submissions
87+
88+
This functions mostly exists to make the API consistent between different
89+
types of data
90+
"""
91+
return pl.DataFrame(submissions)
92+
93+
94+
def latest_flat_submissions_data() -> pl.DataFrame:
95+
"""
96+
Thin wrapper on getting latest information from the database, and
97+
converting into a polars data frame
98+
"""
99+
pretalx_data = get_latest_submissions_data()
100+
submissions = parse_latest_submissions_to_objects(pretalx_data)
101+
return flat_submissions_data(submissions)
102+
103+
104+
def group_submissions_by_state(submissions: pl.DataFrame) -> pl.DataFrame:
105+
by_state = submissions.group_by("state").len().sort("len", descending=True)
106+
107+
return by_state
108+
109+
110+
def piechart_submissions_by_state(submissions_by_state: pl.DataFrame):
111+
fig = px.pie(
112+
submissions_by_state,
113+
values="len",
114+
names="state",
115+
title="state of the submission",
116+
color_discrete_sequence=px.colors.qualitative.Pastel,
117+
)
118+
119+
return fig

intbot/core/bot/main.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
import io
2+
13
import discord
24
from asgiref.sync import sync_to_async
35
from core.analysis.products import latest_flat_product_data
6+
from core.analysis.submissions import (
7+
group_submissions_by_state,
8+
latest_flat_submissions_data,
9+
piechart_submissions_by_state,
10+
)
411
from core.models import DiscordMessage, InboxItem
512
from discord.ext import commands, tasks
613
from django.conf import settings
@@ -220,6 +227,26 @@ async def products(ctx):
220227
await ctx.send(f"```{str(data)}```")
221228

222229

230+
@bot.command()
231+
async def submissions_status(ctx):
232+
df = await sync_to_async(latest_flat_submissions_data)()
233+
by_state = group_submissions_by_state(df)
234+
235+
await ctx.send(f"```{str(by_state)}```")
236+
237+
238+
@bot.command()
239+
async def submissions_status_pie_chart(ctx):
240+
df = await sync_to_async(latest_flat_submissions_data)()
241+
by_state = group_submissions_by_state(df)
242+
piechart = piechart_submissions_by_state(by_state)
243+
244+
png_bytes = piechart.to_image(format="png")
245+
file = discord.File(io.BytesIO(png_bytes), filename="submissions_by_state.png")
246+
247+
await ctx.send(file=file)
248+
249+
223250
def run_bot():
224251
bot_token = settings.DISCORD_BOT_TOKEN
225252
bot.run(bot_token)

intbot/core/views.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
from core.analysis.products import latest_flat_product_data
2+
from core.analysis.submissions import (
3+
group_submissions_by_state,
4+
latest_flat_submissions_data,
5+
piechart_submissions_by_state,
6+
)
27
from django.conf import settings
38
from django.contrib.auth.decorators import login_required
49
from django.template.response import TemplateResponse
510
from django.utils import timezone
11+
from django.utils.safestring import mark_safe
612

713

814
def days_until(request):
@@ -37,3 +43,27 @@ def products(request):
3743
"rows": rows,
3844
},
3945
)
46+
47+
48+
@login_required
49+
def submissions(request):
50+
"""
51+
Show some basic aggregation of submissions data
52+
"""
53+
54+
df = latest_flat_submissions_data()
55+
by_state = group_submissions_by_state(df)
56+
piechart = piechart_submissions_by_state(by_state)
57+
58+
return TemplateResponse(
59+
request,
60+
"submissions.html",
61+
{
62+
"piechart": mark_safe(
63+
piechart.to_html(
64+
full_html=False,
65+
include_plotlyjs="cdn",
66+
)
67+
),
68+
},
69+
)

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, products
7+
from core.views import days_until, products, submissions
88
from django.contrib import admin
99
from django.urls import path
1010

@@ -18,4 +18,5 @@
1818
# Public Pages
1919
path("days-until/", days_until),
2020
path("products/", products),
21+
path("submissions/", submissions),
2122
]

intbot/templates/submissions.html

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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>Submissions</h1>
16+
17+
<div id="piechart">
18+
{{ piechart }}
19+
</div>
20+
<p style="text-align: center">This is not particularly useful now, but it's here as a proof of concept :)</p>
21+
22+
<table>
23+
<thead>
24+
{% for column in columns %}
25+
<td>{{ column }}</td>
26+
{% endfor %}
27+
</thead>
28+
29+
{% for row in rows %}
30+
<tr>
31+
{% for cell in row %}
32+
<td>{{ cell }}</td>
33+
{% endfor %}
34+
</tr>
35+
{% endfor %}
36+
</table>
37+
38+
</div>
39+
</body>
40+
</html>

0 commit comments

Comments
 (0)