Django: check if ManyToManyField is empty from a Field object - django

This seems super simple but surprisingly could not find any clue on SO or Django docs yet.
I want to check if a particular ManyToManyField is empty but cant figure out a way to do it yet. Here's my exact usecase, if it helps:
for field in school._meta.get_fields(): # school is the Model object
if (field.get_internal_type() == 'ManyToManyField'):
#if (find somehow if this m2m field is empty)
#do something if empty
else:
if (field.value_to_string(self) and field.value_to_string(self)!='None'):
#do something when other fields are blank or null
Found this post which looks similar but is about filtering all ManyToManyFields that are empty in a Model object, so doesn't help the case above.
all() or count() or empty() or exists() don't seem to work on ManyToManyFields.
if (field): returns True (since its referring to the Manager)
Didn't find a relevant option in the Field reference or ManyToManyField reference

getattr(school,field.name).exists() worked for me. But all ears to know if there's a better approach
(i.e querying on model_object.field instead of field object)

How about using first() instead? That should definitely run quicker than all() and may run quicker than count().
IE:
def __str__(self):
if self.m2mField.first():
print('Object where m2mField contains stuff.')
else:
print('Object with nothing in m2mField.')

In my case I was overriding the clean() method for a model and had to perform validation if the m2m field was empty. getattr() returned an exception so I had to use .count(). This should also work for django-modelcluster's ParentalManyToMany fields.
def clean(self, *args, **kwargs):
if self.m2m_field.count() == 0:
raise ValidationError("No children")

Related

In Django admin, how can I filter a MultipleChoiceField depending on a previous MultipleChoiceField?

In my django website, I have 3 classes: Thing, Category and SubCategory.
Thing has 2 ForeignKeys: "Category" and "SubCategory" (such as Car and Ferrari).
SubCategory has 1 ForeighKey: "Category" (Ferrari is in the category Car)
When I create an instance of Thing in the Admin part and when I choose a Category, I would like that the "SubCategory" field only shows the SubCategories linked to the Category I chose. Is that possible?
I saw the possibility to change the AdminForm like:
class ThingFormAdmin(forms.ModelForm):
def __init__(self,Category,*args,**kwargs):
super (ThingFormAdmin,self ).__init__(*args,**kwargs) # populates the post
self.fields['sub_category'].queryset = SubCategory.objects.filter(category= ... )
But I don't know what to write on the ...
Thanks for the help!
in general always this solution would work:
you need some javascript to catch what has been selected for first selection. then do filtering agin using javascript.
but in django admin, there is autocomplete_fields available. using this would create a kind of selection-input that uses ajax to do some magic filtering on choices when user types some characters. it uses the get_search_results method of the related models admin.ModelAdmin class. overriding that method and giving some extra data to that method could help. but it's the longest way to walk.
Thanks! I will look in the 1st answer after the 2nd, because the fact that I don't have to write JS is very nice, as I am very bad in it.
I manage to use the autocomplete_field to work, but I am stuck with the redefinition of the get_search_results method. If I understood correctly the doc, it will be something like:
def get_search_results(self, request, queryset, search_term):
queryset, use_distinct = super().get_search_results(request, queryset, search_term)
try:
cat = search_term
except ValueError:
queryset |= self.model.objects.all()
else:
queryset |= self.model.objects.filter(category=cat)
return queryset, use_distinct
But I don't understand from where this search_term comes from, and how I can specify it. Any ideas?
https://simpleisbetterthancomplex.com/tutorial/2018/01/29/how-to-implement-dependent-or-chained-dropdown-list-with-django.html
This tutorial will walk you through every step of doing whatever I presume you need to do.

How do I check for the existence of a specific record in Django?

