12 Commits

15 changed files with 139 additions and 40 deletions

View File

@@ -53,7 +53,6 @@ IMPORT_EXPORT_SKIP_ADMIN_LOG = True
IMPORT_FORMATS = [CSV] IMPORT_FORMATS = [CSV]
MIDDLEWARE = [ MIDDLEWARE = [
'flightslot.middleware.RedirectNonSuperuserFromAdminMiddleware', # custom middleware to show "user" page to non superuser
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
@@ -61,6 +60,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'flightslot.middleware.RedirectNonSuperuserFromAdminMiddleware', # custom middleware to show "user" page to non superuser
] ]
ROOT_URLCONF = 'cntmanage.urls' ROOT_URLCONF = 'cntmanage.urls'

View File

@@ -47,6 +47,7 @@ INSTALLED_APPS = [
'colorfield', 'colorfield',
'import_export', 'import_export',
'django_admin_action_forms', 'django_admin_action_forms',
'polymorphic'
] ]
# Import Export plugin settings # Import Export plugin settings
@@ -56,7 +57,6 @@ IMPORT_EXPORT_SKIP_ADMIN_LOG = True
IMPORT_FORMATS = [CSV] IMPORT_FORMATS = [CSV]
MIDDLEWARE = [ MIDDLEWARE = [
'flightslot.middleware.RedirectNonSuperuserFromAdminMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
@@ -65,6 +65,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'flightslot.middleware.RedirectNonSuperuserFromAdminMiddleware', # custom middleware to show "user" page to non superuser
] ]
ROOT_URLCONF = 'cntmanage.urls' ROOT_URLCONF = 'cntmanage.urls'

View File

@@ -38,4 +38,4 @@ COPY ./docker/entrypoint.sh ./
ENTRYPOINT ["/app/entrypoint.sh"] ENTRYPOINT ["/app/entrypoint.sh"]
# Command to be executed after entry point # Command to be executed after entry point
CMD ["gunicorn", "cntmanage.wsgi:application", "--bind", "0.0.0.0:8000"] CMD ["gunicorn", "cntmanage.wsgi:application", "--bind", "0.0.0.0:8000", "--timeout", "600"]

View File

@@ -79,7 +79,7 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
# Each of this iterations fills the table for a student # Each of this iterations fills the table for a student
row: int = 2 row: int = 2
row_offset: int = 0 row_offset: int = 0
for i, q in enumerate(queryset.order_by("student__surname", "student__name", "student__course"), start=1): for i, q in enumerate(queryset.order_by("week", "student__surname", "student__name", "student__course"), start=1):
student_data: List[str] student_data: List[str]
student_phone: str = q.student.phone if q.student.phone else "" student_phone: str = q.student.phone if q.student.phone else ""
student_email: str = q.student.email student_email: str = q.student.email

View File

@@ -9,8 +9,7 @@ from .models.weekpref import WeekPreference
from .admins.course_adm import CourseAdmin from .admins.course_adm import CourseAdmin
from .admins.student_adm import StudentAdmin from .admins.student_adm import StudentAdmin
from .admins.mission_adm import MissionProfileAdmin from .admins.mission_adm import MissionProfileAdmin
from .admins.weekpred_adm import WeekPreferenceAdmin from .admins.weekpref_adm import WeekPreferenceAdmin
#from .admins.hourbuilding_adm import HourBuilding, HourBuildingInLine
from django.contrib.admin import AdminSite from django.contrib.admin import AdminSite

View File

