custom html field in the django admin changelist_view - django

I'd like to some little customisation with the django admin -- particularly the changelist_view
class FeatureAdmin(admin.ModelAdmin):
list_display = (
'content_object_change_url',
'content_object',
'content_type',
'active',
'ordering',
'is_published',
)
list_editable = (
'active',
'ordering',
)
list_display_links = (
'content_object_change_url',
)
admin.site.register(get_model('features', 'feature'), FeatureAdmin)
The idea is that the 'content_object_change_url' could be a link to another object's change_view... a convenience for the admin user to quickly navigate directly to the item.
The other case I'd have for this kind of thing is adding links to external sources, or thumbnails of image fields.
I had thought I'd heard of a 'insert html' option -- but maybe I'm getting ahead of myself.
Thank you for your help!

You can provide a custom method on the FeatureAdmin class which returns HTML for content_object_change_url:
class FeatureAdmin(admin.ModelAdmin):
[...]
def content_object_change_url(self, obj):
return 'Click to change' % obj.get_absolute_url()
content_object_change_url.allow_tags=True
See the documentation.

Pay attention and use format_html (See docs here) as the mark_safe util has been deprecated since version 1.10. Moreover, support for the allow_tags attribute on ModelAdmin methods will be removed since version 1.11.
from django.utils.html import format_html
from django.contrib import admin
class FeatureAdmin(admin.ModelAdmin):
list_display = (
'change_url',
[...]
)
def change_url(self, obj):
return format_html('<a target="_blank" href="{}">Change</a>', obj.get_absolute_url())
change_url.short_description='URL'

It took me two hours to find out why Daniel Roseman's solution doesn't work for me. Even though he is right, there's one exception: when you want to make custom calculated fields (read only) in the Admin. This wont work. The very easy solution (but hard to find) is to return your string in a special constructor: SafeText(). Maybe this is linked with Django 2 or with readonly_fields (which behave differently from classical fields)
Here's a working sample that works but doesn't without SafeText():
from django.utils.safestring import SafeText
class ModelAdminWithData(admin.ModelAdmin):
def decrypt_bin_as_json(self, obj):
if not obj:
return _("Mode insert, nothing to display")
if not obj.data:
return _("No data in the game yet")
total = '<br/><pre>{}</pre>'.format(
json.dumps(json.loads(obj.data),
indent=4).replace(' ', ' '))
return SafeText(total) # !! working solution !! <------------------
decrypt_bin_as_json.short_description = _("Data")
decrypt_bin_as_json.allow_tags = True
readonly_fields = ('decrypt_bin_as_json',)
fieldsets = (
(_('Data dump'), {
'classes': ('collapse',),
'fields': ('decrypt_bin_as_json',)
}),
)

Related

Why Django does admin change page displays html instead of link after upgrading from Django 1.11 to 2.2?

I recently upgraded to Django 2.2 and now the HTML of my link is display instead of an actual link.
Here is the code I suspect has changed in behavior:
class RequestAdmin(admin.ModelAdmin):
ordering = ('id', 'status', )
list_display = ('detail_link', 'status', 'requester', 'added', 'type', 'change_description', 'approve_or_deny')
... omitted for brevity ...
# ID in list is rendered as link to open request details page
def detail_link(self, obj):
return '%s' % (('https://' if self.request.is_secure() else 'http://'),
self.request.META['HTTP_HOST'],
(settings.GUI_ROOT if settings.GUI_ROOT != '/' else ''),
'/#/requests/', obj.id, obj.id)
Before this would render a link. But now if renders this text instead:
1
Django is much more different from 2.0 version
Use format_html for your link
P.S. I'd even suggest you to use Django 3.0 to avoid similar problems in the future

ForeignKeyAutocompleteAdmin

