How do I reverse the URL for an admin action? - django

I'm posting this because I searched stackoverflow and docs for a long time without finding an answer -- hopefully this helps somebody out.
The question is, for testing purposes, how do I find the URL that's related to admin actions for a specific model?
Admin model urls can all be found by reverse(admin:appname_modelname_*), where * is the action (change, delete, etc). But I couldn't find one for the admin actions, and since I was defining custom actions, I'd like to get the url.

This took a fair bit of digging, I couldn't find anything in the Django docs about it and I ended up having to inspect the source code of a third party library.
Essentially there are 2 URL patterns, one for bulk actions and one for object actions:
Bulk: r'admin/<app_label>/<model_name>/actions/(?P<tool>\\w+)/$'
Object: r'admin/<app_label>/<model_name>/(?P<pk>.+)/actions/(?P<tool>\\w+)/$'
The URL name pattern is <app_label>_<model_name>_actions
Therefore we can reverse the bulk view:
Using args: reverse("admin:<app_label>_<model_name>_actions", args=["foo"])
Using kwargs: reverse("admin:<app_label>_<model_name>_actions", kwargs={"tool": "foo"})
and reverse the object view:
Using args: reverse("admin:<app_label>_<model_name>_actions", args=[1, "foo"])
Using kwargs: reverse("admin:<app_label>_<model_name>_actions", kwargs={"pk": 1, "tool": "foo"})

The URL for all custom actions is reverse(admin:<appname>_<modelname>_changelist), but the action name is specified in the action field of the POST data.

The answer, which is hard to find, is that actions are referenced by reverse(admin:appname_modelname_changelist)

Related

Django syndication framework: prevent appending SITE_ID to the links

According to the documentation here: https://djangobook.com/syndication-feed-framework/
If link doesn’t return the domain, the syndication framework will
insert the domain of the current site, according to your SITE_ID
setting
However, I'm trying to generate a feed of magnet: links. The framework doesn't recognize this and attempts to append the SITE_ID, such that the links end up like this (on localhost):
<link>http://localhost:8000magnet:?xt=...</link>
Is there a way to bypass this?
Here's a way to do it with monkey patching, much cleaner.
I like to create a separate folder "django_patches" for these kinds of things:
myproject/django_patches/__init__.py
from django.contrib.syndication import views
from django.contrib.syndication.views import add_domain
def add_domain_if_we_should(domain, url, secure=False):
if url.startswith('magnet:'):
return url
else:
return add_domain(domain, url, secure=False)
views.add_domain = add_domain_if_we_should
Next, add it to your INSTALLED_APPS so that you can patch the function.
settings.py
INSTALLED_APPS = [
'django_overrides',
...
]
This is a bit gnarly, but here's a potential solution if you don't want to give up on the Django framework:
The problem is that the method add_domain is buried deep in a huge method within syndication framework, and I don't see a clean way to override it. Since this method is used for both the feed URL and the feed items, a monkey patch of add_domain would need to consider this.
Django source:
https://github.com/django/django/blob/master/django/contrib/syndication/views.py#L178
Steps:
1: Subclass the Feed class you're using and do a copy-paste override of the huge method get_feed
2: Modify the line:
link = add_domain(
current_site.domain,
self._get_dynamic_attr('item_link', item),
request.is_secure(),
)
To something like:
link = self._get_dynamic_attr('item_link', item)
I did end up digging through the syndication source code and finding no easy way to override it and did some hacky monkey patching. (Unfortunately I did it before I saw the answers posted here, all of which I assume will work about as well as this one)
Here's how I did it:
def item_link(self, item):
# adding http:// means the internal get_feed won't modify it
return "http://"+item.magnet_link
def get_feed(self, obj, request):
# hacky way to bypass the domain handling
feed = super().get_feed(obj, request)
for item in feed.items:
# strip that http:// we added above
item['link'] = item['link'][7:]
return feed
For future readers, this was as of Django 2.0.1. Hopefully in a future patch they allow support for protocols like magnet.

Filter all queries based on url parameters

I have in a Django project all my urls based on the following syntax:
/ID_PROGRAM/ID_PROJECT/blablabla
I would like by default that all my queries have the following filters:
.filter(program=ID_PROGRAM).filter(project=ID_PROJECT)
How can I apply these filters automatically to all my queries? My idea was to define a new manager. But is the manager able to access to the url parameters? I this the best way to do?
To complet the question, I want to enrich all my queries without having to pass explicitly the view parameters to the manager.
You could have just tried it to see if it works.
Yes, managers do accept parameters
class MyModelManager(models.Manager):
def my_filters(self, id_prog, id_proj):
return super(MyModelManager, self).get_query_set().filter(program=id_prog, project=id_proj)
and in the views:
MyModelManager.objects.my_filters(id_prog, id_proj)
Documentation on custom managers
Python promotes "Explicit is better than implicit"
karthikr is almost right, but you can also use:
1 - decorator above your function. Decorator will get args from url and put objects to any variable
2 - write mixin and aply it to view. Mixin will get args from url at overriden dispatch and save filter result to self.custom_context. Override get_context_data to merge contexts.

