diff --git a/src/cal_bc/projects/forms/model_section.py b/src/cal_bc/projects/forms/model_section.py new file mode 100644 index 0000000..bff11ad --- /dev/null +++ b/src/cal_bc/projects/forms/model_section.py @@ -0,0 +1,12 @@ +from django.forms import ModelForm +from cal_bc.projects.models.model_section import ModelSection +from django.utils.translation import gettext as _ + + +class ModelSectionForm(ModelForm): + class Meta: + model = ModelSection + fields = ["section_fields"] + + def get_labels(self) -> dict[str, str]: + return {"section_fields": _(self.object.name)} diff --git a/src/cal_bc/projects/forms/project.py b/src/cal_bc/projects/forms/project.py index 9c5d916..a354226 100644 --- a/src/cal_bc/projects/forms/project.py +++ b/src/cal_bc/projects/forms/project.py @@ -16,23 +16,11 @@ class Meta: fields = [ "name", "district", - "type", - "location", - "construction_period_length", - "data_direction", - "peak_periods_length", + "model", ] labels = { "name": _("Project Name"), - "type": _("Project Type"), - "location": _("Project Location"), - "construction_period_length": _("Length of Construction Period"), - "data_direction": _("One- or Two-Way Data"), - "peak_periods_length": _("Length of Peak Period(s) (up to 24 hrs)"), - } - - help_texts = { - "construction_period_length": _("years"), - "peak_periods_length": _("hours"), + "model": _("Model"), + "district": _("District"), } diff --git a/src/cal_bc/projects/forms/project_field.py b/src/cal_bc/projects/forms/project_field.py new file mode 100644 index 0000000..ee1ca4c --- /dev/null +++ b/src/cal_bc/projects/forms/project_field.py @@ -0,0 +1,19 @@ +from django.forms import ModelForm +from cal_bc.projects.models.project_field import ProjectField +from django.utils.translation import gettext as _ + + +class ProjectFieldForm(ModelForm): + class Meta: + model = ProjectField + fields = ["value", "project"] + labels = {"value": _("Value")} + widgets = {"project": Hidden()} + + def get_labels(self) -> dict[str, str]: + return {"value": _(self.object.model_section_field.name)} + + +ProjectFieldFormset = inlineformset_factory( + ModelSectionField, ProjectField, form=ProjectFieldForm, allow_create=False +) diff --git a/src/cal_bc/projects/migrations/0001_create_models.py b/src/cal_bc/projects/migrations/0001_create_models.py new file mode 100644 index 0000000..5023892 --- /dev/null +++ b/src/cal_bc/projects/migrations/0001_create_models.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.7 on 2025-10-11 00:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Model", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(null=False)), + ("url", models.CharField(null=False)), + ], + ), + ] diff --git a/src/cal_bc/projects/migrations/0001_initial.py b/src/cal_bc/projects/migrations/0001_initial.py deleted file mode 100644 index 37bcd3f..0000000 --- a/src/cal_bc/projects/migrations/0001_initial.py +++ /dev/null @@ -1,11 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-11 00:25 - -from django.db import migrations - - -class Migration(migrations.Migration): - initial = True - - dependencies = [] - - operations = [] diff --git a/src/cal_bc/projects/migrations/0002_create_projects.py b/src/cal_bc/projects/migrations/0002_create_projects.py index 4d70b12..0aeb0f9 100644 --- a/src/cal_bc/projects/migrations/0002_create_projects.py +++ b/src/cal_bc/projects/migrations/0002_create_projects.py @@ -1,11 +1,12 @@ -# Generated by Django 5.2.7 on 2025-10-11 00:34 +# Generated by Django 5.2.7 on 2025-10-18 19:46 +import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ("projects", "0001_initial"), + ("projects", "0001_create_models"), ] operations = [ @@ -21,7 +22,15 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("name", models.CharField()), + ("name", models.CharField(null=False)), + ( + "model", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="projects.model", + null=False, + ), + ), ], ), ] diff --git a/src/cal_bc/projects/migrations/0003_add_project_district.py b/src/cal_bc/projects/migrations/0003_add_project_district.py new file mode 100644 index 0000000..7d6992c --- /dev/null +++ b/src/cal_bc/projects/migrations/0003_add_project_district.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.7 on 2025-10-15 22:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("projects", "0002_create_projects"), + ] + + operations = [ + migrations.AddField( + model_name="project", + name="district", + field=models.IntegerField( + choices=[ + (1, "District 1 - Eureka"), + (2, "District 2 - Redding"), + (3, "District 3 - Marysville / Sacramento"), + (4, "District 4 - Bay Area / Oakland"), + (5, "District 5 - Central Coast"), + (6, "District 6 - Fresno / Bakersfield"), + (7, "District 7 - Los Angeles / Ventura"), + (8, "District 8 - San Bernardino / Riverside"), + (9, "District 9 - Bishop"), + (10, "District 10 - Stockton"), + (11, "District 11 - San Diego"), + (12, "District 12 - Orange County"), + ], + null=False, + ), + ), + ] diff --git a/src/cal_bc/projects/migrations/0003_project_construction_period_length_and_more.py b/src/cal_bc/projects/migrations/0003_project_construction_period_length_and_more.py deleted file mode 100644 index 5b6ff8a..0000000 --- a/src/cal_bc/projects/migrations/0003_project_construction_period_length_and_more.py +++ /dev/null @@ -1,133 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-15 22:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("projects", "0002_create_projects"), - ] - - operations = [ - migrations.AddField( - model_name="project", - name="construction_period_length", - field=models.IntegerField(null=True), - ), - migrations.AddField( - model_name="project", - name="data_direction", - field=models.IntegerField( - choices=[(1, "One-Way"), (2, "Two-Way")], default=2 - ), - ), - migrations.AddField( - model_name="project", - name="district", - field=models.IntegerField( - choices=[ - (1, "District 1 - Eureka"), - (2, "District 2 - Redding"), - (3, "District 3 - Marysville / Sacramento"), - (4, "District 4 - Bay Area / Oakland"), - (5, "District 5 - Central Coast"), - (6, "District 6 - Fresno / Bakersfield"), - (7, "District 7 - Los Angeles / Ventura"), - (8, "District 8 - San Bernardino / Riverside"), - (9, "District 9 - Bishop"), - (10, "District 10 - Stockton"), - (11, "District 11 - San Diego"), - (12, "District 12 - Orange County"), - ], - null=True, - ), - ), - migrations.AddField( - model_name="project", - name="location", - field=models.IntegerField( - choices=[ - (1, "Southern California"), - (2, "Northern California"), - (3, "Rural"), - ], - null=True, - ), - ), - migrations.AddField( - model_name="project", - name="peak_periods_length", - field=models.IntegerField(default=5), - ), - migrations.AddField( - model_name="project", - name="type", - field=models.CharField( - choices=[ - ( - "Highway Capacity Expansion", - [ - ("general_highway", "General Highway"), - ("hov_lane_addition", "HOV Lane Addition"), - ("hot_lane_addition", "HOT Lane Addition"), - ("passing_lane", "Passing Lane"), - ("intersection", "Intersection"), - ("truck_only_lane", "Truck Only Lane"), - ("bypass", "Bypass"), - ("queuing", "Queuing"), - ("pavement", "Pavement"), - ], - ), - ( - "Rail or Transit Capacity Expansion", - [ - ("passenger_rail", "Passenger Rail"), - ("light_rail", "Light Rail (LRT)"), - ( - "highway_rail_grade_crossing", - "Highway-Rail Grade Crossing", - ), - ], - ), - ( - "Highway Operational Improvement", - [ - ("auxiliary_lane", "Auxiliary Lane"), - ("freeway_connector", "Freeway Connector"), - ("hov_connector", "HOV Connector"), - ("hov_drop_ramp", "HOV Drop Ramp"), - ("off_ramp_widening", "Off-Ramp Widening"), - ("on_ramp_widening", "On-Ramp Widening"), - ("hot_lane_conversion", "Hot Lane Conversion"), - ], - ), - ( - "Transportation Management Systems (TMS)", - [ - ("ramp_metering", "Ramp Metering"), - ( - "ramp_metering_signal_coordination", - "Ramp Metering Signal Coordination", - ), - ("incident_management", "Incident Management"), - ("traveler_information", "Traveler Information"), - ( - "arterial_signal_management", - "Arterial Signal Management", - ), - ( - "transit_vehicle_location", - "Transit Vehicle Location (AVL)", - ), - ( - "transit_vehicle_signal_priority", - "Transit Vehicle Signal Priority", - ), - ("bus_rapid_transit", "Bus Rapid Transit (BRT)"), - ], - ), - ], - null=True, - ), - ), - ] diff --git a/src/cal_bc/projects/migrations/0004_create_model_section.py b/src/cal_bc/projects/migrations/0004_create_model_section.py new file mode 100644 index 0000000..6fccc2f --- /dev/null +++ b/src/cal_bc/projects/migrations/0004_create_model_section.py @@ -0,0 +1,37 @@ +# Generated by Django 5.2.7 on 2025-10-18 20:18 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("projects", "0003_add_project_district"), + ] + + operations = [ + migrations.CreateModel( + name="ModelSection", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(null=False)), + ("help_text", models.CharField(null=False)), + ( + "model", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="projects.model", + null=False, + ), + ), + ], + ), + ] diff --git a/src/cal_bc/projects/migrations/0005_create_model_section_field.py b/src/cal_bc/projects/migrations/0005_create_model_section_field.py new file mode 100644 index 0000000..55b7a17 --- /dev/null +++ b/src/cal_bc/projects/migrations/0005_create_model_section_field.py @@ -0,0 +1,38 @@ +# Generated by Django 5.2.7 on 2025-10-18 23:24 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("projects", "0004_create_model_section"), + ] + + operations = [ + migrations.CreateModel( + name="ModelSectionField", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(null=False)), + ("cell", models.CharField(null=False)), + ("help_text", models.CharField(null=False)), + ( + "model_section", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="projects.modelsection", + null=False, + ), + ), + ], + ) + ] diff --git a/src/cal_bc/projects/migrations/0006_create_project_field.py b/src/cal_bc/projects/migrations/0006_create_project_field.py new file mode 100644 index 0000000..8ed1483 --- /dev/null +++ b/src/cal_bc/projects/migrations/0006_create_project_field.py @@ -0,0 +1,44 @@ +# Generated by Django 5.2.7 on 2025-10-18 23:24 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("projects", "0005_create_model_section_field"), + ] + + operations = [ + migrations.CreateModel( + name="ProjectField", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("value", models.CharField(null=False)), + ( + "model_section_field", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="projects.modelsectionfield", + null=False, + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="projects.project", + null=False, + ), + ), + ], + ), + ] diff --git a/src/cal_bc/projects/models/model.py b/src/cal_bc/projects/models/model.py new file mode 100644 index 0000000..3f76bca --- /dev/null +++ b/src/cal_bc/projects/models/model.py @@ -0,0 +1,9 @@ +from django.db import models + + +class Model(models.Model): + name = models.CharField(null=False) + url = models.CharField(null=False) + + def __str__(self): + return self.name diff --git a/src/cal_bc/projects/models/model_section.py b/src/cal_bc/projects/models/model_section.py new file mode 100644 index 0000000..50982ae --- /dev/null +++ b/src/cal_bc/projects/models/model_section.py @@ -0,0 +1,14 @@ +from django.db import models + +from .model import Model + + +class ModelSection(models.Model): + name = models.CharField(null=False) + help_text = models.CharField(null=False) + model = models.ForeignKey( + Model, on_delete=models.CASCADE, related_name="sections", null=False + ) + + def __str__(self): + return self.name diff --git a/src/cal_bc/projects/models/model_section_field.py b/src/cal_bc/projects/models/model_section_field.py new file mode 100644 index 0000000..d07668f --- /dev/null +++ b/src/cal_bc/projects/models/model_section_field.py @@ -0,0 +1,18 @@ +from django.db import models + +from .model_section import ModelSection + + +class ModelSectionField(models.Model): + name = models.CharField(null=False) + cell = models.CharField(null=False) + help_text = models.CharField(null=False) + section = models.ForeignKey( + ModelSection, + on_delete=models.CASCADE, + related_name="section_fields", + null=False, + ) + + def __str__(self): + return self.name diff --git a/src/cal_bc/projects/models/project.py b/src/cal_bc/projects/models/project.py index 2c0efde..6294727 100644 --- a/src/cal_bc/projects/models/project.py +++ b/src/cal_bc/projects/models/project.py @@ -1,6 +1,8 @@ from django.db import models from django.utils.translation import gettext as _ +from .model import Model + class Project(models.Model): class District(models.IntegerChoices): @@ -17,83 +19,86 @@ class District(models.IntegerChoices): ELEVEN = 11, _("District 11 - San Diego") TWELVE = 12, _("District 12 - Orange County") - class HighwayCapacityType(models.TextChoices): - GENERAL_HIGHWAY = "general_highway", _("General Highway") - HOV_LANE_ADDITION = "hov_lane_addition", _("HOV Lane Addition") - HOT_LANE_ADDITION = "hot_lane_addition", _("HOT Lane Addition") - PASSING_LANE = "passing_lane", _("Passing Lane") - INTERSECTION = "intersection", _("Intersection") - TRUCK_ONLY_LANE = "truck_only_lane", _("Truck Only Lane") - BYPASS = "bypass", _("Bypass") - QUEUING = "queuing", _("Queuing") - PAVEMENT = "pavement", _("Pavement") + # class HighwayCapacityType(models.TextChoices): + # GENERAL_HIGHWAY = "general_highway", _("General Highway") + # HOV_LANE_ADDITION = "hov_lane_addition", _("HOV Lane Addition") + # HOT_LANE_ADDITION = "hot_lane_addition", _("HOT Lane Addition") + # PASSING_LANE = "passing_lane", _("Passing Lane") + # INTERSECTION = "intersection", _("Intersection") + # TRUCK_ONLY_LANE = "truck_only_lane", _("Truck Only Lane") + # BYPASS = "bypass", _("Bypass") + # QUEUING = "queuing", _("Queuing") + # PAVEMENT = "pavement", _("Pavement") - class RailTransitCapacityType(models.TextChoices): - PASSENGER_RAIL = "passenger_rail", _("Passenger Rail") - LIGHT_RAIL = "light_rail", _("Light Rail (LRT)") - HIGHWAY_RAIL_GRADE_CROSSING = ( - "highway_rail_grade_crossing", - _("Highway-Rail Grade Crossing"), - ) + # class RailTransitCapacityType(models.TextChoices): + # PASSENGER_RAIL = "passenger_rail", _("Passenger Rail") + # LIGHT_RAIL = "light_rail", _("Light Rail (LRT)") + # HIGHWAY_RAIL_GRADE_CROSSING = ( + # "highway_rail_grade_crossing", + # _("Highway-Rail Grade Crossing"), + # ) - class HighwayOperationalType(models.TextChoices): - AUXILIARY_LANE = "auxiliary_lane", _("Auxiliary Lane") - FREEWAY_CONNECTOR = "freeway_connector", _("Freeway Connector") - HOV_CONNECTOR = "hov_connector", _("HOV Connector") - HOV_DROP_RAMP = "hov_drop_ramp", _("HOV Drop Ramp") - OFF_RAMP_WIDENING = "off_ramp_widening", _("Off-Ramp Widening") - ON_RAMP_WIDENING = "on_ramp_widening", _("On-Ramp Widening") - HOT_LANE_CONVERSION = "hot_lane_conversion", _("Hot Lane Conversion") + # class HighwayOperationalType(models.TextChoices): + # AUXILIARY_LANE = "auxiliary_lane", _("Auxiliary Lane") + # FREEWAY_CONNECTOR = "freeway_connector", _("Freeway Connector") + # HOV_CONNECTOR = "hov_connector", _("HOV Connector") + # HOV_DROP_RAMP = "hov_drop_ramp", _("HOV Drop Ramp") + # OFF_RAMP_WIDENING = "off_ramp_widening", _("Off-Ramp Widening") + # ON_RAMP_WIDENING = "on_ramp_widening", _("On-Ramp Widening") + # HOT_LANE_CONVERSION = "hot_lane_conversion", _("Hot Lane Conversion") - class TransportationManagmentSystemsType(models.TextChoices): - RAMP_METERING = "ramp_metering", _("Ramp Metering") - RAMP_METERING_SIGNAL_COORDINATION = ( - "ramp_metering_signal_coordination", - _("Ramp Metering Signal Coordination"), - ) - INCIDENT_MANAGEMENT = "incident_management", _("Incident Management") - TRAVELER_INFORMATION = "traveler_information", _("Traveler Information") - ARTERIAL_SIGNAL_MANAGEMENT = ( - "arterial_signal_management", - _("Arterial Signal Management"), - ) - TRANSIT_VEHICLE_LOCATION = ( - "transit_vehicle_location", - _("Transit Vehicle Location (AVL)"), - ) - TRANSIT_VEHICLE_SIGNAL_PRIORITY = ( - "transit_vehicle_signal_priority", - _("Transit Vehicle Signal Priority"), - ) - BUS_RAPID_TRANSIT = "bus_rapid_transit", _("Bus Rapid Transit (BRT)") + # class TransportationManagmentSystemsType(models.TextChoices): + # RAMP_METERING = "ramp_metering", _("Ramp Metering") + # RAMP_METERING_SIGNAL_COORDINATION = ( + # "ramp_metering_signal_coordination", + # _("Ramp Metering Signal Coordination"), + # ) + # INCIDENT_MANAGEMENT = "incident_management", _("Incident Management") + # TRAVELER_INFORMATION = "traveler_information", _("Traveler Information") + # ARTERIAL_SIGNAL_MANAGEMENT = ( + # "arterial_signal_management", + # _("Arterial Signal Management"), + # ) + # TRANSIT_VEHICLE_LOCATION = ( + # "transit_vehicle_location", + # _("Transit Vehicle Location (AVL)"), + # ) + # TRANSIT_VEHICLE_SIGNAL_PRIORITY = ( + # "transit_vehicle_signal_priority", + # _("Transit Vehicle Signal Priority"), + # ) + # BUS_RAPID_TRANSIT = "bus_rapid_transit", _("Bus Rapid Transit (BRT)") - TYPE_CHOICES = { - _("Highway Capacity Expansion"): HighwayCapacityType, - _("Rail or Transit Capacity Expansion"): RailTransitCapacityType, - _("Highway Operational Improvement"): HighwayOperationalType, - _( - "Transportation Management Systems (TMS)" - ): TransportationManagmentSystemsType, - } + # TYPE_CHOICES = { + # _("Highway Capacity Expansion"): HighwayCapacityType, + # _("Rail or Transit Capacity Expansion"): RailTransitCapacityType, + # _("Highway Operational Improvement"): HighwayOperationalType, + # _( + # "Transportation Management Systems (TMS)" + # ): TransportationManagmentSystemsType, + # } - class Location(models.IntegerChoices): - SO_CAL = 1, _("Southern California") - NO_CAL = 2, _("Northern California") - RURAL = 3, _("Rural") + # class Location(models.IntegerChoices): + # SO_CAL = 1, _("Southern California") + # NO_CAL = 2, _("Northern California") + # RURAL = 3, _("Rural") - class DataDirection(models.IntegerChoices): - ONE_WAY = 1, _("One-Way") - TWO_WAY = 2, _("Two-Way") + # class DataDirection(models.IntegerChoices): + # ONE_WAY = 1, _("One-Way") + # TWO_WAY = 2, _("Two-Way") - name = models.CharField() - district = models.IntegerField(choices=District, null=True) - type = models.CharField(choices=TYPE_CHOICES, null=True) - location = models.IntegerField(choices=Location, null=True) - construction_period_length = models.IntegerField(null=True) - data_direction = models.IntegerField( - choices=DataDirection, default=DataDirection.TWO_WAY + name = models.CharField(null=False) + district = models.IntegerField(choices=District, null=False) + model = models.ForeignKey( + Model, on_delete=models.CASCADE, related_name="projects", null=False ) - peak_periods_length = models.IntegerField(default=5) + # type = models.CharField(choices=TYPE_CHOICES, null=True) + # location = models.IntegerField(choices=Location, null=True) + # construction_period_length = models.IntegerField(null=True) + # data_direction = models.IntegerField( + # choices=DataDirection, default=DataDirection.TWO_WAY + # ) + # peak_periods_length = models.IntegerField(default=5) def __str__(self): return self.name diff --git a/src/cal_bc/projects/models/project_field.py b/src/cal_bc/projects/models/project_field.py new file mode 100644 index 0000000..d4d2260 --- /dev/null +++ b/src/cal_bc/projects/models/project_field.py @@ -0,0 +1,20 @@ +from django.db import models + +from .model_section_field import ModelSectionField +from .project import Project + + +class ProjectField(models.Model): + project = models.ForeignKey( + Project, on_delete=models.CASCADE, related_name="fields", null=False + ) + model_section_field = models.ForeignKey( + ModelSectionField, + on_delete=models.CASCADE, + related_name="project_fields", + null=False, + ) + value = models.CharField(null=False) + + def __str__(self): + return self.value diff --git a/src/cal_bc/projects/templates/projects/_form.html b/src/cal_bc/projects/templates/projects/_form.html index 33256ea..998222c 100644 --- a/src/cal_bc/projects/templates/projects/_form.html +++ b/src/cal_bc/projects/templates/projects/_form.html @@ -51,7 +51,7 @@

