Access different fields on 'ManyRelatedManager' using Django Rest Framework? - python

I'm trying to access fields on the through table of my ManyToMany link to serialize into a JSON via Django Rest Frameworks.
My models involved in the many to many are:
class Mage(models.Model):
arcana = models.ManyToManyField('ArcanumAbility', through='CharacterArcanumLink', related_name='mage_by_arcana')
class ArcanumAbility(models.Model):
class Arcana(AutoNumber):
FATE = ()
MIND = ()
SPIRIT = ()
DEATH = ()
FORCES = ()
TIME = ()
SPACE = ()
LIFE = ()
MATTER = ()
PRIME = ()
arcanum = EnumField(Arcana)
class Meta:
verbose_name_plural = "Arcana Abilities"
def __str__(self):
return self.arcanum.label
class CharacterArcanumLink(Trait):
PRIORITY_CHOICES = (
(0, 'Unassigned'), (1, 'Ruling'), (2, 'Common'), (3, 'Inferior')
)
priority = models.PositiveSmallIntegerField(
choices=PRIORITY_CHOICES, default=0)
mage = models.ForeignKey('Mage')
arcana = models.ForeignKey('ArcanumAbility')
class Meta:
unique_together = ('mage', 'arcana')
def __str__(self):
return self.arcana.arcanum.label
Where the Trait mixin provides a current_value
To serialize the above relation into my JSON, I have tried these two patters on my serializer:
class CharacterArcanumLinkSerializer(serializers.ModelSerializer):
class Meta:
model = CharacterArcanumLink
fields = ('current_value', 'arcana')
class MageSerializer(serializers.ModelSerializer):
arcana = CharacterArcanumLinkSerializer()
....
class Meta:
model = Mage
fields = (...., 'arcana', ....)
depth = 1
But that gives me this error:
AttributeError at /mages
'ManyRelatedManager' object has no attribute 'arcana'
Which is from (ultimately):
C:\Python34\lib\site-packages\rest_framework\fields.py in get_attribute
if instance is None:
# Break out early if we get `None` at any point in a nested lookup.
return None
try:
if isinstance(instance, collections.Mapping):
instance = instance[attr]
else:
instance = getattr(instance, attr) ...
except ObjectDoesNotExist:
return None
if is_simple_callable(instance):
instance = instance()
return instance
▼ Local vars
Variable: Value
instance: <django.db.models.fields.related.create_many_related_manager.<locals>.ManyRelatedManager object at 0x0000000004E4D4A8>
attr: 'arcana'
attrs: ['arcana']
(Question: What trick to I need to go from my ManyRelatedManager to it's fields?)
And I've also tried not specifying a special serializer, and just having 'arcana' in my fields, and pull it from my model. That leads to this error:
TypeError at /mages
<Arcana.FATE: 1> is not JSON serializable
Where the 1 is from the PK on the ArcanumAbility not the value on the through table. The issue here is that the Mage class has a M2M field that points to the 'ArcanumAbility' model, so all that DRF tries to do is serialize the Enum on it.
So what method should I use if I want a JSON dictionary of all the relationships from Mage to ArcanumAbility with data from the through table?
Responding to Mark R., I'd like it looks like so:
....
"arcanum": {
"Fate": 2,
"Spirit": 0,
"Mind": 3,
....
}
Hopefully that's a clear enough sample.

As discussed, if you add a related_name="linked_arcana" to the mage field in the CharacterArcanumLink class, you should be able to do something like this:
class MageSerializer(serializers.ModelSerializer):
arcana = serializers.SerializerMethodField()
def get_arcana(self, obj):
if obj:
return {str(x): x.current_value for x in obj.linked_arcana.all()}

I have gotten this working like so:
arcana = serializers.SerializerMethodField()
def get_arcana(self, obj):
if obj:
return {str(link): link.current_value
for link in CharacterArcanumLink.objects.filter(mage=obj)}
Heavily inspired by this answer.

Related

Intercept and replace serializer fields on initialization in Django

So I have a Django project with Django REST Framework with large number of models. For frontend to be user friendly I should display not only related object's id but also name. My idea for the solution was to replace all the PrimaryKeyRelated fields with StringRelatedFields in serializers on response. As the number of models is large I decided to make a single abstract serializer/mixin and intercept field creation replacing the field if is of correct type. This is how far I got up to now:
class AbstractSerializer(serializers.ModelSerializer):
class Meta:
model: AbstractModel = AbstractModel
read_only_fields: list = [
'created_at',
'created_by',
'modified_at',
'modified_by',
'is_deleted',
'deleted_at',
'deleted_by'
] + ['is_active'] if 'is_active' in [field.attname for field in model._meta.fields] else []
abstract: bool = True
def to_representation(self, instance):
serializer = AbstractRequestResponseSerializer(instance)
return serializer.data
class AbstractRequestResponseSerializer(AbstractSerializer):
class Meta(AbstractSerializer.Meta):
pass
#classmethod
def _get_declared_fields(cls, bases, attrs):
fields = [(field_name, attrs.pop(field_name))
for field_name, obj in list(attrs.items())
if isinstance(obj, Field)]
fields.sort(key=lambda x: x[1]._creation_counter)
new_fields = []
for field in fields:
if isinstance(field, PrimaryKeyRelatedField):
field = StringRelatedField(source=field.source, required=False)
new_fields.append(field)
fields = new_fields
known = set(attrs)
def visit(name):
known.add(name)
return name
base_fields = [
(visit(name), f)
for base in bases if hasattr(base, '_declared_fields')
for name, f in base._declared_fields.items() if name not in known
]
return OrderedDict(base_fields + fields)
This gives an infinite loop error because of __new__ method and I started to wonder if I am overriding the right function. I also tried to replace to_representation function but I guess that function occurs too late in the flow when all the field instances are created already. Which function should I override?
class ParentModelSerializer(serializers.ModelSerializer):
class Meta:
model = ParentModel
fields = '__all__'
class ChildModelSerializer(serializers.ModelSerializer):
parent = ParentModelSerializer(read_only=True)
class Meta:
model = ChildModel
fields = '__all__'
Or if you want to display the children in your parent:
class ParentModelSerializer(serializers.ModelSerializer):
children = ChildModelSerializer(read_only=True, many=True)
# children is the "related_name"
class Meta:
model = ParentModel
fields = '__all__'
class ChildModelSerializer(serializers.ModelSerializer):
class Meta:
model = ChildModel
fields = '__all__'
Maybe I phrased the question incorrectly (you could rephrase that to help future generations :) ), but the solution I made looks like this:
class AbstractSerializer(serializers.ModelSerializer):
class Meta:
model: AbstractModel = AbstractModel
read_only_fields: list = [
'created_at',
'created_by',
'modified_at',
'modified_by',
'is_deleted',
'deleted_at',
'deleted_by'
] + ['is_active'] if 'is_active' in [field.attname for field in model._meta.fields] else []
abstract: bool = True
def to_representation(self, instance):
ret = OrderedDict()
fields = self._readable_fields
for field in fields:
if isinstance(field, PrimaryKeyRelatedField):
parent = field.parent
field_name = field.field_name
source = field.source
if source != field_name:
field = StringRelatedField(source=field.source, required=False)
else:
field = StringRelatedField(required=False)
field.bind(field_name, parent)
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
if check_for_none is None:
ret[field.field_name] = None
else:
ret[field.field_name] = field.to_representation(attribute)
return ret

