Alter Django admin change list title text - python

I'm creating some custom views for the Django admin interface that use the standard change-list as an interim stage. This works fine, apart from the fact the change-list page H1 is 'Select object to change'. 'Change' is not the right verb for the action the user will be undertaking in my custom views.
I have found the django.contrib.admin templates that control the layout of the change-list pages (change_list.html and change_list_results.html) but I cannot find where the title is supplied from. I'm guessing it is a variable passed in by a view someplace?
How can I override this text with something less misleading e.g. 'Select object' instead of 'Select object to change'? I'm OK with changing it across all the change-list views, not just the particular ones I'm trying to customise; but I'd prefer a solution that is an override as opposed to a modification of the django.contrib.admin code if possible.
Update: I have found the view responsible for the change list, it's main.py in django\contrib\admin\views. The variable is self.title on line 69 (Django 1.0). I have acheived the result I am looking for by editing this line
self.title = (self.is_popup and ugettext('Select %s') % force_unicode(self.opts.verbose_name) or ugettext('Select %s to change') % force_unicode(self.opts.verbose_name))
to read
self.title = (self.is_popup and ugettext('Select %s') % force_unicode(self.opts.verbose_name) or ugettext('Select %s') % force_unicode(self.opts.verbose_name))
I'd still be really interested to hear a better way of achieving the same result that doesn't involve hacking the django.contrib.admin code - it looks like there already is an option to have the title the way I'd like it, but I'm not sure how to trigger that?

Not sure if still relevant, but another way to do this would be passing the extra_content for the changelist_view method. For ex:
from django.contrib import admin
class MyCustomAdmin(admin.ModelAdmin):
def changelist_view(self, request, extra_context=None):
extra_context = {'title': 'Change this for a custom title.'}
return super(MyCustomAdmin, self).changelist_view(request, extra_context=extra_context)

For current versions of Django:
class CustomChangeList(django.contrib.admin.views.main.ChangeList):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title = 'My Cool Title'
class MyAdmin(ModelAdmin):
def get_changelist(self, request, **kwargs):
return CustomChangeList

There is already ticket for ChangeList customization: http://code.djangoproject.com/ticket/9749. This will give the ability to change many additional aspects of admin application. Unfortunately there is no clean way to achieve your goals.

You can change the title on "Select ... to change" page with "extra_context" in "changelist_view()" as shown below:
# "admin.py"
from django.contrib import admin
from .models import Person
#admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
def changelist_view(self, request, extra_context=None):
extra_context = {'title': 'This is a custom title.'} # Here
return super().changelist_view(request, extra_context=extra_context)
This is how the title on "Select Person to change" page looks like as shown below:

You can override the method and pass it the title in extra_content, see:
def change_view(self, request, object_id, form_url='', extra_context=None):
extra_context = {'title': 'Hello Title'}
return super(BlogAdmin, self).change_view(request, object_id,
form_url, extra_context=extra_context)

As of Django 3.1.7
I think the OP is asking about the changelist "content" title (the one shown on the page below breadcrumbs, not in the browser tab title). Django sets it from the model's verbose_name_plural (set in model class' class Meta). If it is not explicitly set, Django uses the model class name with 's' suffixed. Here is the code from Django admin change_list.html:
<!-- CONTENT-TITLE -->
{% block content_title %}
<h1>{{ cl.opts.verbose_name_plural|capfirst }}</h1>
{% endblock %}
So if just setting the verbose_name_plural does not suffice/work for you, consider overriding the change_list.html template and do your thing in the {% block content_title %}. If it is too complicated to do in the template, you can pass your own context data to the admin template as given in the SO answer here:
Django how to pass custom variables to context to use in custom admin template?

Related

Add argument to all views without having to edit them all to include variable in Django

