Django Admin Model on add fails to render related change link - python

Given following admin settings:
class BrokerLocationSetForm(forms.ModelForm):
class Meta:
model = BrokerLocationSet
fields = ('broker', 'program', 'label', 'locations')
widgets = {
'locations': autocomplete.ModelSelect2Multiple(url='admin-autocomplete-location', forward=('broker','program')),
}
class BrokerLocationSetAdmin(admin.ModelAdmin):
model = BrokerLocationSet
form = BrokerLocationSetForm
list_display=['broker', 'program', 'label']
admin.site.register(BrokerLocationSet, BrokerLocationSetAdmin)
When I try navigate to add view in admin for BrokerLocationSetForm it raises following error:
raise NoReverseMatch(msg) NoReverseMatch: Reverse for 'program_program_change' with arguments '(u'__fk__',)' not found. 1 pattern(s) tried: [u'admin/program/program/(?P<program_pk>\\d+)/change/$']
When I debug in shell:
reverse('admin:broker_broker_change', 'myapp.urls', args=(u'__fk__',))
it outputs:
u'/admin/broker/broker/fk/change/'
but for:
reverse('admin:program_program_change', 'myapp.urls', args=(u'__fk__',))
I get same error as above. After some debugging I sensed that somehow admin was passing a string instead of an int into reverse function while it expected an integer as below :
reverse('admin:program_program_change', 'myapp.urls', args=(u'1',))
u'/admin/program/program/1/change/'
Since django admin does this url reversing magic I am not sure where I should customize this to fix the bug. I have got this code base fairly new and to get sense completely.
How I can fix above bug by customizing admin model or form. I dont want to update 'admin:program_program_change' but probably provide an alternate route to same view! . Is it possible ? please advise !

I found a solution however, I am not sure if this is best one. Since ProgramAdmin expects a numeric parameter while popup link from BrokerLocationSetAdmin is expecting a route with a string parameter. e.g
reverse('admin:program_program_change', 'myapp.urls', args=(u'__fk__',))
Solution was to inject another admin route with same name to ProgramAdmin model by overriding its get_urls method as follow:
class ProgramAdmin(admin.ModelAdmin):
...
...
def get_urls(self):
from django.conf.urls import url
from functools import update_wrapper
def wrap(view):
def wrapper(*args, **kwargs):
return self.admin_site.admin_view(view)(*args, **kwargs)
wrapper.model_admin = self
return update_wrapper(wrapper, view)
urls = super(ProgramAdmin, self).get_urls()
info = self.model._meta.app_label, self.model._meta.model_name
alt_urls=[
url(r'^(?P<program_pk>\w+)/change/$', wrap(self.change_view), name='%s_%s_change' % info),
]
return urls+alt_urls
Now we have two routes with same name but different paths parameter e.g:
/admin/program/program/<program_pk>/change/ django.contrib.admin.options.change_view admin:program_program_change
admin/program/program/(?P\d+)/change/$
/admin/program/program/<program_pk>/change/ django.contrib.admin.options.change_view admin:program_program_change
admin/program/program/(?P\w+)/change/$
Depending on the context one of route will be used.

Related

Extending Wagtail Modeladmin CreateView I get a NoReverseMatch and if I comment the code the routing problem persists

On wagtail modeladmin for a given model, I need to modify the Create_Button so that it shows a create view with a subset of model fields and adding an API call to an external service.
Overriding the Edit Button I could do this seamlessly, just binding the property
#wagtail_hooks.py
class MyModelAdmin(Modeladmin):
model = MyModel
...
edit_view_class = MyModelEditView
...
and on the MyModelEditView(EditView) just added my extended logic on the form_valid method.
Following the same pattern, I created a MyModelCreateView(CreateView),
added the property to MyModelAdmin as:
create_view_class=MyModelCreateView
which is defined like:
#wagtail_hooks.py
class MyModelCreateView(CreateView):
"""
we only want user to define 2 fields of the model, then call an API REST
that returns the other fields, then save object to backend db
"""
panels = [
FieldPanel('field1'),
FieldPanel('field2')
]
def get_edit_handler(self):
panels = self.panels
edit_handler = ObjectList(panels).bind_to(model=MyModel, request=self.request)
return edit_handler
def create_mymodel(self, form):
data = form.cleaned_data
for field in data:
data[field] = cleanup(field)
res = requests.post(...)
if res.status_code == 200:
logger.info("created mymodel")
...
else:
logger.error(f"something went wrong: {res.status_code}\n{res.content}")
...
return True
def form_valid(self, form, *args, **kwargs):
res = self.create_mymodel(form)
try:
self.form_valid(form, *args, **kwargs)
except Exception:
print(Exception)
form_valid_return = super().form_valid(form, *args, **kwargs)
return form_valid_return
Once define this, with many doubts on which modeladmin classmethods should I override to achieve our needs: get_context_data, dispatch, form_valid, ...
is just a matter of python manage.py runserver, navigate to the admin/mymodel path, and crash:
.venv/lib/python3.6/site-packages/django/urls/resolvers.py", line 676, in _reverse_with_prefix
raise NoReverseMatch(msg)
django.urls.exceptions.NoReverseMatch: Reverse for 'mymodel_modeladmin_edit' with arguments '('',)' not found. 1 pattern(s) tried: ['admin/mymodel/edit/(?P<instance_pk>[-\\w]+)/$']
[15/Jul/2020 15:08:08] "GET /admin/mymodel/ HTTP/1.1" 500 798083
So you think, Ok, I did something wrong, comment last code, put a ipdb trace ... and the problem persists with the NoReverseMatch, there's no way to get rid of this.
Clean browser cache, switch to another browser, re-run runserver, re-create database, nothing makes this issue go back to stable.
Eventually you switch git branch, re-run git clone, re-install virtualenv, drop backend db, and then you have a fresh local implementation of your project, until I try to mess again with this problem.
So my question is, what the hell is going on? where is this cached routing thing ? I've worked with django for many years and never faced such behaviour.
did you try looking for the urls in your application:
try this command
./manage.py show_urls | grep /create/
do you see any instance pk with creating views?

