django условие агрегирования внешнего ключа по значению - PullRequest
1 голос
/ 24 февраля 2020

TL; DR Я хочу сделать длинный аннотированный запрос (в части диспетчера объектов проекта ниже) более элегантным и эффективным, и иметь возможность самостоятельно пересчитывать его после обновления связанного объекта.

Справочная информация Я строю рынок - каждый клиент должен иметь возможность создать проект, содержащий детали. Части являются объектами для ценовых предложений.

Каждая деталь содержит поле Enum статуса - чтобы мы могли знать, готов ли он к участию в торгах или уже в работе.

Im Using

Необходимая мне функциональность

  • Проекты должны фильтроваться по их содержанию состояние деталей - с указанием c условий.
  • Обновление статуса проекта должно обновлять все связанные детали.
  • Проекты можно создавать, обновлять и удалять с помощью одного и того же менеджера объектов.

Модель детали Только соответствующие части кода:

class PartStatuses(Enum):
    Draft = "Saved but not published"
    PendingBID = "It's BIDing time!"
    Proposal = "All BIDs are set"
    PendingPO = "Waiting for vendor to approve PO"
    WorkInProgress = "Vendor has accepted a PO"
    OnItsWay = "The part is ready and now await to be delivered"
    Delivered = "Delivery process has ended"
    Disputed = "Open for Dispute"
    Closed = "Part has received"
    Paid = "Vendor received the payment"

    @classmethod
    def choices(cls):
        return [(key.name, key.value) for key in cls]

class Part(models.Model):
    id = models.AutoField(primary_key=True)
    title = models.CharField(max_length=100)
    project = models.ForeignKey('Project', on_delete=models.CASCADE, related_name='part')
    status = models.CharField(choices=PartStatuses.choices(),
                              max_length=100,
                              default=PartStatuses.Draft)
    ...

Модель проекта Сначала я попытался использовать метод свойства для статуса проекта поле и метод установки для обновления состояния деталей.

class Project(models.Model):
    id = models.AutoField(primary_key=True)
    owner = models.ForeignKey(User, on_delete=models.CASCADE, editable=False,
                              limit_choices_to={'is_vendor': False})
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    ...

    def got_parts(self):
        return self.part.count()

    @property
    def _status(self):
        if self.got_parts(): 
            all_parts: object = self.part.values_list('status')

            # Project is done and paid
            if all_parts.filter(status=PartStatuses.Paid.value).count() == all_parts.count():
                return str(PartStatuses.Paid.name)

            # At least one parts are open for disputed
            if all_parts.filter(status=PartStatuses.Disputed.value).count() > 0:
                return str(PartStatuses.Disputed.name)

            # Some parts are draft
            if all_parts.filter(status=PartStatuses.Draft.value).count() > 0:
                return str(PartStatuses.Draft.name)

            # Some parts are pending to bid
            if all_parts.filter(status=PartStatuses.PendingBID.value).count() > 0:
                return str(PartStatuses.PendingBID.name)

            # Some parts are on proposal
            if all_parts.filter(status=PartStatuses.Proposal.value).count() > 0:
                return str(PartStatuses.Proposal.name)

            # Some parts are pending to vendor approve PO
            if all_parts.filter(status=PartStatuses.PendingPO.value).count() > 0:
                return str(PartStatuses.PendingPO.name)

            # Some parts are in progress of working
            if all_parts.filter(status=PartStatuses.WorkInProgress.value).count() > 0:
                return str(PartStatuses.WorkInProgress.name)

            # Some parts are on their way
            if all_parts.filter(status=PartStatuses.OnItsWay.value).count() > 0:
                return str(PartStatuses.OnItsWay.name)

            # Some parts has been marked by delivery guys as delivered
            if all_parts.filter(status=PartStatuses.Delivered.value).count() > 0:
                return str(PartStatuses.Delivered.name)

            # Some parts has been marked by customer as delivered
            if all_parts.filter(status=PartStatuses.Closed.value).count() > 0:
                return str(PartStatuses.Closed.name)

            # Error with the parts status mapping
            return "Other"

        else:  # No parts - this projects is draft
            return str(PartStatuses.Draft.name)

    @_status.setter
    def _status(self, status):
        if any(k[0] == status for k in PartStatuses.choices()) and self.got_parts():
            for part in self.part.all():
                part.status = PartStatuses[status].value
                part.save()

    def __str__(self):
        return self.title

