Django many-to-many contstaints validation - django

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

Related

filtering the queryset in an Inline object in Django Admin

The context is an inventory app of an e-commerce project.
As expected, there's a Product model. In an attempt to normalize the DB schema, I have created these additional models:
ProductType (ForeignKey relationship in Product)
ProductSpecification (a FK in ProductType)
ProductInventory (with a FK to Product)
ProductSpecificationValue (FK to both ProductSpecification and ProductInventory)
I hope this makes sense - each ProductType has many Products, and also many Specifications.
ProductInventory (suggestions of a better name are welcome), with a FK to Product, is a model with SKU and quantity fields. And the fun part - it has a ManyToMany relationship to ProductSpecification, through ProductSpecificationValue.
Now the part where I am lost is adding this whole thing to Django Admin site.
I can create ProductTypes and Products without any problem, and a ProductTypeAdmin has an inline to add those specs:.
# admin.register(ProductType)
class ProductTypeAdmin(admin.ModelAdmin):
inlines = [
ProductSpecificationInline,
]
The problem starts when I try to add a ProductInventory entity. Because it's associated with a given ProductType, I want to limit the choice of inlines to only those which are related to the same ProductType.
class ProductSpecificationValueInline(admin.TabularInline):
model = ProductSpecificationValue
def get_formset(self, request, obj=None, **kwargs): # obj is always the current Product
print(f">>>>>>>>>>>>>>>>>>>>>> get_formset on PSVI")
print(f"obj: {obj}")
print(f"kwargs: {kwargs}")
fset = super().get_formset(request, obj, **kwargs)
assert fset is not None
print(f"fset dir: {dir(fset)}")
# qs = fset.get_queryset(self)
# print(f"qs: {qs}")
return fset
the print of 'dir(fset)' shows that 'get_queryset' function is available - but whe I try to call it, I get an error:
File "/home/devuser/.pyenv/versions/venv38/lib/python3.8/site-packages/django/forms/models.py", line 744, in get_queryset
'ProductSpecificationValueInline' object has no attribute 'queryset'
I know that I need a queryset attached to ProductSpecifications dropdown, so that I can filter the ProductSpecification objects by the ProductType, which is available on the 'obj'. But I guess I'm not overriding a correct method of the ModelAdmin (or it's subclass admin.TabularInline)? So for now I am stcu with all of Specs for all Types showing up.
Well, thank you for your time y'all; in case you want to see the admin.py/models.py, here they are
PS: this way of getting some queryset (not saying it's a right one), kinda "works":
qs = super().get_queryset(request)
, but that queryset comes empty

Allow Django Admin to change whether a field is required

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

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 ModelForm- How to make a form generated uneditable

I am learning django form and want to know how to make a model form generated display only.
models.py
class Person(models.Model):
first_name = models.CharField(max_length=40, null=True)
last_name = models.CharField(max_length=40, null=True)
#more fields
forms.py
class PersonForm(ModelForm):
class Meta:
model = Person
To generate a form with some existing data in the database:
person=Person.objects.get(id=someid)
person_form = PersonForm(instance = person)
All the fields in the form are editable in the page. However, I just want to display the data.
After some searching in StackOverflow I found a similar solution how to show a django ModelForm field as uneditable , which teaches how to set individual field uneidtable.
But I want to make the whole form uneditable. Is there any better way to do so instead of setting all the fields as uneditable one by one?
Thank you very much for your help.
Updates: I find the flowing code helps make the form uneditable, but still not sure whether this is the correct way to do it.
for field in person_form.fields:
person_form.fields[field].widget.attrs['readonly'] = True
Thank you for giving your advice.
There is no attribute called editable or something similar on the form which can act on all the fields. So, you can't do this at form level.
Also, there is no such attribute on Field class used by django forms as well, so it wouldn't be possible to set such attribute and make the field read only. So, you will have to operate on on the fields of the form in __init__ of your form.
class PersonForm(ModelForm):
class Meta:
model = Person
def __init__(self, *args, **kwargs):
super(PersonForm, self).__init__(*args, **kwargs)
for name, field in self.fields.iteritems():
field.widget.attrs['readonly'] = 'true'
In case, you only want to make some fields uneditable, change the __init__.
def __init__(self, *args, **kwargs):
super(PersonForm, self).__init__(*args, **kwargs)
uneditable_fields = ['first_name', 'last_name']
for field in uneditable_fields:
self.fields[field].widget.attrs['readonly'] = 'true'
Another solution perhaps, do not have to do any processing, just display like this..
<table border='1'>
{% for field in form%}
<tr>
<td>{{field.label}}</td>
<td>{{field.value}}</td>
</tr>
{% endfor%}
</table>
I know, old question, but since I had the same question this week it might help other people.
This technique only works if you want the whole form to be readonly. It overrides any posted data (see def clean(self)) and sets the widget attributes to readonly.
Note: Setting the widget attributes to readonly does not prevent altering the model object instance.
class MyModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
if self.is_readonly():
for k,f in self.fields.iteritems():
f.widget.attrs['readonly'] = True
def clean(self):
if self.is_readonly():
return {}
return super(CompanyQuestionUpdateForm, self).clean()
def is_readonly(self, question):
if your_condition:
return True
return False
class Meta:
model = MyModel
It is possible to implement field widget to render bound ModelForm field values wrapped into div or td, sample implementation is there
https://github.com/Dmitri-Sintsov/django-jinja-knockout/blob/master/django_jinja_knockout/widgets.py
# Read-only widget for existing models.
class DisplayText(Widget):
Then a form metaclass can be implemented which will set field widget to DisplayText for all ModelForm fields automatically like that:
https://github.com/Dmitri-Sintsov/djk-sample/search?utf8=%E2%9C%93&q=DisplayModelMetaclass
class ClubDisplayForm(BootstrapModelForm, metaclass=DisplayModelMetaclass):
class Meta(ClubForm.Meta):
widgets = {
'category': DisplayText()
}
Feel free to use or to develop your own versions of widget / form metaclass.
There was discussion about read-only ModelForms at django bug ticket:
https://code.djangoproject.com/ticket/17031
closed as "Froms are for processing data, not rendering it."
But I believe that is mistake for these reasons:
ModelForms are not just processing data, they also map forms to models. Read-only mapping is the subset of mapping.
There are inline formsets and having read-only inline formsets is even more convenient, it leaves a lot of burden from rendering relations manually.
Class-based views can share common templates to display and to edit ModelForms. Thus read-only display ModelForms increase DRY (one of the key Django principles).

How to access both directions of ManyToManyField in Django Admin?

The Django admin filter_horizontal setting gives a nice widget for editing a many-to-many relation. But it's a special setting that wants a list of fields, so it's only available on the (admin for the) model which defines the ManyToManyField; how can I get the same widget on the (admin for the) other model, reading the relationship backwards?
My models look like this (feel free to ignore the User/UserProfile complication; it's the real use case though):
class Site(models.Model):
pass
class UserProfile(models.Model):
user = models.OneToOneField(to=User,unique=True)
sites = models.ManyToManyField(Site,blank=True)
I can get a nice widget on the admin form for UserProfile with
filter_horizontal = ('sites',)
but can't see how to get the equivalent on the Site admin.
I can also get part-way by adding an inline to SiteAdmin, defined as:
class SiteAccessInline(admin_module.TabularInline):
model = UserProfile.sites.through
It's roundabout and unhandy though; the widget is not at all intuitive for simply managing the many-to-many relationship.
Finally, there's a trick described here which involves defining another ManyToManyField on Site and making sure it points to the same database table (and jumping through some hoops because Django isn't really designed to have different fields on different models describing the same data). I'm hoping someone can show me something cleaner.
Here's a (more or less) tidy solution, thanks to http://blog.abiss.gr/mgogoulos/entry/many_to_many_relationships_and and with a fix for a Django bug taken from http://code.djangoproject.com/ticket/5247
from django.contrib import admin as admin_module
class SiteForm(ModelForm):
user_profiles = forms.ModelMultipleChoiceField(
label='Users granted access',
queryset=UserProfile.objects.all(),
required=False,
help_text='Admin users (who can access everything) not listed separately',
widget=admin_module.widgets.FilteredSelectMultiple('user profiles', False))
class SiteAdmin(admin_module.ModelAdmin):
fields = ('user_profiles',)
def save_model(self, request, obj, form, change):
# save without m2m field (can't save them until obj has id)
super(SiteAdmin, self).save_model(request, obj, form, change)
# if that worked, deal with m2m field
obj.user_profiles.clear()
for user_profile in form.cleaned_data['user_profiles']:
obj.user_profiles.add(user_profile)
def get_form(self, request, obj=None, **kwargs):
if obj:
self.form.base_fields['user_profiles'].initial = [ o.pk for o in obj.userprofile_set.all() ]
else:
self.form.base_fields['user_profiles'].initial = []
return super(SiteAdmin, self).get_form(request, obj, **kwargs)
This uses the same widget as the filter_horizontal setting, but hard-coded into the form.