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:
pacnpal
2025-11-08 15:34:04 -05:00
parent 9c46ef8b03
commit d6ff4cc3a3
335 changed files with 61926 additions and 73 deletions

View File

@@ -0,0 +1,236 @@
"""
Admin interface for versioning models.
Provides Django admin interface for viewing version history,
comparing versions, and managing version records.
"""
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from unfold.admin import ModelAdmin
from apps.versioning.models import EntityVersion
@admin.register(EntityVersion)
class EntityVersionAdmin(ModelAdmin):
"""
Admin interface for EntityVersion model.
Provides read-only view of version history with search and filtering.
"""
# Display settings
list_display = [
'version_number',
'entity_link',
'change_type',
'changed_by_link',
'submission_link',
'changed_field_count',
'created',
]
list_filter = [
'change_type',
'entity_type',
'created',
]
search_fields = [
'entity_id',
'comment',
'changed_by__email',
'changed_by__username',
]
ordering = ['-created']
date_hierarchy = 'created'
# Read-only admin (versions should not be modified)
readonly_fields = [
'id',
'entity_type',
'entity_id',
'entity_link',
'version_number',
'change_type',
'snapshot_display',
'changed_fields_display',
'changed_by',
'submission',
'comment',
'ip_address',
'user_agent',
'created',
'modified',
]
fieldsets = (
('Version Information', {
'fields': (
'id',
'version_number',
'change_type',
'created',
'modified',
)
}),
('Entity', {
'fields': (
'entity_type',
'entity_id',
'entity_link',
)
}),
('Changes', {
'fields': (
'changed_fields_display',
'snapshot_display',
)
}),
('Metadata', {
'fields': (
'changed_by',
'submission',
'comment',
'ip_address',
'user_agent',
)
}),
)
def has_add_permission(self, request):
"""Disable adding versions manually."""
return False
def has_delete_permission(self, request, obj=None):
"""Disable deleting versions."""
return False
def has_change_permission(self, request, obj=None):
"""Only allow viewing versions, not editing."""
return False
def entity_link(self, obj):
"""Display link to the entity."""
try:
entity = obj.entity
if entity:
# Try to get admin URL for entity
admin_url = reverse(
f'admin:{obj.entity_type.app_label}_{obj.entity_type.model}_change',
args=[entity.pk]
)
return format_html(
'<a href="{}">{}</a>',
admin_url,
str(entity)
)
except:
pass
return f"{obj.entity_type.model}:{obj.entity_id}"
entity_link.short_description = 'Entity'
def changed_by_link(self, obj):
"""Display link to user who made the change."""
if obj.changed_by:
try:
admin_url = reverse(
'admin:users_user_change',
args=[obj.changed_by.pk]
)
return format_html(
'<a href="{}">{}</a>',
admin_url,
obj.changed_by.email
)
except:
return obj.changed_by.email
return '-'
changed_by_link.short_description = 'Changed By'
def submission_link(self, obj):
"""Display link to content submission if applicable."""
if obj.submission:
try:
admin_url = reverse(
'admin:moderation_contentsubmission_change',
args=[obj.submission.pk]
)
return format_html(
'<a href="{}">#{}</a>',
admin_url,
obj.submission.pk
)
except:
return str(obj.submission.pk)
return '-'
submission_link.short_description = 'Submission'
def changed_field_count(self, obj):
"""Display count of changed fields."""
count = len(obj.changed_fields)
if count == 0:
return '-'
return f"{count} field{'s' if count != 1 else ''}"
changed_field_count.short_description = 'Changed Fields'
def snapshot_display(self, obj):
"""Display snapshot in a formatted way."""
import json
snapshot = obj.get_snapshot_dict()
# Format as pretty JSON
formatted = json.dumps(snapshot, indent=2, sort_keys=True)
return format_html(
'<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;">{}</pre>',
formatted
)
snapshot_display.short_description = 'Snapshot'
def changed_fields_display(self, obj):
"""Display changed fields in a formatted way."""
if not obj.changed_fields:
return format_html('<em>No fields changed</em>')
html_parts = ['<table style="width: 100%; border-collapse: collapse;">']
html_parts.append('<thead><tr style="background: #f5f5f5;">')
html_parts.append('<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Field</th>')
html_parts.append('<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Old Value</th>')
html_parts.append('<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">New Value</th>')
html_parts.append('</tr></thead><tbody>')
for field_name, change in obj.changed_fields.items():
old_val = change.get('old', '-')
new_val = change.get('new', '-')
# Truncate long values
if isinstance(old_val, str) and len(old_val) > 100:
old_val = old_val[:97] + '...'
if isinstance(new_val, str) and len(new_val) > 100:
new_val = new_val[:97] + '...'
html_parts.append('<tr>')
html_parts.append(f'<td style="padding: 8px; border: 1px solid #ddd;"><strong>{field_name}</strong></td>')
html_parts.append(f'<td style="padding: 8px; border: 1px solid #ddd; color: #d32f2f;">{old_val}</td>')
html_parts.append(f'<td style="padding: 8px; border: 1px solid #ddd; color: #388e3c;">{new_val}</td>')
html_parts.append('</tr>')
html_parts.append('</tbody></table>')
return format_html(''.join(html_parts))
changed_fields_display.short_description = 'Changed Fields'
def get_queryset(self, request):
"""Optimize queryset with select_related."""
qs = super().get_queryset(request)
return qs.select_related(
'entity_type',
'changed_by',
'submission',
'submission__user'
)