Specify Django proxy model in admin foreign key - python

In Django 1.11, I have a model Friend, and a proxy model Relative:
class FriendManager(models.Manager):
def get_queryset(self):
return super(RelativeManager, self).get_queryset().filter(is_relative=False)
class Friend(models.Model):
# Model fields defined here
objects = FriendManager()
class RelativeManager(models.Manager):
def get_queryset(self):
return super(RelativeManager, self).get_queryset().filter(is_relative=True)
class Relative(Friend):
class Meta:
proxy = True
objects = RelativeManager()
def save(self, *args, **kwargs):
self.is_relative = True
super(Relative, self).save(*args, **kwargs)
I also have a model FriendPortrait, which has a foreign key field friend:
class FriendPortrait(models.Model):
friend = models.ForeignKey(Friend)
And a proxy on that:
class RelativePortrait(FriendPortrait):
class Meta:
proxy = True
Now, I want the detail view for RelativePortraits to only show relatives in the drop-down for friend.
admin.py:
#admin.register(RelativePortrait)
class RelativePortraitAdmin(admin.ModelAdmin):
fields = ('friend')
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'friend':
kwargs['queryset'] = Relative.objects.all()
return super(RelativePortraitAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
This works, in that only relatives are displayed in the friend drop-down. However, when I try to save a portrait, Django admin gives me a validation error:
friend instance with id 14 does not exist.
How can I specify that I want to use a proxy model for my foreign key in the RelativePortraitAdmin?

The problem here is that your ForeignKey points to the Friend model. The model's default manager filters out all relatives, so this will not work.
A simple way to solve this would be to restructure your models a bit. Introducing something like a generic Person model and having Friend and Relative inherit from it with proxy=True. The Person model shouldn't have a manager that pre-filters the instances; then you could have your ForeignKey point to person.

Related

Access related ManyToManyField data pre-save in the Django Model.save method

We would like to access related ManyToManyField data pre-save within the Model.save method, however the data isn't available yet via the Django ORM because it's related ManyToManyField data and doesn't get set until post-save of the primary record.
Here's some example code of the relationship and where the related ManyToMany records are accessed in Model.save
class Friend(models.Model):
name = models.CharField(max_length=50)
class Person(models.Model):
name = models.CharField(max_length=50)
friends = models.ManyToManyField(Friend)
def save(self, *args, **kwargs):
friends = self.friends.all()
# 'friends' is an empty QuerySet at this point
# I'd like to do something with friends here,
# but it gets set after save
super(Friend, self).save(*args, **kwargs)
Example use case where friends are passed in on save:
friend = Friend.objects.all()[0]
friend2 = Friend.objects.all()[1]
friends = [friend, friend2]
Person.objects.create(friends=friends)
m2m relations establish after instance saved and get it's own id,so you can't access it within override save method,two way to archieve:
one: after django 1.9,transaction tools provide new method to listen db communication,doc is here.demo code is:
from django.db import transaction
class Person(models.Model):
name = models.CharField(max_length=50)
friends = models.ManyToManyField(Friend)
def save(self, *args, **kwargs):
instance = super(Person, self).save(*args, **kwargs)
transaction.on_commit(self.update_friend)
return instance
def update_friend(self):
for friend in self.friends.all():
print(friend.__str__())
second way is use signal,here is demo:
from django.db.models.signals import m2m_changed
#receiver(m2m_changed, sender=Person.friends.through)
def friends_change(sender, action, pk_set, instance=None, **kwargs):
if action in ['post_add', 'post_remove']:
queryset = instance.friends.all()
for friend in queryset:
print(friend.__str__())

Limit choices and validate django's foreign key to related objects (also in REST)

I have my models.py like this:
class Category(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=256, db_index=True)
class Todo(models.Model):
user = models.ForeignKey(User)
category = models.ForeignKey(Category)
...
And I want to limit choices of Category for Todo to only those ones where Todo.user = Category.user
Every solutuion that I've found was to set queryset for a ModelForm or implement method inside a form. (As with limit_choices_to it is not possible(?))
The problem is that I have not only one model with such limiting problem (e.g Tag, etc.)
Also, I'm using django REST framework, so I have to check Category when Todo is added or edited.
So, I also need functions validate in serializers to limit models right (as it does not call model's clean, full_clean methods and does not check limit_choices_to)
So, I'm looking for a simple solution, which will work for both django Admin and REST framework.
Or, if it is not possible to implement it the simple way, I'm looking for an advice of how to code it the most painless way.
Here what I've found so far:
To get Foreignkey showed right in admin, you have to specify a form in ModelAdmin
class TodoAdminForm(ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['category'].queryset = Category.objects.filter(user__pk=self.instance.user.pk)
#admin.register(Todo)
class TodoAdmin(admin.ModelAdmin):
form = TodoAdminForm
...
To get ManyToManyField showed right in InlineModelAdmin (e.g. TabularInline) here comes more dirty hack (can it be done better?)
You have to save your quiring field value from object and then manually set queryset in the field. My through model has two members todo and tag
And I'd like to filter tag field (pointing to model Tag):
class MembershipInline(admin.TabularInline):
model = Todo.tags.through
def get_formset(self, request, obj=None, **kwargs):
request.saved_user_pk = obj.user.pk # Not sure if it can be None
return super().get_formset(request, obj, **kwargs)
def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
if db_field.name == 'tag':
kwargs['queryset'] = Tag.objects.filter(user__pk=request.saved_user_pk)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
And finally, to restrict elements only to related in Django REST framework, I have to implement custom Field
class PrimaryKeyRelatedByUser(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
return super().get_queryset().filter(user=self.context['request'].user)
And use it in my serializer like
class TodoSerializer(serializers.ModelSerializer):
category = PrimaryKeyRelatedByUser(required=False, allow_null=True, queryset=Category.objects.all())
tags = PrimaryKeyRelatedByUser(required=False, many=True, queryset=Tag.objects.all())
class Meta:
model = Todo
fields = ('id', 'category', 'tags', ...)
Not sure if it actually working in all cases as planned. I'll continue this small investigation.
Question still remains. Could it be done simplier?

Make models belong to users in django

I am using django.contrib.auth and I am wondering how I can make an instance of a model "belong to" a particular user. Do I need to define a foreign key in the model that points to the user? I want users to keep their CRUD to themselves.
# The objects created from this model should belong
# to the user who created them and should not be
# viewable by other users of the website.
from django.contrib.auth.models import User
class Classroom(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=30)
def __unicode__ (self):
return self.name
Thanks.
This is the general approach I take:
Creating models that belong to the user
On the model, have a foreign key to the user.
Define a custom ModelForm that requires a user to be passed to the constructor (__init__()). Also, override the form's save() method so that the user passed to the constructor is added to the model instance, before it's saved.
Define class based views based on CreateView etc. that override the get_form_kwargs method to pass the form the user stored in the request.
Here's the code:
# models.py
from django.db import models
from django.contrib.auth.models import User
class MyModel(models.Model):
"A model that belongs to a user."
user = models.ForeignKey(User)
# forms.py
from django import forms
from .models import MyModel
class MyModelForm(forms.ModelForm):
"""A form for creating/updating MyModels that accepts
the user as a kwarg.
"""
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super(MyModelForm, self).__init__(*args, **kwargs)
def save(self, *args, **kwargs):
self.instance.user = self.user
return super(MyModelForm, self).save(*args, **kwargs)
class Meta:
model = MyModel
# Don't show the user on the form
exclude = ('user',)
# views.py
from django.views.generic import CreateView
from .models import MyModel
from .forms import MyModelForm
class MyModelCreateView(CreateView):
"View for creating a MyModel belonging to the current user."
model = MyModel
form_class = MyModelForm
def get_form_kwargs(self, *args, **kwargs):
form_kwargs = super(MyModelCreateView,
self).get_form_kwargs(*args, **kwargs)
# Pass the current user to the form constructor
form_kwargs['user'] = self.request.user
return form_kwargs
Listing models that belong to the user
This is more straightforward - you extend ListView and filter the queryset by the current user. (Read more about ListView.)
Editing models that belong to the user
Use a similar approach to MyModelCreateView, but extending an UpdateView instead. The important thing here is to check that the user has permission to edit:
# views.py
from django.views.generic import UpdateView
from django.core.exceptions import PermissionDenied
...
class MyModelUpdateView(UpdateView):
"View for the current user editing a MyModel."""
model = MyModel
form_class = MyModelForm
def get_object(self, *args, **kwargs):
object = super(MyModelUpdateView, self).get_object(*args, **kwargs)
# Raise a permission denied if the current user doesn't
# 'own' the MyModel they're trying to edit.
if object.user != self.request.user:
raise PermissionDenied
def get_form_kwargs(self, *args, **kwargs):
form_kwargs = super(MyModelCreateView,
self).get_form_kwargs(*args, **kwargs)
# Pass the current user to the form constructor
form_kwargs['user'] = self.request.user
return form_kwargs
If you would like to have multiple objects belong to the user, a foreign key field to the User object will work. Note, you can still use a foreign key, and have one instance by passing
the unique attribute to the field definition.
If one object (and only one) and belong to a user, use a one to one field. A one to one field can be accessed from either side of the models
You can use user.classroom or classroom.user, these bindings can be changed with the related_name attribute of one to one field definitions.

How to change display text in django admin foreignkey dropdown

I have a task list, with ability to assign users. So I have foreignkey to User model in the database. However, the default display is username in the dropdown menu, I would like to display full name (first last) instead of the username. If the foreignkey is pointing to one of my own classes, I can just change the str function in the model, but User is a django authentication model, so I can't easily change it directly right?
Anyone have any idea how to accomplish this?
Thanks a lot!
You can create a new ModelForm for your Task model, which will display the list of users however you like (code here assumes a model named Task with a 'user' attribute):
def get_user_full_name_choices:
return [(user, user.get_full_name()) for user in User.objects.all()]
class TaskAdminForm(forms.ModelForm):
class Meta:
model = Task
user = forms.ChoiceField(choices=get_user_full_name_choices)
Then, tell your ModelAdmin class to use the new form:
class TaskAdmin(admin.ModelAdmin):
form = TaskAdminForm
There is another choice:
USERS = [(user.id, user.get_full_name()) for user in User.objects.all()]
USERS.insert(0, ('', '----'))
class TaskAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(TaskAdminForm, self).__init__(*args, **kwargs)
self.fields['user'].choices=USERS
A more generic, verbose solution, using a ModelChoiceField as base. Avoids fiddling with querysets, just focusing on modifying the visible value in the dropdown.
create your own formfield:
class CustomLabelChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
return "Howdy {}".format(obj.name)
use it either with formfield_for_dbfield (as in the link), or, more verbose, in your admin form:
class TaskAdminForm(forms.ModelForm):
user = CustomLabelChoiceField(queryset=User.objects.all()
class Meta:
model = Task
# obviously, link it with your admin
class TaskAdmin(admin.ModelAdmin):
form = TaskAdminForm
By overriding formfield_for_foreignkey(), you can display the combination of "first_name" and "last_name" to Django Admin without creating a custom "forms.ModelChoiceField" and a custom "forms.ModelForm" as shown below:
#admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
formfield = super().formfield_for_foreignkey(db_field, request, **kwargs)
if db_field.name == "user":
formfield.label_from_instance = lambda obj: f'{obj.first_name} ({obj.last_name})'
return formfield

How do I restrict foreign keys choices to related objects only in django

I have a two way foreign relation similar to the following
class Parent(models.Model):
name = models.CharField(max_length=255)
favoritechild = models.ForeignKey("Child", blank=True, null=True)
class Child(models.Model):
name = models.CharField(max_length=255)
myparent = models.ForeignKey(Parent)
How do I restrict the choices for Parent.favoritechild to only children whose parent is itself? I tried
class Parent(models.Model):
name = models.CharField(max_length=255)
favoritechild = models.ForeignKey("Child", blank=True, null=True, limit_choices_to = {"myparent": "self"})
but that causes the admin interface to not list any children.
I just came across ForeignKey.limit_choices_to in the Django docs.
Not sure yet how it works, but it might be the right thing here.
Update: ForeignKey.limit_choices_to allows one to specify either a constant, a callable or a Q object to restrict the allowable choices for the key. A constant obviously is of no use here, since it knows nothing about the objects involved.
Using a callable (function or class method or any callable object) seems more promising. However, the problem of how to access the necessary information from the HttpRequest object remains. Using thread local storage may be a solution.
2. Update: Here is what has worked for me:
I created a middleware as described in the link above. It extracts one or more arguments from the request's GET part, such as "product=1", and stores this information in the thread locals.
Next there is a class method in the model that reads the thread local variable and returns a list of ids to limit the choice of a foreign key field.
#classmethod
def _product_list(cls):
"""
return a list containing the one product_id contained in the request URL,
or a query containing all valid product_ids if not id present in URL
used to limit the choice of foreign key object to those related to the current product
"""
id = threadlocals.get_current_product()
if id is not None:
return [id]
else:
return Product.objects.all().values('pk').query
It is important to return a query containing all possible ids if none was selected so that the normal admin pages work ok.
The foreign key field is then declared as:
product = models.ForeignKey(
Product,
limit_choices_to={
id__in=BaseModel._product_list,
},
)
The catch is that you have to provide the information to restrict the choices via the request. I don't see a way to access "self" here.
The 'right' way to do it is to use a custom form. From there, you can access self.instance, which is the current object. Example --
from django import forms
from django.contrib import admin
from models import *
class SupplierAdminForm(forms.ModelForm):
class Meta:
model = Supplier
fields = "__all__" # for Django 1.8+
def __init__(self, *args, **kwargs):
super(SupplierAdminForm, self).__init__(*args, **kwargs)
if self.instance:
self.fields['cat'].queryset = Cat.objects.filter(supplier=self.instance)
class SupplierAdmin(admin.ModelAdmin):
form = SupplierAdminForm
The new "right" way of doing this, at least since Django 1.1 is by overriding the AdminModel.formfield_for_foreignkey(self, db_field, request, **kwargs).
See http://docs.djangoproject.com/en/1.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.formfield_for_foreignkey
For those who don't want to follow the link below is an example function that is close for the above questions models.
class MyModelAdmin(admin.ModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "favoritechild":
kwargs["queryset"] = Child.objects.filter(myparent=request.object_id)
return super(MyModelAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
I'm only not sure about how to get the current object that is being edited. I expect it is actually on the self somewhere but I'm not sure.
This isn't how django works. You would only create the relation going one way.
class Parent(models.Model):
name = models.CharField(max_length=255)
class Child(models.Model):
name = models.CharField(max_length=255)
myparent = models.ForeignKey(Parent)
And if you were trying to access the children from the parent you would do
parent_object.child_set.all(). If you set a related_name in the myparent field, then that is what you would refer to it as. Ex: related_name='children', then you would do parent_object.children.all()
Read the docs http://docs.djangoproject.com/en/dev/topics/db/models/#many-to-one-relationships for more.
If you only need the limitations in the Django admin interface, this might work. I based it on this answer from another forum - although it's for ManyToMany relationships, you should be able to replace formfield_for_foreignkey for it to work. In admin.py:
class ParentAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
self.instance = obj
return super(ParentAdmin, self).get_form(request, obj=obj, **kwargs)
def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
if db_field.name == 'favoritechild' and self.instance:
kwargs['queryset'] = Child.objects.filter(myparent=self.instance.pk)
return super(ChildAdmin, self).formfield_for_foreignkey(db_field, request=request, **kwargs)
#Ber: I have added validation to the model similar to this
class Parent(models.Model):
name = models.CharField(max_length=255)
favoritechild = models.ForeignKey("Child", blank=True, null=True)
def save(self, force_insert=False, force_update=False):
if self.favoritechild is not None and self.favoritechild.myparent.id != self.id:
raise Exception("You must select one of your own children as your favorite")
super(Parent, self).save(force_insert, force_update)
which works exactly how I want, but it would be really nice if this validation could restrict choices in the dropdown in the admin interface rather than validating after the choice.
I'm trying to do something similar. It seems like everyone saying 'you should only have a foreign key one way' has maybe misunderstood what you're trying do.
It's a shame the limit_choices_to={"myparent": "self"} you wanted to do doesn't work... that would have been clean and simple. Unfortunately the 'self' doesn't get evaluated and goes through as a plain string.
I thought maybe I could do:
class MyModel(models.Model):
def _get_self_pk(self):
return self.pk
favourite = models.ForeignKey(limit_choices_to={'myparent__pk':_get_self_pk})
But alas that gives an error because the function doesn't get passed a self arg :(
It seems like the only way is to put the logic into all the forms that use this model (ie pass a queryset in to the choices for your formfield). Which is easily done, but it'd be more DRY to have this at the model level. Your overriding the save method of the model seems a good way to prevent invalid choices getting through.
Update
See my later answer for another way https://stackoverflow.com/a/3753916/202168
Do you want to restrict the choices available in the admin interface when creating/editing a model instance?
One way to do this is validation of the model. This lets you raise an error in the admin interface if the foreign field is not the right choice.
Of course, Eric's answer is correct: You only really need one foreign key, from child to parent here.
An alternative approach would be not to have 'favouritechild' fk as a field on the Parent model.
Instead you could have an is_favourite boolean field on the Child.
This may help:
https://github.com/anentropic/django-exclusivebooleanfield
That way you'd sidestep the whole problem of ensuring Children could only be made the favourite of the Parent they belong to.
The view code would be slightly different but the filtering logic would be straightforward.
In the admin you could even have an inline for Child models that exposed the is_favourite checkbox (if you only have a few children per parent) otherwise the admin would have to be done from the Child's side.
A much simpler variation of #s29's answer:
Instead of customising the form,
You can simply restrict the choices available in form field from your view:
what worked for me was:
in forms.py:
class AddIncomingPaymentForm(forms.ModelForm):
class Meta:
model = IncomingPayment
fields = ('description', 'amount', 'income_source', 'income_category', 'bank_account')
in views.py:
def addIncomingPayment(request):
form = AddIncomingPaymentForm()
form.fields['bank_account'].queryset = BankAccount.objects.filter(profile=request.user.profile)
from django.contrib import admin
from sopin.menus.models import Restaurant, DishType
class ObjInline(admin.TabularInline):
def __init__(self, parent_model, admin_site, obj=None):
self.obj = obj
super(ObjInline, self).__init__(parent_model, admin_site)
class ObjAdmin(admin.ModelAdmin):
def get_inline_instances(self, request, obj=None):
inline_instances = []
for inline_class in self.inlines:
inline = inline_class(self.model, self.admin_site, obj)
if request:
if not (inline.has_add_permission(request) or
inline.has_change_permission(request, obj) or
inline.has_delete_permission(request, obj)):
continue
if not inline.has_add_permission(request):
inline.max_num = 0
inline_instances.append(inline)
return inline_instances
class DishTypeInline(ObjInline):
model = DishType
def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
field = super(DishTypeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
if db_field.name == 'dishtype':
if self.obj is not None:
field.queryset = field.queryset.filter(restaurant__exact = self.obj)
else:
field.queryset = field.queryset.none()
return field
class RestaurantAdmin(ObjAdmin):
inlines = [
DishTypeInline
]

Categories