Auto generating django admin

Has anyone got a method to auto generate django admin, complete with inlines etc.?

I’ve taken a look at admin_generator in django-extensions, and it doesnt quite have what I need. I’ve also tried experimenting with code like the below. But curious if others have more sophisticated approaches to this.

class ListModelAdmin(admin.ModelAdmin):
    def __init__(self, model, admin_site):
        self.list_display = [field.name for field in model._meta.fields]
        super().__init__(model, admin_site)


models = apps.get_models()
for model in models:
    try:
        admin.site.register(model, ListModelAdmin)
    except admin.sites.AlreadyRegistered:
        pass

I would say that this is not generally a good idea.
I often find myself defining different displays, filters, permissions for each admin.
In this case you’re only dumping your database CRUD into a frontend, and missing some of the cool features of admin, like: custom actions, specialized filters, omitting some models that aren’t relevant for the staff team.

I’m not sure I’m following what you mean by “complete with inlines”. Are you saying you want this automatically-generated admin class to contain inlines for every model that has a ForeignKey to that model? That may be tough to do due to the sequence of events in the initialization process. (That’s conjecture on my part, not a statement of direct knowledge.)

If you’re just looking for a quick way to register all your models in an app with the admin, I use the following:
admin.site.register(apps.all_models['my_app'].values())
I have this in each admin.py file for each app where I just want a simple admin interface.

Note: If you want to see some ugly code in this topic, see Abusing the Django Admin app - DEV Community

3 Likes

Thanks, that’s helpful. I ended up playing around with the below last night and got what I need.

Only using this for the models where I don’t want to run custom functions or show data in a specific way. A bit of a lifesaver. The overhead of building and maintaining admin models is insane.

Taking another look at the code you posted…

What about instead on creating inlines automatically, you simply create links via your get_url method to any other models which have a relationship back to the currently viewed modeladmin (e.g. those with onetoones). So you can then easily click through to the related model.

I guess it’s the reverse of what you’re already doing in the referenced code. As you’re already capturing relationships which are specified in the modeladmin which you’re currently viewing.

Perhaps you could use the below to get all related objects of the model (incl. those specified by other models), then get the actual model associated with that object, and then finally, use your get_url method to get the url of that models instance.

def get_all_related_objects(model):
    return [
        f for f in model._meta.get_fields()
        if (f.one_to_many or f.one_to_one) and f.auto_created and not f.concrete
    ]
self.get_url_factory(related_object_model, model_name)

I’m not the best at this sort of thing, but curious on your thoughts.

There are some really interesting ideas there!

It might work - it’s probably worth a try. My concern would be my lack of knowledge of when this code would be run in building the admin objects compared to when the models in different apps get registered. What I don’t know is whether this might be running in “AppA” before the models in “AppZ” get registered, and so the related object managers from AppZ don’t exist yet.

mmm, good point. I didn’t really want to overcomplicate it further – but there is a handler to check if a model is already registered. So, perhaps this function can be called recursively on any references to related model fields. Either that, or simply put an input parameter for which target model to create the admin for, and put some onus on the dev to call the function(s) in the correct order for their apps (rather than simply looping all models in apps.get_models()).

Again - it’s conjecture at this point.

Actually, in thinking about it more seriously, I’m becoming more inclined to believe that the models must all be registered before the admin is built. I would probably be more surprised to encounter a problem along these lines.

I played around with this again this morning. I think I have something which I’m somewhat happy with.

I ended up adapting a package noted in the code comments below. It allows for onetoone and onetomany relations to be linked to an admin class.

I’m using this as a mixin for admin classes which I want to add actions or other customizations to. Or otherwise, registering all models via a loop with this as the base class. The result is that any registered model can have all of its related models navigated to, and avoids needing to write a heap of custom inlines.

It’s been noted here that issues will arise if the related models aren’t registered. But perhaps some checks can be added into this in order to prevent unregistered relations from being included.

I’m also seeing this as being quite messy right now. I think it in-fact makes more sense to strip the admin-relation-links package back out.

