Django - Javascript dynamic inline FormSet with autocomplete - django

I'm trying to make a kind of scheduler event editor with the ability to attach participants.
Models
class Session(models.Model):
start_time = models.DateTimeField()
end_time = models.DateTimeField()
class Participation(models.Model):
session = models.ForeignKey(Session)
participant = models.ForeignKey(User)
status = models.CharField(max_length=1, choices=STATUSES)
In the editor I'd like to have an autocomplete search input from which I can find users to add to the session
Preview
Here I have typed "laurent" and I'm going to add a person by clicking on one of the resulting names
Participant colors depend on their status
I have a form for the Session object defined with start & end times
Now I think I should have an inline formset for Participations
Questions
Do you suggest that I use an inline formset for the participants ?
How can I dynamically add/delete participant rows ?

The question seems very simple but a proper response would involve several answers.
I will give my solutions point by point, using jQuery.
Autocomplete
This is the simple part. You can use a plugin like select2 or jqueryui autocomplete and a view that finds users like
def search_users(request):
search = request.GET.get('term')
users = User.objects.filter(
Q(first_name__icontains=search)
| Q(last_name__icontains=search)
)
ulist = list({'id': u.id, 'value': u'%s %s' % (u.first_name, u.last_name)}
for u in users)
return JsonResponse(ulist)
This view is compatible with the default jQuery UI Autocomplete plugin
Dynamic Formset
This is the tricky one. The key is to take advantage of management_form and form.DELETE. Here is my solution:
Use an inline formset for the participants (with one extra form)
Print the management_form
Add form lines with jQuery after autocomplete selection by cloning a hidden empty form (the extra one) and incrementing id_form-TOTAL_FORMS
Delete form lines with jQuery by hiding them and checking a hidden delete checkbox
Template
<form method="post">{% csrf_token %}
{{ sessionform }}
<div>
{{ participant_formset.management_form }}
<label for="part_search">Search: </label><input id="part_search" />
<ul id="participation_set">
{% for tform in participant_formset %}
{{ tform.id }}
<li>
<span class="participant">
{{ tform.participant }}{{ tform.instance.participant.name }}
</span>
<span class="status">{{ tform.status }}</span>
<span class="delete ui-icon ui-icon-circle-minus">
{{ tform.DELETE }}
</span>
</li>
{% endfor %}
</ul>
</div>
</form>
CSS
/* Delete button */
#participation_set .delete {
display: inline-block;
vertical-align: middle;
cursor: pointer;
}
/* Hidden delete checkbox */
#participation_set .delete input {
display: none;
}
/* Deleted form */
#participation_set li.deleted {
display: none;
}
/* Last hidden form to clone */
#participation_set li:last-child {
display: none;
}
jQuery
/*! This adds a form line
* Call it on autocomplete select
*/
function add_aform(inst, item) {
if ($(':input[name$="participant"][value=' + item.id + ']').length) {
return false;
}
var total = $('#id_' + inst + '-TOTAL_FORMS').val();
var sul = '#' + inst;
var li = $(sul + ' li:last-child');
var new_li = li.clone().appendTo(sul);
li.find('span.participant').append(item.label);
li.find(':input[name$="participant"]').val(item.id);
new_li.find(':input').each(function () {
var new_name = $(this).attr('name')
.replace('-' + (total - 1) + '-', '-' + total + '-');
$(this).attr('name', new_name);
});
new_li.find('label').each(function () {
var tmp = $(this).attr('for')
.replace('-' + (total - 1) + '-', '-' + total + '-');
$(this).attr('for', new_for);
});
new_li.find('.delete').click(del_aform);
$('#id_' + inst + '-TOTAL_FORMS').val(++total);
}
/*! This removes a form line
* Call it on click from delete buttons (placed inside each li)
*/
function del_aform() {
$(this).parents('li').addClass('deleted');
$(this).find(':checkbox').attr('checked', true);
}
I know I could also use an empty_form instance and use __prefix__ to replace the ids which simplifies the javascript for a better maintainability, but I didn't find a way to factorize the code between the true form and the empty one.
View
The view is pretty standard using inlineformset_factory with extra set to 1 (to get the only hidden form to clone). Also don't forget to use a HiddenInput widget for the field participant

This plugin would help you get the auto-complete functionality you're looking for:
https://github.com/millioner/django-ajax-select

Related

How to reference a variable from outside it's loop scope in Django template

