Override Django Admin URLs for Specific Model? - django

First a little background:
I have an Event model that has various event_types. I want to break one of those event types, 'Film', into it's own admin. I have the basic functionality in place: a proxy model inheriting from Event, named Film, a custom manager for that proxy model that filters it to only 'film' event types, and it's own ModelAdmin.
The problem is with the reverse. I now need to filter out films from the main Event admin. I don't want to alter the Event model or its default manager, because the impact would be too widespread. So, I tried creating another proxy model, EventAdminProxy, with the sole purpose of providing a filtered list of events in the admin. I then register this model, instead of Event, with the existing ModelAdmin.
This obviously works, but it has the unfortunate side-effect of altering the URLs in the admin. Instead of the changelist being at "/admin/event/event/", it's now at "/admin/event/eventadminproxy/".
What I'm trying to do is keep this setup, but also keep the old URL. I've tried overloading the ModelAdmin's get_urls method, but from what I can tell, you can't control the full URL there, only what comes after the "/app_label/model_class/" part.
I thought about overriding it in the main urls.py, but can't figure out an acceptable view to tie into. The actual views are only available on the instantiated ModelAdmin object, not the class itself.
Any ideas of how override the URL being used in the admin?

Looking at the Django source, the admin URLs are built in two places, in the ModelAdmin instances, and in the AdminSite instances.
The part you want to change is built in the AdminSite instance (django.contrib.admin.sites.AdminSite), you can subclass that and override the get_urls method. If you look at the second half of the method you'll see this:
# Add in each model's views.
for model, model_admin in self._registry.iteritems():
urlpatterns += patterns('',
url(r'^%s/%s/' % (model._meta.app_label, model._meta.module_name),
include(model_admin.urls))
)
There it is adding the model's ._meta.module_name which is just the model's name lowercased (django.db.models.options.Options.contribute_to_class).
An easy way out is to override the Site's get_urls method and add a dict or special case for the Proxy model so it uses a different url instead of model._meta.module_name, something along the lines:
class MyAdminSite(AdminSite):
module_name_dict = {
EventAdminProxy: 'myfunkymodulename'
}
def get_urls(self):
base_patterns = super(MyAdminSite, self).get_urls()
my_patterns = patterns('',)
for model, model_admin in self._registry.iteritems():
if model in self.module_name_dict:
module_name = self.module_name_dict[model]
my_patterns += patterns('',
url(r'^%s/%s/' % (model._meta.app_label, module_name),
include(model_admin.urls))
)
return my_patterns + base_patterns

You could override the queryset-method of your EventModelAdmin and filter the queryset so that Film-Events get excluded.
Something similar to this:
class EventAdmin(admin.ModelAdmin):
def queryset(self, request):
qs = super(EventAdmin, self).queryset(request)
return qs.exclude(event_type='film')

You could also subclass ChangeList and override the url_for_result() method to customise change urls, (learned from another answer), e.g.:
from django.contrib.admin.views.main import ChangeList
class FooChangeList(ChangeList):
def url_for_result(self, obj):
return '/foos/foo/{obj.pk}/'
class FooAdmin(admin.ModelAdmin):
def get_changelist(self, request, **kwargs):
return FooChangeList
Adapted example for the question:
from django.contrib.admin.views.main import ChangeList
from django.urls import reverse
class FilmAdmin(admin.ModelAdmin):
def get_changelist(self, request, **kwargs):
class FilmChangeList(ChangeList):
def url_for_result(self, obj):
return reverse('admin:events_event_change', args=(obj.pk, ))
return FilmChangeList

Related

Django admin loads view template when url pattern is same as model name

