Using Django's CheckConstraint with annotations - django

I have a Django model where each instance requires a unique identifier that is derived from three fields:
class Example(Model):
type = CharField(blank=False, null=False) # either 'A' or 'B'
timestamp = DateTimeField(default=timezone.now)
number = models.IntegerField(null=True) # a sequential number
This produces a label of the form [type][timestamp YEAR][number], which must be unique unless number is null.
I thought I might be able to use a couple of annotations:
uid_expr = Case(
When(
number=None,
then=Value(None),
),
default=Concat(
'type', ExtractYear('timestamp'), 'number',
output_field=models.CharField()
),
output_field=models.CharField()
)
uid_count_expr = Count('uid', distinct=True)
I overrode the model's manager's get_queryset to apply the annotations by default and then tried to use CheckConstraint:
class Example(Model):
...
class Meta:
constraints = [
models.CheckConstraint(check=Q(uid_cnt=1), name='unique_uid')
]
This fails because it's unable to find a field on the instance called uid_cnt, however I thought annotations were accessible to Q objects. It looks like CheckConstraint queries against the model directly rather than using the queryset returned by the manager:
class CheckConstraint(BaseConstraint):
...
def _get_check_sql(self, model, schema_editor):
query = Query(model=model)
...
Is there a way to apply a constraint to an annotation? Or is there a better approach?
I'd really like to enforce this at the db layer.
Thanks.

This is pseudo-code, but try:
class Example(Model):
...
class Meta:
constraints = [
models.UniqueConstraint(
fields=['type', 'timestamp__year', 'number'],
condition=Q(number__isnull=False),
name='unique_uid'
)
]

Related

DB constraints vs. clean() method in Django

After creating a clean() method to avoid overlapping date ranges in an admin form, I added an ExclusionContraint to ensure integrity at the DB level, too:
class DateRangeFunc(models.Func):
function = 'daterange'
output_field = DateRangeField()
class Occupancy(models.Model):
unit = models.ForeignKey(Unit, on_delete=models.CASCADE)
number_of = models.IntegerField()
begin = models.DateField()
end = models.DateField(default=datetime.date(9999,12,31))
class Meta:
constraints = [
ExclusionConstraint(
name="exclude_overlapping_occupancies",
expressions=(
(
DateRangeFunc(
"begin", "end", RangeBoundary(inclusive_lower=True, inclusive_upper=True)
),
RangeOperators.OVERLAPS,
),
("unit", RangeOperators.EQUAL),
),
),
]
This constraint works as expected, but it seems to precede clean(), because any overlap raises an IntegrityError for the admin form. I would have expected that clean() is called first.
I have two questions (related, but not identical to this question):
Is there any way to change the order of evaluation (clean() β†’ ExclusionConstraint)?
Which method (save()?) would I need to override to catch the IntegrityError raised by the constraint?
[Django 4.1.5/Python 3.11.1/PostgreSQL 14.6]

How can I use a related field in a SlugRelatedField?

I have the following structures
class State(models.Model):
label = models.CharField(max_length=128)
....
class ReviewState(models.Model):
state = models.ForeignKey(State, on_delete=models.CASCADE)
...
class MySerializer(serializers.HyperlinkedModelSerializer):
state = serializers.SlugRelatedField(queryset=ReviewState.objects.all(), slug_field='state__label', required=False)
class Meta:
model = MyModel
fields = [
'id',
'state', # this points to a ReviewState object
....
]
What I'm trying to do is using the State object's label as the field instead. But it doesn't seem like djangorestframework likes the idea of using __ to lookup slug fields. Would it be possible to do this? If it was:
class MySerializer(serializers.HyperlinkedModelSerializer):
state = serializers.SlugRelatedField(queryset=State.objects.all(), slug_field='label', required=False)
that would be no problem, but I'm trying to use the ReviewState instead. I'm also trying to avoid having a ReviewStateSerializer as the resulting json would look like this
{...
'state': {'state': 'Pending'}}
}
Interesting question, and well put.
Using SlugRelatedField('state__label', queryset=...) works fine, with 1 caveat: its just calling queryset.get(state__label="x") which errors if there isn't exactly 1 match.
1) Write a custom field?
Inherit from SlugRelatedField and override to_internal_value(), maybe by calling .first() instead of .get(), or whatever other logic you need.
2) Re-evaluate this relationship, maybe its 1-to-1? a choice field?
I'm a bit confused on how this would all work, since you can have a "1 to many" with State => ReviewState. The default lookup (if you don't do #1) will throw an error when multiple matches occur.
Maybe this is a 1-to-1 situation with the model? Perhaps the ReviewState can use a ChoiceField instead of a table of states?
Perhaps the 'label' can be the PK of the State table, and also a SlugField rather than a non-unique CharField?
3) Write different serializers for the List and Create cases
DRF doesn't give us a built-in way to do this, but this reliance on "one serializer to do it all" is the cause of a lot of problems I see on SO. Its just really hard to get what you want without having different serializers for different cases. It's not hard to roll-your-own mixin to do it, but here's an example which uses an override:
from rest_framework import serializers as s
class MyCreateSerializer(s.ModelSerializer):
state = s.SlugRelatedField(...)
...
class MyListSerializer(s.ModelSerializer):
# use dotted notation, serializers read *object* attributes
state = s.CharField(source="state.state.label")
...
class MyViewSet(ModelViewSet):
queryset = MyModel.objects.select_related('state__state')
...
def get_serializer_class(self):
if self.action == "create":
return MyCreateSerializer
else:
return MyListSerializer

