Refactor test utilities and enhance ASGI settings

- Cleaned up and standardized assertions in ApiTestMixin for API response validation.
- Updated ASGI settings to use os.environ for setting the DJANGO_SETTINGS_MODULE.
- Removed unused imports and improved formatting in settings.py.
- Refactored URL patterns in urls.py for better readability and organization.
- Enhanced view functions in views.py for consistency and clarity.
- Added .flake8 configuration for linting and style enforcement.
- Introduced type stubs for django-environ to improve type checking with Pylance.
This commit is contained in:
pacnpal
2025-08-20 19:51:59 -04:00
parent 69c07d1381
commit 66ed4347a9
230 changed files with 15094 additions and 11578 deletions

View File

@@ -9,40 +9,59 @@ from .models import Location
#
# This admin interface is kept for data migration and cleanup purposes only.
@admin.register(Location)
class LocationAdmin(admin.ModelAdmin):
list_display = ('name', 'location_type', 'city', 'state', 'country', 'created_at')
list_filter = ('location_type', 'country', 'state', 'city')
search_fields = ('name', 'street_address', 'city', 'state', 'country')
readonly_fields = ('created_at', 'updated_at', 'content_type', 'object_id')
list_display = (
"name",
"location_type",
"city",
"state",
"country",
"created_at",
)
list_filter = ("location_type", "country", "state", "city")
search_fields = ("name", "street_address", "city", "state", "country")
readonly_fields = ("created_at", "updated_at", "content_type", "object_id")
fieldsets = (
('⚠️ DEPRECATED MODEL', {
'description': 'This model is deprecated. Use domain-specific location models instead.',
'fields': (),
}),
('Basic Information', {
'fields': ('name', 'location_type')
}),
('Geographic Coordinates', {
'fields': ('latitude', 'longitude')
}),
('Address', {
'fields': ('street_address', 'city', 'state', 'country', 'postal_code')
}),
('Content Type (Read Only)', {
'fields': ('content_type', 'object_id'),
'classes': ('collapse',)
}),
('Metadata', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
})
(
"⚠️ DEPRECATED MODEL",
{
"description": "This model is deprecated. Use domain-specific location models instead.",
"fields": (),
},
),
("Basic Information", {"fields": ("name", "location_type")}),
("Geographic Coordinates", {"fields": ("latitude", "longitude")}),
(
"Address",
{
"fields": (
"street_address",
"city",
"state",
"country",
"postal_code",
)
},
),
(
"Content Type (Read Only)",
{
"fields": ("content_type", "object_id"),
"classes": ("collapse",),
},
),
(
"Metadata",
{"fields": ("created_at", "updated_at"), "classes": ("collapse",)},
),
)
def get_queryset(self, request):
return super().get_queryset(request).select_related('content_type')
return super().get_queryset(request).select_related("content_type")
def has_add_permission(self, request):
# Prevent creating new generic Location objects
return False

View File

@@ -1,7 +1,8 @@
from django.apps import AppConfig
import os
class LocationConfig(AppConfig):
path = os.path.dirname(os.path.abspath(__file__))
default_auto_field = 'django.db.models.BigAutoField'
name = 'location'
default_auto_field = "django.db.models.BigAutoField"
name = "location"

View File

@@ -13,28 +13,30 @@ from .models import Location
# NOTE: All classes below are DEPRECATED
# Use domain-specific location forms instead
class LocationForm(forms.ModelForm):
"""DEPRECATED: Use domain-specific location forms instead"""
class Meta:
model = Location
fields = [
'name',
'location_type',
'latitude',
'longitude',
'street_address',
'city',
'state',
'country',
'postal_code',
"name",
"location_type",
"latitude",
"longitude",
"street_address",
"city",
"state",
"country",
"postal_code",
]
class LocationSearchForm(forms.Form):
"""DEPRECATED: Location search functionality has been moved to parks app"""
query = forms.CharField(
max_length=255,
required=True,
help_text="This form is deprecated. Use location search in the parks app."
help_text="This form is deprecated. Use location search in the parks app.",
)

View File

