diff --git a/cntmanage/cntmanage/settings.py b/cntmanage/cntmanage/settings.py index bbfd0ad..f190c78 100644 --- a/cntmanage/cntmanage/settings.py +++ b/cntmanage/cntmanage/settings.py @@ -43,6 +43,7 @@ INSTALLED_APPS = [ 'colorfield', 'import_export', 'django_admin_action_forms', + 'polymorphic' ] # Import Export plugin settings @@ -52,7 +53,7 @@ IMPORT_EXPORT_SKIP_ADMIN_LOG = True IMPORT_FORMATS = [CSV] MIDDLEWARE = [ - 'flightslot.middleware.RedirectNonSuperuserFromAdminMiddleware', + 'flightslot.middleware.RedirectNonSuperuserFromAdminMiddleware', # custom middleware to show "user" page to non superuser 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', diff --git a/cntmanage/cntmanage/urls.py b/cntmanage/cntmanage/urls.py index 167fabb..c485b3c 100644 --- a/cntmanage/cntmanage/urls.py +++ b/cntmanage/cntmanage/urls.py @@ -9,7 +9,3 @@ urlpatterns = [ path('user/', flightslot_user.urls), path("", lambda r: redirect("/user/")), # la root porta gli utenti nella pagina giusta ] - -admin.site.site_header = "Flight Scheduler 🛫" -admin.site.site_title = "Flight Scheduler 🛫" -admin.site.index_title = "Welcome to CantorAir Flight Scheduler Portal" diff --git a/cntmanage/flightslot/actions/exportweek.py b/cntmanage/flightslot/actions/exportweek.py index 3587ac8..9654ab8 100644 --- a/cntmanage/flightslot/actions/exportweek.py +++ b/cntmanage/flightslot/actions/exportweek.py @@ -10,7 +10,7 @@ from typing import List from ..models.weekpref import WeekPreference from ..models.missions import Training -from ..models.hourbuildings import HourBuilding,HourBuildingLeg +from ..models.hourbuildings import HourBuilding, HourBuildingLegFlight, HourBuildingLegStop, HourBuildingLegBase def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse: @@ -41,7 +41,7 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) -> # Header titles days = [f"{datetime.strptime(f"{year} {week} {x}", "%G %V %u").strftime("%A")} {datetime.strptime(f"{year} {week} {x}", "%G %V %u").day}" for x in range(1,8)] - headers = ["Week", "Student", "Course", *days, "Cell.", "Mail", "Notes"] + headers = ["Week", "Student", "Course", *days, "Notes", "Cell.", "Mail"] # Header fields positions week_index: int = headers.index("Week") + 1 @@ -59,7 +59,12 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) -> # Cell styles border_thick: Side = Side(style='thick', color='000000') + border_thin: Side = Side(style='thin', color='000000', border_style='dashed') border_bottom: Border = Border(bottom=border_thick) + border_bottom_thin: Border = Border(bottom=border_thin) + border_left: Border = Border(left=border_thick) + border_right: Border = Border(right=border_thick) + border_right_thin: Border = Border(right=border_thin) border_all: Border = Border(bottom=border_thick, top=border_thick, left=border_thick, right=None) # Scrittura header @@ -102,15 +107,14 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) -> mission_name if t.sunday else "" ] mission_notes = t.notes if t.notes else "--" - mission_data.append([str(q.week), *student_data, *mission_days, student_phone, student_email, mission_notes]) + mission_data.append([str(q.week), *student_data, *mission_days, mission_notes, student_phone, student_email, ]) # Fill HourBuilding rows hb_name: str hb_days: List[str] - hb_notes: str hb_data: List[List[str]] = [] for h in HourBuilding.objects.filter(weekpref = q.id): - hb_name = f"HB-{h.aircraft}\nVedi Note ->" + hb_name = f"HB - {h.aircraft}\nVedi Note ->" hb_days = [ hb_name if h.monday else "", hb_name if h.tuesday else "", @@ -120,53 +124,71 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) -> hb_name if h.saturday else "", hb_name if h.sunday else "" ] - hb_notes = f"{h.notes}\n----\n" if h.notes else "" - hb_legs = HourBuildingLeg.objects.filter(hb_id = h.id) - for hh in hb_legs: - hb_notes += f"{hh.departure} -> {hh.destination} [{hh.time}]\n" if not hh.stop else f"STOP at {hh.departure} [{hh.time}]\n" - hb_notes.strip('\n') - hb_data.append([str(q.week), *student_data, *hb_days, str(q.student.phone), q.student.email, hb_notes]) + hb_notes: List[str] = [f"{h.notes}", "---"] if h.notes else [] + hb_legs_all = HourBuildingLegBase.objects.filter(hb_id = h.id) + for hh in hb_legs_all: + time_str: str = ':'.join(str(hh.time).split(':')[:2]) # keep only hours and minutes + if isinstance(hh, HourBuildingLegFlight): + hb_notes.append(f"{hh.departure} -> {hh.destination} [{time_str}]{f' / PAX: {hh.pax.capitalize()}' if hh.pax else ''}") + elif isinstance(hh, HourBuildingLegStop): + hb_notes.append(f"STOP [{time_str}] {"Refuel" if hh.refuel else ""}" ) + hb_data.append([str(q.week), *student_data, *hb_days, "\n".join(hb_notes), str(q.student.phone), q.student.email]) # Build rows for table all_data: List[List[str]] = mission_data + hb_data student_start: int = row + row_offset - for r in all_data: - for j, c in enumerate(r, start=1): - cell = ws.cell(row = row + row_offset, column = j, value = c) + for row_content in all_data: + for c, cell_content in enumerate(row_content, start=1): + cell = ws.cell(row = row + row_offset, column = c, value = cell_content) cell.alignment = center # Format Student Name - if j == student_index: + if c == student_index: cell.font = bold_black - # Format Course Column - if j == course_index and q.student.course: + # Format Course Column with color + elif c == course_index and q.student.course: cell.font = bold_black cell.fill = PatternFill("solid", fgColor=str(q.student.course.color).lstrip('#').lower()) - - prev_cell_val: str = r[0] + # Add internal borders between mix cells and notes + elif c > course_index and c <= note_index: + cell.border = border_bottom_thin + border_right_thin + # Fill mix cells if the cell is not empty + if c > course_index and c < note_index: + if len(cell_content): + cell.fill = PatternFill('solid', fgColor="f0f0f0") + + prev_cell_val: str = row_content[0] merge_start: bool = False merge_col_start: int = 1 - for c, v in enumerate(r, start=1): + for c, cell_content in enumerate(row_content, start=1): # Merge cells in the row - if v == prev_cell_val and not merge_start: + if cell_content == prev_cell_val and not merge_start: merge_start = True merge_col_start = c-1 # start merge from previous column - elif v != prev_cell_val and merge_start: + elif cell_content != prev_cell_val and merge_start: merge_start = False ws.merge_cells(start_row=row+row_offset, end_row=row+row_offset, start_column=max(merge_col_start,1), end_column=max(c-1,1)) # end merge to previous column - prev_cell_val = v + prev_cell_val = cell_content # Incement row counter row_offset += 1 # End week preferences for this student - student_end: int = row + row_offset -1 + student_end: int = row + row_offset - 1 + + # Add thick border to the last cell row of this student + for c in range(course_index, mail_index + 1): + ws.cell(row=student_end, column=c).border = Border(bottom=border_thick, right=border_thin) + # And for last column also a vertical border all student high + if c == mail_index: + for row_content in range(student_start, student_end + 1): + ws.cell(row=row_content, column=c).border += border_right # Merge Week, thick border - # ws.cell(row=student_start, column=week_index).border = border_all - # ws.merge_cells(start_row=student_start, end_row=student_end, start_column=week_index, end_column=week_index) + ws.cell(row=student_start, column=week_index).border = border_all + ws.merge_cells(start_row=student_start, end_row=student_end, start_column=week_index, end_column=week_index) # Merge Name, thick border ws.cell(row=student_start, column=student_index).border = border_all ws.merge_cells(start_row=student_start, end_row=student_end, start_column=student_index, end_column=student_index) @@ -177,13 +199,15 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) -> # Merge Mail ws.merge_cells(start_row=student_start, end_row=student_end, start_column=mail_index, end_column=mail_index) - # Add thick border to the last cell row of this student - for i in range(course_index, len(all_data[0])+1): - ws.cell(row=student_end, column=i).border = border_bottom - # Keep the largest column + max_len: List[int] = [] for column_cells in ws.columns: - length: int = max(len(str(cell.value)) for cell in column_cells) + for cell in column_cells: + cell_lines = str(cell.value).splitlines() + if len(cell_lines) == 0: + continue + max_len.append(max([len(ll) for ll in cell_lines])) + length: int = max(max_len) col_letter: str = "A" if column_cells[0].column: col_letter = get_column_letter(column_cells[0].column) @@ -191,8 +215,7 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) -> ### End of Student Loop ### - # Save document in HttpResponse wb.save(response) - return response \ No newline at end of file + return response diff --git a/cntmanage/flightslot/admin.py b/cntmanage/flightslot/admin.py index 4823d35..66444bb 100644 --- a/cntmanage/flightslot/admin.py +++ b/cntmanage/flightslot/admin.py @@ -10,6 +10,7 @@ from .admins.course_adm import CourseAdmin from .admins.student_adm import StudentAdmin from .admins.mission_adm import MissionProfileAdmin from .admins.weekpred_adm import WeekPreferenceAdmin +#from .admins.hourbuilding_adm import HourBuilding, HourBuildingInLine from django.contrib.admin import AdminSite @@ -31,6 +32,10 @@ flightslot_user = FlightSlotUserSite(name="user_site") # registra SOLO i modelli autorizzati flightslot_user.register(WeekPreference, WeekPreferenceAdmin) +admin.site.site_header = "Flight Scheduler Admin 🛫" +admin.site.site_title = "Flight Scheduler Admin 🛫" +admin.site.index_title = "Welcome to CantorAir Flight Scheduler Administrator Portal" + admin.site.register(Course, CourseAdmin) admin.site.register(MissionProfile, MissionProfileAdmin) admin.site.register(Student, StudentAdmin) diff --git a/cntmanage/flightslot/admins/hourbuilding_adm.py b/cntmanage/flightslot/admins/hourbuilding_adm.py index 9e7fc06..30dbb4c 100644 --- a/cntmanage/flightslot/admins/hourbuilding_adm.py +++ b/cntmanage/flightslot/admins/hourbuilding_adm.py @@ -7,36 +7,63 @@ from django.http import HttpRequest from durationwidget.widgets import TimeDurationWidget -from ..models.hourbuildings import HourBuilding, HourBuildingLeg +from ..models.hourbuildings import HourBuilding, HourBuildingLegBase, HourBuildingLegFlight, HourBuildingLegStop from ..models.weekpref import WeekPreference from datetime import date -class HourBuildingLegForm(forms.ModelForm): +class HourBuildingLegFlightForm(forms.ModelForm): class Meta: - model = HourBuildingLeg - fields = '__all__' + model = HourBuildingLegFlight + fields = "__all__" widgets = { - 'time': TimeDurationWidget(show_days=False, - show_seconds=False + "time": TimeDurationWidget(show_days=False, + show_seconds=False, + attrs={ + "style": ( + "margin-right:5px; margin-left:5px; width:40px; min:0; max:5" ) + }) + } + +class HourBuildingLegStopForm(forms.ModelForm): + class Meta: + model = HourBuildingLegStop + fields = "__all__" + widgets = { + "time": TimeDurationWidget(show_days=False, + show_seconds=False, + attrs={ + "style": ( + "margin-right:5px; margin-left:5px; width:40px;" + ) + }) } # Register your models here. -class HourBuildingLegInline(nested_admin.NestedTabularInline): - model = HourBuildingLeg - form = HourBuildingLegForm - extra = 0 - fk_name = 'hb' - max_num = 5 - formfield_overrides = { - models.CharField: {'widget': TextInput(attrs={'size':'20'})}, - models.TextField: {'widget': Textarea(attrs={'rows':4, 'cols':35})}, - } +class HourBuildingLegBaseInLine(nested_admin.NestedStackedPolymorphicInline): + model = HourBuildingLegBase + fk_name = "hb" + verbose_name_plural = "Hour Building Legs" + + class HourBuildingLegFlightInLine(nested_admin.NestedStackedPolymorphicInline.Child): + model = HourBuildingLegFlight + form = HourBuildingLegFlightForm + fk_name = "hourbuildinglegbase_ptr" + fields = ("departure", "time", "destination", "pax", ) + hide_title = True + + class HourBuildingLegStopInLine(nested_admin.NestedStackedPolymorphicInline.Child): + model = HourBuildingLegStop + form = HourBuildingLegFlightForm + fk_name = "hourbuildinglegbase_ptr" + fields = ("time", "refuel", ) + + 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: + 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 @@ -45,26 +72,16 @@ class HourBuildingLegInline(nested_admin.NestedTabularInline): 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): model = HourBuilding + inlines = (HourBuildingLegBaseInLine,) extra = 0 - inlines = [HourBuildingLegInline] - fk_name = 'weekpref' - verbose_name_plural = "Hour Building" max_num = 7 + fk_name = "weekpref" + verbose_name_plural = "Hour Buildings" formfield_overrides = { - models.CharField: {'widget': TextInput(attrs={'size':'20'})}, - models.TextField: {'widget': Textarea(attrs={'rows':4, 'cols':35})}, + models.CharField: {"widget": TextInput(attrs={"size":"20"})}, + 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 = 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) diff --git a/cntmanage/flightslot/admins/weekpred_adm.py b/cntmanage/flightslot/admins/weekpred_adm.py index ec17676..dd4fd06 100644 --- a/cntmanage/flightslot/admins/weekpred_adm.py +++ b/cntmanage/flightslot/admins/weekpred_adm.py @@ -18,8 +18,8 @@ from ..actions.exportweek import export_selected from datetime import date -class WeekPreferenceAdmin(nested_admin.NestedModelAdmin): - inlines = (TrainingInLIne, HourBuildingInLine,) +class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin): + inlines = (TrainingInLIne, HourBuildingInLine, ) list_display = ("week", "student__surname","student__name", "student__course", "course_color", "student_brief_mix",) list_filter = ("week", "student__course", "student",) actions = ("export",) diff --git a/cntmanage/flightslot/migrations/0018_hourbuildinglegbase_hourbuildinglegflight_and_more.py b/cntmanage/flightslot/migrations/0018_hourbuildinglegbase_hourbuildinglegflight_and_more.py new file mode 100644 index 0000000..6158fff --- /dev/null +++ b/cntmanage/flightslot/migrations/0018_hourbuildinglegbase_hourbuildinglegflight_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 5.2.8 on 2025-11-21 11:20 + +import datetime +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('flightslot', '0017_alter_missionprofile_mtype_alter_weekpreference_week'), + ] + + operations = [ + migrations.CreateModel( + name='HourBuildingLegBase', + fields=[ + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('time', models.DurationField(default=datetime.timedelta(seconds=3600))), + ('hb', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='flightslot.hourbuilding')), + ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), + ], + options={ + 'abstract': False, + 'base_manager_name': 'objects', + }, + ), + migrations.CreateModel( + name='HourBuildingLegFlight', + fields=[ + ('hourbuildinglegbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='flightslot.hourbuildinglegbase')), + ('departure', models.CharField(default='LILV', max_length=4)), + ('destination', models.CharField(default='LILV', max_length=4)), + ], + options={ + 'abstract': False, + 'base_manager_name': 'objects', + }, + bases=('flightslot.hourbuildinglegbase',), + ), + migrations.CreateModel( + name='HourBuildingLegStop', + fields=[ + ('hourbuildinglegbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='flightslot.hourbuildinglegbase')), + ('location', models.CharField(default='LILV', max_length=4)), + ('refuel', models.BooleanField(default=False)), + ], + options={ + 'abstract': False, + 'base_manager_name': 'objects', + }, + bases=('flightslot.hourbuildinglegbase',), + ), + migrations.DeleteModel( + name='HourBuildingLeg', + ), + ] diff --git a/cntmanage/flightslot/migrations/0019_remove_hourbuildinglegstop_location_and_more.py b/cntmanage/flightslot/migrations/0019_remove_hourbuildinglegstop_location_and_more.py new file mode 100644 index 0000000..05d642b --- /dev/null +++ b/cntmanage/flightslot/migrations/0019_remove_hourbuildinglegstop_location_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 5.2.8 on 2025-11-21 16:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('flightslot', '0018_hourbuildinglegbase_hourbuildinglegflight_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='hourbuildinglegstop', + name='location', + ), + migrations.AddField( + model_name='hourbuildinglegflight', + name='pax', + field=models.CharField(max_length=16, null=True), + ), + migrations.AlterField( + model_name='hourbuildinglegbase', + name='time', + field=models.DurationField(), + ), + migrations.AlterField( + model_name='hourbuildinglegflight', + name='departure', + field=models.CharField(max_length=4), + ), + migrations.AlterField( + model_name='hourbuildinglegflight', + name='destination', + field=models.CharField(max_length=4), + ), + ] diff --git a/cntmanage/flightslot/models/hourbuildings.py b/cntmanage/flightslot/models/hourbuildings.py index 33d3c4e..3de50e6 100644 --- a/cntmanage/flightslot/models/hourbuildings.py +++ b/cntmanage/flightslot/models/hourbuildings.py @@ -1,11 +1,11 @@ from django.utils.translation import gettext_lazy as _ from django.db import models -from datetime import timedelta + +from polymorphic.models import PolymorphicModel from ..models.weekpref import WeekPreference from ..models.aircrafts import AircraftTypes - class HourBuilding(models.Model): id = models.BigAutoField( primary_key=True @@ -66,7 +66,7 @@ class HourBuilding(models.Model): def __str__(self): return f"Hour Building: {self.aircraft}" -class HourBuildingLeg(models.Model): +class HourBuildingLegBase(PolymorphicModel): id = models.BigAutoField( primary_key=True ) @@ -76,32 +76,63 @@ class HourBuildingLeg(models.Model): on_delete=models.CASCADE ) + time = models.DurationField( + null=False, + blank=False + ) + + # Change displayed name in the inline form + class Meta(PolymorphicModel.Meta): + verbose_name = "Flight Leg or Stop" + verbose_name_plural = "Flight Legs or Stops" + + def __str__(self): + return f"Hour Building Leg" + +class HourBuildingLegFlight(HourBuildingLegBase): departure = models.CharField( null=False, blank=False, - default="LILV", max_length=4 ) destination = models.CharField( null=False, blank=False, - default="LILV", max_length=4 ) - time = models.DurationField( - null=False, - default = timedelta(hours=1) + pax = models.CharField( + null=True, + blank=True, + max_length=16, + verbose_name="Pax (optional)" ) - stop = models.BooleanField( + # Change displayed name in the inline form + class Meta(HourBuildingLegBase.Meta): + verbose_name = "Flight leg" + verbose_name_plural = "Flight legs" + + def save(self, *args, **kwargs): + self.departure = self.departure.upper().strip() + self.destination = self.destination.upper().strip() + super().save(*args, **kwargs) + + def __str__(self): + return f"{self.departure} -> {self.destination}" + +class HourBuildingLegStop(HourBuildingLegBase): + + refuel = models.BooleanField( default=False ) + # Change displayed name in the inline form + class Meta(HourBuildingLegBase.Meta): + verbose_name = "Stop" + verbose_name_plural = "Stops" + def __str__(self): - if self.stop: - return "Refuelling Stop" - else: - return f"Flight Leg: {self.departure} -> {self.destination}" - \ No newline at end of file + return f"Refuel" if self.refuel else f"No Refuel" + \ No newline at end of file diff --git a/cntmanage/flightslot/models/students.py b/cntmanage/flightslot/models/students.py index 973e9d7..2582a01 100644 --- a/cntmanage/flightslot/models/students.py +++ b/cntmanage/flightslot/models/students.py @@ -51,24 +51,24 @@ class Student(models.Model): # Override save method to add user for login upon Student creation def save(self, *args, **kwargs): - creating = self.pk is None + creating: bool = self.pk is None super().save(*args, **kwargs) if creating and not self.user: - username = f"{self.name.lower()}.{self.surname.lower()}" + username: str = f"{self.name.lower()}.{self.surname.lower()}" # Avoid username conflict with progressive number base_username = username - counter = 1 + counter: int = 1 while User.objects.filter(username=username).exists(): username = f"{base_username}{counter}" counter += 1 # Create user - user = User.objects.create_user( + user: User = User.objects.create_user( first_name=self.name, last_name=self.surname, username=username, email=self.email, password=self.default_password(), - is_staff=True + is_staff=True # allows access to admin page ) student_group, _ = Group.objects.get_or_create(name="StudentGroup") diff --git a/cntmanage/poetry.lock b/cntmanage/poetry.lock index 141d35f..b0c1f3c 100644 --- a/cntmanage/poetry.lock +++ b/cntmanage/poetry.lock @@ -2,14 +2,14 @@ [[package]] name = "asgiref" -version = "3.10.0" +version = "3.11.0" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "asgiref-3.10.0-py3-none-any.whl", hash = "sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734"}, - {file = "asgiref-3.10.0.tar.gz", hash = "sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e"}, + {file = "asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d"}, + {file = "asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4"}, ] [package.extras] @@ -143,6 +143,21 @@ python-monkey-business = ">=1.0.0" dev = ["Pillow", "black", "dj-database-url", "django-selenosis", "flake8", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "selenium"] test = ["Pillow", "dj-database-url", "django-selenosis", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "selenium"] +[[package]] +name = "django-polymorphic" +version = "4.1.0" +description = "Seamless polymorphic inheritance for Django models" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "django_polymorphic-4.1.0-py3-none-any.whl", hash = "sha256:0ce3984999e103a0d1a434a5c5617f2c7f990dc3d5fb3585ce0fadadf9ff90ea"}, + {file = "django_polymorphic-4.1.0.tar.gz", hash = "sha256:4438d95a0aef6c4307cd6c83ead387e1142ce80b65188a931ec2f0dbdd9bfc51"}, +] + +[package.dependencies] +Django = ">=3.2" + [[package]] name = "et-xmlfile" version = "2.0.0" @@ -421,4 +436,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "6bf43236f441d8b6bf8d1928910d169d3b29cfa499bb7d09d97ea227f8115658" +content-hash = "e932d0af75c888d83fecefaaad1d018c508881a3bfde2ea640a82790e3567855" diff --git a/cntmanage/pyproject.toml b/cntmanage/pyproject.toml index 6f403fb..aa51730 100644 --- a/cntmanage/pyproject.toml +++ b/cntmanage/pyproject.toml @@ -17,6 +17,7 @@ django-import-export = "^4.3.13" django-colorfield = "^0.14.0" openpyxl = "^3.1.5" django-admin-action-forms = "^2.2.1" +django-polymorphic = "^4.1.0" [build-system]