Django Administration - Customize list appearance and selection of ManyToManyField keys - python

I have a database with 1000+ songs. I have a custom model "Schedule" that accepts songs as field.
models.py
from django.db import models
class Song(models.Model):
title = models.CharField(max_length=255)
words = models.TextField()
slug = models.SlugField()
date = models.DateTimeField(auto_now_add=True)
snippet = models.CharField(max_length=50)
def __str__(self):
return self.title
class Schedule(models.Model):
songs = models.ManyToManyField(Song)
date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return str(self.date)
admin.py
from django.contrib import admin
from .models import Song, Schedule
#admin.register(Song)
class SongModel(admin.ModelAdmin):
search_fields = ('title',)
list_display = ('title',)
list_per_page = 100
#admin.register(Schedule)
class ScheduleModel(admin.ModelAdmin):
search_fields = ('date',)
list_display = ('date',)
list_per_page = 100
I want to be able to add any song I want to a schedule, but it is difficult do to so through the default list in the Django-Administration, which looks like this. I have to scroll and CTRL+select each one of them, then add them.
I'd like something more more practical where I can select, search, etc.
What are my options? I don't know where to start looking.

Option 1
It's only comfortable if you have very few related items (few songs in schedule). But it is super easy and will be better than what you have now. (django.contrib.admin comes with built-in select2.)
#admin.register(Schedule)
class ScheduleAdmin(admin.ModelAdmin):
...
autocomplete_fields = ("songs",)
Option 2
(upd: damn, forgot it at first, it's also super simple and quite efficient)
It looks alright-ish. Usable. Not particularly comfortable. But better than ctrl-clicking the stuff.
#admin.register(Schedule)
class ScheduleAdmin(admin.ModelAdmin):
...
filter_horizontal = ('songs',)
Option 3
If you want a comfortable UI without implementing custom pages or actions (which are unfortunately a complete mess), you should use a StackedInline admin.
It's quite a bit more difficult though.
First you will need a through model. (I don't think inlines are possible with auto-generated many-to-many models.) It's basucally your many-to-many between the two models. Something like that:
class ScheduleSongNM(models.Model):
song = models.ForeignKey("Song", null=False)
schedule = models.ForeignKey("Schedule", null=False)
Tell your Schedule model to use your custom through model:
class Schedule(models.Model):
songs = models.ManyToManyField(Song, through="ScheduleSongNM")
Now create an inline admin for the ScheduleSongNM:
class ScheduleSongInline(admin.StackedInline):
model = ScheduleSongNM
fields = ["song"]
autocomplete_fields = ["song"] # select2 works here too
Finally, tell your Schedule admin that it has an inline now:
#admin.register(Schedule)
class ScheduleAdmin(admin.ModelAdmin):
...
inlines = [ScheduleSongInline]
...
Maybe I missed something (obviously I haven't tested it), but I think you got the general idea. In the end you get a box inside of your Schedule admin that looks something like that (plus auto completion for song names):

Related

How can I concatenate two different model query and order by a field that both models has

How can I concatenate two different model query and order by a field that both models has like progress fields.
For example
models.py
class Gig(models.Model):
author= models.ForeignKey(User)
title = models.CharFields()
progress = models.IntegerField()
class Project(models.Model):
author= models.ForeignKey(User)
title = models.CharFields()
progress = models.IntegerField()
Can I do my view.py like this, for me to achieve it?
İf No, How can I achieve it?
views.py
def fetch_all_item(request):
gig = Gig.objects.filter(author_id = request.user.id)
project = Project.objects.filter(author_id = request.user.id)
total_item = (gig + project).order_by("progress")
return render(request, "all_product.html", {"item": total_item})
I am trying to join two query set from Gig and Project models then send it to frontend in an ordering form by a field name called progress.
You can let Python do the sorting for you, like:
from operator import attrgetter
def fetch_all_item(request):
gig = Gig.objects.filter(author=request.user)
project = Project.objects.filter(author=request.user)
total_item = sorted([*gig, *project], attrgetter('progress'))
return render(request, "all_product.html", {'item': total_item})
It might however be better to remodel this to a single model with a type field that disambiguates between a Gig and a Project.
Note: It is normally better to make use of the settings.AUTH_USER_MODEL [Django-doc] to refer to the user model, than to use the User model [Django-doc] directly. For more information you can see the referencing the User model section of the documentation.
In general, such design when there are common fields is better accomplished by inheriting from some base class. E.g.:
class CommonActivity(models.Model):
# common fields for Project/Gig
author = models.ForeignKey(User)
title = models.CharFields()
progress = models.IntegerField()
class Gig(CommonActivity):
pass # or other fields related to Gig only
class Project(CommonActivity):
pass
Then if you want to query both - you query CommonActivity.
If remodel not possible - then filter via Python as #WillemVanOnsem suggested.
However such filtering will be way less efficient

How to make a TabularInline orderable by user?

I created an admin view using the TabularInline class. With this view it is not possible to let the user order/sort the table to their liking, like within a standard ModelAdmin view. I would like to achieve this. I am using Django 2.2.1.
What I tried
I searched the Django docs, but besides hard coding 'ordering' within the admin.py file, I couldn't find anything on this topic. This is not what I want, I want the admin user to choose how to order.
I tried to use adminsortable2, however I ran into some issues I couldn't resolve. This got me thinking: is it really not possible with the standard Django package.
My code
My model consists of TimeFrames, which are made up by TimeSlots.
This is what my model.py looks like:
from django.db import models
class TimeFrame(models.Model):
name = models.CharField(max_length=30)
def __str__(self):
return self.name
class TimeSlot(models.Model):
DAY_CHOICES = ('work', 'Work'), ('weekend', 'Weekend')
begin_time = models.TimeField()
end_time = models.TimeField()
reference = models.CharField(max_length=30)
day = models.CharField(max_length=30, choices=DAY_CHOICES)
timeframe = models.ForeignKey('TimeFrame', on_delete=models.CASCADE)
I don't want to register a separate TimeSlot admin view, since the TimeSlots are always part of a TimeFrame. They don't exist on their own.
from django.contrib import admin
from .models import *
class TimeSlotInline(admin.TabularInline):
model = TimeSlot
ordering = ("day", "begin_time") #hard-coded ordering
#admin.register(TimeFrame)
class TimeFrameAdmin(admin.ModelAdmin):
inlines = [TimeSlotInline]
Expected outcome
Enable the admin user to sort like in a standard ModelAdmin view.

How to make a django model "commentable", "likeable" and "rateable"

I am using Django 2.0.8 and Python 3.5 for a project. I have different models in my project, some of which, I want to allow commenting on - with both the object (e.g. a blogpost) and comments to the blogpost being likeable.
I am using the threaded comments django app to provide commenting functionality.
Assuming I have a model Foo (see below):
from django.db import models
from django.conf import settings
class Foo(models.Model):
owner = models.ForeignKey(settings.AUTH_USER_MODEL, blank=False, null=False, default=1, on_delete = models.PROTECT)
# ...
class Likeable():
pass
class Rateable():
pass
How could I use mixins (or any other mechanism for that matter), to make the object Foo "commentable" (i.e. an object which can be commented upon), "likeable" (i.e. an object which can be commented upon) and "rateable" (i.e. an object which can be rated?)- bearing in mind that comments on an objects may be BOTH liked and rated.
According to django documentation , you can achieve this using the Content types Framework. ContentType is a generic model that permits you to track all the models included in INSTALLED_APPS using for that their app_label, model_name and pk. The way it works is easy:
Your generic Comment model
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.conf import settings
class Comment(models.Model):
# Generic relation fields
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
# Model specific fields
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE
)
comment = models.TextField()
created = models.DatetimeField(auto_now_add=True)
# ...
Your reusable generic relation model. The best way is using abstract model classes or mixins. For example, using abstract models:
from django.db import models
from django.contrib.contenttypes.fields import GenericRelation
class Commentable(models.Model):
comments = GenericRelation(Comment)
class Meta:
abstract = True
Your Commentable model:
from django.db import models
class Foo(Commentable, Likeable, ...):
# your stuff
How to use it:
# Add a new comment to Foo
foo = new Foo()
foo.save()
foo.comments.create(author=author, comment="Your comment")
# Retrieve all comments from an specific user no matter the base model
comments = Comment.objects.filter(author=author)
EDIT As #ozren1983 said, each approach has its own downsides, but this is the standard way to do it.
The main advantages are:
You can retrieve all the comments (for example) made in all your commentable models in just one query. Using the approach of having a comment, like, etc table per model, you would need to concatenate a query per model. This could be problematic and a bit challenging if you have a lot of models or if you want to merge the results and order them, for example.
Just one table per functionality (comments, likes) implies just one database migration in case of change. This could be key if your database is huge.
The main disadvantage is the lack of integrity checks of this generic relationship in database. But if you plan to use the django ORM strictly, nothing should be broken.
BONUS: Another approach that many projects use is inheriting the models (one to one relationship) from an specific one called Item or Thread. Then, you can add all the comments, likes, etc functionalities to this model. This is called multi-table inheritance. An example:
from django.db import models
class Thread(models.Model):
pass
class Comment(models.Model):
# Relation with thread
thread = models.ForeignKey(
Thread,
on_delete=models.CASCADE,
related_name="comments"
)
# Model specific fields
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE
)
comment = models.TextField()
created = models.DatetimeField(auto_now_add=True)
# ...
class Foo(Thread):
pass
Unlike using the generic relationships, the main advantage of this method is that, this way, you have database integrity checks.
The main disadvantage is that your database structure could become complex.
Based on my experience and recommendations in Two scoops of Django, I would advise against using GenericForeignKey and GenericRelation. Two big downsides of that approach are:
slow queries
danger of data corruption
Instead, I would use following approach. Let's say you have 3 models:
class User(models.Model):
username = models.CharField(max_length=255)
class Author(models.Model):
name = models.CharField(max_length=255)
class Post(models.Model):
title = models.CharField(max_length=255)
author = models.ForeignKey(Author)
Add abstract Like model, and use it as base class for other models that will implement liking functionality.
class Like(models.Model):
user = models.ForeignKey(User)
date_created = models.DateTimeField(auto_now_add=True)
class Meta:
abstract = True
class AuthorLike(Like):
author = models.ForeignKey(Author)
class PostLike(Like):
post = models.ForeignKey(Post)
Similarly, add abstract Rating model and use it as a base class:
class Rating(models.Model):
user = models.ForeignKey(User)
rate = models.PositiveSmallIntegerField()
date_created = models.DateTimeField(auto_now_add=True)
class Meta:
abstract = True
class AuthorRating(Rating):
author = models.ForeignKey(Author)
class PostRating(Rating):
post = models.ForeignKey(Post)
You can use same approach to enable liking and rating to the Comments model you are using:
from threadedcomments.models import ThreadedComment
class ThreadedCommentRating(Rating):
threadedcomment = models.ForeignKey(ThreadedComment)
class ThreadedCommentLike(Like):
threadedcomment = models.ForeignKey(ThreadedComment)
The django-contrib-comments app, according to documentation, makes use of GenericForeignKey, meaning its own model can create a relation to any other model in your project.
A simple solution would be to just copy that existing functionality, creating your own Like/Rate application based on the same concept (i.e. storing the Like/Rate models in that application's models).
I think you would get very far starting out by forking the https://github.com/django/django-contrib-comments codebase.
(I assume you have searched and failed to find an already existing application that already does this).

How to make models connected via ManyToMany relationship editable in two places at the same time in Django admin?

I have two models - News and Subject. News model has a ManyToManyField related to Subject, like that:
subjects = models.ManyToManyField(NewsSubject, verbose_name=u'Subjects', blank=True,null=True,related_name='news')
And I need to have an ability not only to choose Subjects for News in admin, but also to choose News while editing Subject.
I'd started from creating model form:
class NewsSubjectForm(forms.ModelForm):
news = ModelMultipleChoiceField(queryset=News.objects.all(),
label="News",
required=False,
#initial=News.objects.all(),
)
class Meta:
model = NewsSubject
Here is the question - how I should specify initial values for news according to reverse many-to-many relationship? And how to add a plus button for simple adding?
I am not sure that it can meet your needs but i've done something similar by using TabularInline. Somethink like this should work (i didn't test the following code).
class Subject(models.Model):
...
class News(models.Model):
subject = models.ManyToManyField(Subject, through="NewsSubject")
class NewsSubject(models.Model):
news = models.ForeignKey(News)
subject = models.ForeignKey(Subject)
class NewsOfSubjectInline(admin.TabularInline):
model = NewsSubject
raw_id_fields = ('news',)
class SubjectAdmin(admin.ModelAdmin):
inlines = [NewsOfSubjectInline,]
class SubjectOfNewsInline(admin.TabularInline):
model = NewsSubject
raw_id_fields = ('subject',)
class NewsAdmin(admin.ModelAdmin):
inlines = [SubjectOfNewsInline,]

admin template for manytomany

I have a manytomany relationship between publication and pathology. Each publication can have many pathologies. When a publication appears in the admin template, I need to be able to see the many pathologies associated with that publication. Here is the model statement:
class Pathology(models.Model):
pathology = models.CharField(max_length=100)
def __unicode__(self):
return self.pathology
class Meta:
ordering = ["pathology"]
class Publication(models.Model):
pubtitle = models.TextField()
pathology = models.ManyToManyField(Pathology)
def __unicode__(self):
return self.pubtitle
class Meta:
ordering = ["pubtitle"]
Here is the admin.py. I have tried variations of the following, but always
get an error saying either publication or pathology doesn't have a foreign key
associated.
from myprograms.cpssite.models import Pathology
class PathologyAdmin(admin.ModelAdmin):
# ...
list_display = ('pathology', 'id')
admin.site.register(Pathology, PathologyAdmin)
class PathologyInline(admin.TabularInline):
#...
model = Pathology
extra = 3
class PublicationAdmin(admin.ModelAdmin):
# ...
ordering = ('pubtitle', 'year')
inlines = [PathologyInline]
admin.site.register(Publication,PublicationAdmin)
Thanks for any help.
Unless you are using a intermediate table as documented here http://docs.djangoproject.com/en/dev/ref/contrib/admin/#working-with-many-to-many-intermediary-models, I don't think you need to create an Inline class. Try removing the line includes=[PathologyInline] and see what happens.
I realize now that Django is great for the administration (data entry) of a website, simple searching and template inheritance, but Django and Python are not very good for complex web applications, where data is moved back and forth between a database and an html template. I have decided to combine Django and PHP, hopefully, applying the strengths of both. Thanks for you help!
That looks more like a one-to-many relationship to me, tho I'm somewhat unclear on what exactly Pathologies are. Also, so far as I understand, Inlines don't work on manytomany. That should work if you flip the order of the models, remove the manytomany and add a ForeignKey field to Publication in Pathology.
class Publication(models.Model):
pubtitle = models.TextField()
def __unicode__(self):
return self.pubtitle
class Meta:
ordering = ["pubtitle"]
class Pathology(models.Model):
pathology = models.CharField(max_length=100)
publication = models.ForeignKey(Publication)
def __unicode__(self):
return self.pathology
class Meta:
ordering = ["pathology"]

Categories