如何使用Django Rest Framework在嵌套数据中交换列表中的两个项目

问题描述 投票:0回答:1

我试图在列表中交换两个项目,并想知道最好的方法。我能找到的唯一解决方案是不保留项目的pk,这是一个问题,因为其他对象依赖于它。

我正在使用Django 2.0.10和Django Rest Framework。

我有嵌套数据,其中列表包含有限数量的项目。

每个项目都有一个订单,它是一个整数,在该列表中必须是唯一的,并且每个列表只能有固定数量的值。

假设所有列表始终具有其最大项目数。

我想允许用户在列表中上下移动项目,这意味着交换两个项目。最简单的方法是修改每个项目的“order”属性,但鉴于所有有效的订单值都已被使用,我无法看到如何执行此操作。我不能给第1项订单2并保存它,因为已经存在第2项。并且在交换操作期间没有我可以分配的临时值。

所以,我正在做的是:

  1. 创建每个项目的深层副本
  2. 将新订单分配给每个副本
  3. 删除两个原始项目
  4. 将每个副本的pk设置为None
  5. copy_1.save()和copy_2.save()来创建新项目

这有效,但当然每个新对象都有与原始对象不同的主键。我的项目有一个slug,这意味着我仍然可以识别原始项目并链接到它,但Item的子对象现在已经丢失了它们的引用。

这似乎是其他人过去会做的事情。

有没有办法在创建对象后更新pk而不允许pk被其他操作编辑,或者保存具有新订单值的项目并避免冲突?

我想我可以通过数据库搜索任何其他引用已被删除/替换的项目的对象,但这只是一个丑陋的解决方案,只需要更改两个数字!

非常感谢任何建议!

这是我的代码:

models.朋友

"""Models for lists, items
    """
import uuid

from django.db import models
from django.utils.http import int_to_base36
from django.core.validators import MaxValueValidator, MinValueValidator
from django.contrib.auth import get_user_model

ID_LENGTH = 12
USER = get_user_model()

def slug_gen():
    """Generates a probably unique string that can be used as a slug when routing

    Starts with a uuid, encodes it to base 36 and shortens it
    """

    #from base64 import b32encode
    #from hashlib import sha1
    #from random import random

    slug = int_to_base36(uuid.uuid4().int)[:ID_LENGTH]
    return slug

class List(models.Model):
    """Models for lists
    """
    slug = models.CharField(max_length=ID_LENGTH, default=slug_gen, editable=False)
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    created_by_id = models.ForeignKey(USER, on_delete=models.CASCADE, related_name='list_created_by_id')
    created_by_username = models.CharField(max_length=255) # this shold be OK given that the list will be deleted if the created_by_id user is deleted
    created_at = models.DateTimeField(auto_now_add=True)
    parent_item = models.ForeignKey('Item', on_delete=models.SET_NULL, null=True, related_name='lists')
    modified_by = models.ForeignKey(USER, on_delete=models.SET_NULL, null=True,
        related_name='list_modified_by')
    modified_at = models.DateTimeField(auto_now_add=True)
    name = models.CharField(max_length=255)
    description = models.CharField(max_length=5000, blank=True, default='')
    is_public = models.BooleanField(default=False)

    def __str__(self):
        return self.name


class Item(models.Model):
    """Models for list items
    """
    slug = models.CharField(max_length=ID_LENGTH, default=slug_gen, editable=False)
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    modified_at = models.DateTimeField(auto_now_add=True)
    name = models.CharField(max_length=255, blank=True, default='')
    description = models.CharField(max_length=5000, blank=True, default='')
    list = models.ForeignKey(List, on_delete=models.CASCADE, related_name='items')
    order = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(10)])

    class Meta:
        unique_together = ('list', 'order')
        ordering = ['order']

    def __unicode__(self):
        return '%d: %s' % (self.order, self.name)

从api.py中提取:

@detail_route(methods=['post'])
    def moveup(self, request, pk=None):

        if self.request.user.is_authenticated:
            # find the item to move up
            item = Item.objects.get(pk=pk)

            item_order = item.order
            parent_list = item.list

            if item.order == 1:
                return Response({'message': 'Item is already at top of list'}, status=status.HTTP_403_FORBIDDEN)

            item_copy = copy.deepcopy(item)

            # find the item above with which to swap the first item
            item_above = Item.objects.get(list=parent_list, order=item_order-1)
            item_above_copy = copy.deepcopy(item_above)

            # swap the order on the item copies
            item_copy.order = item_order-1
            item_above_copy.order = item_order

            # set pk to None so save() will create new objects
            item_copy.pk = None
            item_above_copy.pk = None

            # delete the original items to free up the order values for the new items
            item.delete()
            item_above.delete()

            # 
            item_copy.save()
            item_above_copy.save()

            return Response({'message': 'Item moved up'}, status=status.HTTP_200_OK)

        return Response(status=status.HTTP_401_UNAUTHORIZED)
python django django-rest-framework swap
1个回答
0
投票

