1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-19 04:49:37 +02:00

Merge pull request #275 from seanmorley15/development

Merge for v0.6.0 update
This commit is contained in:
Sean Morley 2024-08-24 16:30:22 -04:00 committed by GitHub
commit 030cacf4b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 267 additions and 39 deletions

View file

@ -13,7 +13,7 @@ WORKDIR /code
# Install system dependencies
RUN apt-get update \
&& apt-get install -y git postgresql-client \
&& apt-get install -y git postgresql-client gdal-bin libgdal-dev \
&& apt-get clean
# Install Python dependencies

View file

@ -13,6 +13,9 @@ done
>&2 echo "PostgreSQL is up - continuing..."
# run sql commands
# psql -h "$PGHOST" -U "$PGUSER" -d "$PGDATABASE" -f /app/backend/init-postgis.sql
# Apply Django migrations
python manage.py migrate

View file

@ -58,7 +58,7 @@ INSTALLED_APPS = (
'adventures',
'worldtravel',
'users',
# 'django_apscheduler',
'django.contrib.gis',
)
@ -101,7 +101,7 @@ SIMPLE_JWT = {
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': getenv('PGDATABASE'),
'USER': getenv('PGUSER'),
'PASSWORD': getenv('PGPASSWORD'),
@ -228,11 +228,9 @@ SWAGGER_SETTINGS = {
'LOGOUT_URL': 'logout',
}
# For demo purposes only. Use a white list in the real world.
CORS_ORIGIN_ALLOW_ALL = True
from os import getenv
CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()]
@ -262,5 +260,3 @@ LOGGING = {
},
},
}
SCHEDULER_AUTOSTART = True

View file

@ -11,4 +11,4 @@ psycopg2-binary
Pillow
whitenoise
django-resized
django-apscheduler
django-geojson

View file

@ -4,7 +4,7 @@
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"type": "MultiPolygon",
"coordinates": [
[
[1.9221462784913, 48.457599361977],

View file

@ -10,7 +10,7 @@
"ISOCODE": "MX-CMX"
},
"geometry": {
"type": "Polygon",
"type": "MultiPolygon",
"coordinates": [
[
[-99.111241, 19.561498],

View file

@ -16,7 +16,7 @@
"AWATER": 23736382213
},
"geometry": {
"type": "Polygon",
"type": "MultiPolygon",
"coordinates": [
[
[-94.0430515276176, 32.6930299766656],

View file

@ -4,11 +4,66 @@ 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 setGeometry(region_code):
# Assuming the file name is the country code (e.g., 'AU.json' for Australia)
country_code = region_code.split('-')[0]
json_file = os.path.join('static/data', f'{country_code.lower()}.json')
if not os.path.exists(json_file):
print(f'File {country_code}.json does not exist (it probably hasn''t been added, contributors are welcome!)')
return None
try:
with open(json_file, 'r') as f:
geojson_data = json.load(f)
except json.JSONDecodeError as e:
print(f"Invalid JSON in file for {country_code}: {e}")
return None
if 'type' not in geojson_data or geojson_data['type'] != 'FeatureCollection':
print(f"Invalid GeoJSON structure for {country_code}: missing or incorrect 'type'")
return None
if 'features' not in geojson_data or not geojson_data['features']:
print(f"Invalid GeoJSON structure for {country_code}: missing or empty 'features'")
return None
for feature in geojson_data['features']:
try:
properties = feature.get('properties', {})
isocode = properties.get('ISOCODE')
if isocode == region_code:
geometry = feature['geometry']
geos_geom = GEOSGeometry(json.dumps(geometry))
if isinstance(geos_geom, Polygon):
Region.objects.filter(id=region_code).update(geometry=MultiPolygon([geos_geom]))
print(f"Updated geometry for region {region_code}")
return MultiPolygon([geos_geom])
elif isinstance(geos_geom, MultiPolygon):
Region.objects.filter(id=region_code).update(geometry=geos_geom)
print(f"Updated geometry for region {region_code}")
return geos_geom
else:
print(f"Unexpected geometry type for region {region_code}: {type(geos_geom)}")
return None
except (KeyError, ValueError, GEOSException) as e:
print(f"Error processing region {region_code}: {e}")
print(f"No matching region found for {region_code}")
return None
def saveCountryFlag(country_code):
flags_dir = os.path.join(media_root, 'flags')
@ -616,7 +671,9 @@ class Command(BaseCommand):
)
if created:
self.stdout.write(f'Inserted {name} into worldtravel regions')
setGeometry(id)
else:
setGeometry(id)
self.stdout.write(f'Updated {name} in worldtravel regions')
def insert_countries(self, countries):
@ -627,6 +684,7 @@ class Command(BaseCommand):
)
if created:
saveCountryFlag(country_code)
self.stdout.write(f'Inserted {name} into worldtravel countries')
else:
saveCountryFlag(country_code)
@ -641,5 +699,7 @@ class Command(BaseCommand):
)
if created:
self.stdout.write(f'Inserted {name} into worldtravel regions')
setGeometry(id)
else:
setGeometry(id)
self.stdout.write(f'{name} already exists in worldtravel regions')

View file

@ -0,0 +1,19 @@
# Generated by Django 5.0.8 on 2024-08-23 17:01
import django.contrib.gis.db.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('worldtravel', '0003_alter_region_name_en'),
]
operations = [
migrations.AddField(
model_name='country',
name='geometry',
field=django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=4326),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 5.0.8 on 2024-08-23 17:47
import django.contrib.gis.db.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('worldtravel', '0004_country_geometry'),
]
operations = [
migrations.RemoveField(
model_name='country',
name='geometry',
),
migrations.AddField(
model_name='region',
name='geometry',
field=django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=4326),
),
]

View file

@ -1,6 +1,7 @@
from django.db import models
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.contrib.gis.db import models as gis_models
User = get_user_model()
@ -35,6 +36,7 @@ class Country(models.Model):
default=AFRICA
)
class Meta:
verbose_name = "Country"
verbose_name_plural = "Countries"
@ -47,6 +49,7 @@ class Region(models.Model):
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)
def __str__(self):
return self.name

