Is there a way to extend another apps ModelAdmin?
I have a project that uses functionality offered by django.contrib.comments.
The CommentsAdmin ModelAdmin class has:
actions = ["flag_comments", "approve_comments", "remove_comments"]
I would like to extend the CommentsAdmin ModelAdmin in my project to include an action ban_user.
I've tried creating my own NewCommentsAdmin(CommentsAdmin) object in my admin.py file and registering it, but I get a notice 'AlreadyRegistered at /admin/' 'The model Comment is already registered'.
class NewCommentAdmin(CommentAdmin):
actions = ['ban_user']
def ban_user(self, request, queryset):
pass
admin.site.register(Comment, NewCommentAdmin)
Is there a way to do this without modifying the original django.contrib.comments code?
Here's how I do it in one project for the User model. In the admin.py for my app:
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
class MyUserAdmin(UserAdmin):
# ...
admin.site.unregister(User)
admin.site.register(User, MyUserAdmin)
Unregister the Comment model first.
I guess you have something like this at the top of your file:
from django.contrib.comments.admin import CommentAdmin
This import executes the registration of the model (at the very bottom of this admin file) again.
One idea that doesn't look very nice (I actually haven't tried it) could be:
from django.contrib.comments.models import Comment
from django.contrib import admin
from django.contrib.admin.sites import NotRegistered
# Try to unregister the Comment model
# that was registered via the auto_discover method
try:
admin.site.unregister(Comment)
except NotRegistered:
pass
# Now we can load the CommentAdmin (which reregisters the admin model)
from django.contrib.comments.admin import CommentAdmin
# We have to unregister again:
try:
admin.site.unregister(Comment)
except NotRegistered:
pass
# Now your stuff...
I guess this could be done better but it should work. To make this approach work, the application that contains this file has to be after the comments application in INSTALLED_APPS.
Now to your class. I think if you write actions = ['ban_user'] you actually overwrite all the actions in the parent class. I think it is the easiest way to override the get_actions method:
class NewCommentAdmin(CommentAdmin):
def get_actions(self, request):
actions = super(NewCommentAdmin, self).get_actions(request)
# Do some logic here based on request.user if you want
# to restrict the new action to certain users
actions.append('ban_user')
return actions
def ban_user(self, request, queryset):
pass
admin.site.register(Comment, NewCommentAdmin)
Hope that helps (or at least gives an idea) :)
Have a look at https://github.com/kux/django-admin-extend
It offers some easy to use functions and decorators that implement the functionality you're requesting in a very flexible manner. The documentation does a pretty good job at explaining why using this approach is better than direct inheritance.
It also has support for injecting bidirectional many to many fields.
Related
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)
Well, let me explain this.
I am working on a simple django admin project.
In the admin.py file, I have the following admin classes:
class A_Admin(admin.ModelAdmin):
#some stuff
class B_Admin(admin.ModelAdmin):
#some stuff
I want to override the get_urls() method of A_Admin that if I click a button on A_Admin instance change page, it will redirect the page to B_Admin changelist page.
(I know there are many ways to do what I want and what I mentioned above is not the best, but this is what I want. So let skip the discussion why I insist on this solution.)
I want to the following:
def get_urls(self):
#django's code
#inside the urlpattern
urlpattern = (
#default urls from django admin
.....
url(r'^some_url$',
wrap(super(B_Admin, self).changelist_view),
name='%s_%s_delete' % info),
....)
return urlpatterns
This is not working, since 'self' is a A_Admin class object rather than B_Admin obejct.
So is there any way to get the proxy of calss A_Admin inside B_Admin?
I just wanna override changelist_view of A and call it inside B.
Is this possible?
Thanks in advance
You should just instantiate B_Admin and use its method.
I believe the following code should work:
from django.contrib import admin
from my_app.models import B_Model # The model for which B_Admin is used
def get_urls(self):
#django's code
#inside the urlpattern
urlpattern = (
#default urls from django admin
.....
url(r'^some_url$',
wrap(B_Admin(B_Model, admin.site).changelist_view),
name='%s_%s_delete' % info),
....)
return urlpatterns
UPDATE: Most probably, B_Admin was already instantiated when you called
admin.site.register(B_Model, B_Admin)
So instead of doing
B_Admin(B_Model, admin.site)
again you can just get it from the AdminSite's registry:
admin.site._registry[B_Model]
I have a meta class for the Django User model that I use to add extra methods (overly simplified version):
# project.models.pie_lover.py
from django.contrib.auth.models import User
class PieLover(User):
class Meta:
app_label = "core"
proxy = True
def likes_pie(self):
return True
In my view, I wish to get the logged in PieLover and see if he likes pie (silly thing to do because a PieLover always loves pie, but in a real world situation this may not be the case). My problem lies in the way Django logs in the users, I use the built-in login(request) function for this and as a result the object stored in request.user is a User object, not a PieLover.
# project.views.like_pie.py
from ..models.pie_lover import PieLover
def index(request):
pie_lover = request.user
if pie_lover.likes_pie():
print "Try pie, try!"
else:
print "BLASPHEMER!"
If I try to do this Django tells me that the User object has no method likes_pie which is to be expected as request.user is not a PieLover instance.
As a quick workaround I just get the PieLover which has the same ID as the User but that means an extra DB hit.
How can I make Django use PieLover by default in the request?
I was thinking that instead of making another database query to get the proper PieLover object to create a new PieLover object and pass request.user to it at initialization but I don't know what the implications of this are.
After poking around I found, what seems to me, the easiest and non-hackish way to access the PieLover methods for a given User instance. I have added this to a custom middleware:
from models.pie_lover import PieLover
class PieLoverMiddleware(object):
def process_request(self, request):
request.pie_lover = PieLover(request.user)
How can I make Django use PieLover by default in the request?
You don't.
Read this before you do anything else: https://docs.djangoproject.com/en/1.3/topics/auth/#storing-additional-information-about-users
Your "extension" to user should be a separate model with all of your extension methods in that separate model.
You can then navigate from User (in the request) to your extension trivially using the get_profile() method already provided.
from django.contrib.auth import models as auth_models
def _my_func(self):
return True
auth_models.User.my_func = _my_func
I'm using django-socialregistration to manage my site's connection with Facebook.
When a user clicks the "Connect with Facebook" button, I am able to automatically create a new Django user and log them in. However, I also need to create a UserProfile (my AUTH_PROFILE_MODULE) record for them which contains their Facebook profile information (email, name, location).
I believe I need to override socialregistration's "setup" view so I can do what I need to do with UserProfile. I've added the following to my project's urls.py file:
url( r'^social/setup/$', 'myapp.views.socialreg.pre_setup', name='socialregistration_setup'),
My custom view is here "/myapp/views/socialreg.py" and looks like:
from socialregistration.forms import UserForm
def pre_setup(request, template='socialregistration/setup.html',
form_class=UserForm, extra_context=dict()):
# will add UserProfile storage here...
return socialregistration.views.setup(request, template, form_class, extra_context)
The socialregistration view signature I'm overriding looks like this:
def setup(request, template='socialregistration/setup.html',
form_class=UserForm, extra_context=dict()):
...
I'm getting the error "ViewDoesNotExist at /social/setup/: Could not import myapp.views.socialreg. Error was: No module named socialregistration.views" when I try the solution above.
The socialregistration app is working fine when I don't try to override the view, so it is likely installed correctly in site-packages. Anyone know what I'm doing wrong?
OK, as Tim noted, this particular problem was path related.
Bigger picture, the way to accomplish what I wanted (creating a linked UserProfile when django-socialregistration creates a user) is best done by passing in a custom form into socialregistration's "setup" view, as the author suggested here: http://github.com/flashingpumpkin/django-socialregistration/issues/issue/36/#comment_482137
Intercept the appropriate url in your urls.py file:
from myapp.forms import UserForm
url('^social/setup/$', 'socialregistration.views.setup',
{ 'form_class': UserForm }, name='socialregistration_setup'),
(r'^social/', include('socialregistration.urls')),
You can base your UserForm off socialregistration's own UserForm, adding in code to populate and save the UserProfile.
I've been struggling with this problem for 5 hours and I have a feeling it's a simple solution that I'm just overlooking.
I'm trying to tie in a third party module (Django Activity Stream) that uses a series of senders and receivers to post data about user activity to a database table. Everything is set up and installed correctly, but I get a 'Signal' Object has No Attribute 'Save' error when I try to run it.
I suspect the problem is in my syntax somewhere. I'm just getting started with Signals, so am probably overlooking something a veteran will spot immediately.
In views.py I have:
from django.db.models.signals import pre_save
from actstream import action ##This is the third-party app
from models import Bird
def my_handler(sender, **kwargs):
action.save(sender, verb='was saved')
#return HttpResponse("Working Great")
pre_save.connect(my_handler, sender=Bird)
def animal(request):
animal = Bird()
animal.name = "Douglas"
animal.save()
The Django Activity Stream app has this signals.py file:
from django.dispatch import Signal
action = Signal(providing_args=['actor','verb','target','description','timestamp'])
And then this models.py file:
from datetime import datetime
from operator import or_
from django.db import models
from django.db.models.query import QuerySet
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from django.utils.timesince import timesince as timesince_
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User
from actstream import action
...
def action_handler(verb, target=None, **kwargs):
actor = kwargs.pop('sender')
kwargs.pop('signal', None)
action = Action(actor_content_type=ContentType.objects.get_for_model(actor),
actor_object_id=actor.pk,
verb=unicode(verb),
public=bool(kwargs.pop('public', True)),
description=kwargs.pop('description', None),
timestamp=kwargs.pop('timestamp', datetime.now()))
if target:
action.target_object_id=target.pk
action.target_content_type=ContentType.objects.get_for_model(target)
action.save()
action.connect(action_handler, dispatch_uid="actstream.models")
Your main problem is in the discipline in maintaining coding style, or rather in this case, lack of. You will find that it is easier to identify problems in your code if you do not use the same name to refer to multiple things within the same module; give each object a unique, meaningful name, and refer to it using only that name.
The bottom line here is that the docs for that project contain bad code. This line:
action.save(sender, verb='was saved')
isn't ever going to work. The from actstream import action ultimately imports a signal from actstream.signals, and signals do not and never have had a save method. Especially not with such an odd signature of sender, verb.
At first I thought maybe the author had done something odd with subclassing Signal, but after looking at the rest of the codebase, that's just not the case. I'm not entirely sure what the intention of those docs was supposed to be, but the right thing to do in your handler will either be to save a new Action (imported from actstream.models) instance, or to do something with your model.
Sadly, the project's repository has a pretty sorry set of tests/examples, so without downloading and trying the app myself, I can't tell you what needs to happen there. You might try contacting the author or simply try finding a better-documented/better-maintained Activity Streams app.