@@ -86,7 +86,10 @@ class Migration(migrations.Migration):
"street_address",
models.CharField(blank=True, max_length=255, null=True),
),
("city", models.CharField(blank=True, max_length=100, null=True)),
(
"city",
models.CharField(blank=True, max_length=100, null=True),
),
(
"state",
models.CharField(
@@ -96,8 +99,14 @@ class Migration(migrations.Migration):
null=True,
),
),
("country", models.CharField(blank=True, max_length=100, null=True)),
("postal_code", models.CharField(blank=True, max_length=20, 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(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
@@ -115,7 +124,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name="LocationEvent",
fields=[
("pgh_id", models.AutoField(primary_key=True, serialize=False)),
(
"pgh_id",
models.AutoField(primary_key=True, serialize=False),
),
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
("pgh_label", models.TextField(help_text="The event label.")),
("id", models.BigIntegerField()),
@@ -175,7 +187,10 @@ class Migration(migrations.Migration):
"street_address",
models.CharField(blank=True, max_length=255, null=True),
),
("city", models.CharField(blank=True, max_length=100, null=True)),
(
"city",
models.CharField(blank=True, max_length=100, null=True),
),
(
"state",
models.CharField(
@@ -185,8 +200,14 @@ class Migration(migrations.Migration):
null=True,
),
),
("country", models.CharField(blank=True, max_length=100, null=True)),
("postal_code", models.CharField(blank=True, max_length=20, 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(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(

View File

@@ -40,7 +40,10 @@ class Migration(migrations.Migration):
constraint=models.CheckConstraint(
condition=models.Q(
models.Q(("latitude__isnull", True), ("longitude__isnull", True)),
models.Q(("latitude__isnull", False), ("longitude__isnull", False)),
models.Q(
("latitude__isnull", False),
("longitude__isnull", False),
),
_connector="OR",
),
name="location_coordinates_complete",

View File

@@ -7,6 +7,7 @@ from django.contrib.gis.geos import Point
import pghistory
from core.history import TrackedModel
@pghistory.track()
class Location(TrackedModel):
"""
@@ -14,84 +15,93 @@ class Location(TrackedModel):
using GenericForeignKey. Stores detailed location information
including coordinates and address components.
"""
# Generic relation fields
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
content_object = GenericForeignKey("content_type", "object_id")
# Location name and type
name = models.CharField(max_length=255, help_text="Name of the location (e.g. business name, landmark)")
location_type = models.CharField(max_length=50, help_text="Type of location (e.g. business, landmark, address)")
name = models.CharField(
max_length=255,
help_text="Name of the location (e.g. business name, landmark)",
)
location_type = models.CharField(
max_length=50,
help_text="Type of location (e.g. business, landmark, address)",
)
# Geographic coordinates
latitude = models.DecimalField(
max_digits=9,
max_digits=9,
decimal_places=6,
validators=[
MinValueValidator(-90),
MaxValueValidator(90)
],
validators=[MinValueValidator(-90), MaxValueValidator(90)],
help_text="Latitude coordinate (legacy field)",
null=True,
blank=True
blank=True,
)
longitude = models.DecimalField(
max_digits=9,
max_digits=9,
decimal_places=6,
validators=[
MinValueValidator(-180),
MaxValueValidator(180)
],
validators=[MinValueValidator(-180), MaxValueValidator(180)],
help_text="Longitude coordinate (legacy field)",
null=True,
blank=True
blank=True,
)
# GeoDjango point field
point = gis_models.PointField(
srid=4326, # WGS84 coordinate system
null=True,
blank=True,
help_text="Geographic coordinates as a Point"
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")
state = models.CharField(
max_length=100,
blank=True,
null=True,
help_text="State/Region/Province",
)
country = models.CharField(max_length=100, blank=True, null=True)
postal_code = models.CharField(max_length=20, blank=True, null=True)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
indexes = [
models.Index(fields=['content_type', 'object_id']),
models.Index(fields=['city']),
models.Index(fields=['country']),
models.Index(fields=["content_type", "object_id"]),
models.Index(fields=["city"]),
models.Index(fields=["country"]),
]
ordering = ['name']
ordering = ["name"]
constraints = [
# Business rule: Latitude must be within valid range (-90 to 90)
models.CheckConstraint(
name="location_latitude_range",
check=models.Q(latitude__isnull=True) | (models.Q(latitude__gte=-90) & models.Q(latitude__lte=90)),
violation_error_message="Latitude must be between -90 and 90 degrees"
check=models.Q(latitude__isnull=True)
| (models.Q(latitude__gte=-90) & models.Q(latitude__lte=90)),
violation_error_message="Latitude must be between -90 and 90 degrees",
),
# Business rule: Longitude must be within valid range (-180 to 180)
models.CheckConstraint(
name="location_longitude_range",
check=models.Q(longitude__isnull=True) | (models.Q(longitude__gte=-180) & models.Q(longitude__lte=180)),
violation_error_message="Longitude must be between -180 and 180 degrees"
check=models.Q(longitude__isnull=True)
| (models.Q(longitude__gte=-180) & models.Q(longitude__lte=180)),
violation_error_message="Longitude must be between -180 and 180 degrees",
),
# Business rule: If coordinates are provided, both lat and lng must be present
# Business rule: If coordinates are provided, both lat and lng must
# be present
models.CheckConstraint(
name="location_coordinates_complete",
check=models.Q(latitude__isnull=True, longitude__isnull=True) |
models.Q(latitude__isnull=False, longitude__isnull=False),
violation_error_message="Both latitude and longitude must be provided together"
check=models.Q(latitude__isnull=True, longitude__isnull=True)
| models.Q(latitude__isnull=False, longitude__isnull=False),
violation_error_message="Both latitude and longitude must be provided together",
),
]
@@ -101,7 +111,9 @@ class Location(TrackedModel):
location_parts.append(self.city)
if self.country:
location_parts.append(self.country)
location_str = ", ".join(location_parts) if location_parts else "Unknown location"
location_str = (
", ".join(location_parts) if location_parts else "Unknown location"
)
return f"{self.name} ({location_str})"
def save(self, *args, **kwargs):
@@ -132,7 +144,8 @@ class Location(TrackedModel):
def coordinates(self):
"""Returns coordinates as a tuple"""
if self.point:
return (self.point.y, self.point.x) # Returns (latitude, longitude)
# Returns (latitude, longitude)
return (self.point.y, self.point.x)
elif self.latitude is not None and self.longitude is not None:
return (float(self.latitude), float(self.longitude))
return None
@@ -153,7 +166,10 @@ class Location(TrackedModel):
"""
if not self.point:
return Location.objects.none()
return Location.objects.filter(
point__distance_lte=(self.point, distance_km * 1000) # Convert km to meters
point__distance_lte=(
self.point,
distance_km * 1000,
) # Convert km to meters
).exclude(pk=self.pk)

View File

@@ -1,101 +1,106 @@
from django.test import TestCase
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.contrib.gis.geos import Point
from django.contrib.gis.measure import D
from .models import Location
from parks.models.companies import Operator
from parks.models import Park
from parks.models import Park, Company as Operator
class LocationModelTests(TestCase):
def setUp(self):
# Create test company
self.operator = Operator.objects.create(
name='Test Operator',
website='http://example.com'
name="Test Operator", website="http://example.com"
)
# Create test park
self.park = Park.objects.create(
name='Test Park',
owner=self.operator,
status='OPERATING'
name="Test Park", owner=self.operator, status="OPERATING"
)
# Create test location for company
self.operator_location = Location.objects.create(
content_type=ContentType.objects.get_for_model(Operator),
object_id=self.operator.pk,
name='Test Operator HQ',
location_type='business',
street_address='123 Operator St',
city='Operator City',
state='CS',
country='Test Country',
postal_code='12345',
point=Point(-118.2437, 34.0522) # Los Angeles coordinates
name="Test Operator HQ",
location_type="business",
street_address="123 Operator St",
city="Operator City",
state="CS",
country="Test Country",
postal_code="12345",
point=Point(-118.2437, 34.0522), # Los Angeles coordinates
)
# Create test location for park
self.park_location = Location.objects.create(
content_type=ContentType.objects.get_for_model(Park),
object_id=self.park.pk,
name='Test Park Location',
location_type='park',
street_address='456 Park Ave',
city='Park City',
state='PC',
country='Test Country',
postal_code='67890',
point=Point(-111.8910, 40.7608) # Park City coordinates
name="Test Park Location",
location_type="park",
street_address="456 Park Ave",
city="Park City",
state="PC",
country="Test Country",
postal_code="67890",
point=Point(-111.8910, 40.7608), # Park City coordinates
)
def test_location_creation(self):
"""Test location instance creation and field values"""
# Test company location
self.assertEqual(self.operator_location.name, 'Test Operator HQ')
self.assertEqual(self.operator_location.location_type, 'business')
self.assertEqual(self.operator_location.street_address, '123 Operator St')
self.assertEqual(self.operator_location.city, 'Operator City')
self.assertEqual(self.operator_location.state, 'CS')
self.assertEqual(self.operator_location.country, 'Test Country')
self.assertEqual(self.operator_location.postal_code, '12345')
self.assertEqual(self.operator_location.name, "Test Operator HQ")
self.assertEqual(self.operator_location.location_type, "business")
self.assertEqual(self.operator_location.street_address, "123 Operator St")
self.assertEqual(self.operator_location.city, "Operator City")
self.assertEqual(self.operator_location.state, "CS")
self.assertEqual(self.operator_location.country, "Test Country")
self.assertEqual(self.operator_location.postal_code, "12345")
self.assertIsNotNone(self.operator_location.point)
# Test park location
self.assertEqual(self.park_location.name, 'Test Park Location')
self.assertEqual(self.park_location.location_type, 'park')
self.assertEqual(self.park_location.street_address, '456 Park Ave')
self.assertEqual(self.park_location.city, 'Park City')
self.assertEqual(self.park_location.state, 'PC')
self.assertEqual(self.park_location.country, 'Test Country')
self.assertEqual(self.park_location.postal_code, '67890')
self.assertEqual(self.park_location.name, "Test Park Location")
self.assertEqual(self.park_location.location_type, "park")
self.assertEqual(self.park_location.street_address, "456 Park Ave")
self.assertEqual(self.park_location.city, "Park City")
self.assertEqual(self.park_location.state, "PC")
self.assertEqual(self.park_location.country, "Test Country")
self.assertEqual(self.park_location.postal_code, "67890")
self.assertIsNotNone(self.park_location.point)
def test_location_str_representation(self):
"""Test string representation of location"""
expected_company_str = 'Test Operator HQ (Operator City, Test Country)'
expected_company_str = "Test Operator HQ (Operator City, Test Country)"
self.assertEqual(str(self.operator_location), expected_company_str)
expected_park_str = 'Test Park Location (Park City, Test Country)'
expected_park_str = "Test Park Location (Park City, Test Country)"
self.assertEqual(str(self.park_location), expected_park_str)
def test_get_formatted_address(self):
"""Test get_formatted_address method"""
expected_address = '123 Operator St, Operator City, CS, 12345, Test Country'
self.assertEqual(self.operator_location.get_formatted_address(), expected_address)
expected_address = "123 Operator St, Operator City, CS, 12345, Test Country"
self.assertEqual(
self.operator_location.get_formatted_address(), expected_address
)
def test_point_coordinates(self):
"""Test point coordinates"""
# Test company location point
self.assertIsNotNone(self.operator_location.point)
self.assertAlmostEqual(self.operator_location.point.y, 34.0522, places=4) # latitude
self.assertAlmostEqual(self.operator_location.point.x, -118.2437, places=4) # longitude
self.assertAlmostEqual(
self.operator_location.point.y, 34.0522, places=4
) # latitude
self.assertAlmostEqual(
self.operator_location.point.x, -118.2437, places=4
) # longitude
# Test park location point
self.assertIsNotNone(self.park_location.point)
self.assertAlmostEqual(self.park_location.point.y, 40.7608, places=4) # latitude
self.assertAlmostEqual(self.park_location.point.x, -111.8910, places=4) # longitude
self.assertAlmostEqual(
self.park_location.point.y, 40.7608, places=4
) # latitude
self.assertAlmostEqual(
self.park_location.point.x, -111.8910, places=4
) # longitude
def test_coordinates_property(self):
"""Test coordinates property"""
@@ -103,7 +108,7 @@ class LocationModelTests(TestCase):
self.assertIsNotNone(company_coords)
self.assertAlmostEqual(company_coords[0], 34.0522, places=4) # latitude
self.assertAlmostEqual(company_coords[1], -118.2437, places=4) # longitude
park_coords = self.park_location.coordinates
self.assertIsNotNone(park_coords)
self.assertAlmostEqual(park_coords[0], 40.7608, places=4) # latitude
@@ -121,14 +126,14 @@ class LocationModelTests(TestCase):
nearby_location = Location.objects.create(
content_type=ContentType.objects.get_for_model(Operator),
object_id=self.operator.pk,
name='Nearby Location',
location_type='business',
street_address='789 Nearby St',
city='Operator City',
country='Test Country',
point=Point(-118.2438, 34.0523) # Very close to company location
name="Nearby Location",
location_type="business",
street_address="789 Nearby St",
city="Operator City",
country="Test Country",
point=Point(-118.2438, 34.0523), # Very close to company location
)
nearby = self.operator_location.nearby_locations(distance_km=1)
self.assertEqual(nearby.count(), 1)
self.assertEqual(nearby.first(), nearby_location)
@@ -138,39 +143,39 @@ class LocationModelTests(TestCase):
# Test company location relation
company_location = Location.objects.get(
content_type=ContentType.objects.get_for_model(Operator),
object_id=self.operator.pk
object_id=self.operator.pk,
)
self.assertEqual(company_location, self.operator_location)
# Test park location relation
park_location = Location.objects.get(
content_type=ContentType.objects.get_for_model(Park),
object_id=self.park.pk
object_id=self.park.pk,
)
self.assertEqual(park_location, self.park_location)
def test_location_updates(self):
"""Test location updates"""
# Update company location
self.operator_location.street_address = 'Updated Address'
self.operator_location.city = 'Updated City'
self.operator_location.street_address = "Updated Address"
self.operator_location.city = "Updated City"
self.operator_location.save()
updated_location = Location.objects.get(pk=self.operator_location.pk)
self.assertEqual(updated_location.street_address, 'Updated Address')
self.assertEqual(updated_location.city, 'Updated City')
self.assertEqual(updated_location.street_address, "Updated Address")
self.assertEqual(updated_location.city, "Updated City")
def test_point_sync_with_lat_lon(self):
"""Test point synchronization with latitude/longitude fields"""
location = Location.objects.create(
content_type=ContentType.objects.get_for_model(Operator),
object_id=self.operator.pk,
name='Test Sync Location',
location_type='business',
name="Test Sync Location",
location_type="business",
latitude=34.0522,
longitude=-118.2437
longitude=-118.2437,
)
self.assertIsNotNone(location.point)
self.assertAlmostEqual(location.point.y, 34.0522, places=4)
self.assertAlmostEqual(location.point.x, -118.2437, places=4)

View File

@@ -6,7 +6,7 @@
#
# Domain-specific location models are managed through their respective apps:
# - Parks app for ParkLocation
# - Rides app for RideLocation
# - Rides app for RideLocation
# - Parks app for CompanyHeadquarters
#
# This file is kept for reference during migration cleanup only.
@@ -14,20 +14,18 @@
from django.urls import path
from . import views
app_name = 'location'
app_name = "location"
# NOTE: All URLs below are DEPRECATED
# The location app URLs should not be included in the main URLconf
urlpatterns = [
# DEPRECATED: Use /parks/search/location/ instead
path('search/', views.LocationSearchView.as_view(), name='search'),
# DEPRECATED: Use /parks/search/reverse-geocode/ instead
path('reverse-geocode/', views.reverse_geocode, name='reverse_geocode'),
path("search/", views.LocationSearchView.as_view(), name="search"),
# DEPRECATED: Use /parks/search/reverse-geocode/ instead
path("reverse-geocode/", views.reverse_geocode, name="reverse_geocode"),
# DEPRECATED: Use domain-specific location models instead
path('create/', views.LocationCreateView.as_view(), name='create'),
path('<int:pk>/update/', views.LocationUpdateView.as_view(), name='update'),
path('<int:pk>/delete/', views.LocationDeleteView.as_view(), name='delete'),
path("create/", views.LocationCreateView.as_view(), name="create"),
path("<int:pk>/update/", views.LocationUpdateView.as_view(), name="update"),
path("<int:pk>/delete/", views.LocationDeleteView.as_view(), name="delete"),
]

View File

@@ -1,51 +1,48 @@
# DEPRECATED: These views are deprecated and no longer used.
#
#
# Location search functionality has been moved to the parks app:
# - parks.views.location_search
# - parks.views.reverse_geocode
#
# Domain-specific location models are now used instead of the generic Location model:
# - ParkLocation in parks.models.location
# - RideLocation in rides.models.location
# - RideLocation in rides.models.location
# - CompanyHeadquarters in parks.models.companies
#
# This file is kept for reference during migration cleanup only.
import json
import requests
from django.views.generic import View
from django.http import JsonResponse
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.cache import cache
from django.conf import settings
from django.views.decorators.http import require_http_methods
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_protect
from django.db.models import Q
from location.forms import LocationForm
from .models import Location
# NOTE: All classes and functions below are DEPRECATED
# Use the equivalent functionality in the parks app instead
class LocationSearchView(View):
"""DEPRECATED: Use parks.views.location_search instead"""
pass
class LocationCreateView(LoginRequiredMixin, View):
"""DEPRECATED: Use domain-specific location models instead"""
pass
class LocationUpdateView(LoginRequiredMixin, View):
"""DEPRECATED: Use domain-specific location models instead"""
pass
class LocationDeleteView(LoginRequiredMixin, View):
"""DEPRECATED: Use domain-specific location models instead"""
pass
@require_http_methods(["GET"])
def reverse_geocode(request):
"""DEPRECATED: Use parks.views.reverse_geocode instead"""
return JsonResponse({'error': 'This endpoint is deprecated. Use /parks/search/reverse-geocode/ instead'}, status=410)
return JsonResponse(
{
"error": "This endpoint is deprecated. Use /parks/search/reverse-geocode/ instead"
},
status=410,
)