2

So I'm having a bit of trouble with trying to create a model that will define dynamic proxy models that manage a related model in the admin site. I know that sentence was confusing, so I'll just share my code instead.

models.py

class Cateogry(models.Model):
    name = models.CharField(...)

class Tag(models.Model):
    name = models.CharField(...)
    category = models.ForeignKey(Cateogry)

What I want to achieve is that in the admin site, instead of having one ModelAdmin for the Tag model, for each category I will have a modeladmin for all related tags. I have achieved this using this answer. Say I have a category named A:

def create_modeladmin(modeladmin, model, name = None):
    class  Meta:
        proxy = True
        app_label = model._meta.app_label

    attrs = {'__module__': '', 'Meta': Meta}

    newmodel = type(name, (model,), attrs)

    admin.site.register(newmodel, modeladmin)
    return modeladmin

class CatA(TagAdmin):
    def queryset(self, request):
        qs = super(CatA, self).queryset(request)
        return qs.filter(cateogry = Cateogry.objects.filter(name='A'))

create_modeladmin(CatA, name='CategoryAtags', model=Tag)

But this is not good enough, because obviously I still need to manually subclass the TagAdmin model and then run create_modeladmin. What I need to do, is loop over all Category objects, for each one create a dynamic subclass for Tagadmin (named after the category), then create a dynamic proxy model from that, and this is where my head starts spinning.

for cat in Category.objects.all():
    NewSubClass = #somehow create subclass of TagAdmin, the name should be '<cat.name>Admin' instead of NewSubClass 
    create_modeladmin(NewSubClass, name=cat.name, model=Tag)

Any guidance or help would be much appreciated

Community
  • 1
  • 1
yuvi
  • 18,155
  • 8
  • 56
  • 93

2 Answers2

1

Dynamic ModelAdmins don't work well together with the way admin registeres models. I suggest to create subviews in the CategoryAdmin.

from django.conf.urls import patterns, url
from django.contrib import admin
from django.contrib.admin.options import csrf_protect_m
from django.contrib.admin.util import unquote
from django.core.urlresolvers import reverse

from demo_project.demo.models import Category, Tag

class TagAdmin(admin.ModelAdmin):
    # as long as the CategoryTagAdmin class has no custom change_list template
    # there needs to be a default admin for Tags
    pass
admin.site.register(Tag, TagAdmin)

class CategoryTagAdmin(admin.ModelAdmin):
    """ A ModelAdmin invoked by a CategoryAdmin"""

    read_only_fields = ('category',)

    def __init__(self, model, admin_site, category_admin, category_id):
        self.model = model
        self.admin_site = admin_site
        self.category_admin = category_admin
        self.category_id = category_id
        super(CategoryTagAdmin, self).__init__(model, admin_site)

    def queryset(self, request):
        return super(CategoryTagAdmin, self).queryset(request).filter(category=self.category_id)


class CategoryAdmin(admin.ModelAdmin):

    list_display = ('name', 'tag_changelist_link')

    def tag_changelist_link(self, obj):
        info = self.model._meta.app_label, self.model._meta.module_name
        return '<a href="%s" >Tags</a>' % reverse('admin:%s_%s_taglist' % info, args=(obj.id,))
    tag_changelist_link.allow_tags = True
    tag_changelist_link.short_description = 'Tags'

    @csrf_protect_m
    def tag_changelist(self, request, *args, **kwargs):
        obj_id = unquote(args[0])
        info = self.model._meta.app_label, self.model._meta.module_name

        category = self.get_object(request, obj_id)

        tag_admin = CategoryTagAdmin(Tag, self.admin_site, self, category_id=obj_id )

        extra_context = {
            'parent': {
                'has_change_permission': self.has_change_permission(request, obj_id),
                'opts': self.model._meta,
                'object': category,
            },
        }
        return tag_admin.changelist_view(request, extra_context)


    def get_urls(self):
        info = self.model._meta.app_label, self.model._meta.module_name
        urls= patterns('', 
            url(r'^(.+)/tags/$', self.admin_site.admin_view(self.tag_changelist), name='%s_%s_taglist' % info )
        )
        return urls + super(CategoryAdmin, self).get_urls()


admin.site.register(Category, CategoryAdmin)

The items in the categories changelist have an extra column with a link made by the tag_changelist_link pointing to the CategoryAdmin.tag_changelist. This method creates a CategoryTagAdmin instance with some extras and returns its changelist_view.

This way you have a filtered tag changelist on every category. To fix the breadcrumbs of the tag_changelist view you need to set the CategoryTagAdmin.change_list_template to a own template that {% extends 'admin/change_list.html' %} and overwrites the {% block breadcrumbs %}. That is where you will need the parent variable from the extra_context to create the correct urls.

If you plan to implement a tag_changeview and tag_addview method you need to make sure that the links rendered in variouse admin templates point to the right url (e.g. calling the change_view with a form_url as paramter). A save_model method on the CategoryTagAdmin can set the default category when adding new tags.

def save_model(self, request, obj, form, change):
    obj.category_id = self.category_id
    super(CategoryTagAdmin, self).__init__(request, obj, form, change)

If you still want to stick to the apache restart aproach ... Yes you can restart Django. It depends on how you are deploying the instance. On an apache you can touch the wsgi file that will reload the instance os.utime(path/to/wsgi.py. When using uwsgi you can use uwsgi.reload().

You can check the source code of Rosetta how they are restarting the instance after the save translations (views.py).

kanu
  • 726
  • 5
  • 9
  • This is some goddamn fantastic code right there. Even though it's not perfect yet, it's definitely shown me the right direction to take, and you also pointed me as to the option of how to restart the server. Thank you very much, you got the bounty, you very much deserve it! – yuvi Dec 07 '13 at 20:19
0

So I found a half-solution.

def create_subclass(baseclass, name):
    class Meta:
        app_label = 'fun'

    attrs = {'__module__': '', 'Meta': Meta, 'cat': name }
    newsub = type(name, (baseclass,), attrs)
    return newsub

class TagAdmin(admin.ModelAdmin):
    list_display = ('name', 'category')
    def get_queryset(self, request):
        return Tag.objects.filter(category = Category.objects.filter(name=self.cat))

for cat in Category.objects.all():
    newsub = create_subclass(TagAdmin, str(cat.name))
    create_modeladmin(newsub, model=Tag, name=str(cat.name))

It's working. But every time you add a new category, you need to refresh the server before it shows up (because admin.py is evaluated at runtime). Does anyone know a decent solution to this?

yuvi
  • 18,155
  • 8
  • 56
  • 93