Django custom field change not detected by migrations - python

I have multiple custom fields in Django. First, they extended PositiveSmallIntegerField as we used int choices like:
class BusinessLegalFormField(models.PositiveSmallIntegerField):
choices = [(1,'SRO'), (2, ....]
def __init__(self, *args, **kwargs):
if not args or not args[0]:
kwargs.setdefault('verbose_name', 'Právna forma')
kwargs['choices'] = self.choices
super().__init__(*args, **kwargs)
Then I changed it to CharField and TextChoices:
class BusinessLegalFormField(models.CharField):
class Choices(models.TextChoices):
ZIVNOST = 'zivnost', 'Živnosť'
SRO = 'sro', 'Spoločnosť s ručením obmedzeným'
AS = 'as', 'Akciová spoločnosť'
def __init__(self, *args, **kwargs):
if not args or not args[0]:
kwargs.setdefault('verbose_name', 'Právna forma')
kwargs.setdefault('max_length', 64)
kwargs['choices'] = self.Choices.choices
super().__init__(*args, **kwargs)
The problem is that I just realized that Django didn't detect the type change. When I change something like null=True to null=False it is detected, but the DB type is not changed from number to char.
How can I make it work?

This very situation, and the solution for it, is described in the documentation:
You can’t change the base class of a custom field because Django won’t detect the change and make a migration for it... You must create a new custom field class and update your models to reference it.
class CustomCharField(models.CharField):
...
class CustomTextField(models.TextField):
...

Related

How to remove fields on Django ModelForm?

I would like to build a form with dynamically fields depends on needs and i have tried this code but doesn't work, the model form show all fields.
forms.py:
class CustomModelForm(forms.ModelForm):
class Meta:
model = app_models.CustomModel
fields = '__all__'
def __init__(self, excluded_fields=None, *args, **kwargs):
super(CustomModelForm, self).__init__(*args, **kwargs)
for meta_field in self.fields:
if meta_field in excluded_fields:
# None of this instructions works
-> del self.fields[meta_field]
-> self.fields.pop(meta_field)
-> self.fields.remove(meta_field)
Anybody could help me ?
Thanks in advance.
Alternatively, could you use the modelform_factory?
from django.forms import modelform_factory
CustomModelForm = modelform_factory(MyModel, exclude=('field_1', 'field_2'))
That way you could determine the exclude fields before creating the form, and just pass them into the factory, no need to override the constructor then.
The problem was the list of excluded_fields came from model._meta.get_fields(), not is a list of strings, and the if condition didnt matched well because self.fields is a python ordereddict.
This code solve the problem:
class CustomModelForm(forms.ModelForm):
class Meta:
model = app_models.CustomModel
fields = '__all__'
def __init__(self, excluded_fields=None, *args, **kwargs):
super(CustomModelForm, self).__init__(*args, **kwargs)
show_fields = []
for field in excluded_fields:
show_fields.append(field.name)
for meta_field in list(self.fields):
if meta_field not in show_fields:
del self.fields[meta_field]

How to safely inherit from django models.Model class?

I have a following DB structure:
class Word(models.Model):
original = models.CharField(max_length=40)
translation = models.CharField(max_length=40)
class Verb(Word):
group = models.IntegerField(default=1)
In my view, I need to create a Word object first, and after determination of its group (depending on Word.original), create a Verb object, and save it.
What is the best way to inherit from the Word class and save the object as Verb ?
There are several solutions that I've tried:
1) Modification of the __init__ method in Verb :
class Verb(Word):
group = models.IntegerField(default=1)
def __init__(self, base_word):
self.original = base_word.original
self.translation = base_word.translation
This causes a lot of errors, since I'm overriding the django's built-in __init__ method.
2) Using super().__init__():
class Verb(Word):
group = models.IntegerField(default=1)
def __init__(self, base_word):
super().__init__()
self.original = base_word.original
self.translation = base_word.translation
Apparently, this works pretty well:
base_word = Word()
new_verb = Verb(base_word)
new_verb.save()
But there are two problems:
It causes an error when trying to see the objects in django admin page:
__init__() takes 2 positional arguments but 9 were given
This is still too much code, it doesn't feel right. I still need to write this:
self.original = base_word.original
self.translation = base_word.translation
in every subclass. And this is just an example. In real project, I have much more fields. I suppose there is a more elegant solution.
Overriding __init__ is not the right way to do this. Django models perform a lot of behind the scenes work, which overriding __init__ can conflict with, unless you do it in a safe way by following these rules:
Don't alter the signature of __init__ -- meaning you shouldn't change the arguments that the method accepts.
Perform the custom __init__ logic after calling the super().__init__(*args, **kwargs) method.
In this particular case, you might use django's proxy model inheritance features.
VERB = "V"
NOUN = "N"
# ...
WORD_TYPE_CHOICES = (
(VERB, "Verb"),
(NOUN, "Noun"),
# ...
)
class Word(models.Model):
original = models.CharField(max_length=40)
translation = models.CharField(max_length=40)
WORD_TYPE = "" # This is overridden in subclasses
word_type = models.CharField(
max_length=1,
blank=True,
editable=False, # So that the word type isn't editable through the admin.
choices=WORD_TYPE_CHOICES,
default=WORD_TYPE, # Defaults to an empty string
)
def __init__(self, *args, **kwargs):
# NOTE: I'm not 100% positive that this is required, but since we're not
# altering the signature of the __init__ method, performing the
# assignment of the word_type field is safe.
super().__init__(*args, **kwargs)
self.word_type = self.WORD_TYPE
def __str__(self):
return self.original
def save(self, *args, **kwargs):
# In the save method, we can force the subclasses to self-assign
# their word types.
if not self.word_type:
self.word_type = self.WORD_TYPE
super().save(*args, **kwargs)
class WordTypeManager(models.Manager):
""" This manager class filters the model's queryset so that only the
specific word_type is returned.
"""
def __init__(self, word_type, *args, **kwargs):
""" The manager is initialized with the `word_type` for the proxy model. """
self._word_type = word_type
super().__init__(*args, **kwargs)
def get_queryset(self):
return super().get_queryset().filter(word_type=self._word_type)
class Verb(Word):
# Here we can force the word_type for this proxy model, and set the default
# manager to filter for verbs only.
WORD_TYPE = VERB
objects = WordTypeManager(WORD_TYPE)
class Meta:
proxy = True
class Noun(Word):
WORD_TYPE = NOUN
objects = WordTypeManager(WORD_TYPE)
class Meta:
proxy = True
Now we can treat the different word types as if they were separate models, or access all of them together through the Word model.
>>> noun = Noun.objects.create(original="name", translation="nombre")
>>> verb = Verb(original="write", translation="escribir")
>>> verb.save()
# Select all Words regardless of their word_type
>>> Word.objects.values_list("word_type", "original")
<QuerySet [('N', 'name'), ('V', 'write')]>
# Select the word_type based on the model class used
>>> Noun.objects.all()
<QuerySet [<Noun: name>]>
>>> Verb.objects.all()
<QuerySet [<Verb: write>]>
This works with admin.ModelAdmin classes too.
#admin.register(Word)
class WordAdmin(admin.ModelAdmin):
""" This will show all words, regardless of their `word_type`. """
list_display = ["word_type", "original", "translation"]
#admin.register(Noun)
class NounAdmin(WordAdmin):
""" This will only show `Noun` instances, and inherit any other config from
WordAdmin.
"""

