Skip to content

Commit

Permalink
Merge pull request #240 from seanmorley15/development
Browse files Browse the repository at this point in the history
Multiple images and new creation modal
  • Loading branch information
seanmorley15 authored Aug 18, 2024
2 parents b14cf9c + d1a49b7 commit b8d7363
Show file tree
Hide file tree
Showing 23 changed files with 1,146 additions and 997 deletions.
17 changes: 16 additions & 1 deletion backend/server/adventures/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
from django.contrib import admin
from django.utils.html import mark_safe
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage
from worldtravel.models import Country, Region, VisitedRegion


Expand Down Expand Up @@ -57,6 +57,20 @@ def image_display(self, obj):
else:
return

class AdventureImageAdmin(admin.ModelAdmin):
list_display = ('user_id', 'image_display')

def image_display(self, obj):
if obj.image: # Ensure this field matches your model's image field
public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/')
public_url = public_url.replace("'", "")
return mark_safe(f'<img src="{public_url}/media/{obj.image.name}" width="100px" height="100px"')
else:
return

image_display.short_description = 'Image Preview'


class CollectionAdmin(admin.ModelAdmin):
def adventure_count(self, obj):
return obj.adventure_set.count()
Expand All @@ -78,6 +92,7 @@ def adventure_count(self, obj):
admin.site.register(Note)
admin.site.register(Checklist)
admin.site.register(ChecklistItem)
admin.site.register(AdventureImage, AdventureImageAdmin)

admin.site.site_header = 'AdventureLog Admin'
admin.site.site_title = 'AdventureLog Admin Site'
Expand Down
19 changes: 19 additions & 0 deletions backend/server/adventures/migrations/0001_adventure_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 5.0.8 on 2024-08-15 23:20

import django_resized.forms
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('adventures', 'migrate_images'),
]

operations = [
migrations.AddField(
model_name='adventure',
name='image',
field=django_resized.forms.ResizedImageField(blank=True, crop=None, force_format='WEBP', keep_meta=True, null=True, quality=75, scale=None, size=[1920, 1080], upload_to='images/'),
),
]
27 changes: 27 additions & 0 deletions backend/server/adventures/migrations/0002_adventureimage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 5.0.8 on 2024-08-15 23:17

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


class Migration(migrations.Migration):

dependencies = [
('adventures', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='AdventureImage',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('image', django_resized.forms.ResizedImageField(crop=None, force_format='WEBP', keep_meta=True, quality=75, scale=None, size=[1920, 1080], upload_to='images/')),
('adventure', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='adventures.adventure')),
('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 5.0.8 on 2024-08-15 23:31

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('adventures', '0001_adventure_image'),
]

operations = [
migrations.AlterField(
model_name='adventureimage',
name='adventure',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='adventures.adventure'),
),
]
29 changes: 29 additions & 0 deletions backend/server/adventures/migrations/migrate_images.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from django.db import migrations

def move_images_to_new_model(apps, schema_editor):
Adventure = apps.get_model('adventures', 'Adventure')
AdventureImage = apps.get_model('adventures', 'AdventureImage')

for adventure in Adventure.objects.all():
if adventure.image:
AdventureImage.objects.create(
adventure=adventure,
image=adventure.image,
user_id=adventure.user_id,
)


class Migration(migrations.Migration):

dependencies = [
('adventures', '0001_initial'),
('adventures', '0002_adventureimage'),
]

operations = [
migrations.RunPython(move_images_to_new_model),
migrations.RemoveField(
model_name='Adventure',
name='image',
),
]
10 changes: 10 additions & 0 deletions backend/server/adventures/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,13 @@ def clean(self):

def __str__(self):
return self.name

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/')
adventure = models.ForeignKey(Adventure, related_name='images', on_delete=models.CASCADE)

def __str__(self):
return self.image.url
42 changes: 31 additions & 11 deletions backend/server/adventures/serializers.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import os
from .models import Adventure, ChecklistItem, Collection, Note, Transportation, Checklist
from .models import Adventure, AdventureImage, ChecklistItem, Collection, Note, Transportation, Checklist
from rest_framework import serializers

class AdventureSerializer(serializers.ModelSerializer):

class AdventureImageSerializer(serializers.ModelSerializer):
class Meta:
model = Adventure
fields = '__all__'
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
model = AdventureImage
fields = ['id', 'image', 'adventure']
read_only_fields = ['id']

# def to_representation(self, instance):
# representation = super().to_representation(instance)

# # Build the full URL for the image
# request = self.context.get('request')
# if request and instance.image:
# public_url = request.build_absolute_uri(instance.image.url)
# else:
# public_url = f"{os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/')}/media/{instance.image.name}"

# representation['image'] = public_url
# return representation

def to_representation(self, instance):
representation = super().to_representation(instance)
Expand All @@ -18,11 +30,19 @@ def to_representation(self, instance):
public_url = public_url.replace("'", "")
representation['image'] = f"{public_url}/media/{instance.image.name}"
return representation

def validate_activity_types(self, value):
if value:
return [activity.lower() for activity in value]
return value



class AdventureSerializer(serializers.ModelSerializer):
images = AdventureImageSerializer(many=True, read_only=True)
class Meta:
model = Adventure
fields = ['id', 'user_id', 'name', 'description', 'rating', 'activity_types', 'location', 'date', 'is_public', 'collection', 'created_at', 'updated_at', 'images', 'link', 'type', 'longitude', 'latitude']
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']

