mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:11:13 -05:00
- 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.
288 lines
8.4 KiB
Python
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
|