I am sorry if the title is poorly phrased, but what i am trying to do is to use a variable that was in a for loop outside of it's scope ( in another part of the template )
here is my template:
<div class="inventory-content">
<div class='category'>
<div>Categories</div>
<div class='category-checkbox'>
{%for category in items%}
<input type="checkbox" id="{{category.id}}" name="{{category.name}}" value="{{category.id}}">
<label for="{{category.name}}"> {{category.name}}</label><br>
{%endfor%}
</div>
</div>
<div class='items'></div>
</div>
<script>
$('.category-checkbox input[type="checkbox"]').click(function() {
if ($(this).is(':checked')) {
// Add the element to the div with an id identifier
$('.items').append(`<div id="item_${this.id}">123</div>`);
} else {
// Remove the element from the div targeted by the id identifier
$(`#item_${this.id}`).remove();
}
});
</script>
the view.py
def index(request,pk):
vessel_id = Vessel.objects.get(id=pk)
categories = vessel_id.category.all()
item = categories.prefetch_related(
'items')
context ={"vessel_id":vessel_id,'items':item}
return render(request,'inventory.html',context)
i want to be able to use that category variable so i can loop through all the items in that specific category and add them in the items <div>, if there is a better way to do this please guide me !
Replace click with on event as you are dynamically adding elements like this:
$('.category-checkbox input[type="checkbox"]').on('click', function() {
// rest of your code
});

How to fire an 'if condition' after making an ajax call to a view?

I'm building a search bar that searches for objects in my database and if the search finds some matching objects, it then displays the objects in the html page without reloading it. But after I fire the Ajax call and get the tuples from the database, they are not being rendered on the template
So, the flow is like this: I search for 'apple' in the search bar, hit search button and fire the ajax call to my view. After that, I set a boolean value 'search_successful' to 'True' if I find any 'apple' in my database. I then pass it to my template from where I fired the ajax in the first place. Now I am successfully getting the data and the boolean sets to 'True' correctly. Until this point everything works perfect. But after passing the context to my template, It does not fire the if condition that will display the data if search_successful = True. All this is carried out by an ajax call without reloading the page.
Here is the if condition which I expect to work after the ajax call has been made to print the data:
<form id="search_object">
{% csrf_token %}
<input type="text" id="search_my_object" placeholder="Search Item" id="search_my_object" class="form-control" required="true" style="border-color:#f4511e;"/>
<br>
<input type="submit" value="SEARCH" class="btn btn-lg" style="border-color:#f4511e; background-color:Transparent; color:white; margin-left:47%;">
</form>
{% if search_successful is True %}
# Just print the retrieved data passed in the context
{% endif %}
<script type="text/javascript">
$(document).on('submit', '#search_object', function(e){
e.preventDefault();
$.ajax({
type: 'POST',
url: '{% url 'daily_object_search' %}',
data: {
search_object_name: $('#search_my_object').val(),
csrfmiddlewaretoken: $('input[name=csrfmiddlewaretoken]').val()
},
success: function(data){
if(data == "true"){
alert('This Object is Present');
}
else if(data == "invalid_query"){
alert("Please enter a valid object to be searched");
}
else{
alert('This object is not present');
}
}
});
});
Views.py :
def daily_object_search(request):
context = {'search_successful': False}
if request.method == 'POST':
object_name = request.POST['search_object_name']
print("The query for search :", object_name)
if len(object_name) <= 2:
print("The length of the object name :", len(object_name))
return HttpResponse('invalid_query')
my_cursor = mydb.cursor()
sql = "select * from education_fruits where tag = '" + object_name + "'"
my_cursor.execute(sql)
result = my_cursor.fetchall()
print(result)
if my_cursor.rowcount <= 0:
print("No object found")
return HttpResponse('false')
context['search_successful'] = True
# Just append the 'result' data in the context before sending it
return HttpResponse('true', context)
I tried printing the data in the if condition but removed it as it was giving me the mentioned error in the browser console : XML Parsing Error: syntax error Location: http://127.0.0.1:8000/login/daily_object_search
Line Number 1, Column 1.
Am I missing something in my understanding of ajax calls and templates?
Does it really have to be a boolean? If not, you can do this and compare strings instead.
Views.py
context['search_successful'] = "true"
template
{% if search_successful|stringformat:"i" == "true" %}
# Do whatever
{% endif %}
# stringformat:"i", converts your template variable into String since you want to make sure that you're comparing two objects with the same type.
# You can also change the string, it doesn't have to be "true" you can use "found", "checked" etc.

How to add new form to django formset and not lose user input from previous forms [duplicate]