class ModelAdminAutoMixin(admin.ModelAdmin):
    """
    https://github.com/gitaarik/django-admin-relation-links
    """

    def _add_admin_field(self, field_name, func):
        if not hasattr(self, field_name):
            setattr(self, field_name, func)

        if field_name not in self.readonly_fields:
            self.readonly_fields += (field_name,)

    def _add_change_link(self, model_field_name, admin_field_name):

        def make_change_link(model_field_name):
            def func(instance):
                return self._get_change_link(instance, model_field_name, admin_field_name)

            self.decorate_link_func(func, model_field_name)
            return func

        self._add_admin_field(admin_field_name, make_change_link(model_field_name))

    def _get_change_link(self, instance, model_field_name, admin_field_name):
        try:
            target_instance = getattr(instance, model_field_name)
        except Exception:
            return

        return get_link_field(
            reverse(
                '{}:{}_{}_change'.format(
                    self.admin_site.name,
                    target_instance._meta.app_label,
                    target_instance._meta.model_name
                ),
                args=[target_instance.pk]
            ),
            self.link_label(admin_field_name, target_instance)
        )
    def _add_changelist_link(self, model_field_name, admin_field_name):

        def make_changelist_link(model_field_name):
            def func(instance):
                return self._get_changelist_link(instance, model_field_name)
            self.decorate_link_func(func, model_field_name)
            return func

        self._add_admin_field(admin_field_name, make_changelist_link(model_field_name))

    def _get_changelist_link(self, instance, model_field_name):
        try:
            target_instance = getattr(instance, model_field_name)
        except Exception:
            return

        def get_url():
            return reverse(
                '{}:{}_{}_changelist'.format(
                    self.admin_site.name,
                    *self._get_app_model(instance, model_field_name)
                )
            )

        def get_lookup_filter():
            return instance._meta.get_field(model_field_name).field.name

        def get_label():
            return target_instance.model._meta.verbose_name_plural.capitalize()

        return get_link_field(
            '{}?{}={}'.format(get_url(), get_lookup_filter(), instance.pk), get_label()
        )

    def _get_app_model(self, instance, model_field_name):
        model_meta = getattr(instance, model_field_name).model._meta
        app = model_meta.app_label
        model = model_meta.model_name

        return app, model

    def decorate_link_func(self, func, model_field_name):
        func.short_description = model_field_name.replace('_', ' ').capitalize()

        try:
            field = self.model._meta.get_field(model_field_name)
        except:
            pass
        else:
            if hasattr(field.related_model._meta, 'ordering') and len(field.related_model._meta.ordering) > 0:
                func.admin_order_field = '{}__{}'.format(
                    field.name,
                    field.related_model._meta.ordering[0].replace('-', '')
                )

    def link_label(self, admin_field_name, target_instance):
        label_method_name = '{}_label'.format(admin_field_name)
        if hasattr(self, label_method_name):
            return getattr(self, label_method_name)(target_instance)

        return str(target_instance)

    def __init__(self, model, admin_site):
        list_filter_threshold = 10
        list_filter_type_includes = (
            DateField,
            DateTimeField,
            ForeignKey,
            BooleanField,
        )
        list_display_threshold = 10
        list_display_type_includes = (
            DateField,
            DateTimeField,
            ForeignKey,
            BooleanField,
            CharField,
        )
        search_includes = [
            'name',
            'slug',
            'title',
        ]
        date_hierarchy_includes = [
            'joined_at',
            'updated_at',
            'created_at',
            'modified_at',
        ]

        self.list_display = ()
        self.list_filter = ()
        self.search_fields = ()
        self.readonly_fields = ()
        self.date_hierarchy = None

        fields = [
            f for f in model._meta.fields
            if not (f.many_to_one or f.one_to_one)
        ]

        for field in fields:
            if field.primary_key or isinstance(field, list_display_type_includes) and \
                    len(self.list_display) < list_display_threshold:
                self.list_display += (field.name,)

            if isinstance(field, list_filter_type_includes) and len(self.list_filter) < list_filter_threshold:
                self.list_filter += (field.name,)

            if field.name in search_includes:
                self.search_fields += (field.name,)

            if field.name in date_hierarchy_includes and not self.date_hierarchy:
                self.date_hierarchy = field.name

        change_links = [
            f.name for f in model._meta.get_fields()
            if f not in model._meta.fields and f.one_to_one
        ]

        for model_field_name, admin_field_name in parse_field_config(change_links):
            self._add_change_link(model_field_name, admin_field_name)

        changelist_links = [
            f.name for f in model._meta.get_fields()
            if f not in model._meta.fields and f.one_to_many
        ]

        for model_field_name, admin_field_name in parse_field_config(changelist_links):
            self._add_changelist_link(model_field_name, admin_field_name)

        super().__init__(model, admin_site)

@KenWhitesell – what would be the best way to exclude any one_to_many relationship fields from the below, which don’t currently have an object? that is, they’re currently null/empty?

        changelist_links = [
            f.name for f in model._meta.get_fields()
            if f not in model._meta.fields and f.one_to_many
        ]

You got me on that one. I don’t know if the models are sufficiently registered at that point to be able to issue a query or not. I’d suggest trying it to see what happens.

I mean more simply, how can you query the relationship (e.g. filter().exists()) as part of the above loop? I think field has a related_model property?

It would be a query on the model, not the field.

If m is an instance to a model (i.e. m = MyModel), then m.objects.exists() is your desired function.

(<guess> You might want to look at the “related_model” attribute of the field. </guess>)

I’m not in front of my machine right now, but I guess modifying the above to something like?

if f not in model._meta.fields and f.one_to_many and getattr(model, f.related_model._meta.model_name).exists()

??

I’m not sure if that’ll work. I think I need a way to get the actual related_name.

edit – actually, I would have thought this would work: getattr(model, f.get_accessor_name()).exists()

edit – sorry, its obvious what I’m doing wrong here now that I’ve taken a closer look, that is, treating the model as an instance. I managed to get what I want by simply adding the below to the get_changelist_link def.

        if not target_instance.exists():
            return