From 7c7d0e1e62acb9c08ae934283b72f111e9c75d82 Mon Sep 17 00:00:00 2001 From: Emanuele Date: Sat, 6 Dec 2025 15:03:11 +0100 Subject: [PATCH 1/5] fake commit --- note.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/note.txt b/note.txt index 1562a56..bcc57c4 100644 --- a/note.txt +++ b/note.txt @@ -4,3 +4,4 @@ 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 ogni richiesta ha un colore diverso che cicla con delle tinte pastello +lakkhfashdfkajsdfhlkjh \ No newline at end of file -- 2.49.1 From 42417927c93e0eb7959979d27de086aa13dc2535 Mon Sep 17 00:00:00 2001 From: Emanuele Date: Sat, 6 Dec 2025 18:52:03 +0100 Subject: [PATCH 2/5] 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 -- 2.49.1 From c93171dbc35156f64f297fef8b7092dd56b2188a Mon Sep 17 00:00:00 2001 From: Emanuele Date: Sat, 6 Dec 2025 19:10:46 +0100 Subject: [PATCH 3/5] fix endfile spacing --- cntmanage/flightslot/actions/send_email.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cntmanage/flightslot/actions/send_email.py b/cntmanage/flightslot/actions/send_email.py index aa90380..0aa5ac2 100644 --- a/cntmanage/flightslot/actions/send_email.py +++ b/cntmanage/flightslot/actions/send_email.py @@ -59,4 +59,4 @@ def send_mail_password(request: HttpRequest, queryset: QuerySet[Student]) -> Non 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 + return -- 2.49.1 From ec7ae4a48e8652125286871eb49036937f2932af Mon Sep 17 00:00:00 2001 From: Emanuele Date: Sat, 6 Dec 2025 19:14:10 +0100 Subject: [PATCH 4/5] fix message --- cntmanage/flightslot/actions/send_email.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cntmanage/flightslot/actions/send_email.py b/cntmanage/flightslot/actions/send_email.py index 0aa5ac2..06a6a35 100644 --- a/cntmanage/flightslot/actions/send_email.py +++ b/cntmanage/flightslot/actions/send_email.py @@ -58,5 +58,5 @@ def send_mail_password(request: HttpRequest, queryset: QuerySet[Student]) -> Non 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()}") + messages.success(request=request, message=f"Email Sent To: {student.surname} {student.name[0].upper()}. -> {mail.to.pop()}") return -- 2.49.1 From a9587776e8136774dc22064f75298d48e77c1a72 Mon Sep 17 00:00:00 2001 From: Emanuele Date: Tue, 9 Dec 2025 10:45:25 +0100 Subject: [PATCH 5/5] Fixed static and template email files for container --- cntmanage/cntmanage/settings.py | 2 +- cntmanage/cntmanage/settings_prod.py | 7 ++- cntmanage/docker/flightslot.Dockerfile | 5 ++- cntmanage/flightslot/actions/send_email.py | 51 +++++++++++++--------- cntmanage/flightslot/admins/student_adm.py | 4 +- cntmanage/static/password | 1 - cntmanage/templates/admin/base_site.html | 4 +- note.txt | 3 ++ 8 files changed, 47 insertions(+), 30 deletions(-) delete mode 100644 cntmanage/static/password diff --git a/cntmanage/cntmanage/settings.py b/cntmanage/cntmanage/settings.py index ee47d6b..82efb31 100644 --- a/cntmanage/cntmanage/settings.py +++ b/cntmanage/cntmanage/settings.py @@ -154,4 +154,4 @@ 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 +EMAIL_FILE_PATH = "/mnt/d/Test/flightslot-mail" # change this to a proper location diff --git a/cntmanage/cntmanage/settings_prod.py b/cntmanage/cntmanage/settings_prod.py index 686b723..50d50cc 100644 --- a/cntmanage/cntmanage/settings_prod.py +++ b/cntmanage/cntmanage/settings_prod.py @@ -75,7 +75,7 @@ ROOT_URLCONF = 'cntmanage.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': ['/var/www/templates'], + 'DIRS': ['/var/www/templates', '/app/templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -139,6 +139,9 @@ USE_TZ = True # https://docs.djangoproject.com/en/5.1/howto/static-files/ STATIC_URL = "static/" STATIC_ROOT = "/var/www/static/" +STATICFILES_DIRS = [ + "/app/static/" +] STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" # Default primary key field type @@ -155,4 +158,4 @@ EMAIL_PORT = 587 EMAIL_USE_TLS = True # Use dummy backed for testing -EMAIL_BACKEND = "django.core.mail.backends..dummy.EmailBackend" +EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" diff --git a/cntmanage/docker/flightslot.Dockerfile b/cntmanage/docker/flightslot.Dockerfile index 7fea967..fb8d0d6 100644 --- a/cntmanage/docker/flightslot.Dockerfile +++ b/cntmanage/docker/flightslot.Dockerfile @@ -19,11 +19,12 @@ FROM python:3.12-slim AS deploy WORKDIR /app # Copy application custom static files RUN mkdir -p static -COPY ./static/cantorair.jpg ./static -COPY ./static/cantorair_blue.jpg ./static +COPY ./static/* ./static # Copy application custom templates for admin page RUN mkdir -p /templates/admin +RUN mkdir -p /templates/email COPY ./templates/admin/* ./templates/admin/ +COPY ./templates/email/* ./templates/email/ # Copy and install application wheel package COPY --from=builder /build/dist/*.whl ./ RUN pip install --no-cache-dir *.whl diff --git a/cntmanage/flightslot/actions/send_email.py b/cntmanage/flightslot/actions/send_email.py index 06a6a35..b5569b9 100644 --- a/cntmanage/flightslot/actions/send_email.py +++ b/cntmanage/flightslot/actions/send_email.py @@ -1,6 +1,6 @@ -from django.conf import settings +from django.contrib.staticfiles import finders from django.contrib import messages -from django.core.mail import EmailMultiAlternatives +from django.core.mail import EmailMultiAlternatives, get_connection from django.http import HttpRequest from django.db.models.query import QuerySet from django.utils.safestring import SafeText @@ -11,24 +11,27 @@ from ..models.students import Student from smtplib import SMTPException from email.mime.image import MIMEImage -import os +from typing import List 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: + filename: str + candidates = finders.find("cantorair.png") + if not candidates: messages.error(request=request, message="Cannot Load CantorAir Logo") return + elif isinstance(candidates, list): + filename = candidates.pop() + else: + filename = candidates + 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") + + # build mail list filling template + mails: List[EmailMultiAlternatives] = [] for student in queryset: if not student.user or not student.email: # skip student if has not an associated user continue @@ -52,11 +55,19 @@ def send_mail_password(request: HttpRequest, queryset: QuerySet[Student]) -> Non ) 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}") + mails.append(mail) 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 + + # Open only one conenction and send mass email + try: + with get_connection() as conn: + conn.send_messages(mails) + except SMTPException as e: + messages.error(request=request, message=f"Send Mail SMTP error: {e.strerror}") + except Exception as e: + messages.error(request=request, message=f"Send Mail General error: {e}") + else: + messages.success(request=request, message=f"Successfully sent {len(mails)} messages") + + return \ No newline at end of file diff --git a/cntmanage/flightslot/admins/student_adm.py b/cntmanage/flightslot/admins/student_adm.py index 3cd85f1..0e99952 100644 --- a/cntmanage/flightslot/admins/student_adm.py +++ b/cntmanage/flightslot/admins/student_adm.py @@ -115,9 +115,9 @@ class StudentAdmin(ImportMixin, AdminConfirmMixin, AdminActionFormsMixin, admin. 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: + @admin.action(description="Send Access Credentials e-mail") + def send_mail(self, request: HttpRequest, queryset: QuerySet[Student], *args: Any) -> None: send_mail_password(request=request, queryset=queryset) # Return the initial form for import confirmations, request course to user diff --git a/cntmanage/static/password b/cntmanage/static/password deleted file mode 100644 index 520af21..0000000 --- a/cntmanage/static/password +++ /dev/null @@ -1 +0,0 @@ -admin: CantorAir2k25 diff --git a/cntmanage/templates/admin/base_site.html b/cntmanage/templates/admin/base_site.html index d4e9d88..1ed4a89 100644 --- a/cntmanage/templates/admin/base_site.html +++ b/cntmanage/templates/admin/base_site.html @@ -8,8 +8,8 @@

-

diff --git a/note.txt b/note.txt index bc7eaa7..443535e 100644 --- a/note.txt +++ b/note.txt @@ -3,3 +3,6 @@ OK aereo assegnato allo studente di fianco al numero della missione per PPL, inv OK le missioni ripetute su piu' giorni hanno una cella per giorno (non unite) 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 + + +password: CantorAir2k25 -- 2.49.1