1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-24 23:39:37 +02:00

feat: add City model and serializer, update RegionCard and create CityCard component, enhance admin interface for City management

This commit is contained in:
Sean Morley 2025-01-09 11:11:02 -05:00
parent a883d4104d
commit 44810e6343
14 changed files with 409 additions and 18 deletions

View file

@ -2,7 +2,7 @@ import os
from django.contrib import admin
from django.utils.html import mark_safe
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category
from worldtravel.models import Country, Region, VisitedRegion
from worldtravel.models import Country, Region, VisitedRegion, City
from allauth.account.decorators import secure_admin_login
admin.autodiscover()
@ -51,6 +51,16 @@ class RegionAdmin(admin.ModelAdmin):
number_of_visits.short_description = 'Number of Visits'
class CityAdmin(admin.ModelAdmin):
list_display = ('name', 'region', 'country')
list_filter = ('region', 'region__country')
search_fields = ('name', 'region__name', 'region__country__name')
def country(self, obj):
return obj.region.country.name
country.short_description = 'Country'
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from users.models import CustomUser
@ -127,6 +137,7 @@ admin.site.register(Checklist)
admin.site.register(ChecklistItem)
admin.site.register(AdventureImage, AdventureImageAdmin)
admin.site.register(Category, CategoryAdmin)
admin.site.register(City, CityAdmin)
admin.site.site_header = 'AdventureLog Admin'
admin.site.site_title = 'AdventureLog Admin Site'

View file

@ -276,4 +276,4 @@ LOGGING = {
},
}
# https://github.com/dr5hn/countries-states-cities-database/tags
COUNTRY_REGION_JSON_VERSION = 'v2.4'
COUNTRY_REGION_JSON_VERSION = 'v2.5'

View file

@ -1,7 +1,7 @@
import os
from django.core.management.base import BaseCommand
import requests
from worldtravel.models import Country, Region
from worldtravel.models import Country, Region, City
from django.db import transaction
import json
@ -37,16 +37,28 @@ def saveCountryFlag(country_code):
class Command(BaseCommand):
help = 'Imports the world travel data'
def add_arguments(self, parser):
parser.add_argument('--force', action='store_true', help='Force download the countries+regions+states.json file')
def handle(self, *args, **options):
countries_json_path = os.path.join(settings.MEDIA_ROOT, f'countries+regions-{COUNTRY_REGION_JSON_VERSION}.json')
if not os.path.exists(countries_json_path):
res = requests.get(f'https://raw.githubusercontent.com/dr5hn/countries-states-cities-database/{COUNTRY_REGION_JSON_VERSION}/countries%2Bstates.json')
force = options['force']
countries_json_path = os.path.join(settings.MEDIA_ROOT, f'countries+regions+states-{COUNTRY_REGION_JSON_VERSION}.json')
if not os.path.exists(countries_json_path) or force:
res = requests.get(f'https://raw.githubusercontent.com/dr5hn/countries-states-cities-database/{COUNTRY_REGION_JSON_VERSION}/json/countries%2Bstates%2Bcities.json')
if res.status_code == 200:
with open(countries_json_path, 'w') as f:
f.write(res.text)
else:
self.stdout.write(self.style.ERROR('Error downloading countries+regions.json'))
self.stdout.write(self.style.ERROR('Error downloading countries+regions+states.json'))
return
elif not os.path.isfile(countries_json_path):
self.stdout.write(self.style.ERROR('countries+regions+states.json is not a file'))
return
elif os.path.getsize(countries_json_path) == 0:
self.stdout.write(self.style.ERROR('countries+regions+states.json is empty'))
else:
self.stdout.write(self.style.SUCCESS('countries+regions+states.json already exists'))
return
with open(countries_json_path, 'r') as f:
data = json.load(f)
@ -54,14 +66,18 @@ class Command(BaseCommand):
with transaction.atomic():
existing_countries = {country.country_code: country for country in Country.objects.all()}
existing_regions = {region.id: region for region in Region.objects.all()}
existing_cities = {city.id: city for city in City.objects.all()}
countries_to_create = []
regions_to_create = []
countries_to_update = []
regions_to_update = []
cities_to_create = []
cities_to_update = []
processed_country_codes = set()
processed_region_ids = set()
processed_city_ids = set()
for country in data:
country_code = country['iso2']
@ -102,6 +118,11 @@ class Command(BaseCommand):
latitude = round(float(state['latitude']), 6) if state['latitude'] else None
longitude = round(float(state['longitude']), 6) if state['longitude'] else None
# Check for duplicate regions
if state_id in processed_region_ids:
self.stdout.write(self.style.ERROR(f'State {state_id} already processed'))
continue
processed_region_ids.add(state_id)
if state_id in existing_regions:
@ -121,6 +142,39 @@ class Command(BaseCommand):
)
regions_to_create.append(region_obj)
self.stdout.write(self.style.SUCCESS(f'State {state_id} prepared'))
if 'cities' in state and len(state['cities']) > 0:
for city in state['cities']:
city_id = f"{state_id}-{city['id']}"
city_name = city['name']
latitude = round(float(city['latitude']), 6) if city['latitude'] else None
longitude = round(float(city['longitude']), 6) if city['longitude'] else None
# Check for duplicate cities
if city_id in processed_city_ids:
self.stdout.write(self.style.ERROR(f'City {city_id} already processed'))
continue
processed_city_ids.add(city_id)
if city_id in existing_cities:
city_obj = existing_cities[city_id]
city_obj.name = city_name
city_obj.region = region_obj
city_obj.longitude = longitude
city_obj.latitude = latitude
cities_to_update.append(city_obj)
else:
city_obj = City(
id=city_id,
name=city_name,
region=region_obj,
longitude=longitude,
latitude=latitude
)
cities_to_create.append(city_obj)
self.stdout.write(self.style.SUCCESS(f'City {city_id} prepared'))
else:
state_id = f"{country_code}-00"
processed_region_ids.add(state_id)
@ -141,13 +195,16 @@ class Command(BaseCommand):
# Bulk create new countries and regions
Country.objects.bulk_create(countries_to_create)
Region.objects.bulk_create(regions_to_create)
City.objects.bulk_create(cities_to_create)
# Bulk update existing countries and regions
Country.objects.bulk_update(countries_to_update, ['name', 'subregion', 'capital'])
Region.objects.bulk_update(regions_to_update, ['name', 'country', 'longitude', 'latitude'])
City.objects.bulk_update(cities_to_update, ['name', 'region', 'longitude', 'latitude'])
# Delete countries and regions that are no longer in the data
Country.objects.exclude(country_code__in=processed_country_codes).delete()
Region.objects.exclude(id__in=processed_region_ids).delete()
City.objects.exclude(id__in=processed_city_ids).delete()
self.stdout.write(self.style.SUCCESS('All data imported successfully'))

