I have just begun to play around with Django admin views, and to start off, I am trying to do something very simple: showing several fields in the listing of objects using list_display as explained here: https://docs.djangoproject.com/en/dev/ref/contrib/admin/
This is my dead simple code:
class ArticleAdmin(admin.ModelAdmin):
list_display = ('title', 'category')
Unfortunately, the list_display option is causing the columnar view to appear, but only some of the objects (40 out of 85) are now displaying in the listing. I cannot deduce why certain objects are showing over the others - their fields look like they are filled similarly. It's clearly not paginating, because when I tried it on an admin of another model, it showed only 2 objects out of about 70 objects.
What might be going on here?
[UPDATE] Article Model:
class Article(models.Model):
revision = models.ForeignKey('ArticleRevision', related_name="current_revision")
category = models.ForeignKey('meta.Category')
language = models.ForeignKey('meta.Language', default=get_default_language)
created = models.DateTimeField(auto_now_add=True, editable=False)
changed = models.DateTimeField(auto_now=True, editable=False)
title = models.CharField(max_length=256)
resources = models.ManyToManyField('oer.Resource', blank=True)
image = models.ManyToManyField('media.Image', blank=True)
views = models.IntegerField(editable=False, default=0)
license = models.ForeignKey('license.License', default=get_default_license)
slug = models.SlugField(max_length=256)
difficulty = models.PositiveIntegerField(editable=True, default=0)
published = models.NullBooleanField()
citation = models.CharField(max_length=1024, blank=True, null=True)
Before adding list_display:
After adding list_display:
[UPDATE] This behaviour occurs only when ForeignKey fields are included in list_display tuple. Any of them.
[UPDATE] Category model code:
class Category(models.Model):
title = models.CharField(max_length=256)
parent = models.ForeignKey('self')
project = models.NullBooleanField(default=False)
created = models.DateTimeField(auto_now_add=True, editable=False)
slug = models.SlugField(max_length=256, blank=True)
def __unicode__(self):
return self.title
This behavior is caused by a foreign key relation somewhere that is not declared as nullable, but nonetheless has a null value in the database. When you have a ManyToOne relationship in list_display, the change list class will always execute the query using select_related. (See the get_query_set method in django.contrib.admin.views.ChangeList).
select_related by default follows all foreign keys on each object, so any broken foreign key found by this query will cause data to drop out when the query is evaluated. This is not specific to the admin; you can interactively test it by comparing the results of Article.objects.all() to Article.objects.all().select_related().
There's no simple way to control which foreign keys the admin will look up - select_related takes some parameters, but the admin doesn't expose a way to pass them through. In theory you could write your own ChangeList class and override get_query_set, but I don't recommend that.
The real fix is to make sure your foreign key model fields accurately reflect the state of your database in their null settings. Personally, I'd probably do this by commenting out all FKs on Article other than Category, seeing if that helps, then turning them back on one by one until things start breaking. The problem doesn't have to be with a FK on an article itself; if a revision, language or category has a broken FK that will still cause the join to miss rows. Or if something they relate to has a broken FK, etc etc.
Related
I am running into a little bit of unique problem and wanted to see which solution fit best practice or if I was missing anything in my design.
I have a model - it has a field on it that represents a metric. That metric is a foreign key to an object which can come from several database tables.
Idea one:
Multiple ForeignKey fields. I'll have the benefits of the cascade options, direct access to the foreign key model instance from MyModel, (although that's an easy property to add), and the related lookups. Pitfalls include needing to check an arbitrary number of fields on the model for a FK. Another is logic to make sure that only one FK field has a value at a given time (easy to check presave) although .update poses a problem. Then theres added space in the database from all of the columns, although that is less concerning.
class MyModel(models.Model):
source_one = models.ForeignKey(
SourceOne,
null=True,
blank=True,
on_delete=models.SET_NULL,
db_index=True
)
source_two = models.ForeignKey(
SourceTwo,
null=True,
blank=True,
on_delete=models.SET_NULL,
db_index=True
)
source_three = models.ForeignKey(
SourceThree,
null=True,
blank=True,
on_delete=models.SET_NULL,
db_index=True
)
Idea two:
Store a source_id and source on the model. Biggest concern I have with this is needing to maintain logic to set these fields to null if the source is deleted. It otherwise seems like a cleaner solution, but not sure if the overhead to make sure the data is accurate is worth it. I can probably write some logic in a delete hook on the fk models to clean MyModel up if necessary.
class MyModel(models.Model):
ONE = 1
TWO = 2
THREE = 3
SOURCES = (
(ONE, "SourceOne"),
(TWO, "SourceTwo"),
(THREE, "SourceThree")
)
source_id = models.PositiveIntegerField(null=True, blank=True)
source = models.PositiveIntegerField(null=True, blank=True, choices=SOURCES)
I would love the communities opinion.
Your second idea seems fragile as the integrity is not ensured by the database as you have pointed out yourself.
Without knowing more about the use case, it's difficult to provide an enlightened advice however if your "metric" object is refered by many other tables, I wonder if you should consider approaching this the other way round, i.e. defining the relationships from the models consuming this metric.
To exemplify, let's say that your project is a photo gallery and that your model represents a tag. Tags could be associated to photos, photo albums or users (e.g.. the tags they want to follow).
The approach would be as follow:
class Tag(models.Model):
pass
class Photo(models.Model):
tags = models.ManyToManyField(Tag)
class Album(models.Model):
tags = models.ManyToManyField(Tag)
class User(AbstractUser):
followed_tags = models.ManyToManyField(Tag)
You may even consider to factor in this relationship in an abstract model as outlined below:
class Tag(models.Model):
pass
class TaggedModel(models.Model):
tags = models.ManyToManyField(Tag)
class Meta:
abstract = True
class Photo(TaggedModel):
pass
As mentioned in the comments, you are looking for a Generic Relation:
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
class SourceA(models.Model):
name = models.CharField(max_length=45)
class SourceB(models.Model):
name = models.CharField(max_length=45)
class MyModel(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
source = GenericForeignKey('content_type', 'object_id')
There are three parts to setting up a Generic Relation:
Give your model a ForeignKey to ContentType. The usual name for this field is “content_type”.
Give your model a field that can store primary key values from the models you’ll be relating to. For most models, this means a PositiveIntegerField. The usual name for this field is “object_id”.
Give your model a GenericForeignKey, and pass it the names of the two fields described above. If these fields are named “content_type” and “object_id”, you can omit this – those are the default field names GenericForeignKey will look for.
Now you can pass any Source instance to the source field of MyModel, regardless of which model it belongs to:
source_a = SourceA.objects.first()
source_b = SourceB.objects.first()
MyModel.objects.create(source=source_a)
MyModel.objects.create(source=source_b)
I am having trouble deciding how to structure my models for a particular data structure.
The models I have would be Posts, Groups, Users.
I want the Post model that can be posted from a groups page or user page and potentially more, like an events page.
Posts would contain fields for text, images(fk), user, view count, rating score (from -- a reference to where ever it was posted from like user or group page, though I am unsure how to make this connection yet)
I thought about using a Generic Foreign Key to assign a field to different models but read articles suggesting to avoid it. I tried the suggested models, but I wasn't unsure if they were the right approach for what I required.
At the moment I went with Alternative 4 - multi-table inheritance
class Group(models.Model):
name = models.CharField(max_length=64)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='_groups')
members = models.ManyToManyField(
settings.AUTH_USER_MODEL)
def __str__(self):
return f'{self.name} -- {self.created_by}'
def save(self, *args, **kwargs):
# https://stackoverflow.com/a/35647389/1294405
created = self._state.adding
super(Group, self).save(*args, **kwargs)
if created:
if not self.members.filter(pk=self.created_by.pk).exists():
self.members.add(self.created_by)
class Post(models.Model):
content = models.TextField(blank=True, default='')
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="%(app_label)s_%(class)s_posts",
related_query_name="%(app_label)s_%(class)ss")
# class Meta:
# abstract = True
def __str__(self):
return f'{self.content} -- {self.created_by}'
class PostImage(models.Model):
image = models.ImageField(upload_to=unique_upload)
post = models.ForeignKey(
Post, related_name='images', on_delete=models.CASCADE)
def __str__(self):
return '{}'.format(self.image.name)
class UserPost(models.Model):
post = models.OneToOneField(
Post, null=True, blank=True, related_name='_uPost', on_delete=models.CASCADE)
class GroupPost(models.Model):
post = models.OneToOneField(
Post, null=True, blank=True, related_name='_gPost', on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
To do some specific filters ex:
Filter specific group post
Post.objects.filter(_gPost__group=group)
Filter specific user post
Post.objects.filter(created_by=user) # exclude groups with _gPost__isnull=False
Create post to user/group
p = Post.objects.create(...)
up = UserPost.objects.create(post=p)
gp = GroupPost.objects.create(post=p)
Really I am wondering if this is a sensible approach. The current way of a filter and creating feel odd. So only thing making me hesitant on this approach is just how it looks.
So, is Generic ForeignKey the place to use here or the current multi-table approach. I tried going with inheritance with abstract = True and that was unable to work as I need a foreign key to base post model. Even with no abstract, I got the foreign key reference, but filter became frustrating.
Edit:
So far only weird issues(but not really) are when filtering I have to be explicit to exclude some field to get what I want, using only .filter(created_by=...) only would get all other intermediate tables.
Filter post excluding all other tablets would requirePost.objects.filter(_uPost__isnull=True, _gPost__isnull=True, _**__isnull=True) which could end up being tedious.
I think your approach is sensible and that's probably how I would structure it.
Another approach would be to move the Group and Event foreignkeys into the Post model and let them be NULL/None if the Post wasn't posted to a group or event. That improves performance a bit and makes the filters a bit more sensible, but I would avoid that approach if you think Posts can be added to many other models in the future (as you'd have to keep adding more and more foreignkeys).
At the moment I will stick with my current pattern.
Some extra reading for anyone interested.
https://www.slideshare.net/billkarwin/sql-antipatterns-strike-back/32-Polymorphic_Associations_Of_course_some
I have a model OrderPage which is manytomany to Site. In Django admin, I want to restrict the selection of sites(Sites which belong to existing OrderPage can not be selected again). Can I do it with unique_together ? I get an error with following model ManyToManyFields are not supported in unique_together
class OrderPage(models.Model):
description = models.CharField(max_length=255, blank=False)
sites = models.ManyToManyField(Site)
class Meta:
unique_together = (('id', 'sites'),)
class Order(models.Model):
order_page = models.ForeignKey(OrderPage)
class OrderPageAdmin(admin.ModelAdmin):
filter_horizontal = ('sites',)
admin.site.register(OrderPage, OrderPageAdmin)
If an Site can have only one OrderPage, you don't need to worry about unique_together.
Ideally you should subclass Site and use a ForeignKey from that to OrderPage. That would natively give you what you're looking for: each site would be able to have one OrderPage, and each OrderPage multiple Sites. This would be the cleanest but you would have to use your subclass throughout the program in place of the original Site which might be more work than you want right now.
class BetterSite(Site):
order_page = models.ForeignKey('OrderPage')
The dirtier way is to keep your M2M and just set the site as unique, since there should only ever be one entry on each site in the M2M table. You would use a 'through' table so you could set the custom uniqueness value:
class OrderPage(models.Model):
description = models.CharField(max_length=255, blank=False)
sites = models.ManyToManyField(Site, through='OrderPageToSite')
class OrderPageToSite(models.Model):
order_page = models.ForeignKey(OrderPage)
site = models.ForeignKey(Site, unique=True)
(Note that I've left these simple but in your FK fields you should also consider setting on_delete and related_name)
I imported my (PHP) old site's database tables into Django. By default it created a bunch of primary key fields within the model (since most of them were called things like news_id instead of id).
I just renamed all the primary keys to id and removed the fields from the model. The problem then came specifically with my News model. New stuff that I add doesn't appear in the admin. When I remove the following line from my ModelAdmin, they show up:
list_display = ['headline_text', 'news_category', 'date_posted', 'is_sticky']
Specifically, it's the news_category field that causes problems. If I remove it from that list then I see my new objects. Now, when I edit those items directly (hacking the URL with the item ID) they have a valid category, likewise in the database. Here's the model definitions:
class NewsCategory(models.Model):
def __unicode__(self):
return self.cat_name
#news_category_id = models.IntegerField(primary_key=True, editable=False)
cat_name = models.CharField('Category name', max_length=75)
cat_link = models.SlugField('Category name URL slug', max_length=75, blank=True, help_text='Used in URLs, eg spb.com/news/this-is-the-url-slug/ - generated automatically by default')
class Meta:
db_table = u'news_categories'
ordering = ["cat_name"]
verbose_name_plural = "News categories"
class News(models.Model):
def __unicode__(self):
return self.headline_text
#news_id = models.IntegerField(primary_key=True, editable=False)
news_category = models.ForeignKey('NewsCategory')
writer = models.ForeignKey(Writer) # todo - automate
headline_text = models.CharField(max_length=75)
headline_link = models.SlugField('Headline URL slug', max_length=75, blank=True, help_text='Used in URLs, eg spb.com/news/this-is-the-url-slug/ - generated automatically by default')
body = models.TextField()
extra = models.TextField(blank=True)
date_posted = models.DateTimeField(auto_now_add=True)
is_sticky = models.BooleanField('Is this story featured on the homepage?', blank=True)
tags = TaggableManager(blank=True)
class Meta:
db_table = u'news'
verbose_name_plural = "News"
You can see where I've commented out the autogenerated primary key fields.
It seems like somehow Django thinks my new items don't have news_category_ids, but they definitely do. I tried editing an existing piece of news and changing the category and it worked as normal. If I run a search for one of the new items, it doesn't show up, but the bottom of the search says "1 News found", so something is going on.
Any tips gratefully received.
EDIT: here's my ModelAdmin too:
class NewsCategoryAdmin(admin.ModelAdmin):
prepopulated_fields = {"cat_link": ("cat_name",)}
list_display = ['cat_name', '_cat_count']
def _cat_count(self, obj):
return obj.news_set.count()
_cat_count.short_description = "Number of news stories"
class NewsImageInline(admin.TabularInline):
model = NewsImage
extra = 1
class NewsAdmin(admin.ModelAdmin):
prepopulated_fields = {"headline_link": ("headline_text",)}
list_display = ['headline_text', 'news_category', 'date_posted', 'is_sticky'] #breaking line
list_filter = ['news_category', 'date_posted', 'is_sticky']
search_fields = ['headline_text']
inlines = [NewsImageInline]
The answer you are looking for I think would lie in the SQL schema that you altered and not in the django models.
It could probably have something to do with null or blank values in the news_category_id, or news that belongs to a category that doesn't exist in the news_category. Things I'd check:
You have renamed the primary key on the News category from news_category_id to id. Does the foreign key on the News also map to news_category_id and not anything else?
Are all the values captured in the news.news_category also present in news_category.id
Also, as an aside, I don't see any reason why you need to rename the primary keys to id from something that they already are. Just marking them primary_key=True works just fine. Django provides you a convenient alias pk to access a model's integer primary key, irrespective of what the name of the field actually is.
How would I go about prepopulating a slug from a foreign key? Here are how some of my models are setup:
Class Title(models.Model):
title = models.CharField(max_length=256)
slug = models.SlugField()
class Issue(models.Model):
title = models.ForeignKey(Title)
number = models.IntegerField(help_text="Do not include the '#'.")
slug = models.SlugField()
admin.py:
class IssueAdmin (admin.ModelAdmin):
prepopulated_fields = {"slug": ("title",)}
admin.site.register(Issue, IssueAdmin)
What the Issue prepopulates is the ID of the foreign key, but I suppose I would need it to preopulate the slug of the foreign key. How would I go about doing this? I am using Django 1.3. I have checked other threads, but they seem to refer to version of Django a few years older that don't work anymore.
I need the Titles to display the list of issues. So far, it works. And you can click on the link to the issue to see what the issue displays.
I feel as if reworking the Title to abstract classes the way Skidoosh will not allow me to view subsets of objects....
If you check the docs (http://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.prepopulated_fields) it does state that you can't reference a foreign key field.
Looking at you design would this not work better:
class BaseModel(models.Model):
title = models.CharField(max_length=256)
slug = models.SlugField()
class Issue(BaseModel):
number = models.IntegerField(help_text="Do not include the '#'.")
class ComicBookSeries(BaseModel):
issues = models.ForeignKey(Issue)
You need to declare the classes in that order!
Hope that helps!