From 6e9724979a957a99087a97b3e15b91d4b334c658 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Tue, 10 Sep 2024 23:00:13 -0400 Subject: [PATCH 1/9] Region dataset --- backend/server/adventures/admin.py | 4 +- .../management/commands/download-countries.py | 115 ++++++++++++++++++ ...ove_country_continent_country_subregion.py | 22 ++++ ...e_region_geometry_remove_region_name_en.py | 21 ++++ .../0008_region_latitude_region_longitude.py | 23 ++++ .../0009_alter_country_country_code.py | 18 +++ backend/server/worldtravel/models.py | 30 +---- 7 files changed, 205 insertions(+), 28 deletions(-) create mode 100644 backend/server/worldtravel/management/commands/download-countries.py create mode 100644 backend/server/worldtravel/migrations/0006_remove_country_continent_country_subregion.py create mode 100644 backend/server/worldtravel/migrations/0007_remove_region_geometry_remove_region_name_en.py create mode 100644 backend/server/worldtravel/migrations/0008_region_latitude_region_longitude.py create mode 100644 backend/server/worldtravel/migrations/0009_alter_country_country_code.py diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py index ab8bfc6..226074d 100644 --- a/backend/server/adventures/admin.py +++ b/backend/server/adventures/admin.py @@ -21,8 +21,8 @@ class AdventureAdmin(admin.ModelAdmin): class CountryAdmin(admin.ModelAdmin): - list_display = ('name', 'country_code', 'continent', 'number_of_regions') - list_filter = ('continent', 'country_code') + list_display = ('name', 'country_code', 'number_of_regions') + list_filter = ('country_code', 'subregion') def number_of_regions(self, obj): return Region.objects.filter(country=obj).count() diff --git a/backend/server/worldtravel/management/commands/download-countries.py b/backend/server/worldtravel/management/commands/download-countries.py new file mode 100644 index 0000000..411f55b --- /dev/null +++ b/backend/server/worldtravel/management/commands/download-countries.py @@ -0,0 +1,115 @@ +import os +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model +import requests +from worldtravel.models import Country, Region +from django.db import transaction +from django.contrib.gis.geos import GEOSGeometry, Polygon, MultiPolygon +from django.contrib.gis.geos.error import GEOSException +import json + +from django.conf import settings + +media_root = settings.MEDIA_ROOT + + +def saveCountryFlag(country_code): + flags_dir = os.path.join(media_root, 'flags') + + # Check if the flags directory exists, if not, create it + if not os.path.exists(flags_dir): + os.makedirs(flags_dir) + + # Check if the flag already exists in the media folder + flag_path = os.path.join(flags_dir, f'{country_code}.png') + if os.path.exists(flag_path): + print(f'Flag for {country_code} already exists') + return + + res = requests.get(f'https://flagcdn.com/h240/{country_code.lower()}.png'.lower()) + if res.status_code == 200: + with open(flag_path, 'wb') as f: + f.write(res.content) + print(f'Flag for {country_code} downloaded') + else: + print(f'Error downloading flag for {country_code}') + +class Command(BaseCommand): + help = 'Imports the world travel data' + + def handle(self, *args, **options): + countries_json_path = os.path.join(settings.MEDIA_ROOT, 'countries+regions.json') + if not os.path.exists(countries_json_path): + res = requests.get('https://raw.githubusercontent.com/dr5hn/countries-states-cities-database/master/countries%2Bstates.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')) + return + + with open(countries_json_path, 'r') as f: + data = json.load(f) + + countries_to_create = [] + regions_to_create = [] + + for country in data: + country_code = country['iso2'] + country_name = country['name'] + country_subregion = country['subregion'] + + country_obj = Country( + name=country_name, + country_code=country_code, + subregion=country_subregion + ) + countries_to_create.append(country_obj) + + saveCountryFlag(country_code) + self.stdout.write(self.style.SUCCESS(f'Country {country_name} prepared')) + + # Bulk create countries first + with transaction.atomic(): + Country.objects.bulk_create(countries_to_create, ignore_conflicts=True) + + # Fetch all countries to get their database IDs + countries = {country.country_code: country for country in Country.objects.all()} + + for country in data: + country_code = country['iso2'] + country_obj = countries[country_code] + + if country['states']: + for state in country['states']: + name = state['name'] + state_id = f"{country_code}-{state['state_code']}" + latitude = round(float(state['latitude']), 6) if state['latitude'] else None + longitude = round(float(state['longitude']), 6) if state['longitude'] else None + + region_obj = Region( + id=state_id, + name=name, + country=country_obj, + longitude=longitude, + latitude=latitude + ) + regions_to_create.append(region_obj) + self.stdout.write(self.style.SUCCESS(f'State {state_id} prepared')) + else: + # Create one region with the name of the country if there are no states + region_obj = Region( + id=f"{country_code}-00", + name=country['name'], + country=country_obj + ) + regions_to_create.append(region_obj) + self.stdout.write(self.style.SUCCESS(f'Region {country_code}-00 prepared for {country["name"]}')) + + # Bulk create regions + with transaction.atomic(): + Region.objects.bulk_create(regions_to_create, ignore_conflicts=True) + + self.stdout.write(self.style.SUCCESS('All data imported successfully')) + + diff --git a/backend/server/worldtravel/migrations/0006_remove_country_continent_country_subregion.py b/backend/server/worldtravel/migrations/0006_remove_country_continent_country_subregion.py new file mode 100644 index 0000000..cefae81 --- /dev/null +++ b/backend/server/worldtravel/migrations/0006_remove_country_continent_country_subregion.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.8 on 2024-09-11 02:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('worldtravel', '0005_remove_country_geometry_region_geometry'), + ] + + operations = [ + migrations.RemoveField( + model_name='country', + name='continent', + ), + migrations.AddField( + model_name='country', + name='subregion', + field=models.CharField(blank=True, max_length=100, null=True), + ), + ] diff --git a/backend/server/worldtravel/migrations/0007_remove_region_geometry_remove_region_name_en.py b/backend/server/worldtravel/migrations/0007_remove_region_geometry_remove_region_name_en.py new file mode 100644 index 0000000..742221d --- /dev/null +++ b/backend/server/worldtravel/migrations/0007_remove_region_geometry_remove_region_name_en.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0.8 on 2024-09-11 02:20 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('worldtravel', '0006_remove_country_continent_country_subregion'), + ] + + operations = [ + migrations.RemoveField( + model_name='region', + name='geometry', + ), + migrations.RemoveField( + model_name='region', + name='name_en', + ), + ] diff --git a/backend/server/worldtravel/migrations/0008_region_latitude_region_longitude.py b/backend/server/worldtravel/migrations/0008_region_latitude_region_longitude.py new file mode 100644 index 0000000..bf6d06d --- /dev/null +++ b/backend/server/worldtravel/migrations/0008_region_latitude_region_longitude.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.8 on 2024-09-11 02:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('worldtravel', '0007_remove_region_geometry_remove_region_name_en'), + ] + + operations = [ + migrations.AddField( + model_name='region', + name='latitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), + ), + migrations.AddField( + model_name='region', + name='longitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), + ), + ] diff --git a/backend/server/worldtravel/migrations/0009_alter_country_country_code.py b/backend/server/worldtravel/migrations/0009_alter_country_country_code.py new file mode 100644 index 0000000..b09300d --- /dev/null +++ b/backend/server/worldtravel/migrations/0009_alter_country_country_code.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.8 on 2024-09-11 02:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('worldtravel', '0008_region_latitude_region_longitude'), + ] + + operations = [ + migrations.AlterField( + model_name='country', + name='country_code', + field=models.CharField(max_length=2, unique=True), + ), + ] diff --git a/backend/server/worldtravel/models.py b/backend/server/worldtravel/models.py index de0d87f..d54fe95 100644 --- a/backend/server/worldtravel/models.py +++ b/backend/server/worldtravel/models.py @@ -9,33 +9,11 @@ User = get_user_model() default_user_id = 1 # Replace with an actual user ID class Country(models.Model): - AFRICA = 'AF' - ANTARCTICA = 'AN' - ASIA = 'AS' - EUROPE = 'EU' - NORTH_AMERICA = 'NA' - OCEANIA = 'OC' - SOUTH_AMERICA = 'SA' - - CONTINENT_CHOICES = [ - (AFRICA, 'Africa'), - (ANTARCTICA, 'Antarctica'), - (ASIA, 'Asia'), - (EUROPE, 'Europe'), - (NORTH_AMERICA, 'North America'), - (OCEANIA, 'Oceania'), - (SOUTH_AMERICA, 'South America'), - ] id = models.AutoField(primary_key=True) name = models.CharField(max_length=100) - country_code = models.CharField(max_length=2) - continent = models.CharField( - max_length=2, - choices=CONTINENT_CHOICES, - default=AFRICA - ) - + country_code = models.CharField(max_length=2, unique=True) #iso2 code + subregion = models.CharField(max_length=100, blank=True, null=True) class Meta: verbose_name = "Country" @@ -47,9 +25,9 @@ class Country(models.Model): class Region(models.Model): id = models.CharField(primary_key=True) name = models.CharField(max_length=100) - name_en = models.CharField(max_length=100, blank=True, null=True) country = models.ForeignKey(Country, on_delete=models.CASCADE) - geometry = gis_models.MultiPolygonField(srid=4326, null=True, blank=True) + 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) def __str__(self): return self.name From 9ba5b25ab54df8df4cbb5106ba888a53c21b6b41 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Tue, 10 Sep 2024 23:12:01 -0400 Subject: [PATCH 2/9] Update existing regions --- .../management/commands/download-countries.py | 137 +++++++++++------- backend/server/worldtravel/models.py | 2 +- 2 files changed, 84 insertions(+), 55 deletions(-) diff --git a/backend/server/worldtravel/management/commands/download-countries.py b/backend/server/worldtravel/management/commands/download-countries.py index 411f55b..aff0f2e 100644 --- a/backend/server/worldtravel/management/commands/download-countries.py +++ b/backend/server/worldtravel/management/commands/download-countries.py @@ -51,65 +51,94 @@ class Command(BaseCommand): with open(countries_json_path, 'r') as f: data = json.load(f) - countries_to_create = [] - regions_to_create = [] - - for country in data: - country_code = country['iso2'] - country_name = country['name'] - country_subregion = country['subregion'] - - country_obj = Country( - name=country_name, - country_code=country_code, - subregion=country_subregion - ) - countries_to_create.append(country_obj) - - saveCountryFlag(country_code) - self.stdout.write(self.style.SUCCESS(f'Country {country_name} prepared')) - - # Bulk create countries first with transaction.atomic(): - Country.objects.bulk_create(countries_to_create, ignore_conflicts=True) + existing_countries = {country.country_code: country for country in Country.objects.all()} + existing_regions = {region.id: region for region in Region.objects.all()} - # Fetch all countries to get their database IDs - countries = {country.country_code: country for country in Country.objects.all()} + countries_to_create = [] + regions_to_create = [] + countries_to_update = [] + regions_to_update = [] - for country in data: - country_code = country['iso2'] - country_obj = countries[country_code] - - if country['states']: - for state in country['states']: - name = state['name'] - state_id = f"{country_code}-{state['state_code']}" - latitude = round(float(state['latitude']), 6) if state['latitude'] else None - longitude = round(float(state['longitude']), 6) if state['longitude'] else None + processed_country_codes = set() + processed_region_ids = set() - region_obj = Region( - id=state_id, - name=name, - country=country_obj, - longitude=longitude, - latitude=latitude + for country in data: + country_code = country['iso2'] + country_name = country['name'] + country_subregion = country['subregion'] + + processed_country_codes.add(country_code) + + if country_code in existing_countries: + country_obj = existing_countries[country_code] + country_obj.name = country_name + country_obj.subregion = country_subregion + countries_to_update.append(country_obj) + else: + country_obj = Country( + name=country_name, + country_code=country_code, + subregion=country_subregion ) - regions_to_create.append(region_obj) - self.stdout.write(self.style.SUCCESS(f'State {state_id} prepared')) - else: - # Create one region with the name of the country if there are no states - region_obj = Region( - id=f"{country_code}-00", - name=country['name'], - country=country_obj - ) - regions_to_create.append(region_obj) - self.stdout.write(self.style.SUCCESS(f'Region {country_code}-00 prepared for {country["name"]}')) + countries_to_create.append(country_obj) - # Bulk create regions - with transaction.atomic(): - Region.objects.bulk_create(regions_to_create, ignore_conflicts=True) + saveCountryFlag(country_code) + self.stdout.write(self.style.SUCCESS(f'Country {country_name} prepared')) - self.stdout.write(self.style.SUCCESS('All data imported successfully')) + if country['states']: + for state in country['states']: + name = state['name'] + state_id = f"{country_code}-{state['state_code']}" + latitude = round(float(state['latitude']), 6) if state['latitude'] else None + longitude = round(float(state['longitude']), 6) if state['longitude'] else None - + processed_region_ids.add(state_id) + + if state_id in existing_regions: + region_obj = existing_regions[state_id] + region_obj.name = name + region_obj.country = country_obj + region_obj.longitude = longitude + region_obj.latitude = latitude + regions_to_update.append(region_obj) + else: + region_obj = Region( + id=state_id, + name=name, + country=country_obj, + longitude=longitude, + latitude=latitude + ) + regions_to_create.append(region_obj) + self.stdout.write(self.style.SUCCESS(f'State {state_id} prepared')) + else: + state_id = f"{country_code}-00" + processed_region_ids.add(state_id) + if state_id in existing_regions: + region_obj = existing_regions[state_id] + region_obj.name = country_name + region_obj.country = country_obj + regions_to_update.append(region_obj) + else: + region_obj = Region( + id=state_id, + name=country_name, + country=country_obj + ) + regions_to_create.append(region_obj) + self.stdout.write(self.style.SUCCESS(f'Region {state_id} prepared for {country_name}')) + + # Bulk create new countries and regions + Country.objects.bulk_create(countries_to_create) + Region.objects.bulk_create(regions_to_create) + + # Bulk update existing countries and regions + Country.objects.bulk_update(countries_to_update, ['name', 'subregion']) + Region.objects.bulk_update(regions_to_update, ['name', 'country', '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() + + self.stdout.write(self.style.SUCCESS('All data imported successfully')) \ No newline at end of file diff --git a/backend/server/worldtravel/models.py b/backend/server/worldtravel/models.py index d54fe95..66e10d4 100644 --- a/backend/server/worldtravel/models.py +++ b/backend/server/worldtravel/models.py @@ -44,4 +44,4 @@ class VisitedRegion(models.Model): def save(self, *args, **kwargs): if VisitedRegion.objects.filter(user_id=self.user_id, region=self.region).exists(): raise ValidationError("Region already visited by user.") - super().save(*args, **kwargs) + super().save(*args, **kwargs) \ No newline at end of file From bd6d60d24d79aca36b0a0a5e41ad89f893b228e9 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 11 Sep 2024 09:31:25 -0400 Subject: [PATCH 3/9] Update entrypoint --- backend/entrypoint.sh | 2 +- .../worldtravel/management/commands/download-countries.py | 8 +++----- backend/server/worldtravel/serializers.py | 4 ++-- frontend/src/lib/components/CountryCard.svelte | 2 +- frontend/src/lib/types.ts | 5 +++-- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh index 269a5aa..cbb40db 100644 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -34,7 +34,7 @@ EOF fi # Sync the countries and world travel regions -python manage.py worldtravel-seed --force +python manage.py download-countries # Start Django server python manage.py runserver 0.0.0.0:8000 diff --git a/backend/server/worldtravel/management/commands/download-countries.py b/backend/server/worldtravel/management/commands/download-countries.py index aff0f2e..d0929a0 100644 --- a/backend/server/worldtravel/management/commands/download-countries.py +++ b/backend/server/worldtravel/management/commands/download-countries.py @@ -1,19 +1,17 @@ import os from django.core.management.base import BaseCommand -from django.contrib.auth import get_user_model import requests from worldtravel.models import Country, Region from django.db import transaction -from django.contrib.gis.geos import GEOSGeometry, Polygon, MultiPolygon -from django.contrib.gis.geos.error import GEOSException import json from django.conf import settings media_root = settings.MEDIA_ROOT - def saveCountryFlag(country_code): + # For standards, use the lowercase country_code + country_code = country_code.lower() flags_dir = os.path.join(media_root, 'flags') # Check if the flags directory exists, if not, create it @@ -26,7 +24,7 @@ def saveCountryFlag(country_code): print(f'Flag for {country_code} already exists') return - res = requests.get(f'https://flagcdn.com/h240/{country_code.lower()}.png'.lower()) + res = requests.get(f'https://flagcdn.com/h240/{country_code}.png'.lower()) if res.status_code == 200: with open(flag_path, 'wb') as f: f.write(res.content) diff --git a/backend/server/worldtravel/serializers.py b/backend/server/worldtravel/serializers.py index 6cb9c46..4e529c5 100644 --- a/backend/server/worldtravel/serializers.py +++ b/backend/server/worldtravel/serializers.py @@ -16,13 +16,13 @@ class CountrySerializer(serializers.ModelSerializer): class Meta: model = Country fields = '__all__' # Serialize all fields of the Adventure model - read_only_fields = ['id', 'name', 'country_code', 'continent', 'flag_url'] + read_only_fields = ['id', 'name', 'country_code', 'subregion', 'flag_url'] class RegionSerializer(serializers.ModelSerializer): class Meta: model = Region fields = '__all__' # Serialize all fields of the Adventure model - read_only_fields = ['id', 'name', 'country', 'name_en', 'geometry'] + read_only_fields = ['id', 'name', 'country', 'longitude', 'latitude'] class VisitedRegionSerializer(serializers.ModelSerializer): class Meta: diff --git a/frontend/src/lib/components/CountryCard.svelte b/frontend/src/lib/components/CountryCard.svelte index 0fbdace..95fbde8 100644 --- a/frontend/src/lib/components/CountryCard.svelte +++ b/frontend/src/lib/components/CountryCard.svelte @@ -21,7 +21,7 @@