@@ -51,7 +51,6 @@ class HourBuildingLegBaseInLine(nested_admin.NestedStackedPolymorphicInline):
form = HourBuildingLegFlightForm form = HourBuildingLegFlightForm
fk_name = "hourbuildinglegbase_ptr" fk_name = "hourbuildinglegbase_ptr"
fields = ("departure", "time", "destination", "pax", ) fields = ("departure", "time", "destination", "pax", )
hide_title = True
class HourBuildingLegStopInLine(nested_admin.NestedStackedPolymorphicInline.Child): class HourBuildingLegStopInLine(nested_admin.NestedStackedPolymorphicInline.Child):
model = HourBuildingLegStop model = HourBuildingLegStop
@@ -61,18 +60,6 @@ class HourBuildingLegBaseInLine(nested_admin.NestedStackedPolymorphicInline):
child_inlines = (HourBuildingLegFlightInLine, HourBuildingLegStopInLine, ) child_inlines = (HourBuildingLegFlightInLine, HourBuildingLegStopInLine, )
# If user is a student deny edit permission for week past the current one
def has_change_permission(self, request: HttpRequest, obj: HourBuilding | None = None):
if hasattr(request.user, "student") and obj:
current_week = date.today().isocalendar().week
if not obj.DoesNotExist and current_week > obj.weekpref.week:
return False
return True
def has_delete_permission(self, request: HttpRequest, obj: HourBuilding | None = None):
return self.has_change_permission(request=request, obj=obj)
class HourBuildingInLine(nested_admin.NestedTabularInline): class HourBuildingInLine(nested_admin.NestedTabularInline):
model = HourBuilding model = HourBuilding
inlines = (HourBuildingLegBaseInLine,) inlines = (HourBuildingLegBaseInLine,)
@@ -85,3 +72,16 @@ class HourBuildingInLine(nested_admin.NestedTabularInline):
models.TextField: {"widget": Textarea(attrs={"rows":4, "cols":35})}, models.TextField: {"widget": Textarea(attrs={"rows":4, "cols":35})},
} }
# If user is a student deny edit permission for week past the current one
def has_change_permission(self, request: HttpRequest, obj: WeekPreference | None = None):
if hasattr(request.user, 'student') and obj:
current_week: int = date.today().isocalendar().week
if current_week > obj.week:
return False
return True
def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None):
return self.has_change_permission(request=request, obj=obj)
def has_add_permission(self, request: HttpRequest, obj: WeekPreference | None = None):
return self.has_change_permission(request=request, obj=obj)

View File

@@ -34,6 +34,6 @@ class MissionProfileResource(ModelResource):
fields = ("mtype", "mnum", "duration") fields = ("mtype", "mnum", "duration")
import_id_fields = ("mtype", "mnum") import_id_fields = ("mtype", "mnum")
class MissionProfileAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin): class MissionProfileAdmin(AdminActionFormsMixin, ImportMixin, admin.ModelAdmin):
list_display = ("mtype", "mnum", "notes") list_display = ("mtype", "mnum", "notes")
list_filter = ("mtype",) list_filter = ("mtype",)

View File

