How to correctly use Fetch in JavaScript and Django? - javascript

I am trying to make a METAR decoder as shown:
I am using fetch in Vanilla js and I plan to send the entered code to a Django view. From the Django view, the decoded data will be taken and displayed in the template.
views.py
def ToolsPageView(request):
if request.method == "POST":
jsonData = json.loads(request.body)
metarCode = jsonData.get('Metar')
return JsonResponse("Success", safe=False)
return render(request, 'app/tools.html')
urls.py
...
path("tools", views.ToolsPageView, name="tools")
tools.html
<div class="metar-code-decode">
<form method="POST" action="{% url 'tools' %}" id="metar-form">
{% csrf_token %}
<input type="text" placeholder="Enter METAR: " id="metar-value"> <br>
<input type="submit" id="metar-button">
</form>
</div>
tool.js
function getDecodedMetar() {
let formButton = document.querySelector("#metar-button");
formButton.onclick = function (e) {
let metarCode = document.querySelector("#metar-value").value;
sendMetar(metarCode);
//e.preventDefault();
//getMetar(metarCode);
};
}
function sendMetar(metarCode) {
fetch('/tools', {
method: "POST",
headers: {
"X-CSRFToken": getCookie("csrftoken"),
},
body: JSON.stringify({
Metar: metarCode,
}),
});
}
I have used the same code for POST using fetch where I had to let user update his/her profile. And that worked. But, this does not work and the error keeps on changing from time to time after restarting the server. At the first try, there was no error produced and the server also showed a POST request being made. And the latest error which I am getting is "In order to allow non-dict objects to be serialized set the safe parameter to False." I get the same thing again and again even after setting safe=False within the JsonResponse(). Worth to note, request when converted to request.json() gives an error.
Am I using fetch wrongly? If yes, what is the correct way?

I'm not sure you have the flow right. The idea is that the button, when clicked, will call a function (fetch) that will send data to the view, which will decode it and send it back to the JavaScript, so that it could be displayed without reloading the entire page.
I think this might help:
let formButton = document.querySelector("#metar-button");
// When the button is clicked,
formButton.onclick = function(e) {
// do NOT send the form the usual way
e.preventDefault();
let metarCode = document.querySelector("#metar-value").value;
// Run the function that will send the code to the ToolsPageView
sendMetar(metarCode);
}
async function sendMetar(metarCode) {
const response = await fetch('/tools', {
method: "POST",
headers: {
"X-CSRFToken": getCookie("csrftoken"),
},
body: JSON.stringify({
'Metar': metarCode,
}),
})
.then(response => response.json())
.then(data => {
console.log(data);
// extract the decoded value from the data sent back from the view
// display it by targeting the element in your html that you want
// to display it
});
}
And in your view,
def ToolsPageView(request):
if request.method == "POST":
jsonData = json.loads(request.body)
metarCode = jsonData.get('Metar')
# Remove the original JsonResponse
# return JsonResponse("Success", safe=False)
# and INSTEAD,
# Send the code back to the JavaScript
# I don't THINK you need safe=False here?
return JsonResponse({'MetarCode': metarCode})
return render(request, 'app/tools.html')

Related

How to remove old form to stop double-submission

