Как создать rest api с вложенными url в DRY-моде?
Я пытаюсь написать простое приложение для работы с Geocache. Доступ к бэкенду должен работать следующим образом:
Один объект геокэша содержит общую информацию (например, дату создания или уровень сложности), а также несколько инструкций, которые имеют фиксированный порядок (пример инструкции: Идите сначала к координатам lon/lat).
Общая структура URL имеет вид
.example.com/geocache/
Просмотр списка тайников (получить)example.com/geocache/<geocache_pk>/
Детальный вид для тайника (получить/поставить/поместить/удалить) (все инструкции должны быть коллективно отображены здесь, но ими нельзя манипулировать)example.com/geocache/<geocache_pk>/instruction/
Только для создания новых инструкций (разместить)example.com/geocache/<geocache_pk>/instruction/<instruction_position/>
Только для манипулирования/удаления инструкций (put/delete)
Я пытался реализовать эту структуру с помощью пользовательских действий с регулярными выражениями в url_path
, но я чувствую, что это недостаточно DRY. Я изучаю Django всего несколько дней, поэтому я могу упустить некоторые сложные паттерны.
Просьба также сообщить мне, имеет ли общий подход смысл для вас.
Спасибо за ваши усилия! Я очень ценю любые предложения, чтобы стать лучше.
models.py
from django.db import models
from django.db.models import F
from django.db.models.signals import pre_save, post_delete
from django.dispatch import receiver
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
class Geocache(models.Model):
class DifficultyLevel(models.TextChoices):
BEGINNER = 1, _('Beginner')
INTERMEDIATE = 2, _('Intermediate')
ADVANCED = 3, _('Advanced')
user_created = models.ForeignKey(User, related_name='geocaches_created', on_delete=models.CASCADE)
date_created = models.DateTimeField(auto_now_add=True)
user_last_modified = models.ForeignKey(User, related_name='geocaches_modified', on_delete=models.CASCADE)
date_last_modified = models.DateTimeField(auto_now=True)
title = models.CharField(max_length=70)
difficulty_level = models.IntegerField(choices=DifficultyLevel.choices)
def __str__(self):
return self.title
class GeocacheInstruction(models.Model):
class Meta:
ordering = ['geocache', 'position']
geocache = models.ForeignKey(Geocache, related_name='instructions', on_delete=models.CASCADE)
position = models.IntegerField()
loc_lon = models.DecimalField(max_digits=9, decimal_places=6)
loc_lat = models.DecimalField(max_digits=9, decimal_places=6)
title = models.CharField(max_length=70)
instruction = models.TextField()
def __str__(self):
return self.title
def is_saved(self):
return self.id is not None
@receiver(pre_save, sender=GeocacheInstruction)
def rearrange_geocache_instruction_positions_pre_save(sender, instance, *args, **kwargs):
"""
rearranges all positions before a new instruction gets inserted to maintain
a sequential ordering of this field
"""
# updating objects should not cause a reordering
if instance.is_saved():
return
geocaches = instance.geocache.instructions.filter(position__gte=instance.position)
geocaches.update(position=F('position')+1)
@receiver(post_delete, sender=GeocacheInstruction)
def rearrange_geocache_instruction_positions_post_delete(sender, instance, *args, **kwargs):
"""
rearranges all positions after an instruction was deleted to maintain
a sequential ordering of this field
"""
geocaches = instance.geocache.instructions.filter(position__gt=instance.position)
geocaches.update(position=F('position')-1)
serializers.py
from rest_framework import serializers
from geocaches.models import Geocache, GeocacheInstruction
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
class GeocacheInstructionSerializer(serializers.ModelSerializer):
class Meta:
model = GeocacheInstruction
fields = ['position', 'loc_lon', 'loc_lat', 'title', 'instruction']
def create(self, validated_data):
geocache = self.context.get('geocache')
GeocacheInstruction.objects.create(geocache=geocache, **validated_data)
return self
def validate(self, data):
"""
there should always be a sequential positioning therefore a new position
is only allowed in the range from 0 to [highest_position] + 1
"""
geocache = self.context.get('geocache')
upper_bound = geocache.instructions.count() + 1
if not (1 <= data['position'] <= upper_bound):
raise ValidationError(
_('The position %(position)s is not in the range from 1 - %(upper_bound)s.'),
params={'position': data['position'], 'upper_bound': upper_bound}
)
return data
class GeocacheListSerializer(serializers.ModelSerializer):
class Meta:
model = Geocache
fields = ['id', 'title', 'difficulty_level']
class GeocacheDetailSerializer(serializers.ModelSerializer):
user_created = serializers.ReadOnlyField(source='user_created.username')
user_last_modified = serializers.ReadOnlyField(source='user_last_modified.username')
instructions = GeocacheInstructionSerializer(many=True, read_only=True)
class Meta:
model = Geocache
fields = ['user_created', 'date_created', 'user_last_modified', 'date_last_modified', 'title',
'difficulty_level', 'instructions']
views.py
urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from geocaches import views
app_name = 'geocaches'
router = DefaultRouter()
router.register(r'geocache', views.GeocacheViewSet, basename='geocache')
urlpatterns = [
path('', include(router.urls)),
]
urlpatterns += [
path('api-auth/', include('rest_framework.urls')),
]