I want to dynamically add new forms to a Django formset, so that when the user clicks an "add" button it runs JavaScript that adds a new form (which is part of the formset) to the page.
This is how I do it, using jQuery:
My template:
<h3>My Services</h3>
{{ serviceFormset.management_form }}
{% for form in serviceFormset.forms %}
<div class='table'>
<table class='no_error'>
{{ form.as_table }}
</table>
</div>
{% endfor %}
<input type="button" value="Add More" id="add_more">
<script>
$('#add_more').click(function() {
cloneMore('div.table:last', 'service');
});
</script>
In a javascript file:
function cloneMore(selector, type) {
var newElement = $(selector).clone(true);
var total = $('#id_' + type + '-TOTAL_FORMS').val();
newElement.find(':input').each(function() {
var name = $(this).attr('name').replace('-' + (total-1) + '-','-' + total + '-');
var id = 'id_' + name;
$(this).attr({'name': name, 'id': id}).val('').removeAttr('checked');
});
newElement.find('label').each(function() {
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
$(this).attr('for', newFor);
});
total++;
$('#id_' + type + '-TOTAL_FORMS').val(total);
$(selector).after(newElement);
}
What it does:
cloneMore accepts selector as the first argument, and the type of formset as the 2nd one. What the selector should do is pass it what it should duplicate. In this case, I pass it div.table:last so that jQuery looks for the last table with a class of table. The :last part of it is important because the selector is also used to determine what the new form will be inserted after. More than likely you'd want it at the end of the rest of the forms. The type argument is so that we can update the management_form field, notably TOTAL_FORMS, as well as the actual form fields. If you have a formset full of, say, Client models, the management fields will have IDs of id_clients-TOTAL_FORMS and id_clients-INITIAL_FORMS, while the form fields will be in a format of id_clients-N-fieldname with N being the form number, starting with 0. So with the type argument the cloneMore function looks at how many forms there currently are, and goes through every input and label inside the new form replacing all the field names/ids from something like id_clients-(N)-name to id_clients-(N+1)-name and so on. After it is finished, it updates the TOTAL_FORMS field to reflect the new form and adds it to the end of the set.
This function is particularly helpful to me because the way it is setup it allows me to use it throughout the app when I want to provide more forms in a formset, and doesn't make me need to have a hidden "template" form to duplicate as long as I pass it the formset name and the format in which the forms are laid out.
Simplified version of Paolo's answer using empty_form as a template.
<h3>My Services</h3>
{{ serviceFormset.management_form }}
<div id="form_set">
{% for form in serviceFormset.forms %}
<table class='no_error'>
{{ form.as_table }}
</table>
{% endfor %}
</div>
<input type="button" value="Add More" id="add_more">
<div id="empty_form" style="display:none">
<table class='no_error'>
{{ serviceFormset.empty_form.as_table }}
</table>
</div>
<script>
$('#add_more').click(function() {
var form_idx = $('#id_form-TOTAL_FORMS').val();
$('#form_set').append($('#empty_form').html().replace(/__prefix__/g, form_idx));
$('#id_form-TOTAL_FORMS').val(parseInt(form_idx) + 1);
});
</script>
Paolo's suggestion works beautifully with one caveat - the browser's back/forward buttons.
The dynamic elements created with Paolo's script will not be rendered if the user returns to the formset using the back/forward button. An issue that may be a deal breaker for some.
Example:
1) User adds two new forms to the formset using the "add-more" button
2) User populates the forms and submits the formset
3) User clicks the back button in the browser
4) Formset is now reduced to the original form, all dynamically added forms are not there
This is not a defect with Paolo's script at all; but a fact of life with dom manipulation and browser's cache.
I suppose one could store the values of the form in the session and have some ajax magic when the formset loads to create the elements again and reload the values from the session; but depending on how anal you want to be about the same user and multiple instances of the form this may become very complicated.
Anyone has a good suggestion for dealing with this?
Thanks!
Simulate and imitate:
Create a formset which corresponds to the situation before clicking the "add" button.
Load the page, view the source and take a note of all <input> fields.
Modify the formset to correspond to the situation after clicking the "add" button (change the number of extra fields).
Load the page, view the source and take a note of how the <input> fields changed.
Create some JavaScript which modifies the DOM in a suitable way to move it from the before state to the after state.
Attach that JavaScript to the "add" button.
While I do know formsets use special hidden <input> fields and know approximately what the script must do, I don't recall the details off the top of my head. What I described above is what I would do in your situation.
For the coders out there who are hunting resources to understand the above solutions a little better:
Django Dynamic Formsets
After reading the above link, the Django documentation and previous solutions should make a lot more sense.
Django Formset Documentation
As a quick summary of what I was getting confused by: The Management Form contains an overview of the forms within. You must keep that information accurate in order for Django to be aware of the forms you add. (Community, please give me suggestions if some of my wording is off here. Im new to Django.)
One option would be to create a formset with every possible form, but initially set the unrequired forms to hidden - ie, display: none;. When it's necessary to display a form, set it's css display to block or whatever is appropriate.
Without know more details of what your "Ajax" is doing, it's hard to give a more detailed response.
Another cloneMore version, which allows for selective sanitization of fields. Use it when you need to prevent several fields from being erased.
$('table tr.add-row a').click(function() {
toSanitize = new Array('id', 'product', 'price', 'type', 'valid_from', 'valid_until');
cloneMore('div.formtable table tr.form-row:last', 'form', toSanitize);
});
function cloneMore(selector, type, sanitize) {
var newElement = $(selector).clone(true);
var total = $('#id_' + type + '-TOTAL_FORMS').val();
newElement.find(':input').each(function() {
var namePure = $(this).attr('name').replace(type + '-' + (total-1) + '-', '');
var name = $(this).attr('name').replace('-' + (total-1) + '-','-' + total + '-');
var id = 'id_' + name;
$(this).attr({'name': name, 'id': id}).removeAttr('checked');
if ($.inArray(namePure, sanitize) != -1) {
$(this).val('');
}
});
newElement.find('label').each(function() {
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
$(this).attr('for', newFor);
});
total++;
$('#id_' + type + '-TOTAL_FORMS').val(total);
$(selector).after(newElement);
}
There is a small issue with the cloneMore function. Since it's also cleaning the value of the django auto-generated hidden fields, it causes django to complain if you try to save a formset with more than one empty form.
Here is a fix:
function cloneMore(selector, type) {
var newElement = $(selector).clone(true);
var total = $('#id_' + type + '-TOTAL_FORMS').val();
newElement.find(':input').each(function() {
var name = $(this).attr('name').replace('-' + (total-1) + '-','-' + total + '-');
var id = 'id_' + name;
if ($(this).attr('type') != 'hidden') {
$(this).val('');
}
$(this).attr({'name': name, 'id': id}).removeAttr('checked');
});
newElement.find('label').each(function() {
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
$(this).attr('for', newFor);
});
total++;
$('#id_' + type + '-TOTAL_FORMS').val(total);
$(selector).after(newElement);
}
Because all answers above use jQuery and make some things a bit complex I wrote following script:
function $(selector, element) {
if (!element) {
element = document
}
return element.querySelector(selector)
}
function $$(selector, element) {
if (!element) {
element = document
}
return element.querySelectorAll(selector)
}
function hasReachedMaxNum(type, form) {
var total = parseInt(form.elements[type + "-TOTAL_FORMS"].value);
var max = parseInt(form.elements[type + "-MAX_NUM_FORMS"].value);
return total >= max
}
function cloneMore(element, type, form) {
var totalElement = form.elements[type + "-TOTAL_FORMS"];
total = parseInt(totalElement.value);
newElement = element.cloneNode(true);
for (var input of $$("input", newElement)) {
input.name = input.name.replace("-" + (total - 1) + "-", "-" + total + "-");
input.value = null
}
total++;
element.parentNode.insertBefore(newElement, element.nextSibling);
totalElement.value = total;
return newElement
}
var addChoiceButton = $("#add-choice");
addChoiceButton.onclick = function() {
var choices = $("#choices");
var createForm = $("#create");
cloneMore(choices.lastElementChild, "choice_set", createForm);
if (hasReachedMaxNum("choice_set", createForm)) {
this.disabled = true
}
};
First you should set auto_id to false and so disable the duplication of id and name. Because the input names have to be unique in there form, all identification is done with them and not with id's.
You also have to replace the form, type and the container of the formset. (In the example above choices)
Yea I'd also recommend just rendering them out in the html if you have a finite number of entries. (If you don't you'll have to user another method).
You can hide them like this:
{% for form in spokenLanguageFormset %}
<fieldset class="languages-{{forloop.counter0 }} {% if spokenLanguageFormset.initial_forms|length < forloop.counter and forloop.counter != 1 %}hidden-form{% endif %}">
Then the js is really simple:
addItem: function(e){
e.preventDefault();
var maxForms = parseInt($(this).closest("fieldset").find("[name*='MAX_NUM_FORMS']").val(), 10);
var initialForms = parseInt($(this).closest("fieldset").find("[name*='INITIAL_FORMS']").val(), 10);
// check if we can add
if (initialForms < maxForms) {
$(this).closest("fieldset").find("fieldset:hidden").first().show();
if ($(this).closest("fieldset").find("fieldset:visible").length == maxForms ){
// here I'm just hiding my 'add' link
$(this).closest(".control-group").hide();
};
};
}
I can recommend django-dynamic-formsets for everyone who is just looking for a solution that works out of the box.
It replaced all the code I derived from the solutions proposed and gives some additional functionality, such as deleting forms or stylizing related buttons.

