1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-19 12:59:36 +02:00

Initial framework for custom categories

This commit is contained in:
Sean Morley 2024-11-14 09:37:35 -05:00
parent c3f37b66d0
commit 4a7f720773
8 changed files with 205 additions and 11 deletions

View file

@ -1,15 +1,30 @@
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
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category
from worldtravel.models import Country, Region, VisitedRegion
class AdventureAdmin(admin.ModelAdmin):
list_display = ('name', 'type', 'user_id', 'is_public')
list_filter = ('type', 'user_id', 'is_public')
list_display = ('name', 'get_category', 'get_visit_count', 'user_id', 'is_public')
list_filter = ( 'user_id', 'is_public')
search_fields = ('name',)
def get_category(self, obj):
if obj.category and obj.category.display_name and obj.category.icon:
return obj.category.display_name + ' ' + obj.category.icon
elif obj.category and obj.category.name:
return obj.category.name
else:
return 'No Category'
get_category.short_description = 'Category'
def get_visit_count(self, obj):
return obj.visits.count()
get_visit_count.short_description = 'Visit Count'
class CountryAdmin(admin.ModelAdmin):
list_display = ('name', 'country_code', 'number_of_regions')
@ -81,6 +96,9 @@ class VisitAdmin(admin.ModelAdmin):
image_display.short_description = 'Image Preview'
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'user_id', 'display_name', 'icon')
search_fields = ('name', 'display_name')
class CollectionAdmin(admin.ModelAdmin):
def adventure_count(self, obj):
@ -105,6 +123,7 @@ admin.site.register(Note)
admin.site.register(Checklist)
admin.site.register(ChecklistItem)
admin.site.register(AdventureImage, AdventureImageAdmin)
admin.site.register(Category, CategoryAdmin)
admin.site.site_header = 'AdventureLog Admin'
admin.site.site_title = 'AdventureLog Admin Site'

View file

@ -0,0 +1,34 @@
# Generated by Django 5.0.8 on 2024-11-14 04:30
from django.conf import settings
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0010_collection_link'),
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=200)),
('display_name', models.CharField(max_length=200)),
('icon', models.CharField(default='🌎', max_length=200)),
('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'Categories',
},
),
migrations.AddField(
model_name='adventure',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='adventures.category'),
),
]

View file

@ -0,0 +1,59 @@
from django.db import migrations
def migrate_categories(apps, schema_editor):
# Use the historical models
Adventure = apps.get_model('adventures', 'Adventure')
Category = apps.get_model('adventures', 'Category')
ADVENTURE_TYPES = {
'general': ('General', '🌍'),
'outdoor': ('Outdoor', '🏞️'),
'lodging': ('Lodging', '🛌'),
'dining': ('Dining', '🍽️'),
'activity': ('Activity', '🏄'),
'attraction': ('Attraction', '🎢'),
'shopping': ('Shopping', '🛍️'),
'nightlife': ('Nightlife', '🌃'),
'event': ('Event', '🎉'),
'transportation': ('Transportation', '🚗'),
'culture': ('Culture', '🎭'),
'water_sports': ('Water Sports', '🚤'),
'hiking': ('Hiking', '🥾'),
'wildlife': ('Wildlife', '🦒'),
'historical_sites': ('Historical Sites', '🏛️'),
'music_concerts': ('Music & Concerts', '🎶'),
'fitness': ('Fitness', '🏋️'),
'art_museums': ('Art & Museums', '🎨'),
'festivals': ('Festivals', '🎪'),
'spiritual_journeys': ('Spiritual Journeys', '🧘‍♀️'),
'volunteer_work': ('Volunteer Work', '🤝'),
'other': ('Other', ''),
}
adventures = Adventure.objects.all()
for adventure in adventures:
# Access the old 'type' field using __dict__ because it's not in the model anymore
old_type = adventure.__dict__.get('type')
if old_type in ADVENTURE_TYPES:
category, created = Category.objects.get_or_create(
name=old_type,
user_id=adventure.user_id,
defaults={
'display_name': ADVENTURE_TYPES[old_type][0],
'icon': ADVENTURE_TYPES[old_type][1],
}
)
adventure.category = category
adventure.save()
else:
print(f"Unknown type: {old_type}")
class Migration(migrations.Migration):
dependencies = [
('adventures', '0011_category_adventure_category'),
]
operations = [
migrations.RunPython(migrate_categories),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 5.0.8 on 2024-11-14 04:51
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0012_migrate_types_to_categories'),
]
operations = [
migrations.RemoveField(
model_name='adventure',
name='type',
),
migrations.AlterField(
model_name='adventure',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='adventures.category'),
),
]

View file