use django_filters to filter for multiple arguments

I am using Relay, Django, Graphene Graphql.
I would like to use django_filters to filter for multiple arguments of type on accommodation. This is described in my schema file and atm looks like:
class AccommodationNode(DjangoObjectType) :
class Meta:
model = Accommodation
interfaces = (relay.Node,)
filter_fields = ['type']
This works perfectly if I pass a single string like: {"accommodationType": "apartment"}, but what if I want to filter for all accommodations that are apartments OR hotels? something like: {"accommodationType": ["apartment","hotel"]}
This is my model:
class Accommodation(models.Model):
ACCOMMODATION_TYPE_CHOICES = (
('apartment', 'Apartment'),
('host_family', 'Host Family'),
('residence', 'Residence'),
)
school = models.ForeignKey(School, on_delete=models.CASCADE, related_name='accommodations')
type = models.CharField(
max_length=200,
choices=ACCOMMODATION_TYPE_CHOICES,
default='apartment'
)
def __str__(self):
return str(self.school) + " - " + self.type
Is there any way I can do this without writing custom filters as are suggested here? For only one filter field this is a great solution but I'll end up having around 50 throughout my application including linked objects...
Have a look at Django REST Framework Filters:
https://github.com/philipn/django-rest-framework-filters
It supports more than exact matches, like in, which you are looking for, but also exact, startswith, and many more, in the same style of Django's ORM. I use it frequently and have been impressed - it even integrates with DRF's web browseable API. Good luck!
like FlipperPA mentioned, I need to use 'in'. According to the django_filter docs:
β€˜in’ lookups return a filter derived from the CSV-based BaseInFilter.
and an example of BaseInFilter in the docs:
class NumberRangeFilter(BaseInFilter, NumberFilter):
pass
class F(FilterSet):
id__range = NumberRangeFilter(name='id', lookup_expr='range')
class Meta:
model = User
User.objects.create(username='alex')
User.objects.create(username='jacob')
User.objects.create(username='aaron')
User.objects.create(username='carl')
# Range: User with IDs between 1 and 3.
f = F({'id__range': '1,3'})
assert len(f.qs) == 3
The answer to my question:
class AccommodationNode(DjangoObjectType) :
class Meta:
model = Accommodation
interfaces = (relay.Node,)
filter_fields = {
'type': ['in']
}
With the argument {"accommodationType": "apartment,hotel"} will work

Using reverse (Parental)ManyToManyField in ModelAdmin

