Is it possible to manually specify the set of related object to show in an inline, where no foreign key relation exists?
# Parent
class Diary(models.Model):
day = models.DateField()
activities = models.TextField()
# Child
class Sleep(models.Model):
start_time = models.DateTimeField()
end_time = models.DateTimeField()
class SleepInline(admin.TabularInline):
model=Sleep
def get_queryset(self, request):
# Return all Sleep objects where start_time and end_time are within Diary.day
return Sleep.objects.filter(XXX)
class DiaryAdmin(admin.ModelAdmin):
inlines = (SleepInline, )
I want my Diary model admin to display an inline for Sleep models that have start_time equal to the same day as Diary.day. The problem is that the Sleep model does not have a ForeignKey to Diary (instead, the relation is implicit by the use of dates).
Using the above, Django immediately complains that
<class 'records.admin.SleepInline'>: (admin.E202) 'records.Sleep' has no ForeignKey to 'records.Diary'.
How can I show the relevant Sleep instances as inlines on the Diary admin page?
There is no getting around the fact that Django admin inlines are built around ForeignKey fields (or ManyToManyField, OneToOneField). However, if I understand your goal, it's to avoid having to manage "date integrity" between your Diary.day and Sleep.start_time fields, i.e., the redundancy in a foreign key relation when that relation is really defined by Diary.day == Sleep.start_time.date()
A Django ForiegnKey field has a to_field property that allows the FK to index a column besides id. However, as you have a DateTimeField in Sleep and a DateField in Diary, we'll need to split that DateTimeField up. Also, a ForeignKey has to relate to something unique on the "1" side of the relation. Diary.day needs to be set unique=True.
In this approach, your models look like
from django.db import models
# Parent
class Diary(models.Model):
day = models.DateField(unique=True)
activities = models.TextField()
# Child
class Sleep(models.Model):
diary = models.ForeignKey(Diary, to_field='day', on_delete=models.CASCADE)
start_time = models.TimeField()
end_time = models.DateTimeField()
and then your admin.py is just
from django.contrib import admin
from .models import Sleep, Diary
class SleepInline(admin.TabularInline):
model=Sleep
#admin.register(Diary)
class DiaryAdmin(admin.ModelAdmin):
inlines = (SleepInline, )
Even though Sleep.start_time no longer has a date, the Django Admin is quite what you'd expect, and avoids "date redundancy":
Thinking ahead to a more real (and problematic) use case, say every user can have 1 Diary per day:
class Diary(models.Model):
user = models.ForeignKey(User)
day = models.DateField()
activities = models.TextField()
class Meta:
unique_together = ('user', 'day')
One would like to write something like
class Sleep(models.Model):
diary = models.ForeignKey(Diary, to_fields=['user', 'day'], on_delete=models.CASCADE)
However, there's no such feature in Django 1.11, nor can I find any serious discussion of adding that. Certainly composite foreign keys are allowed in Postgres and other SQL DBMS's. I get the impression from the Django source they're keeping their options open: https://github.com/django/django/blob/stable/1.11.x/django/db/models/fields/related.py#L621 hints at a future implementation.
Finally, https://pypi.python.org/pypi/django-composite-foreignkey looks interesting at first, but doesn't create "real" composite foreign keys, nor does it work with Django's admin.
Let me start by showing you the drawbacks of your logic:
When adding a foreign key, there are 2 operations, that are uncommon that require adjusting the relation: creating a new sleep object and updating the times on the sleep object.
When not using a foreign key, each time a diary is requested the lookup for the corresponding Sleep object(s) needs to be done. I'm assuming reading diaries is much more common then alterations of sleep objects, as it will be in most projects out there.
The additional drawback as you've noticed, is that you cannot use relational features. InlineAdmin is a relational feature, so as much as you say "making the admin work", it is really that you demand a hammer to unscrew a bolt.
But...the admin makes use of ModelForm. So if you construct the form with a formset (which cannot be a an inline formset for the same reason) and handle saving that formset yourself, it should be possible. The whole point of InlineFormset and InlineAdmin is to make generation of formsets from related models easier and for that it needs to know the relation.
And finally, you can add urls and build a custom page, and when extending the admin/base.html template, you will have access to the layout and javascript components.
You can achieve this, using nested_admin library.
So in myapp/admin.py:
from nested_admin.nested import NestedStackedInline, NestedModelAdmin
class SleepInline(NestedStackedInline):
model = Sleep
class DiaryAdmin(NestedModelAdmin):
inlines = [SleepInline]
Related
from django.db import models
from djago.db.models import F, Q
class(models.Model):
order_date = models.DateField()
class OrderLine(models.Model):
order = models.ForeignKeyField(Order)
loading_date = models.DateField()
class Meta:
constraints = [
models.CheckConstraint(check=Q(loading_date__gte=F("order__order_date")), name="disallow_backdated_loading")
I want to make sure always orderline loading_date is higher than orderdate
A CHECK constraint can span over a column, or over a table, but not over multiple tables, so this is not possible through a CHECK constraint.
Some databases allow to define triggers. These triggers run for example when a records is created/updated, and can run SQL queries, and decide to reject the creation/update based on such queries, but currently, the Django ORM does not support that.
One could also work with a composite primary key of an id and the creation date of the order, in which case the create timestamp is thus stored in the OrderLine table, and thus one can implement a check at the table level, but Django does not support working with composite primary keys for a number of reasons.
Therefore, besides running raw SQL, for example with a migration file that has a RunSQL operation [Django-doc], but this will likely be specific towards a database.
Therefore probably the most sensical check is to override the model clean() method [Django-doc]. Django however does not run the .clean() method before saving an object in the database, this is only done by ModelForms, and ModelAdmins. We can thus add a check with:
from django.core.exceptions import ValidationError
class OrderLine(models.Model):
order = models.ForeignKeyField(
Order,
on_delete=models.CASCADE
)
loading_date = models.DateField()
def clean(self):
if self.loading_date < self.order.order_date:
raise ValidationError('Can not load before ordering')
return super().clean()
I'm struggling to display a manytomany field in the admin with the related model in a user-friendly manner. The project is already up and running so adding a through table is not preferred.
The set-up is something along these lines;
class User(AbstractUser):
is_member_of_organization = models.ManyToManyField(Organization, blank=True, verbose_name=_("is member of organization(s)"), related_name='orgmembers')
class Organization(models.Model):
name = models.CharField(max_length=60, verbose_name=_("organization name"))
the only reasonable way I manage to display the related users with organization admin is via a TabularInline
admin.py
class UserOrgAdminInLine(admin.TabularInline):
model = User.is_admin_for_organization.through
extra = 0
#admin.register(Organization)
class OrganizationAdmin(admin.ModelAdmin):
inlines = (UserOrgAdminInLine,)
However, looking up users is not convenient as soon as their number increases. I would like something similar to filer_horizontal but I am not sure how to include it directly in the OrganizationAdmin admin class. Furthermore, I am using fieldsets (which I believe should have no special rules or syntax to it compared to ordinary fields = .
One little subquestion - in the tabular inline, when I use only model = User django throws an error that there is no foreign key to it, but when I use the additional .is_admin_for_organization.through it works, but there is no through table and I though that this work just in that case. Why is that?
Any help would be much appreciated.
Try adding
class UserOrgAdminInLine
raw_id_fields = ("is_member_of_organization",)
I have the following situation. I have three models, Post, User and Friends.
class User(models.Model):
name = models.CharField(max_length=100)
class Friend(models.Model):
user1 = models.ForeignKey(User,related_name='my_friends1')
user2 = models.ForeignKey(User,related_name='my_friends2')
class Post(models.Model):
subject = models.CharField(max_length=100)
user = models.ForeignKey(User)
Every time I bring users, I want to bring the number of his friends:
User.objects.filter(name__startswith='Joe').annotate(fc=Count('my_friends1'))
This works fine.
However, I want to make this work when I bring the users as nested objects of Post. I'm using there select_related to minimized DB calls, so I want to do something like:
Post.objects.filter(subject='sport').select_related('user').annotate(user__fc=Count('user__my_friends1'))
However, this creates field user__fc under post, and not field fc under post.user.
Is there a way to achieve this functionality?
You can make use of Prefetch class:
from django.db.models import Count, Prefetch
posts = Post.objects.all().prefetch_related(Prefetch('user', User.objects.annotate(fc=Count('my_friends1'))))
for post in posts:
print(post.subject)
print(post.user.fc)
NB : this does two database queries (Django does the join between Post and User in this case) :
'SELECT "myapp_post"."id", "myapp_post"."subject", "myapp_post"."user_id" FROM "myapp_post"
'SELECT "myapp_user"."id", "myapp_user"."password", "myapp_user"."last_login", "myapp_user"."is_superuser", "myapp_user"."username", "myapp_user"."first_name", "myapp_user"."last_name", "myapp_user"."email", "myapp_user"."is_staff", "myapp_user"."is_active", "myapp_user"."date_joined", COUNT("myapp_friend"."id") AS "fc" FROM "myapp_user" LEFT OUTER JOIN "myapp_friend" ON ("myapp_user"."id" = "myapp_friend"."user1_id") WHERE "myapp_user"."id" IN (3, 4) GROUP BY "myapp_user"."id", "myapp_user"."password", "myapp_user"."last_login", "myapp_user"."is_superuser", "myapp_user"."username", "myapp_user"."first_name", "myapp_user"."last_name", "myapp_user"."email", "myapp_user"."is_staff", "myapp_user"."is_active", "myapp_user"."date_joined"
You can define a custom manger for your models, as described here and then override its get_queryset() method to add the custom column to your model upon query.
In order to use this manager for a reverse relation, you should set the base manager as described in the docs.
Another approach would be something like this, which you specify the manager of the related model with a hard-coded attribute.
I have two models designated for tracking what users have upvoted an Article instance (in another app, in this case articlescraper).
from django.contrib.auth.models import User
class UserProfile(models.Model):
user = models.OneToOneField(User)
articles_upvoted = models.ManyToManyField('useraccounts.UpvotedArticle',
null=True,
blank=True)
class UpvotedArticle(models.Model):
article = models.ForeignKey('articlescraper.Article')
user = models.ForeignKey(User)
In a Django shell, I've tried to get a list of articles by interacting with UserProfile:
a = UserProfile.objects.get(pk=1)
a.articles_upvoted.all()
Which returns:
[]
However, then I went a little further:
b = UpvotedArticle.objects.filter(user=User.objects.get(pk=1))
b
Which returns:
[<UpvotedArticle: Arch Linux Lexmark S305 Drivers>, <UpvotedArticle: Structure of a Haystack project>]
Which is the expected behavior, and is mirrored in the Django admin in both UserProfile and UpvotedArticle categories.
I don't understand, however, why attempting to get a list of articles can't be done the way I initially tried to using a.articles_upvoted.all() if the two models are linked.
Because these aren't the same relationship. By defining a ForeignKey on one side, and a ManyToMany on the other, you've given the database two separate places to store information about article upvoting.
You should remove the ManyToManyField on UserProfile, and just use the automatic reverse relationship:
a = UserProfile.objects.get(pk=1)
a.upvotedarticle_set.all()
Alternatively, you could recognize UpvotedArticle as the "through" table of the ManyToMany relationship, and mark it as such explicitly in the definition of articles_upvoted - note though that the relationship should be with articlescraper.Article, not UpvotedArticle:
article_upvoted = models.ManyToManyField(articlescraper.Article, null=True,
blank=True, through=UpvotedArticle)
Although since you're not adding any extra data on that relationship, which is the usual reason for defining an explicit through table, you may want to drop it completely and just rely on the automatic one that Django will create.
This is how my models look:
class QuestionTagM2M(models.Model):
tag = models.ForeignKey('Tag')
question = models.ForeignKey('Question')
date_added = models.DateTimeField(auto_now_add=True)
class Tag(models.Model):
description = models.CharField(max_length=100, unique=True)
class Question(models.Model):
tags = models.ManyToManyField(Tag, through=QuestionTagM2M, related_name='questions')
All I really wanted to do was add a timestamp when a given manytomany relationship was created. It makes sense, but it also adds a bit of complexity. Apart from removing the .add() functionality [despite the fact that the only field I'm really adding is auto-created so it technically shouldn't interfere with this anymore]. But I can live with that, as I don't mind doing the extra QuestionTagM2M.objects.create(question=,tag=) instead if it means gaining the additional timestamp functionality.
My issue is I really would love to be able to preserve my filter_horizontal javascript widget in the admin. I know the docs say I can use an inline instead, but this is just too unwieldy because there are no additional fields that would actually be in the inline apart from the foreign key to the Tag anyway.
Also, in the larger scheme of my database schema, my Question objects are already displayed as an inline on my admin page, and since Django doesn't support nested inlines in the admin [yet], I have no way of selecting tags for a given question.
Is there any way to override formfield_for_manytomany(self, db_field, request=None, **kwargs) or something similar to allow for my usage of the nifty filter_horizontal widget and the auto creation of the date_added column to the database?
This seems like something that django should be able to do natively as long as you specify that all columns in the intermediate are automatically created (other than the foreign keys) perhaps with auto_created=True? or something of the like
There are ways to do this
As provided by #obsoleter in the comment below : set QuestionTagM2M._meta.auto_created = True and deal w/ syncdb matters.
Dynamically add date_added field to the M2M model of Question model in models.py
class Question(models.Model):
# use auto-created M2M model
tags = models.ManyToMany(Tag, related_name='questions')
# add date_added field to the M2M model
models.DateTimeField(auto_now_add=True).contribute_to_class(
Question.tags.through, 'date_added')
Then you could use it in admin as normal ManyToManyField.
In Python shell, use Question.tags.through to refer the M2M model.
Note, If you don't use South, then syncdb is enough; If you do, South does not like
this way and will not freeze date_added field, you need to manually write migration to add/remove the corresponding column.
Customize ModelAdmin:
Don't define fields inside customized ModelAdmin, only define filter_horizontal. This will bypass the field validation mentioned in Irfan's answer.
Customize formfield_for_dbfield() or formfield_for_manytomany() to make Django admin to use widgets.FilteredSelectMultiple for the tags field.
Customize save_related() method inside your ModelAdmin class, like
def save_related(self, request, form, *args, **kwargs):
tags = form.cleaned_data.pop('tags', ())
question = form.instance
for tag in tags:
QuestionTagM2M.objects.create(tag=tag, question=question)
super(QuestionAdmin, self).save_related(request, form, *args, **kwargs)
Also, you could patch __set__() of the ReverseManyRelatedObjectsDescriptor field descriptor of ManyToManyField for date_added to save M2M instance w/o raise exception.
From https://docs.djangoproject.com/en/dev/ref/contrib/admin/#working-with-many-to-many-intermediary-models
When you specify an intermediary model using the through argument to a ManyToManyField, the admin will not display a widget by default. This is because each instance of that intermediary model requires more information than could be displayed in a single widget, and the layout required for multiple widgets will vary depending on the intermediate model.
However, you can try including the tags field explicitly by using fields = ('tags',) in admin. This will cause this validation exception
'QuestionAdmin.fields' can't include the ManyToManyField field 'tags' because 'tags' manually specifies a 'through' model.
This validation is implemented in https://github.com/django/django/blob/master/django/contrib/admin/validation.py#L256
if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created:
raise ImproperlyConfigured("'%s.%s' "
"can't include the ManyToManyField field '%s' because "
"'%s' manually specifies a 'through' model." % (
cls.__name__, label, field, field))
I don't think that you can bypass this validation unless you implement your own custom field to be used as ManyToManyField.
The docs may have changed since the previous answers were posted. I took a look at the django docs link that #Irfan mentioned and it seems to be a more straight forward then it used to be.
Add an inline class to your admin.py and set the model to your M2M model
class QuestionTagM2MInline(admin.TabularInline):
model = QuestionTagM2M
extra = 1
set inlines in your admin class to contain the Inline you just defined
class QuestionAdmin(admin.ModelAdmin):
#...other stuff here
inlines = (QuestionTagM2MInline,)
Don't forget to register this admin class
admin.site.register(Question, QuestionAdmin)
After doing the above when I click on a question I have the form to do all the normal edits on it and below that are a list of the elements in my m2m relationship where I can add entries or edit existing ones.