Multiple buttons on a flask webapp?

I'm making my first webapp using python and flask, it is a simple calculator but I'm currently stuck trying to use more than one button. At the beginning it was abe just to show a graph, here is the python code:
class FormulaForm(Form):
formula = StringField('formula')
graph = SubmitField('graph')
#app.route('/')
def calculate():
form = FormulaForm()
formula = request.args.get('formula','')
points = mp.make_points(formula,0,7)
comp = make_plot(points[0],points[1])
return render_template('index.html',the_script=comp[0],the_div=comp[1],form=form)
And here is the html code:
<form method="GET" action="">
<br />
{{ form.formula }}
<br />
{{ form.graph }}
</form>
So far so good. But I don't know how to add more functionality, for example I would like to add a button that shows the formula evaluated at some value x. I tried adding an extra inputfield and an extra button in the form, something like this:
class FormFormula(Form):
formula = StringField('formula')
graph = SubmitField('graph')
evaluate = StringField('evaluate_at')
evaluate = SubmitField('evaluate')
But then I don't know how to make the view handle two different actions.
I think I found a solution here but it only works when the method is "POST" and that makes the page reload which I don't want. So, is there a way to use multiple buttons in the same view?
#app.route('/start' ,methods=['POST'])
def stop():
"process done here"
return Something
Your app.py like this and and html file this
<script src="static/js/ajax.js"></script>
<script type="text/javascript" language="javascript">
$(document).ready(function() {
$("#start").click(function(event){
$.post(
"/start",
function(data) {
window.alert(data);
}
);
});
});
</script>
<button id ="start" type="button" value = "Load Data">Start</button>

