Email config, email template and more!
This commit is contained in:
0
quotes/__init__.py
Normal file
0
quotes/__init__.py
Normal file
43
quotes/admin.py
Normal file
43
quotes/admin.py
Normal 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
6
quotes/apps.py
Normal 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
109
quotes/email_utils.py
Normal 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)
|
||||
112
quotes/migrations/0001_initial.py
Normal file
112
quotes/migrations/0001_initial.py
Normal 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"],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
quotes/migrations/__init__.py
Normal file
0
quotes/migrations/__init__.py
Normal file
121
quotes/models.py
Normal file
121
quotes/models.py
Normal 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}"
|
||||
Reference in New Issue
Block a user