I'm very new to both Python and Django and I'm having issues with a nullable Foreign Key relation. I found similar issues to this one, but none of them seemed to be covering my use case.
I'm using Django 1.8.17, and DRF 3.1.0
I have the following classes in Django (I've simplified them out to just the relevant fields since I can't easily copy/paste my code here):
class Rationale(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
class Alert(models.Model):
id = models.AutoField(primary_key=True)
rationale = models.ForeignKey(Rationale, null=True, blank=True)
priority = models.IntegerField(default=1)
class AlertHistory(models.Model):
id = models.AutoField(primary_key=True)
alert = = models.ForeignKey(Alert)
rationale = models.ForeignKey(Rationale, null=True, blank=True)
priority = models.IntegerField(null=True)
class AlertHistoryListView(generics.ListCreateAPIView):
queryset = AlertHistory.objects.all()
serializer_class = AlertHistorySerializer
pagination_class = DefaultPagination
filter_backends = (filters.DjangoFilterBackend, filters.OrderingFilter)
filter_class = AlertHistoryFilterSet
filter_fields = ['alert']
ordering_fields = filter_fields
class AlertHistoryFilterSet(django_filters.FilterSet):
class Meta:
model = AlertHistory
fields = ['alert']
The idea here is to capture changes to the alert in the history table. A user can update the priority or the Rationale.
The Rationale table is look-up that is pre-populated with a JSON fixture. A user can select a rationale to give the reason why the alert is open. Rationale is optional though, and therefore nullable.
However I get an error when I try to set the Rationale to None:
{'rationale': [u'Incorrect type. Expected pk value, received unicode.']}
So searching around led me to: the PrimaryKeyRelatedField: http://www.django-rest-framework.org/api-guide/relations/#primarykeyrelatedfield
I then updated my AlertHistory serializer:
class AlertHistorySerializer(serializers.ModelSerializer):
rationale = serializers.PrimaryKeyRelatedField(read_only=True, allow_null=True)
class Meta:
model = AlertHistory
This fixed my first issue, but led to problems in the test where I'm updating the Rationale. By marking it read_only, I unsurprisingly cannot update that field.
The documentation says I need to specify either read_only=True or the queryset. However it doesn't provide an example of how to do that, and I can't figure it out or find any examples anywhere.
I need to cover both of the following cases:
data = {'alert' : 1, 'priority' : 2, rationale: 1 } to set the rationale to the foreign key for Rationale 1.
And:
data = {'alert' : 1, 'priority' : 2, rationale: None } if a user wants to set the rationale to null. This use-case is more likely when they are simply updating the priority without selecting a rationale.
So I tried defining my queryset to the "all"
rationale = serializers.PrimaryKeyRelatedField(queryset=Rationale.objects.all(), allow_null=True)
but this causes all my tests where rationale is None to give the original exception:
{'rationale': [u'Incorrect type. Expected pk value, received unicode.']}, even though I now have allow_null set to True.
I then tried defining:
rationale = serializers.PrimaryKeyRelatedField(queryset=Rationale.objects.get(pk=rationale), allow_null=True)
but it doesn't know what rationale is.
I also tried:
rationale = serializers.PrimaryKeyRelatedField(source='rationale', allow_null=True)
but that leads me to the error:
AssertionError: Relational field must provide a 'queryset' argument, or set read_only=True.
How do I properly define my queryset?
Thanks.
use this serializer:
class AlertHistorySerializer(serializers.ModelSerializer):
class Meta:
model = AlertHistory
model:
class AlertHistory(models.Model):
id = models.AutoField(primary_key=True)
alert = = models.ForeignKey(Alert)
rationale = models.ForeignKey(Rationale, null=True)
priority = models.IntegerField(null=True)
sample json for creating alert history object:
{
"alert": 1,
"rationale": null,
"priority": 2
}
if you want to set rationale as null send null in json
PS: make sure your DB schema is updated and all the migrations have been applied.
Related
I have a moderation model :
class ItemModeration(models.Model):
class Meta:
indexes = [
models.Index(fields=['object_id', 'content_type']),
]
unique_together = ('content_type', 'object_id')
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
item = GenericForeignKey('content_type', 'object_id')
published = models.BooleanField(default=False)
...
A descriptor to attach a moderation object on-the-fly :
class ItemModerationDescriptor(object):
def __init__(self, **default_kwargs):
self.default_kwargs = default_kwargs
def __get__(self, instance, owner):
ctype = ContentType.objects.get_for_model(instance.__class__)
try:
moderation = ItemModeration.objects.get(content_type__pk=ctype.id,
object_id=instance.pk)
except ItemModeration.DoesNotExist:
moderation = ItemModeration(item=instance,**self.default_kwargs)
moderation.save()
return moderation
And a model I want to moderate :
class Product(models.Model):
user = models.ForeignKey(
User,
null=True,
on_delete=models.SET_NULL)
created = models.DateTimeField(
auto_now_add=True,
blank=True, null=True,
)
modified = models.DateTimeField(
auto_now=True,
blank=True, null=True,
)
name = models.CharField(
max_length=PRODUCT_NAME_MAX_LENGTH,
blank=True, null=True,
)
moderation = ItemModerationDescriptor()
Now I can see a product 'published' state easily :
p=Product(name='my super product')
p.save()
print(p.moderation.published)
-> False
The generic relation is useful because I will be able to search the objects to moderate whatever the type is : it could be products, images, comments.
to_moderate_qs = ItemModeration.objects.filter(published=False)
Now, how can I get a filtered list of published products ?
I would like to do something like this
published_products_qs = Product.objects.filter(moderation__published=True, name__icontains='sony')
But, of course, it won't work as moderation attribute is not a Django model field.
How can I do that efficiently ? I am thinking a about an appropriate JOIN, but I cannot see how to do that with django without using raw SQL.
Django has a great built in answer for this: the GenericRelation. Instead of your descriptor, just define a generic relation on your Product model and use it as a normal related field:
from django.contrib.contenttypes.fields import GenericRelation
class Product(models.Model):
...
moderation = GenericRelation(ItemModeration)
Then handle creation as you normally would with a related model, and filtering should work exactly as you stipulated. To work as your current system, you'd have to put in a hook or save method to create the related ItemModeration object when creating a new Product, but that's no different from other related django models. If you really want to keep the descriptor class, you can obviously make use of a secondary model field for the GenericRelation.
You can also add related_query_name to allow filtering the ItemModeration objects based only on the Product content type.
WARNING if you do use a GenericRelation note that it has a fixed cascading delete behavior. So if you don't want ItemModeration object to be deleted when you delete the Product, be careful to add a pre_delete hook or equivalent!
Update
I unintentionally ignored the OneToOne aspect of the question because the GenericForeignKey is a one-to-many relation, but similar functionality can be effected via smart use of QuerySets. It's true, you don't have access to product.moderation as a single object. But, for example, the following query iterates over a filtered list of products and extracts their name, the user's username, and the published date of the related ModerationItem:
Product.objects.filter(...).values_list(
'name', 'user__username', 'moderation__published'
)
You'll have to use the content_type to query the table by specific model type.
like this:
product_type = ContentType.objects.get_for_model(Product)
unpublished_products = ItemModeration.objects.filter(content_type__pk=product_type.id, published=False)
For more details on the topic check contenttypes doc
So I created a simple model as follows
class Titles(models.Model):
titleID = models.CharField(max_length=20,primary_key=True)
title = models.CharField(max_length=100)
class Meta:
verbose_name = "Titles"
verbose_name_plural = verbose_name
def __str__(self):
return self.title
Exposed it as a API as
class TitlesResource(AT.MultipartResource,AT.WrapView,ModelResource):
class Meta:
queryset = coreModels.Titles.objects.all()
authentication = AT.cxenseAMSAPIAuthentication()
authorization=Authorization()
resource_name = 'titles'
allowed_methods = ['get','post','put','patch']
include_resource_uri=False
limit=1000
When I try to create a new object it works but if I mess up any of the fields it still works
eg:
http://localhost:8000/core/titles/
{
"I_am_not_suppling_a_correct_feild": "2",
"title_not": "dept 1"
}
[27/Oct/2017 10:54:12] DEBUG [django.db.backends:90] (0.001) UPDATE "core_titles" SET "title" = '' WHERE "core_titles"."titleID" = ''; args=('', '')
Shouldnt this fail as I am not supplying the needed fields?
When I try to create a new object it works but if I mess up any of the fields it still works
post data can have N no. of fields. It depends how you are handling each one of them.
Shouldn't this fail as I am not supplying the needed fields?
No. When a POST request is made for a ModelResource it is routed to obj_update and then to obj_create unless it is overridden. There it takes value from **kwargs and creates a model entry. In your case it's not there thus taking empty strings.
For reference, look at the documentation:
https://django-tastypie.readthedocs.io/en/latest/non_orm_data_sources.html
I have three models Transaction, Business, and Location. They are defined as follows:
class Business(models.Model):
# Can have zero or more locations. A user can have many businesses.
name = models.CharField(max_length=200, validators=[MinLengthValidator(1)])
# ... and some other fields ...
class Location(models.Model):
# Must have one business. Locations cannot exist without a business
suburb = models.CharField(max_length=150, validators=[MinLengthValidator(1)])
business = models.ForeignKey(Business, related_name='locations')
# ... and some other fields ...
class Transaction(models.Model):
# Can have zero or one business
# Can have zero or one location and the location must belong to the business. If business is empty, location must be empty
business = models.ForeignKey(Business, on_delete=models.SET_NULL, null=True, blank=True, related_name='transactions')
location = models.ForeignKey(Location, on_delete=models.SET_NULL, null=True, blank=True, related_name='transactions')
# ... and some other fields ...
And the serializers:
class BusinessRelatedField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
owner = get_owner_from_context(self.context)
return Business.objects.filter(owner=owner)
class LocationRelatedField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
params = self.context['request'].query_params
business_params = params.get('business')
if business_params is not None:
owner = get_owner_from_context(self.context)
return Location.objects.filter(owner=owner, business=business_params)
else:
return None
class TransactionSerializer(serializers.ModelSerializer):
business = BusinessRelatedField(required=False, allow_null=True)
location = LocationRelatedField(required=False, allow_null=True)
The problem I was facing was that I didn't know how to restrict the value of Location based on the value of Business. I was manually performing this check inside TransactionSerializer's validate method until it occurred to me to create a PrimaryKeyRelatedField subclass and override the get_queryset method. This seemed like a better approach to me (and it's actually working) but I'd like to know if this is the 'normal' way of doing it.
The other problem I'm now facing is that the 'browsable API' no longer shows any choices for Location which I feel is a hint that I might be doing something wrong.
Any help would be appreciated.
You can override the get_fields() method of the serializer and modify the queryset for business and location fields to the desired values.
get_fields() method is used by the serializer to generate the field names -> field instances mapping when .fields property is accessed on it.
class TransactionSerializer(serializers.ModelSerializer):
class Meta:
model = Transaction
fields = (.., business, transaction)
def get_fields(self):
# get the original field names to field instances mapping
fields = super(TransactionSerializer, self).get_fields()
# get the required parameters
owner = get_owner_from_context(self.context)
business_params = self.context['request'].query_params.get('business')
# modify the queryset
fields['business'].queryset = Business.objects.filter(owner=owner)
fields['location'].queryset = Location.objects.filter(owner=owner, business=business_params)
# return the modified fields mapping
return fields
This is a very late answer, however it would not be different back then.
With the information you provided (in the comments as well) and AFAIK there is no way of doing this unless you manipulate the javascript code of the browsable API's templates and add ajax calling methods to it.
DRF browsable API and DRF HTML and forms may help.
I am trying to create the following models. There is a ManyToMany relation from Entry to AUTH_USER_MODEL via the EntryLike intermediate model.
class BaseType(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
creation_time = models.DateTimeField(auto_now_add=True)
last_update_time = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class Title(BaseType):
text = models.CharField(max_length=100)
description = models.TextField()
class EntryLike(BaseType):
entry = models.ForeignKey(Entry)
user = models.ForeignKey(settings.AUTH_USER_MODEL)
class Entry(BaseType):
title = models.ForeignKey(Title, on_delete=models.PROTECT)
text = models.TextField()
user = models.ForeignKey(settings.AUTH_USER_MODEL)
liked_by_users = models.ManyToManyField(settings.AUTH_USER_MODEL, through='EntryLike', through_fields=('entry', 'user'))
Running migrations on the above model scheme throws the error: AttributeError:'str' object has no attribute 'meta'.
Any help in resolving this error would be highly appreciated. Am new to Django & Python, but not to Web Development.
The issue is that settings.AUTH_USER_MODEL is almost certainly not a model instance. It's probably a string that constrains the choices another model can make - settings would be a strange place to leave a model definition.
To do a MTM between the user model and your field above you need need to do:
from django.contrib.auth.models import User
class Entry(BaseType):
title = models.ForeignKey(Title, on_delete=models.PROTECT)
text = models.TextField()
user = models.ForeignKey(User)
def __str__(self):
return self.title
I've added the str function so that it gives a more sensible return when you're manipulating it in admin/shell.
I'd also question whether you need the second set of fields (removed here), as you can use select related between the Entry and EntryLike join table, without any duplication of the fields - you can probably go that way, it's just a bit unnecessary.
Lastly, I'd note that the way I'm using it above just uses the default User object that comes with Django - you may wish to customise it. or extend the base class as you've done here with your own models' base class.
(All of this is predicated on AUTH_USER_MODEL not being a model instance - if it is, can you post the model definition from settings.py? )
from the documentation:
read_only
Set this to True to ensure that the field is used when serializing a representation, but is not used when updating an instance during deserialization.
Defaults to False
required
Normally an error will be raised if a field is not supplied during deserialization. Set to false if this field is not required to be present during deserialization.
Defaults to True.
So I have a model which has a field that's not nullable but I want it to be populated in the pre_save method, so I have set the field to required=False in serializer, but doesn't seem to work. I am still getting error when saving the record.
class FavoriteListSerializer(serializers.ModelSerializer):
owner = serializers.IntegerField(required=False)
class Meta:
model = models.FavoriteList
Update:
I have added serializer_class = serializers.FavoriteListSerializer to the ViewSet, now instead of getting This field is required, which I think got past the validation but then I am getting This field cannot be null. I have checked the pre_save method is not being executed, any ideas?
Yeah, I ran into this issue at some point as well. You need to also update the validation exclusions.
class FavoriteListSerializer(serializers.ModelSerializer):
owner = serializers.IntegerField(required=False)
class Meta:
model = models.FavoriteList
def get_validation_exclusions(self):
exclusions = super(FavoriteListSerializer, self).get_validation_exclusions()
return exclusions + ['owner']
Late Entry to this thread. This issue was fixed in django-rest-framework 2.3.13. Here is the link of the PR.
You use it like this in your case:
class Meta:
model = models.FavoriteList
optional_fields = ['owner', ]
In case somebody lands here with a similar issue, pay attention to the following attributes along with required:
allow_blank:
If set to True then the empty string should be considered a valid value.
allow_null:
Normally an error will be raised if None is passed to a serializer field.
required:
Normally an error will be raised if a field is not supplied during deserialization.
I was straggling to figure out why I was getting a validation error with required=False where I had missed the allow_null attribute.
In 2020, for DRF 3.12.x, the approach that I prefer the approach that relies on
Serializer's extra_kwargs.
So assuming your
class FavoriteListSerializer(serializers.ModelSerializer):
owner = serializers.IntegerField(required=False)
class Meta:
model = models.FavoriteList
fields = ["owner"] # and whatever other fields you want to expose
extra_kwargs = {"owner": {"required": False, "allow_null": True}}
If you have unique_together constraint on one of the fields you are trying to set required=False you need to set validators=[] in serializers Meta like
class FavoriteListSerializer(serializers.ModelSerializer):
owner = serializers.IntegerField(required=False)
class Meta:
model = models.FavoriteList
validators = []
Here is the original answer
You can also do this:
class ASerializer(serializers.HyperlinkedModelSerializer):
owner = serializers.HiddenField(default=serializers.CurrentUserDefault())
...
As referred here: https://www.django-rest-framework.org/api-guide/validators/#advanced-field-defaults
There you can also find the case when you also wanna let the view show owner
I would set model field to allow null value (and possible also default to None)
class FavoriteList(models.Model):
owner = models.PositiveIntegerField(null=True, default=None)
Then it's possible to just leave owner field to Meta section. These fields, without any extra settings, will automatically get all attributes from model field and be non-required.
class FavoriteListSerializer(serializers.ModelSerializer):
class Meta:
model = models.FavoriteList
fields = ('owner',)