Access model instance from model field - django

I want my Django custom model field to set an attribute on the model instance.
I'm sure it's not working this way but here is an example:
class MyField(models.Field):
__metaclass__ = models.SubfieldBase
def __init__(self, *args, **kwargs):
super(MyField, self).__init__(*args, **kwargs)
model_instance = ????
setattr(model_instance, "extra_attribute", "It's working!")
class MyModel(models.Model):
my_field = MyField()
model_instance = MyModel.objects.get(pk=123)
print model_instance.extra_attribute # output: "It's working!"
Django's ForeignKey model field is doing a similar thing, so it is possible :P
I think ForeignKey field is using the contribute_to_class method.

You can create a special Proxy class that will replace the field. When getting or setting field value, you can use 'obj' attribute to access model instance. See this example:
class ObjectField(models.PositiveSmallIntegerField):
class ObjectProxy:
def __init__(self, field):
self.field = field
def __get__(self, obj, model):
if obj is None:
return self.field # this is required for initializing model field
value = obj.__dict__[self.field.name] # get actual field value
# ... here you can do something with value and model instance ("obj")
return value
def __set__(self, obj, value):
# same here
obj.__dict__[self.field.name] = value
def contribute_to_class(self, cls, name):
super().contribute_to_class(cls, name)
# set up our proxy instead of usual field
setattr(cls, name, ObjectField.ObjectProxy(self))

You do not have access to the model instance from inside your Field object, sorry. Django's ForeignKey accomplishes the foo_id thing by having separate name and attname fields, but the actual setting of foo_id = 123 is done the same way as all the other model fields, deep in the QuerySet code, without interacting with the field classes.
And conceptually, what you're trying to do is a bad idea - action-at-a-distance. What if adding a particular field could cause bugs in unrelated model functionality, say, if an attribute another field was expecting got overridden? It would be difficult to debug, to say the least. I don't know what your underlying goal is, but it should probably be done in model code, not a field class.
Here's a ModelField that does what you want:
https://gist.github.com/1987190
That's actually pretty old (like maybe pre-1.0, don't remember now), had to dust it off a bit - I'm not sure if it still works. But it's definitely doable, hopefullly this gives you an idea.

