I'm trying to add a custom button in each row next to a user to the django admin page which makes it easier for the admin to click the button and generate a password reset link for a particular user rather than navigate to the hidden actions section of the django admin page.
Something like this - https://hakibenita.com/how-to-add-custom-action-buttons-to-django-admin
Following is the code I've implemented and it works and navigates to a new page with the correct reset-password request and the reset_password() method executes with a link being sent to the user.
However, when I click the button, I would like to send an AJAX GET request to the same url as above and just a show an alert to the admin on request completion instead of navigating to a new page. Currently I'm unable to run the javascript code using the format_html method in Django (see code below)
class UserAdmin(Admin.modelAdmin):
list_display = ('name', 'email', 'custom_actions')
form = forms.UserAdminForm
def get_urls(self):
urls = super().get_urls()
custom_urls = [
url(
r'reset-password/(?P<user_id>.+)',
self.admin_site.admin_view(self.reset-password),
name='reset-password',
),
]
return custom_urls + urls
def custom_actions(self, obj):
user = user_model.User.get(user_id=obj.id)
password_reset_url = reverse('admin:reset-password', args=[user.id])
return mark_safe(format_html(
f'<a class="button" onclick="parent.location=\'{password_reset_url}\'" >Reset Password</a> '
custom_actions.short_description = 'Custom Actions'
custom_actions.allow_tags = True
def reset_password(self, request, password_reset_id):
password_reset.send_password_reset_link(password_reset_id=password_reset_id)
return HttpResponse(status=200)
Below is the HTML/JS code that I was testing for the behavior I want on the page and works, I was hoping to the stitch the same into the above Django code but Django cannot execute and just shows the script as a string on the admin page.
<button id='jax' class="button">AJAX</button>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
$(document).ready(function() {
$("#jax").click(function(){
$.ajax({
type : "GET",
url : "my-dynamic-url",
timeout:10,
jsonp : "jsonp",
success : function (response, textS, xhr) {
console.log('oof')
alert("Password reset link has been resent");
},
error : function (xmlHttpRequest, textStatus, errorThrown) {
if(textStatus==='timeout')
alert("request timed out");
}
});
});
});
</script>
Is there a correct way to integrate the above javascript code on admin page on the click of the button?
Are you missing out on commas? From an example according to the documentation:
format_html("{} <b>{}</b> {}",
mark_safe(some_html),
some_text,
some_other_text,
)
Maybe you should wrap {password_reset_url} in commas and see if that works.
If this does not solve your problem, elaborating on this will be helpful:
Django cannot execute and just shows the script as a string on the admin page.
Which script are you referring to?
The solution:
Python:
class UserAdmin(Admin.modelAdmin):
list_display = ('name', 'email', 'custom_actions')
form = forms.UserAdminForm
def get_urls(self):
urls = super().get_urls()
custom_urls = [
url(
r'reset-password/(?P<user_id>.+)',
self.admin_site.admin_view(self.reset-password),
name='reset-password',
),
]
return custom_urls + urls
def custom_actions(self, obj):
user = user_model.User.get(user_id=obj.id)
password_reset_url = reverse('admin:reset-password', args=[user.id])
return mark_safe(format_html(
f'<a class="button" onclick="send_password_link(\'{password_reset_url}\'") >Reset Password</a> '
custom_actions.short_description = 'Custom Actions'
custom_actions.allow_tags = True
def reset_password(self, request, password_reset_id):
password_reset.send_password_reset_link(password_reset_id=password_reset_id)
return HttpResponse(status=200)
HTML:
This should be under base-app/templates/your-app/change_list.html
{% extends "admin/change_list.html" %}
{% load static %}
{% block content %}
<script src="{% static 'js/passwordReset.js' %}"></script>
<!-- Render the rest of the ChangeList view by calling block.super -->
{{ block.super }}
{% endblock %}
Javascript:
This should be under base-app/static/static/js/passwordReset.js
function send_password_link(url) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
alert('Password Reset Sent');
}
};
xhttp.open("GET", url, true);
xhttp.send();
}
Related
I'm trying to edit an existing form on my site, and save the edits using Javascript (without requiring a refresh of the page). I'm using Django as well.
So far, when the user clicks 'edit' on the page, the form appropriately appears, showing the information already saved there. But when I click 'save' I get a 404 error.
The issue is in the Javascript function edit_post. I'm not sure if I have used stringify correctly either, I'm new to using Javascript with Django. Any help is appreciated.
function edit_handeler(element) {
id = element.getAttribute("data-id");
document.querySelector(`#post-edit-${id}`).style.display = "block";
document.querySelector(`#post-content-${id}`).style.display = "none";
// everything above this works and opens up the form for editing
edit_btn = document.querySelector(`#edit-btn-${id}`);
edit_btn.textContent = "Save";
edit_btn.setAttribute("class", "text-success edit");
if (edit_btn.textContent == "Save") {
edit_post(id, document.querySelector(`#post-edit-${id}`).value); //here
edit_btn.textContent = "Edit";
edit_btn.setAttribute("class", "text-primary edit");
}}
function edit_post(id, post) {
const body = document.querySelector(`#post-content-${id}`).value;
fetch("/edit_post/", {
method: "POST",
body: JSON.stringify({
body:body
})
}).then((res) => {
document.querySelector(`#post-content-${id}`).textContent = post;
document.querySelector(`#post-content-${id}`).style.display = "block";
document.querySelector(`#post-edit-${id}`).style.display = "none";
document.querySelector(`#post-edit-${id}`).value = post.trim();
});
}
Relevant html - this is inside a card, for the post itself in the html file:
<span id="post-content-{{i.id}}" class="post">{{i.text}}</span> <br>
<textarea data-id="{{i.id}}" id="post-edit-{{i.id}}"
style="display:none;" class="form-control textarea" row="3">{{i.text}}</textarea>
<button class="btn-btn primary" data-id="{{i.id}}" id="edit-btn-{{i.id}}"
onclick="edit_handeler(this)" >Edit</button> <br><br>
views.py
def edit_post(request, pk):
post = Post.objects.get(id=pk)
form = PostForm(instance=post)
if request.method == "POST":
form = PostForm(request.POST, instance=post)
if form.is_valid():
form.save()
return JsonResponse({}, status=201) # this works to edit and save to db
else:
if request.method == "GET":
form = PostForm(instance=post)
form_for_post = {'form': PostForm()}
return render(request, "network/make_post.html", {
"post": post,
"form_for_post": form,
})
urls.py (relevant ones)
path('edit_post/<str:pk>/', views.edit_post, name="edit_post"),
path('edit_post/', views.edit_post),
path("profile/<str:username>", views.profile, name="profile"),
Assuming you're running your django server locally and you're getting a 404 returned from your fetch request, then that means the url path does not exist. Either your django server isn't live or the url you're supplying the fetch request with is incorrect. If /edit_post/ is your desired endpoint, try fetching with the servers full request URL
Something like this
http://localhost:8000/edit_post
Replace 8000 with whatever port your server is running at.
Your fetch body is structured properly btw.
Goal: load more rows from the database to a view using an ajax request when a user clicks the "load more" button. I would like the data to load without a page reload.
Problem: The data being loaded via ajax keeps repeating the same rows on every request and doesn't paginate as per standard request.
Detail: I have a view that loads 4 rows from the database which I paginate using Laravel's built-in pagination. I've added an event listener on a "load more" button which successfully sends the request to the controller, which in turn successfully returns data. The controller returns a partial view of the data I want to display. However this data doesn't seem to increment properly and keeps repeating the records shown on each request. I am not sure what I am missing here, if the problem is in the controller or in the JS?
I am not very experienced with Laravel, PHP and JS since coming from more of a web designer and UI design background and would love to really understand what I am doing wrong here.
PLEASE NO JQUERY EXAMPLES.
Partial view:
#foreach ($products as $product)
<div style="background-color:pink; width: 200px;">
<p>{{ $product->title }}</p>
<img src="/images/product/{{ $product->img }}" alt="{{ $product->title }}" style="width: 50px;">
</div>
#endforeach
Javascript:
(I am updating the button href attribute so the request URL reflects the correct query)
const container = document.querySelector('#sandbox-container');
let button = document.getElementById('load-stuff');
let url = button.getAttribute('href'); // http://127.0.0.1:8000/sandbox?page=2
let pageNum = button.getAttribute('href').substr(35,1);
button.addEventListener('click', (event) => {
event.preventDefault();
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
// if page loads successfully, replace the number at the end of the url with the incremented page number
pageNum++;
newUrl = url.replace(/page=([^d]*)/, `page=${pageNum}`);
button.setAttribute('href', newUrl);
xhr.onload = function() {
if (xhr.status === 200) {
container.insertAdjacentHTML('beforeend', xhr.responseText);
}
else {
console.log(`Request failed, this is the response: ${xhr.responseText}`);
}
};
xhr.send();
})
Controller:
public function sandbox(Request $request)
{
$products = Product::orderBy('title', 'asc')->paginate(4);
if($request->expectsJson()){
return view('sandbox-more', compact('products'));
} else {
return view('sandbox', compact('products'));
}
}
Consider this snippet for your javascript
const container = document.querySelector('#sandbox-container');
let button = document.getElementById('load-stuff');
button.addEventListener('click', (event) => {
event.preventDefault();
const xhr = new XMLHttpRequest();
let url = button.getAttribute('href');
let pageNum = button.getAttribute('data-page-number') || 0;
xhr.open('GET', url, true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
// if page loads successfully, replace the number at the end of the url with the incremented page number
pageNum++;
newUrl = url + '?page=' + pageNum;
xhr.onload = function() {
if (xhr.status === 200) {
container.insertAdjacentHTML('beforeend', xhr.responseText);
button.setAttribute('data-page-number', pageNum);
}
else {
console.log(`Request failed, this is the response: ${xhr.responseText}`);
}
};
xhr.send();
})
What I've done here is to have the page number saved to a dedicated custom attribute "data-page-number". Doing "button.getAttribute('href').substr(35,1)" is inefficient. And then check the page number and increment it on the button's click event. Also, only update the "data-page-number" attribute when the request has been successful. I hope this helps
You should regenerate the pagination every time you make a request to get the correct data. Here is a very good example on doing it via jQuery. Should just adjust it to your needs since you are using pure Javascript.
This is my index.html file
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script type="text/javascript">
// setup some JSON to use
var cars = [
{ "make":"Porsche", "model":"911S" },
{ "make":"Mercedes-Benz", "model":"220SE" },
{ "make":"Jaguar","model": "Mark VII" }
];
window.onload = function() {
// setup the button click
document.getElementById("theButton").onclick = function() {
doWork()
};
}
function doWork() {
// ajax the JSON to the server
$.post("result", JSON.stringify(cars), function(){
});
// stop link reloading the page
event.preventDefault();
}
</script>
This will send data using AJAX to Python:<br /><br />
<form action = "/result" method = "POST">
<button id = "theButton" class ="button">
<span>
<i >Click</i>
</span>
</button>
<form>
This is my json_io.py file to run Flask:
#!flask/bin/python
import sys
from flask import Flask, render_template, request, redirect, Response
import random, json
app = Flask(__name__)
#app.route('/')
def output():
# serve index template
return render_template('index.html')
#app.route('/result', methods = ['POST', "GET"])
def worker():
# read json + reply
data = request.get_json(force = True)
print(data)
return render_template('result.html', result = data[0]["make"])
if __name__ == '__main__':
# run!
HOST = '127.0.0.1'
PORT = 4100
app.run(HOST, PORT, debug = True)
After running the command line and click on click button. I got what I want in the Chrome console.
In order to get into http://127.0.0.1:4100/result , I will comment event.preventDefault(); in index.html. However, when I rerun again, it shows me Bad Request Failed to decode JSON object: Expecting value: line 1 column 1 (char 0)
Are there any ideas on how I can fix this ?
In the index.html file, make a placeholder that will be filled out by the js code handling your AJAX request:
<span id='ajax_result'>placeholder</span>
Then, in the python code, you don't really need to go through the template and can return a string straight away:
#app.route('/result', methods = ['POST', "GET"])
def worker():
data = request.get_json(force = True)
return data[0]['make']
Then, in js, grab the result and put it in the placeholder span:
function doWork() {
$.post("result", JSON.stringify(cars), function(reply){
$('#ajax_result').text(reply);
});
event.preventDefault();
}
Click the button and enjoy your Porsche!
I'm improving a system for controlling a telescope remotely. A Raspberry Pi runs flask, and provides a video stream for a camera attached to the telescope. The telescope's focuser is actuated by a stepper motor controlled with an Arduino.The server provides a website that shows the video stream, and offers two buttons to move the focuser in and out.
When either button is clicked, the client sends a POST to the RasPi, and then the RasPi tells the Arduino to move the focuser. But crucially I did not want the page to refresh while refocusing. Hence, I used jQuery and Ajax to suppress the page refresh.
The relevant code snippets are here:
Python/Flask code:
#app.route('/stream/<wcam>', methods=['GET'])
def stream_get(wcam):
class FocuserForm(FlaskForm):
nsteps = IntegerField('# steps: ', default=1)
focuser_in = SubmitField('Focuser in')
focuser_out = SubmitField('Focuser out')
form = FocuserForm()
return render_template('stream.html', wcam=wcam, form=form)
#app.route('/stream/<wcam>', methods=['POST'])
def stream_post(wcam):
results = request.form
arduino_serial = SerialFocuser()
if results['caller'] == "focuser_in":
command = "MVD" + results['steps'] + "\n"
arduino_serial.send_command(command)
elif results['caller'] == "focuser_out":
command = "MVU" + results['steps'] + "\n"
arduino_serial.send_command(command)
return ''
Web (stream.html):
<html>
<head>
<title>Video Streaming</title>
<style>
...
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script>
$(document).ready(function() {});
</script>
</head>
<body>
<h1>Streaming camera {{ wcam }}</h1>
<br>
<img id="bg" src="{{ url_for('video_feed', wcam=wcam) }}", height="480" width="640">
Back
<br>
<!--######################################################-->
<!--# Focuser handling -->
<!--######################################################-->
<br>
<form id="flaskform" method="POST">
<p>
{{ form.nsteps.label }} {{ form.nsteps() }}
{{ form.focuser_in() }}
{{ form.focuser_out() }}
</p>
</form>
<script>
// $(document).ready(function() { // Moved to header
var form = document.getElementById('flaskform');
function onSubmit(event) {
console.log('onSubmit function');
var objectID = event.explicitOriginalTarget.id;
var nsteps = form.nsteps.value;
var return_data = {caller: "", steps: nsteps};
if (objectID == "focuser_in") {
return_data.caller = objectID;
console.log("Focuser_in detected");
} else if (objectID == "focuser_out") {
return_data.caller = objectID;
console.log("Focuser_out detected");
} else if (objectID == "nsteps") {
console.log("nsteps detected");
event.preventDefault();
return;
} else {
console.log("No matches");
return;
}
console.log("About to run Ajax");
$.ajax({
url: "stream.html",
type: "post",
data: return_data,
success: function(response) {
console.log('It worked!');
},
error: function(xhr, status, text) {
console.log('An error occurred:', status,"; ", text);
},
timeout: 1000 // 1s
}); // Ajax
console.log("After running Ajax");
if (event) { event.preventDefault(); }
}
// prevent when a submit button is clicked
form.addEventListener('submit', onSubmit, false);
//<!--form.addEventListener('submit', onSubmit, false);-->
// prevent submit() calls by overwriting the method
form.submit = onSubmit;
//}); // Moved to header
</script>
</body>
</html>
The problem is as follows:
If I refresh the page on the client's browser and then click a button, ajax does POST, but flask does not seem to receive it. The request times out.
If I now restart the server (I'm developing this with PyCharm, so I just click re-run) without refreshing the page in the client, and then click a button, flask does get the POST, and the focuser works like a charm.
If I refresh the page again, then the buttons stop working until I reset the server.
Why does this happen? Obviously the code works in its main purpose, but somehow the page refresh is breaking something.
I had a similar issue once with a camera thread blocking all calls. When you reset the server, does your camera feed still run (before clicking the button)?
Because basically you are calling your camera feed twice - first with the get call when you refresh your page, then again with the post call.
I'd advice you to refactor your submitted code into an alternative function for clarity:
#app.route('/stream/<wcam>', methods=['POST'])
def moveCommand:
if form.is_submitted():
# POST method
results = request.form
arduino_serial = SerialFocuser()
if results['caller'] == "focuser_in":
command = "MVD" + results['steps'] + "\n"
arduino_serial.send_command(command)
elif results['caller'] == "focuser_out":
command = "MVU" + results['steps'] + "\n"
arduino_serial.send_command(command)
So basically you keep your get method for only the streaming and use the post for the moving around.
Thanks to #Peter van der Wal for pointing me towards the solution.
The video streamer has a while True loop, which continually takes frames from the camera, hence locking the thread.
The solution was to start the app with the threaded option on:
Before:
app.run(host='0.0.0.0', debug=True)
Now:
app.run(host='0.0.0.0', debug=True, threaded=True)
This allows the video streaming thread to continue on its own, while allowing the server to process other commands.
In this step I'm trying to avoid making a form to send data over to django from the frontend.
Ideally I would like to pass a simple string to the queryset with an onclick button, so I can make the tag filters on my Post model and show the ones with the tags from the string input. (just send the string to the queryset, I can deal with the split() and strip() to make it valid)
The simplified html would be something like
<input type="text" name="tag_filter">
<button type="button" onclick="Tag_Filter()">Filter</button>
<script>
function Tag_Filter() {
var tags;
tags = document.getElementById("tag_filter");
}
</script>
And the ListView
class PostListView(ListView):
model = Post #.objects.filter(tags__name__in=["black&white", "red"]).distinct()
template_name = 'post/home.html' # <app>/<model>_<viewtype>.html
#queryset = Post.objects.all()
context_object_name = 'posts'
#ordering = ['-date_posted']
def get_queryset(self):
tag_list = #the return from the Tag_Filter() js
return Post.objects.filter(tags__name__in=tag_list).annotate(num_tags=Count('tags')).filter(num_tags__gte=len(tag_list)).order_by('-date_posted').distinct()
urls.py
urlpatterns = [
path('', PostListView.as_view(), name='post-home'),
other_paths()
]
I suggest use ajax, then your first step is implement ajax ...ajax is from Jquery then you need include the jquery on your template only for one file html or if your prefer in your base.html on the end of file before from your body end tag:
<!-- jQuery library -->
<script src="{% static 'Jquery/jquery.min.js' %}"></script>
....
...
all my files js
....
</body>
</html>
for download this jquery.min.js , please go to this link and save the file in to folder: static/Jquery/:
Link for download Jquery min.js
Don't forget save as jquery.min.js
The next is setup your settings.py for accept the static files:
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
'/static/',
]
the next is put the tag for load static files on the top of your file html or base.html as you wish:
{% load staticfiles %}
<!DOCTYPE html>
<html>
...
...
...
<!-- jQuery library -->
<script src="{% static 'Jquery/jquery.min.js' %}"></script>
....
...
all my files js
....
</body>
</html>
ok, now we include $.ajax first you need include on your button a function that recive your value from your input:
<input type="text" name="tag_filter" id ="tag_filter">
<button type="button" onclick="tag_filter()">Filter</button>
now your function tag_filter:
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
//RETURN YOUR TOKEN FROM DJANGO FOR SEND FROM AJAX
return cookieValue;
}//end function getCookie
function tag_filter(){
var tag_filter = $("#tag_filter").val();
if(tag_filter == '' || tag_filter == null){
alert('write yout filter for search !!');
return false;
}
//now we include the ajax call
//token
var csrftoken = getCookie('csrftoken');
$.ajax({
type: "GET",
url: "search_filter",
data:{
csrfmiddlewaretoken : csrftoken,
tag_filter:tag_filter,
},
dataType: "json",
success: function(data) {
code = data.result;
if(code=='ok_select'){
// here write you code for the success answer from ajax call
console.log(data);
}
},
error: function( jqXHR, textStatus, errorThrown ) {
alert('Error ' + jqXHR.responseText);
}//end error
});
}
now you need create a def for search with the parameter example:
from django.http import HttpResponse, JsonResponse
...
..
def SearchFilter(request):
filter = request.GET['tag_filter']
....
....
data ={
'result':'ok_select'
'data': #your query
}
return JsonResponse(data, safe=False)
now you need declare this def on yours urls.py example:
from myapp.views import SearchFilter
urlpatterns = [
url(r'^search_filter', SearchFilter, name='search_filter'),
...
...
]
this is all... on the case of ajax this return success answer by console check this answer on the console from your web browser favorite..
and good luck..!!