Passing data between views in Django - 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.

Related

get_object() that depends on `request` in UpdateView

I am writing a class-based view to allow users to edit their profile. Since I want the users to access this view with a URL of the type my_profile/edit/ rather than something like profile/<int:pk>/edit/, using a view based on UpdateView is quite cumbersome since getting the user profile object requires to access the request object, and get_object does not directly have access to it.
My two questions:
Shall I use UpdateView in this case?
If so, what would be the best way to override get_object(self, queryset=None)? My best attempt so far is the following:
class EditProfileView(UpdateView):
model = UserProfile
_request = None
def get_object(self, queryset=None):
return get_user_profile(self._request)
def dispatch(self, request, *args, **kwargs): # can also override setup() in newer Django versions
self._request = request
return super().dispatch(request, *args, **kwargs)
This looks clean enough to me except that if one day the Django framework decides that get_object should be called early in the workflow of the view generation, this piece of code could break.
You don't need to do this. All class-based views make the request available as self.request.
Your entire code should just be:
class EditProfileView(UpdateView):
model = UserProfile
def get_object(self, queryset=None):
return get_user_profile(self.request)
Note, even if it didn't, you still wouldn't need to define _request at class level. That's just not necessary in Python.
Also, I don't know what your get_user_profile function does but it can probably be replaced with just self.request.user.profile.

Django use data in a class based view

I have a class-based view in Django.
I have implemented a method get_context_data. A user can log in or update his/her data and is redirected to a class-based view template.
I want the information of the logged-in user inside the class-based view. I'm not rendering the view, just re-directing. Is there any approach like saving the data in memory during computation or global variables so that it can be accessed anywhere in the views.py.
if the user is logging in using the authenticate/login method, and you have the SessionMiddleware loaded as you should, then the user information should be in your request.user object.
In a class-based View object you can read the request like so:
class SomeView(View):
def get(self, *args, **kwargs):
user = self.request.user
#recommended: check if the user is authenticated, ie, not anonymous.
if user.is_authenticated:
check_something(user) #do whatever you need.
in the case of TemplateView subclasses (I assume, since you mention get_context_data) is the same:
class SomeTemplateView(TemplateView):
def get_context_data(self, *args, **kwargs):
if self.request.user and self.request.user.is_authenticated:
#do something
Globals and other things won't work in a Django service, it might work on development, but in production, your user's request could be handler by different python processes altogether and memory won't be shared between them.

Limiting access of FormView to logged-in users that pass a test

I'm building a job application form. A logged-in user is permitted to apply to the job only once (there's only one job). At the moment, a user is able to directly access the job application (FormView), by typing in its specific URL, and an error message is thrown AFTER the user submits the form. To achieve this, I'm doing a couple of things:
(1) Wrapping the job application view in login_required()
(2) Using the form_valid() method to check whether or not a user has already submitted an application to this specific job via:
def form_valid(self, form):
form = form.save(commit=False)
form.user = self.request.user
if Application.objects.filter(user=user_id):
messages.error(self.request, 'Sorry, our records show that you have already applied to this job.')
return redirect('/')
But I'd rather not permit them to reach the page at all. Instead, I want to check whether or not a user has already applied (during the request), and redirect them away from the form, if they have already applied. I have limited access to logged-in users that pass a test in the past, using something like:
def job_application_view(request):
active_user = request.user
if Application.objects.filter(user=active_user):
return HttpResponse("Some response.")
However, I can't seem to figure out how to access request via the FormView Class-Based View. I'm sure I'm missing something simple. Perhaps another method of FormView I'm missing?
You can still use decorators on class-based views, but they are slightly more difficult to apply than function-based views.
class ApplicationView(FormView):
# ...
#method_decorator(user_passes_test(job_application_view))
def dispatch(self, *args, **kwargs):
return super(ApplicationView, self).dispatch(*args, **kwargs)
Answering specific parts of your post...
I have limited access to logged-in users that pass a test in the past
With class-based views, you need to decorate the url or decorate the dispatch method with any decorators you are interested in applying.
However, I can't seem to figure out how to access request via the FormView Class-Based View. I'm sure I'm missing something simple. Perhaps another method of FormView I'm missing?
You can access the request with self.request

django admin - overriding changelist_view for single entry models

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=""):

Accessing request object from form.save

I'm using the pyfacebook module for Django to implement facebook Connect for one of my sites, I'm not using a local User object because I only want to use Facebook for login.
I understand that I can't access the request object from the save method on a Django form, so how would I access the facebook object that the provided middleware gives you?
I want to attach the users facebook UID to an object being created from a form, I could add it as a hidden field to the form but then people could just change it to make it appear that a question came from someone else.
What is the best way to pass in this uid to the save() method?
The correct way to do this is to use an instance of the object you're trying to save, like this:
question = Question(user=int(request.facebook.uid))
form = QuestionForm(request.POST, instance=question)
question = form.save()
question.put()
Do this in your view, NOT in the save() method of your object.
Watch out if any of the fields are required though, you'll have to specify them in the instance objector calling form.save will throw an exception.
You can set a variable on the form when you create it.
views.py
def myview(request):
form = FacebookConnectForm(request)
forms.py
class FacebookConnectForm(forms.Form):
def __init__(self, instance):
self.instance = instance
def save(self):
print self.instance
...