Django Admin - How to Put Icons in front of Model Names - django

I'm in Django 1.6 and I'd like to alter the admin to show an icon in from of each model name (on the admin's index page).
The problem is I can't alter the template (As far as I know) because I'm creating a third party installable app, and when users install it, presumably they could already have their own styles, and customizations for their admin page. (correct me I'm wrong).
So far I've tried doing this in models.py but it's still escaping the HTML on the admin page:
from django.db import models
from django.utils.safestring import mark_safe
class DataConnection(models.Model):
class Meta:
verbose_name_plural = mark_safe("<img src="..." />My Model")
...

Well this seems to work. I monkey patch the admin.site.index and admin.site.app_index methods with my modified versions that grab the response objects and add in the desired HTML and mark it as safe:
def index(self, *args, **kwargs):
response = admin.site.__class__.index(self, *args, **kwargs)
my_app_context = [d for d in response.context_data['app_list'] if d.get('app_label','') == u'my_app_name']
if my_app_context:
for m in my_app_context[0]['models']:
m['name'] = mark_safe(("<img src='...' /> " + unicode(m['name']))
return response
admin.site.index = index.__get__(admin.site, admin.site.__class__)
#...Do something similar for the admin.site.app_index method

Related

Is there an equivalent of django-hitcount template tags but for views?

I am using django-hitcount and am stumped on counting hits within the views.py for my app. The documentation suggests using this
from hitcount.models import HitCount
from hitcount.views import HitCountMixin
# first get the related HitCount object for your model object
hit_count = HitCount.objects.get_for_object(your_model_object)
# next, you can attempt to count a hit and get the response
# you need to pass it the request object as well
hit_count_response = HitCountMixin.hit_count(request, hit_count)
# your response could look like this:
# UpdateHitCountResponse(hit_counted=True, hit_message='Hit counted: session key')
# UpdateHitCountResponse(hit_counted=False, hit_message='Not counted: session key has active hit')
However, I am not looking for responses to a HitCount object. I only want to count the hits in a similar manner to how the template tags provide, like this
{% get_hit_count for [object] within ["days=1,minutes=30"] %}
How exactly do I get the hit count for an object in a given timeframe inside of the views.py ?
I had the same predicament. So I looked into their views and implemented what you are asking in a function-based view. Let's pretend we want to count all views on a user's profile. The view:
import hitcount # import entire package
from hitcount.models import HitCount
def profile(request):
my_profile = Profile.objects.get(user=request.user)
# mark it
hit_count = HitCount.objects.get_for_object(my_profile)
hit_count_response = hitcount.views.HitCountMixin.hit_count(request, hit_count)
# count the views
profile_views = my_profile.hit_count.hits
return render( request, 'profile.html', locals() )
I found that I had to import entire hitcount package to work with hitcount.views.HitCountMixin. I guess my app was confusing hitcount.views.HitCountMixin with hitcount.models.HitCountMixin probably because I had imported everything from my models.py. So, if in your views.py you have from .models import * like me, it is wise to import entire package to specify which HitCountMixin you are referring to... Just like in the code above...
Anyway, ensure that the model you want to count hits inherits from hitcount.models.HitCountMixin and that you have a generic relationship to hitcount.models.HitCount) e.g.
from hitcount.models import HitCountMixin, HitCount
class Profile(models.Model, HitCountMixin):
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
hit_count_generic = GenericRelation( HitCount, object_id_field='object_pk',
related_query_name='hit_count_generic_relation' )
That's it. Logout and login again to your app - I guess to refresh the Django sessions framework (I had to).Then, at the template, call the profile views like a regular local ...
<p> Profile Views: {{profile_views}} </p>

Django-CMS CMSPlugin doesn't trigger pre_save/post_save signals

I am trying to add the content of Django-CMS placeholders to the search index (using Algolia, but I guess this could apply for any indexing service, like Elasticsearch or similar) as soon as they are updated.
Using Django 1.10, django-cms 3.42, I have this model (simplified for this question):
from django.db import models
from cms.models.fields import PlaceholderField
from cms.models import CMSPlugin
class NewsItem(models.Model):
title=models.CharField(_('title'), max_length=200),
content = PlaceholderField('news_content')
I need to do some extra processing as soon as the model field 'content' is saved, and apparently the best way to check for that is to monitor the CMSPlugin model. So I look for saves using from django.db.models.signals.post_save like this:
#receiver(post_save, sender=CMSPlugin)
def test(sender, **kwargs):
logger.info("Plugin saved.")
Now, the problem is that post_save is not triggered as I thought it would. With normal CMS Pages, I noticed that post_save is only triggered when a Page is published, but there is no apparent way to publish a placeholder when used outside the CMS.
The closest similar case I've found is Updating indexes on placeholderfields in real time with django/haystack/solr, but the suggested solution doesn't work.
How could I go about resolving this?
Thank you!
We also had the same search indexing problem when we were implementing djangocms-algolia package, since a placeholder update doesn't trigger an update of the index.
For CMS pages we utilized post_publish and post_unpublish from cms.signals module here.
And for cms apps that use placeholders (eg djangocms-blog) we attached the listeners to post_placeholder_operation, but beware that to make it work your ModelAdmin needs to inherit from PlaceholderAdminMixin:
def update_news_index(sender, operation: str, language: str, **kwargs) -> None:
placeholder: Optional[Placeholder] = None
if operation in (ADD_PLUGIN, DELETE_PLUGIN, CHANGE_PLUGIN, CLEAR_PLACEHOLDER):
placeholder = kwargs.get('placeholder')
elif operation in (ADD_PLUGINS_FROM_PLACEHOLDER, PASTE_PLUGIN, PASTE_PLACEHOLDER):
placeholder = kwargs.get('target_placeholder')
elif operation in (MOVE_PLUGIN, CUT_PLUGIN):
placeholder = kwargs.get('source_placeholder')
else:
pass
if placeholder:
post: Post = Post.objects.language(language_code=language).filter(content=placeholder).first()
if post:
post.save()
signals.post_placeholder_operation.connect(update_news_index, PostAdmin)
signals.post_placeholder_operation.connect(update_news_index, PageAdmin)

Auto register Django auth models using custom admin site

I implemented authentication management using Django auth with the default admin site but then I wanted to use my own AdminSite to rewrite some behaviors:
class OptiAdmin(admin.AdminSite):
site_title = "Optimizer site's admin"
#...Other stuff here
Then registered my own models:
admin_site = OptiAdmin(name='opti_admin')
admin.site.register(MyModel, MyModelAdmin)
#Other stuff here
But when I go to the admin site I am only able to see the models I just registered, which sounds fair to me but I would like to see all the other apps models in this new custom site including the auth's users and groups and I don't know how to do this automatically like the default admin does, pls help :).
Create your own AdminSite with a simple __init__() override.
Import your admin in urls.py.
Replacing the Django Admin and getting the autodiscover() behavior is possible with minimal effort. Here's a project structure generated in the typical django-admin startproject project fashion:
project/
manage.py
project/
__init__.py
settings.py
urls.py
wsgi.py
admin.py # CREATE THIS FILE
project/admin.py: (I think it makes the most sense to do this at the project level.)
from django.contrib.admin import * # PART 1
class MyAdminSite(AdminSite):
site_header = "My Site"
def __init__(self, *args, **kwargs):
super(MyAdminSite, self).__init__(*args, **kwargs)
self._registry.update(site._registry) # PART 2
site = MyAdminSite()
project/urls.py (snippet):
from . import admin # PART 3
urlpatterns = [
url(r'^admin/', admin.site.urls),
]
Part 1 is simple Python. By importing everything from django.contrib.admin into your namespace, it acts as a drop-in replacement. I suppose you don't have to do this, but it helps preserve expectations. Part 3, simply connect up your admin. Part 2 is the real trick. As the documentation says, autodiscover() is called to do the work. All autodiscover does is go through INSTALLED_APPS attempting to import a file called admin.py. Importing runs the code of course and that code is doing the same thing you do to register models (example by decorator and example by method). No magic. You don't have to register your models with your customized admin (as the documentation says).
Autodiscover looks smarter than it is with its register_to kwarg. That indicates you could call autodiscover() yourself passing your own admin. Nope; there's no wiring connected there (future feature?). The assignment happens here and is fixed to the native AdminSite instance here (or here using the decorator). Django contrib models register to that instance and so will any third-party libraries. It's not something you can hook into.
Here's the trick though, _registry is just a dictionary mapping. Let Django autodiscover all the things and then just copy the mapping. That's why self._registry.update(site._registry) works. "self" is your customized AdminSite instance, "site" is Django's instance and you can register your models with either.
(Final note: If models are missing, it's because of import order. All the registration to Django's AdminSite needs to happen before you copy _registry. Registering directly to your customized admin is probably the easiest thing.)
The Django docs suggest using SimpleAdminConfig with a custom admin site.
INSTALLED_APPS = (
...
'django.contrib.admin.apps.SimpleAdminConfig',
...
)
That prevents the models being registered with the default AdminSite.
The docs seem to assume that you will import the models individually and add them to your custom admin site:
from django.contrib.auth.models import Group, User
from django.contrib.auth.admin import GroupAdmin, UserAdmin
admin_site.register(Group, GroupAdmin)
admin_site.register(User, UserAdmin)
This would be very repetitive if you have models in many apps. It doesn't offer any advice how to automatically register models from all your apps with your custom site.
You could try monkey patching admin, and replacing admin.site with your own.
from django.contrib import admin
admin.site = OptiAdmin(name='opti_admin')
Then, when code called admin.site.register(), it would register the model with your admin site. This code would have to run before any models were registered. You could try putting it in the AppConfig for your app, and make sure that your app is above django.contrib.admin.
Adding to JCotton's great answer:
Using django 2.0, overriding site_header and site_title in the custom admin site only works for the index page.
To get it to work with all admin views, extend JCotton's code with the following:
def __init__(self, *args, **kwargs):
super(MyAdminSite, self).__init__(*args, **kwargs)
self._registry.update(site._registry) # PART 2
for model, model_admin in self._registry.items():
model_admin.admin_site = self
Just include init method in your CustomAdminSite class like this.
class CustomAdminSite(admin.AdminSite):
def __init__(self, *args, **kwargs):
super(CustomAdminSite, self).__init__(*args, **kwargs)
self._registry.update(admin.site._registry)

Custom URL for Wagtail model

I have a model like this:
from wagtail.wagtailcore.models import Page
class Blog(Page):
created = models.DateTimeField(auto_now_add=True)
...
..
Right Now, my default, if my slug is hi-there, the blog post is accessible on site_url:/hi-there/ but I want it accessible via site:url/2014/02/05/hi-there/ The page has various methods like url, url_path which one should I override and what's the best practice to achieve something like this in wagtail?
The RoutablePageMixin is the current (v2.0+) way to accomplish this.
Add the module to your installed apps:
INSTALLED_APPS = [
...
"wagtail.contrib.routable_page",
]
Inherit from both wagtail.contrib.routable_page.models.RoutablePageMixin and wagtail.core.models.Page, then define some view methods and decorate them with the wagtail.contrib.routable_page.models.route decorator:
from wagtail.core.models import Page
from wagtail.contrib.routable_page.models import RoutablePageMixin, route
class EventPage(RoutablePageMixin, Page):
...
#route(r'^$') # will override the default Page serving mechanism
def current_events(self, request):
"""
View function for the current events page
"""
...
#route(r'^past/$')
def past_events(self, request):
"""
View function for the past events page
"""
...
# Multiple routes!
#route(r'^year/(\d+)/$')
#route(r'^year/current/$')
def events_for_year(self, request, year=None):
"""
View function for the events for year page
"""
...
New in Wagtail v0.5 is a mechanism that directly addresses this sort of thing:
Embedding URL configuration in Pages or RoutablePage in v1.3.1
(the docs even have an Blog example!)
I was looking into migrating my blog to a wagtail version and wanted to support my previous url schema and had to solve this exact problem. Luckily I just found a solution and want to share it, hopefully this will be helpful for someone else in the future.
The solution is a 2 Step process.
change the url of a blog page to contain the date as well.
class Blog(Page):
created = models.DateTimeField(auto_now_add=True)
def get_url_parts(self, request=None):
super_response = super().get_url_parts(request)
# handle the cases of the original implementation
if super_response is None:
return None
(site_id, root_url, page_path) = super_response
if page_path == None:
return super_response
# In the happy case, add the date fields
# split prefix and slug to support blogs that are nested
prefix, slug, _ = page_path.rsplit("/", 2)
return (
site_id,
root_url,
f"{prefix}/{self.created.year}/{self.created.month}/{self.created.day}/{slug}/",
)
And now we need to make those posts also route-able.
class BlogIndexPage(RoutablePageMixin, Page):
...
def route(self, request, path_components):
if len(path_components) >= 4:
year, month, day, slug, *rest = path_components
try:
subpage = self.get_children().get(slug=slug)
return subpage.specific.route(request, rest)
except Page.DoesNotExist:
...
return super().route(request, path_components)
This solution ignores the date and just use the slug to locate a blog post as the original solution. This should also work when you don't use the RoutablePageMixin.
Hope this is still helpful for someone.

Getting Google App Engine blob info in Django view

This is a follow up question for Django on Google App Engine: cannot upload images
I got part of the upload of images to GAE Blobstore working. Here's what I did:
In models.py I created a model PhotoFeature:
class PhotoFeature(models.Model):
property = models.ForeignKey(
Property,
related_name = "photo_features"
)
caption = models.CharField(
max_length = 100
)
blob_key = models.CharField(
max_length = 100
)
In admin.py I created an admin entry with an override for the rendering of the change_form to allow for insert of the correct action to the Blobstore upload url:
class PhotoFeatureAdmin(admin.ModelAdmin):
list_display = ("property", "caption")
form = PhotoFeatureForm
def render_change_form(self, request, context, *args, **kwargs):
from google.appengine.ext import blobstore
if kwargs.has_key("add"):
context['blobstore_url'] = blobstore.create_upload_url('/admin/add-photo-feature')
else:
context['blobstore_url'] = blobstore.create_upload_url('/admin/update-photo-feature')
return super(PhotoFeatureAdmin, self).render_change_form(request, context, args, kwargs)
As I use standard Django, I want to use the Django views to process the result once GAE has updated the BlobStore in stead of BlobstoreUploadHandler. I created the following views (as per the render_change_form method) and updated urls.py:
def add_photo_feature(request):
def update_photo_feature(request):
This all works nicely but once I get into the view method I'm a bit lost. How do I get the Blob key from the request object so I can store it with PhotoFeature? I use standard Django, not Django non-rel. I found this related question but it appears not to contain a solution. I also inspected the request object which gets passed into the view but could not find anything relating to the blob key.
EDIT:
The Django request object contains a FILES dictionary which will give me an instance of InMemoryUploadedFile. I presume that somehow I should be able to retrieve the blob key from that...
EDIT 2:
Just to be clear: the uploaded photo appears in the Blobstore; that part works. It's just getting the key back from the Blobstore that's missing here.
EDIT 3:
As per Daniel's suggestion I added storage.py from the djangoappengine project which contains the suggested upload handler and added it to my SETTINGS.PY. This results in the following exception when trying to upload:
'BlobstoreFileUploadHandler' object has no attribute 'content_type_extra'
This is really tricky to fix. The best solution I have found is to use the file upload handler from the djangoappengine project (which is associated with django-nonrel, but does not depend on it). That should handle the required logic to put the blob key into request.FILES, as you'd expect in Django.
Edit
I'd forgotten that django-nonrel uses a patched version of Django, and one of the patches is here to add the content-type-extra field. You can replicate the functionality by subclassing the upload handler as follows:
from djangoappengine import storage
class BlobstoreFileUploadHandler(storage.BlobstoreFileUploadHandler):
"""Handler that adds blob key info to the file object."""
def new_file(self, field_name, *args, **kwargs):
# We need to re-process the POST data to get the blobkey info.
meta = self.request.META
meta['wsgi.input'].seek(0)
fields = cgi.FieldStorage(meta['wsgi.input'], environ=meta)
if field_name in fields:
current_field = fields[field_name]
self.content_type_extra = current_field.type_options
super(BlobstoreFileUploadHandler, self).new_file(field_name,
*args, **kwargs)
and reference this subclass in your settings.py rather than the original.