Pour gérer le manque de lignes imbriquées dans Django-admin, j'ai mis des cas spéciaux dans deux des modèles pour créer des liens entre les pages de changement d'administrateur et les administrateurs en ligne de deux modèles.
Ma question est: comment créer un lien depuis la page de changement d'administrateur ou l'administrateur en ligne d'un modèle vers la page de changement d'administrateur ou l'administrateur en ligne d'un modèle connexe proprement, sans hacks désagréables dans le modèle ?
Je voudrais une solution générale que je peux appliquer à la page de changement d'administrateur ou à l'administrateur en ligne de n'importe quel modèle.
J'ai un modèle, post
(pas son vrai nom) qui est à la fois en ligne sur la page d'administration blog
, et a également sa propre page d'administration. La raison pour laquelle il ne peut pas simplement être en ligne est qu'il a des modèles avec des clés étrangères qui n'ont de sens que lorsqu'ils sont modifiés avec lui, et cela n'a de sens que lorsqu'ils sont modifiés avec blog
.
Pour la page d'administration post
, j'ai changé une partie de "fieldset.html" de:
{% if field.is_readonly %}
<p>{{ field.contents }}</p>
{% else %}
{{ field.field }}
{% endif %}
à
{% if field.is_readonly %}
<p>{{ field.contents }}</p>
{% else %}
{% ifequal field.field.name "blog" %}
<p>{{ field.field.form.instance.blog_link|safe }}</p>
{% else %}
{{ field.field }}
{% endifequal %}
{% endif %}
pour créer un lien vers la page d'administration blog
, où blog_link
est une méthode sur le modèle:
def blog_link(self):
return '<a href="%s">%s</a>' % (reverse("admin:myblog_blog_change",
args=(self.blog.id,)), escape(self.blog))
Je n'ai pas pu trouver id
de l'instance blog
n'importe où en dehors de field.field.form.instance
.
Sur la page d'administration de blog
, où post
est en ligne, j'ai modifié une partie de "stacked.html" à partir de:
<h3><b>{{ inline_admin_formset.opts.verbose_name|title }}:</b>
<span class="inline_label">{% if inline_admin_form.original %}
{{ inline_admin_form.original }}
{% else %}#{{ forloop.counter }}{% endif %}</span>
à
<h3><b>{{ inline_admin_formset.opts.verbose_name|title }}:</b>
<span class="inline_label">{% if inline_admin_form.original %}
{% ifequal inline_admin_formset.opts.verbose_name "post" %}
<a href="/admin/myblog/post/{{ inline_admin_form.pk_field.field.value }}/">
{{ inline_admin_form.original }}</a>
{% else %}{{ inline_admin_form.original }}{% endifequal %}
{% else %}#{{ forloop.counter }}{% endif %}</span>
pour créer un lien vers la page d'administration post
car ici j'ai pu trouver le id
stocké dans le champ de la clé étrangère.
Je suis sûr qu'il existe une meilleure façon, plus générale, d'ajouter des liens aux formulaires d'administration sans me répéter; qu'est-ce que c'est?
Nouveau dans Django 1.8: show_change_link pour l'administrateur en ligne .
Définissez show_change_link sur True (False par défaut) dans votre modèle en ligne, afin que les objets en ligne aient un lien vers leur formulaire de modification (où ils peuvent avoir leurs propres lignes).
from Django.contrib import admin
class PostInline(admin.StackedInline):
model = Post
show_change_link = True
...
class BlogAdmin(admin.ModelAdmin):
inlines = [PostInline]
...
class ImageInline(admin.StackedInline):
# Assume Image model has foreign key to Post
model = Image
show_change_link = True
...
class PostAdmin(admin.ModelAdmin):
inlines = [ImageInline]
...
admin.site.register(Blog, BlogAdmin)
admin.site.register(Post, PostAdmin)
Utilisez readonly_fields :
class MyInline(admin.TabularInline):
model = MyModel
readonly_fields = ['link']
def link(self, obj):
url = reverse(...)
return mark_safe("<a href='%s'>edit</a>" % url)
# the following is necessary if 'link' method is also used in list_display
link.allow_tags = True
Je pense que la solution d'agf est assez impressionnante - beaucoup de félicitations pour lui. Mais j'avais besoin de quelques fonctionnalités supplémentaires:
Solution:
def add_link_field(target_model = None, field = '', app='', field_name='link',
link_text=unicode):
def add_link(cls):
reverse_name = target_model or cls.model.__name__.lower()
def link(self, instance):
app_name = app or instance._meta.app_label
reverse_path = "admin:%s_%s_change" % (app_name, reverse_name)
link_obj = getattr(instance, field, None) or instance
url = reverse(reverse_path, args = (link_obj.id,))
return mark_safe("<a href='%s'>%s</a>" % (url, link_text(link_obj)))
link.allow_tags = True
link.short_description = reverse_name + ' link'
setattr(cls, field_name, link)
cls.readonly_fields = list(getattr(cls, 'readonly_fields', [])) + \
[field_name]
return cls
return add_link
Usage:
# 'Apple' is name of model to link to
# 'fruit_food' is field name in `instance`, so instance.fruit_food = Apple()
# 'link2' will be name of this field
@add_link_field('Apple','fruit_food',field_name='link2')
# 'cheese' is name of model to link to
# 'milk_food' is field name in `instance`, so instance.milk_food = Cheese()
# 'milk' is the name of the app where Cheese lives
@add_link_field('cheese','milk_food', 'milk')
class FoodAdmin(admin.ModelAdmin):
list_display = ("id", "...", 'link', 'link2')
Je suis désolé que l'exemple soit si illogique, mais je ne voulais pas utiliser mes données.
C'est ma solution actuelle, basée sur ce qui a été suggéré par Pannu (dans son édition) et Mikhail.
J'ai quelques vues de changement d'administrateur de niveau supérieur que je dois lier à une vue de changement d'administrateur de niveau supérieur d'un objet associé, et quelques vues de changement d'administrateur en ligne que je dois lier à la vue de changement d'administrateur de niveau supérieur du même objet. Pour cette raison, je souhaite factoriser la méthode de lien plutôt que d'en répéter les variations pour chaque vue de changement d'administrateur.
J'utilise un décorateur de classe pour créer le link
appelable et l'ajouter à readonly_fields
.
def add_link_field(target_model = None, field = '', link_text = unicode):
def add_link(cls):
reverse_name = target_model or cls.model.__name__.lower()
def link(self, instance):
app_name = instance._meta.app_label
reverse_path = "admin:%s_%s_change" % (app_name, reverse_name)
link_obj = getattr(instance, field, None) or instance
url = reverse(reverse_path, args = (link_obj.id,))
return mark_safe("<a href='%s'>%s</a>" % (url, link_text(link_obj)))
link.allow_tags = True
link.short_description = reverse_name + ' link'
cls.link = link
cls.readonly_fields = list(getattr(cls, 'readonly_fields', [])) + ['link']
return cls
return add_link
Vous pouvez également passer un appelable personnalisé si vous avez besoin d'obtenir le texte de votre lien d'une manière autre que d'appeler simplement unicode
sur l'objet auquel vous créez un lien.
Je l'utilise comme ceci:
# the first 'blog' is the name of the model who's change page you want to link to
# the second is the name of the field on the model you're linking from
# so here, Post.blog is a foreign key to a Blog object.
@add_link_field('blog', 'blog')
class PostAdmin(admin.ModelAdmin):
inlines = [SubPostInline, DefinitionInline]
fieldsets = ((None, {'fields': (('link', 'enabled'),)}),)
list_display = ('__unicode__', 'enabled', 'link')
# can call without arguments when you want to link to the model change page
# for the model of an inline model admin.
@add_link_field()
class PostInline(admin.StackedInline):
model = Post
fieldsets = ((None, {'fields': (('link', 'enabled'),)}),)
extra = 0
Bien sûr, rien de tout cela ne serait nécessaire si je pouvais imbriquer les vues de changement d'administrateur pour SubPost
et Definition
dans l'administration en ligne de Post
sur Blog
admin changer de page sans patcher Django.
Je reconnais qu'il est difficile de modifier le modèle, donc je crée un widget personnalisé pour afficher un anchor
sur la page de vue de modification de l'administrateur (peut être utilisé sur les formulaires et les formulaires en ligne).
J'ai donc utilisé le widget d'ancrage, ainsi que le remplacement de formulaire pour obtenir le lien sur la page.
forms.py:
class AnchorWidget(forms.Widget):
def _format_value(self,value):
if self.is_localized:
return formats.localize_input(value)
return value
def render(self, name, value, attrs=None):
if not value:
value = u''
text = unicode("")
if self.attrs.has_key('text'):
text = self.attrs.pop('text')
final_attrs = self.build_attrs(attrs,name=name)
return mark_safe(u"<a %s>%s</a>" %(flatatt(final_attrs),unicode(text)))
class PostAdminForm(forms.ModelForm):
.......
def __init__(self,*args,**kwargs):
super(PostAdminForm, self).__init__(*args, **kwargs)
instance = kwargs.get('instance',None)
if instance.blog:
href = reverse("admin:appname_Blog_change",args=(instance.blog))
self.fields["link"] = forms.CharField(label="View Blog",required=False,widget=AnchorWidget(attrs={'text':'go to blog','href':href}))
class BlogAdminForm(forms.ModelForm):
.......
link = forms..CharField(label="View Post",required=False,widget=AnchorWidget(attrs={'text':'go to post'}))
def __init__(self,*args,**kwargs):
super(BlogAdminForm, self).__init__(*args, **kwargs)
instance = kwargs.get('instance',None)
href = ""
if instance:
posts = Post.objects.filter(blog=instance.pk)
for idx,post in enumerate(posts):
href = reverse("admin:appname_Post_change",args=(post["id"]))
self.fields["link_%s" % idx] = forms..CharField(label=Post["name"],required=False,widget=AnchorWidget(attrs={'text':post["desc"],'href':href}))
maintenant dans votre ModelAdmin
remplacez l'attribut form
et vous devriez obtenir le résultat souhaité. Je suppose que vous avez une relation OneToOne
entre ces tables. Si vous en avez une à plusieurs, le côté BlogAdmin
ne fonctionnera pas.
pdate: J'ai fait quelques changements pour ajouter dynamiquement des liens et cela résout également le problème OneToMany
avec le Blog
à Post
j'espère que cela résout le problème. :)
Après Pastebin: Dans votre PostAdmin
j'ai remarqué blog_link
, cela signifie que vous essayez d'afficher le lien blog
sur changelist_view
qui répertorie tous les messages. Si j'ai raison, vous devez ajouter une méthode pour afficher le lien sur la page.
class PostAdmin(admin.ModelAdmin):
model = Post
inlines = [SubPostInline, DefinitionInline]
list_display = ('__unicode__', 'enabled', 'blog_on_site')
def blog_on_site(self, obj):
href = reverse("admin:appname_Blog_change",args=(obj.blog))
return mark_safe(u"<a href='%s'>%s</a>" %(href,obj.desc))
blog_on_site.allow_tags = True
blog_on_site.short_description = 'Blog'
En ce qui concerne les liens post
affichés sur BlogAdmin
changelist_view
vous pouvez faire la même chose que ci-dessus. Ma solution précédente vous montrera le lien un niveau plus bas au change_view
page où vous pouvez modifier chaque instance.
Si vous souhaitez que la page BlogAdmin
affiche les liens vers le post
dans le change_view
alors vous devrez inclure chacun d'eux dans le fieldsets
dynamiquement en remplaçant le get_form
méthode pour class BlogAdmin
et en ajoutant dynamiquement le lien, dans get_form
met le self.fieldsets
, mais d'abord n'utilisez pas de tuples pour pour fieldsets
utilisez plutôt une liste.
Sur la base d'agfs et des suggestions de SummerBreeze, j'ai amélioré le décorateur pour mieux gérer unicode et pouvoir établir un lien vers des champs de clé étrangère en arrière (ManyRelatedManager avec un résultat). Vous pouvez également maintenant ajouter un short_description comme en-tête de liste:
from Django.core.urlresolvers import reverse
from Django.core.exceptions import MultipleObjectsReturned
from Django.utils.safestring import mark_safe
def add_link_field(target_model=None, field='', app='', field_name='link',
link_text=unicode, short_description=None):
"""
decorator that automatically links to a model instance in the admin;
inspired by http://stackoverflow.com/questions/9919780/how-do-i-add-a-link-from-the-Django-admin-page-of-one-object-
to-the-admin-page-o
:param target_model: modelname.lower or model
:param field: fieldname
:param app: appname
:param field_name: resulting field name
:param link_text: callback to link text function
:param short_description: list header
:return:
"""
def add_link(cls):
reverse_name = target_model or cls.model.__name__.lower()
def link(self, instance):
app_name = app or instance._meta.app_label
reverse_path = "admin:%s_%s_change" % (app_name, reverse_name)
link_obj = getattr(instance, field, None) or instance
# manyrelatedmanager with one result?
if link_obj.__class__.__name__ == "RelatedManager":
try:
link_obj = link_obj.get()
except MultipleObjectsReturned:
return u"multiple, can't link"
except link_obj.model.DoesNotExist:
return u""
url = reverse(reverse_path, args = (link_obj.id,))
return mark_safe(u"<a href='%s'>%s</a>" % (url, link_text(link_obj)))
link.allow_tags = True
link.short_description = short_description or (reverse_name + ' link')
setattr(cls, field_name, link)
cls.readonly_fields = list(getattr(cls, 'readonly_fields', [])) + \
[field_name]
return cls
return add_link
Modifier: mis à jour en raison de la disparition du lien.
La lecture de la source des classes d'administration est éclairante: elle montre qu'un objet en contexte est disponible pour une vue d'administration appelée "original".
Voici une situation similaire, où j'avais besoin d'informations supplémentaires ajoutées à une liste de modifications: Ajout de données aux modèles d'administration (sur mon blog).