Email config, email template and more!

This commit is contained in:
KenwoodFox
2025-12-03 18:46:48 -05:00
parent 0183b03bc2
commit 2836d5954e
14 changed files with 781 additions and 36 deletions

0
quotes/__init__.py Normal file
View File

43
quotes/admin.py Normal file
View File

@@ -0,0 +1,43 @@
from django.contrib import admin
from .models import Submission, SubmissionFile
class SubmissionFileInline(admin.TabularInline):
"""Inline admin for submission files."""
model = SubmissionFile
extra = 0
readonly_fields = ["uploaded_at", "file_size"]
@admin.register(Submission)
class SubmissionAdmin(admin.ModelAdmin):
"""Admin interface for submissions."""
list_display = [
"email",
"submitted_at",
"expiration_time",
"is_expired",
"file_count",
]
list_filter = ["submitted_at", "expiration_time"]
search_fields = ["email", "description"]
readonly_fields = ["submitted_at"]
inlines = [SubmissionFileInline]
def file_count(self, obj):
"""Display the number of files in this submission."""
return obj.files.count()
file_count.short_description = "Files"
@admin.register(SubmissionFile)
class SubmissionFileAdmin(admin.ModelAdmin):
"""Admin interface for submission files."""
list_display = ["original_filename", "submission", "file_size", "uploaded_at"]
list_filter = ["uploaded_at"]
search_fields = ["original_filename", "submission__email"]
readonly_fields = ["uploaded_at", "file_size"]

6
quotes/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class QuotesConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "quotes"

109
quotes/email_utils.py Normal file
View File

@@ -0,0 +1,109 @@
"""
Email utilities for sending submission notifications.
"""
from django.core.mail import send_mail, EmailMultiAlternatives
from django.template.loader import render_to_string
from django.conf import settings
def send_submission_emails(submission):
"""
Send emails for a quote submission.
Sends:
1. Notification to owner (OWNER_EMAIL)
2. Confirmation to submitter (submission.email)
Returns tuple (owner_sent, submitter_sent)
"""
files = submission.files.all()
owner_email = getattr(settings, "OWNER_EMAIL", "")
owner_sent = False
submitter_sent = False
# Email 1: Notification to owner
if owner_email:
try:
subject = f"New Quote Request - Submission #{submission.id}"
html_message = render_to_string(
"emails/submission_notification_owner.html",
{
"submission": submission,
"files": files,
},
)
# Plain text fallback
text_message = f"""
New Quote Request Received!
Submitted by: {submission.email}
Submitted at: {submission.submitted_at.strftime('%B %d, %Y %I:%M %p')}
Submission ID: #{submission.id}
Description:
{submission.description or '(No description provided)'}
Files uploaded: {files.count()}
"""
for file in files:
text_message += f"- {file.original_filename} ({file.file_size} bytes)\n"
msg = EmailMultiAlternatives(
subject=subject,
body=text_message,
from_email=settings.DEFAULT_FROM_EMAIL,
to=[owner_email],
)
msg.attach_alternative(html_message, "text/html")
msg.send()
owner_sent = True
except Exception as e:
print(f"Error sending owner email: {e}")
# Email 2: Confirmation to submitter
try:
subject = "Quote Request Received - Sedutto Machineworks"
html_message = render_to_string(
"emails/submission_confirmation.html",
{
"submission": submission,
"files": files,
},
)
# Plain text fallback
text_message = f"""
Thank you for your quote request!
We have received your submission and will review it shortly.
Submission Details:
Submission ID: #{submission.id}
Submitted at: {submission.submitted_at.strftime('%B %d, %Y %I:%M %p')}
Your Notes:
{submission.description or '(No notes provided)'}
Files uploaded: {files.count()}
"""
for file in files:
text_message += f"- {file.original_filename} ({file.file_size} bytes)\n"
text_message += f"\nWe will contact you at {submission.email} regarding your quote request.\n"
text_message += f"\nThis submission will be kept on file until {submission.expiration_time.strftime('%B %d, %Y')}."
msg = EmailMultiAlternatives(
subject=subject,
body=text_message,
from_email=settings.DEFAULT_FROM_EMAIL,
to=[submission.email],
)
msg.attach_alternative(html_message, "text/html")
msg.send()
submitter_sent = True
except Exception as e:
print(f"Error sending submitter email: {e}")
return (owner_sent, submitter_sent)

View File