{country.name}

-
{continentCodeToString(country.continent)}
+
{country.subregion}
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 5e5060a..8fade57 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -39,15 +39,16 @@ export type Country = { id: number; name: string; country_code: string; - continent: string; + subregion: string; flag_url: string; }; export type Region = { id: number; name: string; - name_en?: string; country: number; + latitude: number; + longitude: number; }; export type VisitedRegion = { From 2f012321443a40a4cbf33a9d037da3a99a56e324 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 11 Sep 2024 10:01:36 -0400 Subject: [PATCH 4/9] Lowercase flag urls --- backend/server/worldtravel/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/server/worldtravel/serializers.py b/backend/server/worldtravel/serializers.py index 4e529c5..ab312df 100644 --- a/backend/server/worldtravel/serializers.py +++ b/backend/server/worldtravel/serializers.py @@ -11,7 +11,7 @@ class CountrySerializer(serializers.ModelSerializer): def get_flag_url(self, obj): public_url = self.get_public_url(obj) - return public_url + '/media/' + 'flags/' + obj.country_code + '.png' + return public_url + '/media/' + 'flags/' + obj.country_code.lower() + '.png' class Meta: model = Country From 4dc11db21d922f7a1be26c1f9b5e759d072be296 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 11 Sep 2024 14:56:52 -0400 Subject: [PATCH 5/9] Add region markers to the map --- backend/server/worldtravel/serializers.py | 13 ++++-- frontend/src/routes/map/+page.svelte | 57 ++++++++++++----------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/backend/server/worldtravel/serializers.py b/backend/server/worldtravel/serializers.py index ab312df..b6be995 100644 --- a/backend/server/worldtravel/serializers.py +++ b/backend/server/worldtravel/serializers.py @@ -2,7 +2,6 @@ import os from .models import Country, Region, VisitedRegion from rest_framework import serializers - class CountrySerializer(serializers.ModelSerializer): def get_public_url(self, obj): return os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/').replace("'", "") @@ -15,17 +14,21 @@ class CountrySerializer(serializers.ModelSerializer): class Meta: model = Country - fields = '__all__' # Serialize all fields of the Adventure model + fields = '__all__' read_only_fields = ['id', 'name', 'country_code', 'subregion', 'flag_url'] class RegionSerializer(serializers.ModelSerializer): class Meta: model = Region - fields = '__all__' # Serialize all fields of the Adventure model + fields = '__all__' read_only_fields = ['id', 'name', 'country', 'longitude', 'latitude'] class VisitedRegionSerializer(serializers.ModelSerializer): + 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) + name = serializers.CharField(source='region.name', read_only=True) + class Meta: model = VisitedRegion - fields = '__all__' # Serialize all fields of the Adventure model - read_only_fields = ['user_id', 'id'] \ No newline at end of file + fields = ['id', 'user_id', 'region', 'longitude', 'latitude', 'name'] + read_only_fields = ['user_id', 'id', 'longitude', 'latitude', 'name'] \ No newline at end of file diff --git a/frontend/src/routes/map/+page.svelte b/frontend/src/routes/map/+page.svelte index a157198..5c10b8e 100644 --- a/frontend/src/routes/map/+page.svelte +++ b/frontend/src/routes/map/+page.svelte @@ -54,7 +54,7 @@ } let visitedRegions = data.props.visitedRegions; - let geoJSON = []; + let allRegions = []; let visitArray = []; @@ -72,12 +72,12 @@ // mapped to the checkbox let showGEO = false; $: { - if (showGEO && geoJSON.length === 0) { + if (showGEO && allRegions.length === 0) { (async () => { - geoJSON = await fetch('/api/geojson/').then((res) => res.json()); + allRegions = await fetch('/api/visitedregion/').then((res) => res.json()); })(); } else if (!showGEO) { - geoJSON = []; + allRegions = []; } } @@ -99,7 +99,7 @@ - + {/if} {/each} - {#if showGEO} - - - - - - {/if} + {#each newMarker as marker} {/each} + + {#each allRegions as { longitude, latitude, name, region }} + (clickedName = name)} + class="grid h-8 w-8 place-items-center rounded-full border border-gray-200 bg-green-300 text-black shadow-md" + > + + + + + +
{name}
+

{region}

+
+
+ {/each} From 6ac3f0541f9ea3ec1072358aa594843c049f2500 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 11 Sep 2024 16:08:10 -0400 Subject: [PATCH 6/9] Add capital to countries --- backend/server/main/settings.py | 4 +++- .../management/commands/download-countries.py | 11 ++++++++--- .../migrations/0010_country_capital.py | 18 ++++++++++++++++++ backend/server/worldtravel/models.py | 1 + frontend/src/lib/components/CountryCard.svelte | 9 ++++++++- frontend/src/lib/types.ts | 1 + 6 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 backend/server/worldtravel/migrations/0010_country_capital.py diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index a885736..dab34e7 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -260,4 +260,6 @@ LOGGING = { 'propagate': False, }, }, -} \ No newline at end of file +} + +COUNTRY_REGION_JSON_VERSION = 'v2.4' \ No newline at end of file diff --git a/backend/server/worldtravel/management/commands/download-countries.py b/backend/server/worldtravel/management/commands/download-countries.py index d0929a0..48a6238 100644 --- a/backend/server/worldtravel/management/commands/download-countries.py +++ b/backend/server/worldtravel/management/commands/download-countries.py @@ -6,6 +6,8 @@ from django.db import transaction import json from django.conf import settings + +COUNTRY_REGION_JSON_VERSION = settings.COUNTRY_REGION_JSON_VERSION media_root = settings.MEDIA_ROOT @@ -38,7 +40,7 @@ class Command(BaseCommand): def handle(self, *args, **options): countries_json_path = os.path.join(settings.MEDIA_ROOT, 'countries+regions.json') if not os.path.exists(countries_json_path): - res = requests.get('https://raw.githubusercontent.com/dr5hn/countries-states-cities-database/master/countries%2Bstates.json') + res = requests.get(f'https://raw.githubusercontent.com/dr5hn/countries-states-cities-database/{COUNTRY_REGION_JSON_VERSION}/countries%2Bstates.json') if res.status_code == 200: with open(countries_json_path, 'w') as f: f.write(res.text) @@ -65,6 +67,7 @@ class Command(BaseCommand): country_code = country['iso2'] country_name = country['name'] country_subregion = country['subregion'] + country_capital = country['capital'] processed_country_codes.add(country_code) @@ -72,12 +75,14 @@ class Command(BaseCommand): country_obj = existing_countries[country_code] country_obj.name = country_name country_obj.subregion = country_subregion + country_obj.capital = country_capital countries_to_update.append(country_obj) else: country_obj = Country( name=country_name, country_code=country_code, - subregion=country_subregion + subregion=country_subregion, + capital=country_capital ) countries_to_create.append(country_obj) @@ -132,7 +137,7 @@ class Command(BaseCommand): Region.objects.bulk_create(regions_to_create) # Bulk update existing countries and regions - Country.objects.bulk_update(countries_to_update, ['name', 'subregion']) + Country.objects.bulk_update(countries_to_update, ['name', 'subregion', 'capital']) Region.objects.bulk_update(regions_to_update, ['name', 'country', 'longitude', 'latitude']) # Delete countries and regions that are no longer in the data diff --git a/backend/server/worldtravel/migrations/0010_country_capital.py b/backend/server/worldtravel/migrations/0010_country_capital.py new file mode 100644 index 0000000..35a2d70 --- /dev/null +++ b/backend/server/worldtravel/migrations/0010_country_capital.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.8 on 2024-09-11 19:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('worldtravel', '0009_alter_country_country_code'), + ] + + operations = [ + migrations.AddField( + model_name='country', + name='capital', + field=models.CharField(blank=True, max_length=100, null=True), + ), + ] diff --git a/backend/server/worldtravel/models.py b/backend/server/worldtravel/models.py index 66e10d4..f7f8f99 100644 --- a/backend/server/worldtravel/models.py +++ b/backend/server/worldtravel/models.py @@ -14,6 +14,7 @@ class Country(models.Model): name = models.CharField(max_length=100) country_code = models.CharField(max_length=2, unique=True) #iso2 code subregion = models.CharField(max_length=100, blank=True, null=True) + capital = models.CharField(max_length=100, blank=True, null=True) class Meta: verbose_name = "Country" diff --git a/frontend/src/lib/components/CountryCard.svelte b/frontend/src/lib/components/CountryCard.svelte index 95fbde8..d75e3ed 100644 --- a/frontend/src/lib/components/CountryCard.svelte +++ b/frontend/src/lib/components/CountryCard.svelte @@ -5,6 +5,8 @@ import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); + import MapMarkerStar from '~icons/mdi/map-marker-star'; + export let country: Country; async function nav() { @@ -21,7 +23,12 @@