Display progress bar while an image gallery is loading on django template

I want to display a progress bar (or loading icon) while a image gallery is loading on django template.
Image gallery is having a div in the template and for that div only the progress bar should appear.
Please refer to http://www.openstudio.fr/jquery/ as I am using this gallery
Your best bet is probably to do this through JavaScript instead of trying to do much of anything in Django. You would have Django populate your JavaScript, and then have the JavaScript do your progress bar. I'll use jQuery UI for the progressbar.
Django Template:
var portfolio = {
image_count = {{ images|length }},
images = [
{% for image in images %}{
'src': "{{ image.url }}",
'title': "{{ image.title }}"
}{% if not forloop.last %},{% endif %}{% endfor %}
]
};
JavaScript:
<script>
// This helps us keep track of the progress:
var count = 0;
var updateProgress = function() {
count++;
// Calculate the % we are through the images.
progress = parseInt((count / portfolio.image_count) * 100);
// Update the progressbar.
$("#progressbar").progressbar("value", progress);
// Check if we're done.
if (progress >= 100) {
$("#progressbar").hide();
// Fire up the multimedia portfolio, per the OP.
$('#multimedia-portfolio').multimedia_portfolio({width: 800});
$("#portfolio-cont").show();
}
}
$(function() {
// Initialize the progressbar at 0%.
$("#progressbar").progressbar({value:0});
// Hide the portfolio for now.
$('#portfolio-cont').hide();
if (portfolio) {
// Loop over the images.
for (var i=0; i<portfolio.image_count; i++) {
var image = portfolio.images[i];
// Create an image, a link, an li.
// Once the image is loaded, will call updateProgress.
var img = $('<img>').attr('src', image.src)
.attr('title', image.title)
.load(updateProgress);
var a = $("<a>").attr("href", image.src)
.addClass("thickbox");
$(a).append(img);
var li = $("<li>").append(a);
// Append the li to the ul.
$('#multimedia-portfolio').append(li);
}
}
});
</script>
This is also assuming that you have this(-ish) HTML:
<div id="progressbar"></div>
<div id="portfolio-cont"><ul id="multimedia-portfolio"></ul></div>
Hope that helps you at least get some direction.