django htmx change hx-get url after user input from radio buttons - django

This is forms.py:
class OrderForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.attrs = {
"hx_get": reverse("planner:detail", kwargs={"slug": self.instance.venue}),
"hx_target": "#services",
}
class Meta:
model = Order
fields = ["venue"]
widgets = {"venue": forms.RadioSelect()}
HTML:
<div id="venue-options" class="p-3">
{% crispy form %}
</div>
This is the error when you click one of the radio buttons: The current path, planner/detail/None/, matched the last one. It says None/ because when you join the page none of the radio buttons are clicked so there is no slug to replace it. I need a way to replace None with the slug / id of the object chosen by the user. I need the url to be changed everytime the choise is changed.
I am open to using apline.js but i would like a way of doing it in htmx/django.
Thank you so much.

Basically you are changing a single attribute (hx_get) based on a click. HTMX would make this a roundtrip to the server which seems inefficient as all the data is there on the page (assuming the value of the radiobuttons is the slug you are after). You can do this with pure javascript which would make the process much faster
<script>
//get the form
const venueForm = document.getElementById('venue-options')
//when implementing, replace [name='radio'] with name of radio input fields
const venueRadioButtons = document.querySelectorAll("input[name='radio']")
//get the default venue URL for later alteration
const venueURL = venueForm.getAttribute('hx_get')
// create a clickhandler to change the URL to selected radiobutton
const clickHandler = () => {
//get the slug value
let radioButtonSlug=document.querySelector("input[name='radio']:checked").value
//use reguylar expression to replace last '/word/' value with slug value
let newVenueUrl = venueURL.replace(/\/\w+\/$/, "/" + radioButtonSlug + "/")
//reset the the hx_get value
venueForm.setAttribute('hx_get', newVenueUrl)
};
// Assign the handler to the radiobuttons
venueRadioButtons.forEach(i => i.onchange = () => clickHandler());
</script>

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.

How can I ignore a field validation in flask-wtf?

I have a form to add an item to my database, which includes two buttons: Cancel and Submit. The problem I have is that when I press the Cancel button with an empty form, I get a Please fill out this field. error instead of returning to my home page (see views.py for logic). So how can I get my app to ignore the DataRequired validators when I press the Cancel button?
forms.py:
class ItemForm(FlaskForm):
id = StringField('id', validators=[DataRequired()]
name = StringField('Name', validators=[DataRequired()]
cancel = SubmitField('Cancel')
submit = SubmitField('Submit')
views.py:
def add_item()
form = ItemForm()
if form.validate_on_submit():
if form.submit.data:
# Code to add item to db, removed for brevity.
elif form.cancel.data:
flash('Add operation cancelled')
return redirect(url_for('home.homepage'))
Your cancel button doesn't really need to be a submit button. You can simply have a normal button which takes the user back to the home page (using a href or capturing the onclick event).
If you still want the cancel button to be a WTForms field, one option would be to override the validate method in the form and remove the DataRequired validators on id and name. The below is untested but may give you a starting point to work from.
class ItemForm(FlaskForm):
id = StringField('id')
name = StringField('Name')
cancel = SubmitField('Cancel')
submit = SubmitField('Submit')
def validate(self):
rv = Form.validate(self)
if not rv:
return False
if self.cancel.data
return True
if self.id.data is None or self.name.data is None:
return False
return True

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.

How to create a Django form with a RadioSelect field, and some buttons disabled?

I'm creating a Django form with a set of Radio buttons (as a single RadioSelect field), and I'd like to have some of the buttons grayed out. However, given that the RadioSelect field is a single field on the form, I haven't found a way to do that.
class OrderStatusForm(forms.Form):
os = Order_Status.objects.values_list('id', 'status')
status = forms.ChoiceField(choices=os, widget=forms.RadioSelect())
def makeForm():
newForm = OrderStatusForm()
# next line disables all radio buttons.
newForm.fields['status'].widget.attrs['disabled'] = True
Is there some way to selectively disable the individual radio buttons? Or do I just have to not put the disabled options in the list in the first place?
Further research turned up the fact that the RadioSelect renderer can be replaced, and passed in as a parameter to the initial widget creation function. So I created my own renderer, and attached a list of Booleans, indicating which Radio buttons should be disabled.
The renderer adds a disabled attribute to each Radio button widget as needed, then the force_unicode turns the widget's attributes into HTML that include a disabled="True" value.
from django.utils.safestring import mark_safe
from django.utils.encoding import force_unicode
class RandomRenderer(forms.RadioSelect.renderer):
""" Modifies some of the Radio buttons to be disabled in HTML,
based on an externally-appended Actives list. """
def render(self):
if not hasattr(self, "actives"): # oops, forgot to add an Actives list
return self.original_render()
return self.my_render()
def original_render(self):
return mark_safe(u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>'
% force_unicode(w) for w in self]))
def my_render(self):
midList = []
for x, wid in enumerate(self):
if self.actives[x] == False:
wid.attrs['disabled'] = True
midList.append(u'<li>%s</li>' % force_unicode(wid))
finalList = mark_safe(u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>'
% w for w in midList]))
return finalList
class OrderStatusForm(forms.Form):
os = Order_Status.objects.values_list('id', 'status', 'reason')
activeList = [True, False, True, False, True, False,]
newStatus = forms.ChoiceField(widget=forms.RadioSelect(
renderer=RandomRenderer), choices=os)
newStatus.widget.renderer.actives = activeList
It's a little kludgy - I'm just sticking the actives list directly onto the renderer, which works (love Python's duck-typing), but would be cleaner if I passed the list in some constructors. Unfortunately I had problems with that, so took the easy way out. :)
Maybe try:
status = forms.ChoiceField(choices=os, widget=forms.RadioSelect(attrs={'disabled':'disabled'}))
or
class OrderStatusForm(forms.Form):
def __init__(self, *args, **kwargs):
super(OrderStatusForm, self).__init__(*args, **kwargs)
self.fields['status'].widget.attrs['disabled'] = 'disabled'
os = Order_Status.objects.values_list('id', 'status')
status = forms.ChoiceField(choices=os, widget=forms.RadioSelect())
You could do it in the template with JavaScript, with something like this:
<script>
$(function() {
{% for val in disable %}
$('input[name="status"][value="{{ val }}"]').attr('disabled', true);
{% endfor %}
});
</script>
Assuming that you've got jQuery (for the $) and disable (in the {% for %} loop) is a variable you pass to the template, which evaluates to the list of values that you want grayed out.