DRF SerializerMethodField how to pass parameters

Is there a way to pass paremeters to a Django Rest Framework's SerializerMethodField?
Assume I have the models:
class Owner(models.Model):
name = models.CharField(max_length=10)
class Item(models.Model):
name = models.CharField(max_length=10)
owner = models.ForeignKey('Owner', related_name='items')
itemType = models.CharField(max_length=5) # either "type1" or "type2"
What I need is to return an Owner JSON object with the fields: name, type1items, type2items.
My current solution is this:
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = models.Item
fields = ('name', 'itemType')
class OwnerSerializer(serializers.ModelSerializer):
type1items = serializers.SerializerMethodField(method_name='getType1Items')
type2items = serializers.SerializerMethodField(method_name='getType2Items')
class Meta:
model = models.Owner
fields = ('name', 'type1items', 'type2items')
def getType1Items(self, ownerObj):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType="type1")
return ItemSerializer(queryset, many=True).data
def getType2Items(self, ownerObj):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType="type2")
return ItemSerializer(queryset, many=True).data
This works. But it would be much cleaner if I could pass a parameter to the method instead of using two methods with almost the exact code. Ideally it would look like this:
...
class OwnerSerializer(serializers.ModelSerializer):
type1items = serializers.SerializerMethodField(method_name='getItems', "type1")
type2items = serializers.SerializerMethodField(method_name='getItems', "type2")
class Meta:
model = models.Owner
fields = ('name', 'type1items', 'type2items')
def getItems(self, ownerObj, itemType):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType=itemType)
return ItemSerializer(queryset, many=True).data
In the docs SerializerMethodField accepts only one parameter which is method_name.
Is there any way to achieve this behaviour using SerializerMethodField? (The example code here is overly simplified so there might be mistakes.)
There is no way to do this with the base field.
You need to write a custom serializer field to support it. Here is an example one, which you'll probably want to modify depending on how you use it.
This version uses the kwargs from the field to pass as args to the function. I'd recommend doing this rather than using *args since you'll get more sensible errors, and flexibility in how you write your function/field definitions.
class MethodField(SerializerMethodField):
def __init__(self, method_name=None, **kwargs):
# use kwargs for our function instead, not the base class
super().__init__(method_name)
self.func_kwargs = kwargs
def to_representation(self, value):
method = getattr(self.parent, self.method_name)
return method(value, **self.func_kwargs)
Using the field in a serializer:
class Simple(Serializer):
field = MethodField("get_val", name="sam")
def get_val(self, obj, name=""):
return "my name is " + name
>>> print(Simple(instance=object()).data)
{'field': 'my name is sam'}
You could just refactor what you have:
class OwnerSerializer(serializers.ModelSerializer):
type1items = serializers.SerializerMethodField(method_name='getType1Items')
type2items = serializers.SerializerMethodField(method_name='getType2Items')
class Meta:
model = models.Owner
fields = ('name', 'type1items', 'type2items')
def getType1Items(self, ownerObj):
return getItems(ownerObj,"type1")
def getType2Items(self, ownerObj):
return getItems(ownerObj,"type2")
def getItems(self, ownerObj, itemType):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType=itemType)
return ItemSerializer(queryset, many=True).data

