Django admin - OneToOneField inline throws "has no ForeignKey" exception - python

I have a very simple app which at the moment declares two models: one is called "Content" and simply holds content data and the other is "Page" which includes "Content" as a OneToOneField.
The reason I've done this is so that I can have "Page" as an actual concrete class that I use and when other models in other modules I'm planning need page data, they can simply include "Content" as a OneToOneField. I've done it this way to avoid inheritance and use composition instead.
models.py:
from django.db import models
class Content(models.Model):
"""Basic page data which can be used by other modules"""
title = models.CharField(max_length=200)
html_title = models.CharField(max_length=200)
meta_desc = models.CharField(max_length=200)
keywords = models.CharField(max_length=200)
content = models.TextField()
class Page(models.Model):
"""Concrete implementation of a basic page managed by the admin"""
slug = models.SlugField()
content = models.OneToOneField(Content)
def __str__(self):
return self.content.title
admin.py:
from django.contrib import admin
from content.models import Page, Content
class ContentInline(admin.TabularInline):
model = Content
fields = ('title', 'html_title', 'meta_desc', 'keywords', 'content')
class PageAdmin(admin.ModelAdmin):
fields = ('slug',)
inlines = [ContentInline]
On the page admin I get this exception:
Exception at /admin/content/page/add/
<class 'content.models.Content'> has no ForeignKey to <class 'content.models.Page'>
What is says of course is correct, but I cannot seem to find a way of doing what I want, which is to include an inline of the non-defining side of a relationship. I don't want to declare the relationship on "Content" as then I'd have to define every single relationship to it inside that class which would introduce dependencies to other modules, which in my opinion it should know nothing about.
Using Django 1.6 on Python 3.3.
Edit: As indicated in the comments, I've decided to use inheritance. My initial concern about this was that I wanted the flexibility to be able to compose classes from multiple other classes. However, since the Django ORM does support multiple inheritance and if I'd realised that method was called "mixins" (new to Python) I would have got somewhere a lot sooner.
Example mixins with models:
from django.db import models
class Content(models.Model):
"""Basic page data which can be used by other modules"""
title = models.CharField(max_length=200)
html_title = models.CharField(max_length=200)
meta_desc = models.CharField(max_length=200)
keywords = models.CharField(max_length=200)
content = models.TextField()
def __str__(self):
return self.title
class Meta:
abstract = True
class Data(models.Model):
data_name = models.CharField(max_length=200)
class Meta:
abstract = True
class Page(Content, Data):
"""Concrete implementation of a basic page managed by the admin"""
slug = models.SlugField()
And then I can just use it as one model in admin.py.

Another solution is moving the OneToOneField from Content to Page
class Content(models.Model):
"""Basic page data which can be used by other modules"""
title = models.CharField(max_length=200)
html_title = models.CharField(max_length=200)
meta_desc = models.CharField(max_length=200)
keywords = models.CharField(max_length=200)
content = models.TextField()
page = models.OneToOneField(Page, primary_key=True, related_name="content")
class Page(models.Model):
"""Concrete implementation of a basic page managed by the admin"""
slug = models.SlugField()
def __str__(self):
return self.content.title
You can still do page.content and the inline form will work out of the box
EDIT:
One cons of that approach is that it will allow the user to create a page without assigning any content to it (in which case page.content will crash)
Its very easy to overcome this issue by creating custom form
class ContentAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
kwargs["empty_permitted"] = False
super(ContentAdminForm, self).__init__(*args, **kwargs)
Then in the admin page
class ContentInline(admin.TabularInline):
model = Content
form = ContentAdminForm
fields = ('title', 'html_title', 'meta_desc', 'keywords', 'content')

If you don't want to change your models at all, there's a django module to display the non-defining side inline: django_reverse_admin
You'll need to add django_reverse_admin to your requirements.txt:
-e git+https://github.com/anziem/django_reverse_admin.git#egg=django_reverse_admin
Then import it:
admin.py
from django.contrib import admin
from django_reverse_admin import ReverseModelAdmin
from content.models import Page, Content
# don't need to define an inline anymore for Content
class PageAdmin(ReverseModelAdmin):
fields = ('slug',)
inline_reverse = ['content']
inline_type = 'tabular' # or could be 'stacked'

Related

Django Rest Framework - Edit related object set

