From 42417927c93e0eb7959979d27de086aa13dc2535 Mon Sep 17 00:00:00 2001 From: Emanuele Date: Sat, 6 Dec 2025 18:52:03 +0100 Subject: [PATCH] first version of send mail with dummy backend and confirm action --- cntmanage/cntmanage/settings.py | 12 + cntmanage/cntmanage/settings_prod.py | 12 + cntmanage/flightslot/actions/send_email.py | 62 ++++ cntmanage/flightslot/admins/student_adm.py | 15 +- cntmanage/poetry.lock | 17 +- cntmanage/pyproject.toml | 1 + cntmanage/static/cantorair.png | Bin 0 -> 6737 bytes cntmanage/static/password | 2 +- cntmanage/templates/email/mail.html | 408 +++++++++++++++++++++ note.txt | 4 +- 10 files changed, 525 insertions(+), 8 deletions(-) create mode 100644 cntmanage/flightslot/actions/send_email.py create mode 100644 cntmanage/static/cantorair.png create mode 100644 cntmanage/templates/email/mail.html diff --git a/cntmanage/cntmanage/settings.py b/cntmanage/cntmanage/settings.py index 15c1c07..ee47d6b 100644 --- a/cntmanage/cntmanage/settings.py +++ b/cntmanage/cntmanage/settings.py @@ -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 diff --git a/cntmanage/cntmanage/settings_prod.py b/cntmanage/cntmanage/settings_prod.py index a1f4808..686b723 100644 --- a/cntmanage/cntmanage/settings_prod.py +++ b/cntmanage/cntmanage/settings_prod.py @@ -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" diff --git a/cntmanage/flightslot/actions/send_email.py b/cntmanage/flightslot/actions/send_email.py new file mode 100644 index 0000000..aa90380 --- /dev/null +++ b/cntmanage/flightslot/actions/send_email.py @@ -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 \ No newline at end of file diff --git a/cntmanage/flightslot/admins/student_adm.py b/cntmanage/flightslot/admins/student_adm.py index efb7c12..3cd85f1 100644 --- a/cntmanage/flightslot/admins/student_adm.py +++ b/cntmanage/flightslot/admins/student_adm.py @@ -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) diff --git a/cntmanage/poetry.lock b/cntmanage/poetry.lock index 3efef29..f3b6af5 100644 --- a/cntmanage/poetry.lock +++ b/cntmanage/poetry.lock @@ -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" diff --git a/cntmanage/pyproject.toml b/cntmanage/pyproject.toml index 35b3e97..8e2a178 100644 --- a/cntmanage/pyproject.toml +++ b/cntmanage/pyproject.toml @@ -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] diff --git a/cntmanage/static/cantorair.png b/cntmanage/static/cantorair.png new file mode 100644 index 0000000000000000000000000000000000000000..18e68cc23ad29bf8e854ae8b013e9908a1ba7a26 GIT binary patch literal 6737 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 GNB%#;IpstE literal 0 HcmV?d00001 diff --git a/cntmanage/static/password b/cntmanage/static/password index 667feba..520af21 100644 --- a/cntmanage/static/password +++ b/cntmanage/static/password @@ -1 +1 @@ -admin: CantorAdmin2k25 +admin: CantorAir2k25 diff --git a/cntmanage/templates/email/mail.html b/cntmanage/templates/email/mail.html new file mode 100644 index 0000000..b9bfc9d --- /dev/null +++ b/cntmanage/templates/email/mail.html @@ -0,0 +1,408 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/note.txt b/note.txt index bcc57c4..bc7eaa7 100644 --- a/note.txt +++ b/note.txt @@ -1,7 +1,5 @@ OK leg delle hb all'interno del riquadro per i giorni OK aereo assegnato allo studente di fianco al numero della missione per PPL, invece per CPL e IR e HB il tipo va di fiaco al nome dello studente OK le missioni ripetute su piu' giorni hanno una cella per giorno (non unite) -lo studente vede solo le missioni della sua fase PPL->PPL ATPL-> tutto +OK lo studente vede solo le missioni della sua fase PPL->PPL ATPL-> tutto OK ogni richiesta ha un colore diverso che cicla con delle tinte pastello - -lakkhfashdfkajsdfhlkjh \ No newline at end of file