attach mixin to model when it is created based on a field? - django

Can I attach a Mixin automatically to a Model when it is created based on a field?
I have been looking at the 3 options of Model inheritance https://docs.djangoproject.com/en/dev/topics/db/models/#model-inheritance but I am not sure how to achieve what I am looking to do and if it is possible.
I just want ONE table where I will store all the fields, no matter if one field is just for
one of the type.
This table will have a type field.
class Example(models.Model):
objects = ExampleManager()
description = models.CharField(max_length=100, blank=True, null=True)
# All the fields needed for all the types, the common and specific fields
type = models.CharField(max_length=2, choices=('T1','T2'))
Now, when I do:
all = Example.objects.all()
for a in all:
a.quack()
I would like that quack is different according to the type field.
But I would like to avoid to write a quack function in the Example class model with several if.
I would like to encapsulate into classes the logic for each subtype.
Can I attach a Mixin automatically to the Model when it is created based on the type field?
Thanks

You can do this by defining a field subclass with a contribute_to_class method. That's how fields with the choices attribute define a get_FOO_display method on their models.
contribute_to_class is passed the model class, and the name this field is being defined as. You can use the model class to add extra methods. It might work something like this:
class DuckField(models.CharField):
def contribute_to_class(self, cls, name):
super(DuckField, self).contribute_to_class(cls, name)
# method to be added
def quack(self):
return '%s quacks!' % self
# add it to the model
cls.quack = quack

Related

Can I override default CharField to ChoiceField in a ModelForm?

I have a (horrible) database table that will be imported from a huge spreadsheet. The data in the fields is for human consumption and is full of "special cases" so its all stored as text. Going forwards, I'd like to impose a bit of discipline on what users are allowed to put into some of the fields. It's easy enough with custom form validators in most cases.
However, there are a couple of fields for which the human interface ought to be a ChoiceField. Can I override the default form field type (CharField)? (To clarify, the model field is not and cannot be constrained by choices, because the historical data must be stored. I only want to constrain future additions to the table through the create view).
class HorribleTable( models.Model):
...
foo = models.CharField( max_length=16, blank=True, ... )
...
class AddHorribleTableEntryForm( models.Model)
class Meta:
model = HorribleTable
fields = '__all__' # or a list if it helps
FOO_CHOICES = (('square', 'Square'), ('rect', 'Rectangular'), ('circle', 'Circular') )
...?
Perhaps you could render the forms manually, passing the options through the context and make the fields in html.
Take a look at here:https://docs.djangoproject.com/en/4.0/topics/forms/#rendering-fields-manually
I think you can easily set your custom form field as long it will match the data type with the one set in your model (e.g. do not set choices longer than max_length of CharField etc.). Do the following where foo is the same name of the field in your model:
class AddHorribleTableEntryForm(forms.ModelForm):
foo = forms.ChoiceField(choices=FOO_CHOICES)
class Meta:
model = HorribleTable
...
I think this is perfectly fine for a creation form. It's will not work for updates as the values in the DB will most probably not match your choices. For that, I suggest adding a second form handling data updates (maybe with custom permission to restrict it).
UPDATE
Another approach will be to override the forms init method. That way you can handle both actions (create and update) within the same form. Let the user select from a choice field when creating an object. And display as a normal model field for existing objects:
class AddHorribleTableEntryForm(forms.ModelForm):
foo = forms.ChoiceField(choices=FOO_CHOICES)
class Meta:
model = HorribleTable
fields = '__all__' # or a list if it helps
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
instance = kwargs.get("instance", None)
if instance is None:
self.fields["foo"].widget = forms.widgets.Select(choices=self.FOO_CHOICES)

Custom django foreignfield

