1

We're creating a web application using Flask. On one html page, users answer some questions (using <form>). During this process, they are asked to click a button on the page for a few times when they receive random popup notifications. In the .html file, we write javascript functions to save the timestamps of users clicking on the button into an array.

We hope to store both the user's answers and the timestamps of clicking the button. Now from .html to .py, we pass answers using request.form and pass the array of timestamps using ajax json (request.json). The request.form works very well but the request.json always gives a None. We have tried request.get_json(), request.get_json(force=True), request.get_json(silent=True), but none of them works.

Our codes are like the following:

in trial.py:

from flask import (Blueprint, flash, g, redirect, render_template, request, url_for, jsonify, json)
from werkzeug.exceptions import abort
import csv
from flaskr.db import get_db

bp = Blueprint('trial', __name__, url_prefix='/trial')

@bp.route('', methods=('GET', 'POST'))
def trial():
    if request.method == 'POST':
        if "Q1" in request.form:
            ## this part retrieves data from request.form 
            ## skip the details in the following
            db = get_db()
            db.execute(
                    'INSERT INTO trial (...)', (...))
            db.commit()

        timestamps = request.json
        print(timestamps)  ### This is always a None

   return render_template('study/trial.html')

In trial.html:

{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}Trial{% endblock %}</h1>
{% endblock %}

{% block content %}

  <!-- Skip non-related details in the following --> 
  <button class="NotificationButton" onclick="popup()">I Got A Notification</button>

  <div>
    <form name="TrialQuestions" id="TrialQuestions" method="post">
      <!-- skip the details of the questions and input for answers -->                                             
    </form>
  </div>

  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
  <script>window.jQuery || document.write('<script src="{{url_for('static', filename='jquery.js') }}">\x3C/script>')</script>
  
  <script>
    var timestamps = {"trial":[]};

    function playAudio() { 
      <!-- skip non-related details -->
      timestamps["trial"].push(remaining);
    } 

    function popup(){
      <!-- skip non-related details -->
      timestamps["trial"].push(remaining);
    }

    function timeUp(){
      document.TrialQuestions.submit();
      $.ajax({
        type: "POST",
        url: "{{ url_for("trial.trial") }}",
        contentType: "application/json",
        data: JSON.stringify(timestamps),
        dataType: "json",
        success: function(response) {
        console.log(response);
        },
        error: function(err) {
        console.log(err);
        }
      });
      alert("Time Is Up! Your Answers Have Been Submitted!");
    }

  </script>
{% endblock %}

The console.log(response); in trial.html works well, but the timestamps = request.json in trial.py always gives a None. We have searched and tried the suggestions from the following posts but none of them solves the problem:

  1. Passing Javascript array in Python Flask
  2. Get the data received in a Flask request
  3. How to send information from javascript to flask route

Can anyone give some hints? Thanks a lot!

