Files
mnemosyne/docs/Pattern_Organization_V1-00.md
Robert Helewka 99bdb4ac92 Add Themis application with custom widgets, views, and utilities
- Implemented custom form widgets for date, time, and datetime fields with DaisyUI styling.
- Created utility functions for formatting dates, times, and numbers according to user preferences.
- Developed views for profile settings, API key management, and notifications, including health check endpoints.
- Added URL configurations for Themis tests and main application routes.
- Established test cases for custom widgets to ensure proper functionality and integration.
- Defined project metadata and dependencies in pyproject.toml for package management.
2026-03-21 02:00:18 +00:00

8.3 KiB

Organization Model Pattern v1.0.0

Standard pattern for Organization models across Django applications. Each app implements its own Organization model following this pattern to ensure interoperability and consistent field names.

🐾 Red Panda Approval™

This pattern follows Red Panda Approval standards.


Why a Pattern, Not a Shared Model

Organization requirements vary by domain. A financial app needs stock symbols and ISIN codes. A healthcare app needs provider IDs. An education app needs accreditation fields. Shipping a monolithic Organization model with 40+ fields forces every app to carry fields it does not need.

Instead, this pattern defines:

  • Required fields every Organization model must have
  • Recommended fields most apps should include
  • Extension guidelines for domain-specific needs
  • Standard choice values for interoperability

Required Fields

Every Organization model must include these fields:

import uuid
from django.conf import settings
from django.db import models


class Organization(models.Model):
    # Primary key
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

    # Core identity
    name = models.CharField(max_length=255, db_index=True,
                            help_text="Organization display name")
    slug = models.SlugField(max_length=255, unique=True,
                            help_text="URL-friendly identifier")

    # Classification
    type = models.CharField(max_length=20, choices=TYPE_CHOICES,
                            help_text="Organization type")
    status = models.CharField(max_length=20, choices=STATUS_CHOICES,
                              default="active", help_text="Current status")

    # Location
    country = models.CharField(max_length=2, help_text="ISO 3166-1 alpha-2 country code")

    # Audit
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
                                   null=True, blank=True, related_name="created_organizations")
    updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
                                   null=True, blank=True, related_name="updated_organizations")

    class Meta:
        verbose_name = "Organization"
        verbose_name_plural = "Organizations"

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        from django.urls import reverse
        return reverse("organization-detail", kwargs={"slug": self.slug})

Standard Choice Values

Use these exact values for interoperability between apps:

TYPE_CHOICES

TYPE_CHOICES = [
    ("for-profit", "For-Profit"),
    ("non-profit", "Non-Profit"),
    ("government", "Government"),
    ("ngo", "NGO"),
    ("educational", "Educational"),
    ("healthcare", "Healthcare"),
    ("cooperative", "Cooperative"),
]

STATUS_CHOICES

STATUS_CHOICES = [
    ("active", "Active"),
    ("inactive", "Inactive"),
    ("pending", "Pending"),
    ("suspended", "Suspended"),
    ("dissolved", "Dissolved"),
    ("merged", "Merged"),
]
SIZE_CHOICES = [
    ("micro", "Micro (1-9)"),
    ("small", "Small (10-49)"),
    ("medium", "Medium (50-249)"),
    ("large", "Large (250-999)"),
    ("enterprise", "Enterprise (1000+)"),
]

PARENT_RELATIONSHIP_CHOICES (if using hierarchy)

PARENT_RELATIONSHIP_CHOICES = [
    ("subsidiary", "Subsidiary"),
    ("division", "Division"),
    ("branch", "Branch"),
    ("franchise", "Franchise"),
    ("joint-venture", "Joint Venture"),
    ("department", "Department"),
]

Most apps should include these fields:

# Extended identity
legal_name = models.CharField(max_length=255, blank=True, default="",
                              help_text="Full legal entity name")
abbreviated_name = models.CharField(max_length=50, blank=True, default="",
                                    db_index=True, help_text="Short name/acronym")

