How do I add a custom button next to a field in Django admin? - django

I have a Client model, which includes a field for a client API key.
When adding a new client in Django Admin, I'd like to have a button next to the API field to generate a new key (i have the method for this). The field will then be updated with the key once generated.
How can I add this button next to the field? Should I use a custom widget?

In my case I am making an API call with a button I create so I'll throw in how I did that too. Ofcourse your button can do whatever you like.
First, in your model create a function that will output your button. I will use my example, i.e. models.py:
class YourModel(models.Model):
....
def admin_unit_details(self): # Button for admin to get to API
return format_html(u'<a href="#" onclick="return false;" class="button" '
u'id="id_admin_unit_selected">Unit Details</a>')
admin_unit_details.allow_tags = True
admin_unit_details.short_description = "Unit Details"
I then added the field as readonly and added it to the fieldsets, note you can only have either fields or fieldsets defined on the model admin. I aslo added media to overwrite some css and also added the js for where the ajax call will be made, admin.py:
class YourModelAdmin(admin.ModelAdmin):
form = YourModelForm
list_display = ('id', 'agent', 'project', 'completed_date', 'selected_unit', 'is_accepted',
'get_lock_for_admin', 'status')
fields = ('agent', 'project', 'completed_date', 'selected_unit', 'is_accepted',
'lock', 'status')
readonly_fields = ('admin_unit_details', )
...
class Media:
js = ('admin_custom/js/myjs.js',) # in static
css = {'all': ('admin_custom/css/mycss.css', )}
I also wanted to note that I passed the API address and header through the Form, but you can use the right header/password in the code. I just keep mine all in one place (settings.py), forms.py (optional):
from settings import API_ADDRESS, API_HEADER
class MyModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(WorksheetForm, self).__init__(*args, **kwargs)
self.fields['selected_unit'].widget = forms.Select(choices=get_worksheet_unit_choice_list(self.instance.id),
attrs={'api_address': API_ADDRESS, 'api_header': API_HEADER})
....
Lastly here is a look at my js, as referenced by my admin Media class, it is in admin_custom/js/myjs.js:
This is similar to adding an admin image, see here. Also search for allow_tags attribute in this django doc, it shows a good example.
// Make sure jQuery (django admin) is available, use admin jQuery instance
if (typeof jQuery === 'undefined') {
var jQuery = django.jQuery;
}
var unit_information = {};
jQuery( document ).ready(function() {
jQuery('#id_admin_unit_selected').click( function() {
//get the data from id_selected_unit, from the api_header api_address attributes
var unit_select = jQuery('#id_selected_unit');
var header = unit_select.attr('api_header');
var address = unit_select.attr('api_address');
var selected_unit = unit_select.val();
if (header && address && selected_unit){
var unit_address = address + '/units/' + selected_unit
get_unit(header, unit_address)
}
else{
// if can't connect to api, so hide
jQuery('.field-admin_unit_details').hide();
}
});
});
function get_unit(header, address){
jQuery.ajax
({
type: "GET",
url: address,
dataType: 'json',
headers: {
"Authorization": header
},
success: function (result) {
//TODO: output this in a modal & style
unit_information = JSON.stringify(result);
alert(unit_information)
},
error: function(xhr, textStatus, errorThrown) {
alert("Please report this error: "+errorThrown+xhr.status+xhr.responseText);
}
});
}
This outputs it in an alert, you can also log it to the console or define your own modal / style for it.
Hope this helps, Cheers!

Related

Save Django Model when list_editable fields are changed in Django Admin change_list view