Django Redirect - URL with String to URL With Slug

Not sure if I should be doing this or not... but here goes.
I have a url that looks like this:
/deals/category/Apparel/
I changed it to category/Apparel to shorten it.
but also notice the capitalized Apparel --as it is using the category name.
So I added a slug to my Category model and I'm trying to redirect the
deals/category/Apparel to category/apparel where the latter represents the slug
In my deals app I have this URL:
path('category/<str:category>', RedirectView.as_view(pattern_name='category', permanent=True)),
and which I'm trying to redirect to (in my core urls file)
path('category/<slug:slug>', deals_by_category, name='category')
My view for the `deals_by_category' looks like this:
def deals_by_category(request,slug):
category_deals = Deal.objects.filter(category__slug=slug).order_by('expired','-date_added')
category = category_deals[0].category
return render(request, 'deals/category.html', {'category_deals': category_deals, 'category':category})
so when I go to deals/category/Apparel it is redirecting to category/Apparel which is not what I want... and I get an error like this:
Reverse for 'category' with keyword arguments '{'category': 'Apparel'}' not found. 1 pattern(s) tried: ['category\\/(?P<slug>[-a-zA-Z0-9_]+)$']
I guess I understand that it is looking at the category name and trying to match against a slug, but not exactly sure how to correctly redirect this with the correct format.
path('category/<str:category>', RedirectView.as_view(pattern_name='category', permanent=True)),
When you use pattern_name, Django will try to reverse the URL with the same args and kwargs, in this case category='Apparel'.
If you want to use the slug in the URL instead, then you'll have to subclass RedirectView and override get_redirect_url.
from django.shortcuts import get_object_or_404
class CategoryRedirectView(RedirectView):
permanent = True
def get_redirect_url(self, *args, **kwargs):
category = get_object_or_404(Category, name=self.kwargs['category'])
return reverse('category', kwargs={'slug': category.slug})
Then use your view in your URL pattern:
path('category/<slug:slug>', CategoryRedirectView.as_view(), name='category')
I wouldn't set permanent = True until you are sure that the redirect is working as expected. Otherwise browsers may cache incorrect redirects.

How to implement method in Django REST?

Have the next Django REST question.
I have the view.
class MessageViewSet(viewsets.ModelViewSet):
serializer_class = MessageSerializer
queryset = Message.objects.filter(isread = False)
def mark_read():
queryset = Message.objects.update(isread=True)
return Response({'read':queryset})
And router in urls.py
router = SimpleRouter() router.register(r'api/get_messages', MessageViewSet)
urlpatterns = [
url(r'^$', MainView.as_view(), name='main'),
url(r'^', include(router.urls)) ]
Now i have 'get_messages' page which shows all list.
How can i implement a method which would change 'isread' value of model instanse from False to True, when I visit a 'mark_read' page?
As you can see, i tried to write method in the class. But when i'm trying to call it in urls in this way:
router.register(r'api/mark_read', MessageViewSet.mark_read),
Here comes an error.
assert queryset is not None, 'base_name argument not specified, and could ' \
AssertionError: base_name argument not specified, and could not automatically determine the name from the viewset, as it does not have a .queryset attribute.
Maybe i shouldnt use router, and rewrite view and urls in other way. If u know how to solve this problem, please answer. Thanks.
You can use detail_route or list_route decorators.
from rest_framework.decorators import list_route
class MessageViewSet(viewsets.ModelViewSet):
#list_route()
def mark_read(self, request):
queryset = Message.objects.update(isread=True)
return Response({'read':queryset})
With that mark_read method will be available at api/get_messages/mark_read. And you don't need to create separate router, just use one you created for MessageViewSet
docs reference
Since you are using a model viewset you can directly use put or patch rest method to send the desired value for the desired field as the data.
Ideally in rest get should not change model values. If you really want a different end point put the list_route or detail_route decorator on your mark_read method, and make them a valid call for only a put and/or patch call
from rest_framework.decorators import list_route
class MessageViewSet(viewsets.ModelViewSet):
#list_route(methods=['Patch', 'PUT'])
def mark_read(self, request):
queryset = Message.objects.update(isread=True)
return Response({'read':queryset})
Thanks to #ivan-semochkin and #Shaumux for replies. Advices were really helpful.
That is my route. I used detail_route instead of list_route.
#detail_route(methods=['get','put'], url_name='mark_read/')
def mark_read(self, request, pk=None):
queryset = Message.objects.filter(pk=pk).update(isread=True)
return Response({'read':queryset})
Now 'isread' value is changing wnen i visit 'mark_read' page.
Link: "api/get_messages/pk/mark_read"
Does anyone know, is it posslible to make links looking the next way:
"api/get_messages" - list, "api/mark_read/pk" - changing isread value.
Is it possible to create something like this? "api/mark_read?=pk"

Django Admin - Adding custom views to model

So basically what I would like is to have a custom view (print view) for one of my model.
I added a custom button and I changed the get_url() of my model:
def get_urls(self):
urls = super(MyModelAdmin, self).get_urls()
my_urls = patterns('',
url(r'^/print/schedule$',
self.admin_site.admin_view(views.PrintScheduleDetailView.as_view()), name='print_schedule'),
)
return my_urls + urls
When I try to view it:
my_app/my_model/primary_key/print/schedule/
It tells me the object with the pk 1/print/schedule doesn't exist....
What can I do?
Thanks,
Ara
EDIT: Here is my view class:
class PrintScheduleDetailView(generic.DetailView):
model = Tournament
template_name = 'print/schedule.html'
I got the link to kinda work...
localhost/tournament/print/schedule/
works but it tells Generic detail view PrintScheduleDetailView must be called with either an object pk or a slug.... I tried adding a primary key localhost/tournament/print/schedule/pk but didnt work...
Thanks,
Ara
Perhaps the regular expression should be changed to remove the '^' symbol like:
url(r'/print/schedule$'
According to Detailview of class based views:
By default this requires self.queryset and a pk or slug argument
in the URLconf, but subclasses can override this to return any object.
url(r'^/(?P<pk>[\d]+)/print/schedule/$', self.admin_site.admin_view(views.PrintScheduleDetailView.as_view()), name='print_schedule'),)
I believe you need to pass the pk, except if you are fetching it through another way (you haven't pasted your Class View).

How to use current logged in user as PK for Django DetailView?

When defining URL patterns, I am supposed to use a regular expression to acquire a PK from the URL.
What if I want a URL that has no PK, and if it's not provided, it will use the currently logged in user? Examples:
visiting /user will get a DetailView of the currently logged in user
/user/edit will show an UpdateView for the currently logged in user
I tried hard-coding the pk= in the Detail.as_view() call but it reports invalid keyword.
How do I specify that in the URL conf?
My sample code that shows PK required error when visiting /user URL:
urlpatterns = patterns('',
url(r'user/$',
DetailView.as_view(
model=Account,
template_name='user/detail.html')),
)`
An alternative approach would be overriding the get_object method of the DetailView subclass, something along the line of:
class CurrentUserDetailView(UserDetailView):
def get_object(self):
return self.request.user
Much cleaner, simpler and more in the spirit of the class-based views than the mixin approach.
EDIT: To clarify, I believe that two different URL patterns (i.e. one with a pk and the other without) should be defined separately in the urlconf. Therefore they could be served by two different views as well, especially as this makes the code cleaner. In this case the urlconf might look something like:
urlpatterns = patterns('',
url(r"^users/(?P<pk>\d+)/$", UserDetailView.as_view(), name="user_detail"),
url(r"^users/current/$", CurrentUserDetailView.as_view(), name="current_user_detail"),
url(r"^users/$", UserListView.as_view(), name="user_list"),
)
And I've updated my example above to note that it inherits the UserDetailView, which makes it even cleaner, and makes it clear what it really is: a special case of the parent view.
As far as I know, you can't define that on the URL definition, since you don't have access to that information.
However, what you can do is create your own mixin and use it to build views that behave like you want.
Your mixin would look something like this:
class CurrentUserMixin(object):
model = Account
def get_object(self, *args, **kwargs):
try:
obj = super(CurrentUserMixin, self).get_object(*args, **kwargs)
except AttributeError:
# SingleObjectMixin throws an AttributeError when no pk or slug
# is present on the url. In those cases, we use the current user
obj = self.request.user.account
return obj
and then, make your custom views:
class UserDetailView(CurrentUserMixin, DetailView):
pass
class UserUpdateView(CurrentUserMixin, UpdateView):
pass
Generic views uses always RequestContext. And this paragraph in the Django Documentation says that when using RequestContext with auth app, the template gets passed an user variable that represents current user logged in. So, go ahead, and feel free to reference user in your templates.
You can get the details of the current user from the request object. If you'd like to see a different user's details, you can pass the url as parameter. The url would be encoded like:
url(r'user/(?P<user_id>.*)$', 'views.user_details', name='user-details'),
views.user_details 2nd parameter would be user_id which is a string (you can change the regex in the url to restrict integer values, but the parameter would still of type string). Here's a list of other examples for url patterns from the Django documentation.

Categories

Resources