Refactor code structure and remove redundant changes

This commit is contained in:
pacnpal
2025-11-09 16:31:34 -05:00
parent 2884bc23ce
commit eb68cf40c6
1080 changed files with 27361 additions and 56687 deletions

View File

View File

@@ -0,0 +1,115 @@
"""
Django admin interface for Contact submissions.
"""
from django.contrib import admin
from django.utils.html import format_html
from django.utils import timezone
from .models import ContactSubmission
@admin.register(ContactSubmission)
class ContactSubmissionAdmin(admin.ModelAdmin):
"""Admin interface for managing contact submissions."""
list_display = [
'ticket_number',
'name',
'email',
'category',
'status_badge',
'assigned_to',
'created_at',
]
list_filter = [
'status',
'category',
'created_at',
'assigned_to',
]
search_fields = [
'ticket_number',
'name',
'email',
'subject',
'message',
]
readonly_fields = [
'id',
'ticket_number',
'user',
'created_at',
'updated_at',
'resolved_at',
]
fieldsets = (
('Contact Information', {
'fields': ('ticket_number', 'name', 'email', 'user', 'category')
}),
('Message', {
'fields': ('subject', 'message')
}),
('Status & Assignment', {
'fields': ('status', 'assigned_to', 'admin_notes')
}),
('Resolution', {
'fields': ('resolved_at', 'resolved_by'),
'classes': ('collapse',)
}),
('Metadata', {
'fields': ('id', 'created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
def status_badge(self, obj):
"""Display status with colored badge."""
colors = {
'pending': '#ff9800',
'in_progress': '#2196f3',
'resolved': '#4caf50',
'archived': '#9e9e9e',
}
color = colors.get(obj.status, '#9e9e9e')
return format_html(
'<span style="background-color: {}; color: white; padding: 3px 10px; '
'border-radius: 3px; font-weight: bold;">{}</span>',
color,
obj.get_status_display()
)
status_badge.short_description = 'Status'
def save_model(self, request, obj, form, change):
"""Auto-set resolved_by when status changes to resolved."""
if change and 'status' in form.changed_data:
if obj.status == 'resolved' and not obj.resolved_by:
obj.resolved_by = request.user
obj.resolved_at = timezone.now()
super().save_model(request, obj, form, change)
actions = ['mark_as_in_progress', 'mark_as_resolved', 'assign_to_me']
def mark_as_in_progress(self, request, queryset):
"""Mark selected submissions as in progress."""
updated = queryset.update(status='in_progress')
self.message_user(request, f'{updated} submission(s) marked as in progress.')
mark_as_in_progress.short_description = "Mark as In Progress"
def mark_as_resolved(self, request, queryset):
"""Mark selected submissions as resolved."""
updated = queryset.filter(status__in=['pending', 'in_progress']).update(
status='resolved',
resolved_at=timezone.now(),
resolved_by=request.user
)
self.message_user(request, f'{updated} submission(s) marked as resolved.')
mark_as_resolved.short_description = "Mark as Resolved"
def assign_to_me(self, request, queryset):
"""Assign selected submissions to current user."""
updated = queryset.update(assigned_to=request.user)
self.message_user(request, f'{updated} submission(s) assigned to you.')
assign_to_me.short_description = "Assign to Me"

View File

@@ -0,0 +1,7 @@
from django.apps import AppConfig
class ContactConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.contact'
verbose_name = 'Contact Management'

View File

@@ -0,0 +1,300 @@
# Generated by Django 4.2.8 on 2025-11-09 17:45
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import pgtrigger.compiler
import pgtrigger.migrations
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
("pghistory", "0006_delete_aggregateevent"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="ContactSubmission",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=255)),
("email", models.EmailField(max_length=254)),
("subject", models.CharField(max_length=255)),
("message", models.TextField()),
(
"category",
models.CharField(
choices=[
("general", "General Inquiry"),
("bug", "Bug Report"),
("feature", "Feature Request"),
("abuse", "Report Abuse"),
("data", "Data Correction"),
("account", "Account Issue"),
("other", "Other"),
],
default="general",
max_length=50,
),
),
(
"status",
models.CharField(
choices=[
("pending", "Pending Review"),
("in_progress", "In Progress"),
("resolved", "Resolved"),
("archived", "Archived"),
],
db_index=True,
default="pending",
max_length=20,
),
),
(
"ticket_number",
models.CharField(
blank=True,
help_text="Auto-generated ticket number for tracking",
max_length=20,
null=True,
unique=True,
),
),
(
"admin_notes",
models.TextField(
blank=True,
help_text="Internal notes for admin use only",
null=True,
),
),
("resolved_at", models.DateTimeField(blank=True, null=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"assigned_to",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="assigned_contacts",
to=settings.AUTH_USER_MODEL,
),
),
(
"resolved_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="resolved_contacts",
to=settings.AUTH_USER_MODEL,
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="contact_submissions",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Contact Submission",
"verbose_name_plural": "Contact Submissions",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="ContactSubmissionEvent",
fields=[
("pgh_id", models.AutoField(primary_key=True, serialize=False)),
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
("pgh_label", models.TextField(help_text="The event label.")),
(
"id",
models.UUIDField(
default=uuid.uuid4, editable=False, serialize=False
),
),
("name", models.CharField(max_length=255)),
("email", models.EmailField(max_length=254)),
("subject", models.CharField(max_length=255)),
("message", models.TextField()),
(
"category",
models.CharField(
choices=[
("general", "General Inquiry"),
("bug", "Bug Report"),
("feature", "Feature Request"),
("abuse", "Report Abuse"),
("data", "Data Correction"),
("account", "Account Issue"),
("other", "Other"),
],
default="general",
max_length=50,
),
),
(
"status",
models.CharField(
choices=[
("pending", "Pending Review"),
("in_progress", "In Progress"),
("resolved", "Resolved"),
("archived", "Archived"),
],
default="pending",
max_length=20,
),
),
(
"ticket_number",
models.CharField(
blank=True,
help_text="Auto-generated ticket number for tracking",
max_length=20,
null=True,
),
),
(
"admin_notes",
models.TextField(
blank=True,
help_text="Internal notes for admin use only",
null=True,
),
),
("resolved_at", models.DateTimeField(blank=True, null=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"assigned_to",
models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to=settings.AUTH_USER_MODEL,
),
),
(
"pgh_context",
models.ForeignKey(
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to="pghistory.context",
),
),
(
"pgh_obj",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="events",
related_query_name="+",
to="contact.contactsubmission",
),
),
(
"resolved_by",
models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to=settings.AUTH_USER_MODEL,
),
),
(
"user",
models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
migrations.AddIndex(
model_name="contactsubmission",
index=models.Index(
fields=["status", "-created_at"], name="contact_con_status_0384dd_idx"
),
),
migrations.AddIndex(
model_name="contactsubmission",
index=models.Index(
fields=["category", "-created_at"],
name="contact_con_categor_72d10a_idx",
),
),
migrations.AddIndex(
model_name="contactsubmission",
index=models.Index(
fields=["ticket_number"], name="contact_con_ticket__fac4eb_idx"
),
),
pgtrigger.migrations.AddTrigger(
model_name="contactsubmission",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "contact_contactsubmissionevent" ("admin_notes", "assigned_to_id", "category", "created_at", "email", "id", "message", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "resolved_at", "resolved_by_id", "status", "subject", "ticket_number", "updated_at", "user_id") VALUES (NEW."admin_notes", NEW."assigned_to_id", NEW."category", NEW."created_at", NEW."email", NEW."id", NEW."message", NEW."name", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."resolved_at", NEW."resolved_by_id", NEW."status", NEW."subject", NEW."ticket_number", NEW."updated_at", NEW."user_id"); RETURN NULL;',
hash="cbbb92ce277f4fa1d4fe3dccd8e111b39c9bc9a6",
operation="INSERT",
pgid="pgtrigger_insert_insert_32905",
table="contact_contactsubmission",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="contactsubmission",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "contact_contactsubmissionevent" ("admin_notes", "assigned_to_id", "category", "created_at", "email", "id", "message", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "resolved_at", "resolved_by_id", "status", "subject", "ticket_number", "updated_at", "user_id") VALUES (NEW."admin_notes", NEW."assigned_to_id", NEW."category", NEW."created_at", NEW."email", NEW."id", NEW."message", NEW."name", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."resolved_at", NEW."resolved_by_id", NEW."status", NEW."subject", NEW."ticket_number", NEW."updated_at", NEW."user_id"); RETURN NULL;',
hash="ff38205a830f0b09c39d88d8bcce780f7c2fd2ab",
operation="UPDATE",
pgid="pgtrigger_update_update_a7348",
table="contact_contactsubmission",
when="AFTER",
),
),
),
]

View File

@@ -0,0 +1,135 @@
"""
Contact submission models for user inquiries and support tickets.
"""
import uuid
import pghistory
from django.db import models
from django.utils import timezone
@pghistory.track()
class ContactSubmission(models.Model):
"""
User-submitted contact form messages and support tickets.
Tracks all communication from users for admin follow-up.
"""
STATUS_CHOICES = [
('pending', 'Pending Review'),
('in_progress', 'In Progress'),
('resolved', 'Resolved'),
('archived', 'Archived'),
]
CATEGORY_CHOICES = [
('general', 'General Inquiry'),
('bug', 'Bug Report'),
('feature', 'Feature Request'),
('abuse', 'Report Abuse'),
('data', 'Data Correction'),
('account', 'Account Issue'),
('other', 'Other'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# Contact Information
name = models.CharField(max_length=255)
email = models.EmailField()
subject = models.CharField(max_length=255)
message = models.TextField()
category = models.CharField(
max_length=50,
choices=CATEGORY_CHOICES,
default='general'
)
# Status & Assignment
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='pending',
db_index=True
)
ticket_number = models.CharField(
max_length=20,
unique=True,
null=True,
blank=True,
help_text="Auto-generated ticket number for tracking"
)
# User Association (if logged in when submitting)
user = models.ForeignKey(
'users.User',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='contact_submissions'
)
# Assignment & Resolution
assigned_to = models.ForeignKey(
'users.User',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='assigned_contacts'
)
admin_notes = models.TextField(
null=True,
blank=True,
help_text="Internal notes for admin use only"
)
resolved_at = models.DateTimeField(null=True, blank=True)
resolved_by = models.ForeignKey(
'users.User',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='resolved_contacts'
)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'Contact Submission'
verbose_name_plural = 'Contact Submissions'
ordering = ['-created_at']
indexes = [
models.Index(fields=['status', '-created_at']),
models.Index(fields=['category', '-created_at']),
models.Index(fields=['ticket_number']),
]
def __str__(self):
ticket = f" ({self.ticket_number})" if self.ticket_number else ""
return f"{self.name} - {self.get_category_display()}{ticket}"
def save(self, *args, **kwargs):
# Auto-generate ticket number if not set
if not self.ticket_number:
# Format: CONT-YYYYMMDD-XXXX
from django.db.models import Max
today = timezone.now().strftime('%Y%m%d')
prefix = f"CONT-{today}"
# Get the highest ticket number for today
last_ticket = ContactSubmission.objects.filter(
ticket_number__startswith=prefix
).aggregate(Max('ticket_number'))['ticket_number__max']
if last_ticket:
# Extract the sequence number and increment
seq = int(last_ticket.split('-')[-1]) + 1
else:
seq = 1
self.ticket_number = f"{prefix}-{seq:04d}"
# Set resolved_at when status changes to resolved
if self.status == 'resolved' and not self.resolved_at:
self.resolved_at = timezone.now()
super().save(*args, **kwargs)

View File

@@ -0,0 +1,150 @@
"""
Celery tasks for contact submission notifications.
"""
from celery import shared_task
from django.core.mail import send_mail
from django.conf import settings
from django.template.loader import render_to_string
from django.utils.html import strip_tags
@shared_task
def send_contact_confirmation_email(contact_id):
"""
Send confirmation email to user who submitted contact form.
Args:
contact_id: UUID of the ContactSubmission
"""
from .models import ContactSubmission
try:
contact = ContactSubmission.objects.get(id=contact_id)
# Render email template
html_message = render_to_string('emails/contact_confirmation.html', {
'name': contact.name,
'ticket_number': contact.ticket_number,
'subject': contact.subject,
'category': contact.get_category_display(),
'message': contact.message,
})
plain_message = strip_tags(html_message)
# Send email
send_mail(
subject=f'Contact Form Received - Ticket #{contact.ticket_number}',
message=plain_message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[contact.email],
html_message=html_message,
fail_silently=False,
)
return f"Confirmation email sent to {contact.email}"
except ContactSubmission.DoesNotExist:
return f"Contact submission {contact_id} not found"
except Exception as e:
# Log error but don't fail the task
print(f"Error sending contact confirmation: {str(e)}")
raise
@shared_task
def notify_admins_new_contact(contact_id):
"""
Notify admin team of new contact submission.
Args:
contact_id: UUID of the ContactSubmission
"""
from .models import ContactSubmission
from apps.users.models import User
try:
contact = ContactSubmission.objects.get(id=contact_id)
# Get all admin and moderator emails
admin_emails = User.objects.filter(
role__in=['admin', 'moderator']
).values_list('email', flat=True)
if not admin_emails:
return "No admin emails found"
# Render email template
html_message = render_to_string('emails/contact_admin_notification.html', {
'ticket_number': contact.ticket_number,
'name': contact.name,
'email': contact.email,
'subject': contact.subject,
'category': contact.get_category_display(),
'message': contact.message,
'admin_url': f"{settings.SITE_URL}/admin/contact/contactsubmission/{contact.id}/change/",
})
plain_message = strip_tags(html_message)
# Send email
send_mail(
subject=f'New Contact Submission - Ticket #{contact.ticket_number}',
message=plain_message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=list(admin_emails),
html_message=html_message,
fail_silently=False,
)
return f"Admin notification sent to {len(admin_emails)} admin(s)"
except ContactSubmission.DoesNotExist:
return f"Contact submission {contact_id} not found"
except Exception as e:
# Log error but don't fail the task
print(f"Error sending admin notification: {str(e)}")
raise
@shared_task
def send_contact_resolution_email(contact_id):
"""
Send email to user when their contact submission is resolved.
Args:
contact_id: UUID of the ContactSubmission
"""
from .models import ContactSubmission
try:
contact = ContactSubmission.objects.get(id=contact_id)
if contact.status != 'resolved':
return f"Contact {contact_id} is not resolved yet"
# Render email template
html_message = render_to_string('emails/contact_resolved.html', {
'name': contact.name,
'ticket_number': contact.ticket_number,
'subject': contact.subject,
'resolved_by': contact.resolved_by.username if contact.resolved_by else 'Support Team',
})
plain_message = strip_tags(html_message)
# Send email
send_mail(
subject=f'Your Support Ticket Has Been Resolved - #{contact.ticket_number}',
message=plain_message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[contact.email],
html_message=html_message,
fail_silently=False,
)
return f"Resolution email sent to {contact.email}"
except ContactSubmission.DoesNotExist:
return f"Contact submission {contact_id} not found"
except Exception as e:
# Log error but don't fail the task
print(f"Error sending resolution email: {str(e)}")
raise