Allow Django Admin to change whether a field is required - django

I'd like to be able to change whether the email field is required or not, depending on whether I'm showing a friend or a customer, without having to push to git every time and redeploy.
I have set up a Configuration model for other settings in Django which is configurable from the admin, but unfortunately I'm not able to use any of these settings in other Models or Model Fields, as it causes database errors on migration.
This is my code (it fails because of the migration issue)
class UserForm(forms.ModelForm):
stake = StakeField()
email = forms.EmailField(required=getConfig(ConfigData.USER_EMAIL_REQUIRED))
class Meta:
model = User
fields = '__all__'
Is there another way to set the required/blank setting in either the model, modelform or form which won't cause this issue?
Edit for clarity:
I already have the Configuration AdminModel, but I'm struggling to use this value (True/False) in a ModelForm field without database errors, for a user facing form.

What's wrong with just editing the formfield's required attribute?
class UserForm(forms.ModelForm):
stake = StakeField()
email = forms.EmailField(required=False)
def clean_email(self, value):
# In case your database schema does not allow an empty value, otherwise, you can ignore this
if not self.fields['email'].required and not value:
# 'A friend' has used the form and the email field was not required
value = 'whatever default/placeholder value you like'
return value
class MyModelAdmin(admin.ModelAdmin):
form = UserForm
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
if not is_my_friend(request.user):
form.base_fields['email'].required = True
return form

Related

Django many-to-many contstaints validation

I am trying to create a constraint that checks whether both two fields have falsy values. One of these fields is a boolean, and the other is a m2m as in below:
class Test(models.Model):
public = models.BooleanField(default=False)
target_groups = models.ManyToManyField("TargetGroup", blank=True)
class Meta:
constraints = [
models.CheckConstraint(
name="%(app_label)s_%(class)s_check_public_or_target_groups",
check=Q(public=False, target_groups__isnull=True)
)
]
This gets me 'constraints' refers to a ManyToManyField 'target_groups', but ManyToManyFields are not permitted in 'constraints'.
Is there anyway I can check that either public is True or target_groups is not empty when creating/ updating?
I checked this and this.
I tried for example validating on the save method as in the following:
def save(self, *args, **kwargs):
if self.public is False and not self.target_groups.exists():
raise ValidationError(
{"public": _("Notification requires either setting public boolean to true, or providing target groups.")}
)
return super().save(*args, **kwargs)
But the condition for self.target_groups is always false which I think makes sense since the object is not added to the set yet, but how do I validate the passed in data from the request? I use DRF and I can already validate this on serializers, but admins can add this through Django admin as well, so I am trying to validate this on the model level.
I appreciate any insights.
The django admin makes the changes to a ManyToMany field separately from changing the actual object.
Remember that the m2m is saved in a different database table.
from django.contrib import admin
#admin.register(Test)
class TestAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
# wrire your code here
super().save_model(request, obj, form, change)
You can refer to documentions

Django model validation

