ManyToMany field selects all model instances by default / changing default get_queryset - python

I changed my .all method so it would select only instances with published=True:
class EventManager(models.Manager):
def all(self, *args, **kwargs):
return super().get_queryset().filter(published=True, *args, **kwargs)
This is related to the problem model fields:
class Event(models.Model):
related_events = models.ManyToManyField('self', blank=True, related_name='related')
published = models.BooleanField(default=False)
objects = EventManager()
As a result ManyToManyField ends up selecting all the Event instances.
What would you suggest me to do in order to save the published functionality and be able to manually add related events? Thank you.

As far as I know, Django does not use Model.objects as manager, but the Model._basemanager, which normally should return all objects.
You can use limit_choices_to [Django-doc] here to limit the choices of the many-to-many field, like:
from django.db.models import Q
class Event(models.Model):
related_events = models.ManyToManyField(
'self',
limit_choices_to=Q(published=True)
blank=False,
related_name='related'
)
published = models.BooleanField(default=False)
objects = EventManager()
You probably also want to remove blank=True, since that means that by default, you make the field not to show op in the forms. So if you want to manually edit the related events, then.blank=False.
Furthermore a ManyToManyField to 'self' is by default symmatrical. This thus means that if event1 is in the related_events of event2, then event2 is in related_events of event1 as well. If you do not want that, you might want to add symmetrical=False [Django-doc].
Note that there are still some scenario's where non-published events might end up in the related events. For example by adding a published event to the related events, and then "unpublish" it.
As for the manager, I think you better patch the get_queryset method:
class EventManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(published=True)
Otherwise there are lot of ways to "circumvent" the filtering. For example: Event.objects.filter(id__gt=-1) would still give me all Events, but since I did not call .all(), this would thus not filter on published.
In the ModelAdmin, you could aim to specify the queryset for this ManyToManyField with:
class EventAdmin(admin.ModelAdmin):
def get_field_queryset(self, db, db_field, request):
if db_field.name == 'event_dates':
return db_field.remote_field.model.base_manager.all()
else:
super(EventAdmin, self).get_field_queryset(db, db_field, request)

That's what I ended up doing in order to show only published events in my html and show all the events (published and unpublished) in admin dashboard.
class EventManager(models.Manager):
"""
changing get_queryset() to show only published events.
if all is set to True it will show both published and unpublished
if False, which is default it will show only published ones
"""
def get_queryset(self, all=False):
if not all:
return super().get_queryset().filter(published=True)
else:
return super().get_queryset()
class Event(models.Model):
related_events = models.ManyToManyField('self', blank=True, related_name='related')
published = models.BooleanField(default=False)
objects = EventManager()
And in ModelAdmin I call get_queryset with all set to True, otherwise I won't be able to see unpublished ones.
class EventAdmin(admin.ModelAdmin):
def get_queryset(self, request):
return Event.objects.get_queryset(all=True)
I could not simply change my model's all method because it would mess with my ManyToManyField by adding all the model instances to to it. So I did all this.

Related

Django form not populating with POST data

