Grant Grant - 1 year ago 134
Python Question

queryset value for SlugRelatedField when unique_together applies in django-rest

I'm building a simple API for an ESP8266 to connect to in an IoT application, passing a JSON string. In this application there are multiple Monitors (internet connected devices) per Site (location/address), and multiple LogEntries per Site/Monitor.

The API was originally setup with an endpoint like:

Posting a JSON string like:

In the object model, Monitor is a child of Site, but for convenience of entry creation and reporting, the JSON format of the LogEntry posted by each device flattens this structure out, meaning that the LogEntry model also has a FK relationship for both Site and Monitor. In the code below, "textID" is the ID used within the context of the API for the Site/Monitor (e.g. PK values remain "hidden" for API callers).


class Site(models.Model):
name = models.CharField(max_length=32)
textID = models.CharField(max_length=32, blank=True, db_index=True, unique=True)

class Monitor(models.Model):
textID = models.CharField(max_length=32)
site = models.ForeignKey(Site, on_delete=models.CASCADE)

class Meta:
unique_together = ('site', 'textID')

class LogEntry(models.Model):
site = models.ForeignKey(Site, on_delete=models.CASCADE)
monitor = models.ForeignKey(Monitor, on_delete=models.CASCADE)
data_point = models.CharField(max_length=8, default='')

To get this to work on a single site, I created a custom serializer:

class LogEntrySerializer(serializers.HyperlinkedModelSerializer):
site = serializers.SlugRelatedField(slug_field='textID', queryset=Site.objects.all())
monitor = serializers.SlugRelatedField(slug_field='textID', queryset=Monitor.objects.filter())

class Meta:
model = LogEntry
fields = ('pk', 'site', 'monitor', 'data_point', )

This works for reading valid data, and saving when all monitor IDs are unique across sites.

However, if two sites have a Monitor with the same textID—e.g. "Site1/001" and "Site2/001" this breaks, as the Monitor.objects.all() results in multiple records being retrieved (which makes sense and is expected behaviour).

What I'm wanting to do is to have the second queryset (for monitor) limited to the specified site, to avoid this error.

This post almost answers my question, however it benefits from the second field value (user) being available in the request object, something that is not available in this case.

Is there a way I can retrieve the or Site.textID for the queryset value to resolve correctly--e.g. queryset=Monitor.objects.filter(site__textID=xxx)--what would 'xxx' be? Or do I need to completely override the serializer (and not rely on SlugRelatedField)? Or some other approach that might work?

(As an aside: I recognise that this could be achieved by modifying the URL pattern to something like /api///logentries, which would then have this information available as part of the request/context and from a normalisation perspective would be better also. However this would require reflashing of a number of already deployed devices to reflect the changed API details, so I'd like to avoid such a change if possible, even though upon reflection this is probably a cleaner solution/approach long-term.)

Thanks in advance.

Answer Source

You'll need to write your own SlugRelatedField subclass. The unicity constraint that applies to a SlugRelatedField doesn't apply to your case. This can be done by creating a subfield and overriding the get_value to retrieve the site/monitor tuple and to_internal_value to select the appropriate monitor.