What I need
I'm developing a Pull Notification System to an existing Django Project. With there begin over 100+ views I'm looking to find a way to incorporate a argument(notification queryset) into all the views, as this is rendered to my base.html which I can do by passing it into a view's arguments dictionary.
Problem
I want to do this without editing all of the views as this would take a long time and would be required for all future views to include this variable.
What I've tried
Creating a template filter and pass in request.user as variable to return notification for that user. This works, however when the user selects a 'New' notification I want to send a signal back to the server to both redirect them to the notification link and change the status of viewed to 'True' (POST or AJAX). Which would required that specific view to know how to handle that particular request.
What I've considered
• Editing the very core 'View' in Django.
I've tried my best to explain the issue, but if further info is required feel free to ask.
Thanks!
models.py
class NotificationModel(models.Model):
NOTIFICATION_TYPES = {'Red flag':'Red flag'}
NOTIFICATION_TYPES = dict([(key, key) for key, value in NOTIFICATION_TYPES.items()]).items()
notification_type = models.CharField(max_length=50, choices=NOTIFICATION_TYPES, default='')
user = models.ForeignKey(User, on_delete=models.CASCADE)
created = models.DateTimeField(blank=True, null=True)
text = models.CharField(max_length=500, default='')
viewed = models.BooleanField(default=False)
views.py Example
class HomeView(TemplateView):
template_name = 'app/template.html'
def get(self, request, *args, **kwargs):
notifications = NotificationsModel.objects.filter(user=request.user)
args={'notifications':notifications}
return render(request, self.template_name, args)
def notification_handler(self, request)
if 'notification_select' in request.POST:
notification_id = request.POST['notification_id']
notification = NotificationModel.objects.get(id=notification_id)
notification.viewed = True
notification.save()
return redirect #some_url based on conditions
def post(self, request, *args, **kwargs):
notifications = self.notification_handler(request, *args, **kwargs)
if notifications:
return notifications
return self.get(self, request)
With there begin over 100+ views I'm looking to find a way to incorporate a argument(notification queryset) into all the views, as this is rendered to my base.html which I can do by passing it into a view's arguments dictionary.
You don't have to put it in the views (and actually, you shouldn't - views shouldn't have to deal with unrelated responsabilities) - you can just write a simple custom template tag to fetch and render whatever you need in your base template. That's the proper "django-esque" solution FWIW as it leaves your views totally decoupled from this feature.
Have you considered mixins ?
class NotificationMixin:
my_var = 'whatever'
class MyDjangoView(NotificationMixin,.....,View)
pass
Better, Using django builtins..
from django.views.generic import base
class NotificationMixin(base.ContextMixin):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['notification'] = 'whatever'
return context
and use this mixin with all your views AS THE FIRST CLASS INHERITED, See this.
For your case, Typing it instead of applying it to the base class is better, The base view class is not intended to be altered, It's meant to be extended, That's why we can solve the issue by this...
from django.generic.views import View as DjangoBaseView
from .mixins import NotificationMixin
class View(NotificationMixin, DjangoBaseView):
pass
and use any IDE to change all the imported Views to your View and not the django one.

Is there a 'DetailView' in Django Admin?

I know there is a change/update view in Django admin but is there any detail view that just lists out the record's attributes? Kind of like the DetailView in the Django app?
Or does anyone know any 3rd party packages I can install to provide the same functionality?
I too was investigating this recently.
One approach that works is to create a custom ModelAdmin with a detail_view method that forwards the call to ModelAdmin's changeform_view() method. Then this view is added to the urls list via overriding ModelAdmin.get_urls().
Then, in this method set a class attribute, say __detail_view to True. Then override has_change_permission() method, which returns False, if __detail_view is detected and set to True. This will cause AdminSite to render the fields in readonly mode (using the AdminReadonlyField wrapper fields) instead of the standard AdminField objects.
You can also change the change_form_template to a custom template for detail_view to accommodate custom rendering for detail views.
class CustomModelAdmin(admin.ModelAdmin):
def has_change_permission(self, request, obj=None):
if getattr(self, '__detail_view', None):
return False
return super().has_change_permission(request, obj)
def detail_view(self, request, object_id, form_url='', extra_context=None):
setattr(self, '__detail_view', True)
# Custom template for detail view
org_change_form_template = self.change_form_template
self.change_form_template = self.detail_view_template or self.change_form_template
ret = self.changeform_view(request, object_id, form_url, extra_context)
self.change_form_template = org_change_form_template
delattr(self, '__detail_view')
return ret
def get_urls(self):
urls = super().get_urls()
# add detail-view for the object
from django.urls import path
def wrap(view):
def wrapper(*args, **kwargs):
return self.admin_site.admin_view(view)(*args, **kwargs)
wrapper.model_admin = self
return update_wrapper(wrapper, view)
info = self.model._meta.app_label, self.model._meta.model_name
# Replace the backwards compatibility (Django<1.9) change view
# for the detail view.
urls[len(urls)-1] = path('<path:object_id>/', wrap(self.detail_view), name='%s_%s_detail' % info)
return urls
I haven't tried the custom template approach, but using the __detail_view object attribute to force readonly rending seems to work.
The default change_form_template still shows the delete button at the bottom, which is okay I guess. But it needs another button to actually take you to the real change page where the object can be changed. Again template customization is the way to go here. Hint: look at {% submit_row %} in admin templates and model a custom inclusion template tag that displays the Change button, if the user has change permission. Take note to call the has_change_permission() here to get the real permission before setting the __detail_view attribute.
Not sure if there are other implications for doing it this way, but it ought to work.
HTH

