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
17 changes: 16 additions & 1 deletion attendee/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.contrib import admin

from .models import PretixOrder
from .models import AttendeeProfile, PretixOrder


@admin.register(PretixOrder)
Expand All @@ -19,3 +19,18 @@ class PretixOrderAdmin(admin.ModelAdmin):
)
list_filter = ("status", "is_anonymous")
search_fields = ("order_code", "email", "name")


@admin.register(AttendeeProfile)
class AttendeeProfileAdmin(admin.ModelAdmin):
list_display = (
"order",
"job_role",
"country",
"region",
"experience_level",
"industry",
)
list_filter = ("job_role", "country", "region", "experience_level", "industry")
search_fields = ("order__order_code", "job_role", "country")
readonly_fields = ("raw_answers",)
30 changes: 28 additions & 2 deletions attendee/management/commands/fetch_pretix_orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,45 @@

from django.core.management.base import BaseCommand

from attendee.models import PretixOrder
from attendee.models import AttendeeProfile, PretixOrder
from common.pretix_wrapper import PRETIX_EVENT_SLUG, PRETIX_ORG, PretixWrapper


class Command(BaseCommand):
help = "Fetch Pretix orders"
help = "Fetch Pretix orders and sync attendee demographics"

def handle(self, *args, **options):
pretix_wrapper = PretixWrapper(PRETIX_ORG, PRETIX_EVENT_SLUG)
orders_synced = 0
profiles_synced = 0

for order in pretix_wrapper.get_orders():
order_code = order["code"]
pretix_order, created = PretixOrder.objects.get_or_create(
order_code=order_code
)
pretix_order.from_pretix_data(order)
pretix_order.save()
orders_synced += 1

# Sync attendee profile for paid orders
if pretix_order.status == "p":
profile, profile_created = AttendeeProfile.objects.get_or_create(
order=pretix_order
)
profile.populate_from_pretix_data(order)
profile.save()
profiles_synced += 1

if profile_created:
self.stdout.write(
self.style.SUCCESS(
f"Created attendee profile for order {order_code}"
)
)

self.stdout.write(
self.style.SUCCESS(
f"Successfully synced {orders_synced} orders and {profiles_synced} attendee profiles"
)
)
107 changes: 107 additions & 0 deletions attendee/migrations/0002_attendeeprofile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Generated by Django 5.2.8 on 2025-11-20 06:06

import django.contrib.postgres.fields
import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("attendee", "0001_initial"),
("portal", "0002_alter_basemodel_creation_date_and_more"),
]

operations = [
migrations.CreateModel(
name="AttendeeProfile",
fields=[
(
"basemodel_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="portal.basemodel",
),
),
(
"job_role",
models.CharField(
blank=True,
help_text="Professional role/occupation",
max_length=255,
null=True,
),
),
("job_title", models.CharField(blank=True, max_length=255, null=True)),
(
"country",
models.CharField(
blank=True,
help_text="Country of residence",
max_length=100,
null=True,
),
),
(
"region",
models.CharField(
blank=True,
help_text="Geographic region",
max_length=100,
null=True,
),
),
(
"experience_level",
models.CharField(
blank=True,
help_text="Python/programming experience level",
max_length=100,
null=True,
),
),
(
"languages",
django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(max_length=50),
blank=True,
help_text="Spoken languages",
null=True,
size=None,
),
),
("industry", models.CharField(blank=True, max_length=255, null=True)),
(
"company_size",
models.CharField(blank=True, max_length=100, null=True),
),
(
"heard_about",
models.CharField(
blank=True,
help_text="How they heard about PyLadiesCon",
max_length=255,
null=True,
),
),
("raw_answers", models.JSONField(blank=True, null=True)),
(
"order",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="profile",
to="attendee.pretixorder",
),
),
],
options={
"verbose_name": "Attendee Profile",
"verbose_name_plural": "Attendee Profiles",
},
bases=("portal.basemodel",),
),
]
99 changes: 99 additions & 0 deletions attendee/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from enum import StrEnum

from django.contrib.postgres.fields import ArrayField
from django.db import models

from portal.models import BaseModel
Expand Down Expand Up @@ -83,3 +84,101 @@ def set_is_anonymous_from_data(self, data):
in answer["option_identifiers"]
)
return


class AttendeeProfile(BaseModel):
"""
Model to store anonymized attendee demographic data.
This data is collected from pretix registration forms.
"""

order = models.OneToOneField(
PretixOrder, on_delete=models.CASCADE, related_name="profile"
)

# Demographics - all fields are nullable as responses are optional
job_role = models.CharField(
max_length=255, null=True, blank=True, help_text="Professional role/occupation"
)
job_title = models.CharField(max_length=255, null=True, blank=True)
country = models.CharField(
max_length=100, null=True, blank=True, help_text="Country of residence"
)
region = models.CharField(
max_length=100, null=True, blank=True, help_text="Geographic region"
)
experience_level = models.CharField(
max_length=100,
null=True,
blank=True,
help_text="Python/programming experience level",
)
languages = ArrayField(
models.CharField(max_length=50),
null=True,
blank=True,
help_text="Spoken languages",
)
industry = models.CharField(max_length=255, null=True, blank=True)
company_size = models.CharField(max_length=100, null=True, blank=True)
heard_about = models.CharField(
max_length=255,
null=True,
blank=True,
help_text="How they heard about PyLadiesCon",
)

# Store raw answers from pretix in case we need additional data
raw_answers = models.JSONField(null=True, blank=True)

class Meta:
verbose_name = "Attendee Profile"
verbose_name_plural = "Attendee Profiles"

def __str__(self):
return f"Profile for {self.order.order_code}"

def populate_from_pretix_data(self, pretix_data):
"""
Extract attendee demographic data from pretix order data.
This method maps pretix question identifiers to model fields.
"""
# Store all answers for reference
all_answers = []

for position in pretix_data.get("positions", []):
answers = position.get("answers", [])
all_answers.extend(answers)

# Map pretix question identifiers to our fields
# These identifiers should be configured based on the actual pretix form
for answer in answers:
question_id = answer.get("question_identifier")
answer_value = answer.get("answer")

# Map based on question identifiers (these are examples and should be updated)
if question_id == "ROLE":
self.job_role = answer_value
elif question_id == "JOB_TITLE":
self.job_title = answer_value
elif question_id == "COUNTRY":
self.country = answer_value
elif question_id == "REGION":
self.region = answer_value
elif question_id == "EXPERIENCE":
self.experience_level = answer_value
elif question_id == "LANGUAGES":
# Handle multi-select language field
if answer_value:
self.languages = [
lang.strip() for lang in answer_value.split(",")
]
elif question_id == "INDUSTRY":
self.industry = answer_value
elif question_id == "COMPANY_SIZE":
self.company_size = answer_value
elif question_id == "HEARD_ABOUT":
self.heard_about = answer_value

self.raw_answers = all_answers
return self
Loading
Loading