0

I have a DRF API with a model of months, I need to provide a way to make one endpoint for creating or updating objects (so either post or patch endpoint).

I tried taking example from these two answers:

Multiple Lookup fields answer

overriding view's create method for update or create

The second answer is using the view and not the serializer and that's a behaviour I would rather keep.

My code so far:

class MultipleFieldLookupMixin:
    def get_object(self):
        queryset = self.get_queryset()
        queryset = self.filter_queryset(queryset)
        filter = {}
        for field in self.lookup_fields:
            if self.kwargs.get(field, None):
                filter['field'] = self.kwargs['field']
        obj = get_object_or_404(queryset, **filter)
        self.check_object_permissions(self.request, obj)
        return obj

class MonthsViewSet(ModelViewSet, MultipleFieldLookupMixin):
    authentication_classes = (TokenAuthentication,)

    def get_queryset(self):
        query_set = Month.objects.all() 
        return query_set

    serializer_class = MonthSerializer
    lookup_fields = ['month', 'year', 'farm']

    def create(self, request, *args, **kwargs):
        kwarg_field: str = self.lookup_url_kwarg or self.lookup_fields
        self.kwargs[kwarg_field] = request.data[self.lookup_fields]
        try:
            return self.update(request, *args, **kwargs)
        except Month.DoesNotExist:
            return super().create(request, *args, **kwargs)

serializer:

class MonthSerializer(serializers.ModelSerializer):
    class Meta:
        model = Month
        fields = '__all__'

So now trying to create or update a field raises the following error:

    self.kwargs[kwarg_field] = request.data[self.lookup_fields]
TypeError: unhashable type: 'list'

EDIT I've changed the create method to this:

  def create(self, request, *args, **kwargs):
        request_month = request.data['month']
        year = request.data['year']
        farm = request.data['farm']
        days = request.data['days']
        month, created = Month.objects.update_or_create(month=request_month, year=year, farm_id=farm,
                                                        defaults={'month': request_month, 'year': year,
                                                                  'farm_id': farm, 'days': days})
        if not created:
            serializer = MonthSerializer(month)
            serializer.is_valid(raise_exception=True)
            return Response(serializer.data, status=HTTP_200_OK)
        elif not month:
            serializer = MonthSerializer(created)
            serializer.is_valid(raise_exception=True)
            return Response(serializer.data, status=HTTP_200_OK)

My model has a unique toghther constraint:

class Month(models.Model):
    month = models.IntegerField(null=False)
    year = models.IntegerField(null=False)
    date = models.DateField(null=True, blank=True, default=None)
    days = models.DurationField(default=0, )
    farm = models.ForeignKey(to=Farm, on_delete=models.CASCADE, related_name='months')

    objects = models.Manager()

    def save(self, *args, **kwargs):
        self.date = datetime.date(self.year, self.month, 1)
        super(Month, self).save(*args, **kwargs)

    def __str__(self):
        return f'{self.month},{self.year}'

    class Meta:
        unique_together = ('farm', 'date')

but for some reason, it's not updating the object but tries to create it which then raises a constraint violation. Which raises this error now:

tegrityError at /farm_api/months/
duplicate key value violates unique constraint "farm_api_month_farm_id_date_9d276785_uniq"
DETAIL:  Key (farm_id, date)=(1, 2020-08-01) already exists.
  • As the error said, you're passing self.lookup_fields which is a list to the dictionary index. If you want to retrieve the value of each field in lookup_fields you can try `dict([k, request.data[v] for k in self.lookup_fields])` – Ken4scholars Jul 28 '20 at 10:01
  • @Ken4scholars this raises self.kwargs[kwarg_field] = {[(k, request.data[v]) for k, v in self.lookup_fields]} ValueError: too many values to unpack (expected 2) –  Jul 28 '20 at 10:37
  • Because lookup_fields is a list and not a dict. Please look again at what I sent above. I only have the keys k. Should be `request.data[k]` – Ken4scholars Jul 28 '20 at 10:41
  • @Ken4scholars I didn't quite understand how to use your code but I've made a change, will edit. –  Jul 28 '20 at 15:54

1 Answers1

0

As the django documentation suggest you should use UniqueConstraint instead of unique_together. So the Meta class of you model should be:

class Meta:
    constraints = [
        models.UniqueConstraint(fields=['farm', 'date'], name="farm-date")
    ]

Then you can try rework your create function like this in order to handle both update or create:

def create(self, request, *args, **kwargs):
    queryset = self.filter_queryset(self.get_queryset())
    pk = queryset.model._meta.pk.name
    for c in queryset.model._meta.constraints: # -------------CHECKING FOR MULTIPLE PK------
        if isinstance(c, UniqueConstraint):
            pk = list(c.fields)
            break
    instance = queryset.get(**{i: request.data.get(i) for i in pk})
    if instance:  # ------------------------------------------------------------UPDATE------
        partial = kwargs.pop('partial', False)
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)
        return Response(serializer.data)
    else:  # ---------------------------------------------------------------------CREATE------
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

P.D. I'm using the UniqueConstraint fields to get the instance of the model, as those are only the necessary fields to make the query.