General Info {% if editable|default_if_none:True %} {% else %} - Edit Project @@ -59,4 +59,4 @@

General Info - \ No newline at end of file + diff --git a/src/cal_bc/projects/templates/projects/index.html b/src/cal_bc/projects/templates/projects/index.html index 7b3af46..8a03400 100644 --- a/src/cal_bc/projects/templates/projects/index.html +++ b/src/cal_bc/projects/templates/projects/index.html @@ -10,7 +10,7 @@

Projects

- New Project + New Project
@@ -36,7 +36,7 @@

Projects< {{ project }} - Edit + Edit {% endfor %} diff --git a/src/cal_bc/projects/urls.py b/src/cal_bc/projects/urls.py index 8602135..3c80f81 100644 --- a/src/cal_bc/projects/urls.py +++ b/src/cal_bc/projects/urls.py @@ -8,12 +8,20 @@ ProjectDetailView, ) +from .views.project_section import ProjectSectionEditView + + urlpatterns = [ path( "", TemplateView.as_view(template_name="landings/index.html"), name="landings" ), path("projects/", ProjectListView.as_view(), name="projects"), - path("projects/new", ProjectNewView.as_view(), name="project_new"), - path("projects//edit", ProjectEditView.as_view(), name="project_edit"), - path("projects//show", ProjectDetailView.as_view(), name="project"), + path("projects/new", ProjectNewView.as_view(), name="new_project"), + path("projects//", ProjectDetailView.as_view(), name="project"), + path("projects//edit", ProjectEditView.as_view(), name="edit_project"), + path( + "projects//sections//edit", + ProjectSectionEditView.as_view(), + name="edit_project_section", + ), ] diff --git a/src/cal_bc/projects/views/project_section.py b/src/cal_bc/projects/views/project_section.py new file mode 100644 index 0000000..389cf7f --- /dev/null +++ b/src/cal_bc/projects/views/project_section.py @@ -0,0 +1,38 @@ +from django.contrib.auth.mixins import LoginRequiredMixin +from django.forms import inlineformset_factory +from django.views.generic.edit import UpdateView +from cal_bc.projects.models.project_field import ProjectField +from cal_bc.projects.models.model_section_field import ModelSectionField +from cal_bc.projects.models.model_section import ModelSection +from cal_bc.projects.forms.model_section import ModelSectionForm + + +ProjectFieldFormset = inlineformset_factory( + ModelSectionField, ProjectField, fields=["value"] +) + + +class ProjectSectionEditView(LoginRequiredMixin, UpdateView): + model = ModelSection + form_class = ModelSectionForm + template_name = "project_sections/edit.html" + + def get_context_data(self, **kwargs): + data = super(ProjectSectionEditView, self).get_context_data(**kwargs) + formset = None + if self.request.POST: + formset = ProjectFieldFormset(self.request.POST, instance=self.object) + formset.full_clean() + else: + formset = ProjectFieldFormset(instance=self.object) + data["model_section_fields"] = formset + return data + + def form_valid(self, form): + context = self.get_context_data() + formset = context["model_section_fields"] + self.object = form.save() + if formset.is_valid(): + formset.instance = self.object() + formset.save() + return super(ProjectSectionEditView, self).form_valid(form) diff --git a/src/cal_bc_calculator/app.py b/src/cal_bc_calculator/app.py index 65ef8ea..30dd087 100644 --- a/src/cal_bc_calculator/app.py +++ b/src/cal_bc_calculator/app.py @@ -5,7 +5,8 @@ from typing_extensions import Annotated, List from .calculator import Calculator -from .downloader import Downloader, VERSIONS +from .downloader import Downloader +from .versions import VERSIONS app = typer.Typer() diff --git a/src/cal_bc_calculator/downloader.py b/src/cal_bc_calculator/downloader.py index 5ab0134..4bd340b 100644 --- a/src/cal_bc_calculator/downloader.py +++ b/src/cal_bc_calculator/downloader.py @@ -2,13 +2,7 @@ import urllib.request from tqdm import tqdm - -VERSIONS = { - "cal-bc-8-1-sketch": { - "name": "Cal-B/C Sketch v8.1", - "url": "https://dot.ca.gov/-/media/dot-media/programs/transportation-planning/documents/new-state-planning/transportation-economics/cal-bc/2023-cal-bc/2023-non-federal-model/cal-bc-8-1-sketch-a11y.xlsm", - }, -} +from .versions import VERSIONS def progress(t): diff --git a/src/cal_bc_calculator/versions.py b/src/cal_bc_calculator/versions.py new file mode 100644 index 0000000..6687000 --- /dev/null +++ b/src/cal_bc_calculator/versions.py @@ -0,0 +1,6 @@ +VERSIONS = { + "cal-bc-8-1-sketch": { + "name": "Cal-B/C Sketch v8.1", + "url": "https://dot.ca.gov/-/media/dot-media/programs/transportation-planning/documents/new-state-planning/transportation-economics/cal-bc/2023-cal-bc/2023-non-federal-model/cal-bc-8-1-sketch-a11y.xlsm", + }, +} diff --git a/tests/cal_bc/models/test_model.py b/tests/cal_bc/models/test_model.py new file mode 100644 index 0000000..e8569aa --- /dev/null +++ b/tests/cal_bc/models/test_model.py @@ -0,0 +1,15 @@ +import pytest +from cal_bc.projects.models.model import Model + + +@pytest.mark.django_db +class TestModel: + @pytest.fixture + def model(self) -> Model: + return Model.objects.create( + name="Testing 0.0", + url="https://example.com", + ) + + def test_string_representation(self, model: Model) -> None: + assert str(model) == "Testing 0.0" diff --git a/tests/cal_bc/models/test_model_section.py b/tests/cal_bc/models/test_model_section.py new file mode 100644 index 0000000..aada079 --- /dev/null +++ b/tests/cal_bc/models/test_model_section.py @@ -0,0 +1,24 @@ +import pytest +from cal_bc.projects.models.model import Model +from cal_bc.projects.models.model_section import ModelSection + + +@pytest.mark.django_db +class TestModelSection: + @pytest.fixture + def model(self) -> Model: + return Model.objects.create( + name="Testing 0.0", + url="https://example.com", + ) + + @pytest.fixture + def section(self, model: Model) -> ModelSection: + return ModelSection.objects.create( + name="1A", + help_text="Do a barrel roll", + model=model, + ) + + def test_string_representation(self, section: ModelSection) -> None: + assert str(section) == "1A" diff --git a/tests/cal_bc/models/test_model_section_field.py b/tests/cal_bc/models/test_model_section_field.py new file mode 100644 index 0000000..f3c09a1 --- /dev/null +++ b/tests/cal_bc/models/test_model_section_field.py @@ -0,0 +1,34 @@ +import pytest +from cal_bc.projects.models.model import Model +from cal_bc.projects.models.model_section import ModelSection +from cal_bc.projects.models.model_section_field import ModelSectionField + + +@pytest.mark.django_db +class TestModelSectionField: + @pytest.fixture + def model(self) -> Model: + return Model.objects.create( + name="Testing 0.0", + url="https://example.com", + ) + + @pytest.fixture + def section(self, model: Model) -> ModelSection: + return ModelSection.objects.create( + name="1A", + help_text="Do a barrel roll", + model=model, + ) + + @pytest.fixture + def field(self, section: ModelSection) -> ModelSectionField: + return ModelSectionField.objects.create( + name="Distance", + cell="ProjectDistance", + help_text="Jump in a lake", + model_section=section, + ) + + def test_string_representation(self, field: ModelSectionField) -> None: + assert str(field) == "Distance" diff --git a/tests/cal_bc/models/test_project.py b/tests/cal_bc/models/test_project.py index e4ac93c..8798e09 100644 --- a/tests/cal_bc/models/test_project.py +++ b/tests/cal_bc/models/test_project.py @@ -1,9 +1,24 @@ import pytest from cal_bc.projects.models.project import Project +from cal_bc.projects.models.model import Model @pytest.mark.django_db class TestProject: - def test_string_representation(self): - project = Project.objects.create(name="Geary Boulevard MUNI Train") - assert str(project) == "Geary Boulevard MUNI Train" + @pytest.fixture + def model(self) -> Model: + return Model.objects.create( + name="Testing 0.0", + url="https://example.com", + ) + + @pytest.fixture + def project(self, model: Model) -> Project: + return Project.objects.create( + name="Geary LRT", + district=Project.District.FOUR, + model=model, + ) + + def test_string_representation(self, project: Project) -> None: + assert str(project) == "Geary LRT" diff --git a/tests/cal_bc/models/test_project_field.py b/tests/cal_bc/models/test_project_field.py new file mode 100644 index 0000000..34b985a --- /dev/null +++ b/tests/cal_bc/models/test_project_field.py @@ -0,0 +1,50 @@ +import pytest +from cal_bc.projects.models.project_field import ProjectField +from cal_bc.projects.models.project import Project +from cal_bc.projects.models.model import Model +from cal_bc.projects.models.model_section import ModelSection +from cal_bc.projects.models.model_section_field import ModelSectionField + + +@pytest.mark.django_db +class TestProjectField: + @pytest.fixture + def model(self) -> Model: + return Model.objects.create( + name="Testing 0.0", + url="https://example.com", + ) + + @pytest.fixture + def section(self, model: Model) -> ModelSection: + return ModelSection.objects.create( + name="1A", + help_text="Do a barrel roll", + model=model, + ) + + @pytest.fixture + def field(self, section: ModelSection) -> ModelSectionField: + return ModelSectionField.objects.create( + name="Distance", + cell="ProjectDistance", + help_text="Jump in a lake", + model_section=section, + ) + + @pytest.fixture + def project(self, model: Model) -> Project: + return Project.objects.create( + name="Geary LRT", + district=Project.District.FOUR, + model=model, + ) + + @pytest.fixture + def project_field(self, project: Project, field: ModelSectionField) -> ProjectField: + return ProjectField.objects.create( + project=project, model_section_field=field, value="Okay" + ) + + def test_string_representation(self, project_field: ProjectField) -> None: + assert str(project_field) == "Okay" diff --git a/tests/cal_bc/test_projects.py b/tests/cal_bc/test_projects.py index 31585e1..f3d4b6e 100644 --- a/tests/cal_bc/test_projects.py +++ b/tests/cal_bc/test_projects.py @@ -3,11 +3,18 @@ from playwright.sync_api import Page from django.contrib.auth.models import User +from cal_bc.projects.models.model import Model +from cal_bc_calculator.versions import VERSIONS + class TestProjects(StaticLiveServerTestCase): @pytest.fixture(autouse=True) def setup(self, page: Page): self.page = page + model = Model.objects.create( + name=VERSIONS["cal-bc-8-1-sketch"]["name"], + url=VERSIONS["cal-bc-8-1-sketch"]["url"], + ) user = User.objects.create_user(username="caltrans") self.client.force_login(user) cookie = self.client.cookies["sessionid"] @@ -32,12 +39,15 @@ def test_projects(self): self.page.get_by_label("District").select_option( "District 4 - Bay Area / Oakland" ) + self.page.get_by_label("Model").select_option("Cal-B/C Sketch v8.1") + self.page.get_by_role("button", name="Save Project").click() + self.page.wait_for_selector("text=Section 1A") self.page.get_by_label("Project Type").select_option("Light Rail (LRT)") self.page.get_by_label("Project Location").select_option("Northern California") self.page.get_by_label("Length of Construction Period").fill("3") self.page.get_by_label("One- or Two-Way Data").select_option("Two-Way") self.page.get_by_label("Length of Peak Period(s) (up to 24 hrs)").fill("3") - self.page.get_by_role("button", name="Save Project").click() + self.page.get_by_role("button", name="Save Section").click() self.page.wait_for_selector("text=Geary Boulevard Light Rail") self.page.get_by_role("button", name="Sign out caltrans").click() assert self.page.get_by_role("link", name="Sign in with Microsoft") diff --git a/tests/cal_bc/views/test_project_sections.py b/tests/cal_bc/views/test_project_sections.py new file mode 100644 index 0000000..8e31fb8 --- /dev/null +++ b/tests/cal_bc/views/test_project_sections.py @@ -0,0 +1,52 @@ +from django.contrib.auth.models import User +from cal_bc.projects.models.model import Model +from cal_bc.projects.models.model_section import ModelSection +from cal_bc.projects.models.project import Project +from unbrowsed import parse_html, query_by_text +import pytest + + +class TestProjectsViews: + @pytest.fixture + def user(self, django_user_model) -> User: + return django_user_model.objects.create_user(username="caltrans") + + @pytest.fixture + def model(self) -> Model: + return Model.objects.create( + name="Testing 0.0", + url="https://example.com", + ) + + @pytest.fixture + def section(self, model: Model) -> ModelSection: + return ModelSection.objects.create( + name="1A", + help_text="Please fill in this section", + model=model, + ) + + @pytest.fixture + def project(self, model: Model) -> Project: + return Project.objects.create( + name="Monterey LRT", + district=Project.District.FIVE, + model=model, + ) + + def test_edit_project_section( + self, + client, + user: User, + project: Project, + section: ModelSection, + ) -> None: + client.force_login(user) + response = client.get(f"/projects/{project.pk}/section/{section.pk}/edit") + assert response.status_code == 200 + + dom = parse_html(response.content) + + assert query_by_text(dom, "1A") + assert query_by_text(dom, "Please fill in this section") + assert query_by_text(dom, "Save Section") diff --git a/tests/cal_bc/views/test_projects.py b/tests/cal_bc/views/test_projects.py index 34e0868..2efafac 100644 --- a/tests/cal_bc/views/test_projects.py +++ b/tests/cal_bc/views/test_projects.py @@ -1,4 +1,5 @@ from django.contrib.auth.models import User +from cal_bc.projects.models.model import Model from cal_bc.projects.models.project import Project from unbrowsed import parse_html, query_by_text, query_by_label_text import pytest @@ -10,16 +11,21 @@ def user(self, django_user_model) -> User: return django_user_model.objects.create_user(username="caltrans") @pytest.fixture - def project(self) -> Project: + def model(self) -> Model: + return Model.objects.create( + name="Testing 0.0", + url="https://example.com", + ) + + @pytest.fixture + def project(self, model: Model) -> Project: return Project.objects.create( name="Monterey LRT", district=Project.District.FIVE, - type=Project.RailTransitCapacityType.LIGHT_RAIL, - location=Project.Location.NO_CAL, - construction_period_length=4, + model=model, ) - def test_with_projects_index(self, client, user): + def test_projects(self, client, user): client.force_login(user) response = client.get("/projects/") assert response.status_code == 200 @@ -29,7 +35,7 @@ def test_with_projects_index(self, client, user): page = query_by_text(dom, "Projects", exact=False) assert page.to_have_text_content("Showing1of1pages", exact=False) - def test_with_project_new(self, client, user): + def test_new_project(self, client, user): client.force_login(user) response = client.get("/projects/new") assert response.status_code == 200 @@ -37,68 +43,37 @@ def test_with_project_new(self, client, user): assert query_by_text(dom, "Project Data") assert query_by_label_text(dom, "Project Name") assert query_by_label_text(dom, "District") - assert query_by_label_text(dom, "Project Type") - assert query_by_label_text(dom, "Project Location") - assert query_by_label_text(dom, "Length of Construction Period") - assert query_by_label_text(dom, "One- or Two-Way Data") - assert query_by_label_text(dom, "Length of Peak Period(s) (up to 24 hrs)") + assert query_by_label_text(dom, "Model") assert query_by_text(dom, "Save Project") - def test_with_project_edit(self, client, user, project): + def test_edit_project(self, client, user, model, project): client.force_login(user) response = client.get(f"/projects/{project.pk}/edit") assert response.status_code == 200 dom = parse_html(response.content) assert query_by_text(dom, "Project Data") - project_name = query_by_label_text(dom, "Project Name") - district = query_by_label_text(dom, "District") - project_type = query_by_label_text(dom, "Project Type") - project_location = query_by_label_text(dom, "Project Location") - construction_length = query_by_label_text(dom, "Length of Construction Period") - data_direction = query_by_label_text(dom, "One- or Two-Way Data") - peak_period = query_by_label_text( - dom, "Length of Peak Period(s) (up to 24 hrs)" - ) - assert project_name.element.attrs["value"] == "Monterey LRT" - assert district.element.select("[value='5'][selected]").any_matches - assert project_type.element.select("[value='light_rail'][selected]").any_matches - assert project_location.element.select("[value='2'][selected]").any_matches - assert construction_length.element.attrs["value"] == "4" - assert data_direction.element.select("[value='2'][selected]").any_matches - assert peak_period.element.attrs["value"] == "5" + project_name_field = query_by_label_text(dom, "Project Name") + district_field = query_by_label_text(dom, "District") + model_field = query_by_label_text(dom, "Model") + assert project_name_field.element.attrs["value"] == "Monterey LRT" + assert district_field.element.select("[value='5'][selected]").any_matches + assert model_field.element.select(f"[value='{model.pk}'][selected]").any_matches assert query_by_text(dom, "Save Project") - def test_with_project_show(self, client, user, project): + def test_show_project(self, client, user, model, project): client.force_login(user) - response = client.get(f"/projects/{project.pk}/show") + response = client.get(f"/projects/{project.pk}/") assert response.status_code == 200 dom = parse_html(response.content) assert query_by_text(dom, "Project Data") - project_name = query_by_label_text(dom, "Project Name") - district = query_by_label_text(dom, "District") - project_type = query_by_label_text(dom, "Project Type") - project_location = query_by_label_text(dom, "Project Location") - construction_length = query_by_label_text(dom, "Length of Construction Period") - data_direction = query_by_label_text(dom, "One- or Two-Way Data") - peak_period = query_by_label_text( - dom, "Length of Peak Period(s) (up to 24 hrs)" - ) - assert project_name.element.attrs["value"] == "Monterey LRT" - assert project_name.element.select("[disabled]").any_matches - assert district.element.select("[value='5'][selected]").any_matches - assert district.element.select("[disabled]").any_matches - assert project_type.element.select("[value='light_rail'][selected]").any_matches - assert project_type.element.select("[disabled]").any_matches - assert project_location.element.select("[value='2'][selected]").any_matches - assert project_location.element.select("[disabled]").any_matches - assert construction_length.element.attrs["value"] == "4" - assert construction_length.element.select("[disabled]").any_matches - assert data_direction.element.select("[value='2'][selected]").any_matches - assert data_direction.element.select("[disabled]").any_matches - assert peak_period.element.attrs["value"] == "5" - assert peak_period.element.select("[disabled]").any_matches + project_name_field = query_by_label_text(dom, "Project Name") + district_field = query_by_label_text(dom, "District") + model_field = query_by_label_text(dom, "Model") + assert project_name_field.element.attrs["value"] == "Monterey LRT" + assert district_field.element.select("[value='5'][selected]").any_matches + assert model_field.element.select(f"[value='{model.pk}'][selected]").any_matches assert query_by_text(dom, "Edit Project")