@ -1,3 +1,4 @@
from collections.abc import Collection
import uuid
from django.db import models
@ -68,7 +69,8 @@ class Adventure(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)
type = models.CharField(max_length=100, choices=ADVENTURE_TYPES, default='general')
category = models.ForeignKey('Category', on_delete=models.SET_NULL, blank=True, null=True)
name = models.CharField(max_length=200)
location = models.CharField(max_length=200, blank=True, null=True)
activity_types = ArrayField(models.CharField(
@ -88,6 +90,7 @@ class Adventure(models.Model):
# image = ResizedImageField(force_format="WEBP", quality=75, null=True, blank=True, upload_to='images/')
# date = models.DateField(blank=True, null=True)
# end_date = models.DateField(blank=True, null=True)
# type = models.CharField(max_length=100, choices=ADVENTURE_TYPES, default='general')
def clean(self):
if self.collection:
@ -95,6 +98,9 @@ class Adventure(models.Model):
raise ValidationError('Adventures associated with a public collection must be public. Collection: ' + self.trip.name + ' Adventure: ' + self.name)
if self.user_id != self.collection.user_id:
raise ValidationError('Adventures must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Adventure owner: ' + self.user_id.username)
if self.category:
if self.user_id != self.category.user_id:
raise ValidationError('Adventures must be associated with categories owned by the same user. Category owner: ' + self.category.user_id.username + ' Adventure owner: ' + self.user_id.username)
def __str__(self):
return self.name
@ -235,3 +241,24 @@ class AdventureImage(models.Model):
def __str__(self):
return self.image.url
class Category(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)
name = models.CharField(max_length=200)
display_name = models.CharField(max_length=200)
icon = models.CharField(max_length=200, default='🌎')
class Meta:
verbose_name_plural = 'Categories'
unique_together = ['name', 'user_id']
def clean(self) -> None:
self.name = self.name.lower().strip()
return super().clean()
def __str__(self):
return self.name

View file

@ -1,6 +1,6 @@
from django.utils import timezone
import os
from .models import Adventure, AdventureImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit
from .models import Adventure, AdventureImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit, Category
from rest_framework import serializers
class AdventureImageSerializer(serializers.ModelSerializer):
@ -19,6 +19,18 @@ class AdventureImageSerializer(serializers.ModelSerializer):
representation['image'] = f"{public_url}/media/{instance.image.name}"
return representation
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ['id', 'name', 'display_name', 'icon', 'user_id']
read_only_fields = ['id', 'user_id']
def validate_name(self, value):
if Category.objects.filter(name=value).exists():
raise serializers.ValidationError('Category with this name already exists.')
return value
class VisitSerializer(serializers.ModelSerializer):
class Meta:
@ -29,10 +41,11 @@ class VisitSerializer(serializers.ModelSerializer):
class AdventureSerializer(serializers.ModelSerializer):
images = AdventureImageSerializer(many=True, read_only=True)
visits = VisitSerializer(many=True, read_only=False)
category = CategorySerializer(read_only=True)
is_visited = serializers.SerializerMethodField()
class Meta:
model = Adventure
fields = ['id', 'user_id', 'name', 'description', 'rating', 'activity_types', 'location', 'is_public', 'collection', 'created_at', 'updated_at', 'images', 'link', 'type', 'longitude', 'latitude', 'visits', 'is_visited']
fields = ['id', 'user_id', 'name', 'description', 'rating', 'activity_types', 'location', 'is_public', 'collection', 'created_at', 'updated_at', 'images', 'link', 'longitude', 'latitude', 'visits', 'is_visited', 'category']
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'is_visited']
def get_is_visited(self, obj):

View file

@ -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, AdventureImageViewSet, ReverseGeocodeViewSet
from .views import AdventureViewSet, ChecklistViewSet, CollectionViewSet, NoteViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet, AdventureImageViewSet, ReverseGeocodeViewSet, CategoryViewSet
router = DefaultRouter()
router.register(r'adventures', AdventureViewSet, basename='adventures')
@ -13,6 +13,7 @@ router.register(r'notes', NoteViewSet, basename='notes')
router.register(r'checklists', ChecklistViewSet, basename='checklists')
router.register(r'images', AdventureImageViewSet, basename='images')
router.register(r'reverse-geocode', ReverseGeocodeViewSet, basename='reverse-geocode')
router.register(r'categories', CategoryViewSet, basename='categories')
urlpatterns = [

View file

@ -6,10 +6,10 @@ 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, AdventureImage, ADVENTURE_TYPES
from .models import Adventure, Checklist, Collection, Transportation, Note, AdventureImage, Category
from django.core.exceptions import PermissionDenied
from worldtravel.models import VisitedRegion, Region, Country
from .serializers import AdventureImageSerializer, AdventureSerializer, CollectionSerializer, NoteSerializer, TransportationSerializer, ChecklistSerializer
from .serializers import AdventureImageSerializer, AdventureSerializer, CategorySerializer, CollectionSerializer, NoteSerializer, TransportationSerializer, ChecklistSerializer
from rest_framework.permissions import IsAuthenticated
from django.db.models import Q
from .permissions import CollectionShared, IsOwnerOrSharedWithFullAccess, IsPublicOrOwnerOrSharedWithFullAccess
@ -611,6 +611,24 @@ class ActivityTypesView(viewsets.ViewSet):
return Response(allTypes)
class CategoryViewSet(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'])
def categories(self, request):
"""
Retrieve a list of distinct categories for adventures associated with the current user.
Args:
request (HttpRequest): The HTTP request object.
Returns:
Response: A response containing a list of distinct categories.
"""
categories = Category.objects.filter(user_id=request.user.id).distinct()
serializer = CategorySerializer(categories, many=True)
return Response(serializer.data)
class TransportationViewSet(viewsets.ModelViewSet):
queryset = Transportation.objects.all()
serializer_class = TransportationSerializer