SOLUTION AT THE BOTTOM
Problem: Django form populating with list of objects rather than values
Summary: I have 2 models Entities and Breaks. Breaks has a FK relationship to the entity_id (not the PK) on the Entities model.
I want to generate an empty form for all the fields of Breaks. Generating a basic form populates all the empty fields, but for the FK it generates a dropdown list of all objects of the Entities table. This is not helpful so I have excluded this in the ModelForm below and tried to replace with a list of all the entity_ids of the Entities table. This form renders as expected.
class BreakForm(ModelForm):
class Meta:
model = Breaks
#fields = '__all__'
exclude = ('entity',)
def __init__(self, *args, **kwargs):
super(BreakForm, self).__init__(*args, **kwargs)
self.fields['entity_id'] = ModelChoiceField(queryset=Entities.objects.all().values_list('entity_id', flat=True))
The below FormView is the cbv called by the URL. As the below stands if I populate the form, and for the FK column entity_id choose one of the values, the form will not submit. By that field on the form template the following message appears Select a valid choice. That choice is not one of the available choices.
class ContactFormView(FormView):
template_name = "breaks/test/breaks_form.html"
form_class = BreakForm
My initial thoughts were either that the datatype of this field (string/integer) was wrong or that Django needed the PK of the row in the Entities table (for whatever reason).
So I added a post function to the FormView and could see that the request.body was populating correctly. However I can't work out how to populate this into the ModelForm and save to the database, or overcome the issue mentioned above.
Addendum:
Models added below:
class Entity(models.Model):
pk_securities = models.AutoField(primary_key=True)
entity_id = models.CharField(unique=True)
entity_description = models.CharField(blank=True, null=True)
class Meta:
managed = False
db_table = 'entities'
class Breaks(models.Model):
pk_break = models.AutoField(primary_key=True)
date = models.DateField(blank=True, null=True)
entity = models.ForeignKey(Entity, on_delete= models.CASCADE, to_field='entity_id')
commentary = models.CharField(blank=True, null=True)
active = models.BooleanField()
def get_absolute_url(self):
return reverse(
"item-update", args=[str(self.pk_break)]
)
def __str__(self):
return f"{self.pk_break}"
class Meta:
managed = False
db_table = 'breaks'
SOLUTION
Firstly I got this working by adding the following to the Entity Model class. However I didn't like this as it would have consequences elsewhere.
def __str__(self):
return f"{self.entity_id}"
I found this SO thread on the topic. The accepted answer is fantastic and the comments to it are helpful.
The solution is to subclass ModelChoiceField and override the label_from_instance
class EntityChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return obj.entity_id
I think your problem is two fold, first is not rendering the dropdown correctly and second is form is not saving. For first problem, you do not need to do any changes in ModelChoiceField queryset, instead, add to_field_name:
class BreakForm(ModelForm):
class Meta:
model = Breaks
#fields = '__all__'
def __init__(self, *args, **kwargs):
super(BreakForm, self).__init__(*args, **kwargs)
self.fields['entity_id'] = ModelChoiceField(queryset=Entities.objects.all(), to_field_name='entity_id')
Secondly, if you want to save the form, instead of FormView, use CreateView:
class ContactFormView(CreateView):
template_name = "breaks/test/breaks_form.html"
form_class = BreakForm
model = Breaks
In Django, the request object passed as parameter to your view has an attribute called "method" where the type of the request is set, and all data passed via POST can be accessed via the request. POST dictionary. The view will display the result of the login form posted through the loggedin. html.

How to automatically update model fields in Django?

So I have this simple models:
class Room(models.Model):
STATUS = (
('Available', 'Available'),
('Occupied', 'Occupied'),
)
status = models.CharField(choices=STATUS)
name = models.CharField(max_length=200)
class Reservation(models.Model):
STATUS = (
('Confirmed', 'Confirmed'),
('Pending', 'Pending')
)
status = models.CharField(choices=STATUS)
room = models.OneToOneField(Room, on_delete=)
date_created = models.DateTimeField(auto_now_add=True)
I want that whenever I create a new Reservation and assign a room to it, the status field of that particular Room is automatically changed to 'Occupied'.
I think there is a way to do this with Django Signals but I haven't figured out how to implement it on my own yet.
Thanks in advance.
Also, if you think there is a better way to implement said functionality by simplifying or modifying said models please feel free to post it
views:
def room(request):
rooms = Room.objects.all()
context = {'rooms': rooms}
return render(request, 'hotel_app/room.html', context)
def reservation(request):
reservations = Reservation.objects.all()
context = {'reservations': reservations}
return render(request, 'hotel_app/reservations.html', context)
I think there is a way to do this with Django Signals but I haven't figured out how to implement it on my own yet.
Please don't use signals. Signals are an anti-pattern. Except for certain cases, it will often result in more trouble. Indeed, signals do not run on a lot of ORM calls, especially ones where you create/remove/upate in bulk. It furthermore makes saving and updating objects less predictable.
You can simply use .annotate(…) [Django-doc] to determine if there are reservations for a given room:
from django.db.models import Exists, OuterRef
Room.objects.annotate(
is_occupied=Exists(
Reservation.objects.filter(room=OuterRef('pk'))
)
)
The Room objects that arise from this queryset will have an extra attribute .is_occupied that is True if a Reservation for that Room exists, and False otherwise.
If you need this often, you can define a manager for the Room model that will automatically annotate:
class RoomManager(models.Model):
def get_queryset(self, *args, **kwargs):
super().get_queryset(*args, **kwargs).annotate(
is_occupied=Exists(
Reservation.objects.filter(room=OuterRef('pk'))
)
)
class Room(models.Model):
name = models.CharField(max_length=200)
objects = RoomManager()
each time you thus access Room.objects, you get a manager that will return a queryset that is annotated.

