Django modelformset_factory save nothing - python

Created a set of forms for a simple model. When I am try to change the model object data in the form and save these changes in the database, the new data is not saved as a result, and a redirect to the form page with the same data occurs, although at least messages about the success or failure of the operation should be output. The terminal does not return any errors, it is written in the logs that the correct request was sent, the test server works without drops in normal mode. There is an opinion that the reason for this is a refusal to validate, but for what reason this happens and where exactly the error is hidden, it is not yet possible to understand.
And
When I clicked a "save" button, see this message in terminal (see below):
[25/Aug/2017 18:03:06] "GET
/categories/?csrfmiddlewaretoken=igSZl3z8pcF9qRGaMts9hG3T9dyaIpVvAxB672R34bmKvGYd6pymjmtwyEgDHGg2&form-TOTAL_FORMS=6&form-INITIAL_FORMS=5&form-MIN_NUM_FORMS=0&form-MAX_NUM_FORMS=1000&form-0-id=1&form-0-name=fhdrhddh&form-0-order=0&form-1-id=2&form-1-name=gdegasf&form-1-order=6&form-2-id=3&form-2-name=dfdgbsbgsdgs&form-2-order=2&form-3-id=4&form-3-name=dbgsgbasedgbaedvg&form-3-order=3&form-4-id=5&form-4-name=dgfsdg3waesdvz&form-4-order=4&form-5-id=&form-5-name=&form-5-order=0
HTTP/1.1" 200 7502
models.py (categories app)
from django.db import models
# Create your models here.
class Category(models.Model):
name = models.CharField(max_length = 30, db_index = True, unique = True, verbose_name = "Title")
order = models.PositiveSmallIntegerField(default = 0, db_index = True, verbose_name = "Serial number")
def __str__(self):
return self.name
class Meta:
ordering = ["order", "name"]
verbose_name = "category"
verbose_name_plural = "categories"
views.py (categories app)
from django.views.generic.base import TemplateView
from django.forms.models import modelformset_factory
from django.shortcuts import redirect
from django.contrib import messages
from categories.models import Category
from generic.mixins import CategoryListMixin
CategoriesFormset = modelformset_factory(Category, can_delete=True, fields = '__all__', extra=1, max_num=None)
class CategoriesEdit(TemplateView, CategoryListMixin):
template_name = "categories_edit.html"
formset = None
def get(self, request, *args, **kwargs):
self.formset = CategoriesFormset()
return super(CategoriesEdit, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(CategoriesEdit, self).get_context_data(**kwargs)
context["formset"] = self.formset
return context
def post(self, request, *args, **kwargs):
self.formset = CategoriesFormset(request.POST)
if self.formset.is_valid():
self.formset.save()
messages.api.add_message(request, messages.SUCCESS, "List of categories successfully changed")
return redirect("categories_edit")
else:
messages.api.add_message(request, messages.SUCCESS, "Something is wrong!!!")
return super(CategoriesEdit, self).get(request, *args, **kwargs)
mixins.py
from django.views.generic.base import ContextMixin
class CategoryListMixin(ContextMixin):
def get_context_data(self, **kwargs):
context = super(CategoryListMixin, self).get_context_data(**kwargs)
context["current_url"] = self.request.path
return context
categories_edit.html (categories app)
{% extends "categories_base.html" %}
{% block title %} Categories {% endblock %}
{% block main %}
{% include "generic/messages.html" %}
{{ formset.errors }}
<h2>Categories</h2>
<form action="" method="post">
{% include "generic/formset.html" %}
<div class="submit-button"><input type="submit" value="Save"></div>
</form>
{% endblock %}
formset.html (categories app)
{% csrf_token %}
{{ formset.management_form }}
<table class="form">
<tr>
<th></th>
{% with form=formset|first %}
{% for field in form.visible_fields %}
<th>
{{ field.label }}
{% if field.help_text %}
<br>{{ field.help_text }}
{% endif %}
</th>
{% endfor %}
{% endwith %}
</tr>
{% for form in formset %}
<tr>
<td>
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
</td>
{% for field in form.visible_fields %}
<td>
{% if field.errors.count > 0 %}
<div class="error-list">
{{ field.errors }}
</div>
{% endif %}
<div class="control">{{ field }}</div>
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
messages.html (categories app)
{% if messages %}
<div id="messages-list">
{% for message in messages %}
<p class="{{ message.tags }}">{{ message }}</p>
{% endfor %}
</div>
{% endif %}
urls.py (categories app)
from django.conf.urls import url
from django.contrib.auth.decorators import login_required
from categories.views import CategoriesEdit
urlpatterns = [
url(r'^$', login_required(CategoriesEdit.as_view()), name = "categories_edit"),
]
settings.py (project)
"""
Django settings for t****** project.
Generated by 'django-admin startproject' using Django 1.10.6.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""
import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '***************************************************'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'guestbook',
'categories',
'imagepool',
'page',
'main',
'news',
'shop',
'django.contrib.sites',
'django_comments',
'easy_thumbnails',
'taggit',
'precise_bbcode',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 't******.urls'
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',
],
},
},
]
WSGI_APPLICATION = 'tdkennel.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 't******l',
'USER': 'p*******',
'PASSWORD': '********',
'HOST': '',
'PORT': '5432',
}
}
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)
MEDIA_ROOT = os.path.join(BASE_DIR, 'upload')
MEDIA_URL = '/media/'
LOGIN_URL = "login"
LOGOUT_URL = "logout"
SITE_ID = 1
THUMBNAIL_BASEDIR = "thumbnails"
THUMBNAIL_BASEDIR = {"goods.Good.image": {"base": {"size": (200, 100)},},}
LOGIN_REDIRECT_URL = "main"

I recall that something similar happened to me. I ended up using a Form to create the Formset:
try:
from django.forms import ModelForm
from categories.models import Category
from django.forms.models import modelformset_factory
class CategoryForm(ModelForm):
class Meta:
model = Category
fields = '__all__'
CategoryFormSet = modelformset_factory(Category, form=CategoryForm)
and use CategoryFormSet in CategoriesEdit
It's just a workaround, but hope this helps!

My inattention was my mistake. I wrote method="post" in the commented and concatenated part of the code of categories_edit.html (in my project) and forgot to write it in uncommented piece of code:
{# <form method="post" action=""> #}
<form method="" action="">
{% include "generic/formset.html" %}
{# {% csrf_token %} #}
{# {{ formset.as_p }} #}
<div class="submit-button"><input type="submit" value="Save"></div>
</form>
But I confused the users, because wrote everything is correct in my Question, than delete concatenated part of the code and entered the correct piece of code:
<form method="post" action="">
{% include "generic/formset.html" %}
{# {% csrf_token %} #}
{# {{ formset.as_p }} #}
<div class="submit-button"><input type="submit" value="Save"></div>
</form>
I apologize to the users who responded to my question! Code working.

Related

Django AllAuth and Django Crispy Forms

I am trying to override the default django-allauth templates and use django-crispy-forms from improved rendering but having no luck getting the forms to actually submit, meaning I press submit and nothing happens.
Here is my settings.py:
INSTALLED_APPS = [
'about.apps.AboutConfig',
'content.apps.ContentConfig',
'users.apps.UsersConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'allauth',
'allauth.account',
'allauth.socialaccount',
]
...
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',
],
},
},
]
# All Auth settings
AUTHENTICATION_BACKENDS = (
# Needed to login by username in Django admin, regardless of `allauth`
'django.contrib.auth.backends.ModelBackend',
# `allauth` specific authentication methods, such as login by e-mail
'allauth.account.auth_backends.AuthenticationBackend',
)
SITE_ID = 1
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = 'email'
Here is my custom template:
{% extends "about/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
{% block navbar %}{% endblock %}
<div class="site-section mb-5">
<div class="container">
<div class="form-register">
<form method="POST" class="signup" id="signup_form" action="{% url 'account_signup' %}">
{% csrf_token %}
<legend>Signup</legend>
<div class="form-group">
{{ form | crispy }}
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" value="Sign Up">
</div>
</form>
</div>
</div>
</div>
{% endblock %}
I know the template is in the correct location and correctly overriding the django-allauth default templates because it renders, but why won't the submit button submit the form? I also know everything with django-allauth is working, because if I remove the custom template and use the django-allauth template, it will submit the form and redirect properly.
I'm doing this myself now. Really, I'd need to see the data for the {{ form|crispy }} to know exactly what is going on.
However, it isn't enough to simply render a similar form and submit. allauth has stuff running in the background (some of which is not entirely known to me).
I would recommend pulling the templates from the allauth github and editing them directly, removing tags you know are sure you don't need, like "extends 'allauth/base.html', headings, etc.. They're found Here.
Then, I followed the details of the second method from this answer to add the forms as context processors (most useful for my case use):
2. Contex processor
a) Make folder your_project/your_app/context_processor. Put there 2 files - __init__.py and login_ctx.py
b) In login_ctx.py add:
from allauth.account.forms import LoginForm
def login_ctx_tag(request):
return {'loginctx': LoginForm()}
c) In project's SETTINGS add your_app.context_processors.login_ctx.login_form_ctx' inTEMPLATES`
section. Something like:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates'), os.path.join(BASE_DIR, 'templates', 'allauth')],
'APP_DIRS': True,
'OPTIONS': {
'debug': DEBUG,
'context_processors': [
'your_app.context_processors.login_ctx.login_form_ctx', # <- put your
processor here
'django.template.context_processors.debug',
# [...other processors...]
],
},
},
]
d) In your *.html where you need add the next:
{% if not user.is_authenticated %}
<form action="{% url 'account_login' %}" method="post">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.get_full_path }}" />
{{ loginctx }}
<button type="submit">Login</button>
</form>
{% else %}
{# display something else here... (username?) #}
{% endif %}
So if you are rendering your own forms, I would recommend rendering the very same allauth forms instead in the custom template.
Add the crispy tag and all is good.

How to get ckeditor field value in modelform after submit in Django2.1?

I have installed ckeditor.
Placed "path('ckeditor/', include('ckeditor_uploader.urls'))," in urls.py
Placed "'ckeditor'," in INTALLED_APPS in settings.py
Placed
## CKEDITOR CONFIGURATION ##
####################################
CKEDITOR_JQUERY_URL = 'https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js'
CKEDITOR_UPLOAD_PATH = 'uploads/help/'
CKEDITOR_IMAGE_BACKEND = "pillow"
CKEDITOR_CONFIGS = {
'default': {
'toolbar': None,
'height':100,
'width':500,
},
}
###################################
at the end of file in settings.py
models.py :
from ckeditor.fields import RichTextField
from django.db import models
class Help(models.Model):
title = models.CharField(max_length=255)
description = RichTextField(blank=True, null=True) #models.TextField()
class Meta:
managed = False
db_table = 'help'
def __str__(self):
return self.title
forms.py
from ckeditor.widgets import CKEditorWidget
from django import forms
class HelpForm(ModelForm):
description = forms.CharField(widget=CKEditorWidget())
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class Meta:
model = Help
exclude = ('created_by', 'updated_by', 'created', 'updated')
help.html
{% load i18n static widget_tweaks %}
<form id="newFrm" method="post" novalidate>
{% csrf_token %}
{{ form.media }}
<div class="col-lg-6 col-md-8 col-sm-12 col-xs-24">
<div class="form-group">
<label for="">description <span class="required">*</span></label>
{% if form.title.errors %}
{% render_field form.description class="form-control ckeditor error" placeholder="Description" %}
<div class="error-msg show form-error">
{{ form.description.errors}}
</div>
{% else %}
{% render_field form.description class="form-control ckeditor" placeholder="Description" %}
{% endif %}
</div>
</div>
Question: I am not getting the POST value of description after form submit(in views.py). Any help will be appreciated. Thanks in advance.
I found the solution.
Please put below script in your code.
for (var i in CKEDITOR.instances) {
CKEDITOR.instances[i].on('change', function() {
CKEDITOR.instances[i].updateElement() });
}
This code will update the raw data of ckeditor in to related textarea.
Now on submit the form, you will get the data in POST.

Alternative to Django's image.url method?

I'm using an inlineformset so that a user can upload multiple images at once. The images are saved and functionality is as expected, except on the front-end side. When I loop through my formset with a method resembling {{ form. image }}, I can clearly see that my image is saved and when I click the url, I am redirected to the uploaded file. The problem seems to be that the absoulte url is not stored when I try to set the image's URL as a src for an image element.
Trying to log MEDIA_URL and MEDIA_ROOT in a <p> tag yields no results.
settings.py
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
ROOT_URLCONF = 'dashboard_app.urls'
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]
urls.py
from django.conf.urls import url, include
from . import views
from django.conf.urls.static import static
from django.conf import settings
app_name = 'Accounts_Namespace'
urlpatterns = [
url(r'^$', views.Register, name='Accounts_Register'),
url(r'^change-password/$', views.ChangePassword, name="Accounts_Change_Password"),
url(r'^login/$', views.Login, name='Accounts_Login'),
url(r'^logout/$', views.Logout, name='Accounts_Logout'),
url(r'^profile/$', views.ViewProfile, name='Accounts_View_Profile'),
url(r'^profile/edit/$', views.EditProfile, name="Accounts_Edit_Profile"),
url(r'^school/', include('student_map_app.urls', namespace="Student_Maps_Namespace")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
models.py
class Gallery(models.Model):
id = models.AutoField(primary_key=True)
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
image = models.ImageField(upload_to="gallery_images")
uploaded = models.DateTimeField(auto_now_add=True)
views.py
def EditProfile(request):
user = request.user
galleryInlineFormSet = inlineformset_factory(get_user_model(), Gallery, form=GalleryForm)
selectedUserGallery = Gallery.objects.filter(user=user).order_by('uploaded')
userGallery_initial = [{'image': selection.image} for selection in selectedUserGallery] # Using this syntax because formset initials accept dictionaries
if request.method == "POST":
profile_form = ProfileEditForm(request.POST, instance=request.user)
gallery_inlineformset = galleryInlineFormSet(request.POST, request.FILES) # Essentially, we're passing a queryset
if profile_form.is_valid() and gallery_inlineformset.is_valid():
# Altering the User model through the UserProfile model's UserProfileForm representative
user.first_name = profile_form.cleaned_data['first_name']
user.last_name = profile_form.cleaned_data['last_name']
user.save()
new_images = []
for gallery_form in gallery_inlineformset:
image = gallery_form.cleaned_data.get('image')
if image:
new_images.append(Gallery(user=user, image=image))
try:
Gallery.objects.filter(user=user).delete()
Gallery.objects.bulk_create(new_images)
messages.success(request, 'You have updated your profile.')
except IntegrityError:
messages.error(request, 'There was an error saving your profile.')
return HttpResponseRedirect('https://www.youtube.com')
else:
profile_form = ProfileEditForm(request.user)
gallery_inlineformset = galleryInlineFormSet(initial=userGallery_initial)
args = { 'profile_form':profile_form, 'gallery_inlineformset':gallery_inlineformset }
return render(request, 'accounts_app/editprofile.html', args)
editprofile.html
{% block main %}
<section class="Container">
<section class="Main-Content">
<form id="post_form" method="POST" action='' enctype='multipart/form-data'>
{% csrf_token %}
{{ gallery_inlineformset.management_form }}
{% for gallery_form in gallery_inlineformset %}
<div class="link-formset">
{{ gallery_form.image }} <!-- Show the image upload field -->
<p>{{ MEDIA_ROOT }}</p>
<p>{{ MEDIA_URL }}</p>
<img src="/media/{{gallery_form.image.image.url}}">
</div>
{% endfor %}
<input type="submit" name="submit" value="Submit" />
</form>
</section>
</section>
{% endblock %}
Again, when I try:
<img src="{{ MEDIA_URL }}{{ gallery_form.image.url }}">
I get a value of "unknown" as the source, but I can click the link that "{{ gallery_form.image}}" generates and see the image that was uploaded. Trying to log both "MEDIA_URL" and "MEDIA_ROOT" yields no results. Not quite sure where the issue lies.
Use <img src="{{ gallery_form.image.url }}"> and make sure image is not None
add this line in your urls.py
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
No need to add {{MEDIA_URL}} before address of your image. because by default it will add /media before your image url path.
Also be sure to add all path starting media to your urls.
from django.conf import settings
if settings.DEBUG:
urlpatterns += patterns('',
(r'^media/(?P<path>.*)$', 'django.views.static.serve', {
'document_root': settings.MEDIA_ROOT}))
also when trying to print an image url in django template, handle situations that image not exist like this:
<img src="{% if gallery_form.image %}{{ gallery_form.image.url }}{%else%} <default-image-path-here> {%endif%}"
While I didn't figure out why I couldn't use the .url() method Django has predefined, I did however, end up using another solution suggested to me by a user in a previous question of mine. Basically, after a user has uploaded images and we have them stored in a database, we make a variable storing the URL attribute of those images, and access that variable from the template. It looks something like this:
views.py
selectedUserGallery = Gallery.objects.filter(user=user) # Get gallery objects where user is request.user
userGallery_initial = [{'image': selection.image, 'image_url':selection.image.url} for selection in selectedUserGallery if selection.image]
if request.method == "GET":
print("--------GET REQUEST: PRESENTING PRE-EXISTING GALLERY IMAGES.-------")
profile_form = ProfileEditForm(request.user)
gallery_inlineformset = galleryInlineFormSet(initial=userGallery_initial)
template.html
<form id="post_form" method="POST" action='' enctype='multipart/form-data'>
{% csrf_token %}
{{ gallery_inlineformset.management_form }}
{% for gallery_form in gallery_inlineformset %}
<div class="link-formset">
{{ gallery_form.image }} <!-- Show the image upload field, this is not he image var from views.py -->
{% if gallery_form.image is not None %}
<p>The image should be below:</p>
<img src="{{ gallery_form.initial.image_url }}">
{% endif %}
</div>
{% endfor %}
<input type="submit" name="gallery-submit" value="Submit" />
</form>
Also, I ended up replacing most of code from the original post as I'm no longer using bulk_create().

How to add a custom button to Django admin

I want to add a custom button right next to the SAVE button in my admin change form. I tried to add a button in submit_line.html
<input type="submit" action = "admin/{{id??}}" value="show PDF" name="show PDF{{ onclick_attrib }}/>
But, it doesn't redirect to my given page and I don't know how to pass the current id.
Actually, I dont think it's a good idea to change submit_line.html anyways, because I only want it in one model. Is there a better solution for that?
I think you can add your own logic. Here you check app verbose name, to show that button only in your app:
{% if opts.verbose_name == 'Your app verbose name' %}<input type="submit" value="{% trans 'Show PDF' %}" name="_show_pdf" {{ onclick_attrib }}/>{% endif %}
and in your admin.py:
class YourAdmin(admin.ModelAdmin):
def response_change(self, request, obj):
if '_continue' in request.POST:
return HttpResponseRedirect(obj.get_admin_url())
elif '_show_pdf' in request.POST:
# Do some stuf here( show pdf in your case)
Django1.10:
I would do it as follows, to avoid the verbosity and extra template processing of the accepted answer
1) Extend admin/change_form.html in your app's template directory (you are namespacing your app's templates, I hope) and override block submit_row:
{% extends "admin/change_form.html" %}
{% block submit_row %}
<div class="submit-row">
{% if extra_buttons %}
{% for button in extra_buttons %}
{{ button }}
{% endfor %}
{% endif %}
{% if show_save %}<input type="submit" value="{% trans 'Save' %}" class="default" name="_save" />{% endif %}
{% if show_delete_link %}
{% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %}
<p class="deletelink-box">{% trans "Delete" %}</p>
{% endif %}
{% if show_save_as_new %}<input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew" />{% endif %}
{% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother" />{% endif %}
{% if show_save_and_continue %}<input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue" />{% endif %}
</div>
{% endblock %}
This assumes, of course, that button's string representation is an appropriate browser input or button element, and is marked safe with django.utils.safestring.mark_safe. Alternatively, you could use the safe template filter or access the attributes of button directly to construct the <input>. In my opinion, it's better to isolate such things to the python level.
2) Override MyModelAdmin.change_view and MyModelAdmin.change_form_template:
change_form_template = "my_app/admin/change_form.html"
def change_view(self, request, object_id, form_url='', extra_context=None):
extra_context = extra_context or self.extra_context()
return super(PollAdmin, self).change_view(
request, object_id, form_url, extra_context=extra_context,
)
In my case I only needed for users to do a custom action only after they saved, like "Add another", other buttons such as "export to pdf" I put in the change list as there it's super easy to add buttons.
If you are here for a case like mine the built-in messages framework will do the trick.
adding a response message is simple and you will need to override the
admin class save_model, goes like this:
from django.utils.safestring import mark_safe
def save_model(self, request, obj, form, change):
# if new model
if not change:
message_html = "<a href='http://yourSITE/admin/appname/appmodel/add/'>Add another</a>"
messages.add_message(request, messages.INFO, mark_safe(message_html))
super(YourAdminClass, self).save_model(request, obj, form, change)
there are 4 different types of messages (I changed the css of INFO) and they are displayed in the screenshot
You can add a custom button right next to "SAVE" button on "Add" form and "Change" form for a specifc admin.
First, in the root django project directory, create "templates/admin/custom_change_form.html" and "templates/admin/submit_line.html" as shown below:
Next, copy & paste all the code of "change_form.html" under django library whose path is "django/contrib/admin/templates/admin/change_form.html" to "templates/admin/custom_change_form.html" as shown below:
# "templates/admin/custom_change_form.html"
{% extends "admin/base_site.html" %}
{% load i18n admin_urls static admin_modify %}
{% block extrahead %}{{ block.super }}
<script src="{% url 'admin:jsi18n' %}"></script>
{{ media }}
{% endblock %}
... Much more code below
Next, copy & paste all the code of "submit_line.html" under django library whose path is "django/contrib/admin/templates/admin/submit_line.html" to "templates/admin/submit_line.html" as shown below:
# "templates/admin/submit_line.html"
{% load i18n admin_urls %}
<div class="submit-row">
{% block submit-row %}
{% if show_save %}<input type="submit" value="{% translate 'Save' %}" class="default" name="_save">{% endif %}
... Much more code below
Next, add the code below between "{% block submit-row %}" and "{% if show_save %}" on "templates/admin/submit_line.html". *style="float:right;margin-left:8px;" is important for this code below to put a custom button right next to "SAVE" button:
{% if custom_button %}
<input type="submit" style="float:right;margin-left:8px;" value="{% translate 'Custom button' %}" name="_custom_button">
{% endif %}
So, this is the full code as shown below:
# "templates/admin/submit_line.html"
{% load i18n admin_urls %}
<div class="submit-row">
{% block submit-row %}
{% if custom_button %}
<input type="submit" style="float:right;margin-left:8px;" value="{% translate 'Custom button' %}" name="_custom_button">
{% endif %}
{% if show_save %}<input type="submit" value="{% translate 'Save' %}" class="default" name="_save">{% endif %}
... Much more code below
Next, this is the settings for templates in "settings.py":
# "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',
],
},
},
]
Then, add "os.path.join(BASE_DIR, 'templates')" to "DIRS" as shown below:
# "settings.py"
import os # Here
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')], # Here
'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',
],
},
},
]
Now, this is "Person" model as shown below:
# "models.py"
from django.db import models
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
Then, this is "Person" admin as shown below:
# "admin.py"
from django.contrib import admin
from .models import Person
#admin.register(Person) # Here
class PersonAdmin(admin.ModelAdmin):
pass
So next, for "Person" admin, set "admin/custom_change_form.html" to "change_form_template", set "True" to "extra_context['custom_button']" in "changeform_view()" and set "response_add()" and "response_change()" to define the action after pressing "Custom button" on "Add" form and "Change" form respectively as shown below. *Whether or not setting "response_add()" and "response_change()", inputted data to fields is saved after pressing "Custom button" on "Add" form and "Change" form respectively:
# "admin.py"
from django.contrib import admin
from .models import Person
#admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
change_form_template = "admin/custom_change_form.html" # Here
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
extra_context = extra_context or {}
extra_context['custom_button'] = True # Here
return super().changeform_view(request, object_id, form_url, extra_context)
def response_add(self, request, obj, post_url_continue=None): # Here
if "_custom_button" in request.POST:
# Do something
return super().response_add(request, obj, post_url_continue)
else:
# Do something
return super().response_add(request, obj, post_url_continue)
def response_change(self, request, obj): # Here
if "_custom_button" in request.POST:
# Do something
return super().response_change(request, obj)
else:
# Do something
return super().response_change(request, obj)
Finally, "Custom button" is added at the bottom of "Add" form and "Change" form for "Person" admin as shown below:
In addition, for "Person" admin, you can replace "changeform_view()" with "render_change_form()" set "context.update({"custom_button": True})" as shown below:
# "admin.py"
from django.contrib import admin
from .models import Person
#admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
change_form_template = "admin/custom_change_form.html"
def render_change_form(self, request, context, add=False, change=False, form_url="", obj=None):
context.update({"custom_button": True}) # Here
return super().render_change_form(request, context, add, change, form_url, obj)
def response_add(self, request, obj, post_url_continue=None):
if "_custom_button" in request.POST:
# Do something
return super().response_add(request, obj, post_url_continue)
else:
# Do something
return super().response_add(request, obj, post_url_continue)
def response_change(self, request, obj):
if "_custom_button" in request.POST:
# Do something
return super().response_change(request, obj)
else:
# Do something
return super().response_change(request, obj)
Then, "Custom button" is added at the bottom of "Add" form and "Change" form for "Person" admin as well as shown below:

Create custom buttons in admin change_form in Django

I want to add custom buttons to the add/change form at the administration interface. By default, there are only three:
Save and add another
Save and continue editing
Save
I have created some custom methods in my forms.py file, and I want to create buttons to call these methods. I have used the snippet http://djangosnippets.org/snippets/1842/, but it's not exactly what I want. This one allows to create buttons and call methods from the admin.py file and not forms.py.
Is there a way to do that?
This is my admin.py code:
class CategoryAdmin(admin.ModelAdmin):
prepopulated_fields = { "alias": ("title",) }
form = CategoryForm
admin.site.register(Category, CategoryAdmin)
And my forms.py code:
class CategoryForm(forms.ModelForm):
"""
My attributes
"""
def custom_method(self):
print("Hello, World!")
How do I create a button that calls "custom_method()"?
One simple way I found to add buttons is to add another row for the custom buttons. Create an admin directory in your template dir based on your needs. For example I usually add buttons for specific models in a custom template. Make a "templates/admin/app/model/" directory.
Then add a file change_form.html.
{% extends "admin/change_form.html" %}
{% load i18n %}
{% block submit_buttons_bottom %}
<div class="submit-row">
<input type="button" value="{% trans 'Another Button' %}" name="_anotherbutton" />
</div>
{{ block.super }}
{% endblock %}
The code before the {{ block.super }} is inspired by the submit_line.html template used by the template tag {% submit_row %}. I prefer this method because is straightforward but you must live with another row of buttons.
You can override admin/change_form.html. Copy the version in contrib.admin.templates into your project. Mine is myproject/templates/admin/change_form.html, but you could use /myproject/myapp/templates/admin/change_form.html.
Next, edit the copy and change the two references to the existing template tag, {% submit_row %}, to point to your own template tag, {% my_template_tag %}.
Base your template tag on the contrib.admin's {% submit_row %}, but edit the HTML template to contain any extra buttons you want to display.
The submit buttons in a change form are rendered by the submit_row template tag. This tag renders the template admin/submit_line.html. Since you want to add to the existing buttons, your best (and DRYest) approach is to override admin/submit_line.html.
For example, create a file my_project/templates/admin/submit_line.html with the following content:
{% load i18n admin_urls %}
<div class="submit-row">
{% if show_save %}<input type="submit" value="{% trans 'Save' %}" class="default" name="_save" {{ onclick_attrib }}/>{% endif %}
{% if show_delete_link %}<p class="deletelink-box">{% trans "Delete" %}</p>{% endif %}
{% if show_save_as_new %}<input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew" {{ onclick_attrib }}/>{%endif%}
{% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother" {{ onclick_attrib }}/>{% endif %}
{% if show_save_and_continue %}<input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue" {{ onclick_attrib }}/>{% endif %}
<input type="submit" value="{% trans 'New button 1' %}" name="_button1" {{ onclick_attrib }}/>
<input type="submit" value="{% trans 'New button 2' %}" name="_button2" {{ onclick_attrib }}/>
</div>
Most of what's above was copied from django/contrib/admin/templates/submit_line.html. You can also add additional if statements in the template if you only want to show those additional buttons in certain cases.
You can add a custom button at the bottom of "Add" form and "Change" form for a specifc admin.
First, in the root django project directory, create "templates/admin/custom_change_form.html" as shown below:
Next, under "django" library, there is "change_form.html" which is "django/contrib/admin/templates/admin/change_form.html" so copy & paste all the code of "change_form.html" to "custom_change_form.html" as shown below:
# "templates/admin/custom_change_form.html"
{% extends "admin/base_site.html" %}
{% load i18n admin_urls static admin_modify %}
{% block extrahead %}{{ block.super }}
<script src="{% url 'admin:jsi18n' %}"></script>
{{ media }}
{% endblock %}
... Much more code below
Next, there is the code in line 64 on "custom_change_form" as shown below:
# "templates/admin/custom_change_form.html"
{% block submit_buttons_bottom %}{% submit_row %}{% endblock %} # Line 64
Then, add the code below between "{% submit_row %}" and "{% endblock %}":
{% if custom_button %}
<div class="submit-row">
<input type="submit" value="{% translate 'Custom button' %}" name="_custom_button">
</div>
{% endif %}
So, this is the full code as shown below:
# "templates/admin/custom_change_form.html"
{% block submit_buttons_bottom %} # Line 64
{% submit_row %}
{% if custom_button %}
<div class="submit-row">
<input type="submit" value="{% translate 'Custom button' %}" name="_custom_button">
</div>
{% endif %}
{% endblock %}
Next, this is the settings for templates in "settings.py":
# "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',
],
},
},
]
Then, add "os.path.join(BASE_DIR, 'templates')" to "DIRS" as shown below:
# "settings.py"
import os # Here
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')], # Here
'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',
],
},
},
]
Now, this is "Person" model as shown below:
# "models.py"
from django.db import models
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
Then, this is "Person" admin as shown below:
# "admin.py"
from django.contrib import admin
from .models import Person
#admin.register(Person) # Here
class PersonAdmin(admin.ModelAdmin):
pass
So next, for "Person" admin, set "admin/custom_change_form.html" to "change_form_template", set "True" to "extra_context['custom_button']" in "changeform_view()" and set "response_add()" and "response_change()" to define the action after pressing "Custom button" on "Add" form and "Change" form respectively as shown below. *Whether or not setting "response_add()" and "response_change()", inputted data to fields is saved after pressing "Custom button" on "Add" form and "Change" form respectively:
# "admin.py"
from django.contrib import admin
from .models import Person
#admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
change_form_template = "admin/custom_change_form.html" # Here
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
extra_context = extra_context or {}
extra_context['custom_button'] = True # Here
return super().changeform_view(request, object_id, form_url, extra_context)
def response_add(self, request, obj, post_url_continue=None): # Here
if "_custom_button" in request.POST:
# Do something
return super().response_add(request, obj, post_url_continue)
else:
# Do something
return super().response_add(request, obj, post_url_continue)
def response_change(self, request, obj): # Here
if "_custom_button" in request.POST:
# Do something
return super().response_change(request, obj)
else:
# Do something
return super().response_change(request, obj)
Finally, "Custom button" is added at the bottom of "Add" form and "Change" form for "Person" admin as shown below:
In addition, for "Person" admin, you can replace "changeform_view()" with "render_change_form()" set "context.update({"custom_button": True})" as shown below:
# "admin.py"
from django.contrib import admin
from .models import Person
#admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
change_form_template = "admin/custom_change_form.html"
def render_change_form(self, request, context, add=False, change=False, form_url="", obj=None):
context.update({"custom_button": True}) # Here
return super().render_change_form(request, context, add, change, form_url, obj)
def response_add(self, request, obj, post_url_continue=None):
if "_custom_button" in request.POST:
# Do something
return super().response_add(request, obj, post_url_continue)
else:
# Do something
return super().response_add(request, obj, post_url_continue)
def response_change(self, request, obj):
if "_custom_button" in request.POST:
# Do something
return super().response_change(request, obj)
else:
# Do something
return super().response_change(request, obj)
Then, "Custom button" is added at the bottom of "Add" form and "Change" form for "Person" admin as well as shown below:

Categories