Opened 8 years ago
Last modified 8 years ago
#28717 new Cleanup/optimization
Document that using ModelAdmin.list_filter with foreign keys may require a database router
| Reported by: | Adam Brenecki | Owned by: | nobody |
|---|---|---|---|
| Component: | Documentation | Version: | 1.11 |
| Severity: | Normal | Keywords: | |
| Cc: | Triage Stage: | Accepted | |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
I'm using Django's admin on a model on one of a number of secondary databases. The model has managed = False and doesn't exist on the default database. I'm not using a custom database router; I've overridden methods on my ModelAdmin as per here: https://docs.djangoproject.com/en/1.11/topics/db/multi-db/#exposing-multiple-databases-in-django-s-admin-interface. This mostly works, except when I add a ForeignKey field to list_filter, when it appears to try to query the primary database for the values to display in the filter, and I can't find a way to override this behaviour in ModelAdmin.
This happens both on Django 1.11.5 and on master (as of commit 1b73ccc); other models that have list_filters that aren't foreign keys work fine, as does this model if I comment out the line with list_filter.
Below is an excerpt from my admin.py as well as the full stack trace:
from django.contrib import admin from . import models class ApplyOLModelAdmin(admin.ModelAdmin): def save_model(self, request, obj, form, change): obj.save(using=request.applyol) def delete_model(self, request, obj): obj.delete(using=request.applyol) def get_queryset(self, request): return super().get_queryset(request).using(request.applyol) def formfield_for_foreignkey(self, db_field, request, **kwargs): return super().formfield_for_foreignkey( db_field, request, using=request.applyol, **kwargs ) def formfield_for_manytomany(self, db_field, request, **kwargs): return super().formfield_for_manytomany( db_field, request, using=request.applyol, **kwargs ) def has_module_permission(self, request): return True def has_add_permission(self, request, obj=None): return True def has_change_permission(self, request, obj=None): return True def has_delete_permission(self, request, obj=None): return True @admin.register(models.CountryCity) class CountryCityAdmin(ApplyOLModelAdmin): list_display = ('country', 'code', 'name') list_display_links = ('name',) list_filter = ('country',) ordering = ('country__name', 'name')
Environment:
Request Method: GET
Request URL: http://localhost:8000/dev/admin/applyol_editor/countrycity/
Django Version: 2.1.dev20171016175638
Python Version: 3.6.2
Installed Applications:
('sl_admin.apps.applyol_editor',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'form_utils',
'webpack_loader',
'widget_tweaks',
'sl_admin.lib',
'sl_admin.apps.accounts',
'sl_admin.apps.landing',
'sl_admin.apps.form_builder')
Installed Middleware:
('django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'csp.middleware.CSPMiddleware',
'sl_admin.lib.middleware.XUACompatibleMiddleware',
'sl_admin.apps.accounts.middleware.SocialAuthErrorMiddleware',
'sl_admin.lib.environment.EnvironmentMiddleware',
'rollbar.contrib.django.middleware.RollbarNotifierMiddleware')
Traceback:
File "/Users/adambrenecki/Projects/django/django/db/backends/utils.py" in _execute
86. return self.cursor.execute(sql, params)
The above exception (relation "APPLYOL.TBLCOUNTRY" does not exist
LINE 1: ...ODE", "APPLYOL"."TBLCOUNTRY"."ISNATIONALITY" FROM "APPLYOL"....
^
) was the direct cause of the following exception:
File "/Users/adambrenecki/Projects/django/django/core/handlers/exception.py" in inner
35. response = get_response(request)
File "/Users/adambrenecki/Projects/django/django/core/handlers/base.py" in _get_response
128. response = self.process_exception_by_middleware(e, request)
File "/Users/adambrenecki/Projects/django/django/core/handlers/base.py" in _get_response
126. response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/Users/adambrenecki/Projects/django/django/contrib/admin/options.py" in wrapper
574. return self.admin_site.admin_view(view)(*args, **kwargs)
File "/Users/adambrenecki/Projects/django/django/utils/decorators.py" in _wrapped_view
142. response = view_func(request, *args, **kwargs)
File "/Users/adambrenecki/Projects/django/django/views/decorators/cache.py" in _wrapped_view_func
44. response = view_func(request, *args, **kwargs)
File "/Users/adambrenecki/Projects/django/django/contrib/admin/sites.py" in inner
223. return view(request, *args, **kwargs)
File "/Users/adambrenecki/Projects/django/django/utils/decorators.py" in _wrapper
62. return bound_func(*args, **kwargs)
File "/Users/adambrenecki/Projects/django/django/utils/decorators.py" in _wrapped_view
142. response = view_func(request, *args, **kwargs)
File "/Users/adambrenecki/Projects/django/django/utils/decorators.py" in bound_func
58. return func.__get__(self, type(self))(*args2, **kwargs2)
File "/Users/adambrenecki/Projects/django/django/contrib/admin/options.py" in changelist_view
1570. cl = self.get_changelist_instance(request)
File "/Users/adambrenecki/Projects/django/django/contrib/admin/options.py" in get_changelist_instance
705. self,
File "/Users/adambrenecki/Projects/django/django/contrib/admin/views/main.py" in __init__
75. self.queryset = self.get_queryset(request)
File "/Users/adambrenecki/Projects/django/django/contrib/admin/views/main.py" in get_queryset
313. filters_use_distinct) = self.get_filters(request)
File "/Users/adambrenecki/Projects/django/django/contrib/admin/views/main.py" in get_filters
129. self.model, self.model_admin, field_path=field_path
File "/Users/adambrenecki/Projects/django/django/contrib/admin/filters.py" in create
157. return list_filter_class(field, request, params, model, model_admin, field_path=field_path)
File "/Users/adambrenecki/Projects/django/django/contrib/admin/filters.py" in __init__
168. self.lookup_choices = self.field_choices(field, request, model_admin)
File "/Users/adambrenecki/Projects/django/django/contrib/admin/filters.py" in field_choices
195. return field.get_choices(include_blank=False)
File "/Users/adambrenecki/Projects/django/django/db/models/fields/__init__.py" in get_choices
812. limit_choices_to)]
File "/Users/adambrenecki/Projects/django/django/db/models/query.py" in __iter__
270. self._fetch_all()
File "/Users/adambrenecki/Projects/django/django/db/models/query.py" in _fetch_all
1174. self._result_cache = list(self._iterable_class(self))
File "/Users/adambrenecki/Projects/django/django/db/models/query.py" in __iter__
55. results = compiler.execute_sql(chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size)
File "/Users/adambrenecki/Projects/django/django/db/models/sql/compiler.py" in execute_sql
1043. cursor.execute(sql, params)
File "/Users/adambrenecki/Projects/django/django/db/backends/utils.py" in execute
101. return super().execute(sql, params)
File "/Users/adambrenecki/Projects/django/django/db/backends/utils.py" in execute
69. return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
File "/Users/adambrenecki/Projects/django/django/db/backends/utils.py" in _execute_with_wrappers
78. return executor(sql, params, many, context)
File "/Users/adambrenecki/Projects/django/django/db/backends/utils.py" in _execute
86. return self.cursor.execute(sql, params)
File "/Users/adambrenecki/Projects/django/django/db/utils.py" in __exit__
89. raise dj_exc_value.with_traceback(traceback) from exc_value
File "/Users/adambrenecki/Projects/django/django/db/backends/utils.py" in _execute
86. return self.cursor.execute(sql, params)
Exception Type: ProgrammingError at /dev/admin/applyol_editor/countrycity/
Exception Value: relation "APPLYOL.TBLCOUNTRY" does not exist
LINE 1: ...ODE", "APPLYOL"."TBLCOUNTRY"."ISNATIONALITY" FROM "APPLYOL"....
^
Change History (2)
comment:1 by , 8 years ago
comment:2 by , 8 years ago
| Component: | Uncategorized → Documentation |
|---|---|
| Summary: | Admin always queries default database when rendering a list_filter on a ForeignKey → Document that using ModelAdmin.list_filter with foreign keys may require a database router |
| Triage Stage: | Unreviewed → Accepted |
| Type: | Uncategorized → Cleanup/optimization |
Absent another proposal, it sounds like documenting that a database router is required for this case could be a solution.
Yep, that's an oversight in the documentation. However, you can remove the admin customisation and write a database router instead. That will work not just for the admin but throughout your website too. If you are setting
request.applyolin middleware, you can in the same middleware tell your database router to change the database that it should give fordb_for_read(model=ApplyOl). Here's an example you can adapt: https://github.com/yandex/django_replicated/blob/master/django_replicated/router.py