0

I have a view with different 'stages' (as I wanted to keep the same URL without resorting to something like AJAX), which takes data from a form, does some processing, and then would save that data to the database.

Because I can't forward some data via POST, I wanted to save that data in a session.

For some reason, this is not working - at all.

I can see the data being extracted from the form, and I can forward that via POST, but attempting to save or retrieve it to a session either results in blank data, or a KeyError.

This is the view I am using (very trimmed down, but the error is reproducible)

def processpayment(request, *args, **kwargs):
    if request.method == 'POST':
        form = paymentDetailsForm(request.POST)
        amount = request.POST.get('amount', False);
        stage = request.POST.get('stage', False);  
        if (stage == "checkout"):    
            return render(request, "payment.html", {'form': form, 'amount': amount, 'stage': stage})        
            
        if (stage == "complete"):
            return render(request, "payment.html", {'amount': request.session['amount'], 'stage': stage, 'name': request.session['name']}) 
            
        if (amount == "custom"):
            if form.is_valid():
                amount = form_data.get('amount')
                name = form_data.get('name')
                request.session['amount'] = amount
                request.session['name'] = name
                request.session.modified = True
            return render(request, "payment.html", {'form': form, 'stage': stage, 'amount': amount})  
        return render(request, "payment.html", {'amount': amount, 'stage': stage})                  

    else:       
         return render(request, "payment.html")      
    return render(request, "payment.html")

This is the template I am using::

{% load static %}
{% load widget_tweaks %}

<html>

<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.2/css/bootstrap.min.css" integrity="sha512-rt/SrQ4UNIaGfDyEXZtNcyWvQeOq0QLygHluFQcSjaGB04IxWhal71tKuzP6K8eYXYB6vJV4pHkXcmFGGQ1/0w==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>

<body>

<div class="m-3">

{% if stage == "complete" %}  
  
<<h2 class="mx-auto text-center mb-3">Name:<b>&nbsp; {{ name }}</h2>  

{% elif stage == "checkout" %}

<div style="margin-left: 20%; margin-right: 20%;">

    <h2 class="mx-auto text-center mb-3">Amount to be charged: ${{ amount }}</h2>  

    <div class="col-md-12 mx-auto d-flex justify-content-center mt-2">   
        <div class="container">
                <div class="row ">
                <div class="col-md-6 p-3">
                    <form id='formName' name='formName' action="{% url 'processpayment' %}" method="POST">
                    <input type="hidden" id="amount" name="amount" value="{{ amount }}">
                    <input type="hidden" id="stage" name="stage" value="complete">
                    <button type="submit" name="submit" class="btn btn-primary mt-3">Next</button>
                    </form>
                </div>
            </div>
        </div>    
    </div>

</div>

{% elif amount  == "custom" %}
    
<form action="{% url 'processpayment' %}" method="POST">    
    
    <div class="container pt-3 pb-3">  
          
        <input type="hidden" id="stage" name="stage" value="checkout">         

          <div class="row">
            <div class="col-md-6 mb-2">
              {% render_field form.amount class+="form-control" placeholder="AMOUNT" %}
              </div>
   
            <div class="col-md-6 mb-2">
            {% render_field form.name class+="form-control" placeholder="NAME" %} 
            </div>
            </div>   
          
           <div class="row justify-content-center">
             <span class="input-group-btn">
               <button class="btn btn-primary mt-3" type="submit">Next</button>
             </span>
           </div>        
      </div>

</form>

{% else %}

    <div class="col-md-12 mx-auto d-flex justify-content-center mt-2">   
      <div class="container">
      <div class="row ">
        <div class="col-md-12 text-center p-3">
        <form id='formName' name='formName' action="{% url 'processpayment' %}" method="POST">
        <input type="hidden" id="amount" name="amount" value="custom">
        <button type="submit" name="submit" class="btn btn-primary">Make a payment</button>
        </form>
        </div>
      </div>
      </div>    
    </div>
    
    
{% endif %}
    
</div>

</body>
    
</html>