django Admin - Filter foreign key select depending on other choice in edit form (without jQuery)

I am working on a project which is administered by a super admin who puts in data for different companies.
Lets say, I have these models:
class Company(models.Model):
name = models.CharField(max_length=100)
class ContactPerson(models.Model):
name = models.CharField(max_length=100)
company = models.ForeignKey(Company)
class Item(models.Model):
company = models.ForeignKey(Company)
contact_person = models.ForeignKey(ContactPerson)
I need to ensure that I (in django admin) in the edit mode I only see contact persons which belong to the selected company.
Being not in the year 2005 anymore I want to avoid writing loads of super ugly jQuery code.
I guess I could overwrite the admin form for Item. But still I had to make the contact_person optional, so when I create a new Item, the list of contact persons need to be empty. Then I'd select a company, save it and go back to edit. Now the contact_person list would be filled and I could add somebody. But if I now change the comany, I'd have to remove all selected contact persons. Sure, I could to this in the form... but it looks SO hacky and not like a nice django solution.
Anybody got some fancy ideas?
Actually, django provided me with a neat solution.
When you look at the UserAdmin class within the django code, you'll find a built-in way to handle a two-step creation process.
#admin.register(User)
class UserAdmin(admin.ModelAdmin):
...
add_form = UserCreationForm
...
def get_form(self, request, obj=None, **kwargs):
"""
Use special form during user creation
"""
defaults = {}
if obj is None:
defaults['form'] = self.add_form
defaults.update(kwargs)
return super().get_form(request, obj, **defaults)
When the attribute add_form is set and the object has no id yet (= we are creating it), it takes a different form than usual.
I wrapped this idea in an admin mixin like this:
class AdminCreateFormMixin:
"""
Mixin to easily use a different form for the create case (in comparison to "edit") in the django admin
Logic copied from `django.contrib.auth.admin.UserAdmin`
"""
add_form = None
def get_form(self, request, obj=None, **kwargs):
defaults = {}
if obj is None:
defaults['form'] = self.add_form
defaults.update(kwargs)
return super().get_form(request, obj, **defaults)
Now, when I have dependent fields, I create a small form, containing all values independent of - in my case - company and a regular form containing everything.
#admin.register(Item)
class ItemAdmin(AdminCreateFormMixin, admin.ModelAdmin):
form = ItemEditForm
add_form = ItemAddForm
...
Now I can customise the querysets of the dependent field in my edit form:
class ItemEditForm(forms.ModelForm):
class Meta:
model = Item
exclude = ()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['contact_person'].queryset = ContactPerson.objects.filter(company=self.instance.company)
The only drawback is, that all dependent fields need to be nullable for the database. Otherwise you wouldn't be able to save it in the creation process.
Luckily, you can tell django that a field is required in the form but not on database level with blank=False, null=True in the model declaration.
Hope this helps somebody else as well!

DjangoAdmin has_delete_permission executed from another Admin Class

