- 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.
276 lines
8.3 KiB
Markdown
276 lines
8.3 KiB
Markdown
# 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:
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
TYPE_CHOICES = [
|
|
("for-profit", "For-Profit"),
|
|
("non-profit", "Non-Profit"),
|
|
("government", "Government"),
|
|
("ngo", "NGO"),
|
|
("educational", "Educational"),
|
|
("healthcare", "Healthcare"),
|
|
("cooperative", "Cooperative"),
|
|
]
|
|
```
|
|
|
|
### STATUS_CHOICES
|
|
|
|
```python
|
|
STATUS_CHOICES = [
|
|
("active", "Active"),
|
|
("inactive", "Inactive"),
|
|
("pending", "Pending"),
|
|
("suspended", "Suspended"),
|
|
("dissolved", "Dissolved"),
|
|
("merged", "Merged"),
|
|
]
|
|
```
|
|
|
|
### SIZE_CHOICES (recommended)
|
|
|
|
```python
|
|
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)
|
|
|
|
```python
|
|
PARENT_RELATIONSHIP_CHOICES = [
|
|
("subsidiary", "Subsidiary"),
|
|
("division", "Division"),
|
|
("branch", "Branch"),
|
|
("franchise", "Franchise"),
|
|
("joint-venture", "Joint Venture"),
|
|
("department", "Department"),
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
## Recommended Fields
|
|
|
|
Most apps should include these fields:
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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).
|