And just for completeness my forms.py:

from django import forms
from django.forms import widgets
from .models import PaymentMade

class paymentDetailsForm(forms.ModelForm):
    name = forms.CharField(max_length=50)
    amount = forms.IntegerField()

    def __init__(self, *args, **kwargs):
        super(paymentDetailsForm, self).__init__(*args, **kwargs)
        self.fields['name'].required = False
        self.fields['amount'].required = False

    class Meta:
        model = PaymentMade
        fields = ('name', 'amount')

In the 'custom' stage, if I manually set data to be stored in a session, e.g.

request.session['newvar'] = "test works"

I can retrieve that data from the session without any issues at all. So it doesn't seem to be an issue with sessions as such as saving the data from a form into a session variable.

I assume I am missing something very simple, but after working on this for several hours and testing different things I cannot see what I am doing wrong.

edit: I also have the following settings enabled in my settings.py:

SESSION_EXPIRE_AT_BROWSER_CLOSE = True
SESSION_COOKIE_AGE = 1200 
SESSION_SAVE_EVERY_REQUEST = True
Jake Rankin
  • 714
  • 6
  • 22
  • Which session engine are you using? – Dauros Mar 27 '23 at 09:15
  • @Dauros Just the default, `django.contrib.sessions.middleware.SessionMiddleware`, I do have some other settings enabled which I have edited into my question. – Jake Rankin Mar 27 '23 at 09:33
  • You are not sending a 'name' field in the first version of your form (the last one in your template with amount `value='custom`), so that session key is never set with a value – SamSparx Mar 27 '23 at 09:40
  • @SamSparx that is by design. `value=custom` takes to a form where user can enter a custom amount. No name is taken at that point, but it redirects to `value=checkout` which is where the `name` field is coming from. – Jake Rankin Mar 27 '23 at 09:57
  • But the 'complete' stage uses the request.session['name'] value, not the form value, and the session value still never gets set, so the template won't display it. – SamSparx Mar 27 '23 at 10:05
  • @SamSparx The first form a user would see, after the final `else`, simple submits `amount=custom`, which then displays a form with fields for `name` and `amount`. At this point when the form is submitted, both `name` and `amount` are sent to the view, the data is cleaned, and both fields should be saved to the session. In the 'complete' stage, it should be using `request.session['name']`, because that should contain the data from the form saved in the 'custom' stage. – Jake Rankin Mar 27 '23 at 10:11
  • @samsparx The form value and the request.session['name'] value should be the same, except in the final stage the form data no longer exists, which is why it should be being pulled from the session. I'm rereading the code in light of your comments but I can't see why the form value shouldn't be able to be saved and retrieved from a session. – Jake Rankin Mar 27 '23 at 10:14
  • When you `return` in a function the function ends - because the function has 'returned' something. In the second form, 'stage' = 'checkout' simply returns the rendered page with the context variables supplied. You will need to repeat the is_valid() function in each appropriate case in order for it to work how you envision. I'll try and supply some code in an answer. – SamSparx Mar 27 '23 at 10:17
  • @samsparx I am a little confused, so I appreciate that, thank you. I should mention this is a very trimmed down version of my actual view just to demonstrate the issue - in the actual view I have one stage that is GET only, and there is no POST data. That's why I am trying to rely purely on the session as soon as I obtain the data the first time. – Jake Rankin Mar 27 '23 at 10:27
  • Basically, in the code above, the is_valid() function that sets the session variables only runs if the amount=="custom". The function ends too early if the stage=='checkout' or 'complete' as they both return renderings, and even if they didn't return anything, the is_valid() doesn't run later because amount dopes not equal 'custom'. Hopefully the code below makes this a little clearer. – SamSparx Mar 27 '23 at 10:35
  • @SamSparx I do follow what you are saying now (and thank you for yoru answer) - I didn't see that as an issue (and still don't entirely understand how it is), because each stage can only occur in a specific order/position. Complete can only come after 'checkout' and 'checkout' can only come after 'custom', so the 'is_valid' check only being in the 'custom' stage I thought would be fine. However, I am having the same issue using the code in the answer you provided, so it might be something with my setup. Commenting on your answer now. – Jake Rankin Mar 27 '23 at 10:42
  • Have you ever debugged the form error errors and values? – Marco Mar 27 '23 at 11:06
  • @Marco I'm not getting any form errors, and for my testing I am using a single number for 'amount' and a short strong of letters for 'name', e.g. 5 and 'tttt'. – Jake Rankin Mar 27 '23 at 11:07
  • You should use cleaned form data instead of accessing them directly. – Marco Mar 27 '23 at 13:28

