add dynamic field to serializer class - python-3.x

In my serializer class, I have defined two properties, and the third property could be derived from those two properties. Please see the code below
class ItemNameSerializer(NestedCreateUpdateMixin, ModelSerializer):
nested_child_field_name = 'attribute_names'
nested_child_serializer = AttributeNameSerializer
attribute_names = AttributeNameSerializer(many=True)
class Meta:
model = ItemName
fields = '__all__'
From the above code, we can see that
attribute_names = AttributeNameSerializer(many=True)
can be derived by
[nested_child_field_name] = nested_child_serializer(many=true)
So my question is
can I add a dynamic field which will be derived from other fields (to avoid writing redundant code) ?
if yes then how ?
the possible solutions can be of two types
A. overriding some ModelSerializer method.
B. generalized solution for any python class.
please try to provide both type of solutions (if possible)(and may be of some another type ?)

Well I found the Answer myself.
The serializer specific answer:
Turns out django rest frame work initialise the fields from deepcopy of instance (irrelevant)
But you can override __init__ method of the serializer and add field in self.fields. In my case I did it in the NestedCreateUpdateMixin where nested_child_field_name and nested_child_serializer already available
please see following code
def __init__(self, *args, **kwargs):
super(NestedCreateUpdateMixin, self).__init__(*args, **kwargs)
self.fields[self.nested_child_field_name] = self.nested_child_serializer(many=True)

Related

Django REST Serializer permanently changed in init

I'm facing a weird issue with DRF: I have several serializers for which I want to display certain fields only under specific conditions, such as url parameters there being in the request or the user having certain permissions.
To decouple my serializer's presentation logic from the business logic, I decided to add a conditional_fields attribute to the serializer's Meta class: it's a dict in which the keys are strings representing conditions, such as "SHOW_HIDDEN_FIELDS" and their values are lists of field names that need to be removed if the key isn't present in the serializer context. Then I override my viewsets' get_serializer_context method to get the desired values inside of context.
I made a remove_unsatisfied_condition_fields method which does the following:
def remove_unsatisfied_condition_fields(self):
conditional_fields = self.Meta.conditional_fields
for condition, fields in conditional_fields.items():
if not self.context.get(condition, False):
for field in fields:
self.fields.pop(field, None)
The serializers making use of it look like this:
class ExerciseSerializer(serializers.ModelSerializer):
class Meta:
model = Exercise
fields = [
"id",
"text",
"solution",
"correct_choices"
]
conditional_fields = {
"EXERCISE_SHOW_HIDDEN_FIELDS": ["solution", "correct_choices"],
}
Here comes the problem: if I manually call this method inside my serializers, in their __init__ method, like this:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.remove_unsatisfied_condition_fields()
everything works fine.
However, if I make them inherit from a class that does it automatically, like this:
class ConditionalFieldsMixin:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.remove_unsatisfied_condition_fields()
def remove_unsatisfied_condition_fields(self):
conditional_fields = self.Meta.conditional_fields
for condition, fields in conditional_fields.items():
if not self.context.get(condition, False):
for field in fields:
self.fields.pop(field, None)
and make my serializers inherit from it, the following happens:
as soon as a request comes in that doesn't satisfy one of the serializer's conditional_fields condition, the field appears to be "permanently" removed from the serializer: any subsequent requests that involve it, even if they lead to having a context with the proper conditions to show that field, are responded to without that field. It is as if the serializer ceases to have that field forever---only redeploying my application makes it come back.
This is very weird and I have no idea why this only happens if the removal is done inside of a class in the inheritance chain of the serializer vs doing it in the serializer itself.
Does this have to do with some weird Pythonic inheritance rule I'm not aware of or am I missing something about serializers?

How to define the same field for load_only and dump_only params at the Marshmallow scheme?