I have two admin models, one is called Animal and another one is called Person. Each one has its own has_delete_permission on the ModelAdmin class.
The code I am using is listed below.
class Animal(models.Model):
sound = models.CharField(max_length=25, blank=True, null=True)
class Person(models.Model):
sound = models.CharField(max_length=25, blank=True, null=True)
class AnimalAdmin(admin.ModelAdmin):
model = Animal
def has_delete_permission(self, request, obj=None):
if request.POST and request.POST.get('action') == 'delete_selected':
animals = Animal.objects.filter( id__in = request.POST.getlist('_selected_action') )
print (animals)
return True
class PersonAdmin(admin.ModelAdmin):
model = Person
def has_delete_permission(self, request, obj=None):
return True
admin.site.register(Animal, AnimalAdmin)
admin.site.register(Person, PersonAdmin)
When I try to delete a Person instance that have the same ID of some Animals, the instances of Animals are printed. This could be a serious problem if I was doing some logic like changing the database or showing a message to the user.
The point is why has_delete_permission methods of different classes are also executed ?
This happens because of one method of the class AdminSite : each_context.
def each_context(self, request):
"""
Return a dictionary of variables to put in the template context for
*every* page in the admin site.
For sites running on a subpath, use the SCRIPT_NAME value if site_url
hasn't been customized.
"""
This method is called in every pages rendered in the admin backend, and it calls successively get_app_list(request) then _build_app_dict(request), which is looping over all admin models to check get_model_perms(request) :
def get_model_perms(self, request):
"""
Return a dict of all perms for this model. This dict has the keys
``add``, ``change``, ``delete``, and ``view`` mapping to the True/False
for each of those actions.
"""
return {
'add': self.has_add_permission(request),
'change': self.has_change_permission(request),
'delete': self.has_delete_permission(request),
'view': self.has_view_permission(request),
}
So if you override has_delete_permission in others ModelAdmin, it will be called too, each time you display a page.
You are seeing the instances of Animals matching the same indexes of the deleted Person because you filter the queryset with a simple list of indexes coming from request (request.POST.getlist('_selected_action')).
Note : nothing is printed when you are not in a POST request.
That said, you should avoid mixing different things in one function : has_delete_permission is about checking a delete permission on a model. I'm not sure what you want to check/prevent here, but you may find a better place.

Ordering a Many-To-Many field in Django Admin

Here's my setup:
from django.contrib.auth.models import User
class Product(models.Model):
...
email_users = models.ManyToManyField(User, null=True, blank=True)
...
[elsewhere]
class ProductAdmin(admin.ModelAdmin):
list_display = ('name','platform')
admin.site.register(Product, ProductAdmin)
My main problem is, when I'm viewing the "Product" page in the admin section, email users are not being being ordered by their ID by default, and I'd like that to be ordered by their username.
From what I've read so far, it seems like I need to be adding:
email_users.admin_order_field = 'xxxx'
But I'm not quite sure what the syntax is to access the username.
The answer was referred to in Hao Lian's comment above, essentially, this is what needed to be done:
class ProductAdminForm(ModelForm):
email_users = forms.ModelMultipleChoiceField(queryset=User.objects.order_by('username'))
class Meta:
model = Product
class ProductAdmin(admin.ModelAdmin):
list_display = ('name','platform')
form = ProductAdminForm
admin.site.register(Product, ProductAdmin)
Mine was slightly different in the sense that I required the forms.ModelMultipleChoiceField, whereas the answer provided used forms.ModelChoiceField()
Solution above works well, but in my case I lost all attributes and customizations that my component had by default (like required, label, etc).
In some cases could be better override __init__() method in order to customize only your queryset, nothing else will change.
class ProductAdminForm(ModelForm):
class Meta:
model = Product
fields = '__all__' # required in new versions
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['email_users'].queryset = (
self.fields['email_users'].queryset.order_by('username')
)

Categories

Resources