django admin - overriding changelist_view for single entry models - django

I have a model for which i'll have one single instance, so i need to override the changelist_view to by-pass it (only if i have at least one record saved), and jump directly to change_view. i found snippet online and it works well for it, so i wrote my custom changelist_view:
def changelist_view(self, request, extra_context=None):
queryset = self.model.objects.all()
if queryset.count()>0:
try:
obj = queryset[0]
return self.change_view(request, str(obj.id), extra_context)
except IndexError:
pass
return super(MyModelAdmin, self).changelist_view(request, extra_context)
this works until I try to save. The difference from the normal change_view is in the url. the normal has the object id:
http://127.0.0.1:8000/admin/myapp/mymodel/2
instead with modified version i have:
http://127.0.0.1:8000/admin/myapp/mymodel/
if i try to save i got this error:
You called this URL via POST, but the URL doesn't end
in a slash and you have APPEND_SLASH set. Django can't redirect to
the slash URL while maintaining POST data. Change your form to
point to 127.0.0.1:8000/admin/myapp1/mymodel/None/ (note
the trailing slash), or set APPEND_SLASH=False in your
Django settings.
At the moment the only trick that works for me is a HttpResponseRedirect(url), with as url the change_view url hardcoded with object id.
is there a more elegant way?
thanks
Luke

You can change target URL the admin will redirect to after processing your edits by using the response_change method on your model admin. That receives the request and the changed object as parameters, and can return a redirect to a dynamically calculated URL.
def response_change(self, request, obj):
# call the parent version to collect messages for the user
original_response = super(MyModelAdmin, self).response_change(request, obj)
if "_popup" in request.POST:
return original_response
return HttpResponseRedirect(reverse("admin:myapp_mymodel_change", args=[obj.id]))
There's also a response_add, but I doubt you'll need that if you're using a singleton model. Likewise, there are ways to test for whether the user selected "save and add another", "save and continue editing" or just "save" but you probably don't care about that distinction for a singleton.

When using
def changelist_view(self, request, extra_context=None):
django will insert an action="None" in the Html output which causes the above mentioned error when submitting the post. Instead try
def changelist_view(self, request, extra_context=""):

Related

Redirect the user to the previous page which uses a Primary Key (PK)

I'm currently trying to create a form that, upon success, redirects the user to the previous page (which is using a PK key).
Example of previous page : http://127.0.0.1:8000/object/update/8/
(1) What I tried:
def get_success_url(self, **kwargs):
return reverse_lazy('task_update:update', kwargs={'id': self.object.id})
(2) And currently I'm trying to load my form with the same pk than the precedent form (he don't need pk, example : http://127.0.0.1:8000/tag/create/8/)
And then use that PK in the URL for load the precedent page like that :
def get_success_url(self, **kwargs):
return reverse_lazy('task_update', self.kwargs.get('pk'))
but it generates an error :
The included URLconf '8' does not appear to have any patterns in it. If you see the 'urlpatterns' variable with valid patterns in the file then the issue is probably caused by a circular import.
The goal:
Globally, users start at "http://127.0.0.1:8000/object/update/8/", then they press a button which redirects them to a new page to create a tag, and then they get redirected back to the first page.
You pass it through the named parameter kwargs=…, so:
from django.urls import reverse
def get_success_url(self, **kwargs):
return reverse('task_update', kwargs={'pk': self.kwargs['pk']})

Django admin, changelist_view, ModelAdmin seems to act like a singleton

