Source code for coalition.legal.models

"""Models for managing legal documents and user acceptance."""

import uuid
from typing import TYPE_CHECKING

from django.db import models
from django.utils import timezone
from tinymce.models import HTMLField

from coalition.content.html_sanitizer import HTMLSanitizer

if TYPE_CHECKING:
    from typing import Any


[docs] class LegalDocument(models.Model): """ Model for storing legal documents like Terms of Use, Privacy Policy, etc. Supports versioning and tracking of active documents. Only one document of each type can be active at a time. """
[docs] DOCUMENT_TYPES = [ ("terms", "Terms of Use"), ("privacy", "Privacy Policy"), ("cookies", "Cookie Policy"), ("acceptable_use", "Acceptable Use Policy"), ]
[docs] document_type = models.CharField( max_length=20, choices=DOCUMENT_TYPES, help_text="Type of legal document", )
[docs] title = models.CharField( max_length=200, help_text="Title of the document", )
[docs] content = HTMLField( help_text="Full content of the legal document (HTML allowed)", )
[docs] version = models.CharField( max_length=20, help_text="Version identifier (e.g., '1.0', '2023-12-01')", )
[docs] is_active = models.BooleanField( default=False, help_text="Whether this is the currently active version", )
[docs] effective_date = models.DateTimeField( default=timezone.now, help_text="When this version becomes/became effective", )
[docs] created_at = models.DateTimeField( auto_now_add=True, help_text="When this document was created", )
[docs] updated_at = models.DateTimeField( auto_now=True, help_text="When this document was last updated", )
[docs] created_by = models.ForeignKey( "auth.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="created_legal_documents", help_text="Admin user who created this document", )
[docs] class Meta:
[docs] db_table = "legal_document"
[docs] ordering = ["-effective_date", "-created_at"]
[docs] indexes = [ models.Index(fields=["document_type", "is_active"]), models.Index(fields=["effective_date"]), ]
[docs] unique_together = [ ["document_type", "version"], ]
[docs] def __str__(self) -> str: active_marker = " (ACTIVE)" if self.is_active else "" return f"{self.get_document_type_display()} v{self.version}{active_marker}"
[docs] def save(self, *args: "Any", **kwargs: "Any") -> None: """Ensure only one active document per type and sanitize content.""" # Sanitize content before saving if self.content: self.content = HTMLSanitizer.sanitize(self.content) if self.title: self.title = HTMLSanitizer.sanitize_plain_text(self.title) # If this document is being set as active, deactivate others of same type if self.is_active: LegalDocument.objects.filter( document_type=self.document_type, is_active=True, ).exclude(pk=self.pk).update(is_active=False) super().save(*args, **kwargs)
@classmethod
[docs] def get_active_document(cls, document_type: str) -> "LegalDocument | None": """Get the currently active document of a specific type.""" return cls.objects.filter(document_type=document_type, is_active=True).first()
[docs] class TermsAcceptance(models.Model): """ Track acceptance of legal documents by endorsers. This model records when someone accepts specific versions of legal documents, particularly Terms of Use during the endorsement process. """
[docs] endorsement = models.ForeignKey( "endorsements.Endorsement", on_delete=models.CASCADE, related_name="terms_acceptances", help_text="The endorsement this acceptance is associated with", )
[docs] legal_document = models.ForeignKey( LegalDocument, on_delete=models.PROTECT, # Don't allow deletion of accepted documents related_name="acceptances", help_text="The specific version of the document that was accepted", )
[docs] accepted_at = models.DateTimeField( default=timezone.now, help_text="When the terms were accepted", )
[docs] ip_address = models.GenericIPAddressField( null=True, blank=True, help_text="IP address from which terms were accepted", )
[docs] user_agent = models.CharField( max_length=1000, blank=True, help_text="Browser user agent string (truncated if over 1000 chars)", )
[docs] acceptance_token = models.UUIDField( default=uuid.uuid4, unique=True, help_text="Unique token for this acceptance record", )
[docs] class Meta:
[docs] db_table = "terms_acceptance"
[docs] ordering = ["-accepted_at"]
[docs] indexes = [ models.Index(fields=["accepted_at"]), models.Index(fields=["endorsement", "legal_document"]), ]
[docs] def save(self, *args: "Any", **kwargs: "Any") -> None: """Truncate user_agent if it exceeds maximum length.""" if self.user_agent and len(self.user_agent) > 1000: self.user_agent = self.user_agent[:1000] super().save(*args, **kwargs)
[docs] def __str__(self) -> str: return ( f"{self.endorsement.stakeholder} accepted " f"{self.legal_document} at {self.accepted_at}" )