first version of send mail with dummy backend and confirm action

This commit is contained in:
2025-12-06 18:52:03 +01:00
parent 7c7d0e1e62
commit 42417927c9
10 changed files with 525 additions and 8 deletions

View File

@@ -31,6 +31,7 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'admin_confirm',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@@ -143,3 +144,14 @@ STATIC_URL = 'static/'
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
#### Send Email for user password communication ####
# https://docs.djangoproject.com/en/5.2/topics/email/
EMAIL_HOST = "smtp.gmail.com"
EMAIL_HOST_USER = "ema.trabattoni@gmail.com"
EMAIL_HOST_PASSWORD = "okorjsenzptdiwcr"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
EMAIL_FILE_PATH = "/tmp/app-messages" # change this to a proper location

View File

@@ -35,6 +35,7 @@ CSRF_TRUSTED_ORIGINS = ["http://localhost:8000", "http://127.0.0.1:8000", "http:
# Application definition
INSTALLED_APPS = [
'admin_confirm',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@@ -144,3 +145,14 @@ STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
#### Send Email for user password communication ####
# https://docs.djangoproject.com/en/5.2/topics/email/
EMAIL_HOST = "smtp.gmail.com"
EMAIL_HOST_USER = "ema.trabattoni@gmail.com"
EMAIL_HOST_PASSWORD = "okorjsenzptdiwcr"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
# Use dummy backed for testing
EMAIL_BACKEND = "django.core.mail.backends..dummy.EmailBackend"

View File

@@ -0,0 +1,62 @@
from django.conf import settings
from django.contrib import messages
from django.core.mail import EmailMultiAlternatives
from django.http import HttpRequest
from django.db.models.query import QuerySet
from django.utils.safestring import SafeText
from django.template.loader import render_to_string
from ..models.students import Student
from smtplib import SMTPException
from email.mime.image import MIMEImage
import os
def send_mail_password(request: HttpRequest, queryset: QuerySet[Student]) -> None:
img: MIMEImage | None = None
try:
for d in settings.STATICFILES_DIRS:
filename = os.path.join(d, "cantorair.png")
if not os.path.exists(filename):
continue
with open(filename, "rb") as f:
img = MIMEImage(f.read())
img.add_header("Content-ID", "logo_image")
img.add_header("Content-Disposition", "inline", filename="cantorair.png")
break
except:
messages.error(request=request, message="Cannot Load CantorAir Logo")
return
for student in queryset:
if not student.user or not student.email: # skip student if has not an associated user
continue
try:
username: str = student.user.username
password: str = student.default_password()
address: str = student.email
text_message: str = f"Cantor Air Flight Scheduler\nUsername:{username}\nPassword:{password}\n"
html_message: SafeText = render_to_string(
template_name="email/mail.html",
context={"username": username, "password": password}
)
mail: EmailMultiAlternatives = EmailMultiAlternatives(
subject="CantorAir Flight Scheduler 🛫",
from_email="ema.trabattoni@gmail.com",
body=text_message,
to = [ address ]
)
mail.attach(filename=img)
mail.attach_alternative(content=html_message, mimetype="text/html")
mail.send()
except SMTPException as e:
messages.error(request=request, message=f"Send Mail error: {e.strerror}")
except Exception as e:
messages.error(request=request, message=f"General Error: {e}")
else:
messages.success(request=request, message=f"Email sent to {student.surname} {student.name[0].upper()}. -> {mail.to.pop()}")
return

View File

@@ -10,13 +10,15 @@ from import_export.tmp_storages import CacheStorage
from import_export.resources import ModelResource
from import_export.forms import ConfirmImportForm, ImportForm
from django_admin_action_forms import AdminActionFormsMixin, AdminActionForm, action_with_form
from django_admin_action_forms import AdminActionFormsMixin, AdminActionForm, ActionForm, action_with_form
from admin_confirm import AdminConfirmMixin, confirm_action
from ..models.aircrafts import AircraftTypes
from ..models.courses import Course
from ..models.students import Student
from ..actions.assign_aircraft import assign_aircraft
from ..actions.send_email import send_mail_password
from ..custom.colortag import course_color
@@ -64,17 +66,19 @@ class ChangeCourseForm(AdminActionForm):
class ChangeAircraftForm(AdminActionForm):
aircrafts = TypedMultipleChoiceField(choices=AircraftTypes)
class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
class StudentAdmin(ImportMixin, AdminConfirmMixin, AdminActionFormsMixin, admin.ModelAdmin):
model = Student
list_display = ("surname", "name", "course", "course_color", "email", "phone", "username", "password", "active", )
list_filter = ("course", "active", )
search_fields = ("surname", "name", "phone", "email", )
actions = ("change_course", "deactivate_students", "change_aircraft", )
actions = ("change_course", "deactivate_students", "change_aircraft", "send_mail", )
resource_classes = [StudentResource]
confirm_form_class = StudentCustomConfirmImportForm
tmp_storage_class = CacheStorage
skip_admin_log = True
@admin.display(description="Color")
def course_color(self, obj: Student) -> SafeText:
if not obj.course:
@@ -111,6 +115,11 @@ class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
i, ac_types = assign_aircraft(queryset=queryset, data=data)
messages.success(request, f"{i} Students updated to {ac_types}")
@admin.action(description="Send Access Credentials e-mail")
@confirm_action
def send_mail(self, request: HttpRequest, queryset: QuerySet[Student], data: Any) -> None:
send_mail_password(request=request, queryset=queryset)
# Return the initial form for import confirmations, request course to user
def get_confirm_form_initial(self, request: HttpRequest, import_form) -> Dict[str, Any]:
initial: Dict[str, Any] = super().get_confirm_form_initial(request, import_form)

17
cntmanage/poetry.lock generated
View File

@@ -66,6 +66,21 @@ files = [
[package.dependencies]
django = ">=3.2"
[[package]]
name = "django-admin-confirm"
version = "1.0.1"
description = "Adds confirmation to Django Admin changes, additions and actions"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "django-admin-confirm-1.0.1.tar.gz", hash = "sha256:fb6a4b7cb9fc6ccd97f92f88275ee8e3912f3ee0ce82da962ad0a2b1b17cd6a0"},
{file = "django_admin_confirm-1.0.1-py3-none-any.whl", hash = "sha256:271a7135e8e5f0cce94a6c06f708dec794d3538ada37e111c1f8d7c8f762b012"},
]
[package.dependencies]
Django = ">=3.2"
[[package]]
name = "django-colorfield"
version = "0.14.0"
@@ -468,4 +483,4 @@ files = [
[metadata]
lock-version = "2.1"
python-versions = "^3.12"
content-hash = "5147211bd07992aff3915544175c8d95d77511b9d42273d17c4452fbef9299eb"
content-hash = "b45301c627836abac1ef9628e67fc63189b03e7857a7a003854aa1fb30f2a4a3"

View File

@@ -19,6 +19,7 @@ openpyxl = "^3.1.5"
django-admin-action-forms = "^2.2.1"
django-polymorphic = "^4.1.0"
django-phonenumber-field = {extras = ["phonenumberslite"], version = "^8.4.0"}
django-admin-confirm = "^1.0.1"
[build-system]

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -1 +1 @@
admin: CantorAdmin2k25
admin: CantorAir2k25

View File

@@ -0,0 +1,408 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office"
>
<head>
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width: 520px) {
.u-row {
width: 500px !important;
}
.u-row .u-col {
vertical-align: top;
}
.u-row .u-col-100 {
width: 500px !important;
}
}
@media only screen and (max-width: 520px) {
.u-row-container {
max-width: 100% !important;
padding-left: 0px !important;
padding-right: 0px !important;
}
.u-row {
width: 100% !important;
}
.u-row .u-col {
display: block !important;
width: 100% !important;
min-width: 320px !important;
max-width: 100% !important;
}
.u-row .u-col > div {
margin: 0 auto;
}
}
body {
margin: 0;
padding: 0;
}
table,
td,
tr {
border-collapse: collapse;
vertical-align: top;
}
.ie-container table,
.mso-container table {
table-layout: fixed;
}
* {
line-height: inherit;
}
a[x-apple-data-detectors="true"] {
color: inherit !important;
text-decoration: none !important;
}
table,
td {
color: #000000;
}
</style>
</head>
<body
class="clean-body u_body"
style="
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
background-color: #f7f8f9;
color: #000000;
"
>
<!--[if IE]><div class="ie-container"><![endif]-->
<!--[if mso]><div class="mso-container"><![endif]-->
<table
role="presentation"
id="u_body"
style="
border-collapse: collapse;
table-layout: fixed;
border-spacing: 0;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
vertical-align: top;
min-width: 320px;
margin: 0 auto;
background-color: #f7f8f9;
width: 100%;
"
cellpadding="0"
cellspacing="0"
>
<tbody>
<tr style="vertical-align: top">
<td
style="
word-break: break-word;
border-collapse: collapse !important;
vertical-align: top;
"
>
<!--[if (mso)|(IE)]><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color: #F7F8F9;"><![endif]-->
<div
class="u-row-container"
style="padding: 0px; background-color: transparent"
>
<div
class="u-row"
style="
margin: 0 auto;
min-width: 320px;
max-width: 500px;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
background-color: transparent;
"
>
<div
style="
border-collapse: collapse;
display: table;
width: 100%;
height: 100%;
background-color: transparent;
"
>
<!--[if (mso)|(IE)]><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;" align="center"><table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:500px;"><tr style="background-color: transparent;"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="500" style="width: 500px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;" valign="top"><![endif]-->
<div
class="u-col u-col-100"
style="
max-width: 320px;
min-width: 500px;
display: table-cell;
vertical-align: top;
"
>
<div
style="
height: 100%;
width: 100% !important;
border-radius: 0px;
-webkit-border-radius: 0px;
-moz-border-radius: 0px;
"
>
<!--[if (!mso)&(!IE)]><!--><div
style="
box-sizing: border-box;
height: 100%;
padding: 0px;
border-top: 0px solid transparent;
border-left: 0px solid transparent;
border-right: 0px solid transparent;
border-bottom: 0px solid transparent;
border-radius: 0px;
-webkit-border-radius: 0px;
-moz-border-radius: 0px;
"
><!--<![endif]-->
<table
style="font-family: arial, helvetica, sans-serif"
role="presentation"
cellpadding="0"
cellspacing="0"
width="100%"
border="0"
>
<tbody>
<tr>
<td
style="
overflow-wrap: break-word;
word-break: break-word;
padding: 10px;
font-family: arial, helvetica, sans-serif;
"
align="left"
>
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
border="0"
>
<tr>
<td
style="
padding-right: 0px;
padding-left: 0px;
"
align="center"
>
<a
href="https://www.cantorair.it"
target="_blank"
style="
color: rgb(0, 0, 238);
text-decoration: underline;
line-height: inherit;
"
><img
align="center"
border="0"
src="cid:logo_image"
alt="Cantor Air Logo"
title=""
style="
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
clear: both;
display: inline-block !important;
border: none;
height: auto;
float: none;
width: 50%;
max-width: 240px;
"
width="240"
/>
</a>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
<table
style="font-family: arial, helvetica, sans-serif"
role="presentation"
cellpadding="0"
cellspacing="0"
width="100%"
border="0"
>
<tbody>
<tr>
<td
style="
overflow-wrap: break-word;
word-break: break-word;
padding: 10px;
font-family: arial, helvetica, sans-serif;
"
align="left"
>
<!--[if mso]><table role="presentation" width="100%"><tr><td><![endif]-->
<h1
style="
margin: 0px;
line-height: 140%;
text-align: center;
word-wrap: break-word;
font-size: 22px;
font-weight: 400;
"
>
<span
>✈️ Welcome to CantorAir Flight Scheduler
✈️</span
>
</h1>
<!--[if mso]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<table
style="font-family: arial, helvetica, sans-serif"
role="presentation"
cellpadding="0"
cellspacing="0"
width="100%"
border="0"
>
<tbody>
<tr>
<td
style="
overflow-wrap: break-word;
word-break: break-word;
padding: 10px;
font-family: arial, helvetica, sans-serif;
"
align="left"
>
<div
style="
font-size: 14px;
line-height: 140%;
text-align: left;
word-wrap: break-word;
"
>
<p
style="
text-align: center;
line-height: inherit;
margin: 0px;
"
>
<span
>You can access the flight scheduler
website with this link: </span
><br /><a href="https://cms.etss.it/user"
><span>https://cms.etss.it</span></a
>
</p>
<p style="text-align: center"><br /></p>
<p dir="ltr" style="text-align: center">
<span style="font-weight: bold"
>Your access credentials:</span
>
</p>
<p dir="ltr" style="text-align: center">
<br /><span>Username: {{username}}</span
><br /><span>Password: {{password}}</span>
</p>
<p dir="ltr" style="text-align: center">
<br />
</p>
<p dir="ltr" style="text-align: center">
<span
>Flight Scheduling is available from </span
><br /><span style="font-weight: bold"
>Monday to Tuesday until 15.00</span
>
</p>
<p style="text-align: center">
<br /><span>_______________________</span>
</p>
<p dir="ltr" style="text-align: center">
<span style="font-style: italic">Team</span
><br /><span style="font-weight: bold"
>CANTOR AIR IT.ATO.0004</span
>
</p>
<p dir="ltr" style="text-align: center">
<span>&nbsp;Email: </span
><a
href="mailto:info@cantorair.it"
target="_blank"
style="line-height: 115%"
><span>info@cantorair.it</span></a
><br /><span>Phone: +39 035 520035</span
><br /><span>_______________________</span>
</p>
</div>
</td>
</tr>
</tbody>
</table>
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<!--[if mso]></div><![endif]-->
<!--[if IE]></div><![endif]-->
</body>
</html>