If I retrieve a QuerySet from a Django model (using filter(), for example), I can use the QuerySet methods exists() or count() to determine if the result will be void or not:
if myModel.objects.filter(id__lte=100).exists():
# Do something...
However, if I want to retrieve a specific record (using get(), for example)
myModel.objects.get(id=100)
the record object returned has no exists() or count() method. Moreover, if this record doesn't exist, instead of doing the expected thing and returning None, Django flips its crap entirely and breaks with a DoesNotExist exception, so I can't even test for the record's existence in the normal Pythonic way:
# This throws a DoesNotExist exception
if not myModel.objects.get(id=100):
# Do something else...
How do I test for the existence of a specific record (an element of a QuerySet, not a QuerySet itself) so that the app doesn't break?
(Django 3.0)
you can try this way
try:
instance = MyModel.objects.get(id=1)
except MyModel.DoesNotExist:
print "Not Found"
else:
#Do your stuff
OR
MyModel.objects.filter(id=1).exists()
You can use .filter. It will scan the database and if the object is present, it will return a Queryset and if not the length of Queryset will be none.
myModel.objects.filter(id=100)
You can find about filters here:
https://docs.djangoproject.com/en/3.0/topics/db/queries/

Django Tastypie: Allow Random as an Order By Option

Per this question, I know how to randomly order a queryset in the Meta class of a tastypie Resource, but is there a way to make it an available order_by option instead of making it the default? It looks like anything defined in the ordering Meta setting must also be listed in the fields setting and ? obviously isn't a field. Without that I simply get,
{"error": "No matching '?' field for ordering on."}
You can override the apply_sorting method (documentation) on your Resource, maybe something like this (untested):
class YourResource(ModelResource):
def apply_sorting(self, obj_list, options=None):
if options and '?' in options.get('order_by', ''):
return obj_list.order_by('?')
return super(YourResource, self).apply_sorting(obj_list, options)
You might need to copy code from the ModelResource implementation for getting the correct order_by value if this doesn't work as-is.

Django form field validation - How to tell if operation is insert or update?

I'm trying to do this in Django:
When saving an object in the Admin I want to save also another object of a different type based on one of the fields in my fist object.
In order to do this I must check if that second object already exists and return an validation error only for the particular field in the first object if it does.
My problem is that I want the validation error to appear in the field only if the operation is insert.
How do I display a validation error for a particular admin form field based on knowing if the operation is update or insert?
P.S. I know that for a model validation this is impossible since the validator only takes the value parameter, but I think it should be possible for form validation.
This ca be done by writing a clean_[name_of_field] method in a Django Admin Form. The insert or update operation can be checked by testing self.instance.pk.
class EntityAdminForm(forms.ModelForm):
def clean_field(self):
field = self.cleaned_data['field']
insert = self.instance.pk == None
if insert:
raise forms.ValidationError('Some error message!')
else:
pass
return field
class EntityAdmin(admin.ModelAdmin):
form = EntityAdminForm
You have to use then the EntityAdmin class when registering the Entity model with the Django admin:
admin.site.register(Entity, EntityAdmin)
You can write your custom validation at the model level:
#inside your class model ...
def clean(self):
is_insert = self.pk is None
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
#do your business rules
if is_insert:
...
if __some_condition__ :
raise ValidationError('Dups.')
Create a model form for your model. In the clean method, you can set errors for specific fields.
See the docs for cleaning and validating fields that depend on each other for more information.
That is (probably) not an exact answer, but i guess it might help.
Django Admin offers you to override save method with ModelAdmin.save_model method (doc is here)
Also Django api have a get_or_create method (Doc is here). It returns two values, first is the object and second one is a boolean value that represents whether object is created or not (updated an existing record).
Let me say you have FirstObject and SecondObject
In your related admin.py file:
class FirstObjectAdmin(admin.ModelAdmin):
...
...
def save_model(self, request, obj, form, change):
s_obj, s_created = SecondObject.objects.get_or_create(..., defaults={...})
if not s_created:
# second object already exists... We will raise validation error for our first object
...
For the rest, I do not have a clear idea about how to handle it. Since you have the form object at hand, you can call form.fields{'somefield'].validate(value) and write a custom validation for admin. You will probably override clean method and try to trigger a raise ValidationError from ModelAdmin.save_model method. you can call validate and pass a value from there...
You may dig django source to see how django handles this, and try to define some custom validaton steps.

Django ModelForms: Display ManyToMany field as single-select

