Dynamic FilteredSelectMultiple in django-admin - django

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.

Related

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.

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

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!

Using FullCalendar with Django Views

I'm trying to implement the FullCalendar on my website, but am a little new to this and not quite sure how to format what I want to do. I have a view which will grab all of an individual user's events. I want to take those events and populate the calendar with them. My issue is that I don't know what to return in the view or how to handle that return value in the JavaScript function. Here's what I have right now:
View:
def calEvents(request):
user = request.user.get_profile()
eventList = user.eventList
ownedList = user.ownedEvent
events = #Part I'm having trouble with
return HttpResponse(events)
The eventList and ownedEvent keep track of all of a user's events. They have names/dates associated with them. What I don't understand is the format I need to put all that information in to return in my HttpResponse.
My JavaScript function is:
$(document).ready(function() {
$('#calendar').fullCalendar({
eventSources: [
{
url: '/calEvents/',
editable: false,
}
]
});
});
I'm telling it to go to the Django view, but am lost after that. Thanks so much in advance!
I have done this by building a list of dictionaries in my Django view, setting at minimum the required fields of 'title' and the start time, then using simplejson.dumps with cls=DjangoJSONEncoder to return json.
from django.core.serializers.json import DjangoJSONEncoder
def calEvents(request):
# as above, then:
events = []
for event in eventList:
events.append({'title': event.name, 'start': event.start})
# something similar for owned events, maybe with a different className if you like
return HttpResponse(simplejson.dumps(events, cls=DjangoJSONEncoder), mimetype='application/json')
You may also with to limit the events you return based on the starting and ending times provided by the get request:
def calEvents(request):
user = request.user.get_profile()
start_timestamp = request.GET.get('start')
end_timestamp = request.GET.get('end')
start_datetime = datetime.datetime.fromtimestamp(float(start_timestamp))
end_datetime = datetime.datetime.fromtimestamp(float(end_timestamp))
eventList = user.eventList.filter(start_time__lte=end_datetime, end_time__gte=start_datetime)
I am neglecting error handling for the timestamp conversion - fullcalendar will give you appropriate values, but it would be best to allow for the possibility of bad input. And I am making assumptions about the structure of your event models.

Django- Many to Many field querying