Django ModelForm inheritance and Meta inheritance

I have this ModelForm:
class Event(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(Event, self).__init__(*args, **kwargs)
##Here make some changes such as:
self.helper = FormHelper()
self.helper.form_method = 'POST'
##Many settings here which **i don't want to rewrite in 10 child classes**
class Meta:
model = Event
exclude = something...
widgets = some settings here also.
And this child ModelForm:
class UpgradedEvent(Event):
def __init__(self, *args, **kwargs):
super(UpgradedEvent,self).__init__(*args,**kwargs)
class Meta(Event.Meta):
model = UpgradedEvent
UpgradedEvent is a child of Event model but has some extra fields.
How can i inherit all the settings from the Event FORM into UpgradedEvent FORM?
When running the above code, it renders the Event form. Is there a way to inherit only the settings inside __init__ ?
EDIT
Check out the answer, it works great but keep in mind:
you need to create another instance of FormHelper in your child class, otherwise it won't work. So child class should look something like:
class UpgradedEvent(Event):
def __init__(self, *args, **kwargs):
super(UpgradedEvent,self).__init__(*args,**kwargs)
self.helper = FormHelper()
class Meta(Event.Meta):
model = UpgradedEvent
You can obtain the fields the Meta above, and extend the lists, etc.:
class UpgradedEventForm(EventForm):
def __init__(self, *args, **kwargs):
super(UpgradedEventForm,self).__init__(*args,**kwargs)
# some extra settings
# ...
# for example
self.fields['extra_field'].initial = 'initial value of extra field'
class Meta(EventForm.Meta):
model = UpgradedEvent
exclude = EventForm.Meta.exclude + ['extra_exclude1', 'extra_exclude2']
fields = EventForm.Meta.fields + ['extra_field']
So by using inheritance, we can add extra procedures to the __init__ function by performing some extra actions after the super(UpgradedEventForm, self) call, and wwe can access the attributes of our parent, and extend these.
Note that you better name your forms with a Form suffix, since now your models clash with your forms. As a result, your Form seems to have as model a reference to the Form itself. By using proper "nomenclature", you avoid a lot of mistakes.
Create FormWithSettings which will hold common settings for you form classes and inherit it
class FormWithSettings(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(FormWithSettings, self).__init__(*args, **kwargs)
##Here make some changes such as:
self.helper = FormHelper()
self.helper.form_method = 'POST'
##Many settings here which **i don't want to rewrite in 10 child classes**
class Meta:
exclude = something...
widgets = some settings here also.
class EventForm(FormWithSettings):
def __init__(self, *args, **kwargs):
super(EventForm, self).__init__(*args,**kwargs)
class Meta(FormWithSettings.Meta):
model = Event
class UpgradedEventForm(FormWithSettings):
def __init__(self, *args, **kwargs):
super(UpgradedEventForm, self).__init__(*args,**kwargs)
class Meta(FormWithSettings.Meta):
model = UpgradedEvent

Django Form field initial value when updating an instance

I have a custom Django ModelForm that I use to update a model instance.
This is the example model:
class MyModel(models.Model):
number = models.CharField(_("Number"), max_length=30, unique=True)
sent_date = models.DateField(_('Sent date'), null=True, blank=True)
When creating an instance I will pass only the number field, that is why I don't want the sent_date to be required.
Then I have a view that updates the sent_date field, using this custom form:
# Generic form updater
class MyModelUpdateForm(forms.ModelForm):
class Meta:
model = MyModel
fields = []
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make fields mandatory
if hasattr(self, 'required_fields'):
for field_name in self.required_fields:
self.fields[field_name].required = True
# Set initial values
if hasattr(self, 'initial_values'):
for field_name, value in self.initial_values.items():
self.initial[field_name] = value
class SentForm(MyModelUpdateForm):
required_fields = ['sent_date']
initial_values = {'sent_date': datetime.date.today()}
class Meta(MyModelUpdateForm.Meta):
fields = ['sent_date']
field_classes = {'sent_date': MyCustomDateField}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
MyModelUpdateForm is a generic ancestor for concrete forms like SentForm.
In my view whenever there is a GET I manually instantiate the form with:
my_form = SentForm({instance: my_model_instance})
So in this case I would expect the sent_date field to have an initial value set to today's date even tough the real model instance field is None.
If I inspect my_form object it does indeed have these attributes:
initial: {'sent_date': datetime.date(2018, 3, 1)}
instance: my_model_instance
fields: {'sent_date':
...: ...,
'initial': None # Why this is None?
...: ...
}
So apparently it should work but it doesn't: the field is always empty.
So I suspect that the value is coming from my_model_instance.sent_date that is in fact None.
The initial['sent_date'] = datetime.date(2018, 3, 1) is correct.
On the other side fields['sent_date']['initial'] = None it's not.
How can I always show the initial value when my_model_instance.sent_date is None?
Apparently I've solved with:
class MyModelUpdateForm(forms.ModelForm):
class Meta:
model = MyModel
fields = []
def __init__(self, *args, **kwargs):
initial = kwargs.get('initial', {})
if hasattr(self, 'initial_values') and not kwargs.get('data'):
for field_name, value in self.initial_values.items():
if not getattr(kwargs.get('instance', None), field_name, None):
initial[field_name] = value
kwargs.update({'initial': initial})
super().__init__(*args, **kwargs)
# Make fields mandatory
if hasattr(self, 'required_fields'):
for field_name in self.required_fields:
self.fields[field_name].required = True
Even tough it works I wouldn't mind a less hackish solution if anyone has any :)
I have this case in many places in my app without having any problem. However, I use a different way to set up initial value of some fields of an existing instance. Instead of:
self.initial[field_name] = value
I write, after having called super():
self.fields[field_name].initial = value
Can you try and tell the result ?

django : Change default value for an extended model class

I posted a similar question a while earlier, but this one is different. I have a model structure of related classes like:
class Question(models.Model):
ques_type = models.SmallIntegerField(default=TYPE1, Choices= CHOICE_TYPES)
class MathQuestion(Question):
//Need to change default value of ques_type here
// Ex: ques_type = models.SmallIntegerField(default=TYPE2, Choices= CHOICE_TYPES)
I want to change the default value of ques_type in the derived class. How should i accomplish this?
First, in this use of inheritance it is (at least according to my tests) not possible to change the default of the field in the child class. MathQuestion and Question share the same field here, changing the default in the child class affects the field in the parent class.
Now if what only differs between MathQuestion and Question is the behaviour (so, MathQuestion doesn't add any fields besides those defined in Question), then you could make it a proxy model. That way, no database table is created for MathQuestion.
from django.db import models
class Question(models.Model):
ques_type = models.SmallIntegerField(default=2)
class MathQuestion(Question):
def __init__(self, *args, **kwargs):
self._meta.get_field('ques_type').default = 3
super(MathQuestion, self).__init__(*args, **kwargs)
class Meta:
proxy = True
Test:
In [1]: from bar.models import *
In [2]: a=Question.objects.create()
In [3]: a.ques_type
Out[3]: 2
In [4]: b=MathQuestion.objects.create()
In [5]: b.ques_type
Out[5]: 3
Examples above are for proxy models. If you need to change default for model inherited from non-abstract base model you can do following:
from django.db import models
class Base(models.Model):
field_name = models.CharField(...)
class Child(Base):
def __init__(self, *args, **kwargs):
kwargs['field_name'] = kwargs.get('field_name') or 'default value'
super().__init__(*args, **kwargs)
Which will set default if it wasn't passed directly on Model(...) or Model.objects.create(...).
This is easy to do using a closure.
from django.db import models
# You start here, but the default of 2 is not what you really want.
class Question(models.Model):
ques_type = models.SmallIntegerField(default=2)
class MathQuestion(Question):
def __init__(self, *args, **kwargs):
self._meta.get_field('ques_type').default = 3
super(MathQuestion, self).__init__(*args, **kwargs)
class Meta:
proxy = True
The closure allows you to define it how you like it.
from django.db import models
def mkQuestion(cl_default=2):
class i_Question(models.Model):
ques_type = models.SmallIntegerField(default=cl_default)
class i_MathQuestion(i_Question):
def __init__(self, *args, **kwargs):
super(MathQuestion, self).__init__(*args, **kwargs)
return i_MATHQUESTION
MathQuestion = mkQuestion()
MathQuestionDef3 = mkQuestion(3)
# Now feel free to instantiate away.
Use a Form or ModelForm, on which you can override the field. For models, set the default value in it's __init__ method like so:
class Question(models.Model):
ques_type = models.SmallIntegerField(default=2)
class MathQuestion(Question):
def __init__(self, *args, **kwargs):
super(MathQuestion, self).__init__(*args, **kwargs)
self.ques_type = 3
class Meta:
proxy = True
Note that this has to be done after calling the parent class init.
https://docs.djangoproject.com/en/dev/topics/forms/modelforms/

Categories

Resources