Python - Inherit from class which inherit Model

Here is my problem: I try to create layer under
models.Model
My Model -
class MainModel(models.Model):
#staticmethod
def getIf(condition):
results = __class__.objects.filter(condition)
if results.count() > 0:
return results.first()
else:
return None
And that's a model
class User(MainModel):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=256)
date_create = models.DateTimeField(auto_now_add=True)
date_last_login = models.DateTimeField(null=True)
But my project is crushed with error -
django.core.exceptions.FieldError: Local field 'id' in class 'User'
clashes with field of the same name from base class 'MainModel'.
What am I doing wrong?
UPD: if you want to do like this, you need to use subclass Meta in your layer
class MainModel(models.Model):
#staticmethod
def getIf(condition:dict):
results = __class__.objects.filter(condition)
if results.count() > 0:
return results.first()
else:
return None
class Meta:
abstract = True
Thanx, but I'm not trying to override fields, In my layer no one field is not defined. I found my answer, I just have to read documentation.
if you want to do like this, you need to use subclass Meta in your layer
class MainModel(models.Model):
#staticmethod
def getIf(condition:dict):
results = __class__.objects.filter(condition)
if results.count() > 0:
return results.first()
else:
return None
class Meta:
abstract = True
Django adds a field id to all Models, you have to remove it.
Ok I understand your question better now, your answer is there:
In Django - Model Inheritance - Does it allow you to override a parent model's attribute?
Django already adds a field id to your parent model.

Mixin common fields between serializers in Django Rest Framework