I am trying to build a marshmallow scheme to both load and dump data. And I get everything OK except one field.
Problem description
(If you understand the problem, you don't have to read this).
For load data its type is Decimal. And I used it like this before. Now I want to use this schema for dumping and for that my flask API responses with: TypeError: Object of type Decimal is not JSON serializable. OK, I understand. I changed the type to Float. Then my legacy code started to get an exception while trying to save that field to database (it takes Decimal only). I don't want to change the legacy code so I looked for any solution at the marshmallow docs and found load_only and dump_only params. It seems like those are what I wanted, but here is my problem - I want to set them to the same field. So I just wondered if I can define both fields and tried this:
class PaymentSchema(Schema):
money = fields.Decimal(load_only=True)
money = fields.Float(dump_only=True)
I have been expected for a miracle, of course. Actually I was thinking that it will skip first definition (correctly, re-define it). What I got is an absence of the field at all.
Workaround solution
So I tried another solution. I created another schema for dump and inherit it from the former schema:
class PaymentSchema(Schema):
money = fields.Decimal(load_only=True)
class PaymentDumpSchema(PaymentSchema):
money = fields.Float(dump_only=True)
It works. But I wonder if there's some another, native, "marshmallow-way" solution for this. I have been looking through the docs but I can't find anything.
You can use the marshmallow decorator #pre_load in this decorator you can do whatever you want and return with your type
from marshmallow import pre_load
import like this and in this you will get your payload and change the type as per your requirement.
UPD: I found a good solution finally.
NEW SOLUTION
The trick is to define your field in load_fields and dump_fields inside __init__ method.
from marshmallow.fields import Integer, String, Raw
from marshmallow import Schema
class ItemDumpLoadSchema(Schema):
item = Raw()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not (self.only and 'item' not in self.only) and \
not (self.exclude and 'item' in self.exclude):
self.load_fields['item'] = Integer(missing=0)
self.dump_fields['item'] = String()
Usage:
>>> ItemDumpLoadSchema().load({})
{'item': 0}
>>> ItemDumpLoadSchema().dump({'item': 0})
{'item': '0'}
Don't forget to define field in a schema with some field (Raw in my example) - otherwise it may raise an exception in some cases (e.g. using of only and exclude keywords).
OLD SOLUTION
A little perverted one. It based on #prashant-suthar answer. I named load field with suffix _load and implemented #pre_load, #post_load and error handling.
class ArticleSchema(Schema):
id = fields.String()
title = fields.String()
text = fields.String()
class FlowSchema(Schema):
article = fields.Nested(ArticleSchema, dump_only=True)
article_load = fields.Int(load_only=True)
#pre_load
def pre_load(self, data, *args, **kwargs):
if data.get('article'):
data['article_load'] = data.pop('article')
return data
#post_load
def post_load(self, data, *args, **kwargs):
if data.get('article_load'):
data['article'] = data.pop('article_load')
return data
def handle_error(self, exc, data, **kwargs):
if 'article_load' in exc.messages:
exc.messages['article'] = exc.messages.pop('article_load')
raise exc
Why the old solution is not a good solution?
It doesn't allow to inheritate schemas with different handle_error methods defined. And you have to name pre_load and post_load methods with different names.
pass data_key argument to the field definition
Documentation mentions, data_key parameter can be used along with dump_only or load_only to be able to have same field with different functionality.
So you can write your schema as...
class PaymentSchema(Schema):
decimal_money = fields.Decimal(data_key="money", load_only=True)
money = fields.Float(dump_only=True)
This should solve your problem. I am using data_key for similar problem in marshmallow with SQLAlchemyAutoSchema and this fixed my issue.
Edit
Note: The key in ValidationError.messages (error messages) will be decimal_money by default. You may tweak the handle_error method of Schema class to replace decimal_money with money but it is not recommended as you yourself may not be able to differentiate between the error messages fields.
Thanks.

Can I use ModelSerializer (DRF) to move multiple fields to a JSON field in a CREATE method?

I'm building an API with the Django Rest Framework. The main requirement is that it should allow for the flexible inclusion of extra fields in the call. Based on a POST call, I would like to create a new record in Django, where some fields (varying in name and number) should be added to a JSON field (lead_request).
I doubt if I should use the ModelSerializer, as I don't know how to handle the various fields that should be merged into one field as a JSON. In the create method, I can't merge the additional fields into the JSON, as they aren't validated.
class Leads(models.Model):
campaign_id = models.ForeignKey(Campaigns, on_delete=models.DO_NOTHING)
lead_email = models.EmailField(null=True, blank=True)
lead_request = JSONField(default=dict, null=True, blank=True)
class LeadCreateSerializer(serializers.ModelSerializer):
def get_lead_request(self):
return {key: value for key, value in self.request.items() if key.startswith('rq_')}
class Meta:
model = Leads
fields = ['campaign_id',
'lead_email',
'lead_request']
def create(self, validated_data):
return Leads.objects.create(**validated_data)
The documentation mostly talks about assigning validated_data, but here that isn't possible.
If I understood correctly and you want to receive parameters through the URL as well, here's an example of how you could achieve what you want:
class LeadViewSet(viewsets.ModelViewSet):
def create(self, request, *args, **kwargs):
data = request.data
lead_request = generate_lead_request(request)
data['lead_request'] = lead_request
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
...
And on generate_lead_request you could parse all the additional fields that may have been sent through request.data (body) as well as through the request.query_params.
If i understand the problem properly main obstruction here is we don't know the exact JSON data format of lead_request. I am thinking about two possible model of solution for this problem. I not sure either of them is appropriate or not. Just want to share my opinion.
case 1
Lets assume data passed to LeadCreateSerializer in this type of format
data = {
'campaign_id': campaign_id,
'lead_email': lead_email,
'lead_request': {
# lead_request
}
}
Then this is easy, normal model serializer should able to do that. If data is not in properly formatted and it possible to organize before passing to serializer that this should those view or functions responsibility to make it proper format.
case 2
Lets assume this is not possible to organize data before passing that in LeadCreateSerializer then we need to get our related value during the validation or get of lead_request. As this serializer responsibility is to create new instance and for that validate fields so we assume in self.context the whole self.context.request is present.
class LeadCreateSerializer(serializers.ModelSerializer):
def generate_lead_request(self, data):
# do your all possible validation and return
# in dict format
def get_lead_request(self):
request = self.context.request
lead_request = self.generate_lead_request(request.data)
return lead_request
class Meta:
model = Leads
fields = ['campaign_id',
'lead_email',
'lead_request']

Preventing a particular attribute's inheritance in subclass?

If I have the following :
class A:
attrs = [...]
A_attr = [...]
class B(A):
B_attr = [...]
Is there a way to prevent my B subclass from inheriting the A_attr from the A class?
Or would this be considered a bad design and I should better subclass both A and B from a third C class containing all the attrs attributes and add the particular attribute to each subclass like this?
class C:
attrs = [...]
class A(C):
A_attr = [...]
class B(C):
B_attr = [...]
Better idea is to dump the common functionality in a class.
class Commmon:
attrs = [...]
Extend this class who want this extra functonality.
class A(Common):
# only one attribute added in this class
A_attr = [...]
classB(Common):
attrs_B = [...]
Extend class A when that extra attribute is needed in the class, this will bring all those other attributes.
class C(A):
attrs_C = [...]
What this will allow is wherever you want an object of type Common you can provide instance of B as well as C. And wherever you want instance of class A you can provide instance of C. If you add specific instance in each of your subclasses you will not be able to do so.
From Comment
So according to you I should use the second solution I exposed in my question.
No.
Instead of adding the attribute in each subclass, my advice is to add the attribute in a separate class and let your new classes inherit this intermediate class. So you do not have to add the specific attribute in each one of those subclass.
Example is already provided above. Lets see what is the benefit of doing this, as opposed to your suggestion. Take the following function
def foo(obj):
# check to make sure object has the specific attribute
if (isinstance(obj, A)):
pass; #do something
else:
raise TypeError("Object is not an instance of A")
But if we add the specific attribute in each class, the method will need to be changed to something like this:
def foo(obj):
# check to make sure object has the those type which has that specific attribute
if( isinstance(obj, class1) or (isinstance(obj, class2) or ...):
pass; #do something
else:
raise TypeError("Object does not have specific attribute")
Of course, you can perform a check using something like this:
def foo(obj):
# check to make sure obj has attribute
if hasattr(obj, 'property')
pass; # do something
else:
raise TypeError("Object does not have necessary attribute")
Using correct inheritance relationship (as shown in 1st example) will also help your IDE (if you are using one) in inferring types, because IDE can determine which type of object it expects. You can even augment the function with type information like this:
def foo(obj : A):
pass; #do something
That A after colon is a hint to the IDE that function expects an object of type or subtype of A.

Add custom attribute to QCheckBox widget

I have (I think) a simple question but haven't had much luck trying to find an answer. Really new to pyqt!
I am dynamically adding a number of QtGui.QCheckBox() widgets to a gridLayout based on a number of factors. My question is, how can I add a custom attr to each chkbox widget? I want to store a few custom things inside each qt widget.
Thanks for any help. A basic example would be most useful.
Cheers
You can also use the .setProperty() method, aka Dynamic Properties:
self.the_wdiget.setProperty("my_string", "hello")
self.the_wdiget.setProperty("my_bool", True)
self.the_wdiget.setProperty("my_int", 10)
self.the_wdiget.setProperty("my_stringList", ['sl1', 'sl2', 'sl3'])
# And get it by:
self.the_widget.property("my_bool") # etc.
Strings can also be set to translateable. E.g.
self.the_widget.setProperty("my_string", _translate("Dialog", "hello"))
http://doc.qt.io/qt-5/qobject.html#setProperty
Also see:
http://pyqt.sourceforge.net/Docs/PyQt5/qt_properties.html
You can just subclass the QCheckBox class. For example:
class MyCheckBox(QtGui.QCheckBox):
def __init__(self, my_param, *args, **kwargs):
QtGui.QCheckBox.__init__(self, *args, **kwargs)
self.custom_param = my_param
Here we override the __init__ method which is called automatically when you instantiate the class. We add an extra parameter my_param to the signature and then collect any arguments and keyword arguments specified into args and kwargs.
In our new __init__ method, we first call the original QCheckBox.__init__ passing a reference to the new object self and unpacking the arguments are keyword arguments we captured. We then save the new parameter passed in an an instance attribute.
Now that you have this new class, if you previously created (instantiated) checkbox's by calling x = QtGui.QCheckBox('text, parent) you would now call x = MyCheckBox(my_param, 'text', parent) and you could access your parameter via x.custom_param.

Resources