Django: efficient template/string separation and override

I have a generic Django view that renders a template. The template is in an app which other projects will use. Importing projects will typically subclass the View the app provides. The View has a default template, which does a job with generic wording.
99% of the time, subclassing Views will want to only change the text, so rather than make them duplicate the template for the sake of altering non-markup wording, i'm looking for a way to allow users of the class to replace wording in the template in the most efficient way.
Options explored so far:
template partials containing only the text which using apps can override (magic, a lot of user work)
A template_strings method on the view which provides a dict of strings which end up in the template context which subclasses can override
Using (abusing?) the translation system such that the app provides default english translations and using code can provide their own translations instead (not actually worked this one out yet, just an idea)
Doing the above template_strings through AppConfig, but this seems ... yucky like it may get very unweildy with a lot of English strings. If doing this I would create a context-like setup so you don't have to re-declare all strings
Seems like it should be a solved problem to subclass a view which does a complete job and just provide alternate strings for text. Is there a better method than the above? Convention? Something I am missing?
(django 1.11 Python 3.6.2)
You can either inherit TemplateView or add ContextMixin to your view, and then override the get_context_data function like this:
from django.views.generic import TemplateView
class BaseView(TemplateView):
template_name = "common.html"
class SubView(BaseView):
def get_context_data(self, **kwargs):
context = super(SubView, self).get_context_data(**kwargs)
context['content'] = "Some sub view text"
return context
Update: Use template overriding
If you want to separate the text out, this is the better way to go
To allow easily and DRY override template across apps, you might need to install this package (Some other detail here)
We define it similarly as above, but change the template_name instead:
from django.views.generic import TemplateView
class BaseView(TemplateView):
template_name = "main.html"
# on another app
class SubView(BaseView):
template_name = "sub_view.html"
Then the magic is you can extends and override block of the BaseView template like this:
base_app/templates/main.html
<p>I'm Common Text</p>
{% block main %}
<p>I'm Base View</p>
{% endblock %}
sub_app/templates/sub_view.html
{% extends "base_app:main.html" %}
{% block main %}
<p>I'm Sub View</p>
{% endblock %}
The result would be:
<p>I'm Common Text</p>
<p>I'm Sub View</p>
Afaik you covered the options pretty well. My example is probably just a variant of the the template strings but maybe it helps anyway...
class DefaultStringProvider():
TITLE = 'Hello'
DESCRIPTION = 'Original description'
CATEGORY = 'Stuff'
class MyTemplateBaseView(TemplateView):
def get_context_data(self, **kwargs):
return super(MyTemplateBaseView, self).get_context_data(
provider=self.get_string_provider(), **kwargs)
def get_string_provider(self):
return DefaultStringProvider()
class OtherView(MyTemplateBaseView):
template_name = 'welcome.html'
def get_string_provider(self):
p = DefaultStringProvider()
p.TITLE = 'Hello'
p.DESCRIPTION = 'New description'
return p
The idea is to have a default string provider and the base view populates the context with it through get_string_provider().
It will at least be quite clear which strings can be overridden for a user extending the base class and it will not interfere with translations.

When to use get, get_queryset, get_context_data in Django?

