web-dev-qa-db-fra.com

Comment ajouter un lien depuis la page d'administration Django d'un objet vers la page d'administration d'un objet connexe?

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>&nbsp;
<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>&nbsp;
<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?

28
agf

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)
14
bitnik

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
14
Mikhail Korobov

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:

  • pouvoir avoir plusieurs liens pour un administrateur
  • être capable de se lier au modèle dans une autre application

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.

10
SummerBreeze

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.

10
agf

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 BlogAdminchangelist_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.

3
Pannu

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.

2
panni

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).

0