Source code for coalition.endorsements.models

import uuid
from datetime import timedelta
from typing import TYPE_CHECKING

from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from django.utils import timezone

from coalition.content.html_sanitizer import HTMLSanitizer
from coalition.stakeholders.models import Stakeholder

if TYPE_CHECKING:
    from typing import Any


[docs] class Endorsement(models.Model): """ Represents a stakeholder's endorsement of a policy campaign. The endorsement workflow involves multiple steps: 1. Initial submission (status: pending) 2. Email verification (status: verified) 3. Administrative review (status: approved/rejected) 4. Public display (if approved and consent given) Each endorsement links a stakeholder to a campaign and includes verification tokens, submission metadata, and moderation tracking. Only one endorsement per stakeholder per campaign is allowed. """
[docs] STATUS_CHOICES = [ ("pending", "Pending Email Verification"), ("verified", "Email Verified"), ("approved", "Approved for Display"), ("rejected", "Rejected"), ]
[docs] stakeholder = models.ForeignKey( Stakeholder, on_delete=models.CASCADE, related_name="endorsements", help_text="The stakeholder making this endorsement", )
[docs] campaign = models.ForeignKey( "campaigns.PolicyCampaign", on_delete=models.CASCADE, related_name="endorsements", help_text="The policy campaign being endorsed", )
[docs] statement = models.TextField( blank=True, help_text="Optional endorsement statement from the stakeholder", )
[docs] public_display = models.BooleanField( default=True, help_text="Whether this endorsement should be displayed publicly", )
# Email verification fields
[docs] verification_token = models.UUIDField( default=uuid.uuid4, unique=True, help_text="Unique token for email verification", )
[docs] email_verified = models.BooleanField( default=False, help_text="Whether the stakeholder's email has been verified", )
[docs] verification_sent_at = models.DateTimeField( null=True, blank=True, help_text="When the verification email was sent", )
[docs] verified_at = models.DateTimeField( null=True, blank=True, help_text="When the email verification was completed", )
# Admin moderation fields
[docs] status = models.CharField( max_length=20, choices=STATUS_CHOICES, default="pending", help_text="Current status of the endorsement", )
[docs] admin_notes = models.TextField(blank=True, help_text="Internal notes for admins")
[docs] reviewed_by = models.ForeignKey( "auth.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="reviewed_endorsements", help_text="Admin user who reviewed this endorsement", )
[docs] reviewed_at = models.DateTimeField( null=True, blank=True, help_text="When this endorsement was reviewed by an admin", )
# Terms acceptance tracking
[docs] terms_accepted = models.BooleanField( default=False, help_text="Whether the terms of use were accepted", )
[docs] terms_accepted_at = models.DateTimeField( null=True, blank=True, help_text="When the terms were accepted", )
# Organization authorization
[docs] org_authorized = models.BooleanField( default=False, help_text="Whether the stakeholder is authorized to endorse on behalf " "of their organization", )
# Admin display approval
[docs] display_publicly = models.BooleanField( default=False, help_text="Whether admin has approved this endorsement for public display", )
[docs] created_at = models.DateTimeField( auto_now_add=True, help_text="When this endorsement was created", )
[docs] updated_at = models.DateTimeField( auto_now=True, help_text="When this endorsement was last updated", )
[docs] class Meta:
[docs] db_table = "endorsement"
[docs] unique_together = ["stakeholder", "campaign"]
[docs] def __str__(self) -> str: return f"{self.stakeholder} endorses {self.campaign} ({self.status})"
[docs] def save(self, *args: "Any", **kwargs: "Any") -> None: """Sanitize statement field before saving to prevent XSS attacks.""" # Sanitize statement as plain text - endorsements should not contain HTML if self.statement: self.statement = HTMLSanitizer.sanitize_plain_text(self.statement) super().save(*args, **kwargs)
@property
[docs] def is_verification_expired(self) -> bool: """Check if email verification link has expired (24 hours)""" if not self.verification_sent_at: return False expiry_time = self.verification_sent_at + timedelta(hours=24) return timezone.now() > expiry_time
@property
[docs] def should_display_publicly(self) -> bool: """Check if endorsement should be displayed publicly""" return ( self.public_display # User consent and self.email_verified # Email verified and self.status == "approved" # Admin approved and self.display_publicly # Admin selected for display )
[docs] def approve(self, user: User | None = None) -> None: """Approve endorsement for public display""" self.status = "approved" self.reviewed_by = user self.reviewed_at = timezone.now() self.save()
[docs] def reject(self, user: User | None = None, notes: str = "") -> None: """Reject endorsement""" self.status = "rejected" self.reviewed_by = user self.reviewed_at = timezone.now() if notes: self.admin_notes = notes self.save()
[docs] def verify_email(self) -> None: """Mark email as verified and auto-approve if configured""" self.email_verified = True self.verified_at = timezone.now() # Auto-approve only if configured to do so auto_approve = getattr(settings, "AUTO_APPROVE_VERIFIED_ENDORSEMENTS", False) if auto_approve and self.status == "pending": self.status = "approved" elif self.status == "pending": self.status = "verified" self.save()