Compare commits
3 Commits
0183b03bc2
...
53c6fad262
| Author | SHA1 | Date |
|---|---|---|
|
|
53c6fad262 | |
|
|
c97b06e07b | |
|
|
2836d5954e |
|
|
@ -36,6 +36,7 @@ db.sqlite3
|
|||
db.sqlite3-journal
|
||||
/media
|
||||
/staticfiles
|
||||
/local
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
|
|
|
|||
|
|
@ -14,4 +14,9 @@ COPY . /app
|
|||
# Collect static files for WhiteNoise
|
||||
RUN python manage.py collectstatic --noinput
|
||||
|
||||
# Make entrypoint script executable
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["gunicorn", "seduttomachineworks_project.wsgi:application", "--bind", "0.0.0.0:8000"]
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ services:
|
|||
web_upload:
|
||||
image: git.kitsunehosting.net/kenwood/smw-upload:latest
|
||||
volumes:
|
||||
- /var/seduttomachineworks/data:/app/store
|
||||
- /var/seduttomachineworks/data:/app/data
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Run migrations
|
||||
echo "Running database migrations..."
|
||||
python manage.py migrate --noinput
|
||||
|
||||
# Start the application
|
||||
echo "Starting application..."
|
||||
exec "$@"
|
||||
|
||||
|
|
@ -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"]
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class QuotesConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "quotes"
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
"""
|
||||
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 get_file_urls_with_base_url(files):
|
||||
"""
|
||||
Get absolute URLs for files.
|
||||
Constructs full URLs using ALLOWED_HOSTS from settings.
|
||||
"""
|
||||
# Get base URL from ALLOWED_HOSTS
|
||||
allowed_hosts = getattr(settings, "ALLOWED_HOSTS", [])
|
||||
if allowed_hosts and allowed_hosts[0] != "*":
|
||||
# Use first allowed host
|
||||
domain = allowed_hosts[0]
|
||||
# Check if it's localhost or IP for HTTP
|
||||
if (
|
||||
domain.startswith("127.0.0.1")
|
||||
or domain.startswith("localhost")
|
||||
or domain.startswith("10.")
|
||||
):
|
||||
# In DEBUG mode, add port 8000 (default Django dev server)
|
||||
if settings.DEBUG:
|
||||
base_url = f"http://{domain}:8000"
|
||||
else:
|
||||
base_url = f"http://{domain}"
|
||||
else:
|
||||
base_url = f"https://{domain}"
|
||||
else:
|
||||
# Fallback: use localhost with port in DEBUG mode
|
||||
if settings.DEBUG:
|
||||
base_url = "http://127.0.0.1:8000"
|
||||
else:
|
||||
base_url = ""
|
||||
|
||||
file_urls = []
|
||||
for file in files:
|
||||
# In DEBUG mode, use MEDIA_URL (WhiteNoise serves it)
|
||||
# In production, use /uploads/ (nginx serves it)
|
||||
if settings.DEBUG:
|
||||
# Use MEDIA_URL which is "media/"
|
||||
media_url = settings.MEDIA_URL.rstrip("/")
|
||||
file_path = f"/{media_url}/{file.path}"
|
||||
else:
|
||||
# Use /uploads/ for nginx
|
||||
file_path = f"/uploads/{file.path}"
|
||||
|
||||
if base_url:
|
||||
# Make it an absolute URL
|
||||
file_url = base_url + file_path
|
||||
else:
|
||||
# Fallback: relative URL
|
||||
file_url = file_path
|
||||
|
||||
file_urls.append(
|
||||
{
|
||||
"file": file,
|
||||
"url": file_url,
|
||||
}
|
||||
)
|
||||
return file_urls
|
||||
|
||||
|
||||
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()
|
||||
file_urls = get_file_urls_with_base_url(files)
|
||||
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,
|
||||
"file_urls": file_urls,
|
||||
},
|
||||
)
|
||||
|
||||
# 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_url_data in file_urls:
|
||||
file = file_url_data["file"]
|
||||
url = file_url_data["url"]
|
||||
text_message += f"- {file.original_filename} ({file.file_size} bytes)\n"
|
||||
text_message += f" Download: {url}\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,
|
||||
"file_urls": file_urls,
|
||||
},
|
||||
)
|
||||
|
||||
# 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_url_data in file_urls:
|
||||
file = file_url_data["file"]
|
||||
url = file_url_data["url"]
|
||||
text_message += f"- {file.original_filename} ({file.file_size} bytes)\n"
|
||||
text_message += f" Download: {url}\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)
|
||||
|
|
@ -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,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}"
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
"""
|
||||
Email configuration parser and utilities.
|
||||
Parses EMAIL_CONFIG environment variable in format:
|
||||
smtp://<user>@kitsunehosting.net:<password>@smtp.forwardemail.net:465
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from urllib.parse import urlparse, unquote
|
||||
|
||||
|
||||
def parse_email_config(email_config_str):
|
||||
"""
|
||||
Parse EMAIL_CONFIG string into email settings.
|
||||
|
||||
Format: smtp://<user>@kitsunehosting.net:<password>@smtp.forwardemail.net:465
|
||||
|
||||
Returns dict with: user, password, host, port, from_email
|
||||
"""
|
||||
if not email_config_str:
|
||||
return None
|
||||
|
||||
# Parse the URL
|
||||
# Format: smtp://user@domain:password@host:port
|
||||
# We need to handle the @ symbols carefully
|
||||
match = re.match(r"smtp://([^:]+):([^@]+)@([^:]+):(\d+)", email_config_str)
|
||||
if not match:
|
||||
raise ValueError(f"Invalid EMAIL_CONFIG format: {email_config_str}")
|
||||
|
||||
user_with_domain = match.group(1)
|
||||
password = unquote(match.group(2))
|
||||
host = match.group(3)
|
||||
port = int(match.group(4))
|
||||
|
||||
# Extract email address (user@domain)
|
||||
from_email = user_with_domain
|
||||
|
||||
return {
|
||||
"user": user_with_domain,
|
||||
"password": password,
|
||||
"host": host,
|
||||
"port": port,
|
||||
"from_email": from_email,
|
||||
}
|
||||
|
||||
|
||||
def get_email_config():
|
||||
"""Get email configuration from environment."""
|
||||
email_config_str = os.environ.get("EMAIL_CONFIG")
|
||||
if not email_config_str:
|
||||
return None
|
||||
return parse_email_config(email_config_str)
|
||||
|
|
@ -3,6 +3,8 @@ import time
|
|||
from pathlib import Path
|
||||
from django.http import StreamingHttpResponse, JsonResponse
|
||||
from django.conf import settings
|
||||
from django.core.validators import validate_email
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
|
|
@ -33,40 +35,83 @@ def submit_quote(request):
|
|||
|
||||
def process_quote():
|
||||
try:
|
||||
if not settings.DEBUG:
|
||||
raise Exception("Not implemented")
|
||||
# Step 1: Validate email
|
||||
yield send_progress_update(1, "Validating email address...", 5)
|
||||
|
||||
# Step 1: Upload files
|
||||
yield send_progress_update(1, "Uploading files...", 25)
|
||||
# Files are already uploaded at this point, just acknowledge
|
||||
uploaded_files = []
|
||||
for file in files:
|
||||
uploaded_files.append({"name": file.name, "size": file.size})
|
||||
if not email:
|
||||
raise ValueError("Email is required")
|
||||
|
||||
# Step 2: Save files
|
||||
yield send_progress_update(2, "Saving files...", 50)
|
||||
# Save files to disk/storage
|
||||
saved_paths = []
|
||||
for file in files:
|
||||
# Create store directory if it doesn't exist
|
||||
store_dir = Path(settings.MEDIA_ROOT) / "uploads"
|
||||
store_dir.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError:
|
||||
raise ValueError(f"Invalid email address: {email}")
|
||||
|
||||
# Save file
|
||||
file_path = store_dir / file.name
|
||||
with open(file_path, "wb") as f:
|
||||
yield send_progress_update(1, "Email validated successfully", 10)
|
||||
|
||||
# Step 2: Create submission & upload files
|
||||
from quotes.models import Submission, SubmissionFile
|
||||
|
||||
# Create a Submission entry
|
||||
submission = Submission.objects.create(
|
||||
email=email,
|
||||
description=notes,
|
||||
)
|
||||
|
||||
yield send_progress_update(2, "Created submission entry...", 15)
|
||||
|
||||
# Save each file and register in the model
|
||||
total_files = len(files)
|
||||
file_objs = []
|
||||
for idx, file in enumerate(files, start=1):
|
||||
# Use the original filename and save to MEDIA_ROOT/submission_<id>_filename
|
||||
submission_dir = (
|
||||
Path(settings.MEDIA_ROOT) / f"submission_{submission.id}"
|
||||
)
|
||||
submission_dir.mkdir(parents=True, exist_ok=True)
|
||||
target_path = submission_dir / file.name
|
||||
|
||||
with open(target_path, "wb") as f:
|
||||
for chunk in file.chunks():
|
||||
f.write(chunk)
|
||||
saved_paths.append(str(file_path))
|
||||
|
||||
# Step 3: Send emails
|
||||
yield send_progress_update(3, "Sending emails 1/2...", 70)
|
||||
time.sleep(0.5)
|
||||
# TODO: Actually send emails
|
||||
yield send_progress_update(4, "Sending emails 2/2...", 75)
|
||||
time.sleep(0.5)
|
||||
# TODO: Actually send emails
|
||||
emails_sent = True # Placeholder
|
||||
# Register file in SubmissionFile model
|
||||
rel_path = (Path(f"submission_{submission.id}") / file.name).as_posix()
|
||||
submission_file = SubmissionFile.objects.create(
|
||||
submission=submission,
|
||||
original_filename=file.name,
|
||||
path=rel_path,
|
||||
file_size=file.size,
|
||||
)
|
||||
file_objs.append(submission_file)
|
||||
yield send_progress_update(
|
||||
2,
|
||||
f"Uploaded file {idx} of {total_files}: {file.name}",
|
||||
15 + int((idx / total_files) * 30),
|
||||
)
|
||||
|
||||
yield send_progress_update(3, "Files saved...", 50)
|
||||
|
||||
# Step 4: Send emails
|
||||
from quotes.email_utils import send_submission_emails
|
||||
|
||||
yield send_progress_update(4, "Sending notification email...", 70)
|
||||
owner_sent, submitter_sent = send_submission_emails(submission)
|
||||
time.sleep(0.3)
|
||||
|
||||
yield send_progress_update(4, "Sending confirmation email...", 75)
|
||||
time.sleep(0.3)
|
||||
|
||||
# Verify emails were sent
|
||||
if not owner_sent and getattr(settings, "OWNER_EMAIL", ""):
|
||||
yield send_progress_update(
|
||||
"warning",
|
||||
"Warning: Owner notification email may not have been sent",
|
||||
75,
|
||||
)
|
||||
if not submitter_sent:
|
||||
yield send_progress_update(
|
||||
"warning", "Warning: Confirmation email may not have been sent", 75
|
||||
)
|
||||
|
||||
# Complete
|
||||
yield send_progress_update(
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ INSTALLED_APPS = [
|
|||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"quotes",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
|
@ -63,10 +64,23 @@ WSGI_APPLICATION = "seduttomachineworks_project.wsgi.application"
|
|||
# Database
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||
|
||||
# Use local/data directory in DEBUG mode, otherwise use /app/data
|
||||
if DEBUG:
|
||||
DATA_DIR = BASE_DIR / "local" / "data"
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
"NAME": DATA_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
else:
|
||||
DATA_DIR = Path("/app/data")
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": DATA_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +90,7 @@ DATABASES = {
|
|||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "UTC"
|
||||
TIME_ZONE = "America/New_York"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
|
|
@ -94,7 +108,17 @@ STATICFILES_DIRS = [BASE_DIR / "static"]
|
|||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||
|
||||
MEDIA_URL = "media/"
|
||||
MEDIA_ROOT = BASE_DIR / "media"
|
||||
# Use local/data/uploads in DEBUG mode, otherwise use /app/data/uploads
|
||||
if DEBUG:
|
||||
DATA_DIR = BASE_DIR / "local" / "data"
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
MEDIA_ROOT = DATA_DIR / "uploads"
|
||||
MEDIA_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
DATA_DIR = Path("/app/data")
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
MEDIA_ROOT = DATA_DIR / "uploads"
|
||||
MEDIA_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
|
@ -103,3 +127,29 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
|||
|
||||
# Allow iframe embedding (for the quoting upload box)
|
||||
# Note: We'll handle this per-view using @xframe_options_exempt decorator
|
||||
|
||||
# Email configuration
|
||||
EMAIL_CONFIG = None
|
||||
try:
|
||||
from .email_config import get_email_config
|
||||
|
||||
email_config = get_email_config()
|
||||
if email_config:
|
||||
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
||||
EMAIL_HOST = email_config["host"]
|
||||
EMAIL_PORT = email_config["port"]
|
||||
EMAIL_USE_SSL = True # Port 465 uses SSL
|
||||
EMAIL_HOST_USER = email_config["user"]
|
||||
EMAIL_HOST_PASSWORD = email_config["password"]
|
||||
DEFAULT_FROM_EMAIL = email_config["from_email"]
|
||||
SERVER_EMAIL = email_config["from_email"]
|
||||
EMAIL_CONFIG = email_config
|
||||
else:
|
||||
# Fallback to console backend for development
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
except Exception as e:
|
||||
# If email config fails, use console backend
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
|
||||
# Owner email for notifications
|
||||
OWNER_EMAIL = os.environ.get("OWNER_EMAIL", "")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #0066cc;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: #f9f9f9;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background-color: white;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
border-left: 4px solid #0066cc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.file-list li {
|
||||
padding: 8px;
|
||||
background-color: #f0f0f0;
|
||||
margin: 5px 0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.file-list li a {
|
||||
color: #0066cc;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.file-list li a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Quote Request Received</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Thank you for your quote request! We have received your submission and will review it shortly.</p>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>Submission Details:</strong><br>
|
||||
<strong>Submission ID:</strong> #{{ submission.id }}<br>
|
||||
<strong>Submitted at:</strong> {{ submission.submitted_at|date:"F d, Y g:i A" }}
|
||||
</div>
|
||||
|
||||
{% if submission.description %}
|
||||
<div class="info-box">
|
||||
<strong>Your Notes:</strong><br>
|
||||
{{ submission.description|linebreaks }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if files %}
|
||||
<div class="info-box">
|
||||
<strong>Files You Uploaded ({{ files|length }}):</strong>
|
||||
<ul class="file-list">
|
||||
{% for file_url_data in file_urls %}
|
||||
<li>
|
||||
<a href="{{ file_url_data.url }}" target="_blank">{{ file_url_data.file.original_filename }}</a>
|
||||
({{ file_url_data.file.file_size|filesizeformat }})
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p>We will contact you at <strong>{{ submission.email }}</strong> regarding your quote request.</p>
|
||||
|
||||
<p><em>This submission will be kept on file until {{ submission.expiration_time|date:"F d, Y" }}.</em></p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Sedutto Machineworks</p>
|
||||
<p>Thank you for your interest in our services!</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #0066cc;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: #f9f9f9;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background-color: white;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
border-left: 4px solid #0066cc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.file-list li {
|
||||
padding: 8px;
|
||||
background-color: #f0f0f0;
|
||||
margin: 5px 0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.file-list li a {
|
||||
color: #0066cc;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.file-list li a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>New Quote Request Submission</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>HUZZAHH! A new quote request has been submitted!</p>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>Submitted by:</strong> {{ submission.email }}<br>
|
||||
<strong>Submitted at:</strong> {{ submission.submitted_at|date:"F d, Y g:i A" }}<br>
|
||||
<strong>Submission ID:</strong> #{{ submission.id }}
|
||||
</div>
|
||||
|
||||
{% if submission.description %}
|
||||
<div class="info-box">
|
||||
<strong>Description/Notes:</strong><br>
|
||||
{{ submission.description|linebreaks }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if files %}
|
||||
<div class="info-box">
|
||||
<strong>Uploaded Files ({{ files|length }}):</strong>
|
||||
<ul class="file-list">
|
||||
{% for file_url_data in file_urls %}
|
||||
<li>
|
||||
<a href="{{ file_url_data.url }}" target="_blank">{{ file_url_data.file.original_filename }}</a>
|
||||
({{ file_url_data.file.file_size|filesizeformat }})
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p><strong>Note:</strong> This submission will expire on {{ submission.expiration_time|date:"F d, Y g:i A" }}.
|
||||
(But who knows when joe will actually delete it)</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Sedutto Machineworks - Joe is tottally awesome</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Loading…
Reference in New Issue