I am working on my first web dev project and I am completely flummoxed by this error. My html contains a jinja loop to iteratively generate checkboxes with unique ids. I would like to tick checkboxes based on a list of IDs returned from my database with AJAX through jquery, but it seems theres some weird jinja - jquery interaction that means the checkbox ids are not working.
js file example section:
$(document).ready(function () {
$("#selectAll").click(function () {
// get checkbox ids from database as a list:
$.ajax({
url: "/api/get-availability",
type: 'GET',
data: {
"Pro": $("#prochoice").text(),
"Date": [day, month, year].join('/')
},
success: function (data) {
for (let d of data) {
$("#" + d).prop('checked', true); // iterate over ids for checkboxes
}
}
});
}
html file for checkboxes:
<!-- choose time -->
<div class="mt-3 mb-0 m-2" style="text-align: left;"><label>Choose availability:</label>
<!-- iterate over checkboxes using jinja -->
<div class="container row justify-content-md-center">
{% for tt1, tt2 in time %}
<div class="col-md-6">
<div class="custom-control custom-checkbox m-0 mt-1" style="text-align: left;">
<input type="checkbox" name="timescheck" class="custom-control-input" id="{{ tt1 }}">
<label class="custom-control-label" for={{ tt1 }}>{{ tt1 }} - {{ tt2 }}</label>
</div>
</div>
{% endfor %}
</div>
api call
#api.route('/get-availability', methods=['GET'])
def get_availability():
return ['16:00', '16:30', '17:00']
render html template
#website.route("/availability-admin")
def availadmin():
# Set the total time list for checkboxes here:
t1 = ['15:00', '15:30', '16:00', '16:30', '17:00', '17:30']
t2 = ['15:30', '16:00', '16:30', '17:00', '17:30', '18:00']
env = jinja2.Environment()
env.globals.update(zip=zip)
time = zip(t1, t2)
return render_template('availabilty-admin.html', time = time)
Maybe Jinja is interpreting the curly braces as its own syntax, rather than as part of the HTML element's ID. This could solve it. not 100% sure
JS
$(document).ready(function () {
$("#selectAll").click(function () {
// get checkbox ids from database as a list:
$.ajax({
url: "/api/get-availability",
type: 'GET',
data: {
"Pro": $("#prochoice").text(),
"Date": [day, month, year].join('/')
},
success: function (data) {
for (let d of data) {
$("#'" + d + "'").prop('checked', true);// iterate over ids for checkboxes
}
}
});
}
HTML
<!-- choose time -->
<div class="mt-3 mb-0 m-2" style="text-align: left;"><label>Choose availability:</label>
<!-- iterate over checkboxes using jinja -->
<div class="container row justify-content-md-center">
{% for tt1, tt2 in time %}
<div class="col-md-6">
<div class="custom-control custom-checkbox m-0 mt-1" style="text-align: left;">
<input type="checkbox" name="timescheck" class="custom-control-input" id='{{ tt1 }}'>
<label class="custom-control-label" for={{ tt1 }}>{{ tt1 }} - {{ tt2 }}</label>
</div>
</div>
{% endfor %}
</div>
For anyone interested, special characters such as ':' are not accepted for ids. Therefore using the python text.replace(':', '') function before parsing the id was a solution in this case (or better choice of ids in the first place).
Related
My application currently flows through 3 pages:
User selects question in index page
User submits answer in answer page
User is presented with result in results page.
I want to compress that down to a single page where the user submits an answer to the question and result is shown on the same page.
The following django-template code separates questions with Bootstrap accordion. How do I post the form without refreshing the whole page? I want to be able to display the result on the page, update CSS styling with Javascript etc.
<h2>{{ category.title }}</h2>
<div class="accordion" id="accordion{{category.title}}">
{% for challenge in category.challenge_set.all %}
<div class="card">
<div class="card-header" id="heading{{challenge.id}}">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse" data-target="#collapse{{challenge.id}}" aria-expanded="true" aria-controls="collapse{{challenge.id}}">
{{ challenge.question_text }} - {{ challenge.point_value }} points
</button>
</h2>
</div>
<div id="collapse{{challenge.id}}" class="collapse in" aria-labelledby="heading{{challenge.id}}" data-parent="#accordion{{category.title}}">
<div class="card-body">
<p>{{ challenge.description }}</p>
<form action="{% url 'challenges:answer' challenge.id %}" method="post">
{% if challenge|is_answered:request %}
<label for="answered">Answer</label>
<input type="text" name="answered" id="answered" value="{{ challenge.answer_text }}" readonly>
{% else %}
{% csrf_token %}
<label for="answer">Answer</label>
<input type="text" name="answer" id="answer">
<input type="submit" value="Submit">
{% endif %}
</form>
</div>
</div>
{% endfor %}
</div>
Here is the view:
def index(request):
context = {'challenges_by_category_list': Category.objects.all()}
return render(request, 'challenges/index.html', context)
def detail(request, challenge_id):
challenge = get_object_or_404(Challenge, pk=challenge_id)
return render(request, 'challenges/detail.html', {'challenge': challenge})
def results(request, challenge_id, result):
challenge = get_object_or_404(Challenge, pk=challenge_id)
return render(request, 'challenges/results.html', {'challenge':challenge, 'result':result})
def answer(request, challenge_id):
challenge = get_object_or_404(Challenge, pk=challenge_id)
result = "Incorrect, try again!"
if challenge.answer_text.lower() == request.POST['answer'].lower():
current_user = request.user
session = User_Challenge(user=current_user, challenge=challenge, answered=True)
session.save()
points = Profile(user=current_user, points=challenge.point_value)
points.save()
result = "Correct!"
return HttpResponseRedirect(reverse('challenges:results', args=(challenge.id, result)))
You can try this:
Add the below script in your template:
<script src="https://code.jquery.com/jquery-3.1.0.min.js"></script>
write a script and a function inside it to submit the form data.
<script type="text/javascript">
function submitData( challenge_id ){
// Get answer from the input element
var answer = document.getElementById("answer").value;
// add the url over here where you want to submit form & challenge_id is also taken as a parameter.
var url = "<your_url>";
$.ajax({
url: url,
data: {
'answer': answer,
},
dataType: 'JSON',
success: function(data){
// show an alert message when form is submitted and it gets a response from the view where result is provided and if url is provided then redirect the user to that url.
alert(data.result);
if (data.url){
window.open(data.url, '_self');
}
}
});
}
</script>
Change type of the submit button and add an onclick event to call the submitData() function and pass the challenge id to it. And remove the action attr from the form.
see below:
<form method="post">
{% csrf_token %}
{% if challenge|is_answered:request %}
<label for="answered">Answer</label>
<input type="text" name="answered" id="answered" value="{{ challenge.answer_text }}" readonly>
{% else %}
<label for="answer">Answer</label>
<input type="text" name="answer" id="answer">
// over here
<button type="button" onclick="submitData({{ challenge.id }})">
Submit
</button>
{% endif %}
</form>
Return a JsonReponse to the ajax call from the views.
views.py
def answer(request, challenge_id):
answer = request.GET.get('answer', False)
url = False
if challenge.objects.filter(id=challenge_id).exists() and answer:
challenge = Challenge.objects.get(id=challenge_id)
if challenge.answer_text.lower() == answer.lower():
current_user = request.user
session = User_Challenge(user=current_user, challenge=challenge, answered=True)
session.save()
points = Profile(user=current_user, points=challenge.point_value)
points.save()
result = "Correct!"
# specify the url where you want to redirect the user after correct answer
url = ""
else:
result = "Incorrect, try again!"
data = {
'result': result,
'url': url
}
return JsonResponse(data)
I currently have a pricing page on my website that uses the <select> and <option> html elements to let users select a pricing plan.
This is the code for the plan select dropdown:
<select name="plan" class="form-control" id="subscription-plan">
#foreach ($plans as $key => $plan)
<option value="{{ $key }}">{{ $plan }}</option>
#endforeach
</select>
And here is what the form looks like:
I would like to add 2 cards containing the plan info that can be selected by the user and act the same way as the dropdown, except without it being a dropdown. The id="subscription-plan" is important because it sends the corresponding plan the user selects to the Stripe API, which bills the user's credit card for the amount set in Stripe. Does anyone know how I could achieve this?
I would like to do something like this (but without the dropdown styling):
<select name="plan" class="form-control" id="subscription-plan">
#foreach ($plans as $key => $plan)
<option value="{{ $key }}">
<div class="card mb-5 mb-lg-0">
<div class="card-body">
<h5 class="card-title text-muted text-uppercase text-center">Monthly</h5>
<h6 class="card-price text-center">$8.44<span class="period">/month</span></h6>
</div>
</div>
</option>
#endforeach
</select>
UPDATE:
I have tried this:
#foreach ($plans as $key => $plan)
<div class="col-6">
<label>
<input type="radio" name="plan" value="{{ $key }}" class="card-input-element" id="subscription-plan">
<div class="panel panel-default card-input">
<div class="panel-heading">{{ $plan }}</div>
<div class="panel-body">2 Memorials</div>
</div>
</label>
</div>
#endforeach
This code works and gives me the desired effect of having selectable cards, but the Stripe API now only receives the "monthly" plan even if the user submits their credit card details after selecting the "yearly" plan. This is because now both inputs have id="subscription-plan".
Stripe JavaScript:
const stripe = Stripe('{{ env('STRIPE_KEY') }}');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');
const clientSecret = cardButton.dataset.secret;
const plan = document.getElementById('subscription-plan').value;
cardButton.addEventListener('click', async (e) => {
const { setupIntent, error } = await stripe.handleCardSetup(
clientSecret, cardElement, {
payment_method_data: {
billing_details: { name: cardHolderName.value }
}
}
);
if (error) {
// Display "error.message" to the user...
} else {
// The card has been verified successfully...
console.log('handling success', setupIntent.payment_method);
axios.post('subscribe',{
payment_method: setupIntent.payment_method,
plan : plan
}).then((data)=>{
location.replace(data.data.success_url)
});
}
});
UPDATE 2:
I have replaced const plan = document.getElementById('subscription-plan').value; with var plan = document.querySelector('input[name="plan"]:checked').value;. Now I get Uncaught TypeError: Cannot read property 'value' of null in console when I try to submit.
This should be doable by just iterating over the plans to create the cards and not using select at all. You could address the card selection with onclick handlers or by treating the cards as radio buttons sort of like you see here: https://codepen.io/stefanzweifel/pen/RNvGwz
Then instead of using the select value, you'd use the radio value.
you can simply use size attribute to select
<select name="plan" class="form-control" size=3 id="subscription-plan">
<option value="1">
<div class="card mb-5 mb-lg-0">
<div class="card-body">
<h5 class="card-title text-muted text-uppercase text-center">Monthly</h5>
<h6 class="card-price text-center">$8.44<span class="period">/month</span></h6>
</div>
</div>
</option>
<option value="2">
<div class="card mb-5 mb-lg-0">
<div class="card-body">
<h5 class="card-title text-muted text-uppercase text-center">Daily</h5>
<h6 class="card-price text-center">$0.44<span class="period">/day</span></h6>
</div>
</div>
</option>
<option value="3">
<div class="card mb-5 mb-lg-0">
<div class="card-body">
<h5 class="card-title text-muted text-uppercase text-center">Yearly</h5>
<h6 class="card-price text-center">$80.44<span class="period">/year</span></h6>
</div>
</div>
</option>
</select>
I solved the JavaScript side of this issue on my own. What I did was change var plan = document.querySelector('input[name="plan"]:checked').value; into just var plan;. Then, I added:
$('input:radio').on('click', function(e) {
plan = this.value;
});.
Now, the form properly submits the correct plan based on the option the user selects to the Stripe API. This was a surprisingly difficult task for me to get this form to work without the use of the <select> and <option> elements. Here is the finished working code:
HTML
<div class="col-6">
<label>
<input type="radio" name="plan" value="monthly" class="card-input-element" checked="checked">
<div class="panel panel-default card-input">
<div class="panel-heading">Monthly</div>
</div>
</label>
</div>
<div class="col-6">
<label>
<input type="radio" name="plan" value="yearly" class="card-input-element">
<div class="panel panel-default card-input">
<div class="panel-heading">Yearly</div>
</div>
</label>
</div>
JavaScript
<script src="{{ asset('js/jquery-3.4.1.min.js') }}"></script>
<script>
window.addEventListener('load', function() {
const stripe = Stripe('{{ env('STRIPE_KEY') }}');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');
const clientSecret = cardButton.dataset.secret;
var plan;
$('input:radio').on('click', function(e) {
plan = this.value;
});
cardButton.addEventListener('click', async (e) => {
const { setupIntent, error } = await stripe.handleCardSetup(
clientSecret, cardElement, {
payment_method_data: {
billing_details: { name: cardHolderName.value }
}
}
);
if (error) {
// Display "error.message" to the user...
} else {
// The card has been verified successfully...
console.log('handling success', setupIntent.payment_method);
axios.post('subscribe',{
payment_method: setupIntent.payment_method,
plan : plan
}).then((data)=>{
location.replace(data.data.success_url)
});
}
});
});
</script>
More experienced programmers may have known how to achive this from the start, but I guess we all have to learn somehow. Thank you all for your help.
I have a form, which I would like to pre-populate depending on selected item in a drop down list. The HTML code:
My Ajax.js looks like:
$("#domain_selected").on('change', function() {
$.ajax({
url: '/returndomaindetails',
data: {
domainselect: $("#domain_selected").val()
},
success: function(data, status, xhr) { // success callback function
if (data.length > 1) {
var js = JSON.parse(data)
var domainname = js['domainname']
var domainarchitect = js['domainarchitect'];
$('#domainname').empty().append(domainname)
$('#domainarchitect').empty().append(domainarchitect)
$("#domain_details").show()
} else {
$("#domain_details").hide()
}
},
error: function(jqXhr, textStatus, errorMessage) { // error callback
}
});
})
HTML
<div class="card">
<div class="card-body">
<h4 class="card-title">Change Domain</h4>
<form action="{{ url_for('changedomain') }}" method="post">
<div class="form-row">
<div class="form-group col-md-4">
<label for="domain_selected" class="col-form-label">Select domain</label>
<select id="domain_selected" name="domain_selected" class="form-control">
<option>Choose</option>
{% for x in domain %}
<option value="{{x}}">{{ x }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-row" name="domain_details" id="domain_details" style="display: none">
<div id="domain_name" class="form-group col-md-4">
<label class="col-form-label">Domain Name</label>
<input type="text" class="form-control" name="domainname" id="domainname" placeholder="">
</div>
<div class="form-group col-md-4">
<label class="col-form-label">Domain architect</label>
<input type="text" class="form-control" name="domainarchitect" id="domainarchitect" placeholder="">
</div>
</div>
<div>
<button type="submit" class="btn btn-primary">Change Domain</button>
</div>
{% block scripts %}
<script src="{{ url_for('static', filename='js/ajax.js') }}"></script>
{% endblock %}
Now, on the flask endpoint /returndomaindata , I have just a simple print statement to get indication that the GET request arrived, however it doesn't.
#app.route('/returndomaindetails', methods=["GET"])
def returndomaindetails():
if request.method=="GET":
print("got the GET request")
domainselect=request.args.get('domainselect')
data=Domains.returndomaindetails(domainselect)
print(data)
return json.dumps(data)
Seems like the "on change" event is not firing. There are absolutely no errors in the console. Similar functions work without problem in the same file.
It turned out to be indentation problem - a space before the function
I am trying to pass an array to a modal after I get a response from an ajax call on the eventClick() function in fullcalendar v3
// in my blade javascript section
This is getting good data but I need to send the tagcodes to the modal and an array like this..
$tagCodes
which should be an array like this..
'AM','FLEX'
A. I don't know how to make it a usable array.
B. I need to send it to the front-end as a variable called $tagCodes which is an array.
eventClick: function (event, jsEvent, view) {
let id = event.id;
let tag_codes = event.tag_codes;
console.log(tag_codes.split(" ").join(",").replace(/,\s*$/, ""));
( which is giving me this AM,FLEX )
$.ajax({
url: '/agenda/' + id,
type: 'GET',
data: {},
dataType: 'json',
success: function (data) {
$('#calendarModal input[name=id]').val(data.calendar.id);
$("#calendarModal .tagCodes").html(tag_codes.split(" ").join(",").replace(/,\s*$/, ""));
$('#calendarModal').modal();
}
});
}
// in my modal section
<div class="col-12">
<div class="row">
<div class="col-12 tagCodes"></div>
#foreach($tags as $tag)
<div class="col-3 text-center">
<label>
<input type="checkbox"
id="tag-{{ $tag->id }}" name="tags[]" value="{{ $tag->id }}"
#if( in_array($tag->code, [THIS IS WHERE IM TRYING TO SEND THE ARRAY]) ) checked #endif
> {{ $tag->code }}
</label>
</div>
#endforeach
</div>
</div>
Change this
<input type="checkbox"
id="tag-{{ $tag->id }}" name="tags[]" value="{{ $tag->id }}"
#if( in_array($tag->code, [THIS IS WHERE IM TRYING TO SEND THE ARRAY]) ) checked #endif
>
to
<input type="checkbox" id="tag-{{ $tag->code }}" name="tags[]" value="{{ $tag->id }}">
then in JavaScript do this.
var codes = tag_codes.split(" ");
for(var i=0;i<codes.length;i++)
$('#tag-'+codes[i]).prop('checked', true);
I have created a view that generates posts and each post have a comments_set which produces all the comments made by user. When new comment is posted, below function is executed.
$("#comment-post-button").click(function(){
var event_id = document.getElementById('event-id').value;
var url= '/post-comments/'+event_id +'/';
var data = {csrfmiddlewaretoken: document.getElementsByName('csrfmiddlewaretoken')[0].value,
content:document.getElementsByName('comment-post-content')[0].value
}
$.post(url , data, function(){
$("#refresh-comments").load("/comments/" + event_id + '/', function(){
$("#comment-post-content").val("");
});
});
$("#comment-post-content").val("");
return false;
});
The problem is that the page contains multiple posts and each comment submission button has the same id "comment-post-button". So the above function works only for the top post and not for the rest. I can see what the problem is but don't know how to solve this. Please help.
Here is the html markup:
{% for event in events %}
<div class="post">
<div class="post-right">
<div class="post-author">{{ event.author.first_name }}</div>
<div class="post-content">{{ event.description }}</div>
<div class="post-bar">
<div class="post-likes">{{ event.up_votes }}<img src="/site-media/static/images/like.png" /></div>
<div class="post-dislikes">{{ event.down_votes }}<img src="/site-media/static/images/dislike.png" /></div>
<div class="post-timestamp">{{ event.pub_date }}</div>
Comment
</div>
<div class="post-comment">
<form method="post" action="/post-comments/{{ event.id }}/">
{% csrf_token %}
<input type="text" id="comment-post-content" name="comment-post-content" maxlength="200" placeholder="Add a comment..." />
<input type="hidden" id="event-id" value="{{ event.id }}">
<input type="submit" id="comment-post-button" class="comment-post-button" value="Post comment" />
</form>
</div>
<div id="refresh-comments" class="comments">
{% include "comments.html" %}
</div>
</div>
<div class="post-left">
<img src="../FestJanta/site-media/static/images/post.jpg" />
</div>
</div>
{% endfor %}
comments.html:
{% for comment in event.comment_set.all|slice:"3" %}
<div class="comments-right">
{{ comment.author.first_name }}
{{ comment.content }}<br>
<div class="comment-timestamp">{{ comment.pub_date }}</div>
</div>
<div class="comments-left"><img src="../FestJanta/site-media/static/images/comment.jpg" /></div>
{% endfor %}
Final working solution:
$(".comment-post-button").click(function(){
var btn = $(this);
var currentPost = btn.parents('.post');
var event_id = currentPost.find('.event-id').val();
var url= '/post-comments/'+event_id +'/';
var data = {csrfmiddlewaretoken: document.getElementsByName('csrfmiddlewaretoken')[0].value,
content:currentPost.find('input[name="comment-post-content"]').val()
}
$.post(url , data, function(){
$(currentPost.find('.refresh-comments')).load("/comments/" + event_id + '/', function(){
$(currentPost.find('.comment-post-content')).val("");
});
});
return false;
});
Remove id and add class:
<input type="hidden" class="event-id" value="{{ event.id }}">
Do something like this:
$('.comment-post-button').click(function(){
var $btn = $(this);
var $currentPost = $btn.parents('.post');
var event_id = $currentPost.find('.event-id').val();
//...
});
Find each element in $currentPost scope:
Instead of this:
content: document.getElementsByName('comment-post-content')[0].value
Do this:
content: $currentPost.find('input[name="comment-post-content"]').val()
You could do the following:
Identify all post buttons, e.g. by a class like .comment-button
Use the .on() notation of jQuery
Pass the event and use its target property to identify the DOM element that triggered the event
Use traversion to get the correct DOM element of the post
The result should look something like this (untested):
Markup (I basically just got rid of the IDs; not sure how/if django handles this automatically):
{% for event in events %}
<div class="post">
<div class="post-right">
<div class="post-author">{{ event.author.first_name }}</div>
<div class="post-content">{{ event.description }}</div>
<div class="post-bar">
<div class="post-likes">{{ event.up_votes }}<img src="/site-media/static/images/like.png" /></div>
<div class="post-dislikes">{{ event.down_votes }}<img src="/site-media/static/images/dislike.png" /></div>
<div class="post-timestamp">{{ event.pub_date }}</div>
Comment
</div>
<div class="post-comment">
<form method="post" action="/post-comments/{{ event.id }}/">
{% csrf_token %}
<input type="text" name="comment-post-content" maxlength="200" placeholder="Add a comment..." />
<input type="hidden" name="event-id" value="{{ event.id }}">
<input type="submit" class="comment-post-button" value="Post comment" />
</form>
</div>
<div class="comments">
{% include "comments.html" %}
</div>
</div>
<div class="post-left">
<img src="../FestJanta/site-media/static/images/post.jpg" />
</div>
</div>
{% endfor %}
Javascript:
$("body") // Could be any ancestor, body is not the best option
.on('click', '.comment-post-button' function(ev){
var clickTarget = ev.target, // The element clicked on
postElement = $(clickTarget).closest('.post'), // the div enclosing a post
commentSection = $(postElement).find(".comments"), // the div enclosing the comments
commentInputField = $(clickTarget).siblings("[name='comment-post-content']"),
event_id = $(clickTarget).siblings("[name='event-id']").val(),
url= '/post-comments/'+event_id +'/';
// Not sure what this token is, so I will not change that part
var data = {csrfmiddlewaretoken: document.getElementsByName('csrfmiddlewaretoken')[0].value,
content: commentInputField.val()
}
$.post(url , data, function(){
$(commentSection).load("/comments/" + event_id + '/', function(){
$(commentInputField ).val("").prop('disabled', false); // In the callback, empty and reenable
});
});
$(commentInputField ).prop('disabled', true); // I guess disabling the field makes sense
return false;
});
An additional advantage is that you will end up with only one click handler. Note that the solution could be optimized (e.g. by improving the selectors). In addition, jslint will give some warnings.
to give each post & post_Comment_button a unique id, as suggested by someone, change the markup as follows:
{% for event in events %}
<div class="post" id="post_{{forloop.counter}}">
[...]
<input type="submit" id="comment-post-button_{{forloop.counter}}" class="comment-post-button" value="Post comment" />
then change the js function as follows:
$("#comment-post-button").click(function(event){
var buttonNr = event.target.id.replace('comment-post-button_', '');
var postId = 'post_' + buttonNr;
$("#" + postId)... --> and do whatever with it..