1 Answers1

1

Based on the comments, I suspect you are not reaching your is_valid() code when you want it in your various form states. Try something like this.

from django.contrib import messages

def processpayment(request, *args, **kwargs):
    if request.method == 'POST':
        form = paymentDetailsForm(request.POST)
        amount = request.POST.get('amount', False);
        stage = request.POST.get('stage', False);  

        if form.is_valid():
            form_data = form.cleaned_data
            amount = form_data.get('amount')
            name = form_data.get('name')
            if amount:
                request.session['amount'] = amount
            if name:
                request.session['name'] = name
            if name or amount:
                request.session.modified = True

            if (stage == "checkout"):    
                return render(request, "payment.html", {'form': form, 'amount': amount, 'stage': stage})        
        
            if (stage == "complete"):
                return render(request, "payment.html", {'amount': request.session['amount'], 'stage': stage, 'name': request.session['name']}) 
        
            if (amount == "custom"):
                return render(request, "payment.html", {'form': form, 'stage': stage, 'amount': amount})  
        
        else:
            messages.error(request, "Your form is invalid") 
            #print errors to terminal - you can also put them in a template
            print(form.errors)
            print(form.non_field_errors)             
            return render(request, "payment.html", {'amount': amount, 'stage': stage, 'form':form})                      

    return render(request, "payment.html")

To see messages in your template add something like:

    {% for message in messages %}
        {{ message | safe }}
    {% endfor %}
SamSparx
  • 4,629
  • 1
  • 4
  • 16
  • Thank you for this answer, and I do understand what you were saying better now. However, after making the changes in your answer, I still get the same KeyError. So I am wondering if it is something less with the code and more with my setup? – Jake Rankin Mar 27 '23 at 10:43
  • What is the specific error and which stage is it? – SamSparx Mar 27 '23 at 10:52
  • 'complete' stage, KeyError for 'firstname' – Jake Rankin Mar 27 '23 at 10:56
  • I'll assume 'firstname' is 'name' transposed (but obviously make sure you've changed all occurrences as appropriate). I suspect part of the problem is you are advancing the stage regardless of whether or not the form is valid so I changed my answer a little to do some error checking. – SamSparx Mar 27 '23 at 11:49
  • You were right about 'firstname' and thank you again for your answer. After using your code with the error checking, I am getting more information to troubleshoot, but this is leading to me being much more confused. I added in what you suggested to see messages in the template, and I also pass `form.error`s and `form.non_field_errors` to the template. Where I was getting the key error before, now `form.errors` outputs only and literally `>` as the error. I have no idea what to make of that. the messages that are returned are 'Your form is invalid' repeated 5 times. I find this very perplexing. – Jake Rankin Mar 27 '23 at 12:13
  • '>' showing up sounds like an HTML error when you are looping through. Have a look at https://stackoverflow.com/questions/14647723/django-forms-if-not-valid-show-form-with-error-message for a template example or just view your HTML page source and see what is output there. – SamSparx Mar 27 '23 at 18:20
  • I found out that the form was never being considered valid so long as I was passing a text string via `amount` which was defined as an integerField. I added a new variable for payment type to be 'fixed' or 'custom' and pass that along with `amount`, and now everything is fine. It was your suggestions and help that got me to the ultimate solution,and I will use what I learned to influence how I author forms in the future. Thank you for all your help. – Jake Rankin Mar 29 '23 at 06:50