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:
parent
c3f37b66d0
commit
4a7f720773
8 changed files with 205 additions and 11 deletions
|
@ -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'
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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
|
|
@ -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):
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue