Modify read only nested fields in DRF - django

I realize the title sounds silly but I want to be able to change the references to the Group objects for my User instances. But I do not want them to be able to create new groups or edit existing groups. I think what I want is a read only nested field. However, if I set it to read_only=True I do not get the data in my serializers validated data. If I set it to read_only=False then it tries to create a new Group instead of just changing the references.
class GroupSerializer(serializers.ModelSerializer):
permissions = PermissionSerializer(many=True)
class Meta:
model = Group
fields = (
'pk',
'name',
'permissions',
)
class UserSerializer(serializers.ModelSerializer):
groups = GroupSerializer(many=True)
....
class Meta:
model = User
exclude = (
....
)
def update(self, instance, validated_data):
print(validated_data)
return instance
def validate_groups(self, value):
print("validating groups")
....
return value
With read_only=True nothing happens at all. I get the user back on my PATCH request but the user is exactly the same. With read_only=False I get a validation error returned to me {'groups': [{'name': ['group with this name already exists.']}]}
I have also tried overriding the create and update method on the GroupSerializer but with no change.
At most, I want the GroupSerializer just to validate that the group from the data exists.

Really late answer, but I stumbled upon this in another thread here on StackOverflow (unfortunately I can't find it now), and they referred to the following discussion.
The solution I used was to create two serializers - one for reading and another one for writing - and two respective ViewSets including the correct Mixins. So I could list the nested Foreign Keys when using a GET method and only POST using the identifier for the existing Model of the FK relation. I used the depth attribute. I hope that relates to this problem.

The best solution I have found was to use the PrimaryKeyRelatedField and that provides a read only interface where I can select items that already exists and associate them to the object.
Sadly, there is one downside which is that now when viewing those objects in a GET type view the details of the related object are not shown but rather just the PK. I will figure out a way around this soon, maybe multi-serializer viewsets will do the trick.
Here are the docs

Related

Django Rest Framework - Translating external unique code to a internal id and vice versa

I am building a Rest Api that read and update a model called Requirements.
I am using a ModelSerializer.
The Requirements model has a foreign key on the Materials model.
The problem is that my api user does not know the internal id of Materials
He knows only a code that is unique for the material.
Now, the idea is that in PUT he passes me the material_code and I set the material id and in GET he receives the material code based on the material foreign key of Requirements
I managed to make PUT to work by overriding the validate method and declaring:
material_code = serializers.CharField(max_length=50);
This is the code supplied in the end of the post. Notice, please, that this is a snippet of the complete code that is much complex. In the complete code the Requirements serializer is nested inside another serializer that is nested is nested inside another serializer. But I do not think this is relevant to the problem.
Then I manage to make GET to work by the use of a custom source option in material_code field where the source is a property on my Requirements model. For this the declaration must be changed to:
material_code = serializers.ReadOnlyField(source='get_material_code')
For some reason both:
material_code = serializers.Field()
and
material_code = serializers.Field(source='get_material_code')
behaves in a weird way and do not work either with PUT or with GET, raising an exception “Field.to_internal_value() must be implemented”. I've tried to implement to_internal_value and give it a try but failed. And after all, material_code should not go to a internal value. Once I've managed to set the material id, I do not need it.
There is no way I can make both PUT and GET work simultaneously. GET will only work with serializers.ReadOnlyField and PUT with a serializers.CharField.
Using PUT with serializers.ReadOnlyField generates Exception Type: KeyError Exception Value 'material_code'.
Using GET with serializers.CharField generates Exception Type: AttributeError
Exception Value: Got AttributeError when attempting to get a value for field material_code on serializer RequirementsSerializer. The serializer field might be named incorrectly and not match any attribute or key on the Requirements instance. Original exception text was: 'Requirements' object has no attribute 'material_code'.
Maybe the whole approach is wrong.
What I need is a translation between the externally visible code and the internal id.
This should not be that hard. This is my first Python project and maybe there is a built in way do make it work in Django Rest Api, but I was unable to find in the documentation.
I will deeply appreciate any help. By the way, this is my first stackoverflow post. If I did anything wrong, please let me know.
class Materials (models.Model):
class Meta:
db_table = 'materials'
code = models.CharField(max_length=50);
full_name = models.CharField(max_length=50);
def __str__(self):
return self.full_name
class Requirements (models.Model):
class Meta:
db_table = 'requirements'
material = models.ForeignKey(Materials, on_delete=models.CASCADE)
acceptance_method = models.CharField(max_length=4)
#property
def get_material_code(self):
return self.material.code
class RequirementsSerializer(serializers.ModelSerializer):
material_code = serializers.CharField(max_length=50);
material = serializers.HiddenField(default=1)
class Meta:
model = Requirements
fields = [ 'id',
'material' ,
'material_code' ,
'acceptance_method'
]
read_only_fields = ['material']
def validate(self, data):
# Fill material with a pk on Materials models
# This is necessary since the API receive a material code instead of the model id
if Materials.objects.filter(code = data['material_code']).exists() :
data['material'] = Materials.objects.get(code = data['material_code'])
else:
raise serializers.ValidationError('Material '+ data['material_code'] + ' does not exists')
return data
If I am understanding correctly, you want the user to be able to do a PUT using the material code as the identifier of the material (instead of the ID), and you want the same behavior from the GET call. If this is the case, I believe you are looking for the SlugRelatedField of serializers. Example:
class RequirementsSerializer(serializers.ModelSerializer):
material = serializers.SlugRelatedField(slug_field='code')

How can I override user.groups in a custom account model in Django to implement a "virtual" group?

I have a custom account class in a Django app using PermissionsMixin:
class Account(AbstractBaseUser, PermissionsMixin):
Our CMS calls various .groups methods on this class in order to ascertain permissions.
We essentially want to override the queryset that is returned from .groups in this custom Account class and to inject an additional group under specific conditions. (I.e. the user has an active subscription and we then want to return "member" as one of the groups for that user, despite them not actually being in the group.)
How should we handle this override? We need to get the original groups, so that basic group functionality isn't broken, then inject our "virtual" group into the queryset.
Override the get_queryset method ManyRelatedManager. An object of ManyRelatedManager class has access to the parent instance.
Code Sample:
def add_custom_queryset_to_many_related_manager(many_related_manage_cls):
class ExtendedManyRelatedManager(many_related_manage_cls):
def get_queryset(self):
qs = super(ExtendedManyRelatedManager, self).get_queryset()
# some condition based on the instance
if self.instance.is_staff:
return qs.union(Group.objects.filter(name='Gold Subscription'))
return qs
return ExtendedManyRelatedManager
ManyRelatedManager class is obtained from the
ManyToManyDescriptor.
class ExtendedManyToManyDescriptor(ManyToManyDescriptor):
#cached_property
def related_manager_cls(self):
model = self.rel.related_model if self.reverse else self.rel.model
return add_custom_queryset_to_many_related_manager(create_forward_many_to_many_manager(
model._default_manager.__class__,
self.rel,
reverse=self.reverse,
))
Associated the ExtendedManyToManyDescriptor with groups field when
the Account class is initialized.
class ExtendedManyToManyField(ManyToManyField):
def contribute_to_class(self, cls, name, **kwargs):
super(ExtendedManyToManyField, self).contribute_to_class(cls, name, **kwargs)
setattr(cls, self.name, ExtendedManyToManyDescriptor(self.remote_field, reverse=False))
Override PermissionsMixin to use ExtendedManyToManyField for
groups field instead of ManyToManyField.
class ExtendedPermissionsMixin(PermissionsMixin):
groups = ExtendedManyToManyField(
Group,
verbose_name=_('groups'),
blank=True,
help_text=_(
'The groups this user belongs to. A user will get all permissions '
'granted to each of their groups.'
),
related_name="user_set",
related_query_name="user",
)
class Meta:
abstract = True
Reference:
django.db.models.fields.related_descriptors.create_forward_many_to_many_manager
Testing:
account = Account.objects.get(id=1)
account.is_staff = True
account.save()
account.groups.all()
# output
[<Group: Gold Subscription>]
The groups related manager is added by the PermissionMixin, you could actually remove the mixin and add only the parts of it that you need and redefine groups:
class Account(AbstractBaseUser):
# add the fields like is_superuser etc...
# as defined in https://github.com/django/django/blob/master/django/contrib/auth/models.py#L200
default_groups = models.ManyToManyField(Group)
#property
def groups(self):
if self.is_subscribed:
return Group.objects.filter(name="subscribers")
return default_groups.all()
Then you can add your custom groups using the Group model. This approach should work fine as long it is ok for all parts that groups returns a queryset instead of a manager (which probably mostly should be fine as managers mostly offer the same methods - but you probably need to find out yourself).
Update
After reading carefully the docs related to Managers and think about your requirement, I've to say there is no way to achieve the magic you want (I need to override the original, not to add a new ext_groups set - I need to alter third party library behavior that is calling groups.) Without touch the Django core itself (monkey patching would mess up admin, the same with properties).
In the solution I'm proposing, you have the necessary to add a new manager to Group, perhaps, you should start thinking in override that third-party library you're using, and make it use the Group's Manager you're interested in.
If the third-party library is at least medium quality it will have implemented tests that will help you to keep it working after the changes.
Proposed solution
Well, the good news is you can fulfill your business requirements, the bad news is you will have code a little more than you surely expect.
How should we handle this override?
You could use a proxy model to the Group class in order to add a custom manager that returns the desired QuerySet.
A proxy manager won't add an extra table for groups and will keep all the Group functionality besides, you can set custom managers on proxy models too, so, its perfect for this case use.
class ExtendedGroupManager(models.Manager):
def get_queryset(self):
qs = super().get_queryset()
# Do work with qs.
return qs
class ExtendedGroup(Group):
objects = ExtendedGroupManager()
class Meta:
proxy = True
Then your Account class should then have a ManyToMany relationship to ExtendedGroup that can be called ... ext_groups?
Till now you can:
acc = Account(...)
acc.groups.all() # All groups for this account (Django default).
acc.ext_groups.all() # Same as above, plus any modification you have done in get_queryset method.
Then, in views, you can decide if you call one or another depending on a condition of your own selection (Eg. user is subscribed).
Is worth mention you can add a custom manager to an existeing model using the method contribute_to_class
em = ExtendGroupManager()
em.contribute_to_class(Group, 'ext_group')
# Group.ext_group is now available.
If your CMS calls Account.groups directly I would investigate how to override a ManyRelatedManager since the account.groups is actually a django ManyRelatedManager object.
It can probably be achieved using django Proxy model for the Group.
One strategy to investigate would then be:
Add the virtual group to the groups in the database.
Override the ManyRelatedManager by changing get_queryset() (The base queryset)
Something like (pseudo code):
def get_queryset():
account = self.instance # Guess this exist in a RelatedManager
if account.has_active_subscribtion():
return self.get_all_relevant_groups_including_the_virtual_one()
return self.get_all_relevant_groups_excluding_the_virtual_one()
The key here is to get access to the current instance in the custom RelatedManager
This may be a useful discussion around custom related managers.
Subclass a Django ManyRelatedManager a.k.a. ManyToManyField
I do not recommend to try fiddle/monkey patch with the queryset itself since it very easy to break the django admin etc...

Django: How to Properly Use ManyToManyField with Factory Boy Factories & Serializers?

The Problem
I am using a model class Event that contains an optional ManyToManyField to another model class, User (different events can have different users), with a factory class EventFactory (using the Factory Boy library) with a serializer EventSerializer. I believe I have followed the docs for factory-making and serializing, but am receiving the error:
ValueError: "< Event: Test Event >" needs to have a value for field "id"
before this many-to-many relationship can be used.
I know that both model instances must be created in a ManyToMany before linking them, but I do not see where the adding is even happening!
The Question
Can someone clarify how to properly use a ManyToManyField using models, factory boy, and serializers in a way I am not already doing?
The Set-Up
Here is my code:
models.py
#python_2_unicode_compatible
class Event(CommonInfoModel):
users = models.ManyToManyField(User, blank=True, related_name='events')
# other basic fields...
factories.py
class EventFactory(factory.django.DjangoModelFactory):
class Meta:
model = models.Event
#factory.post_generation
def users(self, create, extracted, **kwargs):
if not create:
# Simple build, do nothing.
return
if extracted:
# A list of users were passed in, use them
# NOTE: This does not seem to be the problem. Setting a breakpoint
# here, this part never even fires
for users in extracted:
self.users.add(users)
serializers.py
class EventSerializer(BaseModelSerializer):
serialization_title = "Event"
# UserSerializer is a very basic serializer, with no nested
# serializers
users = UserSerializer(required=False, many=True)
class Meta:
model = Event
exclude = ('id',)
test.py
class EventTest(APITestCase):
#classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(email='test#gmail.com',
password='password')
def test_post_create_event(self):
factory = factories.EventFactory.build()
serializer = serializers.EventSerializer(factory)
# IMPORTANT: Calling 'serializer.data' is the exact place the error occurs!
# This error does not occur when I remove the ManyToManyField
res = self.post_api_call('/event/', serializer.data)
Version Info
Django 1.11
Python 2.7.10
Thank you for any help you can give!
Regarding the error:
It seems like the missing id is due to the use of .build() instead of .create() (or just EventFactory()). The former does not save the model, and therefore it does not get an id value, whereas the latter does (refer to factory docs and model docs).
I suspect the serializer still expects the object to have an id, even though the many-to-many relationship is optional, because it cannot enforce a potential relationship without an id.
However, there might be a simpler solution to the actual task. The above method is a way of generating the POST data passed to post_api_call(). If this data was instead created manually, then both the factory and serializer become unnecessary. The explicit data method might even be better from a test-perspective, because you can now see the exact data which has to produce an expected outcome. Whereas with the factory and serializer method it is much more implicit in what is actually used in the test.