I'm writing my first Django project in Django 2.0.
I noticed another weird behavior with Django urlpatterns.
I have an app starrednotes and model within it as Starred(models.Model)
same is the case with Shared(models.Model) within sharednotes app
I have configured the urlpattern for with path pattern same as the model name
urlpatterns = [
url(r'^starred/$', StarredNotes.as_view(), name='starred'),
url(r'^shared/$', SharedNotes.as_view(), name='shared'),
]
and view StarredNotes is
class StarredNotes(ListView):
template_name = 'notes/starred.html'
model = Starred
context_object_name = 'starred_notes'
def get_queryset(self):
starred_notes = Starred.objects.filter(user=self.request.user)
return starred_notes
#method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super(self.__class__, self).dispatch(request, *args, **kwargs)
The URL setup above is accessible using
http://example.com/notes/shared
http://example.com/notes/starred
But when I'm accessing these two models from admin with URL as
http://example.com/admin/sharednotes/shared
http://example.com/admin/starrednotes/starred
These two links are loading the template setup in the StarredNotes and SharedNotes class instead of admin template.
I can not understand why it is behaving like this. Is it a Django limitation or bug in Django.
Anyway here are two ways you can get rid of it.
1. Change urlpattern
change URL pattern and replace the pattern with something other than the model name.
In my case, this is how my urlpatterns looks now.
urlpatterns = [
url(r'^starred/$', StarredNotes.as_view(), name='starred'),
url(r'^shared/$', SharedNotes.as_view(), name='shared'),
]
2. Change model name
This is not the recommended way but you can rename your model to something else only if urlpattern is more important than the model. Changing model name may require changes to many other places as well.
class SharedNote(models.Model):
# model fields
class StarredNote(models.Model):
# model fields

Use ModelAdmin form in any view

I've spent quite a bit of time setting up the ModelAdmin for my models, and I want to be able to basically reuse it elsewhere on my site, i.e., not just in the admin pages.
Specifically, I want to further subclass the ModelAdmin subclass I created and override some of the methods and attributes to be non-admin specific, then use that second subclass to create a form from a model in one of my ordinary (non-admin) views. But I'm not sure how to get the form out of the ModelAdmin subclass or how to bind it to a specific model.
So for instance, my admin.py looks something like this:
from django.contrib import admin
class MyAdmin(admin.ModelAdmin):
fields = ['my_custom_field']
readonly_fields = ['my_custom_field']
def my_custom_field(self, instance):
return "This is my admin page."
And this works real nice. Now in I want to subclass this as:
class MyNonAdmin(MyAdmin):
def my_custom_field(self, instance):
return "This is NOT my admin page!"
Now I want to use the MyNonAdmin in a view function to generate a form bound to a particular model, and use that form in a template. I'm just not sure how to do that.
Update
I found how to render the form, but it doesn't include any of the readonly_fields. Rendering the form looks like this:
def view_func(request, id):j
res = get_object_or_404(ModelClass, pk=int(id))
admin = MyNonAdmin(ModelClass, None)
form = admin.get_form(request, res)
return render(request, 'my_template.html', dict(
form=form,
))
But as I said, it only renders the form itself, not the other readonly_fields which is what I really care about.

Django admin override save behaviour

Using the Django Admin I would like to have a 'confirmation box' when someone presses the save button in the admin interface i.e. you are trying to update "name or age or sex" to "foo or 23 or m".
You could overwrite the get_form method in your model admin to add another check or the save() method to create a warning. You could also add an intermediate page (like the delete view does)...
i.e.
class MyModelAdmin(admin.ModelAdmin):
def get_urls(self):
urls = super(MyModelAdmin, self).get_urls()
my_urls = patterns('',
(r'^my_view/$', self.my_view)
)
return my_urls + urls
def my_view(self, request):
# custom view which should return an HttpResponse
pass
Read more: http://www.ibm.com/developerworks/web/library/os-django-admin/index.html?ca=drs
If you want a JavaScript method then I imagine you could just overwrite the admin view for that very easily and add a simple confirmation when save is click i.e.
Dave
As the OP is very slim, with no code examples I cannot really help beyond this general answer.

Django class-based "method_splitter" - passing 2 slugs as model name and field value, respectively

