Files
thrilltrack-explorer/django/apps/versioning/models.py
pacnpal d6ff4cc3a3 Add email templates for user notifications and account management
- Created a base email template (base.html) for consistent styling across all emails.
- Added moderation approval email template (moderation_approved.html) to notify users of approved submissions.
- Added moderation rejection email template (moderation_rejected.html) to inform users of required changes for their submissions.
- Created password reset email template (password_reset.html) for users requesting to reset their passwords.
- Developed a welcome email template (welcome.html) to greet new users and provide account details and tips for using ThrillWiki.
2025-11-08 15:34:04 -05:00

288 lines
8.4 KiB
Python

"""
Versioning models for ThrillWiki.
This module provides automatic version tracking for all entities:
- EntityVersion: Generic version model using ContentType
- Full snapshot storage in JSON
- Changed fields tracking with old/new values
- Link to ContentSubmission when changes come from moderation
"""
import json
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from django.conf import settings
from apps.core.models import BaseModel
class EntityVersion(BaseModel):
"""
Generic version tracking for all entities.
Stores a complete snapshot of the entity state at the time of change,
along with metadata about what changed and who made the change.
"""
CHANGE_TYPE_CHOICES = [
('created', 'Created'),
('updated', 'Updated'),
('deleted', 'Deleted'),
('restored', 'Restored'),
]
# Entity reference (generic)
entity_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
related_name='entity_versions',
help_text="Type of entity (Park, Ride, Company, etc.)"
)
entity_id = models.UUIDField(
db_index=True,
help_text="ID of the entity"
)
entity = GenericForeignKey('entity_type', 'entity_id')
# Version info
version_number = models.PositiveIntegerField(
default=1,
help_text="Sequential version number for this entity"
)
change_type = models.CharField(
max_length=20,
choices=CHANGE_TYPE_CHOICES,
db_index=True,
help_text="Type of change"
)
# Snapshot of entity state
snapshot = models.JSONField(
help_text="Complete snapshot of entity state as JSON"
)
# Changed fields tracking
changed_fields = models.JSONField(
default=dict,
help_text="Dict of changed fields with old/new values: {'field': {'old': ..., 'new': ...}}"
)
# User who made the change
changed_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='entity_versions',
help_text="User who made the change"
)
# Link to ContentSubmission (if change came from moderation)
submission = models.ForeignKey(
'moderation.ContentSubmission',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='versions',
help_text="Submission that caused this version (if applicable)"
)
# Metadata
comment = models.TextField(
blank=True,
help_text="Optional comment about this version"
)
ip_address = models.GenericIPAddressField(
null=True,
blank=True,
help_text="IP address of change origin"
)
user_agent = models.CharField(
max_length=500,
blank=True,
help_text="User agent string"
)
class Meta:
verbose_name = 'Entity Version'
verbose_name_plural = 'Entity Versions'
ordering = ['-created']
indexes = [
models.Index(fields=['entity_type', 'entity_id', '-created']),
models.Index(fields=['entity_type', 'entity_id', '-version_number']),
models.Index(fields=['change_type']),
models.Index(fields=['changed_by']),
models.Index(fields=['submission']),
]
unique_together = [['entity_type', 'entity_id', 'version_number']]
def __str__(self):
return f"{self.entity_type.model} v{self.version_number} ({self.change_type})"
@property
def entity_name(self):
"""Get display name of the entity."""
try:
entity = self.entity
if entity:
return str(entity)
except:
pass
return f"{self.entity_type.model}:{self.entity_id}"
def get_snapshot_dict(self):
"""
Get snapshot as Python dict.
Returns:
dict: Entity snapshot
"""
if isinstance(self.snapshot, str):
return json.loads(self.snapshot)
return self.snapshot
def get_changed_fields_list(self):
"""
Get list of changed field names.
Returns:
list: Field names that changed
"""
return list(self.changed_fields.keys())
def get_field_change(self, field_name):
"""
Get old and new values for a specific field.
Args:
field_name: Name of the field
Returns:
dict: {'old': old_value, 'new': new_value} or None if field didn't change
"""
return self.changed_fields.get(field_name)
def compare_with(self, other_version):
"""
Compare this version with another version.
Args:
other_version: EntityVersion to compare with
Returns:
dict: Comparison result with differences
"""
if not other_version or self.entity_id != other_version.entity_id:
return None
this_snapshot = self.get_snapshot_dict()
other_snapshot = other_version.get_snapshot_dict()
differences = {}
all_keys = set(this_snapshot.keys()) | set(other_snapshot.keys())
for key in all_keys:
this_val = this_snapshot.get(key)
other_val = other_snapshot.get(key)
if this_val != other_val:
differences[key] = {
'this': this_val,
'other': other_val
}
return {
'this_version': self.version_number,
'other_version': other_version.version_number,
'differences': differences,
'changed_field_count': len(differences)
}
def get_diff_summary(self):
"""
Get human-readable summary of changes in this version.
Returns:
str: Summary of changes
"""
if self.change_type == 'created':
return f"Created {self.entity_name}"
if self.change_type == 'deleted':
return f"Deleted {self.entity_name}"
changed_count = len(self.changed_fields)
if changed_count == 0:
return f"No changes to {self.entity_name}"
field_names = ', '.join(self.get_changed_fields_list()[:3])
if changed_count > 3:
field_names += f" and {changed_count - 3} more"
return f"Updated {field_names}"
@classmethod
def get_latest_version_number(cls, entity_type, entity_id):
"""
Get the latest version number for an entity.
Args:
entity_type: ContentType of entity
entity_id: UUID of entity
Returns:
int: Latest version number (0 if no versions exist)
"""
latest = cls.objects.filter(
entity_type=entity_type,
entity_id=entity_id
).aggregate(
max_version=models.Max('version_number')
)
return latest['max_version'] or 0
@classmethod
def get_history(cls, entity_type, entity_id, limit=50):
"""
Get version history for an entity.
Args:
entity_type: ContentType of entity
entity_id: UUID of entity
limit: Maximum number of versions to return
Returns:
QuerySet: Ordered list of versions (newest first)
"""
return cls.objects.filter(
entity_type=entity_type,
entity_id=entity_id
).select_related(
'changed_by',
'submission',
'submission__user'
).order_by('-version_number')[:limit]
@classmethod
def get_version_by_number(cls, entity_type, entity_id, version_number):
"""
Get a specific version by number.
Args:
entity_type: ContentType of entity
entity_id: UUID of entity
version_number: Version number to retrieve
Returns:
EntityVersion or None
"""
try:
return cls.objects.get(
entity_type=entity_type,
entity_id=entity_id,
version_number=version_number
)
except cls.DoesNotExist:
return None