diff --git a/cntmanage/cntmanage/settings.py b/cntmanage/cntmanage/settings.py index 15c1c07..82efb31 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 = "/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 a1f4808..50d50cc 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', @@ -74,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': [ @@ -138,9 +139,23 @@ 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 # 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/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 new file mode 100644 index 0000000..b5569b9 --- /dev/null +++ b/cntmanage/flightslot/actions/send_email.py @@ -0,0 +1,73 @@ +from django.contrib.staticfiles import finders +from django.contrib import messages +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 +from django.template.loader import render_to_string + +from ..models.students import Student + +from smtplib import SMTPException +from email.mime.image import MIMEImage + +from typing import List + +def send_mail_password(request: HttpRequest, queryset: QuerySet[Student]) -> None: + img: MIMEImage | None = None + 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 + 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") + mails.append(mail) + except Exception as e: + messages.error(request=request, message=f"General Error: {e}") + + # 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 efb7c12..0e99952 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}") + @confirm_action + @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 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 0000000..18e68cc Binary files /dev/null and b/cntmanage/static/cantorair.png differ diff --git a/cntmanage/static/password b/cntmanage/static/password deleted file mode 100644 index 667feba..0000000 --- a/cntmanage/static/password +++ /dev/null @@ -1 +0,0 @@ -admin: CantorAdmin2k25 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/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 1562a56..443535e 100644 --- a/note.txt +++ b/note.txt @@ -1,6 +1,8 @@ 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 + +password: CantorAir2k25