2

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

Session Editor

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 ?

2 Answers 2

6

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

Sign up to request clarification or add additional context in comments.

Comments

1

This plugin would help you get the auto-complete functionality you're looking for:

https://github.com/millioner/django-ajax-select

1 Comment

+1 for being the first one (and only one for now) to answer :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.