yourlabs / django-autocomplete-light

A fresh approach to autocomplete implementations, specially for Django. Status: v4 alpha, v3 stable, v2 & v1 deprecated.
https://django-autocomplete-light.readthedocs.io
MIT License
1.79k stars 467 forks source link

AJAX submission for multiple select, stuck on solutions #901

Open johncarmack1984 opened 7 years ago

johncarmack1984 commented 7 years ago

Hey there y'all! Here's what I've got.

Running an autocomplete search on a database table of tickers and stock names using a multiple select via django-autocomplete-light.

My aim is to submit the form via AJAX & clear the text box. Instead, what happens is that the form goes through (albeit with some workarounds that are kludgy, see below) but after submission I can't figure out how to get the form to clear. Clearing the form is the real problem, but the submission is wonky too so if there's an obvious solution to that I'm all ears.

Thus far efforts to fix the problem have been focused around getting allowClear: true and data-allow-clear="true"data-allow-clear="true" into all the right places but so far I haven't figured it out. The solution may be totally unrelated, this is my first attempt at modifying AJAX.

AJAX code from this tutorial : Coding for Entrepeneurs: AJAXify Django Forms

I got the AJAX code working perfectly on that tutorial (plain text fields), and I got the autocomplete POST data going through just fine, but combining the form with AJAX, is preventing the multiple-select box from clearing on submit, even when allowClear is hard-set to true in select2.js.

Code:

urls.py

from django.conf.urls import url
from quandl_wiki import views

urlpatterns = [
    url(
        r'^$',
        views.Ticker_name_pair_View.as_view(),
        name='ticker_name_pair_form',
    ),
    url(
        r'^ticker_name_pair_form_success', 
        views.ticker_name_pair_form_success, 
        name='ticker_name_pair_form_success'),
]

models.py

from django.db import models
from django.utils.encoding import python_2_unicode_compatible

# Create your models here.

@python_2_unicode_compatible
class Ticker_name_pair(models.Model):
    ticker = models.CharField(max_length=10)
    name = models.CharField(max_length=1000)

    test = models.ManyToManyField(
        'self',
        blank=True,
        related_name='related_test_models'
    )

    for_inline = models.ForeignKey(
        'self',
        null=True,
        blank=True,
        related_name='inline_test_models'
    )

    def __str__(self):
        return str(self.ticker) + " : " + str(self.name)

forms.py

from dal import autocomplete
from django import forms
from .models import Ticker_name_pair

class Ticker_name_pair_Form(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        # first call parent's constructor
        super(Ticker_name_pair_Form, self).__init__(*args, **kwargs)
        # there's a `fields` property now
        self.fields['add_to_portfolio'].required = False

    add_to_portfolio = forms.ModelChoiceField(
        empty_label=None,
        queryset=Ticker_name_pair.objects.all().order_by('id'),
        #widget = autocomplete.Select2Multiple(
        widget = autocomplete.Select2Multiple(
            'quandl-wiki-search-autocomplete',
            attrs={
                'class':'sidebar-search col span-2-of-3',
            }
        ),
        label=''
    )

    class Meta:
        model = Ticker_name_pair
        fields = ()

mixins.py

from django.http import JsonResponse

class AjaxFormMixin(object):
    def form_invalid(self, form):
        response = super(AjaxFormMixin, self).form_invalid(form)
        if self.request.is_ajax():
            return JsonResponse(form.errors, status=400)
        else:
            return response

    def form_valid(self, form):
        response = super(AjaxFormMixin, self).form_valid(form)
        if self.request.is_ajax():
            print(form.cleaned_data)
            data = {
                'message': "Successfully submitted form data."
            }
            return JsonResponse(data)
        else:
            return response

views.py

from django.shortcuts import render
from dal import autocomplete
from django import forms
from .forms import Ticker_name_pair_Form
from django.db.models import Q
from .mixins import AjaxFormMixin

try:
    from django.urls import reverse_lazy
except ImportError:
    from django.core.urlresolvers import reverse_lazy
from django.views import generic

class Ticker_name_pair_Autocomplete(autocomplete.Select2QuerySetView):
    def get_queryset(self):
        # Don't forget to filter out results depending on the visitor!
        # I have commented out the authetication requirement while asking for help with this :)
        #if not self.request.user.is_authenticated():
        #    return Ticker_name_pair.objects.none()

        qs = Ticker_name_pair.objects.all().order_by('id')

        if self.q:
            qs = qs.filter(Q(ticker__icontains=self.q) | Q(name__icontains=self.q))

        return qs

class Ticker_name_pair_View(AjaxFormMixin, generic.UpdateView):
    model = Ticker_name_pair
    form_class = Ticker_name_pair_Form
    template_name = 'quandl_wiki/ticker_name_pair_form.html'
    success_url = reverse_lazy('ticker_name_pair_form_success')
    context_object_name = 'post_data'

    def get_object(self):
        return Ticker_name_pair.objects.first()

    def clean_add_to_portfolio(self):
        add_to_portfolio = self.cleaned_data.get('add_to_portfolio')
        if add_to_portfolio == ['']:
            raise forms.ValidationError("No stocks selected!")
        return add_to_portfolio

base.html

<!DOCTYPE html>
{% load staticfiles %}
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{% block title %}{% endblock %}</title>
    <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
    <link rel="stylesheet" type="text/css" href="{% static 'vendors/css/normalize.css' %}">
    <link rel="stylesheet" type="text/css" href="{% static 'vendors/css/grid.css' %}">
    <link rel="stylesheet" type="text/css" href="{% static 'vendors/css/ionicons.css' %}">
    <link href="https://fonts.googleapis.com/css?family=Raleway:200,300,300i,500" rel="stylesheet">
    <link rel="stylesheet" type="text/css" href="{% static 'resources/css/style.css' %}">
    <link rel="stylesheet" type="text/css" href="{% static 'resources/css/queries.css' %}">
    <link rel="stylesheet" type="text/css" href="{% static 'vendors/css/animate.css' %}">
    <link rel="stylesheet" type="text/css" href="{% static 'vendors/css/magnific-popup.css' %}">
    <link rel="apple-touch-icon" sizes="180x180" href="{% static 'resources/favicons/apple-touch-icon.png' %}">
    <link rel="icon" type="image/png" sizes="32x32" href="{% static 'resources/favicons/favicon-32x32.png' %}">
    <link rel="icon" type="image/png" sizes="16x16" href="{% static 'resources/favicons/favicon-16x16.png' %}">
    <link rel="manifest" href="{% static 'resources/favicons/manifest.json' %}">
    <link rel="mask-icon" href="{% static 'resources/favicons/safari-pinned-tab.svg' %}" color="#5bbad5">
    <meta name="theme-color" content="#ffffff">
  </head>
  <body>
    <div class="container">

      {% block content %}{% endblock %}

    </div>

  </body>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js" integrity="sha384-vBWWzlZJ8ea9aCX4pEW3rVHjgjt7zpkNpZk+02D9phzyeVkE+jo0ieGizqPLForn" crossorigin="anonymous"></script>
  <script src="{% static 'vendors/js/clamp.js' %}"></script>
  <script src="{% static 'resources/js/scripts.js' %}"></script>
  <script src="{% static 'vendors/js/jquery.magnific-popup.min.js' %}"></script>
  <script src="{% static 'vendors/js/facebook.js' %}"></script>
  <script src="{% static 'vendors/js/pym.v1.min.js' %}"></script>
  <script>
  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 = jQuery.trim(cookies[i]);
          // 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 csrftoken = getCookie('csrftoken');

  function csrfSafeMethod(method) {
    // these HTTP methods do not require CSRF protection
    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
  }
  $.ajaxSetup({
    beforeSend: function(xhr, settings) {
        if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
            xhr.setRequestHeader("X-CSRFToken", csrftoken);
        }
    }
  });
  </script>
  {% block javascript %}
  {% endblock %}
  {% block footer %}
  {% endblock footer %}