最后,我删除了unique_together约束,这似乎与更改项目顺序不兼容。这是一种耻辱,因为乍看之下的约束似乎非常有用并且在example in the docs中,但我认为在实践中你需要重新排序项目的选项。

如果没有约束,您只需更改每个项目的顺序并保存即可,但是您需要手动确保每个项目在列表中都有唯一的顺序。

我添加了一个自定义更新方法,我认为这可以防止任何普通操作编辑订单。我认为这是安全的,但我感觉不如我可以使用数据库约束。

这是我的工作代码。

serialize认识.朋友

class ItemSerializer(serializers.ModelSerializer):
    """
    An item must belong to a list
    """
    class Meta:
        model = Item
        fields = ('id', 'name', 'description', 'list_id', 'modified_at', 'order', 'slug')
        # note 'list_id' is the field that can be returned, even though 'list' is the actual foreign key in the model

models.朋友

"""Models for lists, items
    """
import uuid

from django.db import models
from django.utils.http import int_to_base36
from django.core.validators import MaxValueValidator, MinValueValidator
from django.contrib.auth import get_user_model

ID_LENGTH = 12
USER = get_user_model()

def slug_gen():
    """Generates a probably unique string that can be used as a slug when routing

    Starts with a uuid, encodes it to base 36 and shortens it
    """

    #from base64 import b32encode
    #from hashlib import sha1
    #from random import random

    slug = int_to_base36(uuid.uuid4().int)[:ID_LENGTH]
    return slug

class List(models.Model):
    """Models for lists
    """
    slug = models.CharField(max_length=ID_LENGTH, default=slug_gen, editable=False)
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    created_by_id = models.ForeignKey(USER, on_delete=models.CASCADE, related_name='list_created_by_id')
    created_by_username = models.CharField(max_length=255) # this shold be OK given that the list will be deleted if the created_by_id user is deleted
    created_at = models.DateTimeField(auto_now_add=True)
    parent_item = models.ForeignKey('Item', on_delete=models.SET_NULL, null=True, related_name='lists')
    modified_by = models.ForeignKey(USER, on_delete=models.SET_NULL, null=True,
        related_name='list_modified_by')
    modified_at = models.DateTimeField(auto_now_add=True)
    name = models.CharField(max_length=255)
    description = models.CharField(max_length=5000, blank=True, default='')
    is_public = models.BooleanField(default=False)

    def __str__(self):
        return self.name


class Item(models.Model):
    """Models for list items
    """
    slug = models.CharField(max_length=ID_LENGTH, default=slug_gen, editable=False)
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    modified_at = models.DateTimeField(auto_now_add=True)
    name = models.CharField(max_length=255, blank=True, default='')
    description = models.CharField(max_length=5000, blank=True, default='')
    list = models.ForeignKey(List, on_delete=models.CASCADE, related_name='items')
    order = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(10)])

    class Meta:
        # unique_together = ('list', 'order') # prevents items from being swapped because deferred is not available in mysql
        ordering = ['order']

    def __unicode__(self):
        return '%d: %s' % (self.order, self.name)

API.朋友

from rest_framework import viewsets, permissions
from rest_framework.decorators import detail_route
from rest_framework import status
from rest_framework.response import Response
from rest_framework.exceptions import APIException

from .models import List, Item
from .serializers import ListSerializer, ItemSerializer
from django.db.models import Q

class ItemViewSet(viewsets.ModelViewSet):
    permission_classes = [IsOwnerOrReadOnly, HasVerifiedEmail]
    model = Item
    serializer_class = ItemSerializer

    def get_queryset(self):
        # can view items belonging to public lists and lists the user created
        if self.request.user.is_authenticated:
            return Item.objects.filter(
                Q(list__created_by_id=self.request.user) | 
                Q(list__is_public=True)
            )

        return Item.objects.filter(list__is_public=True)

    @detail_route(methods=['patch'])
    def moveup(self, request, pk=None):

        if self.request.user.is_authenticated:
            # find the item to move up
            item = Item.objects.get(pk=pk)       
            item_order = item.order
            parent_list = item.list_id # note 'list_id' not 'list'

            if item.order == 1:
                return Response({'message': 'Item is already at top of list'}, status=status.HTTP_403_FORBIDDEN)

            # change the item order up one
            item.order = item.order - 1

            # find the existing item above
            item_above = Item.objects.get(list=parent_list, order=item_order-1)
            # and change its order down one
            item_above.order = item_order

            item.save()
            item_above.save()

            # return the new items so the UI can update
            items = [item, item_above]

            serializer = ItemSerializer(items, many=True)

            return Response(serializer.data, status=status.HTTP_200_OK)

        return Response(status=status.HTTP_401_UNAUTHORIZED)

    def perform_update(self, serializer):
        # do not allow order to be changed
        if serializer.validated_data.get('order', None) is not None:
            raise APIException("Item order may not be changed. Use moveup instead.")

        serializer.save()
© www.soinside.com 2019 - 2024. All rights reserved.