How to save stripe webhook data to session variables in Django? - django

I am currently working on a 'success' page for my Django project (which uses Stripe to take payments). I would like to show their order total, with tax, on this page. My Stripe webhook is running and posting data, but for the life of me I can't use this data outside of the webhook's veiw. Normally, when I want to inform one view with data from another, I take advantage of Django's session variables. When I save the session variable, however, attempting to use it in my SuccessView returns 'None' rather than the value saved to it. Any help would be appreciated. Here is my relavent code:
views.py
def stripe_webhook(request):
payload = request.body
sig_header = request.META['HTTP_STRIPE_SIGNATURE']
event = None
try:
event = stripe.Webhook.construct_event(
payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
)
except ValueError as e:
# Invalid payload
return HttpResponse(status=400)
except stripe.error.SignatureVerificationError as e:
# Invalid signature
return HttpResponse(status=400)
# Handle the checkout.session.completed event
if event['type'] == 'checkout.session.completed':
session = event['data']['object']
customer_email = session["customer_details"]["email"]
line_items = stripe.checkout.Session.list_line_items(session["id"])
print(line_items)
stripe_price_id = line_items["data"][0]["price"]["id"]
price = Price.objects.get(stripe_price_id=stripe_price_id)
total_paid_cents = line_items["data"][0]["amount_total"]
total_paid_dollars = total_paid_cents / 100
request.session['total_paid'] = total_paid_dollars
return HttpResponse(status=200)
class SuccessView(TemplateView):
template_name = "musicstudios/success.html"
extra_context = sidebar_context
def get_context_data(self, **kwargs):
context = super(SuccessView, self).get_context_data(**kwargs)
order_id = self.request.session.get('order_id')
order = Order.objects.get(pk=order_id)
product_id = order.product.id
product = Product.objects.get(pk=product_id)
customer = Customer.objects.get(pk=order.customer.id)
total_paid = self.request.session.get('total_paid')
print(total_paid)
context['customer'] = customer
context['product'] = product
context['order'] = order
context['order_total'] = total_paid
return context
success.html
<div class="contentborder">
<span class="mb-2">
<span class="flex flex-col bg-slate-800 bg-opacity-25 border-slate-600 rounded-lg pl-4 pr-2 py-2 mt-2 mb-4">
<span class="flex flex-row justify-between">
<h3 class="text-xl font-bold mb-1">Order Details</h3>
</span>
<p class="py-1"><strong>Order total:</strong> ${{ order_total }}</p>
<p class="py-1"><strong>Order includes:</strong> {{ order.price.price_description }}</p>
{% if order.cust_requests %}
<p class="py-1"><strong>Requests:</strong> {{ order.cust_requests }}</p>
{% else %}
<p class="py-1"><strong>Requests:</strong> No specific requests made.</p>
{% endif %}
{% if order.reference_track %}
<p class="py-1"><strong>Reference track link:</strong> {{ order.reference_track }}
{% else %}
<p class="py-1"><strong>Reference track link:</strong> No reference track linked.</p>
{% endif %}
{% if order.music_file %}
<p class="py-1"><strong>Music file:</strong> {{ order.music_file }}
{% else %}
<p class="py-1"><strong>Music file:</strong> No music file uploaded.</p>
{% endif %}
<p class="py-1"><strong>Order Date:</strong> {{ order.order_date }}
</span>
</span

When Stripe sends the Event to your webhook endpoint, it makes an HTTP request to your server. That request is completely unrelated to the request(s) made by your customer. You can't set something in the request's session for the webhook handler code that could then be shared with the success page's session itself. They are completely separate.
The best option here is to handle this in your SuccessView too. You can look up the state of the Session itself using the Retrieve Checkout Session API during the redirect. Alternatively, you can store this information in your database when the webhook handler receives the Event and then have your SuccessView look at the state in your database.

Related

Django custom admin actions with an intermediate page submit not triggered