# Classification
size = models.CharField(max_length=20, choices=SIZE_CHOICES, blank=True, default="",
                        help_text="Organization size")

# Contact
primary_email = models.EmailField(blank=True, default="", help_text="Primary contact email")
primary_phone = models.CharField(max_length=20, blank=True, default="", help_text="Primary phone")
website = models.URLField(blank=True, default="", help_text="Organization website")

# Address
address_line1 = models.CharField(max_length=255, blank=True, default="")
address_line2 = models.CharField(max_length=255, blank=True, default="")
city = models.CharField(max_length=100, blank=True, default="")
state_province = models.CharField(max_length=100, blank=True, default="")
postal_code = models.CharField(max_length=20, blank=True, default="")

# Content
overview = models.TextField(blank=True, default="", help_text="Organization description")

# Metadata
is_active = models.BooleanField(default=True, help_text="Soft delete flag")
tags = models.JSONField(default=list, blank=True, help_text="Flexible tags")

Hierarchy Pattern

For apps that need parent-child organization relationships:

# Hierarchical relationships
parent_organization = models.ForeignKey(
    "self",
    on_delete=models.SET_NULL,
    null=True,
    blank=True,
    related_name="subsidiaries",
    help_text="Parent organization",
)
parent_relationship_type = models.CharField(
    max_length=20,
    choices=PARENT_RELATIONSHIP_CHOICES,
    blank=True,
    default="",
    help_text="Type of relationship with parent",
)

Hierarchy Utility Functions

def get_ancestors(org):
    """Walk up the parent chain. Returns list of Organization instances."""
    ancestors = []
    current = org.parent_organization
    while current:
        ancestors.append(current)
        current = current.parent_organization
    return ancestors


def get_descendants(org):
    """Recursively collect all child organizations."""
    descendants = []
    for child in org.subsidiaries.all():
        descendants.append(child)
        descendants.extend(get_descendants(child))
    return descendants

⚠️ Warning: Recursive queries can be expensive. For deep hierarchies, consider using django-mptt or django-treebeard, or store a materialized path.


Domain Extension Examples

Financial App

class Organization(BaseOrganization):
    revenue = models.DecimalField(max_digits=15, decimal_places=2, null=True, blank=True)
    revenue_year = models.PositiveIntegerField(null=True, blank=True)
    employee_count = models.PositiveIntegerField(null=True, blank=True)
    stock_symbol = models.CharField(max_length=10, blank=True, default="", db_index=True)
    fiscal_year_end_month = models.PositiveSmallIntegerField(null=True, blank=True)

Healthcare App

class Organization(BaseOrganization):
    npi_number = models.CharField(max_length=10, blank=True, default="")
    facility_type = models.CharField(max_length=30, choices=FACILITY_CHOICES)
    bed_count = models.PositiveIntegerField(null=True, blank=True)
    accreditation = models.JSONField(default=list, blank=True)

Education App

class Organization(BaseOrganization):
    institution_type = models.CharField(max_length=30, choices=INSTITUTION_CHOICES)
    student_count = models.PositiveIntegerField(null=True, blank=True)
    accreditation_body = models.CharField(max_length=100, blank=True, default="")

Anti-Patterns

  • Don't use null=True on CharField/TextField — use blank=True, default=""
  • Don't put all possible fields in a single model — extend per domain
  • Don't use Meta.ordering on Organization — specify in queries
  • Don't override save() for hierarchy calculation — use signals or service functions
  • Don't expose sequential IDs in URLs — use slug or short UUID

Indexing Recommendations

class Meta:
    indexes = [
        models.Index(fields=["name"], name="org_name_idx"),
        models.Index(fields=["status"], name="org_status_idx"),
        models.Index(fields=["type"], name="org_type_idx"),
        models.Index(fields=["country"], name="org_country_idx"),
    ]

Add domain-specific indexes as needed (e.g., stock_symbol for financial apps).