GenericForeignKey and Admin in Django - django

Let's say I have a Post object that can contain Images, Videos, and other media types. I can use a GenericForeignKey to link them together. Something like:
class Post(models.Model):
title = models.CharField(...)
text = models.TextField(...)
class AudioMedia(models.Model):
...
class VideoMedia(models.Model):
...
class ImageMedia(models.Model):
...
class MediaObject(models.Model):
post = models.ForeignKey(Post)
order = models.IntegerField()
content_type_media = models.ForeignKey(
ContentType, limit_choices_to={
'model__in': (
'audiomedia',
'imagemedia',
'videomedia')
})
object_id_media = models.PositiveIntegerField()
obj = generic.GenericForeignKey('content_type_media', 'object_id_media')
Now I can easily create an admin interface, like:
class MediaObjectAdminInLine(admin.StackedInline):
model = MediaObject
ct_field = "content_type_media"
ct_fk_field = "object_id_media"
extra = 0
class PostAdmin(admin.ModelAdmin):
inlines = [MediaObjectAdminInLine]
Now the question :) In admin/, I can easily create a new Post. To the post, I can easily add more MediaObject. In the panel, I have a drop down menu to chose the type (audio, video, ...), but I have to manually enter the ID of the object I want to link with Post.
I have tried various extensions, including grappelli. Some provide the ability to lookup the ID of objects to link here. I want the ability to add objects here, eg, add an AudioMedia, a VideoMedia, an ImageMedia, depending on what I pick from the dropdown.
Any suggestions?

You'd need to quite a bit of work to get this going.
You're asking that the admin dynamically display a modelform, based on what model type you chose from a drop down.
Django's admin does not do that (nor do any known extensions to it).
To make this work, you'll have to:
Write a custom JavaScript event handler which captures the onchange of the model select drop down.
Then calls Django's admin and requests the inline modelform for that model.
Updates the current HTML page with that model form.
Then you'll need to intercept the parent model's modelform's save() method to figure out which child modelform it's dealing with, and correctly save it to the database.
Then you'll need to sort out how to get the parent model's modelform to correctly display the appropriate child model's modelform dependent on the model of the child.
Sound daunting? It is.
Here's an easier way:
Just have a single "Media" model. You'll have a few fields on the model that are only valid for one of your types (though there's plenty of crossover).
Name any fields that are specific to a single Media type with a prefix for that mediatype, i.e. image_size', orvideo_title`.
Attach a JavaScript handler to your ModelAdmin which selectively shows and hides fields based on a dropdown for the media type. Something like this:
class MediaAdmin(admin.ModelAdmin):
class Meta:
js = ["js/media-types.js",]
// media-type.js
(function($) {
$(document).ready(function(){
$('.module[id^=module] .row').hide();
$('.module[id^=module] .row.module').show();
$('.module[id^=module] .row.module select').each(function(){
if ($(this).val() != '')
{
var group = $(this).parent().parent().parent().parent();
var field = $(this).parent().parent().parent();
var mtype = $(this).val().toLowerCase();
if (mtype != '')
{
$('.row', group).not(field).slideUp('fast');
$('.row[class*="'+mtype+'"]', group).slideDown('fast');
$('.row[class*="all"]', group).slideDown('fast');
}
else
{
$('.row', group).not(field).slideUp('fast');
}
}
});
$('.module[id^=module] .row.module select').change(function(){
var group = $(this).parent().parent().parent().parent();
var field = $(this).parent().parent().parent();
var mtype = $(this).val().toLowerCase();
if (mtype != '')
{
$('.row', group).not(field).slideUp('fast');
$('.row[class*="'+mtype+'"]', group).slideDown('fast');
$('.row[class*="all"]', group).slideDown('fast');
}
else
{
$('.row', group).not(field).slideUp('fast');
}
});
});
})(django.jQuery);

django-admin-genericfk doesn't work with Django 1.9.
Other than that I only found the following module:
https://github.com/lexich/genericrelationview
which looks well maintained. Unfortunately, its JS code does not work well with how Django CMS sets up jQuery (noConflict jQuery), so it seems that it is not an option for me. But it should be fine if not used in Django CMS pages but the regular Django Admin.

I realize this is pretty old, but this is still the first result when searching for this.
django-admin-genericfk does exactly what you need.

Related

Displaying fields not intended to be edited in ModelAdmin