{country.name}

-
{country.subregion}
+ {#if country.subregion} +
{country.subregion}
+ {/if} + {#if country.capital} +
{country.capital}
+ {/if}
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 8fade57..4b5bb15 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -41,6 +41,7 @@ export type Country = { country_code: string; subregion: string; flag_url: string; + capital: string; }; export type Region = { From cc7c04b0ec532a86cc15988c37f0dde59e4d9ad0 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 13 Sep 2024 20:33:44 -0400 Subject: [PATCH 7/9] Redownload when region data version changes --- backend/server/adventures/admin.py | 3 +- .../management/commands/download-countries.py | 2 +- backend/server/worldtravel/views.py | 2 +- .../src/routes/worldtravel/[id]/+page.svelte | 28 +++++++++++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py index 226074d..70eedc9 100644 --- a/backend/server/adventures/admin.py +++ b/backend/server/adventures/admin.py @@ -22,7 +22,7 @@ class AdventureAdmin(admin.ModelAdmin): class CountryAdmin(admin.ModelAdmin): list_display = ('name', 'country_code', 'number_of_regions') - list_filter = ('country_code', 'subregion') + list_filter = ('subregion',) def number_of_regions(self, obj): return Region.objects.filter(country=obj).count() @@ -32,6 +32,7 @@ class CountryAdmin(admin.ModelAdmin): class RegionAdmin(admin.ModelAdmin): list_display = ('name', 'country', 'number_of_visits') + list_filter = ('country',) # list_filter = ('country', 'number_of_visits') def number_of_visits(self, obj): diff --git a/backend/server/worldtravel/management/commands/download-countries.py b/backend/server/worldtravel/management/commands/download-countries.py index 48a6238..c9dff83 100644 --- a/backend/server/worldtravel/management/commands/download-countries.py +++ b/backend/server/worldtravel/management/commands/download-countries.py @@ -38,7 +38,7 @@ class Command(BaseCommand): help = 'Imports the world travel data' def handle(self, *args, **options): - countries_json_path = os.path.join(settings.MEDIA_ROOT, 'countries+regions.json') + 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') if res.status_code == 200: diff --git a/backend/server/worldtravel/views.py b/backend/server/worldtravel/views.py index 1332b1e..ba4c3cc 100644 --- a/backend/server/worldtravel/views.py +++ b/backend/server/worldtravel/views.py @@ -34,7 +34,7 @@ def visits_by_country(request, country_code): return Response(serializer.data) class CountryViewSet(viewsets.ReadOnlyModelViewSet): - queryset = Country.objects.all() + queryset = Country.objects.all().order_by('name') serializer_class = CountrySerializer permission_classes = [IsAuthenticated] diff --git a/frontend/src/routes/worldtravel/[id]/+page.svelte b/frontend/src/routes/worldtravel/[id]/+page.svelte index 2b4cf33..3ea1f41 100644 --- a/frontend/src/routes/worldtravel/[id]/+page.svelte +++ b/frontend/src/routes/worldtravel/[id]/+page.svelte @@ -1,6 +1,17 @@