首页 > 解决方案 > 如何以 DRY 方式创建带有嵌套 url 的 rest api?

问题描述

我正在尝试编写一个简单的 Geocache 应用程序。后端访问应该如下工作:

我试图通过自定义操作和正则表达式来完成这个结构,url_path但我觉得它不够 DRY。我只学习 Django 几天,所以我可能会遗漏一些复杂的模式。

如果一般方法对您有意义,也请告诉我。

感谢你的付出!我真的很感激任何让我变得更好的建议。

模型.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)

序列化程序.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']

视图.py

from rest_framework import viewsets, permissions, mixins, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from geocaches.serializers import GeocacheDetailSerializer, GeocacheListSerializer, GeocacheInstructionSerializer
from geocaches.models import Geocache, GeocacheInstruction


class GeocacheViewSet(viewsets.ModelViewSet):

    serializer_class = GeocacheDetailSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

    def get_serializer_class(self):
        if self.action == 'list':
            return GeocacheListSerializer
        elif self.action == 'instruction_list':
            return GeocacheInstructionSerializer
        elif self.action == 'instruction_detail':
            return GeocacheInstructionSerializer
        else:
            return GeocacheDetailSerializer

    def get_queryset(self):
        if self.action == 'list':
            # Todo geodjango
            pass
        return Geocache.objects.all()

    @action(detail=True, url_path='instruction', url_name='instruction-list', methods=['post', 'get'])
    def instruction_list(self, request, pk):
        geocache = self.get_object()
        instructions = geocache.instructions.all()

        if request.method == 'GET':
            serializer = GeocacheInstructionSerializer(instructions, many=True)
            return Response(serializer.data)

        elif request.method == 'POST':
            serializer = GeocacheInstructionSerializer(data=request.data, context={'geocache': geocache})

            if serializer.is_valid():
                serializer.save()
                return Response({'status': 'Instruction created'}, status=status.HTTP_201_CREATED)
            else:
                return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    @action(detail=True,  url_name='instruction-detail', url_path='instruction/(?P<position>[^/.]+)',
            methods=['get', 'put', 'delete'])
    def instruction_detail(self, request, pk, position):
        geocache = self.get_object()
        instruction = get_object_or_404(geocache.instructions, position=position)

        if request.method == 'GET':
            serializer = GeocacheInstructionSerializer(instruction)
            return Response(serializer.data)

        elif request.method == 'PUT':
            serializer = GeocacheInstructionSerializer(instruction, data=request.data, context={'geocache': geocache})

            if serializer.is_valid():
                serializer.save()
                return Response({'status': 'Instruction altered'}, status=status.HTTP_200_OK)
            else:
                return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

        elif request.method == 'DELETE':
            instruction.delete()
            return Response({'status': 'Instruction deleted'}, status=status.HTTP_204_NO_CONTENT)

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

    def perform_update(self, serializer):
        serializer.save(user_last_modified=self.request.user)

网址.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')),
]

标签: djangodjango-rest-frameworkdjango-viewsdjango-rest-viewsetsdrf-nested-routers

解决方案


推荐阅读