From 491be57ab2c19fb72a2e7efb0b7a9833038ace01 Mon Sep 17 00:00:00 2001 From: pacnpal <183241239+pacnpal@users.noreply.github.com> Date: Tue, 5 Nov 2024 04:10:47 +0000 Subject: [PATCH] initial geodjango implementation --- location/migrations/0004_add_point_field.py | 27 +++ .../0005_convert_coordinates_to_points.py | 52 ++++++ .../0006_readd_historical_records.py | 174 ++++++++++++++++++ location/models.py | 51 ++++- media/__pycache__/models.cpython-312.pyc | Bin 5100 -> 5169 bytes media/migrations/0006_photo_is_approved.py | 18 ++ media/models.py | 1 + media/views.py | 6 +- parks/__pycache__/models.cpython-312.pyc | Bin 10438 -> 7907 bytes parks/__pycache__/views.cpython-312.pyc | Bin 21684 -> 24235 bytes parks/forms.py | 124 ++++++++++--- ..._alter_historicalpark_latitude_and_more.py | 5 - .../0008_historicalpark_historicalparkarea.py | 3 - .../0009_migrate_to_location_model.py | 83 +++++++++ .../0010_remove_legacy_location_fields.py | 69 +++++++ parks/models.py | 103 ++--------- parks/views.py | 65 +++++-- static/css/tailwind.css | 67 +++++-- templates/parks/park_detail.html | 67 +++---- templates/rides/ride_detail.html | 78 ++++---- .../__pycache__/settings.cpython-312.pyc | Bin 5767 -> 5806 bytes thrillwiki/settings.py | 4 +- 22 files changed, 768 insertions(+), 229 deletions(-) create mode 100644 location/migrations/0004_add_point_field.py create mode 100644 location/migrations/0005_convert_coordinates_to_points.py create mode 100644 location/migrations/0006_readd_historical_records.py create mode 100644 media/migrations/0006_photo_is_approved.py create mode 100644 parks/migrations/0009_migrate_to_location_model.py create mode 100644 parks/migrations/0010_remove_legacy_location_fields.py diff --git a/location/migrations/0004_add_point_field.py b/location/migrations/0004_add_point_field.py new file mode 100644 index 00000000..1d98f195 --- /dev/null +++ b/location/migrations/0004_add_point_field.py @@ -0,0 +1,27 @@ +# Generated by Django 5.1.2 on 2024-11-04 22:30 + +import django.contrib.gis.db.models.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("location", "0003_alter_historicallocation_city_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="location", + name="point", + field=django.contrib.gis.db.models.fields.PointField( + blank=True, + help_text="Geographic coordinates as a Point", + null=True, + srid=4326, + ), + ), + migrations.DeleteModel( + name="HistoricalLocation", + ), + ] diff --git a/location/migrations/0005_convert_coordinates_to_points.py b/location/migrations/0005_convert_coordinates_to_points.py new file mode 100644 index 00000000..0648cb35 --- /dev/null +++ b/location/migrations/0005_convert_coordinates_to_points.py @@ -0,0 +1,52 @@ +# Generated by Django 5.1.2 on 2024-11-04 22:21 + +from django.db import migrations, transaction +from django.contrib.gis.geos import Point + +def forwards_func(apps, schema_editor): + """Convert existing lat/lon coordinates to points""" + Location = apps.get_model("location", "Location") + db_alias = schema_editor.connection.alias + + # Update all locations with points based on existing lat/lon + with transaction.atomic(): + for location in Location.objects.using(db_alias).all(): + if location.latitude is not None and location.longitude is not None: + try: + location.point = Point( + float(location.longitude), # x coordinate (longitude) + float(location.latitude), # y coordinate (latitude) + srid=4326 # WGS84 coordinate system + ) + location.save(update_fields=['point']) + except (ValueError, TypeError): + print(f"Warning: Could not convert coordinates for location {location.id}") + continue + +def reverse_func(apps, schema_editor): + """Convert points back to lat/lon coordinates""" + Location = apps.get_model("location", "Location") + db_alias = schema_editor.connection.alias + + # Update all locations with lat/lon based on points + with transaction.atomic(): + for location in Location.objects.using(db_alias).all(): + if location.point: + try: + location.latitude = location.point.y + location.longitude = location.point.x + location.point = None + location.save(update_fields=['latitude', 'longitude', 'point']) + except (ValueError, TypeError, AttributeError): + print(f"Warning: Could not convert point back to coordinates for location {location.id}") + continue + +class Migration(migrations.Migration): + + dependencies = [ + ('location', '0004_add_point_field'), + ] + + operations = [ + migrations.RunPython(forwards_func, reverse_func, atomic=True), + ] diff --git a/location/migrations/0006_readd_historical_records.py b/location/migrations/0006_readd_historical_records.py new file mode 100644 index 00000000..3bc51178 --- /dev/null +++ b/location/migrations/0006_readd_historical_records.py @@ -0,0 +1,174 @@ +# Generated by Django 5.1.2 on 2024-11-04 22:32 + +import django.contrib.gis.db.models.fields +import django.core.validators +import django.db.models.deletion +import simple_history.models +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("contenttypes", "0002_remove_content_type_name"), + ("location", "0005_convert_coordinates_to_points"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="HistoricalLocation", + fields=[ + ( + "id", + models.BigIntegerField( + auto_created=True, blank=True, db_index=True, verbose_name="ID" + ), + ), + ("object_id", models.PositiveIntegerField()), + ( + "name", + models.CharField( + help_text="Name of the location (e.g. business name, landmark)", + max_length=255, + ), + ), + ( + "location_type", + models.CharField( + help_text="Type of location (e.g. business, landmark, address)", + max_length=50, + ), + ), + ( + "latitude", + models.DecimalField( + blank=True, + decimal_places=6, + help_text="Latitude coordinate (legacy field)", + max_digits=9, + null=True, + validators=[ + django.core.validators.MinValueValidator(-90), + django.core.validators.MaxValueValidator(90), + ], + ), + ), + ( + "longitude", + models.DecimalField( + blank=True, + decimal_places=6, + help_text="Longitude coordinate (legacy field)", + max_digits=9, + null=True, + validators=[ + django.core.validators.MinValueValidator(-180), + django.core.validators.MaxValueValidator(180), + ], + ), + ), + ( + "point", + django.contrib.gis.db.models.fields.PointField( + blank=True, + help_text="Geographic coordinates as a Point", + null=True, + srid=4326, + ), + ), + ( + "street_address", + models.CharField(blank=True, max_length=255, null=True), + ), + ("city", models.CharField(blank=True, max_length=100, null=True)), + ( + "state", + models.CharField( + blank=True, + help_text="State/Region/Province", + max_length=100, + null=True, + ), + ), + ("country", models.CharField(blank=True, max_length=100, null=True)), + ("postal_code", models.CharField(blank=True, max_length=20, null=True)), + ("created_at", models.DateTimeField(blank=True, editable=False)), + ("updated_at", models.DateTimeField(blank=True, editable=False)), + ("history_id", models.AutoField(primary_key=True, serialize=False)), + ("history_date", models.DateTimeField(db_index=True)), + ("history_change_reason", models.CharField(max_length=100, null=True)), + ( + "history_type", + models.CharField( + choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], + max_length=1, + ), + ), + ], + options={ + "verbose_name": "historical location", + "verbose_name_plural": "historical locations", + "ordering": ("-history_date", "-history_id"), + "get_latest_by": ("history_date", "history_id"), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.RemoveIndex( + model_name="location", + name="location_lo_latitud_7045c4_idx", + ), + migrations.AlterField( + model_name="location", + name="latitude", + field=models.DecimalField( + blank=True, + decimal_places=6, + help_text="Latitude coordinate (legacy field)", + max_digits=9, + null=True, + validators=[ + django.core.validators.MinValueValidator(-90), + django.core.validators.MaxValueValidator(90), + ], + ), + ), + migrations.AlterField( + model_name="location", + name="longitude", + field=models.DecimalField( + blank=True, + decimal_places=6, + help_text="Longitude coordinate (legacy field)", + max_digits=9, + null=True, + validators=[ + django.core.validators.MinValueValidator(-180), + django.core.validators.MaxValueValidator(180), + ], + ), + ), + migrations.AddField( + model_name="historicallocation", + name="content_type", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="contenttypes.contenttype", + ), + ), + migrations.AddField( + model_name="historicallocation", + name="history_user", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/location/models.py b/location/models.py index 84f2136a..b758aa78 100644 --- a/location/models.py +++ b/location/models.py @@ -1,8 +1,10 @@ +from django.contrib.gis.db import models as gis_models from django.db import models from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.validators import MinValueValidator, MaxValueValidator from simple_history.models import HistoricalRecords +from django.contrib.gis.geos import Point class Location(models.Model): """ @@ -27,7 +29,7 @@ class Location(models.Model): MinValueValidator(-90), MaxValueValidator(90) ], - help_text="Latitude coordinate", + help_text="Latitude coordinate (legacy field)", null=True, blank=True ) @@ -38,12 +40,20 @@ class Location(models.Model): MinValueValidator(-180), MaxValueValidator(180) ], - help_text="Longitude coordinate", + help_text="Longitude coordinate (legacy field)", null=True, blank=True ) - # Address components - all made nullable + # GeoDjango point field + point = gis_models.PointField( + srid=4326, # WGS84 coordinate system + null=True, + blank=True, + help_text="Geographic coordinates as a Point" + ) + + # Address components street_address = models.CharField(max_length=255, blank=True, null=True) city = models.CharField(max_length=100, blank=True, null=True) state = models.CharField(max_length=100, blank=True, null=True, help_text="State/Region/Province") @@ -58,7 +68,6 @@ class Location(models.Model): class Meta: indexes = [ models.Index(fields=['content_type', 'object_id']), - models.Index(fields=['latitude', 'longitude']), models.Index(fields=['city']), models.Index(fields=['country']), ] @@ -73,6 +82,15 @@ class Location(models.Model): location_str = ", ".join(location_parts) if location_parts else "Unknown location" return f"{self.name} ({location_str})" + def save(self, *args, **kwargs): + # Sync point field with lat/lon fields for backward compatibility + if self.latitude is not None and self.longitude is not None and not self.point: + self.point = Point(float(self.longitude), float(self.latitude)) + elif self.point and (self.latitude is None or self.longitude is None): + self.longitude = self.point.x + self.latitude = self.point.y + super().save(*args, **kwargs) + def get_formatted_address(self): """Returns a formatted address string""" components = [] @@ -91,6 +109,29 @@ class Location(models.Model): @property def coordinates(self): """Returns coordinates as a tuple""" - if self.latitude is not None and self.longitude is not None: + if self.point: + return (self.point.y, self.point.x) # Returns (latitude, longitude) + elif self.latitude is not None and self.longitude is not None: return (float(self.latitude), float(self.longitude)) return None + + def distance_to(self, other_location): + """ + Calculate the distance to another location in meters. + Returns None if either location is missing coordinates. + """ + if not self.point or not other_location.point: + return None + return self.point.distance(other_location.point) * 100000 # Convert to meters + + def nearby_locations(self, distance_km=10): + """ + Find locations within specified distance in kilometers. + Returns a queryset of nearby Location objects. + """ + if not self.point: + return Location.objects.none() + + return Location.objects.filter( + point__distance_lte=(self.point, distance_km * 1000) # Convert km to meters + ).exclude(pk=self.pk) diff --git a/media/__pycache__/models.cpython-312.pyc b/media/__pycache__/models.cpython-312.pyc index f9ed7f9a2495c45bd5bbecc764fa7cbc28d86587..22ec7541faf472d198482e8107efd1f8b9a84ce7 100644 GIT binary patch delta 377 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 literal 10438 zcmeG?TWlLwb~AjxBtAq@FG`~IuuaF7Uy5IGtW_jgRwC1mB_|ErhD&iq66GPOy)&{c zR%yAZ+eksXR)Ds)>ujTJQ$(%>tQJ^cy}%Zm1T7GtEkKq6#0+d~il!g_lPwiB{L`Lu zheJ{_mGu_==toD;nRCxQ_uSXH=bm$h|5#CBVc>aGvwLiykzxJ~3+kiKBkMUpZZI6f zu>uoggDi`M5=h3^8NSNO8SjikXAvm?dbTWrJXi*@8BT8-#Qhu%4>~ z*udEVdNoWx!#S=ooRhD;t5xO@Y@}2dP^(I)P1A0F^+ROwv$7$U;CMj<%t-jlJP~&b$rnqA z0K!*8Ty!idiBKhl1z)}pj&nXv*@w`i5RUMoTJ$N!KvHv{LuL=$VPz9ZOvOR|nBPcn ze6m)Qh-~`aR5&g{_r!`c^YHW}Pq$dMP%WT1!CBnV%6iz`DPFb|ha*>R?q0-Q@cDdC zyY`PKV*LKeaAdzUPNITvC3-1Zf`+(>aC|HgiYG#Z4@V^6B$vefiVAm3PRs7Nx-g;Q z!jL8qZ-;*|3LwR-=_>T@oYgV=a;~ao!@xLeKh)mryKTujT4(j&IBIe>=k>r`;KQQ} zor|r@m6<)KGOeew4PF1VXEuV;VfF5> z>{V-8cs~_uptpB5-4^DLx^)0Qx464^Xg~L{05@qVY8KVfp8|3N^y2$R%_X|^?K=8v zHIr2}u0$I(sx~TGxV8DUzlG9S86^NUA`3vMz54o?-0Bs~@QbHsV2mnnzWP;5rjL>ZA^Us!Rk7 zJ0sUaKWL-)5`D-Y3gQEX9oXW#08-4?MqAogyQVRkyS{NZeAu&~zx`^~y=%7bYj@p^ zzz2a(jxL_awjTNW`uRZCJviH!tE!zHTr)5>XKL`thMsZm_{5*K?*%0=cingGNVmM4 zb)8DvPJuRMv)KDQvq;3jbm276?vPdlZ3x-{lun_pI0dzdI)^>L+KKF?bGWeV%vukp zb%)6=sEWXU;Ze1GeO|{f@4;U1OpxW6pk@qq;zw*yt6_NE1deCR!)cZZ=OSpJT$Ni` zpa{2wZb`46y`YKmxytbAmJI4C3tG1A)5vQ%(~?=$kD!%u)|QcH;jEkv{%pWe0soa; zg(_ct1JzDzDpiX5<{e|&CHvj{T^4i#mu{k5FOGaK$f5M&Qe~*`LN8S+MSb(GO}$jn zUdpaCtf9NGhVJL&tA0*C&vWwCY|CfdG`?EOcc{#sz>M zx7{TV>cUdZUC?oS&`Si~lS-)+0#{KFo41u%%WakDqr9(_%2IIJb8*%!&&k(H`OZ8m zk8g*O4?at$+FrL*e-~{lJi%QcVK?vR8kW3;HQz&}zM*iGLOROE0^i>4`Ieeg{SEE| zu8zBTF~R-R^KJ&u+gFftfZG9{`oURdl;v8knSzJ7Rwy6lJGr)NOrhnA{1L9bq&}C}<2Qg0Za#Ou??U(RK%gH`#cu{5O(R=-2G0%k^@fJ~&Yn-YhWXfJ zf`q}7n)dYw36bYYSU|wpr0YBno@AVlOQkF>@Xh-!gnG^ehK4U(>=_<77f8A;#yPmr zdlGT*0H-4PSisWTclO-iz|fh#UfI&i#}YzR9H-rwFZ2zb>**eb3Nr)`i3nW6+{6EZ z1p>E{I)efqACtz1{Tgx%sIp-y9{t`F54HLcAsoN-&<#|w4}f2*a2O-wi72?MvXSFQ z!&8DpT95^=hGg?#{?;q?9d_?KegNI#;|KgnE#R^SFav48R(RRUy7-hJke3iOkMqK0 zNaC+bvL%0`B#21-D7+b-aj*Hy+21!F@Na@oS%&QR_O+@=_Sq3;GVg2u&f{RVXA| zLLu*tTnnH09) z0{SM`0*8nTw2s~jI>;07-ueuIUIqLtxd0U~S)IrWqorV8G zP)RX4d)4*nx#_g2R&fl}g~5){j9>(@4iP)^=j?W45bH~w^@+TJMFHxgnu{4Gq>^wx zg+Q@DWWMi}(0`*(d5%)&5ai(ssoZ5Q&MorFaB$k9vkCI@rW3+Z)h%M?v@^_~!oitg zqs)xv6ZQv?6qyE<4ipa}JurR~tUsg>2+E*4BAZnIl15{)Sx`@utQV)ifI$O@UGM0~ z??4k-7bat(Y`AoVzRjUfL=`a8M2ab@O9T};si-Hmejkc7#(R?A&G)D8*ETQc zJ~e-A&erak?fsJh^w%BNU!Hq8ZEd^nJ+SQlS>sO{KVv^TnDzES{n?z=e*M(kskF8I ze&fOAwx4zUq~o)e&%)WpGf*4&##Aw9oU_eyX;b^n(X6SxyrxiHMZdGunaACNeaxo; zptA5pS#TcnbTIR}K+;JK!3l(52c&RRAgC_jzmLKT>d#bDVmNpUlK*!eqTN& zbB$sWLqbzeuT99GC2Qy=K3j1M+KH$G5WhNHvwGCu z@&1V-x&BuDimeUFC*MC=BztdpS8O};mhfhE*4~^pH7klxs+{I)~p;Wkx?GTTyqD~ggQFgvj2DP}Gv3I?snqWWL&Xl69z+;21^ z%|w}44>ErF3xv~;Y9gAqXtr2IlnBihWAWbO@6saF1%D-0&TfY)p(y@DWLrwO~kOTW^9F z0|}|`Du|Jd`6jYqG%A40lE3&vBh#{eG)mysy#lf!6R3>{qGGjXfUr4HV?_fsG9ALv zBpn5lm8X0H{Of-Q0J3)mi@7~l<+<_F2QS^|`k-r3H{X@5+P65BuIfzNI-lh1u2p+; z#@_sxF`3)H_SD_D^ueW7&+d$8_ac|}?9O@)uX;{qJSXpkvYxZ6o}r9qDC@a6+y9l_ zv!>BH>v9cEAH8(zrH{I9buEtlc;e2)&8}?2@q3-=hOU2ccRk6~)UVd;%GB(FL=P~# zYjdvZRaZ;K)sl6!rEP6%dLTdjdJEABV|lvX0W)~F(Z?)wTY41<{s1^CJ2&1!zkYWynBhZ6vx_Dtwkzu^TkBCt)sFiT#lkgZHQd-Hjfw;tv z@ya^LR+@+mktobpuFe+{g1{3PRagW)6=lom1mUA&@z?lif@fGZ4fPF&0v87dWqslb zeD@$|dkA`dvSAXxIEZou&Sp3o%g@LHqXPmL3Pap5HHoJNae^j>pqdkOXUXp(h#){i zDVrt<&Q_Wx1dCSc-o$umJi(E7ky4R&c_O?ZghFsU$AC)j`R^#M&t51K1jn)BW3bLo zt}%}_MvoyCSaUHp@9dFPYeUA`upq8jUr6=ltPMrVa_5Tm7|>iT^E(yYU39OwI#RFY zDjH|sSgmNxR5a$Qs&D@O^6zbEHI;_c=`{-j+40M(_LhvjC0FIn)zs!HoViL@&UN@- zZH>3a7WmuotZm;{6z6WovbMcBI1EOe+pJ71%trwiWA9m>Mj==~bII zWAiR*VY;?sslHrg)9mPKWmBfI3G({GE0rfw{iRDVa!3=)$E$TtXPBP!}qwAnx3@Ior76=R-J7bXB*6NA-u@lYlBsE8B(u; zWIK%EdSWiI(7RIUPxS-Ejkm`G6 zU@9ALUR=JsQgI4))WEoQ;y~Lo&i3U_7`?M6^=i)9uF@8z6=!Ga)vqd>vz0qi{r{N*)$F;2f3=5JBm5($gy>DF$*cxJMZpPqzq_OGSk>=b$jQSB& zXYn8u9`(vw|68~)OOvjUP|;2lK93dn8D0-`jM=t49J^FVmIsZrwPOnl6gh42Y7Uwy z7bY6EJe#b3jsh4aClKvLFo+-kV3P)@u4O*HSCj5a3Iac^j%k9r zX`NsHv_kEXrX%QGM_xslc*_O+`Yo3w!Q`H7R}xDhDFHslI2iReu>~go6;nzL^db~L zhkvmOz%?ZyF>A^->{vLGuHQ3fSNNYvP2lZ||LZuj?a~6b;p+kTZ0aG_C()NRC(?L3^O{o?<5;hSxvzR&{ zBUnU*Asm2mDS)C{U>e{{Nh?w7@?@ zYY{y=8vo~P&AA=zs(#$#ei{9FG*fkEL$7sM))=kXqG(E~2BW6X_4)r85cK%}d<=-{ z^X)MpkzhwplPQ4YGJs)n710!cO`#ffP?PRWnw1aJve@7c5nKb{Z`^8*QC|qAET|VC z8+*Ekdb)f26fOQ^q*GJ(9%9r8%px|2;0FkTQ#ck>yI=R&9YhfHBARMq6CrFd>N94BDF5E zQ>92zLF~Y9Bh)O*EqQt*0ST?>NC&DPSBufuBz$qmCmuSK9{^D;kR1{FBe5uqBu}Q1 zoh`<5(XdLQ5nYX%>X1k{atY#2wHCcbnx-Z`#L!AAq3>SAsJd)+A!O4@J#@e4w> zNw=PDCW~_#JI`&hlik?sWXCN@F(ZPKK}Fn|9L;Y{~W!1mOu9zU-XvARKUUa#HP@J#&su)oD=`RbKGez%0+pJ zi}PVVt_rK->adzeol4TgwP9^s2n%stSQpoa^-Qmp4Do_+LEIQNvb07jjGMwHCTpdl zxH)WQvLF@5Eny3jby7*(8n!Z7FWKUy;Zl$%xTrz0#~on@GZaY9cv-kCULG!wSA;8= z&M3LUu4rM@Bvr=UVRzgU_JF=HS|oYnzOawUW~nOf5BuYRaDe40ma5~yaFEFssU}_< zu8oJnp?F=mj_FFI`glXQf#)WmmsY7U-V|>}CxTxbH zF6tD8b7~wvjtjRkZ5e3G#h_R#hQvBidrmiFZDZC7u)0L6s5_^b(YG^wCFtFt*Pq*+ zH!NWW4;Z|+DQ@XeW61X@mJw1bx^8hW7LQ0u!M4pi`nm_YH}C3Giq{R_7m;Go%_Aa- zBx8x;DTV(wUl`&Q{l>&#PBW~oP+3Z$5Je>pe~JqsBoIG+Qv0o{64dI6cxY6Vld@uz zlab`8JTRDuiX;d&Qcl+hUcQkS>M2il&79Sdo z3@2m9M1tc;Ixv%3P7>0I$y~!hf_B`2in+!t$K(nC#WI{AFqlwAbbyHRs6_8JnJT)_ zWG3Q$B9X;`17d=W#1_!DPd#9|Tg5+0-!t!`lBYlz=f}7qJ$>D7rTErfhLe%lunelQe2#&E!C3O>&BpRMBQA&qSzU8h`ZKFfT}bTo zzpRVyWutTD;`CPHkEGxmxQiI)IymCty-7FEkmto_Jra%9Zjl-Zj$S6$XuO}Xtq;oDgs z*#4N;5$p|{5$!E~MAHy;Iu|=`9z`O8<*bGrq!DLGz|~0((Me~O@J$fqv()T$(#y`r zc!};Wdtu`U+HnVkv;>EWt$sxlAqQk4V|o-o#%<({?Wv~f5SByt30D=sAQNOB;tZ%7%4S-G;6RgVb5!arkenG-w_FZP#ksr#S#3G5b_s#a@0RE1UIz zm0cuDqXz~?i8OHUVdxwg2Uo=itsdBSbO3E3b*}$0G-yyG_2H{Q_1k0ALu4#Wdo)?CGA;w;}6DkR51M~)+mA>hJb zgU^-)xgV1!5a!mxDNIfvJOCiioByw)65&ZSAWQt|A#S>eGg;`jd^U$pTX3U9uXX&< z#%uk##emMV%WR+ex$k#8zk|N&-{WHIK^2QChM1h#k4Jiv^nu@5*a5UD&v~hrIXce%_L9iX>-s{ zpH-KdHThE+wB}?J9l5iap0DYqPuJCHfD!^o34AlX9J0|>%Utx0YKKZwMqjTk!`xa^ zRCi8ACxb4QD!M6UzA05aKV`OHD%b88OU@bT(;*MP7^`YTmR;~gt@$>N3u8hzcMnYY zxX!>Omq2Sh+aiNd?K7hk!TbG z3z_UeC9ok7DWf&TIFf*r#GUfU$eGduw^>er#jbXI{Q+ceV2}3>)i9c&6TkQQnoYuBBq3OA%Y^Cozd!E>nsccMDHfDnjSzmR=w>afn zeEyD-tSswP%tYw9yK9jTg*OwICC&GO4DHeDj=nr%=B^N^cU8u{DCJ(1akr-2t>@QV*p_y$%6bDCZ%fMClJT~uyzS>>7faIK z?rfko6KGEb+B1R9RG{-hdThD=GU&V_B*&==(`%W z=~VBub-5vRMQ7-@X|EJ__M5e7g8-VeSqqx9O^vd{u)S8D_O96OS6{8E%IGxzHxZdTh63v25%Jxq@(8!b6@T2tTF|EcI1v z;kB#y8&Q>BTZbICna`0NIv~l>D@*U;pJFjWcgLD)#@VX5P~i4Vf4(CitmL7G0f-FC ziYt95bd!dZzC3HTpC}^Il)CflyyqEg$MXnh0A|C%nE`o;K7U7TXf+f<{snz*;mktN zGR_<_&T(c`H8rxr!W&fN69Wf#l^Q(z>GLaPDzC87`a7+vXdxp|U{iW3N2F>X(pe(> zf6}659xX;oo;HjNbYz8t{vcHIs70?L;hB_;Z-oX`Xmw-4P&+oQ891?okbsyjrjt7@z5bA&x+ zj5?0N!X7t1d>>HvEa?vQ&QP}v7!Jn)20j2BT`^_^blt?%vlI)&w_zbu->yVYzQkH6 z%R8Ss=jb}QIp4#mFIt5e)@;dZ=Kja{7yWtS%?|*h^;LNvM(d_|9qZ0_OzRvg9BM$4 zzo#sz%lnU+^5s0mJ)_B&&2ziqHEY~-ziEc=MZN77rLwF)eA8NM=o)%E;O>6@$fPRyRjxa0!=?2i!$(|(CM9XvWjlJI!s zL*HK@hzMSUcMxXd5po%mR)i}E?8IjbgX9t_nnz-7Xm8(-$R_R@Nb{mP7^?`oPb%tr z#iNQA9`WG)1IJq_nvL8B6QjdP@-CK<0g#IT7zxfo?kb(7D|_6!?Qmwxr!-T8JzwMb z<@922+k~EjKxD%c8#1njl&c|IUB56;7)pD)W`mQAZ%N9xBRckYp^_}BS9M5=KQ=Zn0r!(d0Ona71uFVEpGr`VOu=CyEvL^~2S3kbv zeW&MH{Tct6(QD53>8)z6X8A`p*Xd0sH$ArbnpFVJwK6SeuDR7H`wTl5 ztFP6q*x9Ilw}uD#-A3)s#Skr(^Iiq#_dgImz)8L}(pj?JYn+0~B_E;_Mmzf}eud(1 z5N?fVe!bp5@f!Mn%=`ghWgbKMIKmhL+dJbZVN^ptK{$eN2;qMn)BF~_5jt`4HGu$^ z_-tV@eQ1NzaSso7`1=+`aIsdl1brgf9`kppHIY?se=0UD;Q~zfB{3-(+#2jegbF>c(S-{0@Nt z-10!ux4BlxVh74GY!Y(85`AiORRzU7Ece1;K1H*en~m?|FrP*ziCVYNnp|@r4bMdE zWSZ^6P4h(G-m;eeklMF4AHevEJdQbTi98)>V#C2^hCQ!*19c}5*hHN|34XNsGa=`n z(2VdpeQ9flm(_*IP=}(6=6+4c-2gS{sC{%>c>^v#qC?Ol7yy*w=ul+%K%z-Lm>|i) z(WFe^xr8G|+Pkf41IB}jamEAQK$cd*N4(amn`1fH$rU@|$veh0{%#5Qb82ev|(sfjIa96dPaAzGJ-lQ#F; zI?K?p9MuJb2_iP#2XCD+sX&DVbLM_P$+yOZ-jxV$I^OS?zzeZb7#E|lNWMp2RGITP z^}N=HdG12M*oQrFvUe^P`?Jl$rX;tl*pzf(Ir`<$hEYjeN$fD6@W;s)0jAYF&wt2u zs#9Di{i=Vt`j=dQc5biO8|3|2O=(u+$ZDKfO>tIJbi)X`8{2r9=Qs10OKZ-Q-+;Rg d$TuW)k7`cGz&;G}lb*sa~9O%e0Nim&-P_ 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 {% endif %} {% endblock %} @@ -19,7 +19,7 @@ }) -
+
{% if user.is_authenticated %}
@@ -37,18 +37,16 @@ {% endif %} -
+
-
-