Writing an admin action so an administrator can select a template they can use to send a message to subscribers by inputting only the subject and text message. Using a filtered list from the admin panel an action called broadcast is triggered on this queryset (the default filter list). The admin action 'broadcast' is a function of a sub-classed UserAdmin class. The intermediate page is displayed that shows a dropdown selector for the emailtype, the queryset items (which will be email addresses, input fields for the subject and message text (message is required field) a button for optional file attachment followed by send or cancel buttons. Problem 1) after hitting the send button the app reverts to the admin change list page. In the broadcast function, the conditional if 'send' in request.POST: is never called.
forms.py
mail_types=(('1','Newsletter Link'),('2','Update Alert'))
class SendEmailForm(forms.Form):
_selected_action = forms.CharField(widget=forms.MultipleHiddenInput)
#Initialized 'accounts' from Account:admin.py Actions: 'send_email' using>> form = SendEmailForm(initial={'accounts': queryset})
my_mail_type=forms.ChoiceField(label='Mail Type',choices=mail_types,required=False)
subject = forms.CharField(widget=forms.TextInput(attrs={'placeholder': ('Subject')}),required=False)
message = forms.CharField(widget=forms.Textarea(attrs={'placeholder': ('Teaser')}),required=True,min_length=5,max_length=1000)
attachment = forms.FileField(widget=forms.ClearableFileInput(),required=False)
accounts = forms.ModelChoiceField(label="To:",
queryset=Account.objects.all(),
widget=forms.SelectMultiple(attrs={'placeholder': ('user_email#somewhere.com')}),
empty_label='user_email#somewhere.com',
required=False,
admin.py
from .forms import SendEmailForm
from django.http import HttpResponseRedirect,HttpResponse
from django.shortcuts import render, redirect
from django.template.response import TemplateResponse
def broadcast(self, request, queryset):
form=None
if 'send' in request.POST:
print('DEBUGGING: send found in post request')
form = SendEmailForm(request.POST, request.FILES,initial={'accounts': queryset,})
if form.is_valid():
#do email sending stuff here
print('DEBUGGING form.valid ====>>> BROADCASTING TO:',queryset)
#num_sent=send_mail('test subject2', 'test message2','From Team',['dummy#hotmail.com'],fail_silently=False, html_message='email_simple_nb_template.html',)
self.message_user(request, "Broadcasting of %s messages has been started" % len(queryset))
print('DEBUGGING: returning to success page')
return HttpResponseRedirect(request, 'success.html', {})
if not form:
# intermediate page right here
print('DEBUGGING: broadcast ELSE called')
form = SendEmailForm(request.POST, request.FILES, initial={'accounts': queryset,})
return TemplateResponse(request, "send_email.html",context={'accounts': queryset, 'form': form},)
send_email.html
{% extends "admin/base_site.html" %}
{% load i18n admin_urls static %}
{% load crispy_forms_tags %}
{% block content %}
<form method="POST" enctype="multipart/form-data" action="" >
{% csrf_token %}
<div>
<div>
<p>{{ form.my_mail_type.label_tag }}</p>
<p>{{ form.my_mail_type }}</p>
</div>
<div>
<p>{{ form.accounts.label_tag }}</p>
<p>
{% for account in form.accounts.queryset %}
{{ account.email }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
<p><select name="accounts" multiple style="display: form.accounts.email">
{% for account in form.accounts.initial %}
<option value="{{ account.email }}" selected>{{ account }}</option>
{% endfor %}
</p></select>
</div>
<div>
<p>{{ form.subject.label_tag }}</p>
<p>{{ form.subject }}</p>
</div>
<div>
<p>{{ form.message.label_tag }}</p>
<p>{{ form.message }}</p>
</div>
<div>
<p>{{ form.attachment.label_tag }}</p>
<p>{{ form.attachment.errors }}</p>
<p>{{ form.attachment }}</p>
</div>
<input type="hidden" name="action" value="send_email" />
<input type="submit" name="send" id="send" value="{% trans 'Send messages' %}"/>
{% trans "Cancel this Message" %}
</div>
</form>
{% endblock %}
Inspecting the browser at the POST call seems to show all the data was bound. Another poster here suggested the admin action buttons divert requests to an internal 'view' and you should redirect to a new view to handle the POST request. I can't get that to work because I can't get a redirect to 'forward' the queryset. The form used in the suggested fix was simpler and did not use the queryset the same way. I have tried writing some FBVs in Forms.py and Views.py and also tried CBVs in views.py but had issues having a required field (message) causing non-field errors and resulting in an invalid form. I tried overriding these by writing def \_clean_form(self): that would ignore this error, which did what it was told to do but resulted in the form essentially being bound and validated without any inputs so the intermediate page didn't appear. Which means the rabbit hole returned to the same place. The send button gets ignored in either case of FBVs or CBVs, which comes back to the admin action buttons Post requests revert to the admin channels!
Any ideas on how to work around this? Key requirements: From the admin changelist action buttons:
the Form on an intermediate page must appear with the queryset passed from the admin changelist filter.
The message input field on the form is a required field.
the send button on the HTML form view needs to trigger further action.
NOTES: My custom Admin User is a subclass of AbstractBaseUser called Account, where I chose not to have a username and am using USERNAME_FIELD='email'. Also, I do not need a Model.py for the SendEmailForm as I don't need to save the data or update the user models, just send the input message using the chosen template and queryset. Help is much appreciated!
It will never work in your case:
You call the action.
You receive the Action Confirmation template render.
After pressing "SEND" in your "confirmation" step, you send a POST request to ModelAdmin, not in your FB-Action.
ModelAdmin gets a POST request without special parameters and shows you a list_view by default.
In your case, you should add a send_email.html template:
{% load l10n %}
{# any your staff here #}
{% block content %}
<form method="POST" enctype="multipart/form-data">
{# any your staff here #}
<div>
<p>{{ form.attachment.label_tag }}</p>
<p>{{ form.attachment.errors }}</p>
<p>{{ form.attachment }}</p>
</div>
{% for obj in accounts %}
<input type="hidden" name="_selected_action" value="{{ obj.pk|unlocalize }}" />
{% endfor %}
<input type="hidden" name="action" value="broadcast" />
{# any your staff here #}
</form>
{% endblock %}
You should change your action view, some things are not working in your code:
def broadcast(self, request, queryset):
form = SendEmailForm(data=request.POST, files=request.FILES, initial={'accounts': queryset})
if 'send' in request.POST:
... # your staff here
if form.is_valid():
... # your staff here
# return HttpResponseRedirect(request, 'success.html', {} ) this is NEVER WORK
return TemplateResponse(request, 'success.html', {})
... # your staff here
return TemplateResponse(request, "send_email.html",context={'accounts': queryset, 'form': form},)
I am giving you a solution that I have TESTED on my project. I am sure, it works.
We were told on DjangoCon Europe 2022 that django-GCBV is like a ModelAdminAction and I've added a link below for the talk.
https://youtu.be/HJfPkbzcCJQ?t=1739
I can't get that to work because I can't get a redirect to 'forward' the queryset
I have a similar use case and save the primary keys of the filtered query set in the session (in your case you may be able to save emails and avoid another query)
def broadcast(self, request, queryset):
request.session["emails"] = list(queryset.values_list("emails", flat=True))
return HttpResponseRedirect("url_to_new_view")
I can then use primary keys to filter query set in the new view. You also handle the form in this new view.
User.objects.filter(email__in=self.request.session["emails"])

Trouble paginating results from a 3rd party API in Django

I'm making a portfolio project where I'm using the Google Books API to do a books search, and the Django Paginator class to paginate the results. I've been able to get search results using a CBV FormView and a GET request, but I can't seem to figure out how to get pagination working for the API response.
The solution I can think of is to append &page=1 to the url of the first search, then pull that param on every GET request and use that to paginate. The problem is, I can't figure out how to append that param on the first search, and I don't know how I'd increment that param value when clicking the pagination buttons.
Here's what I've got now:
Form:
class SearchForm(forms.Form):
search = forms.CharField(label='Search:', max_length=150)
View:
class HomeView(FormView):
template_name = "home.html"
form_class = SearchForm
pageIndex = 0
def get(self, request, *args, **kwargs):
# get submitted results in view and display them on results page. This will be swapped out for an AJAX call eventually
if "search" in request.GET:
# getting search from URL params
search = request.GET["search"]
kwargs["search"] = search
context = super().get_context_data(**kwargs)
# Rest API request
response = requests.get(
f'https://www.googleapis.com/books/v1/volumes?q={search}&startIndex={self.pageIndex}&key={env("BOOKS_API_KEY")}'
)
response = response.json()
items = response.get("items")
# pagination...needs work
paginator = Paginator(items, 2)
page_obj = paginator.get_page(1)
context["results"] = page_obj
return self.render_to_response(context)
else:
return self.render_to_response(self.get_context_data())
Template:
{% extends "base.html" %}
{% block content %}
<form action="/">
{{ form }}
<input type="submit" value="Submit">
</form>
<h1>Books</h1>
<ul>
{% for result in results %}
<li>{{ result.volumeInfo.title }} : {{result.volumeInfo.authors.0}}</li>
{% empty %}
<li>Search to see results</li>
{% endfor %}
</ul>
{% if results %}
<div class="pagination">
<span class="step-links">
{% if results.has_previous %}
« first
previous
{% endif %}
<span class="current">
Page {{ results.number }} of {{ results.paginator.num_pages }}
</span>
{% if results.has_next %}
next
last »
{% endif %}
</span>
</div>
{% endif %}
{% endblock content %}
I also looked at Django REST Framework for this, but the Google Books API response doesn't contain any info on next page, previous page, etc. I've done this kind of pagination in React and it's not difficult, I'm just having trouble adjusting my mental model for how to do this to Django. If anyone could offer some advice on how to make this work, I'd be very grateful.

Django not rendering view

I'm making a messaging system which has a message model and some views for things like inbox and conversation.
Inbox view and template works and is linked to the conversation through the id of the sender. However, when I click on the senders name (The senders name is the link (template link is {{ sender }}) that is meant to trigger the conversation view which is supposed to render the template convo.html ( the project level url for that is url(r'^messaging/inbox/conversation/(?P<id>[\w-]+)/$', message_views.Conversation.as_view(), name="conversation"),) ), Django changes the url but stays on the same page(So it triggers the inbox view and stays on inbox.html and changes the url but doesn't trigger the conversation view and doesn't render the convo.html template). I've tried changing from a function based view to a class based view and it did nothing. What am I doing wrong?
Note: I'm still new to django so forgive me for any bad practices; I'm attempting to teach myself.
views.py (inbox and conversation views)
def inbox(request):
messages = Message.objects.filter(receiver=request.user.id)
senders = {}
for message in messages:
if message.sender not in senders:
senders[message.sender] = message
return render(request, 'inbox.html', {'senders': senders})
# There is only ever one of this view, either the CBV or FBV
def conversation(request, other_user_id):
print(request.method)
if request.method == "POST":
reply = request.post['reply']
other_user = CustomUser.objects.get(id=other_user_id)
message = Message(sender=request.user, receiver=other_user)
message.save()
return redirect("conversation")
else:
other_user = CustomUser.objects.get(id=other_user_id)
messages = Message.objects.filter(Q(receiver=request.user) & Q(sender=other_user))
print(messages)
return render(request, 'convo.html', {'messages': messages})
# Class based conversation view
class Conversation(View):
def post(request, other_user_id):
reply = request.post['reply']
other_user = CustomUser.objects.get(id=other_user_id)
message = Message(sender=request.user, receiver=other_user)
message.save()
return redirect("conversation")
def get(request, other_user_id):
other_user = CustomUser.objects.get(id=other_user_id)
messages = Message.objects.filter(Q(receiver=request.user) & Q(sender=other_user))
print(messages)
return render(request, 'convo.html', {'messages': messages})
template - inbox.html
<!DOCTYPE html>
{% extends "accbase.html" %}
<!--Omitted outbox in favor of conversation-->
{% block content %}
<div class="inbox">
<h1>Inbox</h1>
<hr>
{% for sender in senders %}
{{ sender }}
<br>
{{sender.message.created_at}}
<br>
{% endfor %}
</div>
{% endblock %}
template - convo.html
<!DOCTYPE html>
<!--Template for reciever side of conversation-->
{% extends "accbase.html"%}
{% block content %}
<div class="conversation">
<h1>Conversation with {{sender}}</h1>
<p>
{% for msg in messages %}
<!--Order is descending ~~ bottom to top: newest to oldest-->
{{ msg.content }}
<br>
{% endfor %}
<hr>
<form method="POST">
<label>Reply</label>
<input type="text" name="reply">
</form>
</p>
<button type="submit">Send</button>
</div>
{% endblock %}
project level urls.py
from django.urls import path, re_path, include
from django.conf.urls import url
from messaging import views as message_views
urlpatterns = [
url(r'^messaging/inbox', message_views.inbox, name="inbox"),
url(r'^messaging/inbox/conversation/(?P<id>[\w-]+)/$', message_views.Conversation.as_view(), name="conversation"),
]
As Daniel Roseman said in comments " The problem here is that you didn't terminate the regex for your inbox URL, so it also matches the conversation URL. " so I added the needed $ to the end of the inbox url pattern and that fixed it!

Implementing confirmation view and template in django

I have two views one that accepts inputs and the other for confirmation and execution of an action. My problem is how to confirm the inputs from another view and template.
This is similar when you delete a record. That you should confirm from the user his actions.
Here is the input view. PreprocessinputationView:
def PreprocessInputationView(request, **kwargs):
proj_pk = kwargs.get('pk')
project = Project.objects.get(id=proj_pk)
df = pd.read_csv(project.base_file)
n_cols = df.keys
context = {}
context['df'] = df
context['n_cols'] = n_cols
context['project'] = project
if request.method == 'POST':
# try:
checked_value = request.POST.getlist(u'predictors')
method = ''.join(request.POST.getlist(u'method'))
if checked_value and method:
context['checked_value'] = checked_value
context['method'] = method
return render(request, 'projects/preprocess/confirm_inputation.html', context)
return render(request, 'projects/preprocess/preprocess_inputation.html', context)
The confirmation view goes here. ConfirmInputationView:
def ConfirmInputationView(request, context):
print('method:', context['method'])
project = context['project']
df = pd.read_csv(project.base_file)
n_cols = df.keys
filename = project.base_file.name
tmp = filename.split('/')
filename = str(tmp[1:])
if request.method == 'POST':
# try:
checked_value = context['checked_value']
method = context['method']
if checked_value and (method=='mean'):
df[checked_value].fillna(df[checked_value].mean())
# df.drop(columns=checked_values, inplace=True)
new_df = df.to_csv(index=False)
updated_file = ContentFile(new_df)
updated_file.name = filename
project.base_file = updated_file
project.save()
str_checked_value = ', '.join(checked_value)
context['str_checked_value'] = str_checked_value
if str_checked_value:
messages.success(request, f'Inputation to column(s) {str_checked_value} successful!')
return render(request, 'projects/preprocess/preprocess_inputation.html', context)
The confirmation template. Confirm_inputation.html:
{% extends "base.html" %}
{% block page_heading %}
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">Delete Project</h1>
</div>
{% endblock page_heading %}
{% block content %}
<div class="jumbotron col-xl-8 col-md-6 mb-1"">
<form method=" POST">
{% csrf_token %}
<fieldset class='form-group'>
<p>
You have chosen <strong>{{ method }}</strong> as an inputation method?
Are you sure you want to proceed?
</p>
</fieldset>
<div class="form-group">
<button class="btn btn-danger float-sm-right mr-1" type="submit">Yes, Delete</button>
<a class="btn btn-secondary float-sm-right mr-1" href="{% url 'project-detail' project.id %}">Cancel</a>
</div>
</form>
</div>
{% endblock content %}
The data from the PreprocessImputationView should be passed to ConfirmImputationView for confirmation and processing.
I'm not sure I understand your question or rather problem. So I'll summarize what I understood. Please clarify more, if this doesn't fit your problem.
You have view A (PreprocessInputationView), which shows the user some values/forms and allows some POST action to A.
If view A receives a POST request you check the form input and render template B of view B.
Your rendered template B offers two options to the user: Accept, which triggers POST to view B or decline, which links to some details view.
I think what you're missing is, that context in render is "lost" after rendering. The moment the user sees the finished html page that variable is no longer relevant and inaccessible.
A way to provide the necessary 'method' information to your B view would be to add a form field to your B template, which holds some sort of key for B view to determine on POST what to do. Like a hidden input field with a number. Each number would predetermined stand for a method.

Passing value from previous page to django view

I have a job site where a user enters a zip code into a form and a list of jobs matching that zip code is displayed
search.html :
<h6>Results: {{ number_of_results }}</h6>
{% for job in jobs_matching_query %}
<h2><a class="job_link" href="#">{{ job.job_title}}</a></h2>
<p class="job_address lead">{{ job.establishment_name }} - {{ job.address }}</p>
{% endfor %}
<form action = "{% url 'email_subscription' %}">
<p>Subscribe to recieve job alerts near {{ query }}:</p> <!-- query stores zip code-->
<input type="text" name= "email" placeholder="Email">
<button class="btn" type="submit">Subscribe</button>
</form>
The search form is handled by the following view (not sure if I need to include this or not):
def job_query(request):
if request.method == "GET":
query = request.GET.get('query')
jobs_matching_query = Job.objects.filter(zip_code__iexact = query) | Job.objects.filter(city__iexact=query) | Job.objects.filter(state__iexact=query)
number_of_results = 0
for job in jobs_matching_query:
number_of_results = number_of_results + 1
return render(request, 'core/search.html', {'query': query ,'jobs_matching_query': jobs_matching_query, 'number_of_results': number_of_results})
On that search.html I have an email subscription box (subscribe to recieve alerts for that particular zip code), is there a way to pass the value of query from that page to an email_subscription view? I believe I've seen this done in the url so here is the url for said view
url(r'^email_subscription$', views.email_subscription, name="email_subscription"),
You can use django sesions for that you can save the query in django session
#put it somewhere
request.session['query'] = my_query
#then you can access it from the other view in views.email_subscription
it is going to work but im not sure if this is a practitacal one