gladys0313
  • 2,149
  • 3
  • 23
  • 42
  • your form gets submitting before ajax call take place i.e : `document.TrialQuestions.submit();` after this line you have ajax call . – Swati Jan 04 '21 at 13:15
  • @Swati I also tried swapping these two positions, but it is still a None – gladys0313 Jan 04 '21 at 13:27
  • why not send formdata as well using ajax ? – Swati Jan 04 '21 at 13:53
  • @Swati no special reason. we can also send formdata using ajax, but now sending data using ajax has the above problem. We also tried commenting out all form related operation and only sent timestamps using ajax, but still a None, I think this means the ajax sending has problem... – gladys0313 Jan 04 '21 at 14:13
  • I see did you tried printing `timestamps` does it has correct value ? Also , check browser console for any error and check [this](https://stackoverflow.com/questions/1820927/request-monitoring-in-chrome) as well. – Swati Jan 04 '21 at 14:20
  • @Swati Thanks for the suggestion. in ``.html`` inside the ``.ajax``, ``console.log(response)`` prints the correct value, but in ``.py``, ``print(timestamps)`` after the ``request.json`` gives a "None".. – gladys0313 Jan 04 '21 at 14:24
  • I think the first problem is that your ajax request cannot be completed if your form submit reloads the whole page. In addition, your ajax request expects a response in json format. Since in your example you are not sending any data in json format from the server to the client, the line ```dataType = "json"``` throws an error. Remove this line or reply from your Flask route in json format. If you still want to submit your form, you can send it as soon as your ajax request has been completed. – Detlef Jan 04 '21 at 15:38
  • @Detlef Thanks for your explanation. As you suggested, I made the following two changes: (1) swap the form submit and ajax post in the ``timeUp()`` function in ``trial.html`` and swap the ``request.json`` and ``request.form`` in ``trial.py``; (2) comment the ``dataType:"json"``. However, the result is still a "None". Did I misunderstand anything? – gladys0313 Jan 04 '21 at 15:54

2 Answers2

1

Your form submit results in the page being reloaded and the template being transmitted again as html from the server.

An asynchronous javascript request is only ended when the callback for success or error is called. The transmission of the data is not waited for.

function timeUp() {
  console.log("sending data to " + "{{ url_for('trial.trial') }}")

  $.ajax({
    type: "POST",
    url: "{{ url_for('trial.trial') }}",
    contentType: "application/json",
    data: JSON.stringify(timestamps),
    dataType: "json",
    success: function(response) {
      console.log(response);
      // The data has now been successfully transferred.
      
      // document.TrialQuestions.submit();
    },
    error: function(err) {
      console.log(err);
    }
  });
}

When specifying the data type, the query expects exactly this as an answer. See also here.
For this reason, I am returning a response in json format.

from flask import render_template, request, jsonify

@bp.route('/', methods=('GET', 'POST'))
def trial():
    if request.method == 'POST':
        print('got a post request')

        if request.is_json: # application/json
            # handle your ajax request here!
            print('got json:', request.json)
            return jsonify(success=True) # answer as json
        else: 
            # handle your form submit here!
            print(request.form)
    return render_template('trial.html')

AJAX has the advantage that you can send and load data sets without reloading and rendering the whole page.

Within a transmission, the data is formatted in a predefined format. When you submit a form you use the type "application/x-www-form-urlencoded" or "multipart/form-data". For JSON it is "application/json".
Depending on the selected format, either the query "request.json" returns a "None" or "request.form" is an empty dictionary.

The structure of your data set depends on your requirements.
The following is a suggestion that is similar to the original approach of a form.

function timeUp() {
  const data = {
    "data": $("form[name='TrialQuestions']").serializeArray().map(elem => {
      return [elem["name"], elem["value"]];
    }),
    "ts": timestamps,
  };

  $.ajax({
    type: "POST",
    url: "{{ url_for('trial.trial') }}",
    contentType: "application/json",
    data: JSON.stringify(data),
    dataType: "json",
    success: function(response) {
      console.log(response);
    },
    error: function(err) {
      console.log(err);
    }
  });
}

Should you just want to send a form, for example because you want to do a redirect. Add one or more hidden fields to the form and fill in their value before submitting.

<form name="TrialQuestions" method="post">
  <label for="q1">Question 1</label>
  <input name="q1" id="q1" value="a1" />
  <input name="ts" type="hidden" />
</form>

<script>
  function timeUp() {
    document.forms["TrialQuestions"]["ts"].value = timestamps.join();
    document.forms["TrialQuestions"].submit();
  }
</script>
Detlef
  • 1,285
  • 2
  • 3
  • 12
  • Sorry that I deleted my previous comment. I found a problem in your answer. By using your code, ``request.json`` works but ``request.form`` will give a "None".. – gladys0313 Jan 04 '21 at 18:49
0

... update to the question from myself.

As posted in the question, request.form can work but request.json will give a "None".

Following Detlef's approach, according to what I tried on my side, request.json can work but request.form will give a "None". Please correct me if I mistaken anything.

So, now I propose a trade-off approach: package the form and the timestamps into one ajax JSON object. The code is now like this:

  <div>
    <form name="TrialQuestions" id="TrialQuestions">  
      <label for="Q1">Question 1</label>
      <input name="Q1" id="Q1"> 
      <!-- skip the questions -->                                              
    </form>
  </div>

  <script>
  var TrialQuestionAnswers = [];
  var timestamps = [];
  var formAndTimestamps = [];
  
  function timeUp(){
      TrialQuestionAnswers = [document.getElementById("Q1").value, ..., document.getElementById("Q13").value];
      formAndTimestamps.push(TrialQuestionAnswers);
      formAndTimestamps.push(timestamps);

      $.ajax({
        type: "POST",
        url: "{{ url_for("trial.trial") }}",
        contentType: "application/json",
        data: JSON.stringify(formAndTimestamps),
        dataType: "json",
        success: function(response) {
        console.log(response);
        },
        error: function(err) {
        console.log(err);
        }
      });
      alert("Time Is Up! Your Answers Have Been Submitted!");
    }
  </script>

It can work, but maybe this is not the most efficient solution. Better idea is much appreciated!

gladys0313
  • 2,149
  • 3
  • 23
  • 42