Django-selectable with dynamic inlines - django

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

Related

(Hidden field id) Select a valid choice. That choice is not one of the available choices. (Django)

I'm receiving this error when I try to submit two formsets. After I fill the form and click the save button, it gives the error:
(Hidden field id) Select a valid choice. That choice is not one of the available choices.
I'm trying to create dynamic form so that the user can add new sections and also new lectures inside the section when they click "Add" button. The adding new form function works well, I just have problem saving it to the database.
Views.py
def addMaterials(request, pk):
course = Course.objects.get(id=pk)
sections = CourseSection.objects.filter(course_id=pk)
materials = CourseMaterial.objects.filter(section__in=sections)
SectionFormSet = modelformset_factory(CourseSection, form=SectionForm, extra=0)
sectionformset = SectionFormSet(request.POST or None, queryset=sections)
MaterialFormSet = modelformset_factory(CourseMaterial, form=MaterialForm, extra=0)
materialformset = MaterialFormSet(request.POST or None, queryset=materials)
context = {
'course': course,
'sectionformset': sectionformset,
'materialformset': materialformset,
}
if request.method == "POST":
if all([sectionformset.is_valid() and materialformset.is_valid()]):
for sectionform in sectionformset:
section = sectionform.save(commit=False)
section.course_id = course.id
section.save()
for materialform in materialformset:
material = materialform.save(commit=False)
print(material)
material.section_id = section #section.id or section.pk also doesn't work
material.save()
return('success')
return render(request, 'courses/add_materials.html', context)
Forms.py
class SectionForm(forms.ModelForm):
class Meta:
model = CourseSection
fields = ['section_name', ]
exclude = ('course_id', )
class MaterialForm(forms.ModelForm):
class Meta:
model = CourseMaterial
fields = ['lecture_name', 'contents']
The second formset which is materialformset need the section id from the first formset hence why there is two loop in views.
Can someone help me to solve this. I'm not sure how to fix it.
This is the what I'm trying to do.
I'm new to django but I had to face with the same problem. My solution was to handle singularly each formset inside 'views.py'.
In the template.html, create a tag for each formset you have, than inside that tag put <input type="submit" name="form1">(Note that name is important and must be different with the respect of the form you are submitting).
Then in views.py, instead for writing if all([sectionformset.is_valid() and materialformset.is_valid()]), try like this:
if 'form1' in request.POST:
if sectionformset.is_valid():
sectionformset.save()
# other rows of your code
return('success')
if 'form2' in request.POST:
if materialformset.is_valid():
materialformset.save()
# etc. etc.

Conditional logic for crispy form

I've a form and I wanted to know how I was supposed to hide the field "conditionalWeb" until the user choose "Web application" for the typeOfTheproject field?
I've made my research online but I absolutely don't know how to proceed... Any help would be nice :)
from django import forms
from configurator import models
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Layout
from .models import TypeOfProgram, Language, Framework, Database
from crispy_forms.bootstrap import (PrependedAppendedText, PrependedText, FormActions)
class ConfiguratorForm(forms.Form):
helper = FormHelper()
helper.form_method = 'POST'
helper.form_show_labels = False
queryOfProject = TypeOfProgram.objects.values_list('name')
queryOfFramework = Framework.objects.values_list('name','version')
queryOfDatabase = Database.objects.values_list('name','version')
listFramework = []
listProject = []
conditionalWeb=[]
listFramework=[((q[0],q[1]),q[0]+" version "+q[1])for q in queryOfFramework]
listProject=[(q[0],q[0])for q in queryOfProject]
listDatabase = [((q[0],q[1]),q[0]+" version "+q[1])for q in queryOfDatabase]
typeOfTheproject = forms.ChoiceField(choices = listProject)
conditionalWeb = forms.ChoiceField (choices = [('nothing', '----'),("Only Backend","Only Backend"),("Only Frontend","Only Frontend")])
wantedFramework = forms.MultipleChoiceField(choices = listFramework)
wantedDatabase = forms.MultipleChoiceField(choices = listDatabase)
helper.layout = Layout(
'typeOfTheproject',
'wantedFramework',
'wantedDatabase',
FormActions(Submit('Finalize and find the result','Finalize and find the result', css_class="btn btn-success"))
)
#Not Working
if typeOfTheproject is 'Web application':
helper.layout.append('conditionalWeb')
Thank you :)
You can add a simple javascript for the task:
$(document).ready(function(){
hideShow()
})
// call hideShow when the user clicks on the project_type dropdownlist
$('#id_typeoftheproject').click(function(){
hideShow()
});
function hideShow(){
if(document.getElementById('id_typeoftheproject').value == "7")
{
$('#id_conditionalweb').show();
}
else
{
$('#id_conditionalweb').hide();
}
}
You need to find the actual ids of the fields from database and replace #id_typeoftheproject and #id_conditionalweb . Also the value '7' needs to be replaced with id of web application.

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!

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.

custom html field in the django admin changelist_view

I'd like to some little customisation with the django admin -- particularly the changelist_view
class FeatureAdmin(admin.ModelAdmin):
list_display = (
'content_object_change_url',
'content_object',
'content_type',
'active',
'ordering',
'is_published',
)
list_editable = (
'active',
'ordering',
)
list_display_links = (
'content_object_change_url',
)
admin.site.register(get_model('features', 'feature'), FeatureAdmin)
The idea is that the 'content_object_change_url' could be a link to another object's change_view... a convenience for the admin user to quickly navigate directly to the item.
The other case I'd have for this kind of thing is adding links to external sources, or thumbnails of image fields.
I had thought I'd heard of a 'insert html' option -- but maybe I'm getting ahead of myself.
Thank you for your help!
You can provide a custom method on the FeatureAdmin class which returns HTML for content_object_change_url:
class FeatureAdmin(admin.ModelAdmin):
[...]
def content_object_change_url(self, obj):
return 'Click to change' % obj.get_absolute_url()
content_object_change_url.allow_tags=True
See the documentation.
Pay attention and use format_html (See docs here) as the mark_safe util has been deprecated since version 1.10. Moreover, support for the allow_tags attribute on ModelAdmin methods will be removed since version 1.11.
from django.utils.html import format_html
from django.contrib import admin
class FeatureAdmin(admin.ModelAdmin):
list_display = (
'change_url',
[...]
)
def change_url(self, obj):
return format_html('<a target="_blank" href="{}">Change</a>', obj.get_absolute_url())
change_url.short_description='URL'
It took me two hours to find out why Daniel Roseman's solution doesn't work for me. Even though he is right, there's one exception: when you want to make custom calculated fields (read only) in the Admin. This wont work. The very easy solution (but hard to find) is to return your string in a special constructor: SafeText(). Maybe this is linked with Django 2 or with readonly_fields (which behave differently from classical fields)
Here's a working sample that works but doesn't without SafeText():
from django.utils.safestring import SafeText
class ModelAdminWithData(admin.ModelAdmin):
def decrypt_bin_as_json(self, obj):
if not obj:
return _("Mode insert, nothing to display")
if not obj.data:
return _("No data in the game yet")
total = '<br/><pre>{}</pre>'.format(
json.dumps(json.loads(obj.data),
indent=4).replace(' ', ' '))
return SafeText(total) # !! working solution !! <------------------
decrypt_bin_as_json.short_description = _("Data")
decrypt_bin_as_json.allow_tags = True
readonly_fields = ('decrypt_bin_as_json',)
fieldsets = (
(_('Data dump'), {
'classes': ('collapse',),
'fields': ('decrypt_bin_as_json',)
}),
)