View file

@ -22,7 +22,7 @@ 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']
read_only_fields = ['id', 'name', 'country', 'name_en', 'geometry']
class VisitedRegionSerializer(serializers.ModelSerializer):
class Meta:

View file

@ -8,8 +8,12 @@ from rest_framework.response import Response
from rest_framework.decorators import api_view, permission_classes
import os
import json
from django.http import JsonResponse
from django.contrib.gis.geos import Point
from django.conf import settings
from rest_framework.decorators import action
from django.contrib.staticfiles import finders
from adventures.models import Adventure
@api_view(['GET'])
@permission_classes([IsAuthenticated])
@ -34,6 +38,39 @@ class CountryViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = CountrySerializer
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'])
def check_point_in_region(self, request):
lat = float(request.query_params.get('lat'))
lon = float(request.query_params.get('lon'))
point = Point(lon, lat, srid=4326)
region = Region.objects.filter(geometry__contains=point).first()
if region:
return Response({'in_region': True, 'region_name': region.name, 'region_id': region.id})
else:
return Response({'in_region': False})
# make a post action that will get all of the users adventures and check if the point is in any of the regions if so make a visited region object for that user if it does not already exist
@action(detail=False, methods=['post'])
def region_check_all_adventures(self, request):
adventures = Adventure.objects.filter(user_id=request.user.id, type='visited')
count = 0
for adventure in adventures:
if adventure.latitude is not None and adventure.longitude is not None:
try:
print(f"Adventure {adventure.id}: lat={adventure.latitude}, lon={adventure.longitude}")
point = Point(float(adventure.longitude), float(adventure.latitude), srid=4326)
region = Region.objects.filter(geometry__contains=point).first()
if region:
if not VisitedRegion.objects.filter(user_id=request.user.id, region=region).exists():
VisitedRegion.objects.create(user_id=request.user, region=region)
count += 1
except Exception as e:
print(f"Error processing adventure {adventure.id}: {e}")
continue
return Response({'regions_visited': count})
class RegionViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Region.objects.all()
serializer_class = RegionSerializer

View file

@ -1,3 +1,5 @@
# This script will create a backup of the adventurelog_media volume and store it in the current directory as adventurelog-backup.tar.gz
docker run --rm \
-v adventurelog_adventurelog_media:/backup-volume \
-v "$(pwd)":/backup \