I am using AJAX to submit a form from a modal window. If the form is submitted with errors the server will send back a new form. The container holding the old form is then removed and the new form, with validation errors, is set in it's place.
The issue is that, for some reason, it submits 2 forms at the same exact time after the validation errors are fixed. It is as-if the old form does not get removed?
It does not happen when I use the onclick=functionCall() syntax but rather when I use the $('form').on('submit', e => { syntax. I am trying to use the latter as I was told the onX events are now considered bad practice.
Any help on understanding what is happening and how to fix it would be useful.
Form:
// Submit Form
$('form').on('submit', e => {
e.preventDefault();
let data = new FormData($(e.target).get(0));
const btn = $(e.target).find('button:submit')
const container = document.getElementById('formFields')
$.ajax({
url : e.target.action,
data : data,
type : 'POST',
headers : {"X-CSRFToken": token},
enctype : 'multipart/form-data',
processData : false,
contentType : false,
success : function (data) {
btn.toggleClass('btn-warning btn-secondary').prop('disabled', true)
},
error : function (data) {
container.innerHTML = loadingSpinner;
$(container).empty();
$(container).html(data['responseJSON']['table']).fadeIn('slow');
},
});
});
View:
#login_required
def load_add_memo_form(request, pk=None):
if request.method == 'POST' and request.is_ajax():
memo_form = MemoForm(
data=request.POST,
files=request.FILES,
company=request.tenant)
memo_form.instance.sender = request.user
if pk:
memo = get_object_or_404(Memo, pk=pk)
memo_form = Memo(
data=request.POST,
files=request.FILES,
company=request.tenant,
instance=memo)
if memo_form.is_valid():
if request.user.role.supervisor is None or request.user is memo_form.sender:
memo_form.save()
reason_for_notification = 'memo to view'
notifier = SendNotifications(request, reason_for_notification, memo_form)
notifier.send_notifications()
return JsonResponse({'success': True})
else:
data = {'table': render_to_string('memos/components/add_memo_form.html', context={'form': memo_form})}
return JsonResponse(data, status=400)
else:
memo_form = MemoForm(company=request.tenant)
if pk:
memo = get_object_or_404(Memo, pk=pk)
memo_form = MemoForm(company=request.tenant, instance=memo)
context = {'form': memo_form}
template = 'memos/components/add_memo_form.html'
return render(request, template, context)
And obvious the form is just templating language...

Having Trouble Accessing Variables & Setting Up Stripe Connect (Flask)

I'm building a Flask marketplace app (using Stripe Collect payments then pay out) where users can choose how much they want to pay (think fundraiser).
I am having trouble moving necessary data around appropriately throughout the checkout process and could really use some help.
Once a user enters how much they'd like to donate, the donation amount and the owner of the campaign they'd like to donate to are sent to the below /pay route where they see a form to enter in their card details with a "Submit Payment" button.
#app.route('/pay', methods=['GET', 'POST'])
def pay():
campaign = Campaign.query.get_or_404(request.args["campaign_id"])
owner = User.query.get_or_404(campaign.user_id) #owner of the campaign
donation_amount = request.args['amount_entered'] # EX: "1000"
return render_template('payment_form.html', campaign=campaign, owner=owner, amount=donation_amount)
The file, payment_form.html, has a simple Stripe form like this:
<form method="post" id="payment-form" class="sr-payment-form">
<div class="sr-form-row">
<label for="card-element">Credit or debit card</label>
<div style="width: 30em" id="card-element"></div>
</div>
<div>
<span style="width: 30em; height: 2em; letter-spacing: 0em" id="card-errors" role="alert"></span>
</div>
<button id="card-button" style="width: 33em;">Submit Payment</button>
</form>
And whenever someone enters their card info and submits the payment, I have a JavaScript file that listens for it and processes the payment (this does not work yet).
var form = document.getElementById('payment-form');
form.addEventListener('submit', function(ev) {
ev.preventDefault();
fetch("/pay_now", {
method: "GET",
headers: {
"Content-Type": "application/json"
},
})
.then(response => response.json())
.then(data => {
// not sure what to do here
});
stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: card,
billing_details: {
name: 'Jenny Rosen' //placeholder (would like to replace)
}
}
}).then(function(result) {
if (result.error) {
console.log(result.error.message);
} else {
if (result.paymentIntent.status === 'succeeded') {
}
});
});
This script fetches the below Flask API /pay_now and should return the clientSecret variable as well as other necessary data to complete the transaction.
#app.route('/pay_now', methods=['GET','POST'])
def create_payment():
intent = stripe.PaymentIntent.create(
payment_method_types=['card'],
amount="1000", #need to pass dollar amount here calculated in /pay route
currency="usd",
transfer_data={'destination': owner.stripe_id}, #need to pass owner from /pay route
application_fee_amount="100")
)
client_secret = intent.client_secret
return jsonify({"client_secret": client_secret})
So basically, my dilemma is that I have the amount of the donation and the campaign owner as variables in the /pay route. But I need to access them when I create the stripe.PaymentIntent object when I call the /pay_now API from my JavaScript and then I need to pass the clientSecret variable back to my JavaScript file for confirmCardPayment() to actually complete the payment.
I'm also open to different approaches if mine doesn't make sense.
I am new to Stripe & new to APIs in Flask. Any help or explanation here would be extremely helpful. Thanks in advance!
You'd want to only POST to your /pay_now route, in the body of that POST you should include the amount your user intends to donate. Then it's a simple case of including that amount when creating the PaymentIntent and returning the client secret to be confirmed on the client.
You might want to first do some checks on both the client and server whether the amount makes sense (e.g. if someone enters -1 it correctly gets rejected).
The stripe.confirmCardPayment code should go in the then resolver of your fetch request, after the response has been parsed to JSON:
fetch("/pay_now", {
method: "POST",
body: JSON.stringify({
amount: amount, // get this from the form
}),
headers: {
"Content-Type": "application/json"
},
})
.then(response => response.json())
.then(data => {
stripe.confirmCardPayment(data.clientSecret, {
payment_method: {
card: card,
billing_details: {
name: name, // get this from the form, like you did with the amount
}
}
})
.then(function(result) {
if (result.error) {
console.log(result.error.message);
} else {
if (result.paymentIntent.status === 'succeeded') {
// display success message
}
}
});

jquery ajax request to nodejs server only works on page refresh

I am quite new to AJAX and I am not sure what I am doing wrong. I have a webpage that fetches all comments on a post with an AJAX get request. The issue is that the AJAX request is only successful after the webpage is refreshed. I disabled the cache to see if that would solve the issue, but it didn't.
For example, when I fetch the first comments after refreshing the page from post A and then go onto post B on the website, the comments from the post A appear as the comments for post B, then when I refresh the page the comments for post B are replaced with post B's comments successfully.
I am using jQuery to make the request:
$.ajax({
type: "GET",
url: someURL,
success: (comments) => {
console.log(comments);
comments.questions.forEach(questionComment => {
$('.questionComments').append(
`<div class="comment">
<p>${questionComment.content}</p>
</div>
`
)
});
comments.answers.forEach(answer => {
answer.forEach(answerComment => {
$(`.answer#${answerComment.forId} .answerComments`).append(
`<div class="comment">
<p>${answerComment.content}</p>
</div>
`
)
})
})
},
cache: false
})
Server-Side: (express.js, mongoose)
let allComments = {}
app.get('/questions/:questionID/getComments', (req, res) => {
if (err) return console.error(err)
Comment.find({ forQuestion: true, forId: req.params.questionID }, (err, questionComments) => {
allComments['questions'] = questionComments
})
Answer.find({ questionId: req.params.questionID }, (err, answers) => {
if (err) return console.error(err);
allAnswerComments = []
answers.forEach(answer => {
Comment.find({ forAnswer: true, forId: answer._id }, (err, comments) => {
allAnswerComments.push(comments)
})
});
allComments['answers'] = allAnswerComments
})
res.send(allComments)
})
What the commets object looks like before the reload - a blank object
What the comments object looks like after the reload
When you navigate to a different post / different URL, the object from the previous post / URL is the initial object on the new post, and then when you reload the page the correct object is fetched.
According to your description, the AJAX request works when executed, but the problem is that it's executed only once, at page load. Let's write a function for this purpose:
function sendAJAX() {
$.ajax({
type: "GET",
url: someURL,
success: (comments) => {
console.log(comments);
comments.questions.forEach(questionComment => {
$('.questionComments').append(
`<div class="comment">
<p>${questionComment.content}</p>
</div>
`
)
});
comments.answers.forEach(answer => {
answer.forEach(answerComment => {
$(`.answer#${answerComment.forId} .answerComments`).append(
`<div class="comment">
<p>${answerComment.content}</p>
</div>
`
)
})
})
},
cache: false
})
}
And ensure that it's called periodically:
sendAJAX();
seInterval(sendAJAX, 10000);
This will still be incorrect, because it will add all comments every ten seconds. To improve this, you could add a value representing the moment of the last request and on server-side load only the comments that were created between that moment and the current one.
Can you please describe your working on get parameters how are you setting it.Is there any cookie involved there? Telling the get comment will definitely help us to figure our the problem how ever sendAjax() function will be great idea instead of writing ajax in open Script.
On other side I will never recommend you seInterval(sendAJAX, 10000); instead install a service worker and hit it back from your API when new comment is added. And then it should only fetch the last one and put it in your comment section.
As per your description it seems that the main problem is comment of Post A are appearing in comment of Post B.
Now, there is minimal data provided and by looking and your code, I am assuming that you are not refreshing page programmatically when switching from Post A to Post B.
So, the root cause of your problem is append. You are appending data to your div's. So, before your foreach loops, first clear the html using
$('.questionComments').html("")
And for the answers replace it as below
comments.answers.forEach(answer => {
let first = true;
answer.forEach(answerComment => {
if(first) {
$(`.answer#${answerComment.forId} .answerComments`).html("")
first = false;
}
$(`.answer#${answerComment.forId} .answerComments`).append(
`<div class="comment">
<p>${answerComment.content}</p>
</div>
`
)
})
})
The above snippet is just to demonstrate that you have to identify when the first comment is added and then reset it.
I haven't tested the snippet but I hope you got an idea what is wrong in the code.

Flask server returning 400 error code from JS rest call

I'm calling my Flask view function (API) via Javascript (FETCH).
I'm having success with the GET method, but when using the PATCH method I'm receiving a 400 error code
(Failed to load resource: the server responded with a status of 400 (BAD REQUEST)).
I reviewed the url, seems ok and the ID is being passed as an integer, so no clue on why it's giving this error message.
This function (load_follow_link) is fetching via GET and updating my "Follow" to "Unfollow" tag (no errors here):
function load_follow_link(id) {
apply_csrf_token();
fetch(`/follow_unfollow/${id}`)
.then(response => response.json())
.then(data => {
if (data.is_following)
document.getElementById('follow-unfollow-btn').innerHTML = 'Unfollow';
else
document.getElementById('follow-unfollow-btn').innerHTML = 'Follow';
document.getElementById('followers-count').innerHTML = data.followers_count;
document.getElementById('following-count').innerHTML = data.following_count;
});
}
This is the PATCH function (follow_unfollow) triggering the error message. It is supposed to call the view function and update the DB:
function follow_unfollow(id) {
apply_csrf_token();
fetch(`/follow_unfollow/${id}`, {
method: 'PATCH'
})
.then(() => {
load_follow_link(id);
});
}
view function (doesn't get executed when request method is PATCH)
#app.route('/follow_unfollow/<int:tutor_id>', methods=['GET','PATCH'])
#login_required
def follow_unfollow(tutor_id):
""" GET: returns if user is following the tutor, followers and following total
PUT: follow or unfollow
"""
user = Users.query.get(current_user.id)
try:
tutor = Tutors.query.get(tutor_id)
except NoResultFound:
return JsonResponse({"error": "Tutor not registered"}, status=401)
following_count = user.following_total()
followers_count = tutor.followers_total()
is_following = user.is_following(tutor)
if request.method == 'GET':
return jsonify(is_following=is_following, followers_count=followers_count,
following_count=following_count)
elif request.method == 'PATCH':
if is_following:
user.unfollow(tutor)
else:
user.follow(tutor)
db.session.commit()
return success.return_response(message='Successfully Completed', status=204)
else:
return jsonify(error="GET or PUT request required", status=400)
I appreciate the help
Could be you are not returning a valid response. Where is success defined in success.return_response?
Why not just return jsonify(...)?

Using JavaScript to submit an array and a file object to a Rails backend

I haven't been able to figure out how to get my JavaScript to send a request in a format that Rails will accept when I try to edit a Game with a File parameter and an array parameter in the same payload.
The Rails controller looks like this (simplified, obviously):
class GamesController < ApplicationController
def update
#game = Game.find(params[:id])
authorize #game
respond_to do |format|
if #game.update(game_params)
format.html { render html: #game, success: "#{#game.name} was successfully updated." }
format.json { render json: #game, status: :success, location: #game }
else
format.html do
flash.now[:error] = "Unable to update game."
render :edit
end
format.json { render json: #game.errors, status: :unprocessable_entity }
end
end
end
private
def game_params
params.require(:game).permit(
:name,
:cover,
genre_ids: [],
engine_ids: []
)
end
end
So I have JavaScript like so:
// this.game.genres and this.game.engines come from
// elsewhere, they're both arrays of objects. These two
// lines turn them into an array of integers representing
// their IDs.
let genre_ids = Array.from(this.game.genres, genre => genre.id);
let engine_ids = Array.from(this.game.engines, engine => engine.id);
let submittableData = new FormData();
submittableData.append('game[name]', this.game.name);
submittableData.append('game[genre_ids]', genre_ids);
submittableData.append('game[engine_ids]', engine_ids);
if (this.game.cover) {
// this.game.cover is a File object
submittableData.append('game[cover]', this.game.cover, this.game.cover.name);
}
fetch("/games/4", {
method: 'PUT',
body: submittableData,
headers: {
'X-CSRF-Token': Rails.csrfToken()
},
credentials: 'same-origin'
}).then(
// success/error handling here
)
The JavaScript runs when I hit the submit button in a form, and is supposed to convert the data into a format Rails' backend will accept. Unfortunately, I'm having trouble getting it to work.
I'm able to use JSON.stringify() instead of FormData for submitting the data in the case where there's no image file to submit, like so:
fetch("/games/4", {
method: 'PUT',
body: JSON.stringify({ game: {
name: this.game.name,
genre_ids: genre_ids,
engine_ids: engine_ids
}}),
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': Rails.csrfToken()
},
credentials: 'same-origin'
})
This works fine. But I haven't been able to figure out how to use JSON.stringify when submitting a File object. Alternatively, I can use a FormData object, which works for simple values, e.g. name, as well as File objects, but not for array values like an array of IDs.
A successful form submit with just the ID arrays (using JSON.stringify) looks like this in the Rails console:
Parameters: {"game"=>{"name"=>"Pokémon Ruby", "engine_ids"=>[], "genre_ids"=>[13]}, "id"=>"4"}
However, my current code ends up with something more like this:
Parameters: {"game"=>{"name"=>"Pokémon Ruby", "genre_ids"=>"18,2,15", "engine_ids"=>"4,2,10"}, "id"=>"4"}
Unpermitted parameters: :genre_ids, :engine_ids
Or, if you also upload a file in the process:
Parameters: {"game"=>{"name"=>"Pokémon Ruby", "genre_ids"=>"13,3", "engine_ids"=>"5", "cover"=>#<ActionDispatch::Http::UploadedFile:0x00007f9a45d11f78 #tempfile=#<Tempfile:/var/folders/2n/6l8d3x457wq9m5fpry0dltb40000gn/T/RackMultipart20190217-31684-1qmtpx2.png>, #original_filename="Screen Shot 2019-01-27 at 5.26.23 PM.png", #content_type="image/png", #headers="Content-Disposition: form-data; name=\"game[cover]\"; filename=\"Screen Shot 2019-01-27 at 5.26.23 PM.png\"\r\nContent-Type: image/png\r\n">}, "id"=>"4"}
Unpermitted parameters: :genre_ids, :engine_ids
TL;DR: My question is, how can I send this payload (a name string, an array of IDs, as well as a game cover image) to Rails using JavaScript? What format will actually be accepted and how do I make that happen?
The Rails app is open source if that'd help at all, you can see the repo here. The specific files mentioned are app/controllers/games_controller.rb and app/javascript/src/components/game-form.vue, though I've simplified both significantly for this question.
I figured out that I can do this using ActiveStorage's Direct Upload feature.
In my JavaScript:
// Import DirectUpload from ActiveStorage somewhere above here.
onChange(file) {
this.uploadFile(file);
},
uploadFile(file) {
const url = "/rails/active_storage/direct_uploads";
const upload = new DirectUpload(file, url);
upload.create((error, blob) => {
if (error) {
// TODO: Handle this error.
console.log(error);
} else {
this.game.coverBlob = blob.signed_id;
}
})
},
onSubmit() {
let genre_ids = Array.from(this.game.genres, genre => genre.id);
let engine_ids = Array.from(this.game.engines, engine => engine.id);
let submittableData = { game: {
name: this.game.name,
genre_ids: genre_ids,
engine_ids: engine_ids
}};
if (this.game.coverBlob) {
submittableData['game']['cover'] = this.game.coverBlob;
}
fetch(this.submitPath, {
method: this.create ? 'POST' : 'PUT',
body: JSON.stringify(submittableData),
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': Rails.csrfToken()
},
credentials: 'same-origin'
})
}
I then figured out that, with the way DirectUpload works, I can just send the coverBlob variable to the Rails application, so it'll just be a string. Super easy.
You can convert the File object to a data URL and include that string in JSON, see processFiles function at Upload multiple image using AJAX, PHP and jQuery or use JSON.stringify() on the JavaScript Array and set that as value of FormData object, instead of passing the Array as value to FormData.
submittableData.append('game[name]', JSON.stringify(this.game.name));
submittableData.append('game[genre_ids]', JSON.stringify(genre_ids));
submittableData.append('game[engine_ids]', JSON.stringify(engine_ids));

Categories

Resources