Anybody knows how to create a foreignkey field and make it always point to same model, so far I got these.
class PanMachineTimeUnitField(models.ForeignKey):
def __init__(self, **kwargs):
to = 'panbas.PanBasTimeUnit'
kwargs['verbose_name'] = _('Machine Unit')
kwargs['related_name'] = 'machine_unit'
super(PanMachineTimeUnitField, self).__init__(to, **kwargs)
But I got errors when on start.
I aim to use it like,
machine_unit = PanMachineTimeUnitField()
No further declarations needed.
Edit:
I want this because, I will have this foreignkey in quiet a few places. If I want to change the verbose_name of field, I want all of my fields to be affected by this change. Verbose name was an example, it may be an another attribute.
I dont want to use settings py to declare the defaults, either.
I recommend that you use only a simple function to create a similarly pre-configured instance of ForeignKey: (not an instance of subclass of ForeignKey)
def pan_machine_time_unit_field(**kwargs):
othermodel = 'panbas.PanBasTimeUnit'
on_delete = models.DO_NOTHING # or what you need
kwargs['verbose_name'] = 'Machine Unit'
kwargs.setdefault('related_name', '+')
# or: kwargs.setdefault('related_name', "%(app_label)s_%(class)s_related",
return models.ForeignKey(othermodel, on_delete, **kwargs)
class C(models.Model):
machine_unit = pan_machine_time_unit_field()
# or:
# machine_unit = pan_machine_time_unit_field(related_name='klass_c_children')
The related_name attribute is a name used for backward relation from the target object of othermodel to all objects that reference it. That name must be unique on othermodel ('panbas.PanBasTimeUnit', usually something with app and class name that is unique enough) or that name can be '+' if you don't want to create a backward relationship query set. Both variants are implied in the example. Also remember on_delete.
If you would really need to create a subclass (which makes sense if more methods need be customized), you must also define a deconstruct method for migrations. It would be complicated if you need to modify such subclass later. It can be never removed, renamed etc. due to migrations on a custom field. On the other hand, if you create a simple instance of ForeignKey directly by a function, all about migrations can be ignored.
EDIT
Alternatively you can create an abstract base model with that field and create new models by inheritance or multiple inheritance:
class WithPanBasTimeUnit(models.Model):
machine_unit = models.ForeignKey(
'panbas.PanBasTimeUnit',
models.DO_NOTHING,
verbose_name=_('Machine Unit'),
related_name='%(app_label)s_%(class)s_related'
)
class Meta:
abstract = True
class ExampleModel(WithPanBasTimeUnit, ...or more possible base models...):
... other fields
This solution (inspired by an invalid soution Ykh) useful if you want to add a method to models with that field or to add more fields together, otherwise the original solution is easier.
class PanBasTimeUnit(models.Model):
machine_unit = models.ForeignKey('self', blank=True, null=True,
verbose_name=u'parent')
use 'self' or 'panbas.PanBasTimeUnit' will fine.
You can not have several Foreign Keys to a model with same related_name.
Indeed, on a PanBasTimeUnit instance, which manager should Django return when calling <instance>.machine_unit? This is why you have to be carefull on related models and abstract classes.
It should work fine if you remove kwargs['related_name'] = 'machine_unit' in your code, and replace it with kwargs['related_name'] = "%(app_label)s_%(class)s_related" or something similar.
A slight modification in your attempt should do your work.
class PanMachineTimeUnitField(models.ForeignKey):
def __init__(self, **kwargs):
kwargs["to"] = 'panbas.PanBasTimeUnit'
kwargs['verbose_name'] = _('Machine Unit')
kwargs['related_name'] = 'machine_unit'
super(PanMachineTimeUnitField, self).__init__(**kwargs)
why not use directly machine_unit = models.ForeignKey(panbas.PanBasTimeUnit, verbose_name=_('Machine Unit'), related_name='machine_unit')) ?

Copying a Django Field description from an existing Model to a new one

