Can I override default CharField to ChoiceField in a ModelForm? - django

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)

Related

Listing only usable values in OneToOneField Django

I want to list only usable items in OneToOneField not all items, its not like filtering values in ChoiceField because we need to find out only values which can be used which is based on the principle that whether it has been used already or not.
I am having a model definition as following:
class Foo(models.Model):
somefield = models.CharField(max_length=12)
class Bar(models.Model):
somefield = models.CharField(max_length=12)
foo = models.OneToOneField(Foo)
Now I am using a ModelForm to create forms based on Bar model as:
class BarForm(ModelForm):
class Meta:
model = Bar
Now the problem is in the form it shows list of all the Foo objects available in database in the ChoiceField using the select widget of HTML, since the field is OneToOneField django will force to single association of Bar object to Foo object, but since it shows all usable and unusable items in the list it becomes difficult to find out which values will be acceptable in the form and users are forced to use hit/trial method to find out the right option.
How can I change this behavior and list only those items in the field which can be used ?
Although this is an old topic I came across it looking for the same answer.
Specifically for the OP:
Adjust your BarForm so it looks like:
class BarForm(ModelForm):
class Meta:
model = Bar
def __init__(self, *args, **kwargs):
super(BarForm, self).__init__(*args, **kwargs)
#only provide Foos that are not already linked to a Bar, plus the Foo that was already chosen for this Bar
self.fields['foo'].queryset = Foo.objects.filter(Q(bar__isnull=True)|Q(bar=self.instance))
That should do the trick. You overwrite the init function so you can edit the foo field in the form, supplying it with a more specific queryset of available Foo's AND (rather important) the Foo that was already selected.
For my own case
My original question was: How to only display available Users on a OneToOne relation?
The Actor model in my models.py looks like this:
class Actor(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name = 'peactor')
# lots of other fields and some methods here
In my admin.py I have the following class:
class ActorAdmin(admin.ModelAdmin):
# some defines for list_display, actions etc here
form = ActorForm
I was not using a special form before (just relying on the basic ModelForm that Django supplies by default for a ModelAdmin) but I needed it for the following fix to the problem.
So, finally, in my forms.py I have:
class ActorForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ActorForm, self).__init__(*args, **kwargs)
#only provide users that are not already linked to an actor, plus the user that was already chosen for this Actor
self.fields['user'].queryset = User.objects.filter(Q(peactor__isnull=True)|Q(peactor=self.instance))
So here I make an ActorForm and overwrite the __init__ method.
self.fields['user'].queryset =
Sets the queryset to be used by the user formfield. This formfield is a ModelChoiceField
by default for a OneToOneField (or ForeignKey) on a model.
Q(peactor__isnull=True)|Q(peactor=self.instance)
The Q is for Q-objects that help with "complex" queries like an or statement.
So this query says: where peactor is not set OR where peactor is the same as was already selected for this actor
peactor being the related_name for the Actor.
This way you only get the users that are available but also the one that is unavailable because it is already linked to the object you're currently editing.
I hope this helps someone with the same question. :-)
You need something like this in the init() method of your form.
def __init__(self, *args, **kwargs):
super(BarForm, self).__init__(*args, **kwargs)
# returns Bar(s) who are not in Foo(s).
self.fields['foo'].queryset = Bar.objects.exclude(id__in=Foo.objects.all().values_list(
'bar_id', flat=True))
PS: Code not tested.

django modelform use normal field for relation

I am building a little message system with this model:
class Mail(models.Model):
sender = models.ForeignKey(Character, related_name="+")
to = models.ForeignKey(Character, related_name="+")
sent_at = models.DateTimeField(auto_now_add=True)
subject = models.CharField(max_length=127)
body = models.TextField()
now i made a modelform from this:
class ComposeForm(forms.ModelForm):
class Meta:
model = Mail
exclude = ["folder", "sender", "sent_at"]
however this gives my "to" field a drop down list with all possible characters.
Id like to make this in a normal charfield (later with auto completer) instead of this drop down.
Any idea how i can achieve this?
I've been in a similar place and I found 2 solutions, depending on the needs. The first one is suposing you're going to use something like a select2 and get the query via ajax:
class ComposeForm(forms.ModelForm):
to_char = forms.CharField(max_length=255, required=False) # use the name you want
class Meta:
model = Mail
exclude = ["folder", "sender", "sent_at","to"]
So to_char is empty, and then you manage that field as you want, and when you do the POST, you'll get the value of to_char in the view, and assign to the model where you need it.
The other option I suggest is to use a ModelChoiceField instead of Charfield like this:
class ComposeForm(forms.ModelForm):
to_char = forms.ModelMultipleChoiceField(queryset=Character.objects.all())
class Meta:
model = Mail
exclude = ["folder", "sender", "sent_at","to"]
This will make easier to use an external select tool as select2 (without AJAX)
I choose the first one when the model has thousands of possible choices, so the select hasn't to load all the choices and the page will be fast. I use a select2 in the template over this field, in this case to_char, that loads the options in an AJAX view
I use the second one when there are hundred of choices and using the autocomplete of select2 over this field has no problems, I think if you don't have too many choices this will be the best for you, the ModelChoiceField, you can attach an autocomplete without any trouble
I might have found even a neater solution to this problem. By overriding the default form field in this way:
class ComposeForm(forms.ModelForm):
class Meta:
model = Mail
exclude = ["folder", "sender", "sent_at"]
widgets = {'to': forms.TextInput()}
1) If you do not want to display all the values, then all you need to do is to override to field queryset.
Like:
class ComposeForm(forms.ModelForm):
class Meta:
model = Mail
exclude = ["folder", "sender", "sent_at"]
def __init__(self, *args, **kwargs):
super(ComposeForm, self).__init__(*args, **kwargs)
self.fields['to'].queryset = Character.objects.none()
2) If you want to add autocomplete then you will need view that does the filtering and returns filtered options. You also need some kind of js widget. There are many of those available. On django side you only need to update field widget parameters so your js can pick up the field.

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