Given these two models:
class Product(Model):
upc = CharField(max_length=96, unique=True)
...
class Meta:
app_label = 'scrapers'
class Order(Model):
...
product = ForeignKey('scrapers.Product', related_name='orders', on_delete=models.CASCADE)
objects = OrderManager()
class Meta:
app_label = 'scrapers'
And this admin.py:
class OrderAdmin(ForeignKeyAutocompleteAdmin):
related_search_fields = {
'product': ('upc', 'retailer_offerings__name')
}
fields = ('product', 'client', 'completed', 'expires', 'amount', 'filled')
admin.site.register(Order, OrderAdmin)
Having done collectstatic and declared django_extensions and my app in INSTALLED_APPS. Why am I getting this:
[04/Dec/2016 05:54:28] "GET /admin/scrapers/product/foreignkey_autocomplete/?search_fields=upc&app_label=scrapers&model_name=product&q=045496 HTTP/1.1" 302 0
Not Found: /admin/scrapers/product/foreignkey_autocomplete/change/
[04/Dec/2016 05:54:28] "GET /admin/scrapers/product/foreignkey_autocomplete/change/ HTTP/1.1" 404 1875
On input to the input field (box to the left not pk input to the right)?
The Product table has millions of rows and the default admin configuration doesn't handle this well so I tried the extensions package solution. The widget is requesting product/foreignkey_autocomplete but a quick grep through manage.py show_urls shows that only /orders has been registered with the foreignkeyautocomplete package. I don't see anything in the docs addressing url configuration (I assume this is done when registering with the admin). How can I get this to work?
Partial solution:
after examining the urls and realizing it was attempting top send the search queries to /product/foreignkey_autocomplete/... I tried to create an empty admin for that model as well. It worked but the behavior is still odd. It seems to stop searching after 4-5 characters have been entered and doesn't bother refreshing.
as per my update, adding a ForeignKeyAutocompleteAdmin for the other side of the relation created the missing urls and the functionality seems to work
#Verbal_Kint I think I may have figured the same issue as yours.
I have a InlineModelAdmin "TaskInline" for model Task, a field "SCRIPT" of it is a foreign-key to model TestScript (managed by "TestAdmin").
After I made sure the related model's ModelAdmin (here for me it's TestAdmin) inherits ForeignKeyAutoCompleteAdmin instead of admin.ModelAdmin, then made sure TestAdmin had an method wrap as below:
class TestAdmin(ForeignKeyAutocompleteAdmin):
ForeignKeyAutocompleteAdmin.model = TestScript
def wrap(self, view):
def wrapper(*args, **kwargs):
return self.admin_site.admin_view(view)(*args, **kwargs)
wrapper.model_admin = self
return update_wrapper(wrapper, view)
def get_urls(self):
info = self.model._meta.app_label, self.model._meta.model_name
urlpatterns = super(TestAdmin, self).get_urls()
urlpatterns.insert(0, url(r'^(.+)/run/', self.wrap(self.run_view), name='%s_%s_run' % info))
urlpatterns.insert(0, url(r'^add/$', self.wrap(self.add_view), name='%s_%s_add' % info))
urlpatterns.insert(0, url(r'^add_to_template_mission/$', self.wrap(self.add_to_template_mission_view), name='%s_%s_add_to_template_mission' % info))
urlpatterns.insert(0, url(r'^add_to_mission/$', self.wrap(self.add_to_mission_view), name='%s_%s_add_to_mission' % info))
urlpatterns.insert(0, url(r'^$', self.wrap(self.changelist_view), name='%s_%s_changelist' % info))
return urlpatterns
class TaskInline(ForeignKeyAutocompleteTabularInline):
model = Task
related_search_fields = {
'SCRIPT': ('FILENAME', 'FILE_PATH', 'FILE_CONTENT', ),
}
And, don't forget to have
urlpatterns = super(TestAdmin, self).get_urls()
in get_urls() inside TestAdmin
Then, everything got working fine.
Maybe there is a better way, but this did solve my problem. Hope this can help.

Django form validation, raise error on fieldset

