how to customize sent emails in djoser? - django

I build React&Django project and use Djoser for registration and auth. I want to customize sent email content on Gmail. Where should I change it?

In your django directory create an app or in existing app <my_app>
create a file
email.py
then in <my_app> directory create template folder like:
templates/<my_app>/
within the directory <my_app> create two html files:
ActivateEmail.html
ConfirmationEmail.html
Then in ActivationEmail.html add this code below:
{% load i18n %}
{% block subject %}
{% blocktrans %}Account activation on {{ site_name }}{% endblocktrans %}
{% endblock subject %}
{% block text_body %}
{% blocktrans %}You're receiving this email because you need to finish activation process on {{ site_name }}.{% endblocktrans %}
{% trans "Please go to the following page to activate account:" %}
{{ protocol }}://{{ domain }}/{{ url|safe }}
{% trans "Thanks for using our site!" %}
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
{% endblock text_body %}
{% block html_body %}
<p>{% blocktrans %}You're receiving this email because you need to finish activation process on {{ site_name }}.{% endblocktrans %}</p>
<p>{% trans "Please go to the following page to activate account:" %}</p>
<p>{{ protocol }}://{{ domain }}/{{ url|safe }}</p>
<p>{% trans "Thanks for using our site!" %}</p>
<p>{% blocktrans %}The {{ site_name }} team{% endblocktrans %}</p>
{% endblock html_body %}
And in your ConfirmationEmail.html add this code:
{% load i18n %}
{% block subject %}
{% blocktrans %}{{ site_name }} - Your account has been successfully created and activated!{% endblocktrans %}
{% endblock %}
{% block text_body %}
{% trans "Your account has been created and is ready to use!" %}
{% trans "Thanks for using our site!" %}
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
{% endblock text_body %}
{% block html_body %}
<p>{% trans "Your account has been created and is ready to use!" %}</p>
<p>{% trans "Thanks for using our site!" %}</p>
<p>{% blocktrans %}The {{ site_name }} team{% endblocktrans %}</p>
{% endblock html_body %}
Then email.py within your app <my_app> add this code below:
from djoser import email
from djoser import utils
from djoser.conf import settings
from django.contrib.auth.tokens import default_token_generator
class ActivationEmail(email.ActivationEmail):
template_name = '<my_app>/ActivationEmail.html'
def get_context_data(self):
# ActivationEmail can be deleted
context = super().get_context_data()
user = context.get("user")
context["uid"] = utils.encode_uid(user.pk)
context["token"] = default_token_generator.make_token(user)
context["url"] = settings.ACTIVATION_URL.format(**context)
return context
class ConfirmationEmail(email.ConfirmationEmail):
template_name = '<my_app>/ConfirmationEmail.html'
then EDIT
ActivationEmail.html
ConfirmationEmail.html
however you LIKE.
Then the last step is to add The classes in email.py to replace the default templates
in activation/confirmation email.
The defaults are as in docs: djoser docs
Then in Djoser settings in settings.py file:
DJOSER = {
'USER_CREATE_PASSWORD_RETYPE': True,
'SEND_CONFIRMATION_EMAIL': True,
'SEND_ACTIVATION_EMAIL': True,
'ACTIVATION_URL': 'activate/{uid}/{token}',
'HIDE_USERS': True,
'SERIALIZERS': {
'user': 'myapp.serializers.UserCreateSerializer',
'user_create': 'myapp.serializers.UserCreateSerializer',
'current_user': 'myapp.serializers.UserSerializer',
},
(...)
}
Add this:
DJOSER = {
'USER_CREATE_PASSWORD_RETYPE': True,
'SEND_CONFIRMATION_EMAIL': True,
'SEND_ACTIVATION_EMAIL': True,
'ACTIVATION_URL': 'activate/{uid}/{token}',
'HIDE_USERS': True,
'SERIALIZERS': {
'user': 'myapp.serializers.UserCreateSerializer',
'user_create': 'myapp.serializers.UserCreateSerializer',
'current_user': 'myapp.serializers.UserSerializer',
},
'EMAIL': {
'activation': 'myapp.email.ActivationEmail',
'confirmation': 'myapp.email.ConfirmationEmail',
},
(...)
}
Then send Email activation instead of sending default template for user in his/her email, it'll send the ActivationEmail.html template for them
Note that the Djoser uses the django-template-mail for its activation email template.

