mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-08-02 11:45:17 +02:00
OIDC Auth and City Visits Data
This commit is contained in:
commit
ec3ada986d
59 changed files with 1776 additions and 484 deletions
|
@ -20,21 +20,45 @@ done
|
|||
python manage.py migrate
|
||||
|
||||
# Create superuser if environment variables are set and there are no users present at all.
|
||||
if [ -n "$DJANGO_ADMIN_USERNAME" ] && [ -n "$DJANGO_ADMIN_PASSWORD" ]; then
|
||||
if [ -n "$DJANGO_ADMIN_USERNAME" ] && [ -n "$DJANGO_ADMIN_PASSWORD" ] && [ -n "$DJANGO_ADMIN_EMAIL" ]; then
|
||||
echo "Creating superuser..."
|
||||
python manage.py shell << EOF
|
||||
from django.contrib.auth import get_user_model
|
||||
from allauth.account.models import EmailAddress
|
||||
|
||||
User = get_user_model()
|
||||
if User.objects.count() == 0:
|
||||
User.objects.create_superuser('$DJANGO_ADMIN_USERNAME', '$DJANGO_ADMIN_EMAIL', '$DJANGO_ADMIN_PASSWORD')
|
||||
|
||||
# Check if the user already exists
|
||||
if not User.objects.filter(username='$DJANGO_ADMIN_USERNAME').exists():
|
||||
# Create the superuser
|
||||
superuser = User.objects.create_superuser(
|
||||
username='$DJANGO_ADMIN_USERNAME',
|
||||
email='$DJANGO_ADMIN_EMAIL',
|
||||
password='$DJANGO_ADMIN_PASSWORD'
|
||||
)
|
||||
print("Superuser created successfully.")
|
||||
|
||||
# Create the EmailAddress object for AllAuth
|
||||
EmailAddress.objects.create(
|
||||
user=superuser,
|
||||
email='$DJANGO_ADMIN_EMAIL',
|
||||
verified=True,
|
||||
primary=True
|
||||
)
|
||||
print("EmailAddress object created successfully for AllAuth.")
|
||||
else:
|
||||
print("Superuser already exists.")
|
||||
EOF
|
||||
fi
|
||||
|
||||
|
||||
# Sync the countries and world travel regions
|
||||
# Sync the countries and world travel regions
|
||||
python manage.py download-countries
|
||||
if [ $? -eq 137 ]; then
|
||||
>&2 echo "WARNING: The download-countries command was interrupted. This is likely due to lack of memory allocated to the container or the host. Please try again with more memory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat /code/adventurelog.txt
|
||||
|
||||
|
|
|
@ -38,4 +38,4 @@ http {
|
|||
alias /code/media/; # Serve media files directly
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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, VisitedCity
|
||||
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,8 @@ 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.register(VisitedCity)
|
||||
|
||||
admin.site.site_header = 'AdventureLog Admin'
|
||||
admin.site.site_title = 'AdventureLog Admin Site'
|
||||
|
|
|
@ -20,4 +20,23 @@ class PrintCookiesMiddleware:
|
|||
def __call__(self, request):
|
||||
print(request.COOKIES)
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
return response
|
||||
|
||||
# middlewares.py
|
||||
|
||||
import os
|
||||
from django.http import HttpRequest
|
||||
|
||||
class OverrideHostMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request: HttpRequest):
|
||||
# Override the host with the PUBLIC_URL environment variable
|
||||
public_url = os.getenv('PUBLIC_URL', None)
|
||||
if public_url:
|
||||
# Split the public URL to extract the host and port (if available)
|
||||
host = public_url.split("//")[-1].split("/")[0]
|
||||
request.META['HTTP_HOST'] = host # Override the HTTP_HOST header
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
|
|
|
@ -8,7 +8,7 @@ from django.db.models.functions import Lower
|
|||
from rest_framework.response import Response
|
||||
from .models import Adventure, Checklist, Collection, Transportation, Note, AdventureImage, Category
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from worldtravel.models import VisitedRegion, Region, Country
|
||||
from worldtravel.models import VisitedCity, VisitedRegion, Region, Country, City
|
||||
from .serializers import AdventureImageSerializer, AdventureSerializer, CategorySerializer, CollectionSerializer, NoteSerializer, TransportationSerializer, ChecklistSerializer
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django.db.models import Q
|
||||
|
@ -20,6 +20,7 @@ from django.contrib.auth import get_user_model
|
|||
from icalendar import Calendar, Event, vText, vCalAddress
|
||||
from django.http import HttpResponse
|
||||
from datetime import datetime
|
||||
from django.db.models import Min
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -44,17 +45,25 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
|||
order_direction = self.request.query_params.get('order_direction', 'asc')
|
||||
include_collections = self.request.query_params.get('include_collections', 'true')
|
||||
|
||||
valid_order_by = ['name', 'type', 'start_date', 'rating', 'updated_at']
|
||||
valid_order_by = ['name', 'type', 'date', 'rating', 'updated_at']
|
||||
if order_by not in valid_order_by:
|
||||
order_by = 'name'
|
||||
|
||||
if order_direction not in ['asc', 'desc']:
|
||||
order_direction = 'asc'
|
||||
|
||||
if order_by == 'date':
|
||||
# order by the earliest visit object associated with the adventure
|
||||
queryset = queryset.annotate(earliest_visit=Min('visits__start_date'))
|
||||
queryset = queryset.filter(earliest_visit__isnull=False)
|
||||
ordering = 'earliest_visit'
|
||||
# Apply case-insensitive sorting for the 'name' field
|
||||
if order_by == 'name':
|
||||
elif order_by == 'name':
|
||||
queryset = queryset.annotate(lower_name=Lower('name'))
|
||||
ordering = 'lower_name'
|
||||
elif order_by == 'rating':
|
||||
queryset = queryset.filter(rating__isnull=False)
|
||||
ordering = 'rating'
|
||||
else:
|
||||
ordering = order_by
|
||||
|
||||
|
@ -1150,41 +1159,48 @@ class ReverseGeocodeViewSet(viewsets.ViewSet):
|
|||
Returns a dictionary containing the region name, country name, and ISO code if found.
|
||||
"""
|
||||
iso_code = None
|
||||
town = None
|
||||
city = None
|
||||
county = None
|
||||
town_city_or_county = None
|
||||
display_name = None
|
||||
country_code = None
|
||||
city = None
|
||||
|
||||
# town = None
|
||||
# city = None
|
||||
# county = None
|
||||
|
||||
if 'address' in data.keys():
|
||||
keys = data['address'].keys()
|
||||
for key in keys:
|
||||
if key.find("ISO") != -1:
|
||||
iso_code = data['address'][key]
|
||||
if 'town' in keys:
|
||||
town = data['address']['town']
|
||||
town_city_or_county = data['address']['town']
|
||||
if 'county' in keys:
|
||||
county = data['address']['county']
|
||||
town_city_or_county = data['address']['county']
|
||||
if 'city' in keys:
|
||||
city = data['address']['city']
|
||||
town_city_or_county = data['address']['city']
|
||||
if not iso_code:
|
||||
return {"error": "No region found"}
|
||||
|
||||
region = Region.objects.filter(id=iso_code).first()
|
||||
visited_region = VisitedRegion.objects.filter(region=region, user_id=self.request.user).first()
|
||||
is_visited = False
|
||||
|
||||
region_visited = False
|
||||
city_visited = False
|
||||
country_code = iso_code[:2]
|
||||
|
||||
if region:
|
||||
if city:
|
||||
display_name = f"{city}, {region.name}, {country_code}"
|
||||
elif town:
|
||||
display_name = f"{town}, {region.name}, {country_code}"
|
||||
elif county:
|
||||
display_name = f"{county}, {region.name}, {country_code}"
|
||||
if town_city_or_county:
|
||||
display_name = f"{town_city_or_county}, {region.name}, {country_code}"
|
||||
city = City.objects.filter(name__contains=town_city_or_county, region=region).first()
|
||||
visited_city = VisitedCity.objects.filter(city=city, user_id=self.request.user).first()
|
||||
|
||||
if visited_region:
|
||||
is_visited = True
|
||||
region_visited = True
|
||||
if visited_city:
|
||||
city_visited = True
|
||||
if region:
|
||||
return {"id": iso_code, "region": region.name, "country": region.country.name, "is_visited": is_visited, "display_name": display_name}
|
||||
return {"region_id": iso_code, "region": region.name, "country": region.country.name, "region_visited": region_visited, "display_name": display_name, "city": city.name if city else None, "city_id": city.id if city else None, "city_visited": city_visited}
|
||||
return {"error": "No region found"}
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
|
|
|
@ -42,6 +42,7 @@ INSTALLED_APPS = (
|
|||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.sites',
|
||||
# "allauth_ui",
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'allauth',
|
||||
|
@ -49,8 +50,8 @@ INSTALLED_APPS = (
|
|||
'allauth.mfa',
|
||||
'allauth.headless',
|
||||
'allauth.socialaccount',
|
||||
# "widget_tweaks",
|
||||
# "slippers",
|
||||
'allauth.socialaccount.providers.github',
|
||||
'allauth.socialaccount.providers.openid_connect',
|
||||
'drf_yasg',
|
||||
'corsheaders',
|
||||
'adventures',
|
||||
|
@ -58,6 +59,9 @@ INSTALLED_APPS = (
|
|||
'users',
|
||||
'integrations',
|
||||
'django.contrib.gis',
|
||||
# 'widget_tweaks',
|
||||
# 'slippers',
|
||||
|
||||
)
|
||||
|
||||
MIDDLEWARE = (
|
||||
|
@ -65,6 +69,7 @@ MIDDLEWARE = (
|
|||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'adventures.middleware.OverrideHostMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
|
@ -75,6 +80,8 @@ MIDDLEWARE = (
|
|||
# disable verifications for new users
|
||||
ACCOUNT_EMAIL_VERIFICATION = 'none'
|
||||
|
||||
ALLAUTH_UI_THEME = "night"
|
||||
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
|
@ -120,7 +127,7 @@ USE_L10N = True
|
|||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
SESSION_COOKIE_SAMESITE = None
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.7/howto/static-files/
|
||||
|
@ -143,6 +150,7 @@ STORAGES = {
|
|||
}
|
||||
}
|
||||
|
||||
SILENCED_SYSTEM_CHECKS = ["slippers.E001"]
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
|
@ -175,6 +183,11 @@ SESSION_SAVE_EVERY_REQUEST = True
|
|||
|
||||
FRONTEND_URL = getenv('FRONTEND_URL', 'http://localhost:3000')
|
||||
|
||||
# Set login redirect URL to the frontend
|
||||
LOGIN_REDIRECT_URL = FRONTEND_URL
|
||||
|
||||
SOCIALACCOUNT_LOGIN_ON_GET = True
|
||||
|
||||
HEADLESS_FRONTEND_URLS = {
|
||||
"account_confirm_email": f"{FRONTEND_URL}/user/verify-email/{{key}}",
|
||||
"account_reset_password": f"{FRONTEND_URL}/user/reset-password",
|
||||
|
@ -263,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'
|
|
@ -3,8 +3,8 @@ from django.contrib import admin
|
|||
from django.views.generic import RedirectView, TemplateView
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from users.views import IsRegistrationDisabled, PublicUserListView, PublicUserDetailView, UserMetadataView, UpdateUserMetadataView
|
||||
from .views import get_csrf_token
|
||||
from users.views import IsRegistrationDisabled, PublicUserListView, PublicUserDetailView, UserMetadataView, UpdateUserMetadataView, EnabledSocialProvidersView
|
||||
from .views import get_csrf_token, get_public_url
|
||||
from drf_yasg.views import get_schema_view
|
||||
|
||||
from drf_yasg import openapi
|
||||
|
@ -27,7 +27,10 @@ urlpatterns = [
|
|||
|
||||
path('auth/user-metadata/', UserMetadataView.as_view(), name='user-metadata'),
|
||||
|
||||
path('auth/social-providers/', EnabledSocialProvidersView.as_view(), name='enabled-social-providers'),
|
||||
|
||||
path('csrf/', get_csrf_token, name='get_csrf_token'),
|
||||
path('public-url/', get_public_url, name='get_public_url'),
|
||||
|
||||
path('', TemplateView.as_view(template_name='home.html')),
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
from django.http import JsonResponse
|
||||
from django.middleware.csrf import get_token
|
||||
from os import getenv
|
||||
|
||||
def get_csrf_token(request):
|
||||
csrf_token = get_token(request)
|
||||
return JsonResponse({'csrfToken': csrf_token})
|
||||
|
||||
def get_public_url(request):
|
||||
return JsonResponse({'PUBLIC_URL': getenv('PUBLIC_URL')})
|
|
@ -13,8 +13,10 @@ django-geojson
|
|||
setuptools
|
||||
gunicorn==23.0.0
|
||||
qrcode==8.0
|
||||
# slippers==0.6.2
|
||||
# django-allauth-ui==1.5.1
|
||||
# django-widget-tweaks==1.5.0
|
||||
slippers==0.6.2
|
||||
django-allauth-ui==1.5.1
|
||||
django-widget-tweaks==1.5.0
|
||||
django-ical==1.9.2
|
||||
icalendar==6.1.0
|
||||
icalendar==6.1.0
|
||||
ijson==3.3.0
|
||||
tqdm==4.67.1
|
|
@ -36,7 +36,7 @@ import os
|
|||
|
||||
class UserDetailsSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
User model w/o password
|
||||
User model without exposing the password.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
|
@ -49,8 +49,8 @@ class UserDetailsSerializer(serializers.ModelSerializer):
|
|||
return username
|
||||
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
extra_fields = ['profile_pic', 'uuid', 'public_profile']
|
||||
profile_pic = serializers.ImageField(required=False)
|
||||
|
||||
if hasattr(UserModel, 'USERNAME_FIELD'):
|
||||
extra_fields.append(UserModel.USERNAME_FIELD)
|
||||
|
@ -64,19 +64,14 @@ class UserDetailsSerializer(serializers.ModelSerializer):
|
|||
extra_fields.append('date_joined')
|
||||
if hasattr(UserModel, 'is_staff'):
|
||||
extra_fields.append('is_staff')
|
||||
if hasattr(UserModel, 'public_profile'):
|
||||
extra_fields.append('public_profile')
|
||||
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = ('profile_pic', 'uuid', 'public_profile', 'email', 'date_joined', 'is_staff', 'is_superuser', 'is_active', 'pk')
|
||||
|
||||
model = UserModel
|
||||
fields = ('pk', *extra_fields)
|
||||
fields = ['pk', *extra_fields]
|
||||
read_only_fields = ('email', 'date_joined', 'is_staff', 'is_superuser', 'is_active', 'pk')
|
||||
|
||||
def handle_public_profile_change(self, instance, validated_data):
|
||||
"""Remove user from `shared_with` if public profile is set to False."""
|
||||
"""
|
||||
Remove user from `shared_with` if public profile is set to False.
|
||||
"""
|
||||
if 'public_profile' in validated_data and not validated_data['public_profile']:
|
||||
for collection in Collection.objects.filter(shared_with=instance):
|
||||
collection.shared_with.remove(instance)
|
||||
|
@ -91,20 +86,37 @@ class UserDetailsSerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class CustomUserDetailsSerializer(UserDetailsSerializer):
|
||||
"""
|
||||
Custom serializer to add additional fields and logic for the user details.
|
||||
"""
|
||||
|
||||
has_password = serializers.SerializerMethodField()
|
||||
|
||||
class Meta(UserDetailsSerializer.Meta):
|
||||
model = CustomUser
|
||||
fields = UserDetailsSerializer.Meta.fields + ('profile_pic', 'uuid', 'public_profile')
|
||||
read_only_fields = UserDetailsSerializer.Meta.read_only_fields + ('uuid',)
|
||||
fields = UserDetailsSerializer.Meta.fields + ['profile_pic', 'uuid', 'public_profile', 'has_password']
|
||||
read_only_fields = UserDetailsSerializer.Meta.read_only_fields + ('uuid', 'has_password')
|
||||
|
||||
@staticmethod
|
||||
def get_has_password(instance):
|
||||
"""
|
||||
Computes whether the user has a usable password set.
|
||||
"""
|
||||
return instance.has_usable_password()
|
||||
|
||||
def to_representation(self, instance):
|
||||
"""
|
||||
Customizes the serialized output to modify `profile_pic` URL and add computed fields.
|
||||
"""
|
||||
representation = super().to_representation(instance)
|
||||
|
||||
# Construct profile picture URL if it exists
|
||||
if instance.profile_pic:
|
||||
public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/')
|
||||
#print(public_url)
|
||||
# remove any ' from the url
|
||||
public_url = public_url.replace("'", "")
|
||||
public_url = public_url.replace("'", "") # Sanitize URL
|
||||
representation['profile_pic'] = f"{public_url}/media/{instance.profile_pic.name}"
|
||||
del representation['pk'] # remove the pk field from the response
|
||||
|
||||
# Remove `pk` field from the response
|
||||
representation.pop('pk', None)
|
||||
|
||||
return representation
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from os import getenv
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
@ -9,6 +10,7 @@ from django.conf import settings
|
|||
from django.shortcuts import get_object_or_404
|
||||
from django.contrib.auth import get_user_model
|
||||
from .serializers import CustomUserDetailsSerializer as PublicUserSerializer
|
||||
from allauth.socialaccount.models import SocialApp
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -120,4 +122,31 @@ class UpdateUserMetadataView(APIView):
|
|||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
class EnabledSocialProvidersView(APIView):
|
||||
"""
|
||||
Get enabled social providers for social authentication. This is used to determine which buttons to show on the frontend. Also returns a URL for each to start the authentication flow.
|
||||
"""
|
||||
|
||||
@swagger_auto_schema(
|
||||
responses={
|
||||
200: openapi.Response('Enabled social providers'),
|
||||
400: 'Bad Request'
|
||||
},
|
||||
operation_description="Get enabled social providers."
|
||||
)
|
||||
def get(self, request):
|
||||
social_providers = SocialApp.objects.filter(sites=settings.SITE_ID)
|
||||
providers = []
|
||||
for provider in social_providers:
|
||||
if provider.provider == 'openid_connect':
|
||||
new_provider = f'oidc/{provider.client_id}'
|
||||
else:
|
||||
new_provider = provider.provider
|
||||
providers.append({
|
||||
'provider': provider.provider,
|
||||
'url': f"{getenv('PUBLIC_URL')}/accounts/{new_provider}/login/",
|
||||
'name': provider.name
|
||||
})
|
||||
return Response(providers, status=status.HTTP_200_OK)
|
|
@ -1,9 +1,10 @@
|
|||
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
|
||||
from tqdm import tqdm
|
||||
import ijson
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
@ -37,33 +38,54 @@ def saveCountryFlag(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, 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')
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--force', action='store_true', help='Force download the countries+regions+states.json file')
|
||||
|
||||
def handle(self, **options):
|
||||
force = options['force']
|
||||
batch_size = 100
|
||||
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)
|
||||
self.stdout.write(self.style.SUCCESS('countries+regions+states.json downloaded successfully'))
|
||||
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'))
|
||||
elif Country.objects.count() == 0 or Region.objects.count() == 0 or City.objects.count() == 0:
|
||||
self.stdout.write(self.style.WARNING('Some region data is missing. Re-importing all data.'))
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS('Latest country, region, and state data already downloaded.'))
|
||||
return
|
||||
|
||||
with open(countries_json_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
f = open(countries_json_path, 'rb')
|
||||
parser = ijson.items(f, 'item')
|
||||
|
||||
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:
|
||||
for country in parser:
|
||||
country_code = country['iso2']
|
||||
country_name = country['name']
|
||||
country_subregion = country['subregion']
|
||||
|
@ -93,7 +115,6 @@ class Command(BaseCommand):
|
|||
countries_to_create.append(country_obj)
|
||||
|
||||
saveCountryFlag(country_code)
|
||||
self.stdout.write(self.style.SUCCESS(f'Country {country_name} prepared'))
|
||||
|
||||
if country['states']:
|
||||
for state in country['states']:
|
||||
|
@ -102,6 +123,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:
|
||||
|
@ -120,7 +146,40 @@ class Command(BaseCommand):
|
|||
latitude=latitude
|
||||
)
|
||||
regions_to_create.append(region_obj)
|
||||
self.stdout.write(self.style.SUCCESS(f'State {state_id} prepared'))
|
||||
# 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)
|
||||
|
@ -136,18 +195,35 @@ class Command(BaseCommand):
|
|||
country=country_obj
|
||||
)
|
||||
regions_to_create.append(region_obj)
|
||||
self.stdout.write(self.style.SUCCESS(f'Region {state_id} prepared for {country_name}'))
|
||||
# self.stdout.write(self.style.SUCCESS(f'Region {state_id} prepared for {country_name}'))
|
||||
for i in tqdm(range(0, len(countries_to_create), batch_size), desc="Processing countries"):
|
||||
batch = countries_to_create[i:i + batch_size]
|
||||
Country.objects.bulk_create(batch)
|
||||
|
||||
# Bulk create new countries and regions
|
||||
Country.objects.bulk_create(countries_to_create)
|
||||
Region.objects.bulk_create(regions_to_create)
|
||||
for i in tqdm(range(0, len(regions_to_create), batch_size), desc="Processing regions"):
|
||||
batch = regions_to_create[i:i + batch_size]
|
||||
Region.objects.bulk_create(batch)
|
||||
|
||||
# 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'])
|
||||
for i in tqdm(range(0, len(cities_to_create), batch_size), desc="Processing cities"):
|
||||
batch = cities_to_create[i:i + batch_size]
|
||||
City.objects.bulk_create(batch)
|
||||
|
||||
# Delete countries and regions that are no longer in the data
|
||||
# Process updates in batches
|
||||
for i in range(0, len(countries_to_update), batch_size):
|
||||
batch = countries_to_update[i:i + batch_size]
|
||||
for i in tqdm(range(0, len(countries_to_update), batch_size), desc="Updating countries"):
|
||||
batch = countries_to_update[i:i + batch_size]
|
||||
Country.objects.bulk_update(batch, ['name', 'subregion', 'capital', 'longitude', 'latitude'])
|
||||
|
||||
for i in tqdm(range(0, len(regions_to_update), batch_size), desc="Updating regions"):
|
||||
batch = regions_to_update[i:i + batch_size]
|
||||
Region.objects.bulk_update(batch, ['name', 'country', 'longitude', 'latitude'])
|
||||
|
||||
for i in tqdm(range(0, len(cities_to_update), batch_size), desc="Updating cities"):
|
||||
batch = cities_to_update[i:i + batch_size]
|
||||
City.objects.bulk_update(batch, ['name', 'region', 'longitude', 'latitude'])
|
||||
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'))
|
27
backend/server/worldtravel/migrations/0012_city.py
Normal file
27
backend/server/worldtravel/migrations/0012_city.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
24
backend/server/worldtravel/migrations/0013_visitedcity.py
Normal file
24
backend/server/worldtravel/migrations/0013_visitedcity.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 5.0.8 on 2025-01-09 17:00
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('worldtravel', '0012_city'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='VisitedCity',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('city', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='worldtravel.city')),
|
||||
('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 5.0.8 on 2025-01-09 18:08
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('worldtravel', '0013_visitedcity'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='visitedcity',
|
||||
options={'verbose_name_plural': 'Visited Cities'},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 5.0.8 on 2025-01-13 17:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('worldtravel', '0014_alter_visitedcity_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='city',
|
||||
name='insert_id',
|
||||
field=models.UUIDField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='country',
|
||||
name='insert_id',
|
||||
field=models.UUIDField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='region',
|
||||
name='insert_id',
|
||||
field=models.UUIDField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -17,6 +17,7 @@ class Country(models.Model):
|
|||
capital = models.CharField(max_length=100, blank=True, null=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)
|
||||
insert_id = models.UUIDField(unique=False, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Country"
|
||||
|
@ -31,6 +32,21 @@ class Region(models.Model):
|
|||
country = models.ForeignKey(Country, 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)
|
||||
insert_id = models.UUIDField(unique=False, blank=True, null=True)
|
||||
|
||||
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)
|
||||
insert_id = models.UUIDField(unique=False, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Cities"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
@ -47,4 +63,21 @@ 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)
|
||||
|
||||
class VisitedCity(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
user_id = models.ForeignKey(
|
||||
User, on_delete=models.CASCADE, default=default_user_id)
|
||||
city = models.ForeignKey(City, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.city.name} ({self.city.region.name}) visited by: {self.user_id.username}'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if VisitedCity.objects.filter(user_id=self.user_id, city=self.city).exists():
|
||||
raise ValidationError("City already visited by user.")
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Visited Cities"
|
|
@ -1,5 +1,5 @@
|
|||
import os
|
||||
from .models import Country, Region, VisitedRegion
|
||||
from .models import Country, Region, VisitedRegion, City, VisitedCity
|
||||
from rest_framework import serializers
|
||||
from main.utils import CustomModelSerializer
|
||||
|
||||
|
@ -33,10 +33,20 @@ class CountrySerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class RegionSerializer(serializers.ModelSerializer):
|
||||
num_cities = serializers.SerializerMethodField()
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = '__all__'
|
||||
read_only_fields = ['id', 'name', 'country', 'longitude', 'latitude']
|
||||
read_only_fields = ['id', 'name', 'country', 'longitude', 'latitude', 'num_cities']
|
||||
|
||||
def get_num_cities(self, obj):
|
||||
return City.objects.filter(region=obj).count()
|
||||
|
||||
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)
|
||||
|
@ -46,4 +56,14 @@ class VisitedRegionSerializer(CustomModelSerializer):
|
|||
class Meta:
|
||||
model = VisitedRegion
|
||||
fields = ['id', 'user_id', 'region', 'longitude', 'latitude', 'name']
|
||||
read_only_fields = ['user_id', 'id', 'longitude', 'latitude', 'name']
|
||||
|
||||
class VisitedCitySerializer(CustomModelSerializer):
|
||||
longitude = serializers.DecimalField(source='city.longitude', max_digits=9, decimal_places=6, read_only=True)
|
||||
latitude = serializers.DecimalField(source='city.latitude', max_digits=9, decimal_places=6, read_only=True)
|
||||
name = serializers.CharField(source='city.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = VisitedCity
|
||||
fields = ['id', 'user_id', 'city', 'longitude', 'latitude', 'name']
|
||||
read_only_fields = ['user_id', 'id', 'longitude', 'latitude', 'name']
|
|
@ -2,15 +2,17 @@
|
|||
|
||||
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, VisitedCityViewSet, visits_by_region
|
||||
router = DefaultRouter()
|
||||
router.register(r'countries', CountryViewSet, basename='countries')
|
||||
router.register(r'regions', RegionViewSet, basename='regions')
|
||||
router.register(r'visitedregion', VisitedRegionViewSet, basename='visitedregion')
|
||||
router.register(r'visitedcity', VisitedCityViewSet, basename='visitedcity')
|
||||
|
||||
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'),
|
||||
path('regions/<str:region_id>/cities/visits/', visits_by_region, name='visits-by-region'),
|
||||
]
|
||||
|
|
|
@ -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, VisitedCity
|
||||
from .serializers import CitySerializer, CountrySerializer, RegionSerializer, VisitedRegionSerializer, VisitedCitySerializer
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
@ -33,6 +33,23 @@ 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)
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def visits_by_region(request, region_id):
|
||||
region = get_object_or_404(Region, id=region_id)
|
||||
visits = VisitedCity.objects.filter(city__region=region, user_id=request.user.id)
|
||||
|
||||
serializer = VisitedCitySerializer(visits, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
class CountryViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
queryset = Country.objects.all().order_by('name')
|
||||
serializer_class = CountrySerializer
|
||||
|
@ -93,4 +110,46 @@ class VisitedRegionViewSet(viewsets.ModelViewSet):
|
|||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
def destroy(self, request, **kwargs):
|
||||
# delete by region id
|
||||
region = get_object_or_404(Region, id=kwargs['pk'])
|
||||
visited_region = VisitedRegion.objects.filter(user_id=request.user.id, region=region)
|
||||
if visited_region.exists():
|
||||
visited_region.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
else:
|
||||
return Response({"error": "Visited region not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
class VisitedCityViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = VisitedCitySerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return VisitedCity.objects.filter(user_id=self.request.user.id)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(user_id=self.request.user)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
request.data['user_id'] = request.user
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
# if the region is not visited, visit it
|
||||
region = serializer.validated_data['city'].region
|
||||
if not VisitedRegion.objects.filter(user_id=request.user.id, region=region).exists():
|
||||
VisitedRegion.objects.create(user_id=request.user, region=region)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
def destroy(self, request, **kwargs):
|
||||
# delete by city id
|
||||
city = get_object_or_404(City, id=kwargs['pk'])
|
||||
visited_city = VisitedCity.objects.filter(user_id=request.user.id, city=city)
|
||||
if visited_city.exists():
|
||||
visited_city.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
else:
|
||||
return Response({"error": "Visited city not found."}, status=status.HTTP_404_NOT_FOUND)
|
Loading…
Add table
Add a link
Reference in a new issue