View file

@ -1,3 +1,5 @@
# This script is used to deploy the latest version of AdventureLog to the server. It pulls the latest version of the Docker images and starts the containers. It is a simple script that can be run on the server, possibly as a cron job, to keep the server up to date with the latest version of the application.
echo "Deploying latest version of AdventureLog"
docker compose pull
echo "Stating containers"

View file

@ -4,6 +4,7 @@ services:
web:
#build: ./frontend/
image: ghcr.io/seanmorley15/adventurelog-frontend:latest
container_name: adventurelog-frontend
environment:
- PUBLIC_SERVER_URL=http://server:8000 # MOST DOCKER USERS WILL NEVER NEED TO CHANGE THIS, EVEN IF YOU CHANGE THE PORTS
- ORIGIN=http://localhost:8080
@ -14,7 +15,8 @@ services:
- server
db:
image: postgres:latest
image: postgis/postgis:15-3.3
container_name: adventurelog-db
environment:
POSTGRES_DB: database
POSTGRES_USER: adventure
@ -25,6 +27,7 @@ services:
server:
#build: ./backend/
image: ghcr.io/seanmorley15/adventurelog-backend:latest
container_name: adventurelog-backend
environment:
- PGHOST=db
- PGDATABASE=database
@ -47,6 +50,7 @@ services:
nginx:
image: nginx:latest
container_name: adventurelog-nginx
ports:
- "81:80"
volumes:

View file