I have a listing system in my Django project, with a Tag object that attaches a list of Tags to a Listing. Each object has its own standard Viewset which is passed to router.register(). The current way I'm editing or creating Tags is through a POST or PATCH to /tags/. Ideally, I'd do this by doing a PATCH /listings/[id]/ with a JSON body of {"tags": [{"type": "foo", "text": "bar"}]}. I've provided a slightly simplified version of my code below.
serializers.py
class NestedTagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ['text', 'type']
class ListingSerializer(serializers.ModelSerializer):
tags = NestedTagSerializer(many=True, read_only=False)
class Meta:
model = Listing
fields = ['tags', 'title', 'id']
models.py
class Listing(models.Model):
title = models.CharField(max_length=255)
description = models.TextField()
class Tag(models.Model):
listing = models.ForeignKey(Listing, on_delete=CASCADE)
text = models.CharField(max_length=255)
type = models.CharField(max_length=255)
Is there a nice way to do this with Django Rest Framework?
I faced similar issue before and for me routing was main issue.
To implement nested routing, can use drf-extensions
urls.py
from rest_framework import routers
from rest_framework_extensions.routers import NestedRouterMixin
class NestedDefaultRouter(NestedRouterMixin, routers.DefaultRouter):
pass
router = NestedDefaultRouter()
listing_router = router.register('listings/', ListingView)
tag_router = listing_router.register('tags', TagView, basename='tags', parents_query_lookups=['listing_id']
urlpartterns = router.urls
It will generate urls as following :
/listings/ ListingView listing-list
/listings/<parent_lookup_listing_id>/tags/ TagView listing-tags-list
/listings/<parent_lookup_listing_id>/tags/<pk>/ TagView listing-tags-detail
/listings/<parent_lookup_listing_id>/tags/<pk>\.<format>/ TagView listing-tags-detail
/listings/<parent_lookup_listing_id>/tags\.<format>/ TagView listing-tags-list
parent_lookup_listing_id will be used to determine listing
listing_id = self.kwargs.get('parent_lookup_listing_id')

How to nest a model inside a model in Django database

I have the following models, currently connected by ForeignKey:
class Topic(models.Model):
text = models.CharField(max_length=200)
date_added = models.DateTimeField(auto_now_add=True)
def __str__ (self):
return self.text
class Entry(models.Model):
topic = models.ForeignKey(Topic, on_delete=models.CASCADE)
text = models.TextField()
date_added = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name_plural = 'entries'
def __str__(self):
if len(self.text) > 50:
return self.text[:50] + "..."
else:
return self.text
Is it possible to connect them in a way that in Django admin site I could go to a particular topic and have a list of all the entries displayed? Currently nothing gets displayed and I can only see the connection when I go to a particular entry (there is a dropdown saying to which topic current entry belongs).
EDIT: these are inlines added to the admin.py:
from django.contrib import admin
from javascript.models import Topic, Entry
# Register your models here.
admin.site.register(Topic)
admin.site.register(Entry)
#inlines
class EntryInline(admin.TabularInline):
model = Entry
class TopicAdmin(admin.ModelAdmin):
inlines = [EntryInline]
You can proceed by using inlines(TabularInline or StackedInline)
First of all create inline class for your entries:
class EntryInlines(admin.TabularInline):
model = Entry
# you can customize fields displayed here; make any field as read only if you want
Now include EntryInlines to the model where you want to display data (only if foreign Entry model has foreign key to respective model) as below:
class TopicAdmin(admin.ModelAdmin):
inlines = [EntryInlines] # you can add multiple inline tables too. pass it in the list
# all other customization of your model admin if any
If you have nested foreign keys then you need to add this third-party-package like django-nested-inline

Method for adding sub-section to an inline admin in Django?

So like the title says. I am building a book structure and I am looking to make it so when in the admin panel and editing a book object, one can view and add related pages to the book. The page itself also has related objects called sections that are essentially paragraph objects within the page object.
My model is below:
class Textbook(models.Model):
founder = models.CharField(max_length=256)
title = models.CharField(max_length=256)
cover = models.ImageField(upload_to=get_image_path, blank=True, null=True)
def __str__(self):
return self.title
class Page(models.Model):
textbook = models.ForeignKey(Textbook,related_name="pages")
page_title = models.CharField(max_length = 256)
page_num = models.IntegerField()
def __str__(self):
return self.page_title
class Section(models.Model):
page = models.ForeignKey(Page,related_name="sections")
section_title = models.CharField(max_length=256)
text = models.TextField(max_length = 1024)
image = models.CharField(max_length = 256)
def __str__(self):
return self.section_title
Ideally there would be an add new page button within the textbook add/edit panel where I could then add section to that previously mentioned page.
Current admin.py:
class PageInline(admin.TabularInline):
model = Page
class TextbookAdmin(admin.ModelAdmin):
inlines = [
PageInline,
]
admin.site.register(Textbook,TextbookAdmin)
admin.site.register(Section)
Is there a currently built in method like a nest inline that could be used or I am just not thinking in the right mindset for Django? If this is not the proper or ideal way to structure this, what is the proper method?
If you want to keep the structure perhaps the easiest solution is to use Django Nested Inline package and thus keeping everything in one view.
from django.contrib import admin
from nested_inline.admin import NestedStackedInline, NestedModelAdmin
from .models import Textbook, Page, Section
class SectionInline(NestedStackedInline):
model = Section
extra = 1
class PageInline(NestedStackedInline):
model = Page
extra = 1
inlines = [SectionInline]
#admin.register(Textbook)
class TextbookAdmin(NestedModelAdmin):
inlines = [PageInline]

Django unique together relationship with field and manytomany on self

I'm try create post with language and content, and relate it on other versions of same page, but I'm get stuck
class Page(models.Model):
content = models.TextField()
language = models.CharField(max_length=7, choices=settings.LANGUAGES)
versions = models.ManyToManyField('self', blank=True)
class Meta:
unique_together = ('language', 'versions',)
This will not work properly, because Django not allow make "unique" ManyToMany fields.
Then I'm try make same relationship trough related model:
class VersionsPage(models.Model):
pass
# ToDo: add unique together here, to foreign key field
class Page(models.Model):
...
versions = models.ManyToManyField('self', blank=True, through="VersionsPage")
Anyone know how to make that without using symmetrical=False?
I think you are looking for something like this:
class Page(models.Model):
pass
class PageVersion(models.Model):
page = models.ForeignKey(Page, related_name='versions')
content = models.TextField()
language = models.CharField(max_length=7, choices=settings.LANGUAGES)
class Meta:
unique_together = ('page', 'language',)
#getting all page versions:
page = Page.objects.get(pk=some_id)
versions = page.versions.all()

Django MTMField: limit_choices_to = other_ForeignKeyField_on_same_model?

I've got a couple django models that look like this:
from django.contrib.sites.models import Site
class Photo(models.Model):
title = models.CharField(max_length=100)
site = models.ForeignKey(Site)
file = models.ImageField(upload_to=get_site_profile_path)
def __unicode__(self):
return self.title
class Gallery(models.Model):
name = models.CharField(max_length=40)
site = models.ForeignKey(Site)
photos = models.ManyToManyField(Photo, limit_choices_to = {'site':name} )
def __unicode__(self):
return self.name
I'm having all kinds of fun trying to get the limit_choices_to working on the Gallery model. I only want the Admin to show choices for photos that belong to the same site as this gallery. Is this possible?
Yes. You need to override the form that admin uses for the Gallery model, then limit the queryset of the photos field in that form:
class GalleryAdminForm(django.forms.ModelForm):
class Meta:
model = Gallery
def __init__(self, *args, **kwargs):
super(GalleryAdminForm, self).__init__(*args, **kwargs)
self.fields['segments'].queryset = Photo.objects.filter(site=self.instance.site)
class GalleryAdmin(django.contrib.admin.ModelAdmin):
form = GalleryAdminForm
django.contrib.admin.site.register(Gallery, GalleryAdmin)
I would delete site field on my Photo model and add a ForeignKey to Gallery. I would remove limit_choices_to from photos fields on Gallery model.
Because you are using ForeignKeys to Sites, that means sites don't share galleries and photos. Therefore having those I mentioned above is already useless.
class Photo(models.Model):
title = models.CharField(max_length=100)
gallery = models.ForeignKey(Gallery, related_name='photos')
file = models.ImageField(upload_to=get_site_profile_path)
def __unicode__(self):
return self.title
class Gallery(models.Model):
name = models.CharField(max_length=40)
site = models.ForeignKey(Site)
def __unicode__(self):
return self.name
Once you set the site on a gallery all its photos will inherit this property. And the site will be accessible as photo_instance.gallery.site:
#property
def site(self):
return self.gallery.site
This should work as if you had a site field. But I haven't tested it.
Things change or course, if you decide that a gallery or a photo can appear in multiple sites.
According to the docs, "limit_choices_to has no effect when used on a ManyToManyField with an intermediate table". By my reading, that means it has no effect at all, because ManyToManyFields use intermediate tables...
I haven't tried to make it work in the Admin site, but from your own views, you can create a form and override the queryset used to populate the list of choices:
form.fields["photos"].queryset = request.user.photo_set.all()

Categories