</html>

ticker_name_pair_form.html

{% extends 'base.html' %}

{% block content %}
  {% include "../splash-nav.html" %}
    <div class="row">
      <form class="my-ajax-form" method="POST" data-url='{{ request.build_absolute_uri|safe }}' data-allow-clear="true" action="/">
          {% csrf_token %}
          {{ form.as_table }}
          <input type="submit" style="float: right;" class="sidebar-button col span-1-of-3" value="+" />
      </form>
    </div>
    <br><br>
    <div class="row">
      {{ request.body }}
    </div>
{% endblock %}

{% block javascript %}
<script>
$(document).ready(function(){
    var $myForm = $('.my-ajax-form')
    $myForm.submit(function(event){
        event.preventDefault()
        var $formData = {'formData': $(this).serialize()}
        var $thisURL = $myForm.attr('data-url') || window.location.href // or set your own url
        console.log($formData)
        $.ajax({
            method: "POST",
            url: $thisURL,
            data: $formData,
            success: handleFormSuccess,
            error: handleFormError,
        })
    })

    function handleFormSuccess(data, textStatus, jqXHR){
        console.log(data)
        console.log(textStatus)
        console.log(jqXHR)
        $myForm[0].reset(); // reset form data */
        /*$myForm.each(function () { //added a each loop here
        $(this)._handleClear();
        });*/
        /* $myForm[0].trigger('unselect',$myForm[0]) */
        console.log('Reset command sent')
    }

    function handleFormError(jqXHR, textStatus, errorThrown){
        console.log(jqXHR)
        console.log(textStatus)
        console.log(errorThrown)
    }
})
</script>
{% endblock %}

{% block footer %}
{{ form.media }}
{% endblock %}

Console output of $python manage.py runserver

Page load

[21/Aug/2017 08:23:03] "GET /quandl_wiki/ HTTP/1.1" 200 8425

Click on select box, enabling drop down select

[21/Aug/2017 08:24:13] "GET /quandl-wiki-search-autocomplete/?q=tsla HTTP/1.1" 200 96

type search for TSLA, select TSLA : Tesla Motors, Inc.

``

submit form

{'add_to_portfolio': None}
[21/Aug/2017 08:25:55] "POST /quandl_wiki/ HTTP/1.1" 200 48

^ now the above is a problem, because I suspect it means I'm not getting the actual data on the server end even though the client end (google developer tools) says the form is successfully being sent. So I changed forms.py to require the 'add_to_portfolio' field and now I'm getting a 400 error. [21/Aug/2017 08:31:40] "POST /quandl_wiki/ HTTP/1.1" 400 49

So the form isn't going through, and I can't figure out what to change where to make it work. Further, even when the client thinks it went through, the box doesn't clear. I've scoured the docs as best I know how but am, for now, stuck. Any ideas?

App is at http://dev.stockpicker.io/quandl_wiki

jpic commented 7 years ago

Have you tried emptying the select value and triggering a change ?

jpic commented 7 years ago

I couldn't sign up on your demo: null value in column "portfolio" violates not-null constraint DETAIL: Failing row contains (3, null, 11).

Can't see your demo as anonymous: https://dev.stockpicker.io/quandl_wiki/ 'AnonymousUser' object has no attribute 'profile'