I'm facing a validation problem.
I need to use form validation and model validation together, but django (1.10) doesn't seem to like this.
Here is a short version of my setup:
class MyModel(models.Model):
fk = models.ForeignKey('ap.Model')
foo = models.CharField(max_length=12)
def clean(self):
if self.fk.som_field != self.foo:
raise ValidationError("This did not validate")
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ('fk',)
def view(request):
instance = MyModel(foo='bar')
form = MyModelForm(data=request.POST, instance=instance)
if form.is_valid():
# process
# redirect
# display template
So I have some model field validation in the model itself.
I need this here because it is re-used in other non-form related parts of my application.
And I have some user input validation in the form.
In this case, checking that the provided fk is valid and exists.
But when the form is validated and the user provided 'fk' is not valid, the form is rejected.
But the form also calls MyModel.full_clean add more model validation.
The problem is that calling MyModel.clean() without having any data in the field fk will raise a RelatedObjectDoesNotExist exception.
How can I do this the proper way ?
I feel that MyModel.full_clean() should not be called by the form until the form itself is valid to ensure that the model is validated with at least correct field types in it.
I could embed my MyModel.clean operations in a try/except that would catch a RealtedObjectDoesNotExist, but it has a bad code smell for me.
The fact that MyModel.fk exists should be ensured by the first form layer validation.
As you have found, the model form performs the instance validation (by calling self.instance.full_clean()`) whether or not the form is valid.
You could prevent this behaviour (I might try to override the _post_clean, but I think it's simpler to take account of the behaviour in the clean method.
Rather than catching the exception, it would be simpler to check that self.fk_id is not None before accessing self.fk.
class MyModel(models.Model):
fk = models.ForeignKey('ap.Model')
foo = models.CharField(max_length=12)
def clean(self):
if self.fk_id is not None and self.fk.som_field != self.foo:
raise ValidationError("This did not validate")

Django model forms - disabled fields in changed_data

I have a form base class which checks if the instance the form is updating has changed, and does not save if it has not changed.
this is in my custom model form, I override save:
class MyModelForm(models.ModelForm):
# .. more code here..
def save(self, commit=True):
if self.has_changed():
# Won't do anything if the instance did not changed
return self.instance
return super(MyModelForm, self).save(commit)
A LOT of my forms use this base class.
Now, one of my forms have a few fields which I set to disabled=True (django 1.9 +). So in one of my forms:
def __init__(self, *args, **kwargs):
## ..code
self.fields['address'].disabled = True
After a lot of debugging why the form.has_changed() is True (hence the instance is saved for no reason), even when I save the form without changing the instance. I've found out that django includes disabled fields in changed_data - which makes no sense, as disabled fields should not be altered by the user anyway.
Am I missing something or it is a bug, or maybe that how it should work?
How can I resolve this without too much changes, as the form base class is used a lot in my code.
This is a known issue with DjangoProject with the ticket at https://code.djangoproject.com/ticket/27431 and the corresponding PR at https://github.com/django/django/pull/7502. As this answer is being written the PR is merged with master so the latest version should have this fixed.
A workaround this is as follows
for form in formset:
if form.has_changed() and form not in formset.deleted_forms:
fields = form.changed_data
up_f = [field for field in fields if not form.fields[field].disabled]
if len(up_f) > 0:
updated_data.append(form.cleaned_data)
This results in updated_data having the only forms that are updated and not deleted.

django: use a queryset as modelform initial data

I'm making a settings interface which works by scanning for a settings folder in the installed applications, scanning for settings files, and finally scanning for ModelForms.
I'm at the last step now. The forms are properly found and loaded, but I now need to provide the initial data. The initial data is to be pulled from the database, and, as you can imagine, it must be limited to the authenticated user (via request.user.id).
Keep in mind, this is all done dynamically. None of the names for anything, nor their structure is known in advanced (I really don't want to maintain a boring settings interface).
Here is an example settings form. I just pick the model and which fields the user can edit (this is the extent to which I want to maintain a settings interface).
class Set_Personal_Info(forms.ModelForm):
class Meta:
model = UserProfile
fields = ('nick_name', 'url')
I've looked at modelformset_factory which almost does what I want to do, but it only seems to work with results of two or more. (Here, obj is one of the settings forms)
Formset = modelformset_factory(obj.Meta.model, form=obj)
Formset(queryset=obj.Meta.model.objects.filter(id=request.user.id))
I can't filter the data, I have to get one, and only one result. Unfortunately I can't use get()
Formset = modelformset_factory(obj.Meta.model, form=obj)
Formset(queryset=obj.Meta.model.objects.get(id=request.user.id))
'User' object has no attribute 'ordered'
Providing the query result as initial data also doesn't work as it's not a list.
Formset = modelformset_factory(obj.Meta.model, form=obj)
Formset(initial=obj.Meta.model.objects.get(id=request.user.id))
'User' object does not support indexing
I have a feeling that the answer is right in front of me. How can I pull database from the database and shove it into the form as initial values?
I'm not really sure I understand what you're trying to do - if you're just interested in a single form, I don't know why you're getting involved in formsets at all.
To populate a modelform with initial data from the database, you just pass the instance argument:
my_form = Set_Personal_Info(instance=UserProfile.objects.get(id=request.user.id))
Don't forget to also pass the instance argument when you're instantiating the form on POST, so that Django updates the existing instance rather than creating a new one.
(Note you might want to think about giving better names to your objects. obj usually describes a model instance, rather than a form, for which form would be a better name. And form classes should follow PEP8, and probably include the word 'form' - so PersonalInfoForm would be a good name.)
Based on what I've understand ... if you want to generate a form with dynamic fields you can use this:
class MyModelForm(forms.ModelForm):
def __init__(self, dynamic_fields, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
self.fields = fields_for_model(self._meta.model, dynamic_fields, self._meta.exclude, self._meta.widgets)
class Meta:
model = MyModel
Where dynamic_fields is a tuple.
More on dynamic forms:
http://www.rossp.org/blog/2008/dec/15/modelforms/
http://jacobian.org/writing/dynamic-form-generation/
http://dougalmatthews.com/articles/2009/dec/16/nicer-dynamic-forms-django/
Also Daniel's approach is valid and clean ... Based on your different ids/types etc you can you use different Form objects
forms.py
class MyModelFormA(forms.ModelForm):
class Meta:
model = MyModel
fields = ('field_a','field_b','field_c')
class MyModelFormB(forms.ModelForm):
class Meta:
model = MyModel
fields = ('field_d','field_e','field_f')
views.py
if request.method == 'POST':
if id == 1:
form = MyModelFormA(data=request.POST)
elif id == 2:
form = MyModelFormB(data=request.POST)
else:
form = MyModelFormN(data=request.POST)
if form.is_valid():
form.save() else:
if id == 1:
form = MyModelFormA()
elif id == 2:
form = MyModelFormB()
else:
form = MyModelFormN()

Django: Can I restrict which fields are saved back to the database using forms?

I have a form that I use to display several fields from a record to the user. However, the user should not be able to update all the fields that are displayed. How do I enforce this? It would nice if I could specify which fields to save when calling form.save, but I couldn't get this to work. Here's some of the code:
obj = get_object_or_404(Record, pk=record_id)
if request.method == 'POST':
form = forms.RecordForm(request.POST, instance=obj)
if form.is_valid():
form.save()
I don't think using exclude or fields in the form's Meta definition will work as this will only display the fields the user is allowed to update.
You can override the form's save() method:
class MyModelForm(forms.ModelForm):
def save(self, commit=True):
if self.instance.pk is None:
fail_message = 'created'
else:
fail_message = 'changed'
exclude = ['field_a', 'field_b'] #fields to exclude from saving
return save_instance(self, self.instance, self._meta.fields,
fail_message, commit, construct=False,
exclude=exclude)
Option 1: exclude those fields, and use your template to display the data that should not be changed completely outside of the form itself. It sounds to me like they're not really part of the form, if the user can't change them.
Option 2: In a Django form, how do I make a field readonly (or disabled) so that it cannot be edited?
take this answer to mark your fields as read only... but understand there's no server side security here, so you would want to do something like getting the target model before you update it, and update those offending form fields to the existing data, before you save the form.