mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 06:31:13 -05:00
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.
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
"""
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user