def to_representation(self, instance):
representation = super().to_representation(instance)
return representation

class TransportationSerializer(serializers.ModelSerializer):

Expand Down
3 changes: 2 additions & 1 deletion backend/server/adventures/urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import AdventureViewSet, ChecklistViewSet, CollectionViewSet, NoteViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet
from .views import AdventureViewSet, ChecklistViewSet, CollectionViewSet, NoteViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet, AdventureImageViewSet

router = DefaultRouter()
router.register(r'adventures', AdventureViewSet, basename='adventures')
Expand All @@ -11,6 +11,7 @@
router.register(r'transportations', TransportationViewSet, basename='transportations')
router.register(r'notes', NoteViewSet, basename='notes')
router.register(r'checklists', ChecklistViewSet, basename='checklists')
router.register(r'images', AdventureImageViewSet, basename='images')


urlpatterns = [
Expand Down
97 changes: 95 additions & 2 deletions backend/server/adventures/views.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import uuid
import requests
from django.db import transaction
from rest_framework.decorators import action
from rest_framework import viewsets
from django.db.models.functions import Lower
from rest_framework.response import Response
from .models import Adventure, Checklist, Collection, Transportation, Note
from .models import Adventure, Checklist, Collection, Transportation, Note, AdventureImage
from worldtravel.models import VisitedRegion, Region, Country
from .serializers import AdventureSerializer, CollectionSerializer, NoteSerializer, TransportationSerializer, ChecklistSerializer
from .serializers import AdventureImageSerializer, AdventureSerializer, CollectionSerializer, NoteSerializer, TransportationSerializer, ChecklistSerializer
from rest_framework.permissions import IsAuthenticated
from django.db.models import Q, Prefetch
from .permissions import IsOwnerOrReadOnly, IsPublicReadOnly
Expand Down Expand Up @@ -528,5 +529,97 @@ def get_queryset(self):
user = self.request.user
return Checklist.objects.filter(user_id=user)

def perform_create(self, serializer):
serializer.save(user_id=self.request.user)

class AdventureImageViewSet(viewsets.ModelViewSet):
serializer_class = AdventureImageSerializer
permission_classes = [IsAuthenticated]

def dispatch(self, request, *args, **kwargs):
print(f"Method: {request.method}")
return super().dispatch(request, *args, **kwargs)

@action(detail=True, methods=['post'])
def image_delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)


def create(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
adventure_id = request.data.get('adventure')
try:
adventure = Adventure.objects.get(id=adventure_id)
except Adventure.DoesNotExist:
return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND)

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

return super().create(request, *args, **kwargs)

def update(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)

adventure_id = request.data.get('adventure')
try:
adventure = Adventure.objects.get(id=adventure_id)
except Adventure.DoesNotExist:
return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND)

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

return super().update(request, *args, **kwargs)

def perform_destroy(self, instance):
print("perform_destroy")
return super().perform_destroy(instance)

def destroy(self, request, *args, **kwargs):
print("destroy")
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)

return super().destroy(request, *args, **kwargs)

def partial_update(self, request, *args, **kwargs):
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)

return super().partial_update(request, *args, **kwargs)

@action(detail=False, methods=['GET'], url_path='(?P<adventure_id>[0-9a-f-]+)')
def adventure_images(self, request, adventure_id=None, *args, **kwargs):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)

try:
adventure_uuid = uuid.UUID(adventure_id)
except ValueError:
return Response({"error": "Invalid adventure ID"}, status=status.HTTP_400_BAD_REQUEST)

queryset = AdventureImage.objects.filter(
Q(adventure__id=adventure_uuid) & Q(user_id=request.user)
)

serializer = self.get_serializer(queryset, many=True, context={'request': request})
return Response(serializer.data)

def get_queryset(self):
return AdventureImage.objects.filter(user_id=self.request.user)

def perform_create(self, serializer):
serializer.save(user_id=self.request.user)
29 changes: 26 additions & 3 deletions frontend/src/lib/components/AdventureCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@
dispatch('edit', adventure);
}
let currentSlide = 0;
function goToSlide(index: number) {
currentSlide = index;
}
function link() {
dispatch('link', adventure);
}
Expand All @@ -153,10 +159,27 @@
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-primary-content shadow-xl text-base-content"
>
<figure>
<!-- svelte-ignore a11y-img-redundant-alt -->
{#if adventure.image && adventure.image !== ''}
<img src={adventure.image} alt="Adventure Image" class="w-full h-48 object-cover" />
{#if adventure.images && adventure.images.length > 0}
<div class="carousel w-full">
{#each adventure.images as image, i}
<div
class="carousel-item w-full"
style="display: {i === currentSlide ? 'block' : 'none'}"
>
<img src={image.image} class="w-full h-48 object-cover" alt={adventure.name} />
<div class="flex justify-center w-full py-2 gap-2">
{#each adventure.images as _, i}
<button
on:click={() => goToSlide(i)}
class="btn btn-xs {i === currentSlide ? 'btn-active' : ''}">{i + 1}</button
>
{/each}
</div>
</div>
{/each}
</div>
{:else}
<!-- svelte-ignore a11y-img-redundant-alt -->
<img
src={'https://placehold.co/300?text=No%20Image%20Found&font=roboto'}
alt="No image available"
Expand Down
Loading

0 comments on commit b8d7363

Please sign in to comment.