init is called when Django processes the Model Class, not the Model Instance. So, you can add the attribute to the Model Class (e.g. by using 'add_to_class' http://www.alrond.com/en/2008/may/03/monkey-patching-in-django/ ). To add the attribute to the instance you should override the init of the instance (but I think this is not your case).

How about
model_instance = SomeExtraModel.objects.get(pk=1456)
replacing 1456 with something that makes sense

Related

Django one to many saving model is giving error

I have an exam model and I am trying to add a form to add multiple Quiz instance in the parent model. I am getting the following error
raise ValueError(
ValueError: Cannot assign "": "exam_quiz.quiz" must be a "Quiz" instance.
class ExamQuizAdminForm(forms.ModelForm):
class Meta:
model = exam_quiz
exclude = ['registrationDate','examdate']
quiz = forms.ModelMultipleChoiceField(
queryset=Quiz.objects.all(),
required=False,
label=_("Quiz"),
widget=FilteredSelectMultiple(
verbose_name=_("Quiz"),
is_stacked=False))
def __init__(self, *args, **kwargs):
super(ExamQuizAdminForm, self).__init__(*args, **kwargs)
if self.instance.pk:
self.fields['quiz'].initial = \
self.instance.quiz
def save(self, commit=True):
exam_quiz = super(ExamQuizAdminForm, self).save(commit=False)
exam_quiz.save()
exam_quiz.quiz_set.set(self.cleaned_data['Quiz'])
self.save_m2m()
return exam_quiz
class ExamQuizAdmin(admin.ModelAdmin):
form = ExamQuizAdminForm
list_display = ('examname','month','year')
Assuming exam_quiz.quiz is a m2m field...
I don't think you need to override the save function in this case. Use the save_m2m() function if you want to, say, add additional non form data, and then save both the normal and m2m data from the form. According to the docs
"Calling save_m2m() is only required if you use save(commit=False).
When you use a save() on a form, all data – including many-to-many
data – is saved without the need for any additional method calls."
Here, however, it looks like the only change you're making is adding the m2m data, which the normal save can handle.
Also, you might try making the Quiz lowercase, eg,
exam_quiz.quiz_set.set(self.cleaned_data['quiz'])
as I don't think 'label' or 'verbose_name' affect the HTML field name.

How to update choices option of a field instance, not of the entire field?

I have a model field with choices:
class MyModel(models.Model):
myfield = models.CharField(max_length=1000, choices=(('a','a'),('b','b'))
I know that I can access in forms this specific field and override its choices option like that:
self.instance._meta.get_field(field_name).choices = (('c','c'),('d','d'))
but that will change the choices for the entire model, not for an individual instance. What is the correct way to do it for one specific instance only or it is not possible?
I'm not aware of any way to change the model's field choices on a per-instance basis, but if it's for a form you can override the form's field choices (example written from memory so it might no be 100% accurate):
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, *args, **kw):
super(MyModelForm, self).__init__(*args, **kw)
if some_condition:
self.fields["myfield"].choices = (...)
Important: you want to override self.fields["myfield"], not self.myfield - the latter is a class attribute so changing it would affect all MyModelForm instances for the current process, something you won't usually notice when running the dev server but that will cause very erratic behaviour on production.

Need help understanding many and source fields in a serializer

I am currently trying to familiarize myself with DRF and while going through a tutorial these serializers were used
class EmbeddedAnswerSerializer(serializers.ModelSerializer):
votes = serializers.IntegerField(read_only=True)
class Meta:
model = Answer
fields = ('id', 'text', 'votes',)
class QuestionSerializer(serializers.ModelSerializer):
answers = EmbeddedAnswerSerializer(many=True,source='answer_set')
class Meta:
model = Question
fields = ('id', 'answers', 'created_at', 'text', 'user_id',)
These are the models
class Question(models.Model):
user_id = models.CharField(max_length=36)
text = models.CharField(max_length=140)
created_at = models.DateTimeField(auto_now_add=True)
class Answer(models.Model):
question = models.ForeignKey(Question,on_delete=models.PROTECT)
text = models.CharField(max_length=25)
votes = models.IntegerField(default=0)
My question is in the statement in the Question serializer
answers = EmbeddedAnswerSerializer(many=True,source='answer_set')
what is the purpose of many = True and source='answer_set' ?
I read from the documentation the following regarding many=True
You can also still use the many=True argument to serializer classes.
It's worth noting that many=True argument transparently creates a
ListSerializer instance, allowing the validation logic for list and
non-list data to be cleanly separated in the REST framework codebase.
I am confused by what that means? If I remove many=True from the code I get the error
AttributeError at /api/quest/1/2/
Got AttributeError when attempting to get a value for field `text` on serializer `EmbeddedAnswerSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `RelatedManager` instance.
Original exception text was: 'RelatedManager' object has no attribute 'text'.
Can anyone explain what many=True does and what source field does?
Adding to the answer above by #neverwalkaloner
Many = True
many=True signals that there is more than one object (an iterable) being passed to the serializer. Passing this field in turn triggers the many_init within BaseSerializer to automagically create a ListSerializer instance.
Source Code Snippet:
def __new__(cls, *args, **kwargs):
# We override this method in order to automagically create
# `ListSerializer` classes instead when `many=True` is set.
if kwargs.pop('many', False):
return cls.many_init(*args, **kwargs)
return super(BaseSerializer, cls).__new__(cls, *args, **kwargs)
Source = "xyz"
This tells DRF which object attribute supplies the value for the field. The default assumption is that the field name declared on the serializer is the same as the field on the object instance that supplies the value. In cases where this is not true, source allows you to explicitly supply the object instance where the serializer will look for the value. Here's a peek into the def bind(self, field_name, parent) inside serializers.fields where this happens
Source Code Snippet:
# self.source should default to being the same as the field name.
if self.source is None:
self.source = field_name
# self.source_attrs is a list of attributes that need to be looked up
# when serializing the instance, or populating the validated data.
if self.source == '*':
self.source_attrs = []
else:
self.source_attrs = self.source.split('.')
Finally the value is gotten as follows using the source and source_attrs declared in bind:
def get_attribute(self, instance):
"""
Given the *outgoing* object instance, return the primitive value
that should be used for this field.
"""
try:
return get_attribute(instance, self.source_attrs)
except (KeyError, AttributeError) as exc:
Assuming a Question can have multiple Answers, your approach is correct.
The problem appears to be that the source you supplied is the RelatedManager instance itself, and not the queryset of Answer objects. I assumed DRF resolves this accurately, can you try changing it to source='answer_set.all'.
answer_set is the default RelatedManager name given by Django. It might be wise to name your backward relationship using related_name in the Django model. This can be achieved by changing:
question = models.ForeignKey(Question,on_delete=models.PROTECT, related_name='answers')
Probably not the best explanation and someone can add more details but briefly many=True tells to serializer that it will take list of objects for serilizing proccess. In other words it's just a trigger that allows you to specify will you serialize, well, many objects at once, or just single object.
source on the other side specify which attribute of objects should be serializing with current serializer's field.
In practice this line
answers = EmbeddedAnswerSerializer(many=True, source='answer_set')
means that you want to serialize answer_set attribute of Question object with EmbeddedAnswerSerializer. Since answer_set is list of object you should add many=True as argument to make serializer aware that it will work with list of objects instead of single object.

How to use ModelForm with neo4django?

This seems like a bug but I just want to make sure I'm consuming the API properly.
It seems that support for django's modelform isn't supported on neo4django. Here's what I have:
Simple class:
from neo4django.db import models
class Person(models.NodeModel):
name = models.StringProperty()
The modelform:
class PersonForm(forms.ModelForm):
class Meta:
model = Person
Will trigger exception:
'super' object has no attribute 'editable'
I posted details as an issue:
https://github.com/scholrly/neo4django/issues/135
Because when Django goes to lookup field information using the model's _meta information, it finds a BoundProperty instead of a StringProperty or Property (which has a member called 'editable', but BoundProperty doesn't).
Is there a workaround, or is this an actual bug? Any ideas on how to fix the bug? I'm not familiar with the library codebase.
Thanks!
Below is a reasonable (and quick) workaround for anyone using neo4j with Django.
This solution requires that field names on the form have the exact same name as the attributes of the model.
Inherit the form from this class and set the model under the form class Meta class:
class NeoModelForm(forms.Form):
def __init__(self, *args, **kwargs):
super(NeoModelForm, self).__init__(*args, **kwargs)
self._meta = getattr(self, 'Meta', None)
if not self._meta:
raise Exception('Missing Meta class on %s' % str(self.__class__.__name__))
if not hasattr(self._meta, 'model'):
raise Exception('Missing model on Meta class of %s' % str(self.__class__.__name__))
def save(self, commit=True):
if not self.is_valid():
raise Exception('Failed to validate')
instance = self._meta.model(**self.cleaned_data)
if commit:
instance.save()
return instance
Now you can create a form class like this:
class PersonForm(NeoModelForm):
name = forms.CharField(widget=forms.TextInput())
class Meta:
model = Person
And still be able to save a model instance from a valid form:
form = formclass(request.POST)
if form.is_valid():
obj = form.save()
Plus the commit argument will give you the same solution as django's modelform class- but I didn't bother to implement to save_m2m functionality (which doesn't seem relevant for neo4j as a backend).

Django Forms: Hidden model field?

I've got a Form. I want to include a hidden field that returns a model. I'll set it's value in the view; I just need it to be posted along to the next page.
What field am I supposed to use in the form class?
A hidden field that returns a model? So a model instance ID?
The forms.HiddenInput widget should do the trick, whether on a FK field or CharField you put a model instance ID in.
class MyForm(forms.Form):
hidden_2 = forms.CharField(widget=forms.HiddenInput())
hidden_css = forms.CharField(widget=forms.MostWidgets(attrs={'style': 'display:none;'}))
I suppose the fastest way to get this working is
class MyForm(forms.Form):
model_instance = forms.ModelChoiceField(queryset=MyModel.objects.all(), widget=forms.HiddenInput())
form = MyForm({'model_instance': '1'})
form.cleaned_data['model_instance']
But I don't like the idea of supplying MyModel.objects.all() if you're going to specify one item anyways.
It seems like to avoid that behavior, you'd have to override the form __init__ with a smaller QuerySet.
I think I prefer the old fashioned way:
class MyForm(forms.Form):
model_instance = forms.CharField(widget=forms.HiddenInput())
def clean_model_instance(self):
data = self.cleaned_data['model_instance']
if not data:
raise forms.ValidationError()
try:
instance = MyModel.objects.get(id=data)
except MyModel.DoesNotExist:
raise forms.ValidationError()
return instance
The approach in Yuji's answer uses a clean_model_instance method on the form which is fine if you're only ever doing this once in your code base. If you do it more often, then you might benefit from implementing a custom model field.
This is the code I have:
from django import forms
class ModelField(forms.Field):
Model = None
def prepare_value(self, value):
"""Inject entities' id value into the form's html data"""
if isinstance(value, self.Model):
return value.id
return value
def to_python(self, value):
"""More or less stolen from ModelChoiceField.to_python"""
if value in self.empty_values:
return None
try:
value = self.Model.objects.get(id=value)
except (ValueError, self.Model.DoesNotExist):
raise forms.ValidationError('%s does not exist'
% self.Model.__class__.__name__.capitalize())
return value
If you use that as a base class and then specialise it with your own models then it becomes a useful based. For example:
# In app/fields.py
from .models import CustomModel
class CustomModelField(ModelField):
Model = CustomModel
Then you can pair that with whatever widget you need at the time:
# in app/forms.py
class MyForm(forms.Form):
hidden_custom_model_field = CustomModelField(widget=forms.HiddenInput())
other_widget_custom_model_field = CustomModelField(widget=MyCustomWidget())