@@ -6,16 +6,25 @@ from django.utils.safestring import SafeText
from import_export import fields from import_export import fields
from import_export.admin import ImportMixin from import_export.admin import ImportMixin
from import_export.tmp_storages import CacheStorage
from import_export.resources import ModelResource from import_export.resources import ModelResource
from import_export.widgets import CharWidget 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, action_with_form
from typing import Any, Dict
from ..models.courses import Course from ..models.courses import Course
from ..models.students import Student from ..models.students import Student
from ..custom.colortag import course_color from ..custom.colortag import course_color
# Custom import form to select a course for student input
class StudentCustomConfirmImportForm(ConfirmImportForm):
course = forms.ModelChoiceField(
queryset=Course.objects.all(),
required=False)
# Resource Class for Student data import # Resource Class for Student data import
class StudentResource(ModelResource): class StudentResource(ModelResource):
surname = fields.Field(attribute="surname", column_name="surname") surname = fields.Field(attribute="surname", column_name="surname")
@@ -24,29 +33,39 @@ class StudentResource(ModelResource):
phone = fields.Field(attribute="phone", column_name="phone") phone = fields.Field(attribute="phone", column_name="phone")
# Cleanup fields before entering # Cleanup fields before entering
def before_import_row(self, row: dict[str, str], **kwargs) -> None: def before_import_row(self, row: Dict[str, str], **kwargs) -> None:
row["name"] = SafeText(row["name"].capitalize().strip()) row["name"] = SafeText("-".join(c.capitalize() for c in row["name"].split(" ")).strip())
row["surname"] = SafeText(row["surname"].capitalize().strip()) row["surname"] = SafeText("-".join(c.capitalize() for c in row["surname"].split(" ")).strip())
row["phone"] = SafeText(row["phone"].replace(" ","")) row["phone"] = SafeText(row["phone"].replace(" ",""))
row["email"] = SafeText(row["email"].lower().strip()) row["email"] = SafeText(row["email"].lower().strip())
return super().before_import_row(row, **kwargs) return super().before_import_row(row, **kwargs)
# If course was addedd as a form kwasrg add it to the student after creation
def after_init_instance(self, instance: Student, new: bool, row: Dict[str, str], **kwargs: Dict[str, Any | Course]):
course = kwargs.get("course", None)
if course and isinstance(course, Course):
instance.course = course
class Meta: class Meta:
model = Student model = Student
skip_unchanged = True skip_unchanged = True
report_skipped = True report_skipped = True
fields = ("surname", "name", "email", "phone") fields = ("surname", "name", "email", "phone",)
import_id_fields = ("email", "phone") import_id_fields = ("email", "phone",)
# Form Class for Student course change # Form Class for Student course change
class ChangeCourseForm(AdminActionForm): class ChangeCourseForm(AdminActionForm):
course = forms.ModelChoiceField(queryset=Course.objects.all()) course = forms.ModelChoiceField(queryset=Course.objects.all())
class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin): class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
list_display = ("surname", "name", "course", "course_color", "email", "phone", "password", "active") list_display = ("surname", "name", "course", "course_color", "email", "phone", "username", "password", "active",)
list_filter = ("course", "active") list_filter = ("course", "active",)
actions = ("change_course", "disable_students") search_fields = ("surname", "name", "phone", "email",)
actions = ("change_course", "disable_students",)
resource_classes = [StudentResource] resource_classes = [StudentResource]
confirm_form_class = StudentCustomConfirmImportForm
tmp_storage_class = CacheStorage
skip_admin_log = True
@admin.display(description="Color") @admin.display(description="Color")
def course_color(self, obj: Student) -> SafeText: def course_color(self, obj: Student) -> SafeText:
@@ -58,6 +77,10 @@ class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
def password(self, obj: Student) -> SafeText: def password(self, obj: Student) -> SafeText:
return SafeText(obj.default_password()) return SafeText(obj.default_password())
@admin.display(description="Username")
def username(self, obj: Student) -> SafeText:
return SafeText(obj.default_username())
@admin.action(description="Deactivate Students") @admin.action(description="Deactivate Students")
def disable_students(self, request: HttpRequest, queryset: QuerySet[Student]): def disable_students(self, request: HttpRequest, queryset: QuerySet[Student]):
for q in queryset.all(): for q in queryset.all():
@@ -73,3 +96,18 @@ class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
course = data["course"] course = data["course"]
count: int = queryset.update(course=course) count: int = queryset.update(course=course)
messages.success(request, f"{count} students updated to {course}") messages.success(request, f"{count} students updated to {course}")
# Return the initial form for import confirmations, request course to user
def get_confirm_form_initial(self, request: HttpRequest, import_form):
initial = super().get_confirm_form_initial(request, import_form)
if import_form and hasattr(import_form.cleaned_data, "course"):
course: Course = import_form.cleaned_data["course"]
initial["course"] = course.id
return initial
# Add course to import form kwargs to be used by resource to associate course with all imported students
def get_import_data_kwargs(self, request: HttpRequest, *args, **kwargs):
form: ImportForm | None = kwargs.get("form", None)
if form and hasattr(form, "cleaned_data"):
kwargs["course"] = form.cleaned_data.get("course", None)
return kwargs

View File

