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