In a Django app, I'm having a model Bet which contains a ManyToMany relation with the User model of Django:
class Bet(models.Model):
...
participants = models.ManyToManyField(User)
User should be able to start new bets using a form. Until now, bets have exactly two participants, one of which is the user who creates the bet himself. That means in the form for the new bet you have to chose exactly one participant. The bet creator is added as participant upon saving of the form data.
I'm using a ModelForm for my NewBetForm:
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
widgets = {
'participants': forms.Select()
}
def save(self, user):
... # save user as participant
Notice the redefined widget for the participants field which makes sure you can only choose one participant.
However, this gives me a validation error:
Enter a list of values.
I'm not really sure where this comes from. If I look at the POST data in the developer tools, it seems to be exactly the same as if I use the default widget and choose only one participant. However, it seems like the to_python() method of the ManyToManyField has its problems with this data. At least there is no User object created if I enable the Select widget.
I know I could work around this problem by excluding the participants field from the form and define it myself but it would be a lot nicer if the ModelForm's capacities could still be used (after all, it's only a widget change). Maybe I could manipulate the passed data in some way if I knew how.
Can anyone tell me what the problem is exactly and if there is a good way to solve it?
Thanks in advance!
Edit
As suggested in the comments: the (relevant) code of the view.
def new_bet(request):
if request.method == 'POST':
form = NewBetForm(request.POST)
if form.is_valid():
form.save(request.user)
... # success message and redirect
else:
form = NewBetForm()
return render(request, 'bets/new.html', {'form': form})
After digging in the Django code, I can answer my own question.
The problem is that Django's ModelForm maps ManyToManyFields in the model to ModelMultipleChoiceFields of the form. This kind of form field expects the widget object to return a sequence from its value_from_datadict() method. The default widget for ModelMultipleChoiceField (which is SelectMultiple) overrides value_from_datadict() to return a list from the user supplied data. But if I use the Select widget, the default value_from_datadict() method of the superclass is used, which simply returns a string. ModelMultipleChoiceField doesn't like that at all, hence the validation error.
To solutions I could think of:
Overriding the value_from_datadict() of Select either via inheritance or some class decorator.
Handling the m2m field manually by creating a new form field and adjusting the save() method of the ModelForm to save its data in the m2m relation.
The seconds solution seems to be less verbose, so that's what I will be going with.
I don't mean to revive a resolved question but I was working a solution like this and thought I would share my code to help others.
In j0ker's answer he lists two methods to get this to work. I used method 1. In which I borrowed the 'value_from_datadict' method from the SelectMultiple widget.
forms.py
from django.utils.datastructures import MultiValueDict, MergeDict
class M2MSelect(forms.Select):
def value_from_datadict(self, data, files, name):
if isinstance(data, (MultiValueDict, MergeDict)):
return data.getlist(name)
return data.get(name, None)
class WindowsSubnetForm(forms.ModelForm):
port_group = forms.ModelMultipleChoiceField(widget=M2MSelect, required=True, queryset=PortGroup.objects.all())
class Meta:
model = Subnet
The problem is that ManyToMany is the wrong data type for this relationship.
In a sense, the bet itself is the many-to-many relationship. It makes no sense to have the participants as a manytomanyfield. What you need is two ForeignKeys, both to User: one for the creator, one for the other user ('acceptor'?)
You can modify the submitted value before (during) validation in Form.clean_field_name. You could use this method to wrap the select's single value in a list.
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
widgets = {
'participants': forms.Select()
}
def save(self, user):
... # save user as participant
def clean_participants(self):
data = self.cleaned_data['participants']
return [data]
I'm actually just guessing what the value proivded by the select looks like, so this might need a bit of tweaking, but I think it will work.
Here are the docs.
Inspired by #Ryan Currah I found this to be working out of the box:
class M2MSelect(forms.SelectMultiple):
def render(self, name, value, attrs=None, choices=()):
rendered = super(M2MSelect, self).render(name, value=value, attrs=attrs, choices=choices)
return rendered.replace(u'multiple="multiple"', u'')
The first one of the many to many is displayed and when saved only the selected value is left.
I found an easyer way to do this inspired by #Ryan Currah:
You just have to override "allow_multiple_selected" attribut from SelectMultiple class
class M2MSelect(forms.SelectMultiple):
allow_multiple_selected = False
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
participants = forms.ModelMultipleChoiceField(widget=M2MSelect, required=True, queryset=User.objects.all())