@@ -35,3 +35,6 @@ class TrainingInLIne(nested_admin.NestedTabularInline):
def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None): def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None):
return self.has_change_permission(request=request, obj=obj) return self.has_change_permission(request=request, obj=obj)
def has_add_permission(self, request: HttpRequest, obj: WeekPreference | None = None):
return self.has_change_permission(request=request, obj=obj)

View File

@@ -21,7 +21,8 @@ from datetime import date
class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin): class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
inlines = (TrainingInLIne, HourBuildingInLine, ) inlines = (TrainingInLIne, HourBuildingInLine, )
list_display = ("week", "student__surname","student__name", "student__course", "course_color", "student_brief_mix",) list_display = ("week", "student__surname","student__name", "student__course", "course_color", "student_brief_mix",)
list_filter = ("week", "student__course", "student",) list_filter = ("week", "student__course",)
search_fields = ("student__surname","student__name",)
actions = ("export",) actions = ("export",)
@admin.action(description="Export Selected Preferences") @admin.action(description="Export Selected Preferences")

View File

@@ -8,11 +8,7 @@ class RedirectNonSuperuserFromAdminMiddleware:
def __call__(self, request: HttpRequest): def __call__(self, request: HttpRequest):
# Se l'utente è loggato, non è superuser e prova ad andare in /admin/... # Se l'utente è loggato, non è superuser e prova ad andare in /admin/...
if hasattr(request,"user"): if hasattr(request, "user") and not request.user.is_superuser:
if ( if "/admin/" in request.path:
request.path.startswith("/admin/") and
hasattr(request.user, 'student')
):
return redirect("/user/") # redirect automatico return redirect("/user/") # redirect automatico
return self.get_response(request) return self.get_response(request)

View File

@@ -0,0 +1,35 @@
# Generated by Django 5.2.8 on 2025-11-25 11:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0019_remove_hourbuildinglegstop_location_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='hourbuildinglegbase',
options={'base_manager_name': 'objects', 'verbose_name': 'Flight Leg or Stop', 'verbose_name_plural': 'Flight Legs or Stops'},
),
migrations.AlterModelOptions(
name='hourbuildinglegflight',
options={'base_manager_name': 'objects', 'verbose_name': 'Flight leg', 'verbose_name_plural': 'Flight legs'},
),
migrations.AlterModelOptions(
name='hourbuildinglegstop',
options={'base_manager_name': 'objects', 'verbose_name': 'Stop', 'verbose_name_plural': 'Stops'},
),
migrations.AlterField(
model_name='hourbuildinglegflight',
name='pax',
field=models.CharField(blank=True, max_length=16, null=True, verbose_name='Pax (optional)'),
),
migrations.AlterField(
model_name='weekpreference',
name='week',
field=models.PositiveSmallIntegerField(auto_created=True, db_default=48, db_index=True, verbose_name='Week Number'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.8 on 2025-11-27 10:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0020_alter_hourbuildinglegbase_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='hourbuildinglegstop',
name='refuel',
field=models.BooleanField(default=False, verbose_name='Stop for Refuelling'),
),
]

View File

@@ -125,7 +125,9 @@ class HourBuildingLegFlight(HourBuildingLegBase):
class HourBuildingLegStop(HourBuildingLegBase): class HourBuildingLegStop(HourBuildingLegBase):
refuel = models.BooleanField ( refuel = models.BooleanField (
default=False null=False,
default=False,
verbose_name="Stop for Refuelling"
) )
# Change displayed name in the inline form # Change displayed name in the inline form

View File

@@ -46,8 +46,14 @@ class Student(models.Model):
blank=True blank=True
) )
def default_password(self) -> str: def default_password(self) -> str: # Maximum 4 digits for passowrd
return f"{self.name.lower()[0]}{self.surname.lower()}{self.id}" return f"{self.name.lower()[0]}{self.surname.lower()}{self.id % 10000}"
def default_username(self) -> str:
if self.pk and self.user:
return self.user.username
else:
return ""
# Override save method to add user for login upon Student creation # Override save method to add user for login upon Student creation
def save(self, *args, **kwargs): def save(self, *args, **kwargs):