modify unique_together constraint to save new object and delete old

I have a model with a unique_together constraint on four fields. Instead of raising the normal Validation Error, however, I want to delete the old object and replace it with the newer one (or maybe update the old one? would also work). I'm a little at a loss as to how to go about doing this, or if there's maybe a better way to go about achieving this behavior.
EDIT:
Any downside to modifying the save method to just check the database for an instance with those four fields and deleting it if I find one?
overriding the save method is OK, but it will fetch the database every time, possibly causing performance loss. It will be better, and more pythonic, if you handle the ValidationError:
try:
YourModel.objects.create(
unique_together_field_1,
unique_together_field_2,
unique_together_field_3,
unique_together_field_4,
...
)
except YourModel.ValidationError:
# update or replace the existing model
EDIT
You can use this code in the model's manager:
class YourModelManager(models.Manager):
def create(**kwargs):
try:
super(YourModelManager, self).create(**kwargs)
except YourModel.ValidationError:
# handle here the error
# kwargs is a dictionary with the model fields
and in the model:
class YourModel(models.Model):
unique_together_field_1 = ....
...
class Meta:
unique_together = [...]
objects = YourModelManager()
Check the docs about custom managers.

Modify data before validation step with django rest framework

I have a simple Model that stores the user that created it with a ForeignKey. The model has a corresponding ModelSerializer and ModelViewSet.
The problem is that when the user submits a POST to create a new record, the user should be set by the backend. I tried overriding perform_create on the ModelViewSet to set the user, but it actually still fails during the validation step (which makes sense). It comes back saying the user field is required.
I'm thinking about overriding the user field on the ModelSerializer to be optional, but I feel like there's probably a cleaner and more efficient way to do this. Any ideas?
I came across this answer while looking for a way to update values before the control goes to the validator.
This might be useful for someone else - here's how I finally did it (DRF 3) without rewriting the whole validator.
class MyModelSerializer(serializers.ModelSerializer):
def to_internal_value(self, data):
data['user'] = '<Set Value Here>'
return super(MyModelSerializer, self).to_internal_value(data)
For those who're curious, I used this to round decimal values to precision defined in the model so that the validator doesn't throw errors.
You can make the user field as read_only.
This will ensure that the field is used when serializing a representation, but is not used when creating or updating an instance during deserialization.
In your serializers, you can do something like:
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
extra_kwargs = {
'user' : {'read_only' : True} # define the 'user' field as 'read-only'
}
You can then override the perform_create() and set the user as per your requirements.
Old topic but it could be useful for someone.
If you want to alter your data before validation of serializer:
serializer.initial_data["your_key"] = "your data"
serializer.is_valid(raise_exception=True)