Django Form with no required fields

I want to make a form used to filter searches without any field being required. For example given this code:
models.py:
class Message(models.Model):
happened = models.DateTimeField()
filename = models.CharField(max_length=512, blank=True, null=True)
message = models.TextField(blank=True, null=True)
dest = models.CharField(max_length=512, blank=True, null=True)
fromhost = models.ForeignKey(Hosts, related_name='to hosts', blank=True, null=True)
TYPE_CHOICES = ( (u'Info', u'Info'), (u'Error', u'Error'), (u'File', u'File'), (u'BPS', u'BPS'),)
type = models.CharField(max_length=7, choices=TYPE_CHOICES)
job = models.ForeignKey(Jobs)
views.py:
WHEN_CHOICES = ( (u'', ''), (1, u'Today'), (2, u'Two days'), (3, u'Three Days'), (7, u'Week'),(31, u'Month'),)
class MessageSearch(ModelForm): #Class that makes a form from a model that can be customized by placing info above the class Meta
message = forms.CharField(max_length=25, required=False)
job = forms.CharField(max_length=25, required=False)
happened = forms.CharField(max_length=14, widget=forms.Select(choices=WHEN_CHOICES), required=False)
class Meta:
model = Message
That's the code I have now. As you can see it makes a form based on a model. I redefined message in the form because I'm using an icontains filter so I didn't need a giant text box. I redefined the date mostly because I didn't want to have to mess around with dates (I hate working with dates! Who doesnt?) And I changed the jobs field because otherwise I was getting a drop down list of existing jobs and I really wanted to be able to search by common words. So I was able to mark all of those as not required
The problem is it's marking all my other fields as required because in the model they're not allowed to be blank.
Now in the model they can't be blank. If they're blank then the data is bad and I don't want it in the DB. However the form is only a filter form on a page to display the data. I'm never going to save from that form so I don't care if fields are blank or not. So is there an easy way to make all fields as required=false while still using the class Meta: model = Message format in the form? It's really handy that I can make a form directly from a model.
Also this is my first serious attempt at a django app so if something is absurdly wrong please be kind :)
You can create a custom ModelForm that suit your needs. This custom ModelForm will override the save method and set all fields to be non-required:
from django.forms import ModelForm
class SearchForm(ModelForm):
def __init__(self, *args, **kwargs):
super(SearchForm, self).__init__(*args, **kwargs)
for key, field in self.fields.iteritems():
self.fields[key].required = False
So you could declare your forms by simply calling instead of the ModelForm, e.g.:
class MessageForm(SearchForm):
class Meta:
model = Message
You could also pass empty_permitted=True when you instantiate the form, e.g.,
form = MessageSearch(empty_permitted=True)
that way you can still have normal validation rules for when someone does enter data into the form.
I would give a try to the django-filter module :
http://django-filter.readthedocs.io/en/develop/
fields are not required. these are filters actually. It would look like this :
import django_filters
class MessageSearch(django_filters.FilterSet):
class Meta:
model = Message
fields = ['happened', 'filename', 'message', '...', ]
# django-filter has its own default widgets corresponding to the field
# type of the model, but you can tweak and subclass in a django way :
happened = django_filters.DateFromToRangeFilter()
mandatory, hidden filters can be defined if you want to narrow a list of model depending on something like user rights etc.
also : setup a filter on a 'reverse' relationship (the foreignkey is not in the filtered model : the model is referenced elsewhere in another table), is easy, just name the table where the foreign key of the filtered model field is :
# the 'tags' model has a fk like message = models.ForeignKey(Message...)
tags= django_filters.<some filter>(name='tags')
quick extendable and clean to setup.
please note I didn't wrote this module, I'm just very happy with it :)

Validating an Autocomplete field in Django

I have models similar to the following:
class Band(models.Model):
name = models.CharField(unique=True)
class Event(models.Model):
name = models.CharField(max_length=50, unique=True)
bands = models.ManyToManyField(Band)
and essentially I want to use the validation capability offered by a ModelForm that already exists for Event, but I do not want to show the default Multi-Select list (for 'bands') on the page, because the potential length of the related models is extremely long.
I have the following form defined:
class AddEventForm(ModelForm):
class Meta:
model = Event
fields = ('name', )
Which does what is expected for the Model, but of course, validation could care less about the 'bands' field. I've got it working enough to add bands correctly, but there's no correct validation, and it will simply drop bad band IDs.
What should I do so that I can ensure that at least one (correct) band ID has been sent along with my form?
For how I'm sending the band-IDs with auto-complete, see this related question: Django ModelForm Validate custom Autocomplete for M2M, instead of ugly Multi-Select
You can override the default fields in a ModelForm.
class AddEventForm(forms.ModelForm):
band = forms.CharField(max_length=50)
def clean_band(self):
bands = Band.objects.filter(name=band,
self.data.get('band', ''))
if not bands:
raise forms.ValidationError('Please specify a valid band name')
self.cleaned_data['band_id'] = bands[0].id
Then you can use your autocomplete widget, or some other widget. You can also use a custom widget, just pass it into the band field definition: band = forms.CharField(widget=...)