Let's say I have my pizza application with Topping and Pizza classes and they show in Django Admin like this:
PizzaApp
-
Toppings >>>>>>>>>> Add / Change
Pizzas >>>>>>>>>> Add / Change
But I want them like this:
PizzaApp
-
Pizzas >>>>>>>>>> Add / Change
Toppings >>>>>>>>>> Add / Change
How do I configure that in my admin.py?
A workaround that you can try is tweaking your models.py as follows:
class Topping(models.Model):
.
.
.
class Meta:
verbose_name_plural = "2. Toppings"
class Pizza(models.Model):
.
.
.
class Meta:
verbose_name_plural = "1. Pizzas"
Not sure if it is against the django's best practices but it works (tested with django trunk).
Good luck!
PS: sorry if this answer was posted too late but it can help others in future similar situations.
If you want to solve this in 10 seconds just use spaces in verbose_name_plural, for example:
class Topping(models.Model):
class Meta:
verbose_name_plural = " Toppings" # 2 spaces
class Pizza(models.Model):
class Meta:
verbose_name_plural = " Pizzas" # 1 space
Of course it isn't ellegant but may work for a while before we get a better solution.
I eventually managed to do it thanks to this Django snippet, you just need to be aware of the ADMIN_REORDER setting:
ADMIN_REORDER = (
('app1', ('App1Model1', 'App1Model2', 'App1Model3')),
('app2', ('App2Model1', 'App2Model2')),
)
app1 must not be prefixed with the project name, i.e. use app1 instead of mysite.app1.
There's now a nice Django package for that:
https://pypi.python.org/pypi/django-modeladmin-reorder
Answer in June 2018
This answer is similar to Vasil's idea
I tried to solve similar problems, and then I saw such the fragment.
I made some modifications based on this clip. The code is as follows.
# myproject/setting.py
...
# set my ordering list
ADMIN_ORDERING = [
('pizza_app', [
'Pizzas',
'Toppings'
]),
]
# Creating a sort function
def get_app_list(self, request):
app_dict = self._build_app_dict(request)
for app_name, object_list in ADMIN_ORDERING:
app = app_dict[app_name]
app['models'].sort(key=lambda x: object_list.index(x['object_name']))
yield app
# Covering django.contrib.admin.AdminSite.get_app_list
from django.contrib import admin
admin.AdminSite.get_app_list = get_app_list
...
Note that the sorting list used in this sorting function contains the sorting of all app and its modules in the system. If you don't need it, please design the sorting function according to your own needs.
It works great on Django 2.0
This is actually covered at the very bottom of Writing your first Django app, part 7.
Here's the relevant section:
Customize the admin index page
On a similar note, you might want to
customize the look and feel of the
Django admin index page.
By default, it displays all the apps
in INSTALLED_APPS that have been
registered with the admin application,
in alphabetical order. You may want to
make significant changes to the
layout. After all, the index is
probably the most important page of
the admin, and it should be easy to
use.
The template to customize is
admin/index.html. (Do the same as with
admin/base_site.html in the previous
section -- copy it from the default
directory to your custom template
directory.) Edit the file, and you'll
see it uses a template variable called
app_list. That variable contains every
installed Django app. Instead of using
that, you can hard-code links to
object-specific admin pages in
whatever way you think is best.
Here's the snippet Emmanuel used, updated for Django 1.8:
In templatetags/admin_reorder.py:
from django import template
from django.conf import settings
from collections import OrderedDict
register = template.Library()
# from http://www.djangosnippets.org/snippets/1937/
def register_render_tag(renderer):
"""
Decorator that creates a template tag using the given renderer as the
render function for the template tag node - the render function takes two
arguments - the template context and the tag token
"""
def tag(parser, token):
class TagNode(template.Node):
def render(self, context):
return renderer(context, token)
return TagNode()
for copy_attr in ("__dict__", "__doc__", "__name__"):
setattr(tag, copy_attr, getattr(renderer, copy_attr))
return register.tag(tag)
#register_render_tag
def admin_reorder(context, token):
"""
Called in admin/base_site.html template override and applies custom ordering
of apps/models defined by settings.ADMIN_REORDER
"""
# sort key function - use index of item in order if exists, otherwise item
sort = lambda order, item: (order.index(item), "") if item in order else (
len(order), item)
if "app_list" in context:
# sort the app list
order = OrderedDict(settings.ADMIN_REORDER)
context["app_list"].sort(key=lambda app: sort(order.keys(),
app["app_url"].strip("/").split("/")[-1]))
for i, app in enumerate(context["app_list"]):
# sort the model list for each app
app_name = app["app_url"].strip("/").split("/")[-1]
if not app_name:
app_name = app["name"].lower()
model_order = [m.lower() for m in order.get(app_name, [])]
context["app_list"][i]["models"].sort(key=lambda model:
sort(model_order, model["admin_url"].strip("/").split("/")[-1]))
return ""
In settings.py:
ADMIN_REORDER = (
('app1', ('App1Model1', 'App1Model2', 'App1Model3')),
('app2', ('App2Model1', 'App2Model2')),
)
(insert your own app names in here. Admin will place missing apps or models at the end of the list, so long as you list at least two models in each app.)
In your copy of base_site.html:
{% extends "admin/base.html" %}
{% load i18n admin_reorder %}
{% block title %}{{ title }} | {% trans 'Django site admin' %}{% endblock %}
{% block branding %}
{% admin_reorder %}
<h1 id="site-name">{% trans 'Django administration' %}</h1>
{% endblock %}
{% block nav-global %}{% endblock %}
ADMIN_ORDERING = {
"PizzaApp": [
"Pizzas",
"Toppings",
],
}
def get_app_list(self, request):
app_dict = self._build_app_dict(request)
for app_name, object_list in app_dict.items():
if app_name in ADMIN_ORDERING:
app = app_dict[app_name]
app["models"].sort(
key=lambda x: ADMIN_ORDERING[app_name].index(x["object_name"])
)
app_dict[app_name]
yield app
else:
yield app_dict[app_name]
admin.AdminSite.get_app_list = get_app_list
This solution works for me, modified the one from 林伟雄.
You get to keep the default auth ordering AND specify your own.
If you're using Suit for the AdminSite you can do menu customization using the menu tag.
I was looking for a simple solution where I could order the apps by their name in the admin panel. I came up with the following template tag:
from django import template
from django.conf import settings
register = template.Library()
#register.filter
def sort_apps(apps):
apps.sort(
key = lambda x:
settings.APP_ORDER.index(x['app_label'])
if x['app_label'] in settings.APP_ORDER
else len(apps)
)
print [x['app_label'] for x in apps]
return apps
Then, just override templates/admin/index.html and add that template tag:
{% extends "admin/index.html" %}
{% block content %}
{% load i18n static sort_apps %}
<div id="content-main">
{% if app_list %}
{% for app in app_list|sort_apps %}
<div class="app-{{ app.app_label }} module">
<table>
<caption>
{{ app.name }}
</caption>
{% for model in app.models %}
<tr class="model-{{ model.object_name|lower }}">
{% if model.admin_url %}
<th scope="row">{{ model.name }}</th>
{% else %}
<th scope="row">{{ model.name }}</th>
{% endif %}
{% if model.add_url %}
<td>{% trans 'Add' %}</td>
{% else %}
<td> </td>
{% endif %}
{% if model.admin_url %}
<td>{% trans 'Change' %}</td>
{% else %}
<td> </td>
{% endif %}
</tr>
{% endfor %}
</table>
</div>
{% endfor %}
{% else %}
<p>{% trans "You don't have permission to edit anything." %}</p>
{% endif %}
</div>
{% endblock %}
Then customized the APP_ORDER in settings.py:
APP_ORDER = [
'app1',
'app2',
# and so on...
]
It works great on Django 1.10
Here's a version that gives you a bit more flexibility, namely:
You can partially define apps ordering, leaving the rest for Django to add to the list
You can specify order on modules, or avoid defining it, by using '*' instead
Your defined apps ordering will appear first, then all the rest of apps appended after it
To check the name of your app, either look at the file apps.py inside the app's directory and check for name property of the class Config(AppConfi): or in case that is not present, use the name of the directory for the app in the project.
Add this code somewhere in your settings.py file:
# ======[Setting the order in which the apps/modules show up listed on Admin]========
# set my ordering list
ADMIN_ORDERING = [
('crm', '*'),
('property', '*'),
]
# Creating a sort function
def get_app_list(self, request):
"""
Returns a sorted list of all the installed apps that have been
registered in this site.
Allows for:
ADMIN_ORDERING = [
('app_1', [
'module_1',
'module_2'
]),
('app_2', '*'),
]
"""
app_dict = self._build_app_dict(request)
# Let's start by sorting the apps alphabetically on a list:
app_list = sorted(app_dict.values(), key=lambda x: x['name'].lower())
# Sorting the models alphabetically within each app.
for app in app_list:
if app['app_label'] in [el[0] for el in ADMIN_ORDERING]:
app_list.remove(app)
else:
app['models'].sort(key=lambda x: x['name'])
# Now we order the app list in our defined way in ADMIN_ORDERING (which could be a subset of all apps).
my_ordered_apps = []
if app_dict:
for app_name, object_list in ADMIN_ORDERING:
app = app_dict[app_name]
if object_list == '*':
app['models'].sort(key=lambda x: x['name'])
else:
app['models'].sort(key=lambda x: object_list.index(x['object_name']))
my_ordered_apps.append(app)
# Now we combine and arrange the 2 lists together
my_ordered_apps.extend(app_list)
return my_ordered_apps
# Covering django.contrib.admin.AdminSite.get_app_list
from django.contrib import admin
admin.AdminSite.get_app_list = get_app_list
# =========================================
This is nothing more than overwriting the function defined by Django on the file python2.7/site-packages/django/contrib/admin/sites.py.
That get_app_list method of class AdminSite(object): produces a data structure with all apps on the project, including for Django's auth app, such as:
[
{
"app_label": "auth",
"app_url": "/admin/auth/",
"has_module_perms": "True",
"models": [
{
"add_url": "/admin/auth/group/add/",
"admin_url": "/admin/auth/group/",
"name": "<django.utils.functional.__proxy__ object at 0x11057f990>",
"object_name": "Group",
"perms": {
"add": "True",
"change": "True",
"delete": "True"
}
},
{
"add_url": "/admin/auth/user/add/",
"admin_url": "/admin/auth/user/",
"name": "<django.utils.functional.__proxy__ object at 0x11057f710>",
"object_name": "User",
"perms": {
"add": "True",
"change": "True",
"delete": "True"
}
}
],
"name": "<django.utils.functional.__proxy__ object at 0x108b81850>"
},
{
"app_label": "reservations",
"app_url": "/admin/reservations/",
"has_module_perms": "True",
"models": [
{
"add_url": "/admin/reservations/reservationrule/add/",
"admin_url": "/admin/reservations/reservationrule/",
"name": "<django.utils.functional.__proxy__ object at 0x11057f6d0>",
"object_name": "ReservationRule",
"perms": {
"add": "True",
"change": "True",
"delete": "True"
}
}
],
"name": "Availability"
},
{
"app_label": "blog",
"app_url": "/admin/blog/",
"has_module_perms": "True",
"models": [
{
"add_url": "/admin/blog/category/add/",
"admin_url": "/admin/blog/category/",
"name": "Categories",
"object_name": "Category",
"perms": {
"add": "True",
"change": "True",
"delete": "True"
}
},
{
"add_url": "/admin/blog/post/add/",
"admin_url": "/admin/blog/post/",
"name": "<django.utils.functional.__proxy__ object at 0x11057f110>",
"object_name": "Post",
"perms": {
"add": "True",
"change": "True",
"delete": "True"
}
},
{
"add_url": "/admin/blog/tag/add/",
"admin_url": "/admin/blog/tag/",
"name": "<django.utils.functional.__proxy__ object at 0x11057f390>",
"object_name": "Tag",
"perms": {
"add": "True",
"change": "True",
"delete": "True"
}
}
],
"name": "Blog"
},
(...)
]
This is just a wild stab in the dark, but is there any chance that the order in which you call admin.site.register(< Model class >, < ModelAdmin class >) can determine the display order? Actually, I doubt that would work because I believe Django maintains a registry of the Model -> ModelAdmin objects implemented as a standard Python dictionary, which does not maintain iteration ordering.
If that doesn't behave the way you want, you can always play around with the source in django/contrib/admin. If you need the iteration order maintained, you could replace the _registry object in the AdminSite class (in admin/sites.py) with a UserDict or DictMixin that maintains insertion order for the keys. (But please take this advice with a grain of salt, since I've never made these kinds of changes myself and I'm only making an educated guess at how Django iterates over the collection of ModelAdmin objects. I do think that django/contrib/admin/sites.py is the place to look for this code, though, and the AdminSite class and register() and index() methods in particular are what you want.)
Obviously the nicest thing here would be a simple option for you to specify in your own /admin.py module. I'm sure that's the kind of answer you were hoping to receive. I'm not sure if those options exist, though.
My solution was to make subclasses of django.contrib.admin.sites.AdminSite and django.contrib.admin.options.ModelAdmin .
I did this so I could display a more descriptive title for each app and order the appearance of models in each app. So I have a dict in my settings.py that maps app_labels to descriptive names and the order in which they should appear, the models are ordered by an ordinal field I provide in each ModelAdmin when I register them with the admin site.
Although making your own subclasses of AdminSite and ModelAdmin is encouraged in the docs, my solution looks like an ugly hack in the end.
Copy lib\site-packages\django\contrib\admin\templates\admin\index.html template to project1\templates\admin\ directory, where project1 is the name of your project.
In the copied file, i.e. project1\templates\admin\index.html, replace lines:
{% block content %}
...
{% endblock %}
with:
{% block content %}
<div id="content-main">
{% if app_list %}
<div class="module">
<table>
<caption>App 1</caption>
<tr> <th> Model 1 </th> <td>Description of model 1</td> </tr>
<tr> <th> Model 2 </th> <td>Description of model 1</td> </tr>
<tr> <th> <a href="..." >...</a> </th> <td>...</td> </tr>
</table>
</div>
<div class="module">
<table>
<caption>Authentication and authorization</caption>
<tr> <th> <a href="/admin/auth/user/" >Users</a> </th> <td>List of users</td> </tr>
<tr> <th> <a href="/admin/auth/group/" >Groups</a> </th> <td>List of users' groups</td> </tr>
</table>
</div>
{% else %}
<p>{% trans "You don't have permission to view or edit anything." %}</p>
{% endif %}
</div>
{% endblock %}
where:
app1 is the name of your application with models,
modeli is the name of i-th model in app1.
If you defined more than one application with models in your project, then simply add another table in the above index.html file.
Because we change the template, we can freely change its HTML code. For example we can add a description of the models as it was shown above. You can also restore Add and Change links - I deleted them since I think they are redundant.
The answer is a practical demonstration of the solution from Dave Kasper's answer.
custom_admin.py
from django.contrib.admin import AdminSite
class CustomAdminSite(AdminSite):
def get_urls(self):
urls = super(MyAdminSite, self).get_urls()
return urls
def get_app_list(self, request):
app_dict = self._build_app_dict(request)
ordered_app_list = []
if app_dict:
# TODO: change this dict
admin_ordering = {
'app1': ('Model1', 'Model2'),
'app2': ('Model7', 'Model4'),
'app3': ('Model3',),
}
ordered_app_list = []
for app_key in admin_ordering:
app = app_dict[app_key]
app_ordered_models = []
for model_name in admin_ordering[app_key]:
for model in app_dict[app_key]['models']:
if model['object_name'] == model_name:
app_ordered_models.append(model)
break
app['models'] = app_ordered_models
ordered_app_list.append(app)
return ordered_app_list
admin_site = CustomAdminSite()
urls.py
from custom_admin import admin_site
urlpatterns = [
path('admin/', admin_site.urls),
]
Add the below code to your settings.py:
def get_app_list(self, request):
"""
Return a sorted list of all the installed apps that have been
registered on this site.
"""
ordering = {
# for superuser
'Group': 1,
'User': 2,
# fist app
'TopMessage': 101,
'Slider': 102,
'ProductCategory': 103,
'Product': 104,
'ProductPicture': 105,
# 2nd app
'ProductReview': 201,
'Promotion': 202,
'BestSeller': 203,
}
app_dict = self._build_app_dict(request)
app_list = sorted(app_dict.values(), key=lambda x: x['name'].lower())
for app in app_list:
app['models'].sort(key=lambda x: ordering[x['object_name']])
return app_list
admin.AdminSite.get_app_list = get_app_list
And make changes to the ordering dictionary to match your apps and models. That's it.
The benefit of my solution is that it will show the 'auth' models if the user is a superuser.
Django, by default, orders the models in admin alphabetically. So the order of models in Event admin is Epic, EventHero, EventVillain, Event
Instead you want the order to be
EventHero, EventVillain, Epic then event.
The template used to render the admin index page is admin/index.html and the view function is ModelAdmin.index.
def index(self, request, extra_context=None):
"""
Display the main admin index page, which lists all of the installed
apps that have been registered in this site.
"""
app_list = self.get_app_list(request)
context = {
**self.each_context(request),
'title': self.index_title,
'app_list': app_list,
**(extra_context or {}),
}
request.current_app = self.name
return TemplateResponse(request, self.index_template or
'admin/index.html', context)
The method get_app_list, set the order of the models.:
def get_app_list(self, request):
"""
Return a sorted list of all the installed apps that have been
registered in this site.
"""
app_dict = self._build_app_dict(request)
# Sort the apps alphabetically.
app_list = sorted(app_dict.values(), key=lambda x: x['name'].lower())
# Sort the models alphabetically within each app.
for app in app_list:
app['models'].sort(key=lambda x: x['name'])
return app_list
So to set the order we override get_app_list as:
class EventAdminSite(AdminSite):
def get_app_list(self, request):
"""
Return a sorted list of all the installed apps that have been
registered in this site.
"""
ordering = {
"Event heros": 1,
"Event villains": 2,
"Epics": 3,
"Events": 4
}
app_dict = self._build_app_dict(request)
# a.sort(key=lambda x: b.index(x[0]))
# Sort the apps alphabetically.
app_list = sorted(app_dict.values(), key=lambda x: x['name'].lower())
# Sort the models alphabetically within each app.
for app in app_list:
app['models'].sort(key=lambda x: ordering[x['name']])
return app_list
The code app['models'].sort(key=lambda x: ordering[x['name']]) sets the fixed ordering. Your app now looks like this.
Check the Documentation
In Settings.py file setting the order in which the apps/modules show up listed on Admin
set my ordering list
ADMIN_ORDERING = [
('Your_App1', '*'),
('Your_App2', '*'),
]
Creating a sort function below ADMIN_ORDERING
def get_app_list(self, request):
"""
You Can Set Manually Ordering For Your Apps And Models
ADMIN_ORDERING = [
('Your_App1', [
'module_1',
'module_2'
]),
('Your_App2', '*'),
]
"""
app_dict = self._build_app_dict(request)
# Let's start by sorting the apps alphabetically on a list:
app_list = sorted(app_dict.values(), key=lambda x: x['name'].lower())
# Sorting the models alphabetically within each app.
for app in app_list:
if app['app_label'] in [el[0] for el in ADMIN_ORDERING]:
app_list.remove(app)
else:
app['models'].sort(key=lambda x: x['name'])
# Now we order the app list in our defined way in ADMIN_ORDERING (which could be a subset of all apps).
my_ordered_apps = []
if app_dict:
for app_name, object_list in ADMIN_ORDERING:
app = app_dict[app_name]
if object_list == '*':
app['models'].sort(key=lambda x: x['name'])
else:
app['models'].sort(key=lambda x: object_list.index(x['object_name']))
my_ordered_apps.append(app)
# Now we combine and arrange the 2 lists together
my_ordered_apps.extend(app_list)
return my_ordered_apps
# Covering django.contrib.admin.AdminSite.get_app_list
from django.contrib import admin
admin.AdminSite.get_app_list = get_app_list
Quite a simple and self contained approach can be using the decorator pattern to resort your app and modules in this way:
# admin.py
from django.contrib import admin
def app_resort(func):
def inner(*args, **kwargs):
app_list = func(*args, **kwargs)
# Useful to discover your app and module list:
#import pprint
#pprint.pprint(app_list)
app_sort_key = 'name'
app_ordering = {
"APP_NAME1": 1,
"APP_NAME2": 2,
"APP_NAME3": 3,
}
resorted_app_list = sorted(app_list, key=lambda x: app_ordering[x[app_sort_key]] if x[app_sort_key] in app_ordering else 1000)
model_sort_key = 'object_name'
model_ordering = {
"Model1": 1,
"Model2": 2,
"Model3": 3,
"Model14": 4,
}
for app in resorted_app_list:
app['models'].sort(key=lambda x: model_ordering[x[model_sort_key]] if x[model_sort_key] in model_ordering else 1000)
return resorted_app_list
return inner
admin.site.get_app_list = app_resort(admin.site.get_app_list)
This code sorts at the top only the defined keys in the ordering dict, leaving at the bottom all the other.
The approach is clean but it requires get_app_list to be executed before... which is probably not a good idea when performance is important and you have a large app_list.
This has become a lot easier in Django 4.1:
The AdminSite.get_app_list() method now allows changing the order of apps and models on the admin index page.
You can subclass and override this method to change the order the returned list of apps/models.
Related
I am trying to build a project where I have a DB with toys where there are brands, each brand have products, the products have variety of sizes and sizes have colors. What I want to achieve is to have a home page with filtered the brands which I manage to achieve with the views.py below:
def Index(request):
toys = Toys.objects.values('brand').distinct().order_by('brand')
context = {'index': toys }
return render(request, 'index.html', context)
with models.py
class Toys(models.Model):
brand= models.CharField(max_length=255)
product = models.CharField(max_length=255)
size = models.CharField(max_length=255)
color = models.CharField(max_length=255)
Now on the main page I have a list with all the different brans, what I'm trying to do is when I click on any Brand for the home page to redirect me to a result page with list of the Brand's products, product will lead to all available sizes and so on.
How to implement that in the views?
Many approaches are possible. If you do not wish to have better functionality (e.g. all toys from brand 'X' are deleted when the brand is deleted from database), you could do the following.
Write a view function for each "level" of your listing. In the context dictionary, index contains the query result, listing is a heading to be displayed and level is a variable to select how the link is constructed in the template.
from django.shortcuts import render
from toyapp.models import Toys
def Index(request):
toys = Toys.objects.values('brand').distinct().order_by('brand')
context = { 'index': toys,
'listing': 'Toy brands:',
'level': 'brands',
}
return render(request,'toyapp/index.html', context)
def brandview(request, slug):
toys = Toys.objects.filter(brand=slug).order_by('product')
context = {'level': 'products',
'index': toys,
'listing': 'Products from brand ' + slug,
}
return render(request,'toyapp/index.html', context)
def productview(request, slug1, slug2):
toys_set = Toys.objects.filter(brand=slug1)
toys = toys_set.filter(product=slug2).order_by('size')
context = {'level': 'sizes',
'index': toys,
'listing': 'Sizes for product ' + slug2 + ' from brand ' + slug1
}
return render(request,'toyapp/index.html', context)
def sizeview(request, slug1, slug2, slug3):
toys_set = Toys.objects.filter(brand=slug1)
toys = toys_set.filter(product=slug2).order_by('size')
context = {'level': 'colors',
'index': toys,
'listing': 'Colors for size ' + slug3 + ' products ' + slug2 + ' from brand ' + slug1
}
return render(request,'toyapp/index.html', context)
The template index.html is quite simple with a {% for %} loop to go through items and {% if %} tags to select what kind of a link to create and what text to display.
<!DOCTYPE html>
<body>
<h2>{{ listing }}</h2>
{% for item in index %}
<li>
{% if level == 'brands' %}
<a href="{% url 'brandview' slug=item.brand %}" >{{item.brand}}</a>
{% elif level == 'products' %}
<a href="{% url 'productview' slug1=item.brand slug2=item.product %}" >{{item.product}}</a>
{% elif level == 'sizes' %}
<a href="{% url 'sizeview' slug1=item.brand slug2=item.product slug3=item.size %}" >{{item.size}}</a>
{% elif level == 'colors' %}
{{item.color}}
{% endif %}
</li>
{% endfor %}
</body>
Finally, you need to write urls.py with urlpatterns that parses the HTTP request address and passes the arguments to the view.
from django.urls import path, include
from . import views
urlpatterns = [
path('', views.Index, name='index'),
path('<slug:slug>', views.brandview, name='brandview'),
path('<slug:slug1>,<slug:slug2>', views.productview, name='productview'),
path('<slug:slug1>,<slug:slug2>,<slug:slug3>', views.sizeview, name='sizeview'),
]
I would like to have a Django detail view that shows all the models containing a foreign key for the detail view page for that foreign key value, linked from a list view.
The model ActivityID contains a list of character id's (e.g. 'F1088HW'). There are several other models, each containing the contents of a storage facility, and each item has an ActivityID. So when F1088HW is clicked on, for example, a detail page should show all the facilities that has stuff from that ActivityID.
So far I have the following view based on various other SO questions:
class ActivityDetailView(generic.DetailView):
model = ActivityID
# context_object_name = 'library_set'
# Changed to 'activity' based on comments below
# context_object_name = 'ativity'
def get_queryset(self):
pk = self.kwargs['pk']
return models.objects.filter(activityid = pk)
And ActivityID_detail.html:
{% extends "base_generic.html" %}
{% block content %}
<h1>Field Activity ID: {{ activity }} </h1>
<div style="margin-left:20px;margin-top:20px">
<h4>Libraries</h4>
<dl>
{% for library in activity.library_set.all %}
<dt>{{library}} ({{library.library_set.all.count}})</dt>
{% endfor %}
</dl>
</div>
{% endblock %}
urls.py:
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('activities/', views.ActivityIDsView.as_view(), name='activities'),
path('activities/<int:pk>', views.ActivityDetailView.as_view(), name='activity-detail'),
]
But clicking on an ActivityID in the list view then returns:
AttributeError at /catalog/activities/2429
module 'django.db.models'
has no attribute 'objects'
Any idea what I'm doing wrong?
You are calling models module instead of your model.
Try:
return self.model.objects.filter(id=pk)
As Sergey has pointed out, you're calling the wrong class. But actually the whole approach makes no sense. The detail in this view is the Activity. You don't want to filter the activities by activityid, you want to get the activity by ID and then get is related libraries.
The second part of that is done by the activity.library_set.all in the template, and the first part is what the DetailView would normally do - so you don't need that get_queryset method at all. You should remove it.
I've created a custom admin view as documented here.
class MyAdmin(admin.ModelAdmin):
def get_urls(self):
urls = super().get_urls()
my_urls = [
path('stats/', self.admin_site.admin_view(self.stats)),
]
return my_urls + urls
def stats(self, request):
request.current_app = self.admin_site.name
context = dict(
# Include common variables for rendering the admin template.
self.admin_site.each_context(request),
# Anything else you want in the context...
key='blah',
)
return TemplateResponse(request, "sometemplate.html", context)
The URL is working and the template is loading.
But how, can I get a link to my new custom view into the overview of the Django admin?
There is already a similar question How do you add a new entry into the django admin index?, but all of the provided answers were not very satisfying for me.
Therefore I ran through the source code and was looking for a possibility which wouldn't involve overriding any templates, neither index.html nor app_index.html.
The file django/contrib/admin/sites.py holds the code responsible for rendering index.html and app_index.html, the first is the template that displays what is illustrated in the question.
The method index renders the template index.html and displays the available apps with the registered model admins. It uses the method get_app_list to get the apps. Within this method the method _build_app_dict is called, which gets the models and the model admins.
The method app_index renders the template app_index.html and displays the registered model admins for a single app. It uses the method _build_app_dict, mentioned before.
Thus I decided to override this method in my custom admin. Based on the example in the question it can look like this (differences to the original example are shown in bold):
class MyAdmin(admin.AdminSite):
def get_urls(self):
urls = super().get_urls()
my_urls = [
path('stats/', self.admin_site.admin_view(self.stats), name='stats'),
]
return my_urls + urls
def stats(self, request):
request.current_app = self.admin_site.name
context = dict(
# Include common variables for rendering the admin template.
self.admin_site.each_context(request),
# Anything else you want in the context...
key='blah',
)
return TemplateResponse(request, "sometemplate.html", context)
def _build_app_dict(self, request, label=None):
# we create manually a dict to fake a model for our view 'stats'
# this is an example how the dict should look like, i.e. which keys
# should be present, the actual values may vary
stats = {
'name': 'Stats',
'admin_url': reverse('my_admin:stats'),
'object_name': 'Stats',
'perms': {'delete': False, 'add': False, 'change': False},
'add_url': ''
}
# get the app dict from the parent method
app_dict = super(MyAdmin, self)._build_app_dict(request, label)
# check if there is value for label, then the app_index will be rendered
if label:
# append the manually created dictionary 'stats'
app_dict['models'].append(stats)
# otherwise the index will be rendered
# and we have to get the entry for our app,
# which is in this case 'traffic'
# using TrafficConfig.name or TrafficConfig.label
# might be better than hard coding the value
else:
app = app_dict.get('traffic', None)
# if an entry for 'traffic' has been found
# we append our manually created dictionary
if app:
app['models'].append(stats)
return app_dict
my_admin = MyAdmin(name='my_admin')
my_admin.register(Traffic)
Now when we open our custom admin we'll see something like this:
TRAFFIC
---------------------------------
Traffics + Add \ Change
Stats \ Change
This is because we manipulated the dictionary used to render the template and it uses the values we specified, in this case the most relevant is the name Stats, the admin_url which will call the custom view stats. Since we left add_url empty, there will be no + Add link displayed.
Important is also the penultimate line, where we call our custom admin and pass it a name, it will be used as a url namespace.
EDIT:
Unfortunately I noticed only after posting the answer that the question is asking how to display a link for a custom view created in a ModelAdmin, whereas my answer explains how to do this for a custom admin AdminSite. I hope it still of some help.
I know this is a bit old but I had the same issue and I found a simpler (yet maybe messier) way of doing it, that doesn't involve overriding templates or methods.
I just created a proxy model in models.py such as:
class Proxy(models.Model):
id = models.BigAutoField(db_column='id', primary_key=True)
def __str__(self):
return "<Label: id: %d>" % self.id
class Meta:
managed = False
verbose_name_plural = 'proxies'
db_table = 'proxy'
ordering = ('id',)
Which is just a mysql view that a created from am existing table
create view proxy
as select id
from samples
LIMIT 10;
And finally in admin.py
#admin.register(Proxy)
class LabelAdmin(admin.ModelAdmin):
change_list_template = 'label_view.html'
def changelist_view(self, request, extra_context=None):
...
return render(request, "label_view.html", context)
Although not a good way, but you can try overriding the default index.html of the django admin as :
{% for model in app.models %}
<tr class="model-{{ model.object_name|lower }}">
{% if model.admin_url %}
<th scope="row">{{ model.name }}</th>
{% else %}
<th scope="row">{{ model.name }}</th>
{% endif %}
{% if model.add_url %}
<td>{% trans 'Add' %}</td>
{% else %}
<td> </td>
{% endif %}
{% if model.admin_url %}
<td>{% trans 'Change' %}</td>
{% else %}
<td> </td>
{% endif %}
</tr>
{% endfor %}
<!-- extra link you want to display -->
<tr>
<th scope="row">link_name</th>
<td>{% trans 'Change' %}</td>
</tr>
After overriding this block put index.html inside your templates/admin directory of your project. Hope this helps.
I have a form view for user feedback:
urls.py:
url(
r'^feedback/$',
'tool.views.Feedback',
name='feedback'
),
url(
r'^thanks/$',
direct_to_template, {
'template': 'tool_feedback_thanks.html'
},
name='feedback_thanks'
),
forms.py:
class FeedbackForm(forms.Form):
yes_no = forms.ChoiceField(
choices=YES_NO_CHOICE,
initial=1,
widget=forms.RadioSelect(attrs={'class': 'can_reveal_input'}),
label="Are you happy with Our service?"
)
comments = forms.CharField(
widget=forms.Textarea(attrs={
'class': 'hidden', 'placeholder': 'Leave us your comments...'
}),
required=False,
label=""
)
views.py:
def Feedback(request,
template_name='tool_feedback.html'):
title = u'Leave us some feedback'
form = FeedbackForm(request.POST or None)
if form.is_valid():
yes_no = form.cleaned_data['yes_no']
comments = form.cleaned_data['comments']
sender = "A Unirac website user"
recipients = ['person#example.com']
send_mail(yes_no, comments, sender, recipients)
return HttpResponseRedirect(
reverse('feedback_thanks')
)
return render_to_response(template_name, {
'title': title,
'form': form,
}, RequestContext(request))
This works a treat, but now the client is asking that this form be included on every single page. I guess the form can be submitted via js to the appropriate url, but what is the best way to include the unbound form on every page?
Any help would be much appreciated.
I'd create a context processor, to include the form in every view.
EDIT:
To get the user to the previous URL he/she was browsing, you can use just URLs.
# yourapp/context_processors.py
def feedback_form_context_processor(request):
return {
'feedback_form': FeedbackForm(),
'feedback_form_url': reverse("feed_app:form_process", args=(request.path))
}
This is how urls.py could look like:
urlpatterns = patterns('feed_app.views',
url(r'^process-feedback-form/(?P<next_url>\d+)', 'form_process', name='form_process'),
)
And the view for the form:
def form_process(request, next_url):
# Process form, do your stuff here
# if its valid redirect to the url
return redirect(next_url)
And you should structure your templates to have the correct layout. For example, having a base template:
# templates/base.html
<html>
<body>
..
{% block maincontent %}
{% endblock %}
..
{# The form!!! #}
<form action='{{feedback_form_url}}' method='POST'>
#csrftoken
{{ feedback_form.as_p }}
</form>
</body>
</html>
To create a simple view just use the correct template.
# templates/just_a_random_view.html
{% extends base.html %}
{% block maincontent %}
<h1>Content!</h1>
{% endblock %}
Finally, include it in your settings:
# settings.py
TEMPLATE_CONTEXT_PROCESSORS = (
"django.contrib.auth.context_processors.auth",
...
"yourapp.context_processors.feedback_form_context_processor"
)
I believe that easiest way to include form would be to use a assignment_tag:
In template library:
#register.assignment_tag
def feedback_form(format_string):
return FeedbackForm()
In template
{% feedback_form as form %}
{# display form... %}
{{ form.as_p }}
To add on to #bmihelac, who's answer worked really well for me. Since django 2.0 assignment_tag is deprecated in favor of simple_tag. So you can pretty much follow his answer exactly by replacing assignment_tag with simple_tag, like so:
from django import template
from .forms import FeedbackForm
register = template.Library()
#register.simple_tag
def feedback_form():
return FeedbackForm()
And then just refer to https://docs.djangoproject.com/en/2.1/howto/custom-template-tags/#code-layout for info about how to import it into the template!
I am reading the definitive guide to django and am in Chapter 4 on template inheritance. It seems that I am not doing something as elegant as should be possible as I am having to duplicate some code for the context to appear when calling the child view. Here is the code in views.py:
def homepage(request):
current_date = datetime.datetime.now()
current_section = 'Temporary Home Page'
return render_to_response("base.html", locals())
def contact(request):
current_date = datetime.datetime.now()
current_section = 'Contact page'
return render_to_response("contact.html", locals())
It seems redundant to have to include the current_date line in each function.
Here is the base html file that homepage calls:
<html lang= "en">
<head>
<title>{% block title %}Home Page{% endblock %}</title>
</head>
<body>
<h1>The Site</h1>
{% block content %}
<p> The Current section is {{ current_section }}.</p>
{% endblock %}
{% block footer %}
<p>The current time is {{ current_date }}</p>
{% endblock %}
</body>
</html>
and a child template file:
{% extends "base.html" %}
{% block title %}Contact{% endblock %}
{% block content %}
<p>Contact information goes here...</p>
<p>You are in the section {{ current_section }}</p>
{% endblock %}
If I don't include the current_date line when calling the child file, where that variable should appear is blank.
You can pass a variable to every template by using a Context Processor:
1. Adding the context processor to your settings file
First, you will need to add your custom Context Processor to your settings.py:
# settings.py
TEMPLATE_CONTEXT_PROCESSORS = (
'myapp.context_processors.default', # add this line
'django.core.context_processors.auth',
)
From that you can derive that you will need to create a module called context_processors.py and place it inside your app's folder. You can further see that it will need to declare a function called default (as that's what we included in settings.py), but this is arbitrary. You can choose whichever function name you prefer.
2. Creating the Context Processor
# context_processors.py
from datetime import datetime
from django.conf import settings # this is a good example of extra
# context you might need across templates
def default(request):
# you can declare any variable that you would like and pass
# them as a dictionary to be added to each template's context:
return dict(
example = "This is an example string.",
current_date = datetime.now(),
MEDIA_URL = settings.MEDIA_URL, # just for the sake of example
)
3. Adding the extra context to your views
The final step is to process the additional context using RequestContext() and pass it to the template as a variable. Below is a very simplistic example of the kind of modification to the views.py file that would be required:
# old views.py
def homepage(request):
current_date = datetime.datetime.now()
current_section = 'Temporary Home Page'
return render_to_response("base.html", locals())
def contact(request):
current_date = datetime.datetime.now()
current_section = 'Contact page'
return render_to_response("contact.html", locals())
# new views.py
from django.template import RequestContext
def homepage(request):
current_section = 'Temporary Home Page'
return render_to_response("base.html", locals(),
context_instance=RequestContext(request))
def contact(request):
current_section = 'Contact page'
return render_to_response("contact.html", locals(),
context_instance=RequestContext(request))
So, you can use django.views,generic.simple.direct_to_template instead of render_to_response. It uses RequestContext internaly.
from django.views,generic.simple import direct_to_template
def homepage(request):
return direct_to_template(request,"base.html",{
'current_section':'Temporary Home Page'
})
def contact(request):
return direct_to_template(request,"contact.html",{
'current_section':'Contact Page'
})
Or you can even specify it directly at urls.py such as
urlpatterns = patterns('django.views.generic.simple',
(r'^/home/$','direct_to_template',{
'template':'base.html'
'extra_context':{'current_section':'Temporary Home Page'},
}),
(r'^/contact/$','direct_to_template',{
'template':'contact.html'
'extra_context':{'current_section':'Contact page'},
}),
For django v1.8+ variables returned inside context processor can be accessed.
1. Add the context processor to your TEMPLATES list inside settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'your_app.context_processor_file.func_name', # add this line
],
},
},
]
2. Create new file for context processor and define method for context
context_processor_file.py
def func_name(request):
test_var = "hi, this is a variable from context processor"
return {
"var_for_template" : test_var,
}
3. Now you can get the var_for_template in any templates
for example, add this line inside: base.html
<h1>{{ var_for_template }}</h1>
this will render:
<h1>hi, this is a variable from context processor</h1>
for updating templates to django 1.8+ follow this django doc