Вскоре я понял, что нет возможности запросить это свойство как поле, цитата из ом документы :

Поле, указанное в поиске, должно быть именем поля модели

Поэтому мне пришлось добавить свой собственный Диспетчер пользовательских объектов с тем же условием, что и у свойства модели состояния:

class ProjectManager(models.Manager):
    """QuerySet manager for Project class to add non-database fields."""

    def get_queryset(self):
        """Overrides the models.Manager method"""
        qs = super().get_queryset().annotate(
            parts_num=Count(F('part'), distinct=True),
            parts_paid=Count(F('part'), filter=Q(part__status__exact=PartStatuses.Paid.value), distinct=True),
            parts_disputed=Count(F('part'), filter=Q(part__status__exact=PartStatuses.Disputed.value), distinct=True),
            parts_draft=Count(F('part'), filter=Q(part__status__exact=PartStatuses.Draft.value), distinct=True),
            parts_pending_bid=Count(F('part'), filter=Q(part__status__exact=PartStatuses.PendingBID.value), distinct=True),
            parts_proposal=Count(F('part'), filter=Q(part__status__exact=PartStatuses.Proposal.value), distinct=True),
            parts_workin_progress=Count(F('part'), filter=Q(part__status__exact=PartStatuses.WorkInProgress.value),
                                        distinct=True),
            parts_pending_PO=Count(F('part'), filter=Q(part__status__exact=PartStatuses.PendingPO.value), distinct=True),
            parts_on_its_way=Count(F('part'), filter=Q(part__status__exact=PartStatuses.OnItsWay.value), distinct=True),
            parts_delivered=Count(F('part'), filter=Q(part__status__exact=PartStatuses.Delivered.value), distinct=True),
            parts_closed=Count(F('part'), filter=Q(part__status__exact=PartStatuses.Closed.value), distinct=True),
        ).annotate(
            status=Case(
                When(parts_num=0, then=Value(PartStatuses.Draft.name)),
                When(parts_paid=F('parts_num'), then=Value(PartStatuses.Paid.name)),
                When(parts_disputed__gt=0, then=Value(PartStatuses.Disputed.name)),
                When(parts_draft__gt=0, then=Value(PartStatuses.Draft.name)),
                When(parts_pending_bid__gt=0, then=Value(PartStatuses.PendingBID.name)),
                When(parts_proposal=F('parts_num'), then=Value(PartStatuses.Proposal.name)),
                When(parts_workin_progress__gt=0, then=Value(PartStatuses.WorkInProgress.name)),
                When(parts_pending_PO__gt=0, then=Value(PartStatuses.PendingPO.name)),
                When(parts_on_its_way__gt=0, then=Value(PartStatuses.OnItsWay.name)),
                When(parts_delivered__gt=0, then=Value(PartStatuses.Delivered.name)),
                When(parts_closed__gt=0, then=Value(PartStatuses.Closed.name)),
                default=Value("Other"),
                output_field=CharField()
            )
        )
        return qs

Добавлен менеджер в модель проекта:

class Project(models.Model):
    with_status = ProjectManager()
    objects = models.Manager()

    id = models.AutoField(primary_key=True)
    owner = models.ForeignKey(User, on_delete=models.CASCADE, editable=False,
                              limit_choices_to={'is_vendor': False})
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    title = models.CharField(max_length=100)
    ...

И он используется в моем Представлении проекта