Similar to my previous question, I'm trying to use the related model within ModelAdmin. (This is because I would like it to be available in both admin views.) This time, however I am using the new ParentalManyToManyField or just a normal ManyToManyField which seem to mess things up.
I wrote the following structure:
class B(Model): # or Orderable
...
edit_handler = TabbedInterface([
ObjectList([
FieldPanel('aes', widget=CheckboxSelectMultiple),
], heading=_('Aes'),
),
])
class A(ClusterableModel):
...
bees = ParentalManyToManyField(
B,
related_name='aes',
blank=True,
)
...
edit_handler = TabbedInterface([
ObjectList([
FieldPanel('bees', widget=CheckboxSelectMultiple),
], heading=_('Bees'),
),
])
When trying to reach the page I receive a Field Error:
Unknown field(s) (aes) specified for B
Is what I'm trying to do not possible yet or did I forget a step?
The ParentalManyToManyField needs to be defined on the parent model (which I assume is meant to be B here - i.e. the modeladmin interface is set up to edit an instance of B with several A's linked to it) and referenced by its field name rather than the related_name. Also, it should be the parent model that's defined as ClusterableModel, not the child:
class B(ClusterableModel):
aes = ParentalManyToManyField('A', blank=True)
edit_handler = TabbedInterface([
ObjectList([
FieldPanel('aes', widget=CheckboxSelectMultiple),
], heading=_('Aes')),
])
class A(Model): # doesn't need to be Orderable, because M2M relations don't specify an order
...

How to write a django-rest-framework serializer / field to merge data from generic relations?

I have objects with a generic relation pointing to various other objects, and I need them to be merged (inlined) so the serialized objects look like one whole objects.
E.G:
class Enrollement(models.Model):
hq = models.ForeignKey(Hq)
enrollement_date = models.Datetime()
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
object = generic.GenericForeignKey('content_type', 'object_id')
class Nurse(models.Model):
hospital = models.ForeignKey(Hospital)
enrollement = GenericRelation(Enrollement)
class Pilot(models.Model):
plane = models.ForeignKey(plane)
enrollement = GenericRelation(Enrollement)
When serialized, I'd like to get something like this:
{
count: 50,
next: 'http...',
previous: null,
results: [
{
type: "nurse",
hq: 'http://url/to/hq-detail/view',
enrollement_date: '2003-01-01 01:01:01',
hospital: 'http://url/to/hospital-detail/view'
},
{
type: "pilot",
hq: 'http://url/to/hq-detail/view',
enrollement_date: '2003-01-01 01:01:01',
plante: 'http://url/to/plane-detail/view'
},
]
}
Can I do it, and if yes, how ?
I can nest a generic relation, and I could post process the serilizer.data to obtain what I want, but I'm wondering if there is a better way.
DEAR FRIENDS FROM THE FUTURE: At time of writing, the Django REST Framework team seems to be working on adding more mature support for generic relations. But it is not yet finished. Before copy-pasting this answer into your code base, check https://github.com/tomchristie/django-rest-framework/pull/755 first to see if it's been merged into the repo. There may be a more elegant solution awaiting you. β€” Your ancient ancestor Tyler
Given you're using Django REST Framework, if you did want to do some post-processing (even though you seem hesitant to) you can accomplish something your goal by overriding get_queryset or list in your view. Something like this:
views.py:
from rest_framework.generics import ListAPIView
from rest_framework.response import Response
from models import *
from itertools import chain
class ResultsList(ListAPIView):
def list(self, request, *args, **kwargs):
nurses = Nurse.objects.all()
pilots = Pilot.objects.all()
results = list()
entries = list(chain(nurses, pilots)) # combine the two querysets
for entry in entries:
type = entry.__class__.__name__.lower() # 'nurse', 'pilot'
if isinstance(entry, Nurse):
serializer = NurseSerializer(entry)
hospital = serializer.data['hospital']
enrollement_date = serializer.data['enrollement.date']
hq = serializer.data['enrollement.hq']
dictionary = {'type': type, 'hospital': hospital, 'hq': hq, 'enrollement_date': enrollement_date}
if isinstance(entry, Pilot):
serializer = PilotSerializer(entry)
plane = serializer.data['plane']
enrollement_date = serializer.data['enrollement.date']
hq = serializer.data['enrollement.hq']
dictionary = {'type': type, 'plane': plane, 'hq': hq, 'enrollement_date': enrollement_date}
results.append(dictionary)
return Response(results)
serializers.py
class EnrollementSerializer(serializer.ModelSerializer):
class Meta:
model = Enrollement
fields = ('hq', 'enrollement_date')
class NurseSerializer(serializer.ModelSerializer):
enrollement = EnrollementSerializer(source='enrollement.get')
class Meta:
model = Nurse
fields = ('hospital', 'enrollement')
class PilotSerializer(serializer.ModelSerializer):
enrollement = EnrollementSerializer(source='enrollement.get')
class Meta:
model = Pilot
fields = ('plane', 'enrollement')
Returned response would look like:
[
{
type: "nurse",
hq: "http://url/to/hq-detail/view",
enrollement_date: "2003-01-01 01:01:01",
hospital: "http://url/to/hospital-detail/view"
},
{
type: "pilot",
hq: "http://url/to/hq-detail/view",
enrollement_date: "2003-01-01 01:01:01",
plane: "http://url/to/plane-detail/view"
},
]
Noteworthy:
My serializers.py may be a bit off here because my memory of how to represent generic relations in serializers is a bit foggy. YMMV.
Similarly to ^^ this assumes your serializers.py is in order and has properly set up its generic relationships in line with your models.
We do the get in source=enrollement.get because otherwise a GenericRelatedObjectManager object will be returned if we don't specify a source. That's because that's what a generic relation represents. Using .get forces a query (as in QuerySet query) which accesses the model you set as the source of the generic relation (in this case, class Enrollement(models.Model).
We have to use list(chain()) instead of the | operator because the querysets come from different models. That's why we can't do entries = nurses | pilots.
for entry in entries can surely be made more dry. GLHF.