I have following structure of models in django :
class BodySubPart(models.Model):
body_subpart=models.CharField(max_length=20)
def __str__(self):
return self.body_subpart
class BodyPart(models.Model):
body_part=models.CharField(max_length=20)
body_subpart=models.ManyToManyField(BodySubPart)
def __str__(self):
return self.body_part
Ex:
example,
if BodyPart=head then BodySubPart= face,mouth,eyes,nose.
if BodyPart=arm then BodySubPart= shoulder,fingers,elbow.
like this many body parts are stored.
...
now I want to create a runtime form have two choicefields (BodySubPart and BodyPart) such that when i select the BodyPart it should change the list in BodySubPart.
Ex.
The first choicefield has body parts={head,arm,chest...}
The second choice field should change when i select a particular part
If i select "head" then second choice field should show,
body sub parts={face,mouth,eyes,nose...}
Please help me here.....
What have you tried?? I think you will find people are more willing to help you if you have actually tried something yourself and not just want others to do it for you. It should go something like this:
1) BodyPart.objects.all() # all body parts
2) head = BodyPart.objects.get(body_part='head')
head_subparts = head.body_subpart.all() # all head subparts
django does a great job of explaining how to query these relationships.
https://docs.djangoproject.com/en/dev/topics/db/models/#many-to-many-relationships
In addition there are a number of really great tutorials online regarding djangos' manytomany relationships.
This requires a bit of AJAX, so first step is to create a view to handle that:
from django.core import serializers
from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import get_list_or_404
def ajax_get_bodysubparts(request):
bodypart_id = request.GET.get('bodypart_id')
if bodypart_id:
bodysubparts = get_list_or_404(BodySubPart, bodypart__id=bodypart_id)
data = serializers.serialize('json', bodysubparts)
return HttpResponse(data, mimetype='application/json')
else:
return HttpResponseBadRequest()
Tie that to some URL in urls.py. Then, some JavaScript for your form (assumes jQuery):
$(document).ready(function(){
$('#id_bodypart').change(function(){
var selected = $(this).val();
if (selected) {
$.getJSON('/url/to/ajax/view/', {
'bodypart_id': selected
}, function (data, jqXHR) {
options = [];
for (var i=0; i<data.length; i++) {
options.append('<option value="'+data[i].id+'">'+data[i].body_subpart+'</option>');
}
$('#id_bodysubpart).html(options.join(''));
});
}
});
});
You will probably need a combination of custom form fields and widgets to get what you want.
Check out the django-ajax-filtered-fields project to see if that is close what you are looking for. It will at least provide some guidance if you decide to create your own.
You will need some javascript to make a new request to populate your fields dynamically, so that will also not be available with standard django forms.

Django-selectable with dynamic inlines

I'm using django-selectable ( https://bitbucket.org/mlavin/django-selectable ) with
an admin tabularInline to get autocomplete functionality on one of the inline fields. It works for inlines added at creation time. The problem I'm having is that the autocomplete functionality isn't added when the user adds another row to the inline.
There's a bug and fix for this issue here
https://bitbucket.org/mlavin/django-selectable/issue/12/make-it-work-with-dynamically-added-forms
And looking at jquery.dj.selectable.js near the bottom is :
if (typeof(django) != "undefined" && typeof(django.jQuery) != "undefined") {
if (django.jQuery.fn.formset) {
var oldformset = django.jQuery.fn.formset;
django.jQuery.fn.formset = function(opts) {
var options = $.extend({}, opts);
var addedevent = function(row) {
bindSelectables($(row));
};
var added = null;
if (options.added) {
var oldadded = options.added;
added = function(row) { oldadded(row); addedevent(row); };
}
options.added = added || addedevent;
return oldformset.call(this, options);
};
}
}
It looks like this should make the autocomplete work with dynamically added rows, but I can't work out what to do for this to work.
The admin tabularInline.html has inline_admin_formset so should I be checking for that and not django.jQuery.fn.formset as in the code above ? Or somehow adding inline_admin_formset to django.jQuery.fn ?
Thanks very much for any suggestions.
I'm using version 0.2.
In forms.py there is the inline form :
class GrammarInlineForm(forms.ModelForm):
class Meta:
model = Grammar
widgets = {
'description' :forms.Textarea(attrs={'cols': 80, 'rows': 10, 'class': 'grammarInline'}),
'title' : selectable.AutoCompleteSelectWidget(lookup_class=GrammarLookup, allow_new=True),
}
exclude = ('creation_date', 'creator', 'plan')
def __init__(self, *args, **kwargs):
super(GrammarInlineForm, self).__init__(*args, **kwargs)
In admin.py the inline admin is made and added to the main admin ( PlanAdmin ) :
class GrammarInline(admin.TabularInline):
form = GrammarInlineForm
model = Grammar
extra = 2
def save_formset(self, request,form, formset, change):
instances = formset.save(commit=False)
for instance in instances:
instance.creator = request.user
instance.save()
formset.save_m2m()
class PlanAdmin(admin.ModelAdmin):
form = PlanForm
list_display = ('title', 'topic', 'level', 'description','public', )
inlines = [ ActivityInline, GrammarInline, ]
After reading your ticket http://code.djangoproject.com/ticket/15760 I tried binding to the inlines formsetadd event, like this
django.jQuery('.ui-autocomplete-input').live('formsetadd', function(e, row) {
console.log('Formset add!');
console.log($(row));
});
but looking at django/contrib/admin/media/js/inlines.js
it seems that these triggers aren't in version 1.3.1 of django. Is it necessary to bind to an event that gets triggered when an inline is added? There is a similar case here
https://bitbucket.org/mlavin/django-selectable/issue/31/dynamically-added-forms
but that's using the formset plugin. Is there a way to use bindSelectable(row) to the admin inline ?
The jquery.dj.selectable.js code you posted is there to patch django/contrib/admin/media/js/inlines.js to call bindSelectable(row) when a new row is added. http://code.djangoproject.com/ticket/15760 was opened so that this monkey patch isn't necessary but has not been closed and likely will not be closed for Django 1.4. Again you shouldn't need to do anything to make this work. You don't need to change the template. You don't need to write any additional JS.
The project source has a working example of using a dynamic tabular inline: https://bitbucket.org/mlavin/django-selectable/src/33e4e93b3fb3/example/core/admin.py#cl-39