From 53ab7317872ce40c47f73877357a689276b3b047106945e23a6ddde3de42d79d Mon Sep 17 00:00:00 2001 From: Johannes Randerath Date: Wed, 3 Jul 2024 01:18:17 +0200 Subject: [PATCH] Cookies and tests - Added cookie banner - Improved tests and fixed models accordingly --- transport_accessibility/pt_map/class_names.py | 24 +++--- ...ion_alter_stoptime_location_id_and_more.py | 34 ++++++++ ...me_location_type_location_geometry_type.py | 18 ++++ ...24_remove_farerule_contains_id_and_more.py | 40 +++++++++ ...5_alter_farelegrule_network_id_and_more.py | 29 +++++++ ...026_alter_farerule_contains_id_and_more.py | 28 ++++++ ...egrule_from_timeframe_group_id_and_more.py | 61 +++++++++++++ .../pt_map/model_test_fields.py | 50 +++++++++-- transport_accessibility/pt_map/models.py | 53 ++++++------ .../pt_map/templates/base.html | 52 +++++++++++ .../pt_map/templates/map.html | 86 ++++--------------- transport_accessibility/pt_map/tests.py.m4 | 36 -------- .../pt_map/tests_gtfs_compliance.py | 86 +++++++++++++++---- transport_accessibility/pt_map/views.py | 1 + transport_accessibility/tmp | 1 - 15 files changed, 434 insertions(+), 165 deletions(-) create mode 100644 transport_accessibility/pt_map/migrations/0022_location_alter_stoptime_location_id_and_more.py create mode 100644 transport_accessibility/pt_map/migrations/0023_rename_location_type_location_geometry_type.py create mode 100644 transport_accessibility/pt_map/migrations/0024_remove_farerule_contains_id_and_more.py create mode 100644 transport_accessibility/pt_map/migrations/0025_alter_farelegrule_network_id_and_more.py create mode 100644 transport_accessibility/pt_map/migrations/0026_alter_farerule_contains_id_and_more.py create mode 100644 transport_accessibility/pt_map/migrations/0027_remove_farelegrule_from_timeframe_group_id_and_more.py create mode 100644 transport_accessibility/pt_map/templates/base.html delete mode 100644 transport_accessibility/pt_map/tests.py.m4 delete mode 100644 transport_accessibility/tmp diff --git a/transport_accessibility/pt_map/class_names.py b/transport_accessibility/pt_map/class_names.py index 34a30c3..798ac09 100644 --- a/transport_accessibility/pt_map/class_names.py +++ b/transport_accessibility/pt_map/class_names.py @@ -34,7 +34,7 @@ class_names = [ "Level": pt_map.models.Level, "LocationGroup": pt_map.models.LocationGroup, "LocationGroupStop": pt_map.models.LocationGroupStop, - "LocationsGeojson": pt_map.models.LocationsGeojson, + "Location": pt_map.models.Location, "BookingRule": pt_map.models.BookingRule, "Translation": pt_map.models.Translation, "FeedInfo": pt_map.models.FeedInfo, @@ -66,7 +66,7 @@ class_names = [ "levels": pt_map.models.Level, "location_groups": pt_map.models.LocationGroup, "location_group_stops": pt_map.models.LocationGroupStop, - "locations": pt_map.models.LocationsGeojson, + "locations": pt_map.models.Location, "booking_rules": pt_map.models.BookingRule, "translations": pt_map.models.Translation, "feed_info": pt_map.models.FeedInfo, @@ -98,7 +98,7 @@ class_names = [ pt_map.models.Level: "Level", pt_map.models.LocationGroup: "LocationGroup", pt_map.models.LocationGroupStop: "LocationGroupStop", - pt_map.models.LocationsGeojson: "LocationsGeojson", + pt_map.models.Location: "Location", pt_map.models.BookingRule: "BookingRule", pt_map.models.Translation: "Translation", pt_map.models.FeedInfo: "FeedInfo", @@ -130,7 +130,7 @@ pt_map.models.Shape: "shapes", pt_map.models.Level: "levels", pt_map.models.LocationGroup: "location_groups", pt_map.models.LocationGroupStop: "location_group_stops", - pt_map.models.LocationsGeojson: "locations", + pt_map.models.Location: "locations", pt_map.models.BookingRule: "booking_rules", pt_map.models.Translation: "translations", pt_map.models.FeedInfo: "feed_info", @@ -166,7 +166,7 @@ file_names = { pt_map.models.Level: "levels.txt", pt_map.models.LocationGroup: "location_groups.txt", pt_map.models.LocationGroupStop: "location_group_stops.txt", - pt_map.models.LocationsGeojson: "locations.geojson", + pt_map.models.Location: "locations.geojson", pt_map.models.BookingRule: "booking_rules.txt", pt_map.models.Translation: "translations.txt", pt_map.models.FeedInfo: "feed_info.txt", @@ -199,7 +199,7 @@ reversed_file_mapping = { "Level": "levels", "LocationGroup": "location_groups", "LocationGroupStop": "location_group_stops", - "LocationsGeojson": "locations_geojson", + "Location": "locations_geojson", "BookingRule": "booking_rules", "Translation": "translations", "FeedInfo": "feed_info", @@ -207,7 +207,7 @@ reversed_file_mapping = { } -case_swap = {'Agency': 'agency', 'Stop': 'stops', 'Route': 'routes', 'Trip': 'trips', 'StopTime': 'stop_times', 'Calendar': 'calendar', 'CalendarDate': 'calendar_dates', 'FareAttribute': 'fare_attributes', 'FareRule': 'fare_rules', 'Timeframe': 'timeframes', 'FareMedium': 'fare_media', 'FareProduct': 'fare_products', 'FareLegRule': 'fare_leg_rules', 'FareTransferRule': 'fare_transfer_rules', 'Area': 'areas', 'StopArea': 'stop_areas', 'Network': 'networks', 'RouteNetwork': 'route_networks', 'Shape': 'shapes', 'Frequency': 'frequencies', 'Transfer': 'transfers', 'Pathway': 'pathways', 'Level': 'levels', 'LocationGroup': 'location_groups', 'LocationGroupStop': 'location_group_stops', 'LocationsGeojson': 'locations_geojson', 'BookingRule': 'booking_rules', 'Translation': 'translations', 'FeedInfo': 'feed_info', 'Attribution': 'attributions'} +case_swap = {'Agency': 'agency', 'Stop': 'stops', 'Route': 'routes', 'Trip': 'trips', 'StopTime': 'stop_times', 'Calendar': 'calendar', 'CalendarDate': 'calendar_dates', 'FareAttribute': 'fare_attributes', 'FareRule': 'fare_rules', 'Timeframe': 'timeframes', 'FareMedium': 'fare_media', 'FareProduct': 'fare_products', 'FareLegRule': 'fare_leg_rules', 'FareTransferRule': 'fare_transfer_rules', 'Area': 'areas', 'StopArea': 'stop_areas', 'Network': 'networks', 'RouteNetwork': 'route_networks', 'Shape': 'shapes', 'Frequency': 'frequencies', 'Transfer': 'transfers', 'Pathway': 'pathways', 'Level': 'levels', 'LocationGroup': 'location_groups', 'LocationGroupStop': 'location_group_stops', 'Location': 'locations_geojson', 'BookingRule': 'booking_rules', 'Translation': 'translations', 'FeedInfo': 'feed_info', 'Attribution': 'attributions'} primary_keys = { @@ -221,7 +221,7 @@ primary_keys = { pt_map.models.CalendarDate: "calendar_date_id", pt_map.models.Trip: "trip_id", pt_map.models.LocationGroup: "location_group_id", - pt_map.models.LocationsGeojson: "location_id", + pt_map.models.Location: "location_id", pt_map.models.StopTime: "stop_time_id", pt_map.models.FareAttribute: "fare_id", pt_map.models.FareRule: "fare_rule_id", @@ -255,7 +255,7 @@ classes_by_primary_keys = { 'calendar_date_id': pt_map.models.CalendarDate, 'trip_id': pt_map.models.Trip, 'location_group_id': pt_map.models.LocationGroup, - 'location_id': pt_map.models.LocationsGeojson, + 'location_id': pt_map.models.Location, 'stop_time_id': pt_map.models.StopTime, 'fare_id': pt_map.models.FareAttribute, 'fare_rule_id': pt_map.models.FareRule, @@ -288,8 +288,8 @@ foreign_keys = [ (pt_map.models.CalendarDate, [(pt_map.models.FeedInfo, 'feed_info_id'),]), (pt_map.models.Trip, [(pt_map.models.FeedInfo, 'feed_info_id'),(pt_map.models.Route, 'route_id'), (pt_map.models.Shape, 'shape_id'), ]), (pt_map.models.LocationGroup, [(pt_map.models.FeedInfo, 'feed_info_id'),]), - (pt_map.models.LocationsGeojson, [(pt_map.models.FeedInfo, 'feed_info_id'),]), - (pt_map.models.StopTime, [(pt_map.models.FeedInfo, 'feed_info_id'),(pt_map.models.Trip, 'trip_id'), (pt_map.models.Stop, 'stop_id'), (pt_map.models.LocationGroup, 'location_group_id'), (pt_map.models.LocationsGeojson, 'location_id'), ]), + (pt_map.models.Location, [(pt_map.models.FeedInfo, 'feed_info_id'),]), + (pt_map.models.StopTime, [(pt_map.models.FeedInfo, 'feed_info_id'),(pt_map.models.Trip, 'trip_id'), (pt_map.models.Stop, 'stop_id'), (pt_map.models.LocationGroup, 'location_group_id'), (pt_map.models.Location, 'location_id'), ]), (pt_map.models.FareAttribute, [(pt_map.models.FeedInfo, 'feed_info_id'),(pt_map.models.Agency, 'agency_id'), ]), (pt_map.models.FareRule, [(pt_map.models.FeedInfo, 'feed_info_id'),(pt_map.models.FareAttribute, 'fare_id'), (pt_map.models.Route, 'route_id'), ]), (pt_map.models.Frequency, [(pt_map.models.FeedInfo, 'feed_info_id'),(pt_map.models.Trip, 'trip_id'), ]), @@ -325,7 +325,7 @@ fks = { 'trip_id': pt_map.models.Trip, 'stop_id': pt_map.models.Stop, 'location_group_id': pt_map.models.LocationGroup, - 'location_id': pt_map.models.LocationsGeojson, + 'location_id': pt_map.models.Location, 'fare_id': pt_map.models.FareAttribute, 'from_stop_id': pt_map.models.Stop, 'to_stop_id': pt_map.models.Stop, diff --git a/transport_accessibility/pt_map/migrations/0022_location_alter_stoptime_location_id_and_more.py b/transport_accessibility/pt_map/migrations/0022_location_alter_stoptime_location_id_and_more.py new file mode 100644 index 0000000..474efdf --- /dev/null +++ b/transport_accessibility/pt_map/migrations/0022_location_alter_stoptime_location_id_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.0.6 on 2024-07-01 13:18 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pt_map', '0021_alter_stoptime_continuous_drop_off'), + ] + + operations = [ + migrations.CreateModel( + name='Location', + fields=[ + ('location_id', models.CharField(max_length=255, primary_key=True, serialize=False)), + ('stop_name', models.CharField(blank=True, max_length=255, null=True)), + ('stop_desc', models.CharField(blank=True, max_length=255, null=True)), + ('latitude', models.FloatField()), + ('longitude', models.FloatField()), + ('location_type', models.CharField(choices=[('Polygon', 'Polygon'), ('MultiPolygon', 'MultiPolygon')], max_length=255)), + ('feed_info_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pt_map.feedinfo')), + ], + ), + migrations.AlterField( + model_name='stoptime', + name='location_id', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='pt_map.location'), + ), + migrations.DeleteModel( + name='LocationsGeojson', + ), + ] diff --git a/transport_accessibility/pt_map/migrations/0023_rename_location_type_location_geometry_type.py b/transport_accessibility/pt_map/migrations/0023_rename_location_type_location_geometry_type.py new file mode 100644 index 0000000..e9bf8d7 --- /dev/null +++ b/transport_accessibility/pt_map/migrations/0023_rename_location_type_location_geometry_type.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2024-07-01 13:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('pt_map', '0022_location_alter_stoptime_location_id_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='location', + old_name='location_type', + new_name='geometry_type', + ), + ] diff --git a/transport_accessibility/pt_map/migrations/0024_remove_farerule_contains_id_and_more.py b/transport_accessibility/pt_map/migrations/0024_remove_farerule_contains_id_and_more.py new file mode 100644 index 0000000..6956300 --- /dev/null +++ b/transport_accessibility/pt_map/migrations/0024_remove_farerule_contains_id_and_more.py @@ -0,0 +1,40 @@ +# Generated by Django 5.0.6 on 2024-07-02 14:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pt_map', '0023_rename_location_type_location_geometry_type'), + ] + + operations = [ + migrations.RemoveField( + model_name='farerule', + name='contains_id', + ), + migrations.RemoveField( + model_name='farerule', + name='destination_id', + ), + migrations.RemoveField( + model_name='farerule', + name='origin_id', + ), + migrations.AddField( + model_name='farerule', + name='contains_id', + field=models.ManyToManyField(blank=True, null=True, related_name='fare_rules_for_zone_contains', to='pt_map.route'), + ), + migrations.AddField( + model_name='farerule', + name='destination_id', + field=models.ManyToManyField(blank=True, null=True, related_name='fare_rules_for_zone_as_destination', to='pt_map.route'), + ), + migrations.AddField( + model_name='farerule', + name='origin_id', + field=models.ManyToManyField(blank=True, null=True, related_name='fare_rules_for_zone_as_origin', to='pt_map.route'), + ), + ] diff --git a/transport_accessibility/pt_map/migrations/0025_alter_farelegrule_network_id_and_more.py b/transport_accessibility/pt_map/migrations/0025_alter_farelegrule_network_id_and_more.py new file mode 100644 index 0000000..196c22b --- /dev/null +++ b/transport_accessibility/pt_map/migrations/0025_alter_farelegrule_network_id_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 5.0.6 on 2024-07-02 14:43 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pt_map', '0024_remove_farerule_contains_id_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='farelegrule', + name='network_id', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='pt_map.network'), + ), + migrations.AlterField( + model_name='network', + name='network_name', + field=models.CharField(default='', max_length=255), + ), + migrations.AlterField( + model_name='route', + name='network_id', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='pt_map.network'), + ), + ] diff --git a/transport_accessibility/pt_map/migrations/0026_alter_farerule_contains_id_and_more.py b/transport_accessibility/pt_map/migrations/0026_alter_farerule_contains_id_and_more.py new file mode 100644 index 0000000..1c914e7 --- /dev/null +++ b/transport_accessibility/pt_map/migrations/0026_alter_farerule_contains_id_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.0.6 on 2024-07-02 17:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pt_map', '0025_alter_farelegrule_network_id_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='farerule', + name='contains_id', + field=models.ManyToManyField(blank=True, null=True, related_name='fare_rules_for_zone_contains', to='pt_map.stop'), + ), + migrations.AlterField( + model_name='farerule', + name='destination_id', + field=models.ManyToManyField(blank=True, null=True, related_name='fare_rules_for_zone_as_destination', to='pt_map.stop'), + ), + migrations.AlterField( + model_name='farerule', + name='origin_id', + field=models.ManyToManyField(blank=True, null=True, related_name='fare_rules_for_zone_as_origin', to='pt_map.stop'), + ), + ] diff --git a/transport_accessibility/pt_map/migrations/0027_remove_farelegrule_from_timeframe_group_id_and_more.py b/transport_accessibility/pt_map/migrations/0027_remove_farelegrule_from_timeframe_group_id_and_more.py new file mode 100644 index 0000000..6dab445 --- /dev/null +++ b/transport_accessibility/pt_map/migrations/0027_remove_farelegrule_from_timeframe_group_id_and_more.py @@ -0,0 +1,61 @@ +# Generated by Django 5.0.6 on 2024-07-02 22:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pt_map', '0026_alter_farerule_contains_id_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='farelegrule', + name='from_timeframe_group_id', + ), + migrations.RemoveField( + model_name='farelegrule', + name='to_timeframe_group_id', + ), + migrations.AlterField( + model_name='farerule', + name='contains_id', + field=models.ManyToManyField(blank=True, related_name='fare_rules_for_zone_contains', to='pt_map.stop'), + ), + migrations.AlterField( + model_name='farerule', + name='destination_id', + field=models.ManyToManyField(blank=True, related_name='fare_rules_for_zone_as_destination', to='pt_map.stop'), + ), + migrations.AlterField( + model_name='farerule', + name='origin_id', + field=models.ManyToManyField(blank=True, related_name='fare_rules_for_zone_as_origin', to='pt_map.stop'), + ), + migrations.AlterField( + model_name='stoptime', + name='drop_off_type', + field=models.IntegerField(blank=True, choices=[('Regularly scheduled drop off.', 0), ('No drop off available.', 1), ('Must phone agency to arrange drop off.', 2), ('Must coordinate with driver to arrange drop off.', 3)], null=True), + ), + migrations.AlterField( + model_name='stoptime', + name='pickup_type', + field=models.IntegerField(blank=True, choices=[('Regularly scheduled pickup.', 0), ('No pickup available.', 1), ('Must phone agency to arrange pickup.', 2), ('Must coordinate with driver to arrange pickup.', 3)], null=True), + ), + migrations.AlterField( + model_name='stoptime', + name='timepoint', + field=models.IntegerField(blank=True, choices=[('Times are considered approximate.', 0), ('Times are considered exact.', 1)], null=True), + ), + migrations.AddField( + model_name='farelegrule', + name='from_timeframe_group_id', + field=models.ManyToManyField(null=True, related_name='fare_leg_rules_from', to='pt_map.timeframe'), + ), + migrations.AddField( + model_name='farelegrule', + name='to_timeframe_group_id', + field=models.ManyToManyField(null=True, related_name='fare_leg_rules_to', to='pt_map.timeframe'), + ), + ] diff --git a/transport_accessibility/pt_map/model_test_fields.py b/transport_accessibility/pt_map/model_test_fields.py index cc25d99..868d5b2 100644 --- a/transport_accessibility/pt_map/model_test_fields.py +++ b/transport_accessibility/pt_map/model_test_fields.py @@ -206,8 +206,8 @@ field_requirements = \ }, { "name": "network_id", - "type": "mtm", - "references": Route, + "type": "fk", + "references": Network, "required": "false", "forbidden_if": ["RouteNetwork", True] }, @@ -252,8 +252,7 @@ field_requirements = \ }, { "name": "block_id", - "type": "mtm", - "references": Trip, + "type": "unique", "required": "false", }, { @@ -277,6 +276,42 @@ field_requirements = \ ], "pk": "trip_id", }, + { + "model": "Location", + "fields": [ + { + "name": "location_id", + "type": "pk", + "required": "true", + }, + { + "name": "stop_name", + "type": "str", + "required": "false", + }, + { + "name": "stop_desc", + "type": "str", + "required": "false", + }, + { + "name": "geometry_type", + "type": "enum", + "allowed_values": ["Polygon", "MultiPolygon"], + "required": "true", + }, + { + "name": "latitude", + "type": "lat", + "required": "true" + }, + { + "name": "longitude", + "type": "lon", + "required": "true", + }, + ], + }, { "model": "StopTime", "fields": [ @@ -308,7 +343,7 @@ field_requirements = \ }, { "name": "location_group_id", - "type": "mtm", + "type": "fk", "references": LocationGroup, "required": "false", "forbidden_if_not": [(["stop_id", None], ["location_id", None])], @@ -317,7 +352,7 @@ field_requirements = \ "name": "location_id", "type": "fk", "required": "false", - "references": LocationsGeojson, + "references": Location, "forbidden_if_not": [(["stop_id", None], ["location_group_id", None])], }, { @@ -658,7 +693,8 @@ field_requirements = \ }, { "name": "network_id", - "type": "str", + "type": "fk", + "references": Network, "required": "false", }, { diff --git a/transport_accessibility/pt_map/models.py b/transport_accessibility/pt_map/models.py index f25c35a..bc90c50 100644 --- a/transport_accessibility/pt_map/models.py +++ b/transport_accessibility/pt_map/models.py @@ -7,7 +7,7 @@ Attributes ---------- Classes ------- -Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, FareAttribute, FareRule, Shape, Frequency, Transfer, Pathway, Level, FeedInfo, LocationsGeojson, BookingRule, Translation, Attribution, LocationGroup, LocationGroupStop, RouteNetwork, Network, StopArea, Area, FareMedium, FareProduct, FareLegRule, FareTransferRule, Timeframe +Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, FareAttribute, FareRule, Shape, Frequency, Transfer, Pathway, Level, FeedInfo, Location, BookingRule, Translation, Attribution, LocationGroup, LocationGroupStop, RouteNetwork, Network, StopArea, Area, FareMedium, FareProduct, FareLegRule, FareTransferRule, Timeframe Different files as described in the GTFS Reference """ from django.db import models @@ -72,6 +72,14 @@ class Stop(models.Model): platform_code = models.CharField(max_length=50, blank=True, null=True) feed_info_id = models.ForeignKey(FeedInfo, on_delete=models.CASCADE) +class Network(models.Model): + """ + Represents network.txt from the GTFS Reference. + """ + network_id = models.CharField(max_length=255, primary_key=True) + network_name = models.CharField(max_length=255, default="") + feed_info_id = models.ForeignKey(FeedInfo, on_delete=models.CASCADE) + class Route(models.Model): """ Represents route.txt from the GTFS Reference. @@ -88,7 +96,7 @@ class Route(models.Model): route_sort_order = models.IntegerField(blank=True, null=True) continuous_pickup = models.IntegerField(choices=[("Continous stopping pickup", 0), ("No continuous stopping pickup", 1), ("Must phone agency to arrange continuous stopping pickup off", 2), ("Must coordinate with driver to arrange continuous stopping pickup", 3)], blank=True, null=True) continuous_drop_off = models.IntegerField(choices=[("Continous stopping pickup", 0), ("No continuous stopping pickup", 1), ("Must phone agency to arrange continuous stopping pickup off", 2), ("Must coordinate with driver to arrange continuous stopping pickup", 3)], blank=True, null=True) - network_id = models.IntegerField(blank=True, null=True) + network_id = models.ForeignKey(Network, on_delete=models.CASCADE, null=True) feed_info_id = models.ForeignKey(FeedInfo, on_delete=models.CASCADE) class Shape(models.Model): @@ -175,17 +183,16 @@ class LocationGroup(models.Model): location_group_type = models.CharField(max_length=255) feed_info_id = models.ForeignKey(FeedInfo, on_delete=models.CASCADE) -class LocationsGeojson(models.Model): +class Location(models.Model): """ Represents locations.geojson from the GTFS Reference. """ location_id = models.CharField(max_length=255, primary_key=True) - location_name = models.CharField(max_length=255) - location_lat = models.FloatField() - location_lon = models.FloatField() - location_type = models.CharField(max_length=255) - parent_location_id = models.CharField(max_length=255, blank=True, null=True) - wheelchair_boarding = models.BooleanField(blank=True, null=True) + stop_name = models.CharField(max_length=255, blank=True, null=True) + stop_desc = models.CharField(max_length=255, blank=True, null=True) + latitude = models.FloatField() + longitude = models.FloatField() + geometry_type = models.CharField(max_length=255, choices=[("Polygon", "Polygon"), ("MultiPolygon", "MultiPolygon")]) feed_info_id = models.ForeignKey(FeedInfo, on_delete=models.CASCADE) class BookingRule(models.Model): @@ -219,13 +226,13 @@ class StopTime(models.Model): departure_time = models.CharField(max_length=255, blank=True, null=True) stop_id = models.ForeignKey(Stop, null=True, on_delete=models.CASCADE) location_group_id = models.ForeignKey(LocationGroup, on_delete=models.SET_NULL, blank=True, null=True) - location_id = models.ForeignKey(LocationsGeojson, on_delete=models.SET_NULL, blank=True, null=True) + location_id = models.ForeignKey(Location, on_delete=models.SET_NULL, blank=True, null=True) stop_sequence = models.IntegerField() stop_headsign = models.CharField(max_length=255, blank=True, null=True) - pickup_type = models.IntegerField(blank=True, null=True) - drop_off_type = models.IntegerField(blank=True, null=True) + pickup_type = models.IntegerField(blank=True, null=True, choices=[("Regularly scheduled pickup.", 0), ("No pickup available.", 1), ("Must phone agency to arrange pickup.", 2), ("Must coordinate with driver to arrange pickup.", 3)]) + drop_off_type = models.IntegerField(blank=True, null=True, choices=[("Regularly scheduled drop off.", 0), ("No drop off available.", 1), ("Must phone agency to arrange drop off.", 2), ("Must coordinate with driver to arrange drop off.", 3)]) shape_dist_traveled = models.FloatField(blank=True, null=True) - timepoint = models.IntegerField(blank=True, null=True) + timepoint = models.IntegerField(blank=True, null=True, choices=[("Times are considered approximate.", 0), ("Times are considered exact.", 1)]) start_pickup_drop_off_window = models.CharField(max_length=255, blank=True) end_pickup_drop_off_window = models.CharField(max_length=255, blank=True) continuous_pickup = models.IntegerField(choices=[("Continous stopping pickup", 0), ("No continuous stopping pickup", 1), ("Must phone agency to arrange continuous stopping pickup off", 2), ("Must coordinate with driver to arrange continuous stopping pickup", 3)], null=True) @@ -257,9 +264,9 @@ class FareRule(models.Model): fare_rule_id = models.BigAutoField(primary_key=True) fare_id = models.ForeignKey(FareAttribute, on_delete=models.CASCADE) route_id = models.ForeignKey(Route, on_delete=models.CASCADE, blank=True, null=True) - origin_id = models.IntegerField(blank=True, null=True) - destination_id = models.CharField(max_length=255, blank=True, null=True) - contains_id = models.CharField(max_length=255, blank=True, null=True) + origin_id = models.ManyToManyField(Stop, related_name="fare_rules_for_zone_as_origin", blank=True) + destination_id = models.ManyToManyField(Stop, related_name="fare_rules_for_zone_as_destination", blank=True) + contains_id = models.ManyToManyField(Stop, related_name="fare_rules_for_zone_contains", blank=True) feed_info_id = models.ForeignKey(FeedInfo, on_delete=models.CASCADE) class Frequency(models.Model): @@ -351,14 +358,6 @@ class LocationGroupStop(models.Model): stop_id = models.ForeignKey(Stop, on_delete=models.CASCADE) feed_info_id = models.ForeignKey(FeedInfo, on_delete=models.CASCADE) -class Network(models.Model): - """ - Represents network.txt from the GTFS Reference. - """ - network_id = models.CharField(max_length=255, primary_key=True) - network_name = models.CharField(max_length=255) - feed_info_id = models.ForeignKey(FeedInfo, on_delete=models.CASCADE) - class RouteNetwork(models.Model): """ Represents route_network.txt from the GTFS Reference. @@ -431,11 +430,11 @@ class FareLegRule(models.Model): fare_leg_rule_name = models.CharField(max_length=255) fare_leg_rule_description = models.TextField(blank=True, null=True) leg_group_id = models.CharField(max_length=255, blank=True, null=True) - network_id = models.CharField(max_length=255, blank=True, null=True) + network_id = models.ForeignKey(Network, on_delete=models.CASCADE, null=True) from_area_id = models.ForeignKey(Area, blank=True, null=True, on_delete=models.SET_NULL, related_name='farelegrule_from_area') to_area_id = models.ForeignKey(Area, blank=True, null=True, on_delete=models.SET_NULL, related_name='farelegrule_to_area') - from_timeframe_group_id = models.ForeignKey(Timeframe, blank=True, null=True, on_delete=models.SET_NULL, related_name='farelegrule_from_timeframe') - to_timeframe_group_id = models.ForeignKey(Timeframe, blank=True, null=True, on_delete=models.SET_NULL, related_name='farelegrule_to_timeframe') + from_timeframe_group_id = models.ManyToManyField(Timeframe, related_name="fare_leg_rules_from", blank=True) + to_timeframe_group_id = models.ManyToManyField(Timeframe, related_name="fare_leg_rules_to", blank=True) fare_product_id = models.ForeignKey(FareProduct, on_delete=models.CASCADE) rule_priority = models.IntegerField(blank=True, null=True) feed_info_id = models.ForeignKey(FeedInfo, on_delete=models.CASCADE) diff --git a/transport_accessibility/pt_map/templates/base.html b/transport_accessibility/pt_map/templates/base.html new file mode 100644 index 0000000..e695794 --- /dev/null +++ b/transport_accessibility/pt_map/templates/base.html @@ -0,0 +1,52 @@ +{% load static %} + + + + + + OpenStreetMap with GTFS Toolbar + + + + + + + + + + + {% block content %}{% endblock %} + + + diff --git a/transport_accessibility/pt_map/templates/map.html b/transport_accessibility/pt_map/templates/map.html index f5467bf..75d0744 100644 --- a/transport_accessibility/pt_map/templates/map.html +++ b/transport_accessibility/pt_map/templates/map.html @@ -1,53 +1,6 @@ - - - - - - OpenStreetMap with GTFS Toolbar - - - - - - - - {% load static %} - - - - - +{% extends "base.html" %} +{% load static %} +{% block content %}
@@ -205,5 +158,4 @@ } - - +{% endblock %} diff --git a/transport_accessibility/pt_map/tests.py.m4 b/transport_accessibility/pt_map/tests.py.m4 deleted file mode 100644 index 36fe544..0000000 --- a/transport_accessibility/pt_map/tests.py.m4 +++ /dev/null @@ -1,36 +0,0 @@ -from django.test import TestCase -from pt_map.test_data import * -from pt_map.models import * -import unittest -from django.db import models -import random -define(`models', `Agency, Area, Attribution, BookingRule, Calendar, CalendarDate, FareAttribute, FareLegRule, FareMedium, FareProduct, FareRule, FareTransferRule, FeedInfo, Frequency, Level, LocationGroup, LocationGroupStop, LocationsGeojson, Network, Pathway, Route, RouteNetwork, Shape, Stop, StopArea, StopTime, Timeframe, Transfer, Translation, Trip') -define(`foreach', `ifelse(`$#', `1',``, `$1' `$2' `foreach(shift($@)', `$2'')')') -foreach(models, `echo(`class $1TestCase(TestCase): - def setUp(self): - self.model_fields = [f.name for f in $1._meta.fields] - self.gtfs_fields = get_all_fields("$1") - - def test_all_fields_present(self): - """Make sure the model has properties for all fields - regardless if required - provided by the GTFS standard.""" - for f in self.gtfs_fields: - with self.subTest(f=f): - self.assertIn(f["name"], self.model_fields) - - def test_constructor_all_fields(self): - """Make sure all of the models fields of the model are initializable""" - with self.subTest(name="fixed"): - """Fixed subTest""" - d = data[0] - values = {f["name"]: d[f["type"]] for f in self.gtfs_fields} - obj = $1(**values) - self.assertIsNotNone(obj) - self.assertIsInstance(obj, models.Model) - - with self.subTest(name="other"): - d = data[random.randint(0,len(data))] - values = {f["name"]: d[f["type"]] for f in self.gtfs_fields} - obj = $1(**values) - self.assertIsNotNone(obj) - self.assertIsInstance(obj, models.Model)')') - diff --git a/transport_accessibility/pt_map/tests_gtfs_compliance.py b/transport_accessibility/pt_map/tests_gtfs_compliance.py index 334376c..fa35755 100644 --- a/transport_accessibility/pt_map/tests_gtfs_compliance.py +++ b/transport_accessibility/pt_map/tests_gtfs_compliance.py @@ -1,5 +1,38 @@ -#TODO: Test LocationsGeojson -#TODO: Test MTM +""" +Test GTFS Compliance +==================== + +Test suite meant to make sure the models do their best to enforce compliance of the database with the gtfs standard. +In particular we are testing: +1. All of the txt files the reference suggests have a model representation. +2. All of the fields described in each of the files is representable inside the corresponding model. +3. The fields can be set with standard data for the correct data type. +4. None of the optional fields are enforced to be set. (TODO) +5. All of the required fields are enforced to be set with a legal value. (TODO) +6. If set, the optional fields hold only legal values (TODO) +7. All conditional requirements and restrictions are enforced. (TODO) +8. Foreign keys and many to many sets are actually set within the database and are validated. (TODO) + +Functions +--------- + +_get_test_data(f: function): + Decorator, wrapping instance methods to provide an extendable method to get data from the test data set based on the TestCase's requirements. +_test_constructor(f: function): + Decorator. Basic test to validate if the models are instantiable with the given set of fields. + +Classes +------- + +AllFieldsPresentTestCase(TestCase): + Test case making sure all files are represented with a complete set of fields. (Conditions 1+2 from the above list) + +ConstructorAllFieldsTestCase(TestCase): + Test case making sure the models can be instantiated given valid values for all their fields. (3) + +ConstructorRequiredFieldsOnlyTestCase(TestCase): + Test case making sure the model creation is fine with just the required fields. (4) +""" from django.test import TestCase, TransactionTestCase from pt_map.test_data import * @@ -14,7 +47,8 @@ import datetime def _get_test_data(f): """Get test data for a field type.""" - def wrapper(self, field, model, d, *args, **kwargs): + def wrapper(self, field, model, d, i): + f(self, field, model, d, i) if field["name"] == "service_id": self.add += 1 return f"{d['pk']}{self.add}" @@ -22,19 +56,38 @@ def _get_test_data(f): case "fk": if isinstance(field["references"], list): field["references"] = random.choice(field["references"]) - if not field["references"] == model and field["references"] not in [LocationsGeojson]: - fk = field["references"].objects.create(**{**{f["name"]: self.get_test_data(f, field["references"], d) for f in self.gtfs_fields[field["references"]._meta.object_name] if not f["type"] == "mtm"}, "feed_info_id": self.feed_info}) + if field["references"].objects.all(): + return field["references"].objects.all()[0] + if not field["references"] == model and field["references"]: + fk = field["references"].objects.create(**{**{f["name"]: self.get_test_data(f, field["references"], d, i) for f in self.gtfs_fields[field["references"]._meta.object_name] if not f["type"] == "mtm"}, "feed_info_id": self.feed_info}) + for n, v in {f["name"]: self.get_test_data(f, model, d, i) for f in self.gtfs_fields[field["references"]._meta.object_name] if f["type"] == "mtm"}.items(): + getattr(fk, n).set(v) return fk return None case "unique": - return (d[field["pk"]], "unique") + return (d['pk'], "unique") #TODO case "enum": return random.choice(field["allowed_values"]) case "date": return datetime.datetime.fromisoformat(d["date"]) case "pk": - self.add += 1 - return f"{d['pk']}{self.add}" + return f"{d['pk']}{datetime.datetime.now().timestamp()}" + case "lat": + return d["float"] #TODO + case "lon": + return d["float"] #TODO + case "mtm": + choices = [j for j in range(len(data)-1) if not j == i] + indexes = set([]) + while len(indexes) < 3: + indexes.add(random.choice(choices)) + indexes = list(indexes) + mtm_test_data = [data[j] for j in indexes if field["references"]._meta.pk.name in data[j].keys() and not field["references"].objects.filter(pk=data[j]['pk'])] + qs = [field["references"].objects.create(**{**{f["name"]: self.get_test_data(f, field["references"], mtm_test_data[j], i) for f in self.gtfs_fields[field["references"]._meta.object_name] if not f["type"] == "mtm"}, "feed_info_id": self.feed_info}) for j in range(len(mtm_test_data))] + for m in qs: + for n, v in {f["name"]: self.get_test_data(f, model, d, i) for f in self.gtfs_fields[field["references"]._meta.object_name] if f["type"] == "mtm"}.items(): + getattr(m, n).set(v) + return field["references"].objects.all() case _: return d[field["type"]] return wrapper @@ -42,11 +95,14 @@ def _get_test_data(f): def _test_constructor(index): def decorate(f): def wrapper(self, *args, **kwargs): - for name, model in [tpl for tpl in inspect.getmembers(pt_map.models, inspect.isclass) if tpl[1] not in [FeedInfo, LocationsGeojson]]: + f(self, *args, **kwargs) + for name, model in [tpl for tpl in inspect.getmembers(pt_map.models, inspect.isclass) if tpl[1] not in [FeedInfo,]]: with self.subTest(name=name): d = data[index] - values = {**{f["name"]: self.get_test_data(f, model, d) for f in self.gtfs_fields[name] if not f["type"] == "mtm"}, "feed_info_id": self.feed_info} + values = {**{field["name"]: self.get_test_data(field, model, d, index) for field in self.gtfs_fields[name] if not field["type"] == "mtm"}, "feed_info_id": self.feed_info} obj = model.objects.create(**values) + for n, v in {field["name"]: self.get_test_data(field, model, d, index) for field in self.gtfs_fields[name] if field["type"] == "mtm"}.items(): + getattr(obj, n).set(v) self.assertIsNotNone(obj) self.assertIsInstance(obj, models.Model) return wrapper @@ -55,8 +111,8 @@ def _test_constructor(index): class AllFieldsPresentTestCase(TestCase): """Test for the presence of all the fields of all the models, the GTFS reference describes.""" def setUp(self): - self.model_fields = {name: [*[f.name for f in model._meta.fields], *[f.name for f in model._meta.many_to_many]] for name, model in inspect.getmembers(pt_map.models, inspect.isclass) if not model == LocationsGeojson} - self.gtfs_fields = {name: get_all_fields(name) for name,model in inspect.getmembers(pt_map.models, inspect.isclass) if not model == LocationsGeojson} + self.model_fields = {name: [*[f.name for f in model._meta.fields], *[f.name for f in model._meta.many_to_many]] for name, model in inspect.getmembers(pt_map.models, inspect.isclass)} + self.gtfs_fields = {name: get_all_fields(name) for name,model in inspect.getmembers(pt_map.models, inspect.isclass)} def test_all_fields_present(self): """Make sure the model has properties for all fields - regardless if required - provided by the GTFS standard.""" @@ -76,7 +132,7 @@ class ConstructorAllFieldsTestCase(TransactionTestCase): self.add = 0 @_get_test_data - def get_test_data(self, field, model, d): + def get_test_data(self, field, model, d, i): pass @_test_constructor(0) @@ -91,12 +147,12 @@ class ConstructorAllFieldsTestCase(TransactionTestCase): class ConstructorRequiredFieldsOnlyTestCase(TransactionTestCase): def setUp(self): - self.gtfs_fields = {name: get_required_fields(name) for name, model in inspect.getmembers(pt_map.models, inspect.isclass) if model not in [FeedInfo, LocationsGeojson]} + self.gtfs_fields = {name: get_required_fields(name) for name, model in inspect.getmembers(pt_map.models, inspect.isclass) if model not in [FeedInfo]} self.feed_info = FeedInfo.objects.create(**{f["name"]: data[0][f["type"]] for f in get_required_fields("FeedInfo")}) self.add = 0 @_get_test_data - def get_test_data(self): + def get_test_data(self, field, model, d, i): pass @_test_constructor(0) diff --git a/transport_accessibility/pt_map/views.py b/transport_accessibility/pt_map/views.py index 83ba777..2584f06 100644 --- a/transport_accessibility/pt_map/views.py +++ b/transport_accessibility/pt_map/views.py @@ -10,6 +10,7 @@ index """ from django.shortcuts import render from . import query +import json def index(request): diff --git a/transport_accessibility/tmp b/transport_accessibility/tmp deleted file mode 100644 index 5d228ca..0000000 --- a/transport_accessibility/tmp +++ /dev/null @@ -1 +0,0 @@ -def setUp(self): self.model_fields = [f.name for f in Agency._meta.fields] self.gtfs_fields = get_all_fields("Agency") def test_all_fields_present(self): """Make sure the model has properties for all fields - regardless if required - provided by the GTFS standard.""" for f in self.gtfs_fields: with self.subTest(f=f): self.assertIn(f["name"], self.model_fields) def test_constructor_all_fields(self): """Make sure all of the models fields of the model are initializable""" with self.subTest(name="fixed"): """Fixed subTest""" d = data[0] values = {f["name"]: d[f["type"]] for f in self.gtfs_fields} obj = Agency(**values) self.assertIsNotNone(obj) self.assertIsInstance(obj, models.Model) with self.subTest(name="other"): d = data[random.randint(0,len(data))] values = {f["name"]: d[f["type"]] for f in self.gtfs_fields} obj = Agency(**values) self.assertIsNotNone(obj) self.assertIsInstance(obj, models.Model)