mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-01-02 01:47:04 -05:00
feat: Implement initial schema and add various API, service, and management command enhancements across the application.
This commit is contained in:
@@ -190,9 +190,7 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"average_rating",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=3, null=True
|
||||
),
|
||||
models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True),
|
||||
),
|
||||
],
|
||||
options={
|
||||
@@ -374,21 +372,15 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"height_ft",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=6, null=True
|
||||
),
|
||||
models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True),
|
||||
),
|
||||
(
|
||||
"length_ft",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=7, null=True
|
||||
),
|
||||
models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True),
|
||||
),
|
||||
(
|
||||
"speed_mph",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=5, null=True
|
||||
),
|
||||
models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True),
|
||||
),
|
||||
("inversions", models.PositiveIntegerField(default=0)),
|
||||
(
|
||||
@@ -432,9 +424,7 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"max_drop_height_ft",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=6, null=True
|
||||
),
|
||||
models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True),
|
||||
),
|
||||
(
|
||||
"launch_type",
|
||||
@@ -692,9 +682,7 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="ridelocation",
|
||||
index=models.Index(
|
||||
fields=["park_area"], name="rides_ridel_park_ar_26c90c_idx"
|
||||
),
|
||||
index=models.Index(fields=["park_area"], name="rides_ridel_park_ar_26c90c_idx"),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="ridemodel",
|
||||
|
||||
@@ -89,9 +89,7 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"average_rating",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=3, null=True
|
||||
),
|
||||
models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True),
|
||||
),
|
||||
],
|
||||
options={
|
||||
@@ -140,21 +138,15 @@ class Migration(migrations.Migration):
|
||||
("id", models.BigIntegerField()),
|
||||
(
|
||||
"height_ft",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=6, null=True
|
||||
),
|
||||
models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True),
|
||||
),
|
||||
(
|
||||
"length_ft",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=7, null=True
|
||||
),
|
||||
models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True),
|
||||
),
|
||||
(
|
||||
"speed_mph",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=5, null=True
|
||||
),
|
||||
models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True),
|
||||
),
|
||||
("inversions", models.PositiveIntegerField(default=0)),
|
||||
(
|
||||
@@ -198,9 +190,7 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"max_drop_height_ft",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=6, null=True
|
||||
),
|
||||
models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True),
|
||||
),
|
||||
(
|
||||
"launch_type",
|
||||
|
||||
@@ -220,9 +220,7 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"rank",
|
||||
models.PositiveIntegerField(
|
||||
db_index=True, help_text="Overall rank position (1 = best)"
|
||||
),
|
||||
models.PositiveIntegerField(db_index=True, help_text="Overall rank position (1 = best)"),
|
||||
),
|
||||
(
|
||||
"wins",
|
||||
@@ -323,9 +321,7 @@ class Migration(migrations.Migration):
|
||||
("id", models.BigIntegerField()),
|
||||
(
|
||||
"rank",
|
||||
models.PositiveIntegerField(
|
||||
help_text="Overall rank position (1 = best)"
|
||||
),
|
||||
models.PositiveIntegerField(help_text="Overall rank position (1 = best)"),
|
||||
),
|
||||
(
|
||||
"wins",
|
||||
@@ -487,15 +483,11 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="ridepaircomparison",
|
||||
index=models.Index(
|
||||
fields=["ride_a", "ride_b"], name="rides_ridep_ride_a__eb0674_idx"
|
||||
),
|
||||
index=models.Index(fields=["ride_a", "ride_b"], name="rides_ridep_ride_a__eb0674_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="ridepaircomparison",
|
||||
index=models.Index(
|
||||
fields=["last_calculated"], name="rides_ridep_last_ca_bd9f6c_idx"
|
||||
),
|
||||
index=models.Index(fields=["last_calculated"], name="rides_ridep_last_ca_bd9f6c_idx"),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="ridepaircomparison",
|
||||
@@ -551,9 +543,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddConstraint(
|
||||
model_name="rideranking",
|
||||
constraint=models.CheckConstraint(
|
||||
condition=models.Q(
|
||||
("winning_percentage__gte", 0), ("winning_percentage__lte", 1)
|
||||
),
|
||||
condition=models.Q(("winning_percentage__gte", 0), ("winning_percentage__lte", 1)),
|
||||
name="rideranking_winning_percentage_range",
|
||||
violation_error_message="Winning percentage must be between 0 and 1",
|
||||
),
|
||||
|
||||
@@ -163,27 +163,19 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="ridephoto",
|
||||
index=models.Index(
|
||||
fields=["ride", "is_primary"], name="rides_ridep_ride_id_aa49f1_idx"
|
||||
),
|
||||
index=models.Index(fields=["ride", "is_primary"], name="rides_ridep_ride_id_aa49f1_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="ridephoto",
|
||||
index=models.Index(
|
||||
fields=["ride", "is_approved"], name="rides_ridep_ride_id_f1eddc_idx"
|
||||
),
|
||||
index=models.Index(fields=["ride", "is_approved"], name="rides_ridep_ride_id_f1eddc_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="ridephoto",
|
||||
index=models.Index(
|
||||
fields=["ride", "photo_type"], name="rides_ridep_ride_id_49e7ec_idx"
|
||||
),
|
||||
index=models.Index(fields=["ride", "photo_type"], name="rides_ridep_ride_id_49e7ec_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="ridephoto",
|
||||
index=models.Index(
|
||||
fields=["created_at"], name="rides_ridep_created_106e02_idx"
|
||||
),
|
||||
index=models.Index(fields=["created_at"], name="rides_ridep_created_106e02_idx"),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="ridephoto",
|
||||
|
||||
@@ -147,21 +147,15 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"spec_name",
|
||||
models.CharField(
|
||||
help_text="Name of the specification", max_length=100
|
||||
),
|
||||
models.CharField(help_text="Name of the specification", max_length=100),
|
||||
),
|
||||
(
|
||||
"spec_value",
|
||||
models.CharField(
|
||||
help_text="Value of the specification", max_length=255
|
||||
),
|
||||
models.CharField(help_text="Value of the specification", max_length=255),
|
||||
),
|
||||
(
|
||||
"spec_unit",
|
||||
models.CharField(
|
||||
blank=True, help_text="Unit of measurement", max_length=20
|
||||
),
|
||||
models.CharField(blank=True, help_text="Unit of measurement", max_length=20),
|
||||
),
|
||||
(
|
||||
"notes",
|
||||
@@ -203,21 +197,15 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"spec_name",
|
||||
models.CharField(
|
||||
help_text="Name of the specification", max_length=100
|
||||
),
|
||||
models.CharField(help_text="Name of the specification", max_length=100),
|
||||
),
|
||||
(
|
||||
"spec_value",
|
||||
models.CharField(
|
||||
help_text="Value of the specification", max_length=255
|
||||
),
|
||||
models.CharField(help_text="Value of the specification", max_length=255),
|
||||
),
|
||||
(
|
||||
"spec_unit",
|
||||
models.CharField(
|
||||
blank=True, help_text="Unit of measurement", max_length=20
|
||||
),
|
||||
models.CharField(blank=True, help_text="Unit of measurement", max_length=20),
|
||||
),
|
||||
(
|
||||
"notes",
|
||||
@@ -251,33 +239,23 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"description",
|
||||
models.TextField(
|
||||
blank=True, help_text="Description of variant differences"
|
||||
),
|
||||
models.TextField(blank=True, help_text="Description of variant differences"),
|
||||
),
|
||||
(
|
||||
"min_height_ft",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=6, null=True
|
||||
),
|
||||
models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True),
|
||||
),
|
||||
(
|
||||
"max_height_ft",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=6, null=True
|
||||
),
|
||||
models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True),
|
||||
),
|
||||
(
|
||||
"min_speed_mph",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=5, null=True
|
||||
),
|
||||
models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True),
|
||||
),
|
||||
(
|
||||
"max_speed_mph",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=5, null=True
|
||||
),
|
||||
models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True),
|
||||
),
|
||||
(
|
||||
"distinguishing_features",
|
||||
@@ -307,33 +285,23 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"description",
|
||||
models.TextField(
|
||||
blank=True, help_text="Description of variant differences"
|
||||
),
|
||||
models.TextField(blank=True, help_text="Description of variant differences"),
|
||||
),
|
||||
(
|
||||
"min_height_ft",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=6, null=True
|
||||
),
|
||||
models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True),
|
||||
),
|
||||
(
|
||||
"max_height_ft",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=6, null=True
|
||||
),
|
||||
models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True),
|
||||
),
|
||||
(
|
||||
"min_speed_mph",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=5, null=True
|
||||
),
|
||||
models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True),
|
||||
),
|
||||
(
|
||||
"max_speed_mph",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=5, null=True
|
||||
),
|
||||
models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True),
|
||||
),
|
||||
(
|
||||
"distinguishing_features",
|
||||
@@ -750,9 +718,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="ridemodel",
|
||||
name="description",
|
||||
field=models.TextField(
|
||||
blank=True, help_text="Detailed description of the ride model"
|
||||
),
|
||||
field=models.TextField(blank=True, help_text="Detailed description of the ride model"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodel",
|
||||
@@ -794,9 +760,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelevent",
|
||||
name="description",
|
||||
field=models.TextField(
|
||||
blank=True, help_text="Detailed description of the ride model"
|
||||
),
|
||||
field=models.TextField(blank=True, help_text="Detailed description of the ride model"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelevent",
|
||||
|
||||
@@ -13,8 +13,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="ridemodel",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
help_text="URL-friendly identifier", max_length=255, unique=True
|
||||
),
|
||||
field=models.SlugField(help_text="URL-friendly identifier", max_length=255, unique=True),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -16,9 +16,7 @@ def update_ride_model_slugs(apps, schema_editor):
|
||||
counter = 1
|
||||
base_slug = new_slug
|
||||
while (
|
||||
RideModel.objects.filter(
|
||||
manufacturer=ride_model.manufacturer, slug=new_slug
|
||||
)
|
||||
RideModel.objects.filter(manufacturer=ride_model.manufacturer, slug=new_slug)
|
||||
.exclude(pk=ride_model.pk)
|
||||
.exists()
|
||||
):
|
||||
@@ -37,16 +35,12 @@ def reverse_ride_model_slugs(apps, schema_editor):
|
||||
|
||||
for ride_model in RideModel.objects.all():
|
||||
# Generate old-style slug with manufacturer + name
|
||||
old_slug = slugify(
|
||||
f"{ride_model.manufacturer.name if ride_model.manufacturer else ''} {ride_model.name}"
|
||||
)
|
||||
old_slug = slugify(f"{ride_model.manufacturer.name if ride_model.manufacturer else ''} {ride_model.name}")
|
||||
|
||||
# Ensure uniqueness globally (old way)
|
||||
counter = 1
|
||||
base_slug = old_slug
|
||||
while (
|
||||
RideModel.objects.filter(slug=old_slug).exclude(pk=ride_model.pk).exists()
|
||||
):
|
||||
while RideModel.objects.filter(slug=old_slug).exclude(pk=ride_model.pk).exists():
|
||||
old_slug = f"{base_slug}-{counter}"
|
||||
counter += 1
|
||||
|
||||
|
||||
@@ -39,16 +39,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name="company",
|
||||
name="url",
|
||||
field=models.URLField(
|
||||
blank=True, help_text="Frontend URL for this company"
|
||||
),
|
||||
field=models.URLField(blank=True, help_text="Frontend URL for this company"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="companyevent",
|
||||
name="url",
|
||||
field=models.URLField(
|
||||
blank=True, help_text="Frontend URL for this company"
|
||||
),
|
||||
field=models.URLField(blank=True, help_text="Frontend URL for this company"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="ride",
|
||||
@@ -63,16 +59,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name="ridemodel",
|
||||
name="url",
|
||||
field=models.URLField(
|
||||
blank=True, help_text="Frontend URL for this ride model"
|
||||
),
|
||||
field=models.URLField(blank=True, help_text="Frontend URL for this ride model"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="ridemodelevent",
|
||||
name="url",
|
||||
field=models.URLField(
|
||||
blank=True, help_text="Frontend URL for this ride model"
|
||||
),
|
||||
field=models.URLField(blank=True, help_text="Frontend URL for this ride model"),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="company",
|
||||
|
||||
@@ -23,16 +23,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name="ride",
|
||||
name="park_url",
|
||||
field=models.URLField(
|
||||
blank=True, help_text="Frontend URL for this ride's park"
|
||||
),
|
||||
field=models.URLField(blank=True, help_text="Frontend URL for this ride's park"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="rideevent",
|
||||
name="park_url",
|
||||
field=models.URLField(
|
||||
blank=True, help_text="Frontend URL for this ride's park"
|
||||
),
|
||||
field=models.URLField(blank=True, help_text="Frontend URL for this ride's park"),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="ride",
|
||||
|
||||
@@ -12,13 +12,15 @@ from django.db import migrations
|
||||
|
||||
def populate_computed_fields(apps, schema_editor):
|
||||
"""Populate computed fields for all existing rides."""
|
||||
Ride = apps.get_model('rides', 'Ride')
|
||||
Ride = apps.get_model("rides", "Ride")
|
||||
|
||||
# Disable pghistory triggers during bulk operations to avoid performance issues
|
||||
with pghistory.context(disable=True):
|
||||
rides = list(Ride.objects.all().select_related(
|
||||
'park', 'park__location', 'park_area', 'manufacturer', 'designer', 'ride_model'
|
||||
))
|
||||
rides = list(
|
||||
Ride.objects.all().select_related(
|
||||
"park", "park__location", "park_area", "manufacturer", "designer", "ride_model"
|
||||
)
|
||||
)
|
||||
|
||||
for ride in rides:
|
||||
# Extract opening year from opening_date
|
||||
@@ -39,7 +41,7 @@ def populate_computed_fields(apps, schema_editor):
|
||||
# Park info
|
||||
if ride.park:
|
||||
search_parts.append(ride.park.name)
|
||||
if hasattr(ride.park, 'location') and ride.park.location:
|
||||
if hasattr(ride.park, "location") and ride.park.location:
|
||||
if ride.park.location.city:
|
||||
search_parts.append(ride.park.location.city)
|
||||
if ride.park.location.state:
|
||||
@@ -62,7 +64,7 @@ def populate_computed_fields(apps, schema_editor):
|
||||
("TR", "Transport"),
|
||||
("OT", "Other"),
|
||||
]
|
||||
category_display = dict(category_choices).get(ride.category, '')
|
||||
category_display = dict(category_choices).get(ride.category, "")
|
||||
if category_display:
|
||||
search_parts.append(category_display)
|
||||
|
||||
@@ -79,7 +81,7 @@ def populate_computed_fields(apps, schema_editor):
|
||||
("DEMOLISHED", "Demolished"),
|
||||
("RELOCATED", "Relocated"),
|
||||
]
|
||||
status_display = dict(status_choices).get(ride.status, '')
|
||||
status_display = dict(status_choices).get(ride.status, "")
|
||||
if status_display:
|
||||
search_parts.append(status_display)
|
||||
|
||||
@@ -95,24 +97,24 @@ def populate_computed_fields(apps, schema_editor):
|
||||
if ride.ride_model.manufacturer:
|
||||
search_parts.append(ride.ride_model.manufacturer.name)
|
||||
|
||||
ride.search_text = ' '.join(filter(None, search_parts)).lower()
|
||||
ride.search_text = " ".join(filter(None, search_parts)).lower()
|
||||
|
||||
# Bulk update all rides
|
||||
Ride.objects.bulk_update(rides, ['opening_year', 'search_text'], batch_size=1000)
|
||||
Ride.objects.bulk_update(rides, ["opening_year", "search_text"], batch_size=1000)
|
||||
|
||||
|
||||
def reverse_populate_computed_fields(apps, schema_editor):
|
||||
"""Clear computed fields (reverse operation)."""
|
||||
Ride = apps.get_model('rides', 'Ride')
|
||||
Ride = apps.get_model("rides", "Ride")
|
||||
|
||||
# Disable pghistory triggers during bulk operations
|
||||
with pghistory.context(disable=True):
|
||||
Ride.objects.all().update(opening_year=None, search_text='')
|
||||
Ride.objects.all().update(opening_year=None, search_text="")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('rides', '0018_add_hybrid_filtering_fields'),
|
||||
("rides", "0018_add_hybrid_filtering_fields"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
@@ -19,163 +19,136 @@ from django.db import migrations
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('rides', '0019_populate_hybrid_filtering_fields'),
|
||||
("rides", "0019_populate_hybrid_filtering_fields"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Composite index for park + category filtering (very common)
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_ride_park_category_idx ON rides_ride (park_id, category) WHERE category != '';",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_park_category_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_park_category_idx;",
|
||||
),
|
||||
|
||||
# Composite index for park + status filtering (common)
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_ride_park_status_idx ON rides_ride (park_id, status);",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_park_status_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_park_status_idx;",
|
||||
),
|
||||
|
||||
# Composite index for category + status filtering
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_ride_category_status_idx ON rides_ride (category, status) WHERE category != '';",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_category_status_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_category_status_idx;",
|
||||
),
|
||||
|
||||
# Composite index for manufacturer + category
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_ride_manufacturer_category_idx ON rides_ride (manufacturer_id, category) WHERE manufacturer_id IS NOT NULL AND category != '';",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_manufacturer_category_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_manufacturer_category_idx;",
|
||||
),
|
||||
|
||||
# Composite index for opening year + category (for timeline filtering)
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_ride_opening_year_category_idx ON rides_ride (opening_year, category) WHERE opening_year IS NOT NULL AND category != '';",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_opening_year_category_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_opening_year_category_idx;",
|
||||
),
|
||||
|
||||
# Partial index for operating rides only (most common filter)
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_ride_operating_only_idx ON rides_ride (park_id, category, opening_year) WHERE status = 'OPERATING';",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_operating_only_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_operating_only_idx;",
|
||||
),
|
||||
|
||||
# Partial index for roller coasters only (popular category)
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_ride_roller_coasters_idx ON rides_ride (park_id, status, opening_year) WHERE category = 'RC';",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_roller_coasters_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_roller_coasters_idx;",
|
||||
),
|
||||
|
||||
# Covering index for list views (includes commonly displayed fields)
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_ride_list_covering_idx ON rides_ride (park_id, category, status) INCLUDE (name, opening_date, average_rating);",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_list_covering_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_list_covering_idx;",
|
||||
),
|
||||
|
||||
# GIN index for full-text search on computed search_text field
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_ride_search_text_gin_idx ON rides_ride USING gin(to_tsvector('english', search_text));",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_search_text_gin_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_search_text_gin_idx;",
|
||||
),
|
||||
|
||||
# Trigram index for fuzzy text search
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_ride_search_text_trgm_idx ON rides_ride USING gin(search_text gin_trgm_ops);",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_search_text_trgm_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_search_text_trgm_idx;",
|
||||
),
|
||||
|
||||
# Index for rating-based filtering
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_ride_rating_idx ON rides_ride (average_rating) WHERE average_rating IS NOT NULL;",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_rating_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_rating_idx;",
|
||||
),
|
||||
|
||||
# Index for capacity-based filtering
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_ride_capacity_idx ON rides_ride (capacity_per_hour) WHERE capacity_per_hour IS NOT NULL;",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_capacity_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_capacity_idx;",
|
||||
),
|
||||
|
||||
# Index for height requirement filtering
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_ride_height_req_idx ON rides_ride (min_height_in, max_height_in) WHERE min_height_in IS NOT NULL OR max_height_in IS NOT NULL;",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_height_req_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_height_req_idx;",
|
||||
),
|
||||
|
||||
# Composite index for ride model filtering
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_ride_model_manufacturer_idx ON rides_ride (ride_model_id, manufacturer_id) WHERE ride_model_id IS NOT NULL;",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_model_manufacturer_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_model_manufacturer_idx;",
|
||||
),
|
||||
|
||||
# Index for designer filtering
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_ride_designer_idx ON rides_ride (designer_id, category) WHERE designer_id IS NOT NULL;",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_designer_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_designer_idx;",
|
||||
),
|
||||
|
||||
# Index for park area filtering
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_ride_park_area_idx ON rides_ride (park_area_id, status) WHERE park_area_id IS NOT NULL;",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_park_area_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ride_park_area_idx;",
|
||||
),
|
||||
|
||||
# Roller coaster stats indexes for performance
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_rollercoasterstats_height_idx ON rides_rollercoasterstats (height_ft) WHERE height_ft IS NOT NULL;",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_height_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_height_idx;",
|
||||
),
|
||||
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_rollercoasterstats_speed_idx ON rides_rollercoasterstats (speed_mph) WHERE speed_mph IS NOT NULL;",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_speed_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_speed_idx;",
|
||||
),
|
||||
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_rollercoasterstats_inversions_idx ON rides_rollercoasterstats (inversions);",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_inversions_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_inversions_idx;",
|
||||
),
|
||||
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_rollercoasterstats_type_material_idx ON rides_rollercoasterstats (roller_coaster_type, track_material);",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_type_material_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_type_material_idx;",
|
||||
),
|
||||
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_rollercoasterstats_launch_type_idx ON rides_rollercoasterstats (launch_type);",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_launch_type_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_launch_type_idx;",
|
||||
),
|
||||
|
||||
# Composite index for complex roller coaster filtering
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_rollercoasterstats_complex_idx ON rides_rollercoasterstats (roller_coaster_type, track_material, launch_type) INCLUDE (height_ft, speed_mph, inversions);",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_complex_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_complex_idx;",
|
||||
),
|
||||
|
||||
# Index for ride model filtering and search
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_ridemodel_manufacturer_category_idx ON rides_ridemodel (manufacturer_id, category) WHERE manufacturer_id IS NOT NULL;",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ridemodel_manufacturer_category_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ridemodel_manufacturer_category_idx;",
|
||||
),
|
||||
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_ridemodel_name_trgm_idx ON rides_ridemodel USING gin(name gin_trgm_ops);",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ridemodel_name_trgm_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_ridemodel_name_trgm_idx;",
|
||||
),
|
||||
|
||||
# Index for company role-based filtering
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_company_manufacturer_role_idx ON rides_company USING gin(roles) WHERE 'MANUFACTURER' = ANY(roles);",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_company_manufacturer_role_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_company_manufacturer_role_idx;",
|
||||
),
|
||||
|
||||
migrations.RunSQL(
|
||||
"CREATE INDEX rides_company_designer_role_idx ON rides_company USING gin(roles) WHERE 'DESIGNER' = ANY(roles);",
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_company_designer_role_idx;"
|
||||
reverse_sql="DROP INDEX IF EXISTS rides_company_designer_role_idx;",
|
||||
),
|
||||
|
||||
# Ensure trigram extension is available for fuzzy search
|
||||
migrations.RunSQL(
|
||||
"CREATE EXTENSION IF NOT EXISTS pg_trgm;",
|
||||
reverse_sql="-- Cannot safely drop pg_trgm extension"
|
||||
"CREATE EXTENSION IF NOT EXISTS pg_trgm;", reverse_sql="-- Cannot safely drop pg_trgm extension"
|
||||
),
|
||||
]
|
||||
|
||||
@@ -11,30 +11,30 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rides', '0025_convert_ride_status_to_fsm'),
|
||||
("rides", "0025_convert_ride_status_to_fsm"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Remove the old unique_together constraint
|
||||
migrations.AlterUniqueTogether(
|
||||
name='ridemodel',
|
||||
name="ridemodel",
|
||||
unique_together=set(),
|
||||
),
|
||||
# Add new UniqueConstraints with better error messages
|
||||
migrations.AddConstraint(
|
||||
model_name='ridemodel',
|
||||
model_name="ridemodel",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=['manufacturer', 'name'],
|
||||
name='ridemodel_manufacturer_name_unique',
|
||||
violation_error_message='A ride model with this name already exists for this manufacturer'
|
||||
fields=["manufacturer", "name"],
|
||||
name="ridemodel_manufacturer_name_unique",
|
||||
violation_error_message="A ride model with this name already exists for this manufacturer",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='ridemodel',
|
||||
model_name="ridemodel",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=['manufacturer', 'slug'],
|
||||
name='ridemodel_manufacturer_slug_unique',
|
||||
violation_error_message='A ride model with this slug already exists for this manufacturer'
|
||||
fields=["manufacturer", "slug"],
|
||||
name="ridemodel_manufacturer_slug_unique",
|
||||
violation_error_message="A ride model with this slug already exists for this manufacturer",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -98,23 +98,17 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="coasters_count",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of coasters manufactured (auto-calculated)"
|
||||
),
|
||||
field=models.IntegerField(default=0, help_text="Number of coasters manufactured (auto-calculated)"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="description",
|
||||
field=models.TextField(
|
||||
blank=True, help_text="Detailed company description"
|
||||
),
|
||||
field=models.TextField(blank=True, help_text="Detailed company description"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="founded_date",
|
||||
field=models.DateField(
|
||||
blank=True, help_text="Date the company was founded", null=True
|
||||
),
|
||||
field=models.DateField(blank=True, help_text="Date the company was founded", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
@@ -124,9 +118,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="rides_count",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of rides manufactured (auto-calculated)"
|
||||
),
|
||||
field=models.IntegerField(default=0, help_text="Number of rides manufactured (auto-calculated)"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
@@ -151,9 +143,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
help_text="URL-friendly identifier", max_length=255, unique=True
|
||||
),
|
||||
field=models.SlugField(help_text="URL-friendly identifier", max_length=255, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
@@ -163,23 +153,17 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="coasters_count",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of coasters manufactured (auto-calculated)"
|
||||
),
|
||||
field=models.IntegerField(default=0, help_text="Number of coasters manufactured (auto-calculated)"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="description",
|
||||
field=models.TextField(
|
||||
blank=True, help_text="Detailed company description"
|
||||
),
|
||||
field=models.TextField(blank=True, help_text="Detailed company description"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="founded_date",
|
||||
field=models.DateField(
|
||||
blank=True, help_text="Date the company was founded", null=True
|
||||
),
|
||||
field=models.DateField(blank=True, help_text="Date the company was founded", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
@@ -210,9 +194,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="rides_count",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of rides manufactured (auto-calculated)"
|
||||
),
|
||||
field=models.IntegerField(default=0, help_text="Number of rides manufactured (auto-calculated)"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
@@ -237,9 +219,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
db_index=False, help_text="URL-friendly identifier", max_length=255
|
||||
),
|
||||
field=models.SlugField(db_index=False, help_text="URL-friendly identifier", max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
@@ -321,23 +301,17 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphoto",
|
||||
name="caption",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Photo caption or description", max_length=500
|
||||
),
|
||||
field=models.CharField(blank=True, help_text="Photo caption or description", max_length=500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphoto",
|
||||
name="copyright_info",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Copyright information", max_length=255
|
||||
),
|
||||
field=models.CharField(blank=True, help_text="Copyright information", max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphoto",
|
||||
name="photographer",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Name of the photographer", max_length=255
|
||||
),
|
||||
field=models.CharField(blank=True, help_text="Name of the photographer", max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphoto",
|
||||
@@ -352,9 +326,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphoto",
|
||||
name="source",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Source of the photo", max_length=255
|
||||
),
|
||||
field=models.CharField(blank=True, help_text="Source of the photo", max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphotoevent",
|
||||
@@ -368,16 +340,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphotoevent",
|
||||
name="caption",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Photo caption or description", max_length=500
|
||||
),
|
||||
field=models.CharField(blank=True, help_text="Photo caption or description", max_length=500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphotoevent",
|
||||
name="copyright_info",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Copyright information", max_length=255
|
||||
),
|
||||
field=models.CharField(blank=True, help_text="Copyright information", max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphotoevent",
|
||||
@@ -403,9 +371,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphotoevent",
|
||||
name="photographer",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Name of the photographer", max_length=255
|
||||
),
|
||||
field=models.CharField(blank=True, help_text="Name of the photographer", max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphotoevent",
|
||||
@@ -422,9 +388,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphotoevent",
|
||||
name="source",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Source of the photo", max_length=255
|
||||
),
|
||||
field=models.CharField(blank=True, help_text="Source of the photo", max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodeltechnicalspec",
|
||||
@@ -709,9 +673,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="cars_per_train",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="Number of cars per train", null=True
|
||||
),
|
||||
field=models.PositiveIntegerField(blank=True, help_text="Number of cars per train", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
@@ -727,9 +689,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="inversions",
|
||||
field=models.PositiveIntegerField(
|
||||
default=0, help_text="Number of inversions"
|
||||
),
|
||||
field=models.PositiveIntegerField(default=0, help_text="Number of inversions"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
@@ -766,16 +726,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="ride_time_seconds",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="Duration of the ride in seconds", null=True
|
||||
),
|
||||
field=models.PositiveIntegerField(blank=True, help_text="Duration of the ride in seconds", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="seats_per_car",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="Number of seats per car", null=True
|
||||
),
|
||||
field=models.PositiveIntegerField(blank=True, help_text="Number of seats per car", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
@@ -809,16 +765,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="trains_count",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="Number of trains", null=True
|
||||
),
|
||||
field=models.PositiveIntegerField(blank=True, help_text="Number of trains", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
name="cars_per_train",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="Number of cars per train", null=True
|
||||
),
|
||||
field=models.PositiveIntegerField(blank=True, help_text="Number of cars per train", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
@@ -834,9 +786,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
name="inversions",
|
||||
field=models.PositiveIntegerField(
|
||||
default=0, help_text="Number of inversions"
|
||||
),
|
||||
field=models.PositiveIntegerField(default=0, help_text="Number of inversions"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
@@ -896,16 +846,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
name="ride_time_seconds",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="Duration of the ride in seconds", null=True
|
||||
),
|
||||
field=models.PositiveIntegerField(blank=True, help_text="Duration of the ride in seconds", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
name="seats_per_car",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="Number of seats per car", null=True
|
||||
),
|
||||
field=models.PositiveIntegerField(blank=True, help_text="Number of seats per car", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
@@ -939,8 +885,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
name="trains_count",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="Number of trains", null=True
|
||||
),
|
||||
field=models.PositiveIntegerField(blank=True, help_text="Number of trains", null=True),
|
||||
),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user