{{ park.name }}

- +
+

{{ park.name }}

{% if park.formatted_location %} -
- -

{{ park.formatted_location }}

-
- {% endif %} - +
+ +

{{ park.formatted_location }}

+
+ {% endif %}
+
- {% if park.total_rides %} -
Total Rides
-
{{ park.total_rides }}
+
Total Rides
+
+ {{ park.total_rides|default:"N/A" }} +
- {% endif %} - {% if park.total_roller_coasters %}
-
Roller Coasters
-
{{ park.total_roller_coasters }}
+
Roller Coasters
+
+ {{ park.total_roller_coasters|default:"N/A" }} +
- {% endif %}
{% if park.owner %}
- -
Owner
+ +
Owner
+ class="text-xs text-blue-600 sm:text-sm lg:text-base hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"> {{ park.owner.name }}
@@ -105,19 +103,19 @@ {% if park.opening_date %}
- -
Opened
-
{{ park.opening_date }}
+ +
Opened
+
{{ park.opening_date }}
{% endif %} {% if park.website %} {% endif %} {% if coaster_stats.speed_mph %}
-
Speed
-
{{ coaster_stats.speed_mph }} mph
+
Speed
+
{{ coaster_stats.speed_mph }} mph
{% endif %} {% if coaster_stats.inversions %}
-
Inversions
-
{{ coaster_stats.inversions }}
+
Inversions
+
{{ coaster_stats.inversions }}
{% endif %} {% if coaster_stats.length_ft %}
-
Length
-
{{ coaster_stats.length_ft }} ft
+
Length
+
{{ coaster_stats.length_ft }} ft
+
+ {% else %} +
+
Length
+
N/A
- {% endif %} {% endif %}
@@ -92,24 +96,20 @@
{% if ride.manufacturer %} + class="text-xs text-blue-600 sm:text-sm lg:text-base hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"> {% endif %} {% if ride.designer %}
- -
Designer
+ +
Designer
+ class="text-xs text-blue-600 sm:text-sm lg:text-base hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"> {{ ride.designer.name }}
@@ -118,41 +118,41 @@ {% if coaster_stats.roller_coaster_type %}
- -
Coaster Type
-
{{ coaster_stats.get_roller_coaster_type_display }}
+ +
Coaster Type
+
{{ coaster_stats.get_roller_coaster_type_display }}
{% endif %} {% if coaster_stats.track_material %}
- -
Track Material
-
{{ coaster_stats.get_track_material_display }}
+ +
Track Material
+
{{ coaster_stats.get_track_material_display }}
{% endif %} {% if ride.opening_date %}
- -
Opened
-
{{ ride.opening_date }}
+ +
Opened
+
{{ ride.opening_date }}
{% endif %} {% if ride.capacity_per_hour %}
- -
Capacity
-
{{ ride.capacity_per_hour }}/hr
+ +
Capacity
+
{{ ride.capacity_per_hour }}/hr
{% endif %} {% if coaster_stats.launch_type %}
- -
Launch Type
-
{{ coaster_stats.get_launch_type_display }}
+ +
Launch Type
+
{{ coaster_stats.get_launch_type_display }}
{% endif %}
diff --git a/thrillwiki/__pycache__/settings.cpython-312.pyc b/thrillwiki/__pycache__/settings.cpython-312.pyc index 67b8dc4fb253cf0b7bfe25bbf25d2c5c1798c6f9..2da92e9bc7707840419c9d1ee90aa891d04396f2 100644 GIT binary patch delta 196 zcmZqIU8l=?nwOW00SGR*Yo_OJWb0lM_oa^Yb=eW3^&YSHnCotl?YtXGgp?OhlOL< b6@{1@_&&K!Ruc(kwAx%H!pX!^2UG+AeOo)^ delta 156 zcmZ3d+pf!dnwOW00SI!l>= zB_$?jr{<*;>lNe|m!ub^78mAB4&_>_ED1E=7Ke|&vtx*-zuztP5KmuM|Im;k8KC$} zMj%n7v{{uqf|<<{#J1Yp%$LT>SU>rda16V=5K{x+C#T82BEgJ?n-_|3GO^SG6#)QW C6)b!J diff --git a/thrillwiki/settings.py b/thrillwiki/settings.py index 07963d12..80e76d9f 100644 --- a/thrillwiki/settings.py +++ b/thrillwiki/settings.py @@ -24,6 +24,7 @@ INSTALLED_APPS = [ "django.contrib.messages", "django.contrib.staticfiles", "django.contrib.sites", + "django.contrib.gis", # Add GeoDjango "allauth", "allauth.account", "allauth.socialaccount", @@ -47,6 +48,7 @@ INSTALLED_APPS = [ "history_tracking", "designers", "analytics", + "location", ] MIDDLEWARE = [ @@ -90,7 +92,7 @@ WSGI_APPLICATION = "thrillwiki.wsgi.application" # Database DATABASES = { "default": { - "ENGINE": "django.db.backends.postgresql", + "ENGINE": "django.contrib.gis.db.backends.postgis", # Update to use PostGIS "NAME": "thrillwiki", "USER": "wiki", "PASSWORD": "thrillwiki",