I want to create a "method_splitter" equivalent using class-based views in order to remove some hard-coding in my URL confs.
I would like to have the following URL's:
ListView: http://mysite.com/<model_name>/
DetailView: http://mysite.com/<model_name>/<field_value>/
where the query for the ListView would be:
<model_name>.objects.all()
and the queryset for the DetailView would be:
<model_name>.objects.get(<field>=<field_Value>)
Currently, my views work as a result of some hardcoding in the url conf, but I would like to find an elegant solution that can scale.
My solution does not give a 404, but displays nothing:
views.py
class ListOrDetailView(View):
def __init__(self, **kwargs):
for key, value in kwargs.iteritems():
setattr(self, key, value)
try: #If/Else instead?
def goToDetailView(self, **kwargs):
m = get_list_or_404(self.kwargs['model']) #Is this even needed?
return DetailView(model=self.kwargs['model'], slug=self.kwargs['slug'], template_name='detail.html', context_object_name='object')
except: #If/Else instead?
def goToListView(self, **kwargs):
q = get_object_or_404(self.kwargs['model'], slug=self.kwargs['slug']) #Is this even needed?
return ListView(model=self.kwargs['model'], template_name='list.html', context_object_name='object_list',)
urls.py of MyApp
url(r'^(?P<model>[\w]+)/?(?P<slug>[-_\w]+)/$', ListOrDetailView.as_view()),
As limelights said, this is a horrible design pattern; the whole point of Django is separation of models and views. If you fear that you might have to write a lot of repetitive code to cover all your different models, trust me that it's not much of an issue with class-based views. Essentially, you need to write this for each of your models you wish to expose like this:
Urls.py:
urlpatterns = patterns('',
url(r'^my_model/$', MyModelListView.as_view(), name="mymodel_list"),
url(r'^my_model/(?P<field_value>\w+)/$', MyModelDetailView.as_view(), name="mymodel_detail"),
)
views.py:
from django.views.generic import ListView, DetailView
class MyModelListView(ListView):
model = MyModel
class MyModelDetailView(DetailView):
model = MyModel
def get_queryset(self):
field_value = self.kwargs.get("field_value")
return self.model.objects.filter(field=field_value)

Implementing a generic protected preview in Django

I'm trying to add some kind of generic preview functionality to the Django admin. Opposed to Django's builtin preview-on-site functionality this preview should only be visible to logged in users with specific permissions.
All my content models have the same base class which adds a status like published and unpublished. Obviously unpublished content doesn't appear on the website, but editors should still be able to preview an unpublished site.
I read about class based views in the upcoming Django 1.3 release which might be well suited to implement it in a generic way. With Django 1.2 i can't seem to come up with a solution without touching any single view and adding specific permission checks. Has anyone done something like that before?
I believe the Django Admin already provides a "show on site" option to the admin pages of any models which provides a get_absolute_url() method. Using decorators, it should be possible to do this in a generic way across models
class MyArticleModel(Article): #extends your abstract Article model
title = .....
slug = ......
body = ......
#models.permalink
def get_absolute_url(self): # this puts a 'view on site' link in the model admin page
return ('views.article_view', [self.slug])
#------ custom article decorator -------------------
from django.http import Http404
from django.shortcuts import get_object_or_404
def article(view, model, key='slug'):
""" Decorator that takes a model class and returns an instance
based on whether the model is viewable by the current user. """
def worker_function(request, **kwargs):
selector = {key:kwargs[key]}
instance = get_object_or_404(model, **selector)
del kwargs[key] #remove id/slug from view params
if instance.published or request.user.is_staff() or instance.author is request.user:
return view(request, article=instance, **kwargs)
else:
raise Http404
return worker_function
#------- urls -----------------
url(r'^article/(?(slug)[\w\-]{10-30})$', article_view, name='article-view'),
url(r'^article/print/(?(id)\d+)$',
article(view=generic.direct_to_template,
model=MyArticleModel, key='id'),
name='article-print-view'
)
#------ views ----------------
from django.shortcuts import render_to_response
#article(MyArticleModel)
def article(request, article):
#do processing!
return render_to_response('article_template.html', {'article':instance},
xontext_instance=RequestContext(request) )
Hope this is informative (and hopefully correct ;)