While I can show an uploaded image in list_display is it possible to do this on the per model page (as in the page you get for changing a model)?
A quick sample model would be:
Class Model1(models.Model):
image = models.ImageField(upload_to=directory)
The default admin shows the url of the uploaded image but not the image itself.
Thanks!
Sure. In your model class add a method like:
def image_tag(self):
from django.utils.html import escape
return u'<img src="%s" />' % escape(<URL to the image>)
image_tag.short_description = 'Image'
image_tag.allow_tags = True
and in your admin.py add:
fields = ( 'image_tag', )
readonly_fields = ('image_tag',)
to your ModelAdmin. If you want to restrict the ability to edit the image field, be sure to add it to the exclude attribute.
Note: With Django 1.8 and 'image_tag' only in readonly_fields it did not display. With 'image_tag' only in fields, it gave an error of unknown field. You need it both in fields and in readonly_fields in order to display correctly.
In addition to the answer of Michael C. O'Connor
Note that since Django v.1.9 (updated - tested and worked all the way to Django 3.0)
image_tag.allow_tags = True
is deprecated and you should use format_html(), format_html_join(), or mark_safe() instead
So if you are storing your uploaded files in your public /directory folder, your code should look like this:
from django.utils.html import mark_safe
Class Model1(models.Model):
image = models.ImageField(upload_to=directory)
def image_tag(self):
return mark_safe('<img src="/directory/%s" width="150" height="150" />' % (self.image))
image_tag.short_description = 'Image'
and in your admin.py add:
fields = ['image_tag']
readonly_fields = ['image_tag']
It can be done in admin without modifying model
from django.utils.html import format_html
#admin.register(Model1)
class Model1Admin(admin.ModelAdmin):
def image_tag(self, obj):
return format_html('<img src="{}" />'.format(obj.image.url))
image_tag.short_description = 'Image'
list_display = ['image_tag',]
For Django 1.9
To show image instead of the file path in edit pages, using ImageWidget is nice way to do it.
from django.contrib.admin.widgets import AdminFileWidget
from django.utils.translation import ugettext as _
from django.utils.safestring import mark_safe
from django.contrib import admin
class AdminImageWidget(AdminFileWidget):
def render(self, name, value, attrs=None):
output = []
if value and getattr(value, "url", None):
image_url = value.url
file_name = str(value)
output.append(u' <img src="%s" alt="%s" /> %s ' % \
(image_url, image_url, file_name, _('Change:')))
output.append(super(AdminFileWidget, self).render(name, value, attrs))
return mark_safe(u''.join(output))
class ImageWidgetAdmin(admin.ModelAdmin):
image_fields = []
def formfield_for_dbfield(self, db_field, **kwargs):
if db_field.name in self.image_fields:
request = kwargs.pop("request", None)
kwargs['widget'] = AdminImageWidget
return db_field.formfield(**kwargs)
return super(ImageWidgetAdmin, self).formfield_for_dbfield(db_field, **kwargs)
Usage:
class IndividualBirdAdmin(ImageWidgetAdmin):
image_fields = ['thumbNail', 'detailImage']
Images will show up for the fields, thumbNail and detailImage
With django-imagekit you can add any image like this:
from imagekit.admin import AdminThumbnail
#register(Fancy)
class FancyAdmin(ModelAdmin):
list_display = ['name', 'image_display']
image_display = AdminThumbnail(image_field='image')
image_display.short_description = 'Image'
readonly_fields = ['image_display'] # this is for the change form
While there are some good, functional solutions already shared here, I feel that non-form markup, such as auxiliary image tags, belong in templates, not tacked on to Django form widgets or generated in model admin classes. A more semantic solution is:
Admin Template Overrides
Note: Apparently my reputation isn't high enough to post more than two simple links, so I have created annotations in the following text and included the respective URLs at the bottom of this answer.
From the Django Admin Site documentation:
It is relatively easy to override many of the templates which the admin module uses to generate the various pages of an admin site. You can even override a few of these templates for a specific app, or a specific model.
Django's django.contrib.admin.options.ModelAdmin (commonly accessed under the namespace django.contrib.admin.ModelAdmin) presents a series of possible template paths to Django's template loader in order from most specific to less so. This snippet was copied directly from django.contrib.admin.options.ModelAdmin.render_change_form:
return TemplateResponse(request, form_template or [
"admin/%s/%s/change_form.html" % (app_label, opts.model_name),
"admin/%s/change_form.html" % app_label,
"admin/change_form.html"
], context)
Therefore, considering the aforementioned Django admin template override documentation and the template search paths, suppose one has created an app "articles" in which is defined a model class "Article". If one wants to override or extend only the default Django admin site change form for model articles.models.Article, one would execute the following steps:
Create a template directory structure for the override file.
Although the documentation does not mention it, the template loader will look in app directories first if APP_DIRS1 is set to True.
Because one wants to override the Django admin site template by app label and by model, the resulting directory hierarchy would be: <project_root>/articles/templates/admin/articles/article/
Create the template file(s) in one's new directory structure.
Only the admin change form needs to be overridden so create change_form.html.
The final, absolute path will be <project_root>/articles/templates/admin/articles/article/change_form.html
Completely override or simply extend the default admin change form template.
I wasn't able to locate any information in the Django documentation concerning the context data available to the default admin site templates so I was forced to look at the Django source code.
Default change form template: github.com/django/django/blob/master/django/contrib/admin/templates/admin/change_form.html
A few of the relevant context dictionary definitions can be found in
django.contrib.admin.options.ModelAdmin._changeform_view and django.contrib.admin.options.ModelAdmin.render_change_form
My Solution
Assuming that my ImageField attribute name on the model is "file", my template override to implement image previews would be similar to this:
{% extends "admin/change_form.html" %}
{% block field_sets %}
{% if original %}
<div class="aligned form-row">
<div>
<label>Preview:</label>
<img
alt="image preview"
src="/{{ original.file.url }}"
style="max-height: 300px;">
</div>
</div>
{% endif %}
{% for fieldset in adminform %}
{% include "admin/includes/fieldset.html" %}
{% endfor %}
{% endblock %}
original appears to be the model instance from which the ModelForm was generated. As an aside, I usually don't use inline CSS but it wasn't worth a separate file for a single rule.
Sources:
docs.djangoproject.com/en/dev/ref/settings/#app-dirs
I was trying to figure it out myself and this is what i came up with
#admin.register(ToDo)
class ToDoAdmin(admin.ModelAdmin):
def image_tag(self, obj):
return format_html('<img src="{}" width="auto" height="200px" />'.format(obj.img.url))
image_tag.short_description = 'Image'
list_display = ['image_tag']
readonly_fields = ['image_tag']
This is how it worked for django 2.1 without modifying models.py:
In your Hero model, you have an image field.:
headshot = models.ImageField(null=True, blank=True, upload_to="hero_headshots/")
You can do it like this:
#admin.register(Hero)
class HeroAdmin(admin.ModelAdmin, ExportCsvMixin):
readonly_fields = [..., "headshot_image"]
def headshot_image(self, obj):
return mark_safe('<img src="{url}" width="{width}" height={height} />'.format(
url = obj.headshot.url,
width=obj.headshot.width,
height=obj.headshot.height,
)
)
Django 2.1 update for Venkat Kotra's answer. The answer works fine on Django 2.0.7 and below. But gives server 500 error (if DEBUG=False) or gives
render() got an unexpected keyword argument 'renderer'
The reason is that in Django 2.1: Support for Widget.render() methods without the renderer argument is removed. So, param renderer is mandatory now. We must update function render() of AdminImageWidget to include param renderer. And it must be after attrs (before kwargs if you have it):
class AdminImageWidget(AdminFileWidget):
def render(self, name, value, attrs=None, renderer=None):
output = []
if value and getattr(value, "url", None):
image_url = value.url
file_name = str(value)
output.append(u' <img src="%s" alt="%s" /> %s ' % \
(image_url, image_url, file_name, _('Change:')))
output.append(super(AdminFileWidget, self).render(name, value, attrs, renderer))
return mark_safe(u''.join(output))
Django ver. 3.0.3
models.py:
def image_tag(self):
from django.utils.html import mark_safe
return mark_safe('<img src="%s" width="100px" height="100px" />'%(self.image.url))
image_tag.short_description = 'Image'
admin.py:
list_display = ('image_tag', )
Tested on Django v3.2.*
Just you can this code in your model.py
from django.db import models
from django.utils.html import mark_safe
class Book(models.Model):
image = models.ImageField()
def image_tag(self):
if self.image != '':
return mark_safe('<img src="%s%s" width="150" height="150" />' % (f'{settings.MEDIA_URL}', self.image))
Then add this in admin.py
list_display = ['image_tag']
#palamunder's answer worked for me on Django 2.2 with a couple minor changes.
Model.py
from django.utils.safestring import mark_safe
class AdminCategory(models.Model):
image = models.ImageField(_("Image"),
upload_to='categories/',
blank=True,
default='placeholder.png')
def image_tag(self):
return mark_safe('<img src="%s" width="150" height="150" />' % (
self.image.url)) # Get Image url
image_tag.short_description = 'Image'
Admin.py
admin.site.register(
AdminCategory,
list_display=["image_tag"],
)
If you need to show image preview before save, you could use custom django template + js
admin.py
class UploadedImagePreview(object):
short_description = _('Thumbnail')
allow_tags = True
def __init__(self, image_field, template, short_description=None, width=None, height=None):
self.image_field = image_field
self.template = template
if short_description:
self.short_description = short_description
self.width = width or 200
self.height = height or 200
def __call__(self, obj):
try:
image = getattr(obj, self.image_field)
except AttributeError:
raise Exception('The property %s is not defined on %s.' %
(self.image_field, obj.__class__.__name__))
template = self.template
return render_to_string(template, {
'width': self.width,
'height': self.height,
'watch_field_id': 'id_' + self.image_field # id_<field_name> is default ID
# for ImageField input named `<field_name>` (in Django Admin)
})
#admin.register(MyModel)
class MainPageBannerAdmin(ModelAdmin):
image_preview = UploadedImagePreview(image_field='image', template='admin/image_preview.html',
short_description='uploaded image', width=245, height=245)
readonly_fields = ('image_preview',)
fields = (('image', 'image_preview'), 'title')
image_preview.html
<img id="preview_{{ watch_field_id }}" style="display: none; width: {{ width }}px; height: {{ height }}px" alt="">
<script>
function handleFileSelect(event) {
var files = event.target.files; // FileList object
// Loop through the FileList and render image files as thumbnails
for (var i = 0, f; f = files[i]; i++) {
// Only process image files
if (!f.type.match('image.*')) continue;
// Init FileReader()
// See: https://developer.mozilla.org/en-US/docs/Web/API/FileReader
var reader = new FileReader();
// Closure to capture the file information
reader.onload = (function () {
return function (e) {
// Render background image
document.getElementById('preview_{{watch_field_id}}').src = e.target.result;
// Set `display: block` to preview image container
document.getElementById('preview_{{watch_field_id}}').style.display = 'block';
};
})(f);
// Read in the image file as a data URL
reader.readAsDataURL(f);
}
}
// Change img src after change file input
// watch_field_id — is ID for ImageField input
document.getElementById('{{ watch_field_id }}').addEventListener('change', handleFileSelect, false);
</script>
For example, there is Product model below:
# "models.py"
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=50)
price = models.DecimalField(decimal_places=2, max_digits=5)
image = models.ImageField()
def __str__(self):
return self.name
And, there is Product admin below:
# "admin.py"
from django.contrib import admin
from .models import Product
#admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
pass
Then, an uploaded image is not displayed in "Change" page in Django Admin as shown below:
Now, I override AdminFileWidget then assign CustomAdminFileWidget to formfield_overrides as shown below:
# "admin.py"
from django.contrib import admin
from .models import Product
from django.contrib.admin.widgets import AdminFileWidget
from django.utils.html import format_html
from django.db import models
# Here
class CustomAdminFileWidget(AdminFileWidget):
def render(self, name, value, attrs=None, renderer=None):
result = []
if hasattr(value, "url"):
result.append(
f'''<a href="{value.url}" target="_blank">
<img
src="{value.url}" alt="{value}"
width="100" height="100"
style="object-fit: cover;"
/>
</a>'''
)
result.append(super().render(name, value, attrs, renderer))
return format_html("".join(result))
#admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
formfield_overrides = { # Here
models.ImageField: {"widget": CustomAdminFileWidget}
}
Then, an uploaded image is displayed in "Change" page in Django Admin as shown below:
You can also see my answer explaining how to display uploaded images in "Change List" page in Django Admin.
Related
I have seen this approach in many web applications (e.g. when you subscribe for an insurance), but I can't find a good way to implement it in django. I have several classes in my model which inherit from a base class, and so they have several fields in common. In the create-view I want to use that inheritance, so first ask for the common fields and then ask for the specific fields, depending on the choices of the user.
Naive example, suppose I want to fill a database of places
class Place(Model):
name = models.CharField(max_length=40)
address = models.CharField(max_length=100)
class Restaurant(Place):
cuisine = models.CharField(max_length=40)
website = models.CharField(max_length=40)
class SportField(Place):
sport = models.CharField(max_length=40)
Now I would like to have a create view when there are the common fields (name and address) and then the possibility to choose the type of place (Restaurant / SportField). Once the kind of place is selected (or the user press a "Continue" button) new fields appear (I guess to make it simple the page need to reload) and the old one are still visible, already filled.
I have seen this approach many times, so I am surprised there is no standard way, or some extensions already helping with that (I have looked at Form Wizard from django-formtools, but not really linked to inheritance), also doing more complicated stuff, as having more depth in inheritance.
models.py
class Place(models.Model):
name = models.CharField(max_length=40)
address = models.CharField(max_length=100)
class Restaurant(Place):
cuisine = models.CharField(max_length=40)
website = models.CharField(max_length=40)
class SportField(Place):
sport = models.CharField(max_length=40)
forms.py
from django.db import models
from django import forms
class CustomForm(forms.Form):
CHOICES = (('restaurant', 'Restaurant'), ('sport', 'Sport'),)
name = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Name'}))
address = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Address'}))
type = forms.ChoiceField(
choices=CHOICES,
widget=forms.Select(attrs={'onChange':'renderForm();'}))
cuisine = forms.CharField(required=False, widget=forms.TextInput(attrs={'placeholder': 'Cuisine'}))
website = forms.CharField(required=False, widget=forms.TextInput(attrs={'placeholder': 'Website'}))
sport = forms.CharField(required=False, widget=forms.TextInput(attrs={'placeholder': 'Sport'}))
views.py
from django.http.response import HttpResponse
from .models import Restaurant, SportField
from .forms import CustomForm
from django.shortcuts import render
from django.views import View
class CustomView(View):
def get(self, request,):
form = CustomForm()
return render(request, 'home.html', {'form':form})
def post(self, request,):
data = request.POST
name = data['name']
address = data['address']
type = data['type']
if(type == 'restaurant'):
website = data['website']
cuisine = data['cuisine']
Restaurant.objects.create(
name=name, address=address, website=website, cuisine=cuisine
)
else:
sport = data['sport']
SportField.objects.create(name=name, address=address, sport=sport)
return HttpResponse("Success")
templates/home.html
<html>
<head>
<script type="text/javascript">
function renderForm() {
var type =
document.getElementById("{{form.type.auto_id}}").value;
if (type == 'restaurant') {
document.getElementById("{{form.website.auto_id}}").style.display = 'block';
document.getElementById("{{form.cuisine.auto_id}}").style.display = 'block';
document.getElementById("{{form.sport.auto_id}}").style.display = 'none';
} else {
document.getElementById("{{form.website.auto_id}}").style.display = 'none';
document.getElementById("{{form.cuisine.auto_id}}").style.display = 'none';
document.getElementById("{{form.sport.auto_id}}").style.display = 'block';
}
}
</script>
</head>
<body onload="renderForm()">
<form method="post" action="/">
{% csrf_token %}
{{form.name}}<br>
{{form.address}}<br>
{{form.type}}<br>
{{form.website}}
{{form.cuisine}}
{{form.sport}}
<input type="submit">
</form>
</body>
</html>
Add templates folder in settings.py
TEMPLATES = [
{
...
'DIRS': [os.path.join(BASE_DIR, 'templates')],
...
]
I've created a 2-page working example using modified Class Based Views.
When the form is submitted on the first page, an object of place_type is created. The user is then redirected to the second page where they can update existing details and add additional information.
No separate ModelForms are needed because the CreateView and UpdateView automatically generate the forms from the relevant object's model class.
A single template named place_form.html is required. It should render the {{ form }} tag.
# models.py
from django.db import models
from django.urls import reverse
class Place(models.Model):
"""
Each tuple in TYPE_CHOICES contains a child class name
as the first element.
"""
TYPE_CHOICES = (
('Restaurant', 'Restaurant'),
('SportField', 'Sport Field'),
)
name = models.CharField(max_length=40)
address = models.CharField(max_length=100)
place_type = models.CharField(max_length=40, blank=True, choices=TYPE_CHOICES)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('place_update', args=[self.pk])
# Child models go here...
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('create/', views.PlaceCreateView.as_view(), name='place_create'),
path('<pk>/', views.PlaceUpdateView.as_view(), name='place_update'),
]
# views.py
from django.http import HttpResponseRedirect
from django.forms.models import construct_instance, modelform_factory
from django.views.generic.edit import CreateView, UpdateView
from django.urls import reverse_lazy
from . import models
class PlaceCreateView(CreateView):
model = models.Place
fields = '__all__'
def form_valid(self, form):
"""
If a `place_type` is selected, it is used to create an
instance of that Model and return the url.
"""
place_type = form.cleaned_data['place_type']
if place_type:
klass = getattr(models, place_type)
instance = klass()
obj = construct_instance(form, instance)
obj.save()
return HttpResponseRedirect(obj.get_absolute_url())
return super().form_valid(form)
class PlaceUpdateView(UpdateView):
fields = '__all__'
success_url = reverse_lazy('place_create')
template_name = 'place_form.html'
def get_object(self, queryset=None):
"""
If the place has a `place_type`, get that object instead.
"""
pk = self.kwargs.get(self.pk_url_kwarg)
if pk is not None:
obj = models.Place.objects.get(pk=pk)
if obj.place_type:
klass = getattr(models, obj.place_type)
obj = klass.objects.get(pk=pk)
else:
raise AttributeError(
"PlaceUpdateView must be called with an object pk in the URLconf."
)
return obj
def get_form_class(self):
"""
Remove the `place_type` field.
"""
model = self.object.__class__
return modelform_factory(model, exclude=['place_type',])
We did something similar manually, we created the views and forms based on design and did the linkage based on if conditions.
I think a nice solution would be to dynamically access subclasses of the main class and then do the necessary filtering/lists building.
UPD: I've spent some more time today on this question and made a "less raw" solution that allows to use the inheritance.
You can also check the code below deployed here. It has only one level of inheritance (as in example), though, the approach is generic enough to have multiple levels
views.py
def inheritance_view(request):
all_forms = {form.Meta.model: form for form in forms.PlaceForm.__subclasses__()}
all_forms[models.Place] = forms.PlaceForm
places = {cls._meta.verbose_name: cls for cls in models.Place.__subclasses__()}
# initiate forms with the first one
context = {
'forms': [forms.PlaceForm(request.POST)],
}
# check sub-forms selected on the forms and include their sub-forms (if any)
for f in context['forms']:
f.sub_selected = request.POST.get('{}_sub_selected'.format(f.Meta.model._meta.model_name))
if f.sub_selected:
sub_form = all_forms.get(places.get(f.sub_selected))
if sub_form not in context['forms']:
context['forms'].append(sub_form(request.POST))
# update some fields on forms to render them on the template
for f in context['forms']:
f.model_name = f.Meta.model._meta.model_name
f.sub_forms = {x.Meta.model._meta.verbose_name: x for x in f.__class__.__subclasses__()}
f.sub_options = f.sub_forms.keys() # this is for rendering selector on the form for the follow-up forms
page = loader.get_template(template)
response = HttpResponse(page.render(context, request))
return response
forms.py
class PlaceForm(forms.ModelForm):
class Meta:
model = models.Place
fields = ('name', 'address',)
class RestaurantForm(PlaceForm):
class Meta:
model = models.Restaurant
fields = ('cuisine', 'website',)
class SportFieldForm(PlaceForm):
class Meta:
model = models.SportField
fields = ('sport',)
templates/inheritance.html
<body>
{% for form in forms %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
{% if form.sub_options %}
<select class="change-place" name="{{ form.model_name }}_sub_selected">
{% for option in form.sub_options %}
<option value="{{ option }}" {% if option == form.sub_selected %}selected{% endif %}>{{ option }}</option>
{% endfor %}
</select>
{% endif %}
<button type="submit">Next</button>
</form>
{% endfor %}
</body>
What I didn't make here is saving the form to the database. But it should be rather trivial using the similar snippet:
for f in context['forms']:
if f.is_valid():
f.save()
Add a PlaceType table, and a FK, e.g. type_of_place, to the Place table:
class PlaceType(Model):
types = models.CharField(max_length=40) # sportsfield, restaurants, bodega, etc.
class Place(Model):
name = models.CharField(max_length=40)
address = models.CharField(max_length=100)
type_of_place = models.ForeignKey('PlaceType', on_delete=models.SET_NULL, null=True)
class Restaurant(Place):
cuisine = models.CharField(max_length=40)
website = models.CharField(max_length=40)
This allows you to create a new Place as either SportsField, restaurant or some other type which you can easily add in the future.
When a new place is created, you'll use the standard CreateView and Model Form. Then, you can display a second form which also uses a standard CreateView that is based on the type_of_place value. These forms can be on the same page (and with javascript on the browser side, you'll hide the second form until the first one is saved) or on separate pages--which may be more practical if you intend to have lots of extra columns. The two key points are as follows:
type_of_place determines which form, view, and model to use. For
example, if user chooses a "Sports Field" for type_of_place, then
you know to route the user off to the SportsField model form;
CreateViews are designed for creating just one object/model. When
used as intended, they are simple and easy to maintain.
There are lot of way you can handle multiple froms in django. The easiest way to use inlineformset_factory.
in your froms.py:
forms .models import your model
class ParentFrom(froms.From):
# add fields from your parent model
Restaurant = inlineformset_factory(your parent model name,Your Child model name,fields=('cuisine',# add fields from your child model),extra=1,can_delete=False,)
SportField = inlineformset_factory(your parent model name,Your Child model name,fields=('sport',# add fields from your child model),extra=1,can_delete=False,)
in your views.py
if ParentFrom.is_valid():
ParentFrom = ParentFrom.save(commit=False)
Restaurant = Restaurant(request.POST, request.FILES,) #if you want to add images or files then use request.FILES.
SportField = SportField(request.POST)
if Restaurant.is_valid() and SportField.is_valid():
ParentFrom.save()
Restaurant.save()
SportField.save()
return HttpResponseRedirect(#your redirect url)
#html
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
#{{ Restaurant.errors}} #if you want to show error
{{ Restaurant}}
{{ SportField}}
{{form}}
</form>
you can use simple JavaScript in your html for hide and show your any froms fields
i am new to django and i am unable to get to print to the html page.How do you display the information retrieved from the database into a blank html page?
blank.hml
<body>
<h1>Classes Added</h1>
{% for i in classInfo %}
<h1>{{ i.label }}</h1>
{% endfor %}
</body>
models.py
class EventType(models.Model):
'''
Simple ``Event`` classifcation.
'''
objects = models.Manager()
abbr = models.CharField(_('abbreviation'), max_length=4, unique=False)
label = models.CharField(_('label'), max_length=50)
class Meta:
verbose_name = _('event type')
verbose_name_plural = _('event types')
def __str__(self):
return self.label
views.py
def displayClass(TemplateView):
templateName = 'blank.html'
def get(self,request):
form = ClassCreationForm()
classInfo = EventType.objects.all()
print(classInfo)
args = {'form' : form, 'classInfo' : classInfo}
return render(request,self,templateName,{'form':form})
forms.py
class ClassCreationForm(forms.Form):
classroom = forms.CharField(label = 'Class Name',max_length=50)
I think you need to understand how the views.py file works. You should have your business logic included in there and the pass it along to the template to be rendered. It can be passed along by the return feature you have included in your code. Although, in your return feature you are only passing a templatename and the form you wanted to render. There isn't data related to the EventType queryset being passed to your template as it is not included in the return context.
Now, personally I like working with Django Class-Based-generic-Views (CBV), since a lot of the code is included in there for you. I am not sure if you have got to the point of learning these yet but I would check them out.
If you would like to add a form into this, you could do so by adding FormMixin which is part of the generic mixins Django provides.
How I would structure your view.py code using generic views is as follows:
from django.views import generic
from django.views.generic.edit import FormMixin
from YourApp.forms import ClassCreationForm
from YourApp.models import EventType
class DisplayClass(FormMixin,generic.ListView):
template_name = 'blank.html'
form_class = ClassCreationForm
def get_queryset(self, *args, **kwargs):
return EventType.objects.all()
If you decide to use class based views you will need to add additional criteria to your urls.py file (the .as_view()):
from django.urls import path
from . import views
urlpatterns = [
path('yoururlpath/', views.DisplayClass.as_view()),
]
Then in your template:
{% for i in object_list %}
<h1>{{ i.label }}</h1>
{% endfor %}
rendering your form...
{{ form.as_p }}
The admin actions seem to work on several items selected in the list view of django admin interface:
In my case I would like to have a simple action button on the change (one item) view.
Is there a way to make the django admin actions available there?
I know that I can walk around this problem by going to the list view, and select one item there. But it would be more nice to have it directly available.
Create a template for your model in your app.
templates/admin/<yourapp>/<yourmodel>/change_form.html
With this example content to add a button when changing an existing object.
{% extends "admin/change_form.html" %}
{% block submit_buttons_bottom %}
{{ block.super }}
{% if original %} {# Only show if changing #}
<div class="submit-row">
<a href="{% url 'custom-model-action' original.pk %}">
Another action
</a>
</div>
{% endif %}
{% endblock %}
Link that action to any url and redirect back to your model change object view. More information about extending admin templates.
Update: Added complete common use case for custom action on existing object
urls.py
urlpatterns = [
url(r'^custom_model_action/(?P<object_pk>\d+)/$',
core_views.custom_model_action, name='custom-model-action')
]
views.py
from django.urls import reverse
from django.contrib import messages
from django.http import HttpResponse, HttpResponseRedirect
def custom_model_action(request, object_pk):
messages.info(request, 'Performed custom action!')
return HttpResponseRedirect(
reverse('admin:<yourapp>_<yourmodel>_change', args=[object_pk])
)
If you realy need per-single object, I suggest you to use this solution, eg:
class Gallery(TimeStampedModel):
title = models.CharField(max_length=200)
attachment = models.FileField(upload_to='gallery/attachment/%Y/%m/%d')
def __str__(self):
return self.title
def process_button(self):
return ('<button id="%(id)s class="btn btn-default process_btn" '
'data-value="%(value)s>Process</button>' % {'id': self.pk, 'value': self.attachment.url})
process_button.short_description = 'Action'
process_button.allow_tags = True
In your admin.py, insert process_button into list_display;
class GalleryAdmin(admin.ModelAdmin):
list_display = ['title', 'process_button', 'created']
search_fields = ['title', 'pk']
....
class Media:
js = ('path/to/yourfile.js', )
Then, inside yourfile.js, you can also process it..
$('.process_btn').click(function(){
var id = $(this).attr('id'); // single object id
var value = $(this).data('value'); // single object value
...
});
Hope it helpful..
Not the same as the topic starter asked, but this snippet allows to have Single Object action from on the list page with minimum amount of code
BaseAction code
class AdminActionError(Exception):
pass
class AdminObjectAction:
"""Base class for Django Admin actions for single object"""
short_description = None
exp_obj_state = {}
def __init__(self, modeladmin, request, queryset):
self.admin = modeladmin
self.request = request
self.queryset = queryset
self.__call__()
def validate_qs(self):
count = self.queryset.count()
if count != 1:
self.error("You must select one object for this action.")
if self.exp_obj_state:
if self.queryset.filter(**self.exp_obj_state).count() != 1:
self.error(f'Selected object does not meet the requirements: {self.exp_obj_state}')
def error(self, msg):
raise AdminActionError(msg)
def get_object(self):
return self.queryset.get()
def process_object_action(self, obj):
pass
def validate_obj(self, obj):
pass
def __call__(self, *args, **kwargs):
try:
self.validate_qs()
obj = self.get_object()
self.validate_obj(obj)
except AdminActionError as e:
self.admin.message_user(self.request, f"Failed: {e}", level=messages.ERROR)
else:
with transaction.atomic():
result = self.process_object_action(obj)
self.admin.message_user(self.request, f"Success: {self.short_description}, {result}")
Custom Action [minimum amount of code]
class RenewSubscriptionAction(AdminObjectAction):
short_description = 'Renew subscription'
exp_obj_state = {
'child': None,
'active_status': True,
}
def process_object_action(self, obj):
manager = RenewManager(user=obj.user, subscription=obj)
return manager.process()
AdminClass
class SomeAdmin(admin.ModelAdmin):
actions = [RenewSubscriptionAction]
The built-in admin actions operate on a queryset.
You can use a calable for the action you whant or to show something else:
class ProductAdmin(admin.ModelAdmin):
list_display ('name' )
readonly_fields('detail_url)
def detail_url(self, instance):
url = reverse('product_detail', kwargs={'pk': instance.slug})
response = format_html("""{0}""", product_detail)
return response
or using forms
class ProductForm(forms.Form):
name = forms.Charfield()
def form_action(self, product, user):
return Product.value(
id=product.pk,
user= user,
.....
)
#admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
# render buttons and links to
def product_actions(self, obj):
return format_html(
'<a class="button" href="{}">Action1</a> '
'<a class="button" href="{}">Action 2</a>',
reverse('admin:product-action-1', args=[obj.pk]),
reverse('admin:aproduct-action-3', args=[obj.pk]),
)
for more details about using forms
This is my sample code (and see the screenshot):
models.py
class User(models.Model):
# the variable to take the inputs
user_name = models.CharField(max_length=100)
user_avatar = models.FileField(upload_to = 'images/%Y%m%d')
admin.py
class UserAdmin(admin.ModelAdmin):
exclude = ('user_avatar',)
readonly_fields = ('avatar_readonly',)
def avatar_readonly(self, instance):
value_link = "/mediafileupdown/download/" + str(instance.id)
value_desc = str(instance.user_avatar.name)
return format_html('{}', value_link, value_desc)
avatar_readonly.short_description = "User avatar (read only)"
avatar_readonly.allow_tags=True
screenshot
The problem is: i'd like obtain the 'Currently' user_avatar's link (that's a models.FileField field) via the my 'download' views and not as simple url. I can do that if the field is in the 'readonly_fields' list and so I can overide the html format ... in that case, on the change page, the 'avatar_readonly' link is redirected to the my 'download' view.
The question is: how can I get the same result (overide the html format) in a FileField (like my 'user_avatar') when is not in the readonly_fileds list?
Sorry for my english and many Thanks for the help.
You can use formfield_overrides to modify the AdminFileWidget. This AdminFileWidget can display images and mp4 files, but you can use only for images omitting the if file_name[-4:] == '.mp4':
part.
from django.db.models.fields.files import FileField
from django.contrib.admin.widgets import AdminFileWidget
class AdminMediaWidget(AdminFileWidget):
def render(self, name, value, attrs=None):
output = []
if value and getattr(value, "url", None):
image_url = value.url
file_name = str(value)
if file_name[-4:] == '.mp4':
output.append('<video width="320" height="240" controls>'
'<source src="{0}" type="video/mp4">'
'Your browser does not support the video tag.'
'</video>'.format(image_url))
else:
output.append('<a href="{0}" target="_blank">'
'<img height=200 src="{1}" alt="{2}" />'
'</a>'.format(image_url, image_url, file_name))
output.append(super(AdminFileWidget, self).render(name, value, attrs))
return mark_safe(''.join(output))
class UserAdmin(admin.ModelAdmin):
formfield_overrides = {
FileField: {'widget': AdminMediaWidget},
}
exclude = ('user_avatar',)
readonly_fields = ('avatar_readonly',)
def avatar_readonly(self, instance):
value_link = "/mediafileupdown/download/" + str(instance.id)
value_desc = str(instance.user_avatar.name)
return format_html('{}', value_link, value_desc)
avatar_readonly.short_description = "User avatar (read only)"
avatar_readonly.allow_tags=True
Hope it helps!
I have allowed for, in my html template, using icons with either small square png's or with font-awesome:
<i class="icon-search"></i>Search // using font-awesome
// or
<img src="images/icons/search.png">Search // using images
In the template top-menu.html, I need to be able to use the following:
{{ child.menu_icon_font_awesome }} # in place of "icon-search"
{{ child.menu_icon_image }} # in place of "images/icons/search.png"
How does one get these variables into the child menu nodes in the menu?
Also, how do I get the fieldsets to work in admin.py (much less important)?
My menu_icons app looks like this:
# models.py
from django.db import models
from django.utils.translation import ugettext_lazy as _
from cms.models.pagemodel import Page
from django.core.files.storage import FileSystemStorage
class OverwriteStorage(FileSystemStorage):
"""
Deletes file of same name if exists.
"""
def _save(self, name, content):
if self.exists(name):
self.delete(name)
return super(OverwriteStorage, self)._save(name, content)
def get_available_name(self, name):
return name
class MenuIconFontAwesome(models.Model):
"""
Defines Font Awesome Menu Icon
"""
page = models.ForeignKey(Page,
unique=True,
verbose_name=_("Page"),
editable=False)
menu_icon_font_awesome = models.CharField(max_length=48,
verbose_name="Font Awesome Menu Icon",
blank=True)
class MenuIconImage(models.Model):
"""
Defines Image Menu Icon
"""
page = models.ForeignKey(Page,
unique=True,
verbose_name=_("Page"),
editable=False)
menu_icon_image = models.ImageField('Menu Icon Image',
upload_to = 'menu_icons/',
blank=True,null=True)
# admin.py
from models import MenuIconFontAwesome, MenuIconImage
from cms.admin.pageadmin import PageAdmin
from cms.models.pagemodel import Page
from django.contrib import admin
class MenuIconFontAwesomeAdmin(admin.TabularInline):
"""
Adds field for Font Awesome Menu Icon
"""
model = MenuIconFontAwesome
fieldsets = (
('Menu Icon with Font Awesome', {
'fields': ('menu_icon_font_awesome',),
}),
)
class MenuIconImageAdmin(admin.TabularInline):
"""
Adds field for Image Menu Icon
"""
model = MenuIconImage
fieldsets = (
('Menu Icon with Uploaded Image', {
'fields': ('menu_icon_image',),
}),
)
PageAdmin.inlines.append(MenuIconFontAwesomeAdmin)
PageAdmin.inlines.append(MenuIconImageAdmin)
admin.site.unregister(Page)
admin.site.register(Page, PageAdmin)
# views.py
# this is the part I cannot figure out
Here is the additions using the tip for navigation modifiers. It gives the error, "type object 'MenuIconFontAwesome' has no attribute 'menu_icon_font_awesome'" I am sure there is something obvious I am missing.
from menus.base import Modifier
from menus.menu_pool import menu_pool
from models import MenuIconFontAwesome, MenuIconImage
class MenuIconsMod(Modifier):
"""
Add Menu Icons to the menu nodes
"""
def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb):
if post_cut:
return nodes
for node in nodes:
node.menu_icon_font_awesome = MenuIconFontAwesome.menu_icon_font_awesome
node.menu_icon_image = MenuIconImage.menu_icon_image
return nodes
menu_pool.register_modifier(MenuIconsMod)
Regarding the fieldsets; what I am seeing looks like this, http://imgur.com/ubSBeB3. I just can't sort why those fieldsets get those names and how to override them.
Have a look at menu modifiers (https://django-cms.readthedocs.org/en/2.3.5/extending_cms/app_integration.html#navigation-modifiers), using NavigationNode.attr property you should be able to add any customized data.
I am not allowed to comment, so I will try to wrap it up here.
Can you post some code of what "you have allowed for" in the template? I am not sure I understand what you mean.
If you want to upload an image trough django admin, you need to have an menu_image = models.ImageField(upload_to = '/images/') in your class in your models.py file, and than include it in the template by saying <img src="{{ object.menu_image.url }}" alt="menu_image_description" />.
And than in the admin.py write
from django.contrib import admin
from myproject.models import *
class MYModelNameAdmin(admin.ModelAdmin):
list_display = ('name',)
admin.site.register(MuModelName, MyModelNameAdmin)
What is with the font? Can you edit your question and include the template, the admin.py and the models.py you have till now?