Как создать 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')),
]
Вернуться на верх