I noticed a strange behaviour while extending ModelAdmin.
I have this code:
class MakeModelAdmin(admin.ModelAdmin):
...
def changelist_view(self, request, extra_context=None):
if request.user.is_superuser:
self.list_display = ['company', 'name']
# else:
# self.list_display = ['name']
return super().changelist_view(request, extra_context=extra_context,)
The goal is to change list_display dynamically based on user (supervisor or not).
I log in with two different users, in two different browsers, one of them results to be a superuser, the other isn't.
self.list_display is set by one of users but debugging the request with the other user I can see the variable still set, so it changes the next behavior of the other user's view.
Uncommenting the lines it works but I don't like it at all.
It seems to me it's acting like a singleton.
I also tried to change to:
super(MakeModelAdmin, self).changelist_view(request, extra_context=extra_context,)
But it has the same effect.
Is there any solution for that?
Maybe this is not the right way to achieve my goal?
The documented way to dynamically change the behavior of the admin based on the request is to use the get_* methods. In your case that would be something like:
def get_list_display(self, request):
if request.user.is_superuser:
return ['company', 'name']
else:
return super().get_list_display(request)
As for AdminSite, it's not a singleton (that is, the same instance isn't returned every time you instantiate it). It's just that a single instance is created during the Django setup process and then used to service all subsequent requests.

Passing data between views in Django

I have a FormView (currently a WizardView as I have multiple forms) which takes input from the (anonymous) user and passes the cleaned data to be used in a request to an external api.
However I'm not clear how best to pass the data recieved from one view to the next view?
The data received from the api is to be displayed in the next view.
(If the data was held internally (rather than in an external api) I would probably try a ListView with the relevant parameters passed from the FormView to the ListView via a session)
However the api returned data will have many pages of data.
What's the best way to manage this in django? (This doesn't need to be async as the returned data is required before the user can proceed.)
class QuotesWizard(SessionWizardView):
def get_template_names(self):
return [QUOTES_TEMPLATES[self.steps.current]]
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated():
return redirect(settings.LOGIN_REDIRECT_URL)
return super().dispatch(request, *args, **kwargs)
def done(self, form_list, form_dict, **kwargs):
postcode = form_dict['postcode'].cleaned_data['postcode']
service = form_dict['service'].cleaned_data['service']
usage = form_dict['usage'].cleaned_data['usage']
usage = convert_usage(usage)
# get data from external api
data = get_quotes(postcode, usage_ofgem, service_type)
# redirect to quotes page - how to manage transfer of data?
return redirect('quotes')
I have a similar approach in a module, also not surprisingly with address data.
I've chosen to store the results in the database, so I've created a model for it. One reason is that number of calls have a price, so each call I'm not making, I'm saving money. Another is that I'm not bound by the whims of the remote API: My API is consistent for the browser and Django views and if something changes remotely, I have to fix it only in one place.
The crux is to make a QuerySet in which you override the get method and in your case also the filter method in a similar way:
class PostcodeApiQuerySet(QuerySet):
# fields, other methods
def get(self, *args, **kwargs):
try:
return super(PostcodeApiQuerySet, self).get(*args, **kwargs)
except self.model.DoesNotExist:
self._validate_lookup_kwargs(kwargs)
pc = self._get_field_lookup_value('pc', kwargs)
house_nr = self._get_field_lookup_value('house_nr', kwargs)
house_nr_add = self._get_field_lookup_value('house_nr_add', kwargs)
try:
address = self._fetch_one(
pc, house_nr, house_nr_add=house_nr_add
)
except ObjectDoesNotExist as e:
raise self.model.DoesNotExist(e.args[0])
except KeyError as e:
raise self.model.DoesNotExist(e.args[0])
except ValueError as e:
raise self.model.DoesNotExist(e.args[0])
street = self._handle_foreign_for_address(address)
instance = self.model(street=street, **address)
instance.full_clean()
instance.save()
instance.refresh_from_db()
return instance
The methods called here that are not included, deal with the remote API and mapping of fields to API filters, but it should give you a general idea. The filter method will be a bit more complex, as you will have to figure out what you don't have and as such what you need to fetch remotely.
Once you've got this done, you can reduce your pages of data to a list of ID's, which should easily fit in a session. Then you can use ListView and override get_queryset() to teach it about the list of ID's to get from the session.
EDIT
So the flow is:
FormView (GET): render selection form
FormView (POST), in form_valid: Apply filters to overridden filter method of the custom queryset (Also see Manager.from_queryset) and store ID's in session
FormView (POST), in form_valid creates redirect to success_url
ListView (GET) invokes get_queryset which you've taught to deal with ID's in session
Good luck!
There are multiple ways to accomplish this:
Pass the data using GET params while redirecting. This way the resultant view would be cacheable and stateless. (recommended)
Use django sessions. Add the variable to session, which can be
retrived globally. Sessions act as global variables, so use them
only as last resort.
Use django messages if you want to store variables in session only for next view.

