我正在尝试编写一个简单的 Geocache 应用程序。后端访问应该如下工作:
一个 Geocache 对象包含一般信息(如创建日期或难度级别),但也包含几个指令,它们具有固定的顺序(示例指令:先去坐标 lon/lat)。
一般的 URL 结构是
example.com/geocache/
地理藏宝的列表视图(获取)example.com/geocache/<geocache_pk>/
geocache 的详细视图(get/post/put/delete)(所有指令应在此处集中显示,但不能在此处操作)example.com/geocache/<geocache_pk>/instruction/
仅用于创建新指令(帖子)example.com/geocache/<geocache_pk>/instruction/<instruction_position/>
仅用于指令操作/删除(put/delete)
我试图通过自定义操作和正则表达式来完成这个结构,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')),
]