I have this:
class GenericCharacterFieldMixin():
attributes = serializers.SerializerMethodField('character_attribute')
skills = serializers.SerializerMethodField('character_skill')
def character_attribute(self, obj):
character_attribute_fields = {}
character_attribute_fields['mental'] = {str(trait_item.get()): trait_item.get().current_value
for trait_item in obj.mental_attributes}
character_attribute_fields['physical'] = {str(trait_item.get()): trait_item.get().current_value
for trait_item in obj.physical_attributes}
character_attribute_fields['social'] = {str(trait_item.get()): trait_item.get().current_value
for trait_item in obj.social_attributes}
return character_attribute_fields
def character_skill(self, obj):
character_skill_fields = {}
character_skill_fields['mental'] = {str(trait_item.get()): trait_item.get().current_value
for trait_item in obj.mental_skills}
character_skill_fields['physical'] = {str(trait_item.get()): trait_item.get().current_value
for trait_item in obj.physical_skills}
character_skill_fields['social'] = {str(trait_item.get()): trait_item.get().current_value
for trait_item in obj.social_skills}
return character_skill_fields
class MageSerializer(GenericCharacterFieldMixin, serializers.ModelSerializer):
player = serializers.ReadOnlyField(source='player.username')
arcana = serializers.SerializerMethodField()
def get_arcana(self, obj):
if obj:
return {str(arcana): arcana.current_value for arcana in obj.linked_arcana.all()}
class Meta:
model = Mage
fields = ('id', 'player', 'name', 'sub_race', 'faction', 'is_published',
'power_level', 'energy_trait', 'virtue', 'vice', 'morality', 'size',
'arcana', 'attributes', 'skills')
depth = 1
GenericCharacterFieldMixin is a Mixin of Fields for Characters, that are Generic, i.e. common to all types of characters.
I'd like my Mage Serializer to have these 'mixed in' rather than c/p then between all types of character (Mage is a type of character) hopefully this will increase DRYness in my webapp.
The issue is on the model I have this:
class NWODCharacter(models.Model):
class Meta:
abstract = True
ordering = ['updated_date', 'created_date']
name = models.CharField(max_length=200)
player = models.ForeignKey('auth.User', related_name="%(class)s_by_user")
....
def save(self, *args, **kwargs):
...
attributes = GenericRelation('CharacterAttributeLink')
skills = GenericRelation('CharacterSkillLink')
Which means I get this error:
TypeError at /characters/api/mages
<django.contrib.contenttypes.fields.create_generic_related_manager.<locals>.GenericRelatedObjectManager object at 0x00000000051CBD30> is not JSON serializable
Django Rest Framework thinks I want to serialize my generic relationship.
If I rename the fields in the model (s/attributes/foos/g, s/skills/bars/g) then I get a different (less clear?) error :
ImproperlyConfigured at /characters/api/mages
Field name `attributes` is not valid for model `ModelBase`.
How do I pull those methods and fields into a mixin, without confusing DRF?
Set SerializerMetaclass:
from rest_framework import serializers
class GenericCharacterFieldMixin(metaclass=serializers.SerializerMetaclass):
# ...
This is the solution recommended by DRF's authors.
Solutions suggested in the previous answers are problematic:
user1376455's solution hacks DRF into registering the mixin's fields in _declared_fields by declaring them on the child as different fields. This hack might not work in subsequent versions of the framework.
Nikolay Fominyh's solution changes the mixin to a fully fledged serializer (note that due to this, the name GenericCharacterFieldMixin is very unfortunate for a class which is not a mixin, but a serializer!). This is problematic because it takes the full Serializer class into the multiple inheritance, see the DRF issue for examples demonstrating why this is a bad idea.
Solution is simple as changing
class GenericCharacterFieldMixin():
to
class GenericCharacterFieldMixin(serializers.Serializer):
i had same issue and my google search brought me here. i managed to solve it.
since you are including attributes and skill fields in serialiser, you need to provide serialisation method for it.
this worked for me
class MageSerializer(GenericCharacterFieldMixin, serializers.ModelSerializer):
player = serializers.ReadOnlyField(source='player.username')
arcana = serializers.SerializerMethodField()
attributes = serializers.PrimaryKeyRelatedField(many=True,
read_only= True)
skills = serializers.PrimaryKeyRelatedField(many=True,
read_only= True)
def get_arcana(self, obj):
if obj:
return {str(arcana): arcana.current_value for arcana in obj.linked_arcana.all()}
class Meta:
model = Mage
fields = ('id', 'player', 'name', 'sub_race', 'faction', 'is_published',
'power_level', 'energy_trait', 'virtue', 'vice', 'morality', 'size',
'arcana', 'attributes', 'skills')
depth = 1

How to specify the database for Factory Boy?

FactoryBoy seem to always create the instances in the default database. But I have the following problem.
cpses = CanonPerson.objects.filter(persons__vpd=6,
persons__country="United States").using("global")
The code is pointing to the global database. I haven't found a way to specify the database within the factory:
class CanonPersonFactory(django_factory.DjangoModelFactory):
class Meta:
model = CanonPerson
django_get_or_create = ('name_first', 'p_id')
p_id = 1
name_first = factory.Sequence(lambda n: "name_first #%s" % n)
#factory.post_generation
def persons(self, create, extracted, **kwargs):
if not create:
# Simple build, do nothing.
return
if extracted:
# A list of groups were passed in, use them
for person in extracted:
self.persons.add(person)
Looks like Factory Boy does not provide this feature from box, but you can easily add it manually:
class CanonPersonFactory(django_factory.DjangoModelFactory):
class Meta:
model = CanonPerson
...
#classmethod
def _get_manager(cls, model_class):
manager = super(CanonPersonFactory, cls)._get_manager(model_class)
return manager.using('global')
...
This is now directly supported by adding the database attribute on Meta:
class CanonPersonFactory(django_factory.DjangoModelFactory):
class Meta:
model = CanonPerson
database = 'global'
...

Categories

Resources