Call a view within a view with different methods

I have two views, one to create an item and another to generate the global view of an instance of another object.In this view, I have a form and what I want is to redirect to the previous page after the processing of the view.
Basically :
def view1(request):
if request.method == 'GET':
#heavy processing for the context
return HttpResponse(template.render(context))
def view2(request):
if request.method == 'POST':
# Simply add an element
return view1(request)
Here's what I want to do. The thing is that, as you can see, the method is different from view1 to view2. I can't use redirect because the heavy processing of data of view 1 wouldn't be done (I guess).
Does someone know how can I transform my POST request into a GET and add a parameter ?
Thanks !
You can use this too:
def view(request):
if request.method == "POST"
#add the element
#heavy processing for the context
here if the method is post, it will goto the post method if post is available and u can use the post perimeters in the processing u want
hope its useful, please vote up
or u can use:
from django.views.generic.base import View
class view(View):
def get(self, request, *args, **kwargs ):
#heavy processing for the context
def post(self, request, *args, **kwargs):
#add the element
when you'll call the above url with post method, it will goto post, and for other cases it will load get
What makes you think the processing of data in view1 wouldn't be done on a redirect? A HttpRedirectResponse and its shortcut redirect() will simply give the user's browser a 302 HTTP response code, indicating that the browser should do a new request to the specified url and display that page instead of the current page.
It is actually good practice to always do a redirect after a successful POST request. Otherwise, the data in the POST request would be sent and processed twice if the user refreshes the original page. Using a redirect will reset all POST data and prevent duplicate entries or error messages to the user after a successful request.
To pass a parameter, simply add it to the redirect url as you would with any GET parameter:
from django.http import QueryDict
parameters = QueryDict(foo='bar', myOtherVar='something_else')
url = '%s?%s' % (reverse('my_view_name'), parameters.urlencode())
return redirect(url)
You can use request.REQUEST in that case: https://docs.djangoproject.com/en/dev/ref/request-response/#django.http.HttpRequest.REQUEST
But it seems bad idea, cause it will be deprecated in django 1.7. You shouldn't pass GET parameter to another view as POST, it would be better to use POST in booth views.

Using get_success_url on a DeleteView when relevant data has been removed

In a Django application working with recipes I have subclassed DeleteView to create my IngredientListItemDeleteView, but I would like the result of get_success_url to depend on a property of the item that was just deleted.
I would like to do something like this:
def get_success_url(self):
item = get_object_or_404(IngredientListItem, pk=self.kwargs['pk']) # -> 404
return this_item.recipe.get_absolute_url()
I understand that I get a 404 error because the item in question no longer exists but I have had no luck storing the relevant information about the item (namely, its containing recipe) before it gets deleted. For instance, if I put into the get method any code like
self.success_url = get_object_or_404(IngredientListItem,
pk=self.kwargs['pk']).recipe.get_absolute_url()
then by the time success_url is looked at (after deletion), it has the value None.
How can I make my success URL depend on this property of the deleted item?
In Django 1.6, the delete method has been changed so that the get_success_url method is called before the object is deleted.
def delete(self, request, *args, **kwargs):
"""
Calls the delete() method on the fetched object and then
redirects to the success URL.
"""
self.object = self.get_object()
success_url = self.get_success_url()
self.object.delete()
return HttpResponseRedirect(success_url)
I recommend you override your delete method as above, until you upgrade to Django 1.6. If you need to do this for multiple classes, you could create a mixin.
Note that you don't need to fetch the item from the database with get_item_or_404 -- you can access it in your get_success_url method as self.object.