Related

Django 3: Badly formed URL in password_reset_email.html

I have a Django 3.0.7 app using the default template for sending password reset emails.
For some of our users, the link to reset is badly formed, e.g.:
https://foo.bar/accounts/reset/McU4Mw/5no-9cd52590f0943503=
fa3a/
/>
While it should be:
https://foo.bar/accounts/reset/McU4Mw/5no-9cd52590f0943503fa3a/
This second one is whats actually being sent. But, as I said, some users are receiving the first one.
I assume some of my users' email clients are wrecking the text, but what could I do?
I've considered adding a plaintext url to the email, but it's already plaintext.
EDIT: This is a solution I came up with, and it works. Can anyone think of a reason not to do this?
urls.py
...
path('accounts/reset/<uidb64>/<token>/<str:extra_chars>/', PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
path('accounts/reset/<uidb64>/<token>/', PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
...
EDIT:
Here's the default template:
/site-packages/django/contrib/admin/templates/registration/password_reset_email.html
{% load i18n %}{% autoescape off %}
{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %}
{% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
{% endblock %}
{% trans 'Your username, in case you’ve forgotten:' %} {{ user.get_username }}
{% trans "Thanks for using our site!" %}
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
{% endautoescape %}

Why did the lower built in filter not work in Django template 2.2?

Using Django v2.2 built-in filters for templates.
my template looks like this:
{% load i18n %}{% blocktrans with site_name=current_site.organization.name site_domain=current_site.name %}Hello from {{ site_name }}!
You're receiving this e-mail because your email is subscribed to receiving notification when a Quotation is Pending PO Release.
{{ quote_type }}
{{ quote_type|lower }}
- Quotation: {{ display_quotation_number }}
- PO Date: {{ po_date_display }} (New)
{% endblocktrans %}
{% blocktrans with site_name=current_site.organization.name site_domain=current_site.name %}Thank you for using {{ site_name }}!
{{ site_domain }}{% endblocktrans %}
I can say with certainty that {{ quote_type }} definitely prints something out. {{ quote_type|lower }} prints nothing.
What did I do wrong?
I was certain that the filter is builtin; see https://docs.djangoproject.com/en/2.2/ref/templates/builtins/#lower
UPdate
This is my current workard and it works
extra_context = self.lower_quote_type(extra_context)
quotation.email_people(
recipient_list=recipient_list,
extra_context=extra_context,
)
def lower_quote_type(self, extra_context):
# #makeAnIssue temp workard for #1334 but try to permanently fix it elegantly
if "quote_type" in extra_context:
extra_context["quote_type"] = extra_context["quote_type"].lower()
return extra_context
Then this works
{% load i18n %}{% blocktrans with site_name=current_site.organization.name site_domain=current_site.name %}Hello from {{ site_name }}!
You're receiving this e-mail because your email is subscribed to receiving notification when a Quotation is Pending PO Release.
{{ quote_type }} # this correctly shows up as lower case
- Quotation: {{ display_quotation_number }}
- PO Date: {{ po_date_display }} (New)
{% endblocktrans %}
{% blocktrans with site_name=current_site.organization.name site_domain=current_site.name %}Thank you for using {{ site_name }}!
{{ site_domain }}{% endblocktrans %}

using django authentication password_reset_confirm error

I have an error with password reset. If I attempt to reset using an invalid email I get the password reset sent notice. If I use a valid email I get NoReverseMatch error message
from django.conf.urls import url
from django.contrib import admin
from . import views
from django.contrib.auth import views as auth_views
from django.urls import reverse, reverse_lazy, resolve
# Password URL's ###################################################################################################
url(r'^change-password/$', views.change_password, name='change_password'),
url(
r'^password_reset/$',
auth_views.PasswordResetView.as_view(
template_name="registration/password_reset.html",
email_template_name="registration/password_reset_email.html",
success_url=reverse_lazy("partners:password_reset_done"), # might be required
),
name='password_reset'
),
url(r'^password_reset_done/',
auth_views.PasswordResetDoneView.as_view(
),
name='password_reset_done'
),
url(r'^password_reset_confirm/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$',
auth_views.PasswordResetConfirmView.as_view(
template_name="registration/password_reset_confirm.html",
success_url=reverse_lazy("partners:password_reset_complete"), # might be required
),
name='password_reset_confirm'
),
url(r'^password_reset_complete/$',auth_views.PasswordResetCompleteView.as_view(), name="password_reset_complete"),
]
Please see my screen grabs for my project structure and the error message
project structure
error message received
{% block head %}
<meta charset="UTF-8">
<title>Welcome ! You can login here !</title>
{% endblock head %}
{% block body %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-sm-6 col-md-3">
<h3>Password Reset Email</h3>
<p>Provide your registered email address </p>
{% autoescape off %}
To initiate the password reset process for your {{ user.get_username }} TestSite Account,
click the link below:
{{ protocol }}://{{ domain }}{% url 'partners:password_reset_confirm' uidb64=uid token=token %}
If clicking the link above doesn't work, please copy and paste the URL in a new browser
window instead.
Sincerely,
The AV's BlogTeam
{% endautoescape %}
</div>
</div>
</div>
{% endblock content %}
{% endblock body %}
I am building this in a "partners app" where I have templates/registration with the above password_reset_confirm.html
Password reset email is as below
{% load i18n %}{% autoescape off %}
{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %}
{% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'partners:password_reset_confirm' uidb64=uid token=token %}
{% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}
{% trans "Thanks for using our site!" %}
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
{% endautoescape %}
PLEASE HELP
If you want Django to use the template from your partners app, you need to move partners above django.contrib.admin in your INSTALLED_APPS setting.
I would suggest that you move the password reset urls into a urls.py that does not use the partners namespace. Using {% url 'partners:password_reset_confirm' ... %} will fix this specific error, but there are several other places that you will have to make changes to use the namespace, and I don't think it's worth the effort.

Resetting Password in Django

I have problem in resetting password in Django . After looking at this:Resetting Password, this error still exist....
My error is:
Reverse for 'django.contrib.auth.views.password_reset_confirm' with arguments '()' and keyword arguments '{'uidb36': '1', 'token': '392-804fab6dcec790f0ec6b'}' not found.
Here is my urls.py:
urlpatterns = patterns('lex.views',
url(r'^home/forgotpassword', 'lexrequestpassword', name='lexrequestpassword'),
url(r'^home/resetpassword', 'lexresetpassworddone', name='lexresetpassworddone'),
url(r'^home/reset/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$', 'lexresetpasswordconfirmed', name='lexresetpasswordconfirmed'),
url(r'^home/resetpasswordcompleted', 'lexresetpasswordcompleted', name='lexresetpasswordcompleted'),)
My views.py:
#login_required
def lexrequestpassword(request):
"""
Reset Password
"""
path = reverse('lexresetpassworddone')
return password_reset(request,post_reset_redirect=path)
#login_required
def lexresetpassworddone(request):
"""
Reset Password Done
"""
path = reverse('lexresetpasswordconfirmed')
return password_reset_done(request,template_name=path)
#login_required
def lexresetpasswordconfirmed(request):
"""
Reset Password Confirmed
"""
path = reverse('lexresetpasswordcompleted')
return password_reset_confirm(request,post_reset_redirect=path)
#login_required
def lexresetpasswordcompleted(request):
"""
Reset Password Completed
"""
path = reverse('lexmain')
return password_reset_complete(request,post_reset_redirect=path)
Not sure how to solve this. Need some guidance...
Password Reset
Django has a built-in password reset functions which we are going to use. In this tutorial we will name our app as accounts.
urls.py
from django.conf.urls import patterns, include, url
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
admin.autodiscover()
urlpatterns = patterns('',
url(
r'^admin/',
include(admin.site.urls)
),
#this is the url for password reset authentication
#Don't remove, this is important!
url(
r'^account/',
include('django.contrib.auth.urls')
),
#this is the accounts app main url
url(
r'^accounts/',
include('accounts.urls', namespace="accounts")
),
)+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += staticfiles_urlpatterns()
urls.py
from django.conf.urls import *
urlpatterns = patterns('accounts.views',
url(
r'^forgot-password/$',
'forgot_password',
name="forgot-password"
),
)
views.py
from django.contrib.auth.views import password_reset
from django.shortcuts import render
def forgot_password(request):
if request.method == 'POST':
return password_reset(request,
from_email=request.POST.get('email'))
else:
return render(request, 'forgot_password.html')
Put your base.html in your main templates folder
base.html
<html>
<head>
<title>{% block title %}{% endblock title %}</title>
</head>
<body>
{% block content %}{% endblock content %}
</body>
</html>
put this template in the app templates folder.
forgot_password.html
{% extends "base.html" %}
{% block title %}Forgot Password{% endblock title %}
{% block content %}
<form method="post" action="{% url django.contrib.auth.views.password_reset %}">
{% csrf_token %}
<p>Please enter your email address.
You will receive a link to create a new password via email.</p>
<input type="email" name="email"
placeholder="Your e-mail"><br/>
<button type="submit">Send new password</button>
</form>
{% endblock content %}
settings.py
#add email settings
EMAIL_USE_TLS = True
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = 'user'
EMAIL_HOST_PASSWORD = 'password'
DEFAULT_FROM_EMAIL = 'your email'
Now we will override the password reset templates of the admin. In your main templates folder create registration folder. Inside the registration folder, create these files:
If you want to change the contents of the files. Make sure it is correct or else you will got an errors.
password_reset_complete.html
{% extends "base.html" %}
{% block title %}Password reset complete{% endblock title %}
{% block content %}
<h4>Reset Complete</h4>
<p>Your password has been set.
You may go ahead and log in now.</p>
Log in
{% endblock content %}
password_reset_confirm.html
{% extends "base.html" %}
{% block title %}Setting New password{% endblock title %}
{% block content %}
<h4>Set New Password</h4>
<form action="" method="post">
{% csrf_token %}
{% if validlink %}
<p>Please enter your new password twice.
So we can verify you typed it in correctly.</p>
<p>
{{ form.new_password1.errors }}<br/>
{{ form.new_password1 }}
</p>
<p class="button-height">
{{ form.new_password2.errors }}<br/>
{{ form.new_password2 }}
</p>
{% else %}
<h4>Password reset unsuccessful</h4>
<p>The password reset link was invalid,
possibly because it has already been used.
Please request a new password reset.</p><br/>
{% endif %}
<button type="submit">Change my password</button>
</form>
{% endblock content %}
password_reset_done.html
{% extends "base.html" %}
{% block title %}Password reset successful{% endblock title %}
{% block content %}
<h4>Reset Password</h4>
<p>We've e-mailed you instructions for setting
your password to the e-mail address you submitted.</p>
<p>You should be receiving it shortly.</p>
<p>Login</p>
{% endblock content %}
#password_reset_ email.html
{% load i18n %}
{% load url from future %}
{% autoescape off %}
{% blocktrans %}
You're receiving this e-mail because you requested
a password reset for your user account at {{ site_name }}.
{% endblocktrans %}
{% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %}
{% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.username }}
{% trans "Thanks for using our site!" %}
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
{% endautoescape %}
password_reset_form.html
{% extends "base.html" %}
{% block title %}Password reset form{% endblock title %}
{% block content %}
<h4>Reset Password</h4>
<p>Forgotten your password?
Enter your e-mail address below,
and we'll e-mail instructions for setting a new one.</p>
<form action="" method="post">
{% csrf_token %}
{% if form.email.errors %}`enter code here`
<div class="message red-gradient">{{ form.email.errors }}</div><br/>
{% endif %}
<p>E-mail address: {{ form.email }} <br/><br/>
<input type="submit" value="Reset my password"></p>
</form>
{% endblock content %}
Than your def lexresetpasswordconfirmed(request): should accept also uidb36 and token args.
Reverse for 'django.contrib.auth.views.password_reset_confirm' with arguments '()' and keyword arguments '{'uidb36': '1', 'token': '392-804fab6dcec790f0ec6b'}' not found.
This means that at some point in the execution, you are not calling reverse on lexresetpasswordconfirmed, you are calling it on django.contrib.auth.views.password_reset_confirm.
Where is this error occurring? In the template? If so, make sure that the template you are using has
{% url lexresetpasswordconfirmed uid token %}
and not
{% url django.contrib.auth.views.password_reset_confirm uid token %}
Is the error occurring in a view? If so, somewhere you are calling reverse on django.contrib.auth.views.password_reset_confirm.
Once that error is resolved, then yes you will have to resolve the other error that Alexander pointed out, namely including the uuid and token in the view function:
#login_required
def lexresetpasswordconfirmed(request, uuid36, token):
"""
Reset Password Confirmed
"""
# you need to do SOMETHING with the uuid and token here, RIGHT?!?!
path = reverse('lexresetpasswordcompleted')
return password_reset_confirm(request,post_reset_redirect=path)
So I'm guessing on each of these returns you're using the views from django.contrib.auth, correct? Problem is that one of these views -- probably password_reset_done -- doesn't care that you've provided it with a redirect, it's using its own.
My settings.py:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = os.environ.get('EMAIL_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_PASS')
My urls.py:
urlpatterns = [
path('admin/', admin.site.urls),
path('register/', user_views.register,name='register'),
path('profile/', user_views.profile,name='profile'),
path('login/', auth_views.LoginView.as_view(template_name='users/login.html'),name='login'),
path('logout/',auth_views.LogoutView.as_view(template_name='users/logout.html'),name='logout'),
path('password-reset/',auth_views.PasswordResetView.as_view(template_name='users/password_reset.html'),name='password_reset'),
path('password-reset/done/',auth_views.PasswordResetDoneView.as_view(template_name='users/password_reset_done.html'),name='password_reset_done'),
path('password-reset-confirm/<uidb64>/<token>/',auth_views.PasswordResetConfirmView.as_view(template_name='users/password_reset_confirm.html'),name='password_reset_confirm'),
path('password-reset-complete/',
auth_views.PasswordResetCompleteView.as_view(
template_name='users/password_reset_complete.html'
),
name='password_reset_complete'),
path('', include('blog.urls')),
]
password_reset.html:
{% extends "blog/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="content-section">
<form method="POST">
{% csrf_token %}
<fieldset class="form-group">
<legend class="border-bottom mb-4">Reset Password</legend>
{{ form|crispy }}
</fieldset>
<div class="form-group">
<button class="btn btn-outline-info" type="submit">Request Password Reset</button>
</div>
</form>
</div>
{% endblock content %}
password_reset_done:
{% extends "blog/base.html" %}
{% block content %}
<div class="alert alert-info">
An email has been sent with instructions to reset your password
</div>
{% endblock content %}
password_reset_confirm.html:
{% extends "blog/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="content-section">
<form method="POST">
{% csrf_token %}
<fieldset class="form-group">
<legend class="border-bottom mb-4">Reset Password</legend>
{{ form|crispy }}
</fieldset>
<div class="form-group">
<button class="btn btn-outline-info" type="submit">Reset Password</button>
</div>
</form>
</div>
{% endblock content %}
password_reset_complete.html:
{% extends "blog/base.html" %}
{% block content %}
<div class="alert alert-info">
Your password has been set.
</div>
Sign In Here
{% endblock content %}

Display number of instances for each model in Django's admin index

I need to display number of objects at main django site admin page.
For example, in list of models I need to display
Elephants (6)
instead of
Elephants
I added this code to my model:
class Elephant(models.Model):
....
class Meta:
verbose_name_plural = 'Elephants ' + '(' + unicode(count_elephants()) + ')'
where count_elephants() calculates number of objects. The problem is that verbose_name_plural is calculated at server start and is not called when I delete/insert objects, so this calculated value becomes irrelevant.
Is it possible to do it in correct way?
Thanks!
Since verbose_name_plural is used in many other ways, a better way to do this will be to change the admin index view and admin template.
However, since the admin app can change, this is probably tied to a specific version of
django. I am attaching for example the modified admin taken from django 1.2.5.
(Note: I will use an in place replacement for the index method, but it will be probably better to subclass it instead of replacing the method)
As a start, copy from django/contrib/admin/sites.py the AdminSite.index method and it's required imports, and modify it to include counts (one line changed, look for 'THIS LINE WAS ADDED"). Add it to any of your admin.py files or somewhere else appropriate:
from django.utils.text import capfirst
from django import template
from django.shortcuts import render_to_response
from django.views.decorators.cache import never_cache
from django.utils.translation import ugettext as _
def index_with_count(self, request, extra_context=None):
"""
Displays the main admin index page, which lists all of the installed
apps that have been registered in this site.
"""
app_dict = {}
user = request.user
for model, model_admin in self._registry.items():
app_label = model._meta.app_label
has_module_perms = user.has_module_perms(app_label)
if has_module_perms:
perms = model_admin.get_model_perms(request)
# Check whether user has any perm for this module.
# If so, add the module to the model_list.
if True in perms.values():
model_dict = {
'name': capfirst(model._meta.verbose_name_plural),
'admin_url': mark_safe('%s/%s/' % (app_label, model.__name__.lower())),
'perms': perms,
'count': model.objects.count(), # THIS LINE WAS ADDED
}
if app_label in app_dict:
app_dict[app_label]['models'].append(model_dict)
else:
app_dict[app_label] = {
'name': app_label.title(),
'app_url': app_label + '/',
'has_module_perms': has_module_perms,
'models': [model_dict],
}
# Sort the apps alphabetically.
app_list = app_dict.values()
app_list.sort(lambda x, y: cmp(x['name'], y['name']))
# Sort the models alphabetically within each app.
for app in app_list:
app['models'].sort(lambda x, y: cmp(x['name'], y['name']))
context = {
'title': _('Site administration'),
'app_list': app_list,
'root_path': self.root_path,
}
context.update(extra_context or {})
context_instance = template.RequestContext(request, current_app=self.name)
return render_to_response(self.index_template or 'admin/index.html', context,
context_instance=context_instance
)
site.index = never_cache(type(site.index)(index_with_count, site, AdminSite))
Now copy the django/contrib/admin/templates/admin/index.html file into admin/index.html in any of your templates folders to override the original template and modify it to show the counts:
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% load adminmedia %}{% admin_media_prefix %}css/dashboard.css" />{% endblock %}
{% block coltype %}colMS{% endblock %}
{% block bodyclass %}dashboard{% endblock %}
{% block breadcrumbs %}{% endblock %}
{% block content %}
<div id="content-main">
{% if app_list %}
{% for app in app_list %}
<div class="module">
<table summary="{% blocktrans with app.name as name %}Models available in the {{ name }} application.{% endblocktrans %}">
<caption>{% blocktrans with app.name as name %}{{ name }}{% endblocktrans %}</caption>
{% for model in app.models %}
<tr>
<th scope="row">
{% if model.perms.change %}
{{ model.name }}
{% else %}
{{ model.name }}
{% endif %}
({{ model.count }})
</th>
{% if model.perms.add %}
<td>{% trans 'Add' %}</td>
{% else %}
<td> </td>
{% endif %}
{% if model.perms.change %}
<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 %}
{% block sidebar %}
<div id="content-related">
<div class="module" id="recent-actions-module">
<h2>{% trans 'Recent Actions' %}</h2>
<h3>{% trans 'My Actions' %}</h3>
{% load log %}
{% get_admin_log 10 as admin_log for_user user %}
{% if not admin_log %}
<p>{% trans 'None available' %}</p>
{% else %}
<ul class="actionlist">
{% for entry in admin_log %}
<li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}">
{% if entry.is_deletion %}
{{ entry.object_repr }}
{% else %}
{{ entry.object_repr }}
{% endif %}
<br/>
{% if entry.content_type %}
<span class="mini quiet">{% filter capfirst %}{% trans entry.content_type.name %}{% endfilter %}</span>
{% else %}
<span class="mini quiet">{% trans 'Unknown content' %}</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{% endblock %}
This will do it.
(You will still need to modify the app_index view to see the counts correctly in the app index pages, I leave this as an exercise to you :-)
A 2022 update using Django 4.0
Subclass the default admin site, see the official Django doc on subclassing the AdminSite.
in the subclass, overwrite the _build_app_dict() method to add count in its model_dict as in:
model_dict = {
"name": capfirst(model._meta.verbose_name_plural),
"object_name": model._meta.object_name,
"perms": perms,
"admin_url": None,
"add_url": None,
"count": model.objects.count(), # THIS IS ALL YOU NEED TO ADD
}
Override the default admin site for your project with the subclass that we have created and optimized. see the official Django doc overriding the default admin site.
Override the app_list.html template if you haven't already. Inside your app_list.html template, you can now use the model.count variable like so {{ model.count }}. see the official Django docs on overriding templates.