I have a Django Model which I'm trying to update from the Admin change_list view when a field that is listed in list_editable has a change event fired. Right now the only way to save the updates is by clicking a "Save" button and then the page reloads. I don't want the page to reload at all. I just want it to update the model asynchronously.
For example I have the following model.
class Example(models.Model):
name = models.CharField()
hide = models.BooleanField(default=False)
In the Admin Form I have
class ExampleAdmin(admin.ModelAdmin):
list_display = [
"name",
"hide"
]
list_editable = [
"name",
"hide"
]
When viewing the change list for Examples now I will see the values listed out with an editable text input for name and a checkbox for hide.
I would like to listen for events on those inputs and when they are fired send an async request to update the specific model.
UPDATE
This is what I have right now.
window.addEventListener("load", function() {
(function($) {
$(".field-name .vTextField").on("change",function (event) {
$('#changelist-form').submit()
})
$('#changelist-form').submit(function(event) { // On form submit event
$.ajax({ // create an AJAX call...
data: $(this).serialize(), // get the form data
type: "POST", // GET or POST
url: $(this).attr('action'), // the file to call
success: function(response) { // on success..
console.log(response)
},
error: function(e, x, r) { // on error..
console.log(e)
}
});
event.preventDefault()
return false;
});
})(django.jQuery);
})
I can see this firing in the network requests and can verify that the serialized data being sent is updated. The response is a 200, but when I refresh the page the data has not been updated.
UPDATE
At this point I'm giving up on sending this data to Django and will be sending the serialized data to a custom endpoint and parse through and save the data in a custom save function.
Within your Admin Model add the following
class ExampleAdmin(admin.ModelAdmin):
class Media:
js = ("js/admin/example.js")
list_display = [
"name",
"hide"
]
list_editable = [
"name",
"hide"
]
example.js
window.addEventListener("load", function() {
(function($) {
$(".field-name .vTextField").on("change",function (event) {
submit()
})
function getFormData($form){
var unindexed_array = $form.serializeArray();
var indexed_array = {};
$.map(unindexed_array, function(n, i){
indexed_array[n['name']] = n['value'];
});
return indexed_array;
}
function submit(){
$.ajax({
data: getFormData($('#changelist-form')),
type: "POST",
url: '/api/save-example',
success: function(response) {
console.log(response)
},
error: function(e, x, r) {
console.log(e)
}
});
}
})(django.jQuery);
})
I'm using Django Rest Framework so I created a POST endpoint, but you could also do this in a Django View that is listening for a POST.
When you execute the post Django will send the data categorized by the row number and field name, like so.
form-0-id : 1
form-0-name: example-name
form-0-hide: false
form-0-example_parent: 100
form-1-id : 2
form-1-name: example-name-two
form-1-hide: true
form-1-example_parent: 200
#api_view(["POST"])
def save_example(request):
try:
total_rows = int(request.POST.get("form-TOTAL_FORMS"))
prefix = "form-"
form_fields = {
"id": "id",
"name": "name",
"hide": "hide",
"example_parent": "example_parent_id",
}
for x in range(total_rows):
item = {}
for key, val in form_fields.items():
post_field = prefix + str(x) + "-" + key
field = val
if key == "id":
id = request.POST.get(post_field)
else:
item[field] = request.POST.get(post_field)
Example.objects.filter(id=id).update(**item)
return Response(status=status.HTTP_200_OK)
except Exception as e:
return Response(status=status.HTTP_404_NOT_FOUND)
Note
total_rows = int(request.POST.get("form-TOTAL_FORMS")) which lets you know how to loop through all the form data that is passed back.
This doesn't take different values into consideration. (i.e. Checkboxes will return an on value but no value if not checked.
You will need to update your save_example function accordingly.

Connecting django signal to a button click

