Source code for django_program.conference.models

"""Conference and Section models for django-program."""

from django.conf import settings
from django.db import models
from encrypted_fields import EncryptedCharField


[docs] class Conference(models.Model): """A conference event with dates, venue, and integration settings. The central model that all other apps reference. Stores Pretalx and Stripe configuration so each conference can be managed independently. """ name = models.CharField(max_length=200) slug = models.SlugField(max_length=200, unique=True) start_date = models.DateField() end_date = models.DateField() timezone = models.CharField(max_length=100, default="UTC") venue = models.CharField(max_length=300, blank=True, default="") address = models.CharField(max_length=500, blank=True, default="") website_url = models.URLField(blank=True, default="") pretalx_event_slug = models.CharField(max_length=200, blank=True, default="") stripe_secret_key = EncryptedCharField(max_length=200, blank=True, null=True, default=None) stripe_publishable_key = EncryptedCharField(max_length=200, blank=True, null=True, default=None) stripe_webhook_secret = EncryptedCharField(max_length=200, blank=True, null=True, default=None) qbo_realm_id = models.CharField( max_length=200, blank=True, default="", help_text="QuickBooks Online Company/Realm ID.", ) qbo_access_token = EncryptedCharField( max_length=2000, blank=True, null=True, default=None, help_text="QBO OAuth2 access token.", ) qbo_refresh_token = EncryptedCharField( max_length=2000, blank=True, null=True, default=None, help_text="QBO OAuth2 refresh token.", ) qbo_token_expires_at = models.DateTimeField( null=True, blank=True, help_text="When the QBO access token expires.", ) qbo_client_id = models.CharField( max_length=200, blank=True, default="", help_text="QBO OAuth2 client ID for token refresh.", ) qbo_client_secret = EncryptedCharField( max_length=500, blank=True, null=True, default=None, help_text="QBO OAuth2 client secret for token refresh.", ) total_capacity = models.PositiveIntegerField( default=0, help_text="Maximum total tickets across all types. 0 means unlimited.", ) revenue_budget = models.DecimalField( max_digits=12, decimal_places=2, null=True, blank=True, help_text="Target revenue budget for this conference.", ) target_attendance = models.PositiveIntegerField( null=True, blank=True, help_text="Target number of attendees.", ) grant_budget = models.DecimalField( max_digits=12, decimal_places=2, null=True, blank=True, help_text="Budget allocated for travel grants.", ) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ["-start_date"] permissions = [ ("view_dashboard", "Can view conference dashboard"), ("manage_conference_settings", "Can edit conference settings and sync"), ("view_program", "Can view program content"), ("change_program", "Can edit program content"), ("view_registration", "Can view attendees and orders"), ("change_registration", "Can manage orders and visa letters"), ("view_commerce", "Can view ticket types, add-ons, vouchers"), ("change_commerce", "Can manage ticket types, add-ons, vouchers"), ("view_badges", "Can view badges and templates"), ("change_badges", "Can manage badges and templates"), ("view_sponsors", "Can view sponsors"), ("change_sponsors", "Can manage sponsors"), ("view_bulk_purchases", "Can view bulk purchases"), ("change_bulk_purchases", "Can manage bulk purchases"), ("view_finance", "Can view financial dashboard and expenses"), ("change_finance", "Can manage expenses"), ("view_reports", "Can view reports and analytics"), ("export_reports", "Can export report data"), ("view_checkin", "Can access check-in"), ("use_terminal", "Can use Terminal POS"), ("view_overrides", "Can view Pretalx overrides"), ("change_overrides", "Can manage Pretalx overrides"), ] def __str__(self) -> str: return self.name
[docs] class Section(models.Model): """A distinct segment of a conference (e.g. Tutorials, Talks, Sprints). Sections divide a conference into logical time blocks, each with their own date range. They are ordered by the ``order`` field for display purposes. """ conference = models.ForeignKey( Conference, on_delete=models.CASCADE, related_name="sections", ) name = models.CharField(max_length=200) slug = models.SlugField(max_length=200) start_date = models.DateField() end_date = models.DateField() order = models.PositiveIntegerField(default=0) class Meta: ordering = ["order", "start_date"] unique_together = [("conference", "slug")] def __str__(self) -> str: return f"{self.name} ({self.conference.slug})"
[docs] class FeatureFlags(models.Model): """Per-conference feature toggle overrides. Database-backed flags that override the defaults from ``DJANGO_PROGRAM["features"]``. Changes take effect immediately without server restart. Each conference has at most one row. All boolean fields are nullable: ``None`` means "use default from settings", while an explicit ``True`` or ``False`` overrides. """ conference = models.OneToOneField( "program_conference.Conference", on_delete=models.CASCADE, related_name="feature_flags", ) registration_enabled = models.BooleanField( null=True, blank=True, help_text="Override registration toggle. Leave blank to use default from settings.", ) sponsors_enabled = models.BooleanField( null=True, blank=True, help_text="Override sponsors toggle.", ) travel_grants_enabled = models.BooleanField( null=True, blank=True, help_text="Override travel grants toggle.", ) programs_enabled = models.BooleanField( null=True, blank=True, help_text="Override programs/activities toggle.", ) pretalx_sync_enabled = models.BooleanField( null=True, blank=True, help_text="Override Pretalx sync toggle.", ) visa_letters_enabled = models.BooleanField( null=True, blank=True, help_text="Override visa invitation letters toggle.", ) public_ui_enabled = models.BooleanField( null=True, blank=True, help_text="Override public UI toggle.", ) manage_ui_enabled = models.BooleanField( null=True, blank=True, help_text="Override manage UI toggle.", ) all_ui_enabled = models.BooleanField( null=True, blank=True, help_text="Master UI switch override.", ) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = "feature flags" verbose_name_plural = "feature flags" def __str__(self) -> str: return f"Feature flags for {self.conference}"
[docs] class ExpenseCategory(models.Model): """A category for conference expenses (e.g. Venue, F&B, A/V, Travel, Marketing).""" conference = models.ForeignKey( Conference, on_delete=models.CASCADE, related_name="expense_categories", ) name = models.CharField(max_length=200) slug = models.SlugField(max_length=200) description = models.TextField(blank=True, default="") budget_amount = models.DecimalField( max_digits=12, decimal_places=2, null=True, blank=True, help_text="Budgeted amount for this expense category.", ) order = models.PositiveIntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ["order", "name"] unique_together = [("conference", "slug")] verbose_name_plural = "expense categories" def __str__(self) -> str: return f"{self.name} ({self.conference.slug})"
[docs] class Expense(models.Model): """An individual expense record for a conference.""" conference = models.ForeignKey( Conference, on_delete=models.CASCADE, related_name="expenses", ) category = models.ForeignKey( ExpenseCategory, on_delete=models.CASCADE, related_name="expenses", ) description = models.CharField(max_length=500) amount = models.DecimalField(max_digits=12, decimal_places=2) vendor = models.CharField(max_length=300, blank=True, default="") date = models.DateField() receipt_reference = models.CharField( max_length=200, blank=True, default="", help_text="Invoice or receipt reference number.", ) notes = models.TextField(blank=True, default="") created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="+", ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ["-date", "-created_at"] def __str__(self) -> str: return f"{self.description} (${self.amount})"
[docs] class KPITargets(models.Model): """Per-conference configurable KPI thresholds for the analytics dashboard. When a field is null, the dashboard falls back to hardcoded industry averages. Setting a value overrides the default target for that metric. """ conference = models.OneToOneField( Conference, on_delete=models.CASCADE, related_name="kpi_targets", ) target_conversion_rate = models.DecimalField( max_digits=5, decimal_places=2, null=True, blank=True, help_text="Target cart-to-order conversion rate (%). Industry avg ~3%.", ) target_refund_rate = models.DecimalField( max_digits=5, decimal_places=2, null=True, blank=True, help_text="Maximum acceptable refund rate (%). Typical target: 5%.", ) target_checkin_rate = models.DecimalField( max_digits=5, decimal_places=2, null=True, blank=True, help_text="Target check-in rate (%). Strong turnout >= 80%.", ) target_fulfillment_rate = models.DecimalField( max_digits=5, decimal_places=2, null=True, blank=True, help_text="Target sponsor benefit fulfillment rate (%). Goal: 90%+.", ) target_revenue_per_attendee = models.DecimalField( max_digits=10, decimal_places=2, null=True, blank=True, help_text="Target revenue per attendee ($).", ) target_room_utilization = models.DecimalField( max_digits=5, decimal_places=2, null=True, blank=True, help_text="Target room utilization rate (%). Industry avg ~28%.", ) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = "KPI targets" verbose_name_plural = "KPI targets" def __str__(self) -> str: return f"KPI targets for {self.conference}"