I recently learned that you should override the get method when you specifically want to do something other than what the default view does:
class ExampleView(generic.ListView):
template_name = 'ppm/ppm.html'
def get(self, request):
manager = request.GET.get('manager', None)
if manager:
profiles_set = EmployeeProfile.objects.filter(manager=manager)
else:
profiles_set = EmployeeProfile.objects.all()
context = {
'profiles_set': profiles_set,
'title': 'Employee Profiles'
}
That's simple enough, but when should I use get_queryset or get_context_data over get? To me it seems like they basically do the same thing or am I just missing something? Can I use them together? This is a major source of confusion for me.
So to reiterate: In what cases would I use get over get_queryset or get_context_data and vise versa?
They indeed do different things.
get()
This is a top-level method, and there's one for each HTTP verb - get(), post(), patch(), etc. You would override it when you want to do something before a request is processed by the view, or after. But this is only called when a form view is loaded for the first time, not when the form is submitted. Basic example in the documentation. By default it will just render the configured template and return the HTML.
class MyView(TemplateView):
# ... other methods
def get(self, *args, **kwargs):
print('Processing GET request')
resp = super().get(*args, **kwargs)
print('Finished processing GET request')
return resp
get_queryset()
Used by ListViews - it determines the list of objects that you want to display. By default, it will just give you all for the model you specify. By overriding this method you can extend or completely replace this logic. Django documentation on the subject.
class FilteredAuthorView(ListView):
template_name = 'authors.html'
model = Author
def get_queryset(self):
# original qs
qs = super().get_queryset()
# filter by a variable captured from url, for example
return qs.filter(name__startswith=self.kwargs['name'])
get_context_data()
This method is used to populate a dictionary to use as the template context. For example, ListViews will populate the result from get_queryset() as author_list in the above example. You will probably be overriding this method most often to add things to display in your templates.
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
data['page_title'] = 'Authors'
return data
And then in your template, you can reference these variables.
<h1>{{ page_title }}</h1>
<ul>
{% for author in author_list %}
<li>{{ author.name }}</li>
{% endfor %}
</ul>
Now to answer your main question, the reason you have so many methods is to let you easily stick your custom logic with pin-point accuracy. It not only allows your code to be more readable and modular, but also more testable.
The documentation should explain everything. If still not enough, you may find the sources helpful as well. You'll see how everything is implemented with mixins which are only possible because everything is compartmentalized.
Let's look at the default implementation of ListView's get method:
https://github.com/django/django/blob/92053acbb9160862c3e743a99ed8ccff8d4f8fd6/django/views/generic/list.py#L158
class BaseListView(MultipleObjectMixin, View):
"""
A base view for displaying a list of objects.
"""
def get(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
allow_empty = self.get_allow_empty()
if not allow_empty:
# When pagination is enabled and object_list is a queryset,
# it's better to do a cheap query than to load the unpaginated
# queryset in memory.
if (self.get_paginate_by(self.object_list) is not None
and hasattr(self.object_list, 'exists')):
is_empty = not self.object_list.exists()
else:
is_empty = len(self.object_list) == 0
if is_empty:
raise Http404(_("Empty list and '%(class_name)s.allow_empty' is False.")
% {'class_name': self.__class__.__name__})
context = self.get_context_data()
return self.render_to_response(context)
You will notice that get_queryset gets called in the first line. You can simply overwrite that if you just want to return your model's queryset after applying some filtering/ordering etc.
You don't need to overwrite the whole get method for that because you will be missing on all this provided functionality i.e. pagination, 404 checks etc.
get_context_data merges the resulting queryset together with context data like querystring parameters for pagination etc.
What I would recommend would be to check with django's source every once in a while and try to understand it a little bit so that you can recognize the most appropriate method you can overwrite/replace.

When I use the Django generic How can I Bring two models

I have two models : Advertisment and Banner
when I using "generic view" How can I Bring together at the same time
The code below bring only one Advertisment
urlpatterns = patterns('',
url(r'^(?P<pk>\d+)/$', DetailView.as_view(
model = Advertisment,
context_object_name = 'advertisment',
), name='cars-advertisment-detail'),
url(r'^$', SearchView.as_view(), name='cars-advertisment-search'),
)
Aidan's answer is good if you only want to do it for a single view, but if you want to show banners on each page automatically, you have two main options.
One is to create a template tag that renders the banner, and add this tag to your templates where you want banners to be shown.
Your tag could look like this:
#register.inclusion_tag('banner.html')
def banner_display():
random_banner = Banner.objects.order_by('?')[0]
return {'the_banner': random_banner}
Then, you would create a template that shows the banner:
<img src="{{ the_banner.url|safe }}" />
In your templates, where you need the banner, you would just say {% banner_display %}
The other option you have is to create a custom template context processor. This will inject your banner as a normal variable in all requests. This is perhaps even simpler:
def banner_display(request):
random_banner = Banner.objects.order_by('?')[0]
return {'the_banner': random_banner}
You should save this in a file and then add it to your TEMPLATE_CONTEXT_PROCESSORS setting. Now in every template you have a variable {{ the_banner }}.
You need to override the get_context_data() method of the class based view (as described in the docs).
from django.views.generic import DetailView
class YourDetailView(DetailView):
model = Advertisment
context_object_name = 'advertisment'
def get_context_data(self, *args, **kwargs):
context = super(YourDetailView, self).get_context_data(*args, **kwargs)
if 'banner_id' in self.kwargs:
context['banner'] = get_object_or_404(Banner, pk=self.kwargs['banner_id']
return context
I guess you'll need to update your url conf to include a primary key for the Banner model too.
from your_app.views import YourDetailView
url(r'^(?P<ad_pk>\d+)/(?P<banner_pk>\d+)/$', YourDetailView.as_view(), name='cars-advertisment-detail'),

Categories