I'm trying to use a Django signal to toggle a model's boolean value to False from True when a button is clicked. I think that the Django sender would be the button click and the receiver would be the function that changes the boolean value. I'm not sure how to write out a button click as a signal sender however.
Here is my model with the Boolean value.
From models.py:
class User(AbstractUser):
is_blue = models.BooleanField(default=False)
Any thoughts on a what a signal would look like that take a button click and changes this value to True?
To change your model field, you need to create a separate function for it in your views, e.g.:
from django.http import JsonResponse
from xxx.models import User
def ajax_change_user(request):
is_blue= request.GET.get('is_blue', False)
# Get your User model
user = User.objects.get(pk=user_id)
try:
is_blue = True
user.save()
return JsonResponse({"success": True})
except Exception as e:
return JsonResponse({"success": False})
return JsonResponse(data)
Then you need to add a url for this view:
path('ajax/change_user', views.ajax_change_user, name='ajax_change_user')
Then in your html, you need to add a tag with the necessary class, e.g.:
{% csrf_token %}
Change
And then finally the javascript with ajax call part:
<script>
$(".change-user").on('click', function () {
var user_id = $(this).data('userid);
var is_blue = False # say you start with the status being false
$.ajax({
url: '/ajax/change_user/',
data: {
'csrfmiddlewaretoken': $('input[name="csrfmiddlewaretoken"]').val(),
'is_blue': is_blue
'user_id': user_id
},
dataType: 'json',
success: function (data) {
if (data.success) {
alert("ajax call was successful.");
is_blue = True # if button is clicked, make 'is_blue' True
}else{
alert("ajax call was not successful.");
}
}
});
});
</script>
This is just an example to get you started, you would need to alter it to suit your needs. You could for example add an if statement before the ajax call to check if is_blue is true or not, so that you can toggle the value.

Tabulator PUT via Ajax to Django REST Endpoint - Reduces Table to Last Edited Record

I am using Tabulator with Django to edit a model. After any change to a cell, I use setData to make an Ajax call to a REST endpoint created using Django REST Framework. The database updates ok. The problem is that the response from the server contains only the single record that was updated, and this is making the Tabulator data reduce to only that record.
My question is, how can I get Tabulator to disregard the response, or otherwise have the data be left alone following the edit?
I am pretty new at this stuff (both Django and especially JavaScript) so apologies if I've missed something basic.
My tabulator code is below.
The function getCookie is to generate a CSRF_TOKEN as per the instructions in the Django documentation here. This is then included in the header as 'X-CSRFTOKEN': CSRF_TOKEN.
The variable ajaxConfigPut is used to set the method to PUT and to include the CSRF_TOKEN as noted above. This is then used in the table.setData call later on (table.setData(updateurl, updateData, ajaxConfigPut);).
The function ajaxResponse at the end just checks if the response is an array or not (because Tabulator expects an array which is fine for GET, but the PUT response was only a single {} object. So this function forces the PUT response into an array consisting of one object [{}].
<div id="example-table"></div>
<script type="text/javascript">
// get CSRF token
// https://docs.djangoproject.com/en/dev/ref/csrf/#acquiring-the-token-if-csrf-use-sessions-and-csrf-cookie-httponly-are-false
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 = cookies[i].trim();
// 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 cookieValue;
}
var CSRF_TOKEN = getCookie('csrftoken');
// set variable to customise ajaxConfig for use in the setData call
var ajaxConfigPut = {
method:"PUT", //set request type to Position
headers: {
// "Content-type": 'application/json; charset=utf-8', //set specific content type
'X-CSRFTOKEN': CSRF_TOKEN,
},
};
//create Tabulator on DOM element with id "example-table"
var table = new Tabulator("#example-table", {
ajaxURL:"{% url 'cust_listapi' %}", // reverse pick up the url since in a django template (?)
height:205, // set height of table (in CSS or here), this enables the Virtual DOM and improves render speed dramatically (can be any valid css height value)
layout:"fitColumns", //fit columns to width of table (optional)
columns:[ //Define Table Columns
{title:"Name", field:"name", width:150, editor:true},
{title:"Age", field:"age", hozAlign:"center",editor:true},
{title:"Age_Bar", field:"age", hozAlign:"left", formatter:"progress"},
{title:"Customer Status", field:"is_customer", hozAlign:"left"},
// {title:"Favourite Color", field:"col"},
// {title:"Date Of Birth", field:"dob", sorter:"date", hozAlign:"center"},
],
// see http://tabulator.info/docs/4.6/components#component-cell
cellEdited:function(cell){ //trigger an alert message when the row is clicked
console.log("Cell edited in row " + cell.getData().id
+ " and column " + cell.getField()
+ " from " + cell.getOldValue() + " to "
+ cell.getValue()
+ ". The row pk=" + cell.getData().id
);
console.log(cell.getData());
var updateurl = "{% url 'cust_listapi' %}" + cell.getData().id + "/"
console.log('URL is: ' + updateurl)
// Create variable from full row data but drop the id;
console.log('About to create updateData')
var updateData = {};
updateData[cell.getField()] = cell.getValue();
console.log(updateData);
console.log('About to setData');
table.setData(updateurl, updateData, ajaxConfigPut);
console.log('Finished setData');
//cell.restoreOldValue();
},
ajaxResponse:function(url, params, response){
console.log('Beginning ajaxResponse')
console.log('The type is:', typeof(response));
console.log(Array.isArray(response))
console.log(response)
result = response;
if(Array.isArray(response) === false){
result = [response];
};
return result;
}
});
</script>
Here's a screenshot of the table before editing:
Table Before Editing
And here's a screenshot after editing the top row (changing 'Mabel' to 'Jemima'):
Screenshot after editing
And here's the console log:
Console Log
I tried amending the response from the endpoint so that all records from the database are returned, but the problem with that is it doesn't include the edit, so the Tabulator table data is overwritten. Here's the code I used in the Django views.py. Maybe there's a way to return the data that has been changed?
views.py
from rest_framework import generics, mixins
from apps.app_mymodel.models import Customer
from .serializers import CustomerSerializer
class CustomerListAPIView(generics.ListAPIView):
serializer_class = CustomerSerializer
queryset = Customer.objects.all()
class CustomerUpdateAPIView(generics.GenericAPIView,
mixins.ListModelMixin,
mixins.UpdateModelMixin):
serializer_class = CustomerSerializer
queryset = Customer.objects.all()
# Override the put function here to return all records
def put(self, request, *args, **kwargs):
# return self.update(request, *args, **kwargs)
return self.list(request, *args, **kwargs)
Here's the serializer:
serializers.py
from rest_framework import serializers
from apps.app_mymodel.models import Customer
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = '__all__'
Can someone please point me in the right direction?
None of the Mixins used by your CustomerUpdateAPIView have a method called put. I don't think that function is called. Instead, you can try to override the update method of your viewset. It could look like this:
def update(self, request, *args, **kwargs):
obj = super().update(request, *args, **kwargs) # performs the update operation
return self.list(request, *args, **kwargs)
You can check out this URL to understand the classes you are using: http://www.cdrf.co/
There you will see all the methods of the classes you are using to better understand the flow of your request.

Django admin onclick function

I created the following features on the Django admin list!
admin.py
class LawyerAdmin(admin.ModelAdmin):
list_display = ('lawyer_idx', 'show_firm_url', 'lawyer_name', 'lawyer_birthday', 'lawyer_mobile', 'lawyer_license_num', 'like_cnt', 'recommend_name', 'register_date', 'status', 'lawyer_agent',)
list_filter = ['lawyer_status']
def status(self, obj):
if obj.lawyer_status == "W":
html = "Awaiting certification<br> <input type='button' value='certification' onclick='lawyer_confirm({0})'>"
return format_html(html, obj.lawyer_idx)
admin.site.register(Lawyer, LawyerAdmin)
When I click the On-Click button, I want to run the following script, but I don't know how. Please help me.
1. I want to implement the on-click function.
2. How to run a script
<script>
function lawyer_confirm(lawyer_idx) {
if (confirm('Certified?') == true) {
$.ajax({
url: '/admin/lawyer/view/lawyer_confirm',
data: {
lawyer_idx : lawyer_idx
},
dataType: "json",
type: 'post',
success: function(result){
alert(result.msg);
if (result.code == '0' ) {
location.href = result.retURL;
}
}
});
}
}
</script>
To add media to the admin you can simply add it to the meta class Media of your admin class, e.g.:
class FooAdmin(admin.ModelAdmin):
# regular stuff
class Media:
js = (
'//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js', # jquery
'app/js/myscript.js', # app static folder
)
You can add custom logic to select HTML element and do what you want in JS.

Dynamic FilteredSelectMultiple in django-admin

I don't know if this is even possible, any way, I currently have something as the following:
class Incidence(models.Model):
...
instalation = models.ForeignKey('Instalation')
machine = models.ManyToManyField('Machine')
...
class Machine(models.Model):
...
instalation = models.ForeignKey('Instalation')
...
So Machines belongs to instalations and incidences are related to machines and incidences, the idea is to put a dynamic FilteredSelectMultiple widget to select the machines related with the incidence in the admin page. The admin currently is something as:
class IncidenceMachineForm(forms.ModelForm):
filtered_machine = ModelMultipleChoiceField(
queryset=Machine.objects.order_by('hostname'),
required=False, widget=FilteredSelectMultiple("filtered machine name", is_stacked=False)
)
class Meta:
model = Incidence
And then, the modelAdmin uses the form IncidenceMachineForm. The idea is that when you select the instalation of the incidence, only the machines related to that instalation are available for selection. I guess something as this is not possible:
queryset=Machine.objects.filter(instalation=self.instalation).order_by('hostname'),
Any ideas will be highly appreciated. Thanks!
I notice that FilteredSelectMultiple widget has already cached, converted and changed the name of original widget after the page is loaded, so changing the "option" list of "select" tag is not enough.
I came up with this solution:
wrap "select" list inside another element ("div" for instance)
use data received from ajax call to re-create the original list
call "SelectFilter.init" to re-construct the FilteredSelectMultiple widget
Here is the code I have tested:
$('#id_instalation').change(function() {
var selected = $('#id_instalation').val();
if(selected) {
$.ajax({
url: '/url/to/get/machines/' + selected,
success: function(list) {
var options = [];
options.push('<select multiple="multiple" class="selectfilter" name="machine" id="id_machine">');
for(i in list){
options.push('<option value="' + list[i][0] + '">' +
list[i][1] + '</option>');
}
options.push('</select>');
$('#machine_wrapper').html(options.join(''));
// Change title of widget
var title = $('#id_instalation option:selected"').text().toLowerCase();
SelectFilter.init("id_machine", title, 0, "/path/to/django/media/");
},
error: function() {
alert('Server error');
},
});
}
}
This is the sample of data returned from ajax call:
[[1, "Machine 1"], [2, "Machine 2"], [3, "Machine 3"]]
For server side implementation, please see Chris Pratt's answer
Note: tested with:
jquery-1.7.2
django 1.2.5
You can do that after the model has been saved, and there's an instalation associated with it to use (though the lookup would be instalation=self.instance.instalation).
However, that doesn't do you much good, because if a different instalation is selected the list would still be the one for the old selection, and obviously you get no help when first creating the object.
As a result, the only way to accomplish this is with AJAX. You create a view to receive the selected instalation id, and return a JSON response consisting of machines associated with it. Tie the view into your urlconf, and then hit it with AJAX and update the select box based on the results.
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from django.utils import simplejson
def ajax_admin_get_machines_for_instalation(request):
instalation_id = request.GET.get('instalation_id')
if instalation_id is None:
# instalation_id wasn't provided so return all machines
machines_qs = Machine.objects.all()
else:
instalation = get_object_or_404(Instalation, pk=instalation_id)
machines_qs = Machine.objects.filter(instalation=instalation)
# 'name' is the field you want to use for the display value
machines = machines_qs.values('pk', 'name')
return HttpResponse(simplejson.dumps(machines), mimetype='application/json')
Then the JS:
(function($){
$(document).ready(function(){
function update_machine_options(){
var selected = $('#id_instalation').val();
if (selected) {
$.getJSON('/url/for/ajax/view/', {
instalation_id: selected
}, function(data, jqXHR){
var options = [];
for (k in data) {
options.append('<option value="'+data[k].pk+'">'+data[k].name+'</option>');
}
$('#id_machine').html(options.join(''));
});
}
}
update_machine_options();
$('#id_instalation').change(function(){
update_machine_options();
});
});
})(django.jQuery);
from django.contrib.admin.widgets import FilteredSelectMultiple
#admin.register(YourModel)
class YourModelAdmin(admin.ModelAdmin):
def formfield_for_manytomany(self, db_field, request, **kwargs):
kwargs['widget'] = FilteredSelectMultiple(
db_field.verbose_name,
False,
)
return super().formfield_for_manytomany(db_field, request, **kwargs)
fast and don't need to override ModelForm or etc.
effect all m2m fields.