Django Forbid HttpResponse

I am working on a tiny movie manager by using the out-of-the-box admin module in Django.
I add a "Play" link on the movie admin page to play the movie, by passing the id of this movie. So the backend is something like this:
import subprocess
def play(request, movie_id):
try:
m = Movie.objects.get(pk=movie_id)
subprocess.Popen([PLAYER_PATH, m.path + '/' + m.name])
return HttpResponseRedirect("/admin/core/movie")
except Movie.DoesNotExist:
return HttpResponse(u"The movie is not exist!")
As the code above reveals, every time I click the "play" link, the page will be refreshed to /admin/core/movie, which is the movie admin page, I just do not want the backend to do this kind of things, because I may use the "Search" functions provided by the admin module, so the URL before clicking on "Play" may be something like: "/admin/core/movie/?q=gun", if that response takes effect, then the query criteria will be removed.
So, my thought is whether I can forbid the HttpResponse, in order to let me stay on the current page.
Any suggestions on this issue ?
Thanks in advance.
I used the custom action in admin to implement this function.
So finally I felt that actions are something like procedures, which have no return values, and requests are something like methods(views) with return values...
Thanks !

Creating unique URL/address for a resource to share - Best practices

In my application there is a need to create unique URLs (one per resource) that can be shared. Something like Google Calendar Private address for a calendar. I want to know what are the best practices for this.
If it helps my application is in Django.
Please let me know if this question needs more explanation.
This should be very straightforward. In your urls.py file you want a url like this:
url(r'/resource/(?P<resource_name>\w+)', 'app.views.resource_func', name="priv-resource"),
Then you handle this in views.py with a function called:
def resource_func(request, resource_name):
# look up resource based on unique string resource_name...
Finally, you get to use this in your templates too, using naming:
{% url priv-resource string %}
Just ensure that in your models.py:
class ResourceModel(models.Model)
resource_name = models.CharField(max_size=somelimit, unique=True)
I might even be tempted to use a signal handler to generate this field automatically upon save of the object. See the documentation.

django admin actions on all the filtered objects

Admin actions can act on the selected objects in the list page.
Is it possible to act on all the filtered objects?
For example if the admin search for Product names that start with "T-shirt" which results with 400 products and want to increase the price of all of them by 10%.
If the admin can only modify a single page of result at a time it will take a lot of effort.
Thanks
The custom actions are supposed to be used on a group of selected objects, so I don't think there is a standard way of doing what you want.
But I think I have a hack that might work for you... (meaning: use at your own risk and it is untested)
In your action function the request.GET will contain the q parameter used in the admin search. So if you type "T-Shirt" in the search, you should see request.GET look something like:
<QueryDict: {u'q': [u'T-Shirt']}>
You could completely disregard the querystring parameter that your custom action function receives and build your own queryset based on that request.GET's q parameter. Something like:
def increase_price_10_percent(modeladmin, request, queryset):
if request.GET['q'] is None:
# Add some error handling
queryset=Product.objects.filter(name__contains=request.GET['q'])
# Your code to increase price in 10%
increase_price_10_percent.short_description = "Increases price 10% for all products in the search result"
I would make sure to forbid any requests where q is empty. And where you read name__contains you should be mimicking whatever filter you created for the admin of your product object (so, if the search is only looking at the name field, name__contains might suffice; if it looks at the name and description, you would have a more complex filter here in the action function too).
I would also, maybe, add an intermediate page stating what models will be affected and have the user click on "I really know what I'm doing" confirmation button. Look at the code for django.contrib.admin.actions for an example of how to list what objects are being deleted. It should point you in the right direction.
NOTE: the users would still have to select something in the admin page, otherwise the action function would never get called.
This is a more generic solution, is not fully tested(and its pretty naive), so it might break with strange filters. For me works with date filters, foreign key filters, boolean filters.
def publish(modeladmin,request,queryset):
kwargs = {}
for filter,arg in request.GET.items():
kwargs.update({filter:arg})
queryset = queryset.filter(**kwargs)
queryset.update(published=True)