I have a custom contact form for which I create a sent_time field using auto_now_add to save the time when the user had sent the message.
I am able to list all the information on the listing view of the admin panel however when I try to enter a specific message I hit the following error:
'sent_time' cannot be specified for GeneralContact model form as it is a non-editable field
My attempt to make the fields readonly in the ModelAdmin results in the same error
class GeneralContactAdmin(ModelAdmin):
"""
Admin model for general correspondence via
the main contact form on the information page
"""
model = GeneralContact
list_display = GeneralContact.__all__
search_fields = GeneralContact.__all__
readonly_fields = GeneralContact.__all__
ordering = ('-sent_time',)
list_filter = ('sent_time', 'has_response')
Surely it is possible to be displayed only, perhaps I've done something incorrectly in my models?
Here is the base model I use for the contact model
class ContactFormBase(models.Model):
__all__ = (
'sent_time', 'sender_name', 'sender_email',
'sender_message', 'has_response', 'responded_on'
)
sent_time = models.DateTimeField(auto_now_add=True)
sender_name = models.CharField()
sender_email = models.EmailField()
sender_message = models.TextField()
has_response = models.BooleanField(
default=False,
help_text='Select whether this message has been replied to by an admin.',
)
responded_on = models.DateTimeField(blank=True, null=True)
panels = [
FieldRowPanel([
FieldPanel('sender_name'),
FieldPanel('sender_email'),
]),
FieldPanel('sent_time'),
FieldPanel('sender_message'),
FieldRowPanel([
FieldPanel('has_response'),
FieldPanel('responded_on'),
])
]
class Meta:
abstract = True
ordering = ['-sent_time',]
The actual class being used is rather plain, perhaps something needs to be done here to allow display of readonly fields?
class GeneralContact(ContactFormBase, models.Model):
panels = ContactFormBase.panels
class Meta:
verbose_name = 'General Contact Entry'
verbose_name_plural = 'General Contact Entries'
In the list view all the information is able to be displayed. In the editing view, ideally there would be all of the information about the message and sender as readonly fields and an option for the admin to change the has_response value based on whether someone has responded or not.
In what way could I achieve this?
update
After seeing this Q&A I have changed the auto_now_add to use django.utils.timezone.now as the default on the sent_time attribute and life seems better, the error from the start of the question is gone and the edit view loads up entirely. However, now all the fields are editable which is not desirable.
Looking into the ModelAdmin class provided by Wagtail it appears that readonly_fields isn't available and perhaps only a feature of the django admin class of the same name. So I'm not sure what to do here. Wagtails HelpPanel type of output is what I'm looking for, and I had an idea to use that to display the data but I'm not sure what that looks like or even how it'd be done as I'm just learning django and wagtail.
update 2
Attempted to use HelpPanel instead of FieldPanel in order to try display the values but seems as if the HelpPanel doesn't retrieve the value of the attributes. Checking through these docs I see the mention of things like djangos readonly_field is not included which confirms why one of my former attempts didn't work but I did find mention of inspect_view_enabled which displays the values in a read only fashion and after trying it out it looks very much how I was trying to get it, alas, nothing there is editable which makes sense but I am getting closer.
I am wondering if a good solution would be to override the view or template used for GeneralContactAdmin but unsure if that's the right way to go about it just to output some text for one class.
A simpler solution is to keep the inspect view and only add the has_response to the edit view, but two views, one of which would only be a checkbox is not a nice for UX.
Surely there is a better way to solve this?

Django Admin intercept onchange event of a field and make an action

In my django project i would clear a field value every time another select field have an onChange event.
I have an add form like thisone:
every time Template field change (onChange), Test Case field have to become blank.
How can i do this in a django admin add or edit page?
So many thanks in advance
You could customize Admin asset definition and use JavaScript/jQuery to handle your problem. Here is an example:
admin.py
class TestCaseAdmin(admin.ModelAdmin):
class Media:
js = (
'js/admin.js', # inside app static folder
)
admin.site.register(TestCase, TestCaseAdmin)
js/admin.js
if (!$) {
// Need this line because Django also provided jQuery and namespaced as django.jQuery
$ = django.jQuery;
}
$(document).ready(function() {
$("select[name='template']").change(function() {
$("select['test_case']").val('');
});
});
template, test_case are field name on your model
The event can also be set in the modelform meta widgets dict;
class ReceiptsForm(ModelForm):
class Meta:
model = Receipts
fields = []
widgets = {
'partner_id': forms.Select(attrs={'onchange': 'this.form.submit();'})
}