from url_filter.integrations.drf import DjangoFilterBackend
from rest_framework import viewsets

class ProjectsViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows projects to be viewed or edited.
    """
    serializer_class = ProjectSerializer
    permission_classes = (IsAuthenticated, IsVendorStaffOrOwner)
    queryset = Project.with_status.prefetch_related('part').all()
    filter_backends = [DjangoFilterBackend]
    filter_fields = ('status',)

Но я получил ошибку: django.core.exceptions.FieldDoesNotExist: Project has no field named 'status', поэтому я попытался обойти ее и написал свой собственный класс фильтра:

from url_filter.filtersets import FilterSet
from django.db.models import Q

class ProjectsDynamicFilters(FilterSet):
    def filter(self):
        all_filters = Q()
        if 'status' in self.data:
            all_filters &= Q(status=self.data['status'])

        if len(all_filters):
            return self.queryset.filter(all_filters).distinct()
        else:
            return self.queryset

и использовал его вместо мое свойство filter_field в представлении проекта: filter_class = ProjectsDynamicFilters.

На этом этапе примите очень длинный и неуклюжий аннотированный запрос в диспетчере объектов проекта, я не смог обновить или создать объекты, потому что мой проект Сериализатор имеет поле состояния внутри, и создание или обновление не может просто вернуть экземпляр.

Функция обновления, в частности, не производит пересчет поля состояния после обновления связанных объектов деталей.

Project Serializer

class ProjectSerializer(serializers.HyperlinkedModelSerializer):
    owner = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(),
                                               default=serializers.CurrentUserDefault())
    created_at = serializers.DateTimeField(format="%d-%m-%Y %H:%M", read_only=True)
    updated_at = serializers.DateTimeField(format="%d-%m-%Y %H:%M", read_only=True)

    title = serializers.CharField()
    description = serializers.CharField()
    status = serializers.ChoiceField(choices=PartStatuses.choices(), default=PartStatuses.Draft.name)
    parts = NestedHyperlinkedRelatedField(many=True, source="part", parent_lookup_kwargs={'project_pk': 'project__pk'},
                                          view_name="parts-detail", read_only=True)
    ...

    def update(self, instance, validated_data):
        for data in validated_data:
            if data == "status":
                instance._status = validated_data.get(data)
            else:
                setattr(instance, data, validated_data.get(data))
        instance.save()
        return Project.with_status.get(pk=instance.id)

    def create(self, validated_data):
        validated_data.pop('status', None)
        project = Project.objects.create(**validated_data)
        return Project.with_status.get(pk=project.id)

Вернуться к вопросы

  1. Есть ли способ сделать проект with_status запрос менеджера более элегантным и эффективным?
  2. На данный момент я сохраняю только свойство модели _status для функции setter, которую я использую, чтобы обновить проект со статусом. Есть ли способ сделать эту функцию доступной на уровне модели без повторного расчета?
  3. После изменения статуса детали поле статуса проекта должно быть пересчитано заново. Я обошел его, вернув Project.with_status.get(pk=instance.id) в конце метода сериализатора обновлений. Есть ли более мудрый способ сделать это?

1 Ответ

1 голос
/ 16 марта 2020

Вы можете изменить свой ENUM:

class PartStatuses(Enum):
    Draft = 0, _("Saved but not published")
    PendingBID = 1, _("It's BIDing time!")
    Proposal = 2, _("All BIDs are set")
    PendingPO = 3, _("Waiting for vendor to approve PO")
    WorkInProgress = 4, _("Vendor has accepted a PO")
    OnItsWay = 5, _("The part is ready and now await to be delivered")
    Delivered = 6, _("Delivery process has ended")
    Disputed = 7, _("Open for Dispute")
    Closed = 8, _("Part has received")
    Paid = 9, _("Vendor received the payment")

Таким образом, вы можете сохранить значения Enum в качестве метки и использовать функцию Min() в аннотированном запросе менеджера проекта.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...