Skip to content

Commit

Permalink
Immich Integration
Browse files Browse the repository at this point in the history
  • Loading branch information
seanmorley15 authored Jan 6, 2025
2 parents 66ce802 + 4fdc16d commit a9c2af9
Show file tree
Hide file tree
Showing 73 changed files with 2,042 additions and 254 deletions.
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,22 +83,28 @@ Enjoy AdventureLog! 🎉

# Screenshots

![Adventure Page](screenshots/adventures.png)
Displaying the adventures you have visited and the ones you plan to embark on. You can also filter and sort the adventures.
![Adventure Page](brand/screenshots/adventures.png)
Displays the adventures you have visited and the ones you plan to embark on. You can also filter and sort the adventures.

![Detail Page](screenshots/details.png)
![Detail Page](brand/screenshots/details.png)
Shows specific details about an adventure, including the name, date, location, description, and rating.

![Edit](screenshots/edit.png)
![Edit](brand/screenshots/edit.png)

![Map Page](screenshots/map.png)
![Map Page](brand/screenshots/map.png)
View all of your adventures on a map, with the ability to filter by visit status and add new ones by click on the map.

![Itinerary Page](screenshots/itinerary.png)
![Dashboard Page](brand/screenshots/dashboard.png)
Displays a summary of your adventures, including your world travel stats.

![Country Page](screenshots/countries.png)
![Itinerary Page](brand/screenshots/itinerary.png)
Plan your adventures and travel itinerary with a list of activities and a map view. View your trip in a variety of ways, including an itinerary list, a map view, and a calendar view.

![Region Page](screenshots/regions.png)
![Country Page](brand/screenshots/countries.png)
Lists all the countries you have visited and plan to visit, with the ability to filter by visit status.

![Region Page](brand/screenshots/regions.png)
Displays the regions for a specific country, includes a map view to visually select regions.

# About AdventureLog

Expand Down
13 changes: 12 additions & 1 deletion backend/server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,15 @@ EMAIL_BACKEND='console'
# EMAIL_USE_SSL=True
# EMAIL_HOST_USER='user'
# EMAIL_HOST_PASSWORD='password'
# DEFAULT_FROM_EMAIL='user@example.com'
# DEFAULT_FROM_EMAIL='user@example.com'


# ------------------- #
# For Developers to start a Demo Database
# docker run --name postgres-admin -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=admin -e POSTGRES_DB=admin -p 5432:5432 -d postgis/postgis:15-3.3

# PGHOST='localhost'
# PGDATABASE='admin'
# PGUSER='admin'
# PGPASSWORD='admin'
# ------------------- #
2 changes: 0 additions & 2 deletions backend/server/adventures/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
admin.autodiscover()
admin.site.login = secure_admin_login(admin.site.login)



class AdventureAdmin(admin.ModelAdmin):
list_display = ('name', 'get_category', 'get_visit_count', 'user_id', 'is_public')
list_filter = ( 'user_id', 'is_public')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 5.0.8 on 2025-01-01 21:40

import adventures.models
import django_resized.forms
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('adventures', '0015_transportation_destination_latitude_and_more'),
]