Django Admin Page: Help Text for Model Methods?

I have a model method in Django that I am displaying on an admin page just like I would a model field. With a field, I can just add a help_text argument to it to give a description of what the field is and what the user should put into it. However, with a model method, help_text does not work. Adding the attribute short_description changes the way the method name is displayed, which is sort of okay, but I'm looking for a way to add a few sentences of description beneath the method value that is displayed. Is there any way to do this natively, or would I have to resort to overriding admin templates or something? (Which I do not think is worth it for something this minor).
You can do this using JS.
Replace ID-OF-THE-FIELD with the actual id of the desired field.
(function($) {
var myField = $('#ID-OF-THE-FIELD');
// find the id of the desired field by doing
// Right-Click > Inspect element
var help = $('<p class="help">A very long help text</p>');
help.insertAfter(myField);
})(django.jQuery);
Put this code into a JS file and supply this file using class Media of your ModelAdmin class.

Dynamic show and hide fields in Django admin panel

I have defined model in which one of the filed has definition:
REPEAT = (
('day', 'Daily'),
('week', 'Weekly'),
)
repeats = models.CharField('Repeat', default='day', max_length=5, choices=REPEAT)
Also I have defined related admin model, which is responsible to show my main model in panel.
Is possible to show and hide some fields in admin panel based on choice in repeats field? For example in scenery when user choose 'Daily', then some fields are not required and I want to hide them.
I will be thankful for any advices or hints.
Yes, you can add custom JS to your admin model:
class MyModelAdmin(admin.ModelAdmin):
class Media:
js = ("my_code.js",)
STATIC_URL is appended to your filename automagically.
And your JS function, assuming jQuery, something like:
$(function(){
$('<my-selector>').change(function(){
//do something on select change
});
});

Overriding the admin Media class

Given an admin media class that sets up a rich text editor, like:
class TutorialAdmin(admin.ModelAdmin):
fields...
class Media:
js = ['/paths/to/tinymce.js',]
I would like the ability to selectively override js depending on a field value in the model it references. I've added a "use_editor" boolean to the Tutorial model. The question is, how can I detect whether the current instance has that bool set? I'd like to end up with something like:
class Media:
if self.use_editor:
js = ['/path/to/tinymce.js',]
else:
js = ''
Ideas? Thanks.
Many thanks to Sam Lai on django-users, I finally have a working solution for this. Turns out to be trickier than expected because you can't directly access field values on the instance from within the Admin class - you need to do it by redefining the form used by the Admin class. In addition, you'll need to use _media rather than "class Media:" to set the media property.
The goal is to detect the current instance value of the use_visual_editor field and turn javascript paths on or off depending on its value (so authors can turn off the visual editor on a per-record basis). Here's the final working solution:
models.py
class Tutorial(models.Model):
use_visual_editor = models.BooleanField()
forms.py
from django import forms
from tutorials.models import Tutorial
class TutorialAdminForm(forms.ModelForm):
class Meta:
model = Tutorial
def _media(self):
if self.instance.use_visual_editor == True:
js = ['/paths/to/javascript',]
else:
js = ['']
return forms.Media(js=js)
media = property(_media)
admin.py
from django import forms
....
class TutorialAdmin(admin.ModelAdmin):
form = TutorialAdminForm
Works perfectly!
An alternative approach, given you're using TinyMCE, is to use an additional JS file that adds a 'mceNoEditor' class to textareas you don't want to convert to rich text.
eg
class fooAdmin(admin.Modeladmin)
class Media:
js = ['/path/to/admin-styling.js',
'/paths/to/tinymce.js',]
In your tinymce.js init, you need to ensure there's a class defined for disabling the editor, such as:
editor_deselector : "mceNoEditor",
and in the admin-styling.js file have some kind of jQuery call in the document ready handler that finds certain elements and adds that class before TinyMCE is invoked.
Usually you can do this with the 'id_foo' identifier. eg, if you have a model field called additional_notes:
$('textarea#id_additional_notes').addClass('mceNoEditor');
It's possible to use more sophisticated jQuery selectors too, of course.
HTH
Steve