I'm trying to dynamically generate a new Model, based on fields from an existing Model. Both are defined in /apps/main/models.py. The existing model looks something like this:
from django.db import models
class People(models.Model):
name = models.CharField(max_length=32)
age = models.IntegerField()
height = models.IntegerField()
I have a list containing the names of fields that I would like to copy:
target_fields = ["name", "age"]
I want to generate a new model the has all of the Fields named in target_fields, but in this case they should be indexed (db_index = True).
I originally hoped that I would just be able to iterate over the class properties of People and use copy.copy to copy the field descriptions that are defined on it. Like this:
from copy import copy
d = {}
for field_name in target_fields:
old_field = getattr(People, field_name) # alas, AttributeError
new_field = copy(old_field)
new_field.db_index = True
d[field_name] = new_field
IndexedPeople = type("IndexedPeople", (models.Model,), d)
I wasn't sure if copy.copy()ing Fields would work, but I didn't get far enough to find out: the fields listed in the class definition don't aren't actually included as properties on the class object. I assume they're used for some metaclass shenanigans instead.
After poking around in the debugger, I found some type of Field objects listed in People._meta.local_fields. However, these aren't just simple description that can be copy.copy()ed and used to describe another model. For example, they include a .model property referring to People.
How can I create a field description for a new model based on a field of an existing model?
From poking around in the debugger and the source: all Django models use the ModelBase metaclass defined in /db/models/base.py. For each field in a model's class definition, ModelBase's .add_to_class method will call the field's .contribute_to_class method.
Field.contribute_to_class is defined in /db/models/fields/__init__.py and it is what's responsible for associating a field definition with a particular model. The field is modified by adding the .model property and by calling the .set_attributes_from_name method with the name used in the model's class definition. This in turn adds adds the .attname and .column properties and sets .name and .verbose_name if necessary.
When I inspect the __dict__ property of a newly-defined CharField and compare it with that of a CharField that was already associated with a model, I also see that these are the only differences:
The .creation_counter property is unique for each instance.
The .attrname, .column and .model properties do not exist on the new instance.
The .name and .verbose_name properties is None on the new instance.
It doesn't seem possible to distinguish between .name/.verbose_name properties that were manually specified to the constructor and ones that were automatically generated. You'll need to chose either to always reset them, ignoring any manually-specified values, or never clear them, which would cause them to always ignore any new name they were given in the new model. I want to use the same name as the original fields, so I am not going to touch them.
Knowing what differences exist, I am using copy.copy() to clone the existing instance, then apply these changes to make it behave like a new instance.
import copy
from django.db import models
def copy_field(f):
fp = copy.copy(f)
fp.creation_counter = models.Field.creation_counter
models.Field.creation_counter += 1
if hasattr(f, "model"):
del fp.attname
del fp.column
del fp.model
# you may set .name and .verbose_name to None here
return fp
Given this function, I create the new Model with the following:
target_field_name = "name"
target_field = People._meta.get_field_by_name(target_field_name)[0]
model_fields = {}
model_fields["value"] = copy_field(target_field)
model_fields["value"].db_index = True
model_fields["__module__"] = People.__module__
NewModel = type("People_index_" + field_name, (models.Model,), model_fields)
It works!
Solution
There is build in way for fields copying Field.clone() - method which deconstructs field removing any model dependent references:
def clone(self):
"""
Uses deconstruct() to clone a new copy of this Field.
Will not preserve any class attachments/attribute names.
"""
name, path, args, kwargs = self.deconstruct()
return self.__class__(*args, **kwargs)
So you can use following util to copy fields ensuring that you'll not accidentally affect source fields of model you're copying from:
def get_field(model, name, **kwargs):
field = model._meta.get_field(name)
field_copy = field.clone()
field_copy.__dict__.update(kwargs)
return field_copy
Also can pass some regular kwargs like verbose_name and etc:
def get_field_as_nullable(*args, **kwargs):
return get_field(*args, null=True, blank=True, **kwargs)
Does not work for m2m fields inside of model definition. (m2m.clone() on model definition raises AppRegistryNotReady: Models aren't loaded yet)
Why this instead of abstract models?
Well, depends on case. Some times you don't need inheristance but actuall fields copying. When? For example:
I have a User model and model which represents an application (document for user data update request) for user data update:
class User(models.Model):
first_name = ...
last_name = ...
email = ...
phone_number = ...
birth_address = ...
sex = ...
age = ...
representative = ...
identity_document = ...
class UserDataUpdateApplication(models.Model):
# This application must ONLY update these fields.
# These fiends must be absolute copies from User model fields.
user_first_name = ...
user_last_name = ...
user_email = ...
user_phone_number = ...
So, i shouldn't carry out duplicated fields from my User model to abstract class due to the fact that some other non-user-logic-extending model wants to have exact same fields. Why? Because it's not directly related to User model - User model shouldn't care what depends on it (excluding cases when you want to extend User model), so it shouldn't be separated due to fact that some other model with it's own non User related logic want's to have exact same fields.
Instead you can do this:
class UserDataUpdateApplication(models.Model):
# This application must ONLY update these fields.
user_first_name = get_field(User, 'first_name')
user_last_name = get_field(User, 'last_name')
user_email = get_field(User, 'user_email')
user_phone_number = get_field(User, 'phone_number')
You also would make som util which would generate some abc class "on fly" to avoid code duplication:
class UserDataUpdateApplication(
generate_abc_for_model(
User,
fields=['first_name', 'last_name', 'email', 'phone_number'],
prefix_fields_with='user_'),
models.Model,
):
pass

How to add a custom validator in a subclass that validates a parent field in Django

In Django I have a parent abstract class with a few common fields, and some subclasses that add more fields. In some of these subclasses I would like to add custom validators that validate fields on the parent class.
class Template(models.Model):
text = models.CharField(max_length=200)
class GlobalTemplate(Template):
function = models.ManyToManyField(Function, null=True, blank=True)
I can easily add them on the field in the parent class like this:
class Template(models.Model):
text = models.CharField(max_length=200, validators=[validate_custom])
But in this case I want to add the validator to my child class GlobalTemplate, but have it attached to the text field.
Is this possible in Django?
Thanks!
Field validators are stored in field.validators, it's a list, so you basically need to append your validators there.
To access the field instance, you'll have to play a little bit with the _meta attribute of your object (note that you're not supposed to play with this attribute, so when you update django, you'll have to check that it has not changed). Here's how you could do it:
def get_fields_dict(self):
return dict((field.name, field) for field in self._meta.fields)
def __init__(self, *args, **kwargs):
super(GlobalTeplate, self).__init__(*args, **kwargs)
text_field = self.get_fields_dict()['text']
text_field.validators.append(validate_custom)

Django: making relationships in memory without saving to DB

I have some models with relationships like this:
class Item(model.Model):
name = models.CharField()
class Group(models.Model):
item = models.ManyToManyField(Item)
class Serie(models.Model):
name = models.CharField()
chart = models.ForeignKey(Chart)
group = models.ForeignKey(Group)
class Chart(models.Model):
name = models.CharField()
I need to create a Chart object on the fly, without saving to the DB. But I can't do it because Django tries to use the objects primary keys when assigning the relationships.
I just want Group.add(Item()) to work without having to save the objects to the DB.
Is there any simple way around this?
Reviving here for the sake of future readers:
I've gotten around this use case by defining a private attribute that represents the relationship inside the classes and a property to inspect wether the object can be retrieved from the DB or resides in memory.
Here is a simple example:
class Parent(models.Model):
_children = []
name = models.CharField(max_length=100)
#property
def children(self):
if _children:
return self._children
else:
return self.children_set.all()
def set_virtual_children(self, value): # could use a setter for children
self._children = value # Expose _children to modification
def some_on_the_fly_operation(self):
print(','.join([c.name for c in self.children]))
class Children(models.Model):
parent = models.ForeignKey(Parent)
name = models.CharField(max_length=100)
This way, I can set the "virtual children" and use all the defined methods "on the fly"
EDIT: It seems that approach described here isn't enough for django to allow adding to the ManyToMany relationship.
Have you tried to add primary_key=True and unique=True to the name attribute of the Item model. Then doing Group.add(Item("item_name_here")) should work if you have the possibility to create the name on the fly.
I didn't test it, but I think your way failed because add() wants to use the primary-key which by default is the autoincrementing id that is assigned when it is saved to the database.