@ -13,7 +13,7 @@
export let is_collection: boolean = false;
import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';
let markers: Point[] = [];
let query: string = '';
let places: OpenStreetMapPlace[] = [];
let images: { id: string; image: string }[] = [];
@ -29,6 +29,9 @@
let noPlaces: boolean = false;
let region_name: string | null = null;
let region_id: string | null = null;
let adventure: Adventure = {
id: '',
name: '',
@ -69,6 +72,8 @@
collection: adventureToEdit?.collection || collection_id || null
};
let markers: Point[] = [];
let url: string = '';
let imageError: string = '';
let wikiImageError: string = '';
@ -76,6 +81,7 @@
images = adventure.images || [];
if (adventure.longitude && adventure.latitude) {
markers = [];
markers = [
{
lngLat: { lng: adventure.longitude, lat: adventure.latitude },
@ -84,6 +90,7 @@
activity_type: ''
}
];
checkPointInRegion();
}
if (longitude && latitude) {
@ -98,6 +105,13 @@
}
}
function clearMap() {
console.log('CLEAR');
markers = [];
region_id = null;
region_name = null;
}
let imageSearch: string = adventure.name || '';
async function removeImage(id: string) {
@ -132,6 +146,13 @@
}
}
$: {
if (adventure.type != 'visited') {
region_id = null;
region_name = null;
}
}
async function fetchImage() {
let res = await fetch(url);
let data = await res.blob();
@ -237,6 +258,7 @@
activity_type: data[0]?.type || ''
}
];
checkPointInRegion();
}
}
console.log(data);
@ -274,7 +296,30 @@
}
}
function addMarker(e: CustomEvent<any>) {
async function checkPointInRegion() {
if (adventure.type == 'visited') {
let lat = markers[0].lngLat.lat;
let lon = markers[0].lngLat.lng;
let res = await fetch(`/api/countries/check_point_in_region/?lat=${lat}&lon=${lon}`);
let data = await res.json();
if (data.error) {
addToast('error', data.error);
} else {
if (data.in_region) {
region_name = data.region_name;
region_id = data.region_id;
} else {
region_id = null;
region_name = null;
}
}
} else {
region_id = null;
region_name = null;
}
}
async function addMarker(e: CustomEvent<any>) {
markers = [];
markers = [
...markers,
@ -285,6 +330,8 @@
activity_type: ''
}
];
checkPointInRegion();
console.log(markers);
}
@ -308,6 +355,19 @@
async function handleSubmit(event: Event) {
event.preventDefault();
if (region_id && region_name) {
let res = await fetch(`/api/visitedregion/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ region: region_id })
});
if (res.ok) {
addToast('success', `Region ${region_name} marked as visited`);
}
}
if (adventure.date && adventure.end_date) {
if (new Date(adventure.date) > new Date(adventure.end_date)) {
addToast('error', 'Start date must be before end date');
@ -608,6 +668,9 @@
bind:value={query}
/>
<button class="btn btn-neutral -mt-1" type="submit">Search</button>
<button class="btn btn-neutral -mt-1" type="button" on:click={clearMap}
>Clear Map</button
>
</form>
</div>
{#if places.length > 0}
@ -628,6 +691,7 @@
activity_type: place.type
}
];
checkPointInRegion();
}}
>
{place.display_name}
@ -655,6 +719,9 @@ it would also work to just use on:click on the MapLibre component itself. -->
{/each}
</MapLibre>
</div>
{#if region_name}
<p class="text-lg font-semibold mt-2">Region: {region_name} ({region_id})</p>
{/if}
<div class="mt-4">
<button type="submit" class="btn btn-primary">Save & Next</button>

View file

@ -19,21 +19,10 @@
let showVisited = true;
let showPlanned = true;
$: {
if (!showVisited) {
markers = data.props.markers.filter((marker) => marker.type !== 'visited');
} else {
const visitedMarkers = data.props.markers.filter((marker) => marker.type === 'visited');
markers = [...markers, ...visitedMarkers];
}
if (!showPlanned) {
markers = data.props.markers.filter((marker) => marker.type !== 'planned');
} else {
const plannedMarkers = data.props.markers.filter((marker) => marker.type === 'planned');
markers = [...markers, ...plannedMarkers];
}
console.log(markers);
}
$: filteredMarkers = markers.filter(
(marker) =>
(showVisited && marker.type === 'visited') || (showPlanned && marker.type === 'planned')
);
let newMarker = [];
@ -43,7 +32,6 @@
function addMarker(e) {
newMarker = [];
newMarker = [...newMarker, { lngLat: e.detail.lngLat, name: 'Marker 1' }];
console.log(newMarker);
newLongitude = e.detail.lngLat.lng;
newLatitude = e.detail.lngLat.lat;
}
@ -55,19 +43,15 @@
}
function createNewAdventure(event) {
console.log(event.detail);
let newMarker = {
lngLat: [event.detail.longitude, event.detail.latitude],
name: event.detail.name,
type: 'planned'
type: event.detail.type
};
markers = [...markers, newMarker];
clearMarkers();
console.log(markers);
createModalOpen = false;
}
let visitedRegions = data.props.visitedRegions;
let geoJSON = [];
@ -154,7 +138,7 @@
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full"
standardControls
>
{#each markers as { lngLat, name, type }}
{#each filteredMarkers as { lngLat, name, type }}
{#if type == 'visited'}
<Marker
{lngLat}

View file

@ -44,6 +44,21 @@
a.click();
URL.revokeObjectURL(url);
}
async function checkVisitedRegions() {
let res = await fetch('/api/countries/region_check_all_adventures/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
let data = await res.json();
if (res.ok) {
addToast('success', `${data.regions_visited} regions updated`);
} else {
addToast('error', 'Error updating visited regions');
}
}
</script>
<h1 class="text-center font-extrabold text-4xl mb-6">Settings Page</h1>
@ -152,7 +167,20 @@
<button class="py-2 px-4 btn btn-primary mt-2">Change Email</button>
</form>
</div>
<div class="flex flex-col items-center">
<div class="flex flex-col items-center mt-4">
<h1 class="text-center font-extrabold text-xl mt-4 mb-2">Visited Region Check</h1>
<p>
By selecting this, the server will check all of your visited adventures and mark the regions
they are located in as "visited" in world travel.
</p>
<button class="btn btn-neutral mt-2 mb-2" on:click={checkVisitedRegions}
>Update Visited Regions</button
>
<p>This may take longer depending on the number of adventures you have.</p>
</div>
<div class="flex flex-col items-center mt-4">
<h1 class="text-center font-extrabold text-xl mt-4 mb-2">Data Export</h1>
<button class="btn btn-neutral mb-4" on:click={exportAdventures}> Export to JSON </button>
<p>This may take a few seconds...</p>