I've been asked to add in the admin site of a Django project a new section that will gather information from several models (as it were a Database view) but I'm not allowed to change or add tables/views in the db.
Checking similar questions custom page for Django admin in SO, I've ended up trying to create a "fake" model that won't be managed by Django and adding custom urls in get_urls method.
Let code explain itself:
core/admin.py
class ConfigurationOverview(Model):
aa = ForeignKey(ModelA, on_delete=DO_NOTHING)
bb = ForeignKey(ModelB, on_delete=DO_NOTHING)
cc = ForeignKey(ModelC, on_delete=DO_NOTHING)
class Meta:
# Django won't consider this model
managed = False
# link to the index page at /admin
verbose_name = 'Configuration overview'
app_label = 'core'
#staticmethod
def all():
# gather info from ModelA, ModelB, ModelC and create a collection of ConfigurationOverviews
return []
#register(ConfigurationOverview)
class ConfigurationOverviewAdmin(ModelAdmin):
def get_urls(self):
urls = super(ConfigurationOverviewAdmin, self).get_urls()
my_urls = [
url(
r'^$', # /admin/core/configurationoverview/
self.admin_site.admin_view(self.list_view),
name='core_configurationoverview_list'
)
]
return my_urls + urls
def list_view(self, request):
context = {
'configuration_overviews': ConfigurationOverview.all(),
}
return render(request,
"admin/core/configurationoverview/change_list.html",
context)
templates/admin/core/configurationoverview/change_list.html
{% extends "admin/change_list.html" %}
{% block content %}
AAAA
{% endblock %}
But when accesing /admin/core/configurationoverview/ I'm getting
NoReverseMatch at /admin/core/configurationoverview/
Reverse for 'app_list' with keyword arguments '{'app_label': ''}' not found. 1
but I've defined app_label: core! any hint?
* EDIT *
This is the empty migration I ran:
class Migration(migrations.Migration):
dependencies = [...]
operations = [
migrations.CreateModel(
name='ConfigurationOverview',
fields=[],
options={
'managed': False,
'verbose_name': 'Configuration overview'
},
),
]
You can try to add a regular view and require the user to be a staff member.
views.py
from django.contrib.admin.views.decorators import staff_member_required
#staff_member_required
def configuration_overview(request):
aa = ModelA.objects.all() # customize this queryset if neccesary, paginate ...
bb = ModelB.objects.all() # customize this queryset if neccesary, paginate ...
cc = ModelC.objects.all() # customize this queryset if neccesary, paginate ...
return render(request, 'admin/core/configurationoverview/change_list.html', context={'aa': aa, 'bb': bb, 'cc': cc})
urls.py
urlpatterns = [
###
path('admin/configuration', views.configuration_overview) # customize the path you want
###
]
Related
Urls.py:
app_name = 'main'
urlpatterns = [
path('',include(router.urls)),
path('player_id=<str:player>%season_id=<str:season>',views.MatchesList.as_view())
]
Views.py
class MatchesList(generics.ListAPIView):
serializer_class = MatchesSerializer
permissions = (IsAuthenticated)
def get_queryset(self):
player = self.kwargs['player']
season = self.kwargs['season']
if season is None:
queryset = Matches.objects.filter(player=player).all()
else:
queryset = Matches.objects.filter(player=player,season=season).all()
return queryset
Is there any way to request without the parameter 'season'? Something like that:
app_name = 'main'
urlpatterns = [
path('',include(router.urls)),
path('player_id=<str:player>',views.MatchesList.as_view())
]
Sure. In DRF you can/should use filtering for this.
In urls.py you'll have something like this:
path('matches/<str:player_id>/', views.MatchesList.as_view())
And your URL will look like:
https://yourhost.com/matches/42/?season=spring
For season you'll have to implement a filter (there is a bunch of ways to do it). Filters are optional — if you'll not pass the ?season=something part of the URL, it just will not be applied.
I'm trying to pass an abstract model to an inclusion tag like via takes_context=True. The abstract model contains choices for a model field. I want to pass the abstract model instead of hardcoding the choices in the template to stay DRY. While debugging I realized that the template isn't receiving the model as expected.
# urls.py
...
urlpatterns = [
path('', IndexView.as_view(), name='index'),
]
# views.py
...
class IndexView(TemplateView):
"""Home Page"""
template_name = 'index.html'
def get_context_data(self, **kwargs):
kwargs['model'] = MyModel
return super(IndexView, self).get_context_data(**kwargs)
...
# index.html
{{model}}
The above renders nothing in the browser. When I change the variable to a string, the context renders as expected.
# views.py
...
class IndexView(BaseSearchBarMixin, TemplateView):
"""Home Page"""
template_name = 'index.html'
def get_context_data(self, **kwargs):
kwargs['model'] = 'testing 123'
return super(IndexView, self).get_context_data(**kwargs)
...
# index.html
{{model}} # fixed typo
# browser
testing 123
I have a feeling I'm doing something stupid but don't know what
EDIT:
Per the accepted answer, passing classes to templates isn't possible. Since the class I wanted to pass is an abstract model, there are cases where MyModel.objects.first() could return an empty queryset. I ended up making a custom ContextMixin that added the choices to my class based views.
# myapp.models.users.py
class MyModel(models.Model):
"""shared fields and functions for MyModel models"""
class Meta:
abstract = True
DOWN_VOTE = 'DOWN'
UP_VOTE = 'UP'
VOTE_CHOICES = [
(DOWN_VOTE, 'Down vote'),
(UP_VOTE, 'Up vote'),
]
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
vote = models.CharField(choices=VOTE_CHOICES, default=UP_VOTE, max_length=255)
# views.py
...
class MyModelChoicesContextMixin(ContextMixin):
"""add choices from abstract model to context"""
def get_context_data(self, **kwargs):
"""add choices from abstract model to context"""
context = super(MyModelChoicesContextMixin, self).get_context_data(**kwargs)
context['user_DOWN_vote'] = MyModel.DOWN_VOTE
context['user_UP_vote'] = MyModel.UP_VOTE
return context
class IndexView(MyModelChoicesContextMixin, BaseSearchBarMixin, TemplateView):
"""Home Page"""
template_name = 'index.html'
You are passing in a class, not an instance of the class or a queryset, try:
kwargs['model'] = MyModel.objects.first()
(for example - to get the first).
You can't use the class in the template.
In kwargs you are passing keyword 'model' but in template you are using 'Mymodel' keyword that's why empty template is showing.
2nd thing display model fields like model.field_name.
I have been going through "Tango with Django" and have been unable to solve this problem myself or by looking online. Would anyone know how to approach it?
The relevant page should be opened when I click on the link, but none are going through, which makes me assume something in my view.py file is wrong or even in my url.py or model.py file (index.html seems to be working correctly).
Views.py
# Create your views here.
from django.http import HttpResponse
from django.shortcuts import render
from Spaces.models import Category, Page
def index(request):
# Query the databse for a list of ALL categories currently stored.
# Order the categories by no likes in descending order .
# Retrieve the top 5 only - or all if less than 5.
# Place the list in context_dict dictionary
# that will be passed to the template engine.
category_list = Category.objects.order_by('-likes')[:5]
context_dict = {'categories': category_list}
# Render the response and send it back!
return render(request, 'Spaces/index.html', context=context_dict)
def about(request):
context_dict = {'boldmessage':"Crunchy, creamy, cookie, candy, cupcake!"}
return render(request, 'Spaces/about.html', context=context_dict)
def show_category(request, category_name_slug):
# Create a context dictionary which we can pass
# to the template rendering engine.
context_dict = {}
try:
# Can we find a category name slug with the given name?
# If we can't, the .get() method raises a DoesNotExist exception.
# So the .get() method returns one model instance or raises an exception.
category = Category.objects.get(slug=category_name_slug)
# Retrieve all of the associated pages.
# Note that filter() will return a list of page objects or an empty list
pages = Page.objects.filter(category=category)
# Adds our results list to the template context under name pages.
context_dict['pages'] = pages
# We also add the category object from
# the database to the context dictionary.
# We'll use this in the template to verify that the category exists.
context_dict['category'] = category
except Category.DoesNotExist:
# We get here if we didn't find the specified category.
# Don't do anything -
# the template will display the "no category" message for us.
context_dict['category'] = None
context_dict['pages'] = None
# Go render the response and return it to the client.
return render(request, 'Spaces/category.html', context_dict)
Urls.py
from django.conf.urls import url
from Spaces import views
urlpatterns = [
url(r'^$', views.index, name='index'),
url(r'^about/$', views.about, name='about'),
url(r'^category/(?P<category_name_slug>[\w\-]+)/$',
views.show_category, name='show_category'),
]
models.py
from django.db import models
from django.template.defaultfilters import slugify
class Category(models.Model):
name = models.CharField(max_length=128, unique=True)
views = models.IntegerField(default=0)
likes = models.IntegerField(default=0)
slug = models.SlugField(unique=True, blank=True, null=True)
def save(self, *args, **kwargs):
self.slug = slugify(self.name)
super(Category, self).save(*args, **kwargs)
class Meta:
verbose_name_plural = 'categories'
def __str__(self):
return self.name
class Page(models.Model):
category = models.ForeignKey(Category, on_delete=models.PROTECT)
title = models.CharField(max_length=128)
url = models.URLField()
views = models.IntegerField(default=0)
def __str__(self): # For Python 2, use __unicode__ too
return self.title
index.html
<!DOCTYPE html>
{% load staticfiles %}
<html>
<head>
<title>Spaces</title>
</head>
<body>
<h1>Spaces says...</h1>
<div>hey there partner!</div>
<div>
{% if categories %}
<ul>
{% for category in categories %}
<li>
{{ category.name }}
</li>
{% endfor %}
</ul>
{% else %}
<strong>There are no categories present.</strong>
{% endif %}
</div>
<div>
About Space<br />
<img src="{% static 'images/Spaces.jpg' %}" alt="Picture of Rango" />
</div>
</body>
</html>
populate_spaces.py (test script)
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE',
'Space.settings')
import django
django.setup()
from Spaces.models import Category, Page
def populate():
#First, we will create lists of dictionaries containing the pages
# we want to add into each category.
# Then we will create a dictionary of dictionaries for our categories.
# This might seem a little bit confusing, but it allows us to iterate
# through each data structure, and add the data to our models.
python_pages = [
{"title": "Prahran",
"url":"http://docs.python.org/2/tutorial/", "views":20},
{"title": "South Yarra",
"url":"http://docs.python.org/2/tutorial/", "views":25},
{"title": "etcetera",
"url":"http://docs.python.org/2/tutorial/", "views":35}
]
django_pages = [
{"title" : "Official Django Tutorial",
"url" :"https://docs.djangoproject.com/en/1.9/intro/tutorial01/", "views":36},
{"title":"Django Rocks",
"url":"http://www.djangorocks.com/", "views":23},
{"title":"How to Tango with Django",
"url":"http://www.tangowithdjango.com/", "views":45}
]
other_pages = [
{"title":"Bottle",
"url":"http://bottlepy.org/docs/dev/", "views":3},
{"title":"Flask",
"url":"http://flask.pocoo.org",
"views":34}]
cats = {"Python": {"pages": python_pages, "views": 128, "likes":64},
"Django": {"pages": django_pages, "views": 64, "likes":32},
"Other Frameworks": {"pages": other_pages, "views": 32, "likes":16} }
# If you want to add more categories or pages,
# Add them to the dictionaries above.
# The code below goes through the cats dictionary, then adds each category
# and then adds all the associated pages for that category.
for cat, cat_data in cats.items():
c = add_cat(cat,cat_data)
for p in cat_data["pages"]:
add_page(c, p["title"], p["url"], p["views"])
#Print out the categories we have added.
for c in Category.objects.all():
for p in Page.objects.filter(category=c):
print("-{0})-{1}".format(str(c), str(p)))
def add_page(cat, title, url, views=0):
p = Page.objects.get_or_create(category=cat, title=title)[0]
p.url=url
p.views=views
p.save()
return p
def add_cat(name, cat_data):
c = Category.objects.get_or_create(name=name)[0]
c.likes = cat_data["likes"]
c.views = cat_data["views"]
c.save()
return c
# Start execution here!
if __name__ == '__main__':
print("Starting Spaces population script...")
populate()
I fixed the issue.
Essentially I had indented a return function in my view.py file incorrectly!
sorry if this question has been answered before, but I did a lot of googling without success.
I know how to create custom list_filters in admin views (e.g. subclassing SimpleFilter).
What I really would like, is a way (on the admin list view) to "check" different filters that combines them in a OR formula.
As an example, suppose you have:
# models.py
class Foo(models.Model):
foobar = ...
foofie = ...
...
# admin.py
class FooAdmin(admin.ModelAdmin):
list_filter = ( "foobar", "foofie" )
...
In the admin list view generated by FooAdmin I can choose to filter records either by foobar or by foofie. Is there a way to filter them by the formula: foobar = X OR foofie = Y, where X and Y are two values that foobar and foofie can assume?
Is it even possible?
I know not everything is possible in the django admin views, but it seems a very common request, I wonder if I missed to understand or read something.
Also third party apps allowing it are welcome. Thanks :)
I found a third party app just now, it is django-advanced-filters which may fit you requirement .
It has:
The OR field
OR is an additional field that is added to every rule's available
fields.
It allows constructing queries with OR statements. You can use it by
creating an "empty" rule with this field "between" a set of 1 or more
rules.
I have run a test, add a OR field would work.
This is the screenshot:
Firstly I try to explain the working of django admin filters. When you want to filter your queryset in admin page django looks for all registered filters. If you set value for filter django filter queryset with this value. If you set more than one filter django filter your queryset twice, this equal to queryset = queryset.filter(param1=1).filter(param2=2) or in SQL: SELECT ... WHERE param1=1 AND param2=2. It because you can't do it with standard django's filters. But you can write your own filter like this:
from django.contrib.admin import SimpleListFilter
from django.db.models import Q
from functools import reduce
import operator
from django.core.exceptions import FieldError
class ORListFilter(SimpleListFilter):
title = ''
parameter_name = ''
search_field = ('',)
def queryset(self, request, queryset):
filters = request.GET.copy()
try: #for search
search_field_value = filters.pop('q')[0]
query_params = [Q((key, search_field_value)) for key in self.search_field]
try:
queryset = queryset.filter(reduce(operator.or_, query_params))
except FieldError:
pass
except KeyError:
pass
try:
query_params = [Q((key, value)) for key, value in filters.dict().items()]
queryset = queryset.filter(reduce(operator.or_, query_params))
except TypeError:
pass
return queryset
def lookups(self, request, model_admin):
qs = model_admin.get_queryset(request)
parameters = qs.all().values(self.parameter_name).distinct()
for parameter in parameters:
value = dict(parameter).pop(self.parameter_name, None)
if value:
yield (value, value)
else:
yield (None, 'NULL')
class Field1Filter(ORListFilter):
title = 'title'
parameter_name = 'field1'
search_field = ('search1', 'search2')
class Field2Filter(ORListFilter):
title = 'title'
parameter_name = 'field2'
search_field = ('search1', 'search2')
And register it in admin:
search_fields = ('search1', 'search2')
list_filter = (Field1Filter, Field2Filter)
It doesn't work with standard django's filters and all values in list_filter must inherited from ORListFilter class. Also it doesn't work with datetime filters, but you can add this ability.
Figured out a solution:
import operator
from functools import reduce
from django.contrib.admin import ListFilter, FieldListFilter
from django.db.models import Q
from django.contrib.admin.utils import (
get_fields_from_path, lookup_needs_distinct, prepare_lookup_value,
)
from django.http import QueryDict
class OrListFilter(ListFilter):
parameter_prefix = None
fields = None
def __init__(self, request, params, model, model_admin):
super(OrListFilter, self).__init__(
request, params, model, model_admin)
if self.parameter_prefix is None:
raise ImproperlyConfigured(
"The list filter '%s' does not specify "
"a 'parameter_prefix'." % self.__class__.__name__)
self.model_admin = model_admin
self.model = model
self.request = request
self.filter_specs = self.get_filters(request, {}, prefix=self.parameter_prefix+'-')
for p in self.expected_parameters():
if p in params:
value = params.pop(p)
field = p.split('-')[1]
self.used_parameters[field] = prepare_lookup_value(field, value)
def has_output(self):
return True
# see https://github.com/django/django/blob/1.8.5/django/contrib/admin/views/main.py#L104
def get_filters(self, request, params, prefix=''):
filter_specs = []
for field_path in self.fields:
field = get_fields_from_path(self.model, field_path)[-1]
field_list_filter_class = FieldListFilter.create
spec = field_list_filter_class(field, request, params,
self.model, self.model_admin, field_path=prefix + field_path)
# Check if we need to use distinct()
# use_distinct = (use_distinct or
# lookup_needs_distinct(self.lookup_opts,
# field_path))
filter_specs.append(spec)
return filter_specs
def expected_parameters(self):
parameters = []
for spec in self.filter_specs:
parameters += spec.expected_parameters()
return parameters
def choices(self, cl):
return []
def queryset(self, request, queryset):
origin_GET = request.GET.copy()
fake_GET = QueryDict(mutable=True)
fake_GET.update(self.used_parameters)
request.GET = fake_GET
all_params = {}
for spec in self.get_filters(request, self.used_parameters):
if spec and spec.has_output():
all_params.update(spec.used_parameters)
try:
query_params = [Q((key, value)) for key, value in all_params.items()]
queryset = queryset.filter(reduce(operator.or_, query_params))
except TypeError as e:
pass
# restore
request.GET = origin_GET
return queryset
class OrFilter(OrListFilter):
title = 'Or filter'
parameter_prefix = 'or1'
fields = ("foobar", "foofie")
class FooAdmin(admin.ModelAdmin):
list_filter = (OrFilter, )
app_name/templates/admin/app_name/change_list.html:
{% extends "admin/change_list.html" %}
{% load i18n admin_list %}
{% block filters %}
{% if cl.has_filters %}
<div id="changelist-filter">
<h2>{% trans 'Filter' %}</h2>
{% for spec in cl.filter_specs %}
{% if spec.filter_specs %}
{% admin_list_filter cl spec %}
<ul>
{% for sub_spec in spec.filter_specs %}
<li>{% admin_list_filter cl sub_spec %}</li>
{% endfor %}
</ul>
{% else %}
{% admin_list_filter cl spec %}
{% endif %}
{% endfor %}
</div>
{% endif %}
{% endblock %}
Borrowed some code from #dima-kudosh.
Explanation
ChangeList.get_filters() creates ListFilters (filter_specs) from ModelAdmin.list_filter, then uses ListFilter.queryset() to get_queryset().
FieldListFilter.queryset() uses used_parameters to filter queryset: queryset.filter(**self.used_parameters).
So we can create FieldListFilters from OrListFilter.fields and use their used_parameters to construct OR queries:
all_params = {}
for spec in self.get_filters(request, self.used_parameters):
if spec and spec.has_output():
all_params.update(spec.used_parameters)
try:
query_params = [Q((key, value)) for key, value in all_params.items()]
queryset = queryset.filter(reduce(operator.or_, query_params))
except TypeError as e:
pass
Django Admin Multiple Choice List Filter is a Django app that I wrote to fulfil this requirement, after searching through many posts like this one.
MultipleChoiceListFilter extends SimpleListFilter to allow you to filter on multiple options.
The UI uses clickable links to 'include' and 'exclude' choices from the 'OR' query, rather than ticking/unticking a checkbox. Thus, you have to wait for a round trip to the server, and for the page to refresh, after each click. This could be a performance/UX issue, especially for large numbers of objects.
The behaviour of the 'All' link, and of each choice link, is preserved from the SimpleListFilter - i.e. you can reset the filter to all, or just one, of the choices.
Currently included choices are highlighted in the filter (in blue in the screenshot below).
The template is overridable so you change the interface to suit your needs. Personally I think a bit more space between the choice name and the include/exclude link might help to differentiate the two. Or perhaps a switch icon would be more intuitive than the word 'include'/'exclude'.
I'm fairly new to python and I'm trying to build a URL pattern that takes two fields as parameters. This is part of my model:
CATEGORY_CHOICES = (
('M', 'Music'),
('G', 'Games'),
('T', 'TV'),
('F', 'Film'),
('O', 'Misc'),
)
category = models.CharField(max_length = 1, choices = CATEGORY_CHOICES)
slug = models.SlugField(unique=True, max_length=255)
What I want to achieve is to be able to call a such as: thisblog.com/music/this-post where /music is the category and /this-post is the slug.
I had an attempt but I can't figure out how to do it properly:
urlpatterns = patterns('',
url(r'^admin/', include(admin.site.urls)),
url(r'^$', 'blog.views.index'),
url(r'^(?P<category>[\w\-]+)/(?P<slug>[\w\-]+)/$', blog_post, name = 'blog_post'),
)
This gives me a 'NoReverseMatch at /' error.
Any help is greatly appreciated :)
UPDATE:
In my html template I have a link:
<p><a class="btn btn-default" href="{{post.get_absolute_url}}" role="button">Read more »</a></p>
get_absolute_url is defined as:
def get_absolute_url(self):
return reverse ('blog.views.post', args = [self.slug])
You have two errors in your get_absolute_url method. Firstly, you need to use the URL's name attribute since you have defined it. And secondly, you need to provide both parameters: category as well as slug.
So it should be:
return reverse('blog_post', args=[self.category, self.slug])
If you are using class based views then something like this would work:
# views.py
class ViewPost(DetailView):
model = Post # change this to the model
def get_queryset(self):
queryset = super(ViewQuizListByCategory, self).get_queryset()
return queryset.filter(category=self.kwargs['category'])
# urls.py
from .views import ViewPost
...
url(r'^(?P<category>[\w\-]+)/(?P<slug>[\w\-]+)/$',
ViewPost.as_view(),
name='blog_post'),
...
The template will be post_detail.html and would be placed as <project_root>/post/templates/post/post_detail.html