operations = [
migrations.AlterField(
model_name='adventureimage',
name='image',
field=django_resized.forms.ResizedImageField(crop=None, force_format='WEBP', keep_meta=True, quality=75, scale=None, size=[1920, 1080], upload_to=adventures.models.PathAndRename('images/')),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.0.8 on 2025-01-03 04:05

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('adventures', '0016_alter_adventureimage_image'),
]

operations = [
migrations.AddField(
model_name='adventureimage',
name='is_primary',
field=models.BooleanField(default=False),
),
]
20 changes: 19 additions & 1 deletion backend/server/adventures/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from collections.abc import Collection
import os
from typing import Iterable
import uuid
from django.db import models
from django.utils.deconstruct import deconstructible

from django.contrib.auth import get_user_model
from django.contrib.postgres.fields import ArrayField
Expand Down Expand Up @@ -257,12 +259,28 @@ def clean(self):
def __str__(self):
return self.name

@deconstructible
class PathAndRename:
def __init__(self, path):
self.path = path

def __call__(self, instance, filename):
ext = filename.split('.')[-1]
# Generate a new UUID for the filename
filename = f"{uuid.uuid4()}.{ext}"
return os.path.join(self.path, filename)

class AdventureImage(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
image = ResizedImageField(force_format="WEBP", quality=75, upload_to='images/')
image = ResizedImageField(
force_format="WEBP",
quality=75,
upload_to=PathAndRename('images/') # Use the callable class here
)
adventure = models.ForeignKey(Adventure, related_name='images', on_delete=models.CASCADE)
is_primary = models.BooleanField(default=False)

def __str__(self):
return self.image.url
Expand Down
38 changes: 20 additions & 18 deletions backend/server/adventures/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
class AdventureImageSerializer(CustomModelSerializer):
class Meta:
model = AdventureImage
fields = ['id', 'image', 'adventure']
fields = ['id', 'image', 'adventure', 'is_primary']
read_only_fields = ['id']

def to_representation(self, instance):
Expand Down Expand Up @@ -116,7 +116,7 @@ def get_is_visited(self, obj):
return False

def create(self, validated_data):
visits_data = validated_data.pop('visits', [])
visits_data = validated_data.pop('visits', None)
category_data = validated_data.pop('category', None)
print(category_data)
adventure = Adventure.objects.create(**validated_data)
Expand All @@ -131,6 +131,7 @@ def create(self, validated_data):
return adventure

def update(self, instance, validated_data):
has_visits = 'visits' in validated_data
visits_data = validated_data.pop('visits', [])
category_data = validated_data.pop('category', None)

Expand All @@ -142,24 +143,25 @@ def update(self, instance, validated_data):
instance.category = category
instance.save()

current_visits = instance.visits.all()
current_visit_ids = set(current_visits.values_list('id', flat=True))
if has_visits:
current_visits = instance.visits.all()
current_visit_ids = set(current_visits.values_list('id', flat=True))

updated_visit_ids = set()
for visit_data in visits_data:
visit_id = visit_data.get('id')
if visit_id and visit_id in current_visit_ids:
visit = current_visits.get(id=visit_id)
for attr, value in visit_data.items():
setattr(visit, attr, value)
visit.save()
updated_visit_ids.add(visit_id)
else:
new_visit = Visit.objects.create(adventure=instance, **visit_data)
updated_visit_ids.add(new_visit.id)
updated_visit_ids = set()
for visit_data in visits_data:
visit_id = visit_data.get('id')
if visit_id and visit_id in current_visit_ids:
visit = current_visits.get(id=visit_id)
for attr, value in visit_data.items():
setattr(visit, attr, value)
visit.save()
updated_visit_ids.add(visit_id)
else:
new_visit = Visit.objects.create(adventure=instance, **visit_data)
updated_visit_ids.add(new_visit.id)

visits_to_delete = current_visit_ids - updated_visit_ids
instance.visits.filter(id__in=visits_to_delete).delete()
visits_to_delete = current_visit_ids - updated_visit_ids
instance.visits.filter(id__in=visits_to_delete).delete()

return instance

Expand Down
22 changes: 22 additions & 0 deletions backend/server/adventures/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1032,7 +1032,29 @@ def dispatch(self, request, *args, **kwargs):
@action(detail=True, methods=['post'])
def image_delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)

@action(detail=True, methods=['post'])
def toggle_primary(self, request, *args, **kwargs):
# Makes the image the primary image for the adventure, if there is already a primary image linked to the adventure, it is set to false and the new image is set to true. make sure that the permission is set to the owner of the adventure
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)

instance = self.get_object()
adventure = instance.adventure
if adventure.user_id != request.user:
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)

# Check if the image is already the primary image
if instance.is_primary:
return Response({"error": "Image is already the primary image"}, status=status.HTTP_400_BAD_REQUEST)

# Set the current primary image to false
AdventureImage.objects.filter(adventure=adventure, is_primary=True).update(is_primary=False)

# Set the new image to true
instance.is_primary = True
instance.save()
return Response({"success": "Image set as primary image"})

def create(self, request, *args, **kwargs):
if not request.user.is_authenticated:
Expand Down
Empty file.
9 changes: 9 additions & 0 deletions backend/server/integrations/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.contrib import admin
from allauth.account.decorators import secure_admin_login

from .models import ImmichIntegration

admin.autodiscover()
admin.site.login = secure_admin_login(admin.site.login)

admin.site.register(ImmichIntegration)
6 changes: 6 additions & 0 deletions backend/server/integrations/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class IntegrationsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'integrations'
27 changes: 27 additions & 0 deletions backend/server/integrations/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 5.0.8 on 2025-01-02 23:16

import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='ImmichIntegration',
fields=[
('server_url', models.CharField(max_length=255)),
('api_key', models.CharField(max_length=255)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
Empty file.
15 changes: 15 additions & 0 deletions backend/server/integrations/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.db import models
from django.contrib.auth import get_user_model
import uuid

User = get_user_model()

class ImmichIntegration(models.Model):
server_url = models.CharField(max_length=255)
api_key = models.CharField(max_length=255)
user = models.ForeignKey(
User, on_delete=models.CASCADE)
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)

def __str__(self):
return self.user.username + ' - ' + self.server_url
13 changes: 13 additions & 0 deletions backend/server/integrations/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from .models import ImmichIntegration
from rest_framework import serializers

class ImmichIntegrationSerializer(serializers.ModelSerializer):
class Meta:
model = ImmichIntegration
fields = '__all__'
read_only_fields = ['id', 'user']

def to_representation(self, instance):
representation = super().to_representation(instance)
representation.pop('user', None)
return representation
3 changes: 3 additions & 0 deletions backend/server/integrations/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.test import TestCase

# Create your tests here.
14 changes: 14 additions & 0 deletions backend/server/integrations/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from integrations.views import ImmichIntegrationView, IntegrationView, ImmichIntegrationViewSet

# Create the router and register the ViewSet
router = DefaultRouter()
router.register(r'immich', ImmichIntegrationView, basename='immich')
router.register(r'', IntegrationView, basename='integrations')
router.register(r'immich', ImmichIntegrationViewSet, basename='immich_viewset')

# Include the router URLs
urlpatterns = [
path("", include(router.urls)), # Includes /immich/ routes
]
Loading

0 comments on commit a9c2af9

Please sign in to comment.