I know how to raise an error on a specific field in my django admin form, but I would like the error to be raised on a fieldset. I currently have a list of check boxes in a fieldset and would like an error to be raised, not on a specific field (aka specific check box), but on the entire fieldset.
here is my django admin.py
class EventAdmin(admin.ModelAdmin):
form = EventForm
fieldsets = [
(None, {'fields': [
'approval_state',
'title',
'description'
]
}
),
('Group Owner', {'fields': [
'grpOwner_vcoe',
'grpOwner_cssi',
'grpOwner_essc',
'grpOwner_tmscc',
'grpOwner_inmc',
'grpOwner_cc7',
'grpOwner_ias',
]
}
), ...
class EventForm(forms.ModelForm):
# Form validation
def clean(self):
# Collect data
start = self.cleaned_data.get('start')
end = self.cleaned_data.get('end')
grpOwner_vcoe = self.cleaned_data.get('grpOwner_vcoe')
grpOwner_cssi = self.cleaned_data.get('grpOwner_cssi')
grpOwner_essc = self.cleaned_data.get('grpOwner_essc')
grpOwner_tmscc = self.cleaned_data.get('grpOwner_tmscc')
grpOwner_inmc = self.cleaned_data.get('grpOwner_inmc')
grpOwner_cc7 = self.cleaned_data.get('grpOwner_cc7')
grpOwner_ias = self.cleaned_data.get('grpOwner_ias')
if not (grpOwner_vcoe or grpOwner_cssi or grpOwner_essc or grpOwner_tmscc or grpOwner_inmc or grpOwner_cc7 or grpOwner_ias):
if not self._errors.has_key('Group Owner'):
self._errors['Group Owner'] = ErrorList()
self._errors['Group Owner'].append('Test')
# Check start & end data
if start > end:
if not self._errors.has_key('start'):
self._errors['start'] = ErrorList()
self._errors['start'].append('Event start must occur before event end')
return self.cleaned_data
But this doesn't, I know I can just raise it on each field but I find it much more elegant if I could do it around the fielset
Django forms don't have a concept of fieldsets, they belong to the ModelAdmin class. Therefore there isn't an established way to assign errors to a fieldset instead of a particular field.
You could try overriding the admin templates, in particular, includes/fieldset.html. You could add some code to your form's clean method to make it easy to access the fieldset errors in the template.

Django Haystack custom SearchView for pretty urls

I'm trying to setup Django Haystack to search based on some pretty urls. Here is my urlpatterns.
urlpatterns += patterns('',
url(r'^search/$', SearchView(),
name='search_all',
),
url(r'^search/(?P<category>\w+)/$', CategorySearchView(
form_class=SearchForm,
),
name='search_category',
),
)
My custom SearchView class looks like this:
class CategorySearchView(SearchView):
def __name__(self):
return "CategorySearchView"
def __call__(self, request, category):
self.category = category
return super(CategorySearchView, self).__call__(request)
def build_form(self, form_kwargs=None):
data = None
kwargs = {
'load_all': self.load_all,
}
if form_kwargs:
kwargs.update(form_kwargs)
if len(self.request.GET):
data = self.request.GET
kwargs['searchqueryset'] = SearchQuerySet().models(self.category)
return self.form_class(data, **kwargs)
I keep getting this error running the Django dev web server if I try and visit /search/Vendor/q=Microsoft
UserWarning: The model u'Vendor' is not registered for search.
warnings.warn('The model %r is not registered for search.' % model)
And this on my page
The model being added to the query must derive from Model.
If I visit /search/q=Microsoft, it works fine. Is there another way to accomplish this?
Thanks for any pointers
-Jay
There are a couple of things going on here. In your __call__ method you're assigning a category based on a string in the URL. In this error:
UserWarning: The model u'Vendor' is not registered for search
Note the unicode string. If you got an error like The model <class 'mymodel.Model'> is not registered for search then you'd know that you haven't properly created an index for that model. However this is a string, not a model! The models method on the SearchQuerySet class requires a class instance, not a string.
The first thing you could do is use that string to look up a model by content type. This is probably not a good idea! Even if you don't have models indexed which you'd like to keep away from prying eyes, you could at least generate some unnecessary errors.
Better to use a lookup in your view to route the query to the correct model index, using conditionals or perhaps a dictionary. In your __call__ method:
self.category = category.lower()
And if you have several models:
my_querysets = {
'model1': SearchQuerySet().models(Model1),
'model2': SearchQuerySet().models(Model2),
'model3': SearchQuerySet().models(Model3),
}
# Default queryset then searches everything
kwargs['searchqueryset'] = my_querysets.get(self.category, SearchQuerySet())

Problem using generic views in django

I'm currently working with django generic views and I have a problem I can't figure out.
When using delete_object I get a TypeError exception:
delete_object() takes at least 3 non-keyword arguments (2 given)
Here is the code (I have ommited docstrings and imports):
views.py
def delete_issue(request, issue_id):
return delete_object(request,
model = Issue,
object_id = issue_id,
template_name = 'issues/delete.html',
template_object_name = 'issue')
urls.py
urlpatterns = patterns('issues.views',
(r'(?P<issue_id>\d+)/delete/$', 'delete_issue'),
)
The other generic views (object_list, create_object, etc.) work fine with those parameters. Another problem I have is when using the create_object() function, it says something about a CSRF mechanism, what is that?
You need to provide post_delete_redirect, this means url, where user should be redirected after object is deleted. You can find this in view signature:
def delete_object(request, model, post_delete_redirect, object_id=None,
slug=None, slug_field='slug', template_name=None,
template_loader=loader, extra_context=None, login_required=False,
context_processors=None, template_object_name='object'):