View file

@ -0,0 +1,27 @@
# Generated by Django 5.0.8 on 2025-01-09 15:11
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('worldtravel', '0011_country_latitude_country_longitude'),
]
operations = [
migrations.CreateModel(
name='City',
fields=[
('id', models.CharField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('region', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='worldtravel.region')),
],
options={
'verbose_name_plural': 'Cities',
},
),
]

View file

@ -34,6 +34,19 @@ class Region(models.Model):
def __str__(self):
return self.name
class City(models.Model):
id = models.CharField(primary_key=True)
name = models.CharField(max_length=100)
region = models.ForeignKey(Region, on_delete=models.CASCADE)
longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
class Meta:
verbose_name_plural = "Cities"
def __str__(self):
return self.name
class VisitedRegion(models.Model):
id = models.AutoField(primary_key=True)

View file

@ -1,5 +1,5 @@
import os
from .models import Country, Region, VisitedRegion
from .models import Country, Region, VisitedRegion, City
from rest_framework import serializers
from main.utils import CustomModelSerializer
@ -38,6 +38,12 @@ class RegionSerializer(serializers.ModelSerializer):
fields = '__all__'
read_only_fields = ['id', 'name', 'country', 'longitude', 'latitude']
class CitySerializer(serializers.ModelSerializer):
class Meta:
model = City
fields = '__all__'
read_only_fields = ['id', 'name', 'region', 'longitude', 'latitude']
class VisitedRegionSerializer(CustomModelSerializer):
longitude = serializers.DecimalField(source='region.longitude', max_digits=9, decimal_places=6, read_only=True)
latitude = serializers.DecimalField(source='region.latitude', max_digits=9, decimal_places=6, read_only=True)

View file

@ -2,8 +2,7 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import CountryViewSet, RegionViewSet, VisitedRegionViewSet, regions_by_country, visits_by_country
from .views import CountryViewSet, RegionViewSet, VisitedRegionViewSet, regions_by_country, visits_by_country, cities_by_region
router = DefaultRouter()
router.register(r'countries', CountryViewSet, basename='countries')
router.register(r'regions', RegionViewSet, basename='regions')
@ -12,5 +11,6 @@ router.register(r'visitedregion', VisitedRegionViewSet, basename='visitedregion'
urlpatterns = [
path('', include(router.urls)),
path('<str:country_code>/regions/', regions_by_country, name='regions-by-country'),
path('<str:country_code>/visits/', visits_by_country, name='visits-by-country')
path('<str:country_code>/visits/', visits_by_country, name='visits-by-country'),
path('regions/<str:region_id>/cities/', cities_by_region, name='cities-by-region'),
]

View file

@ -1,6 +1,6 @@
from django.shortcuts import render
from .models import Country, Region, VisitedRegion
from .serializers import CountrySerializer, RegionSerializer, VisitedRegionSerializer
from .models import Country, Region, VisitedRegion, City
from .serializers import CitySerializer, CountrySerializer, RegionSerializer, VisitedRegionSerializer
from rest_framework import viewsets, status
from rest_framework.permissions import IsAuthenticated
from django.shortcuts import get_object_or_404
@ -33,6 +33,14 @@ def visits_by_country(request, country_code):
serializer = VisitedRegionSerializer(visits, many=True)
return Response(serializer.data)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def cities_by_region(request, region_id):
region = get_object_or_404(Region, id=region_id)
cities = City.objects.filter(region=region).order_by('name')
serializer = CitySerializer(cities, many=True)
return Response(serializer.data)
class CountryViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Country.objects.all().order_by('name')
serializer_class = CountrySerializer