Django ModelForms: cleanest way to hide fields and enforce defaults - django

Let's say I have a single ModelForm which can be filled out by different tiers of users. The Admin can edit any field of that form; but for other users, I need to have certain fields pre-defined, and read-only and/or hidden.
Using my CBV's get_form_kwargs method, I have made the form aware of the user that's bringing it up, and, in its __init__ method I react accordingly, tweaking the form's exclude, and the fields' required and initial properties; and then, in my view's form_valid, I further enforce the values. But, frankly, I'm neither sure that every operation I do is actually needed, nor whether there's some gaping hole I'm not aware of.
So, what's the best, cleanest way of doing this?

Assuming there aren't a lot of combinations, I would create a different form that meets the different needs of your users. Then override def get_form_class and return the correct form based on your needs. This keeps the different use cases separate and gives flexibility if you need to change things in the future without breaking the other forms.
# models.py
class Foo(models.Model):
bar = model.CharField(max_length=100)
baz = model.CharField(max_length=100)
biz = model.CharField(max_length=100)
# forms.py
class FooForm(forms.ModelForm): # for admins
class Meta:
model = Foo
class FooForm(forms.ModelForm): # users who can't see bar
boo = forms.CharField()
class Meta:
model = Foo
exclude = ['bar']
class FooFormN(forms.ModelForm): # as many different scenarios as you need
def __init__(self, *args, **kwargs)
super(FooFormN, self).__init__(*args, **kwargs)
self.fields['biz'].widget.attrs['readonly'] = True
class Meta:
model = Foo
# views.py
class SomeView(UpdateView):
def get_form_class(self):
if self.request.user.groups.filter(name="some_group").exists():
return FooForm
# etc.

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)

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.

Change fields of ModelForm dynamically

Is it possible to change what fields are displayed in a ModelForm, dynamically?
I am trying to show only a small number of fields in a ModelForm when the user adds a new instance (of the Model) from the frontend (using an add form) but larger number of fields when the user edits an instance (using an edit form).
The Form class looks something like this:
class SchoolForm(ModelForm):
class Meta:
model = School
#want to change the fields below dynamically depending on whether its an edit form or add form on the frontend
fields = ['name', 'area', 'capacity', 'num_of_teachers']
widgets = {
'area': CheckboxSelectMultiple
}
labels = {
'name': "Name of the School",
'num_of_teachers': "Total number of teachers",
}
Trying to avoid having two separate classes for add and edit since that doesnt seem DRYish. I found some SO posts with the same question for the admin page where we could override get_form() function but that does not apply here.
Also, this answer suggests using different classes as the normal way and using dynamic forms as an alternative. Perhaps dynamics forms is the way forward here but not entirely sure (I also have overridden __init__() and save() methods on the SchoolForm class).
I'm not suere if is a correct way, but i use some method in class to add fields or delete-it. I used like this:
class someForm(forms.ModelForm):
class Meta:
model = Foo
exclude = {"fieldn0","fieldn1"}
def __init__(self, *args, **kwargs):
super(someForm, self).__init__(*args, **kwargs)
self.fields['foofield1'].widget.attrs.update({'class': 'form-control'})
if self.instance.yourMethod() == "FooReturn":
self.fields['city'].widget.attrs.update({'class': 'form-control'})
else:
if 'city' in self.fields: del self.fields['city']
Hope it helps.

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.

Specifying initial field values for ModelForm associated with inherited models

Question : What is the recommended way to specify an initial value for fields if one uses model inheritance and each child model needs to have different default values when rendering a ModelForm?
Take for example the following models where CompileCommand and TestCommand both need different initial values when rendered as ModelForm.
# ------ models.py
class ShellCommand(models.Model):
command = models.Charfield(_("command"), max_length=100)
arguments = models.Charfield(_("arguments"), max_length=100)
class CompileCommand(ShellCommand):
# ... default command should be "make"
class TestCommand(ShellCommand):
# ... default: command = "make", arguments = "test"
I am aware that one can used the initial={...} argument when instantiating the form, however I would rather store the initial values within the context of the model (or at least within the associated ModelForm).
My current approach
What I'm doing at the moment is storing an initial value dict within Meta, and checking for it in my views.
# ----- forms.py
class CompileCommandForm(forms.ModelForm):
class Meta:
model = CompileCommand
initial_values = {"command":"make"}
class TestCommandForm(forms.ModelForm):
class Meta:
model = TestCommand
initial_values = {"command":"make", "arguments":"test"}
# ------ in views
FORM_LOOKUP = { "compile": CompileCommandFomr, "test": TestCommandForm }
CmdForm = FORM_LOOKUP.get(command_type, None)
# ...
initial = getattr(CmdForm, "initial_values", {})
form = CmdForm(initial=initial)
This feels too much like a hack. I am eager for a more generic / better way to achieve this. Suggestions appreciated.
Updated solution (looks promising)
I now have the following in forms.py which allow me to set Meta.default_initial_values without needing extra boilerplate code in views. Default values are used if user does not specify initial={...} args.
class ModelFormWithDefaults(forms.ModelForm):
def __init__(self, *args, **kwargs):
if hasattr(self.Meta, "default_initial_values"):
kwargs.setdefault("initial", self.Meta.default_initial_values)
super(ModelFormWithDefaults, self).__init__(*args, **kwargs)
class TestCommandForm(ModelFormWithDefaults):
class Meta:
model = TestCommand
default_initial_values = {"command":"make", "arguments":"test"}
I don't see that much use in setting initial_values on form's meta if you then have to send to the form init.
I would rather create a subclass of ModelForm that overrides the constructor method and then use that subclass as parent class of the other forms.
e.g.
class InitialModelForm(forms.ModelForm):
#here you override the constructor
pass
class TestCommandForm(InitialModelForm):
#form meta
class CompileCommandForm(InitialModelForm):
#form meta