@@ -0,0 +1,112 @@
# Generated by Django 4.2.27 on 2025-12-03 23:22
from django.db import migrations, models
import django.db.models.deletion
import quotes.models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Submission",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"email",
models.EmailField(
help_text="Email of the person who submitted the quote request",
max_length=254,
),
),
(
"description",
models.TextField(
help_text="Notes, comments, or description provided by the submitter"
),
),
(
"submitted_at",
models.DateTimeField(
auto_now_add=True, help_text="When the submission was created"
),
),
(
"expiration_time",
models.DateTimeField(
default=quotes.models.default_expiration_time,
help_text="When this submission expires (default 20 days from submission)",
),
),
],
options={
"verbose_name": "Submission",
"verbose_name_plural": "Submissions",
"ordering": ["-submitted_at"],
},
),
migrations.CreateModel(
name="SubmissionFile",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"original_filename",
models.CharField(
help_text="Original filename as uploaded by the user",
max_length=255,
),
),
(
"path",
models.CharField(
help_text="Path relative to /data/uploads (e.g., 'filename.pdf')",
max_length=500,
),
),
(
"file_size",
models.PositiveIntegerField(help_text="Size of the file in bytes"),
),
(
"uploaded_at",
models.DateTimeField(
auto_now_add=True, help_text="When the file was uploaded"
),
),
(
"submission",
models.ForeignKey(
help_text="The submission this file belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="files",
to="quotes.submission",
),
),
],
options={
"verbose_name": "Submission File",
"verbose_name_plural": "Submission Files",
"ordering": ["uploaded_at"],
},
),
]

View File

121
quotes/models.py Normal file
View File

@@ -0,0 +1,121 @@
from django.db import models
from django.utils import timezone
from datetime import timedelta
from pathlib import Path
def default_expiration_time():
"""Default expiration time: 20 days from now."""
return timezone.now() + timedelta(days=20)
class Submission(models.Model):
"""Model for quote request submissions."""
email = models.EmailField(
help_text="Email of the person who submitted the quote request"
)
description = models.TextField(
help_text="Notes, comments, or description provided by the submitter"
)
submitted_at = models.DateTimeField(
auto_now_add=True, help_text="When the submission was created"
)
expiration_time = models.DateTimeField(
default=default_expiration_time,
help_text="When this submission expires (default 20 days from submission)",
)
class Meta:
ordering = ["-submitted_at"]
verbose_name = "Submission"
verbose_name_plural = "Submissions"
def __str__(self):
return f"Submission from {self.email} at {self.submitted_at.strftime('%Y-%m-%d %H:%M')}"
def is_expired(self):
"""Check if the submission has expired."""
return timezone.now() > self.expiration_time
def get_file_paths(self):
"""
Get list of all file paths relative to /data/uploads.
Returns list of relative paths as strings.
"""
return [file.path for file in self.files.all()]
def get_file_urls(self):
"""
Get list of URLs for all files in this submission.
Returns list of URLs that can be used to access the files.
"""
from django.conf import settings
from django.urls import reverse
urls = []
for file in self.files.all():
# Path relative to MEDIA_ROOT
relative_path = file.path
# Construct URL
url = f"{settings.MEDIA_URL}{relative_path}"
urls.append(url)
return urls
def get_absolute_file_paths(self):
"""
Get list of absolute file system paths for all files.
Returns list of absolute Path objects.
"""
from django.conf import settings
paths = []
for file in self.files.all():
# Path relative to MEDIA_ROOT/uploads
relative_path = file.path
# Construct absolute path
absolute_path = Path(settings.MEDIA_ROOT) / relative_path
paths.append(absolute_path)
return paths
class SubmissionFile(models.Model):
"""Model for files associated with a submission."""
submission = models.ForeignKey(
Submission,
on_delete=models.CASCADE,
related_name="files",
help_text="The submission this file belongs to",
)
original_filename = models.CharField(
max_length=255, help_text="Original filename as uploaded by the user"
)
path = models.CharField(
max_length=500,
help_text="Path relative to /data/uploads (e.g., 'filename.pdf')",
)
file_size = models.PositiveIntegerField(help_text="Size of the file in bytes")
uploaded_at = models.DateTimeField(
auto_now_add=True, help_text="When the file was uploaded"
)
class Meta:
ordering = ["uploaded_at"]
verbose_name = "Submission File"
verbose_name_plural = "Submission Files"
def __str__(self):
return f"{self.original_filename} ({self.submission.email})"
def get_absolute_path(self):
"""Get the absolute file system path."""
from django.conf import settings
return Path(settings.MEDIA_ROOT) / self.path
def get_url(self):
"""Get the URL to access this file."""
from django.conf import settings
return f"{settings.MEDIA_URL}{self.path}"