Search code examples
ajaxdjangodjango-formsselectize.jsdjango-taggit

Add a custom user-facing form to Django app (uses selectize and taggit)


I'm fairly new to django, and I'm trying to figure out how to create a form using the taggit-selectize widget (or django-taggit). Everything I've found online refers to its use the admin page, but I want the tags to be user-facing and editable - much like the tags I create below this post. So far, I've determined that I need to create a form using a widget:

# models.py
from taggit_selectize.managers import TaggableManager
    tags = TaggableManager()

# forms.py
from taggit_selectize.widgets import TagSelectize
from .models import MyModel

class TagForm(forms.ModelForm):
    class Meta:
        model = MyModel
        fields = ('tags',)
        widgets = {'tags': TagSelectize(),}

but I can't figure out how to include this form in my template so that it appears beside my MyModel objects. Ideally, I guess I'd was expecting it to behave like django-fluent-comments, where I can just call {% render_comment_form for obj %} and call it a day.


Update

I've edited views (see below) and can now access the form in the template, but I can't seem to submit my tags (ideally this wouldn't trigger a redirect, either).

# views.py
from .forms import TagForm
def show_tags(request):
    return render(request, 'tags.html', {'tagform' : TagForm})

# tags.html        
<div>
{{ tagform.media }}
{{ tagform.as_p }}
</div>

Solution

  • So, I finally figured this out. It involves wrapping the tagform in <form> tags and catching the POST request. For the record, this is part of a project that involves using Haystack to return a list of results that I then want to tag. My views.py subclasses a SearchView rather than defining a function as I do here (show_tags()), and rather than one object per page I have multiple.

    For an object obj on the page, you have the following

    # views.py
    from .forms import TagForm
    from .models import MyModel
    from django.views.decorators.http import require_POST
    from django.views.decorators.csrf import csrf_exempt
    
    def show_tags(request):
    
        # Perhaps the request specifies the object, but
        # for simplicity's sake we just pick a specific model instance
        object = MyModel.objects.filter(pk = 123) 
        return render(request, 'tags.html', {'tagform' : TagForm,
                                             'obj' : MyModel})
    
    @require_POST
    @csrf_exempt
    def create_tag(request):
        # See javascript below for creation of POST request
        data = request.POST
        tag_text_raw = data.get('tag_data')
    
        # clean_tag() not shown, but it splits the serialized
        # tag_text_raw and returns a list of strings
        tag_text_clean = clean_tag(tag_text_raw)
    
        obj_pk = data.get('obj_pk')
    
        #save tags to document
        doc = DocInfo.objects.get(pk = obj_pk)
        doc.tags.add(*tag_text_clean)
    
        # not strictly necessary; mainly for logging
        response_data = {'tag_text': tag_text_clean,
                     'obj_pk': obj_pk
                     }
    
        return JsonResponse(response_data)
    

    So show_tags sends the information to the template with render, then the template has access to those objects. This is what didn't make sense to me initially.

    # tags.html (extends base.html)
    {% block scripts %}{{ block.super }}
        <script type="text/javascript" src="{{ STATIC_URL }}js/ajaxtag.js"></script>
      {{ tagform.media }}
    {% endblock %}    
    
    {{ obj.text }}
    <form method="post" action="create_tag/" id="tag-form-{{ obj.pk }}" name="tag-form-obj" data-object-id={{ obj.pk }}>
        {{ tagform.as_p }}
        <input type="submit" name ="tag-form-input" value="Add Tags" />
    </form>
    

    We can catch the POST request with javascript:

        #ajaxtag.js
        (function($)
        {
        // A stripped-down version of ajaxcomments.js from fluent_comments
        // See that file for further expansions
        $.fn.ready(function () {
            var tagform = $('form[name="tag-form-obj"]');
    
            if (tagform.length > 0) {
                // Detect last active input.
                // Submit if return is hit
                tagform.find(':input').focus(setActiveInput).mousedown(setActiveInput);
                tagform.submit(onTagFormSubmit);
            }
        });
    
        function onTagFormSubmit(event)
        {   
            event.preventDefault();  // prevents redirect
            var form = event.target;
    
            create_tag(form);
            return false;
        }
    
        function create_tag(form) 
        {
            console.log("create_tag is working!") // sanity check
            var $form = $(form);
            var tag_text = $form.serialize();
            var url = $form.attr('action');
            var obj_id = $form.attr('data-object-id')
    
            $.ajax({
                url : url,
                type: "POST",
                data: { tag_data: tag_text, obj_pk: obj_id},
    
                success: function (data) {
                    data;
                    console.log(data);
                    console.log('success');
                },
    
                error: function (xhr, errmsg, err) {
                    // Return error to console
                    console.log(xhr.status + ": " + xhr.responseText)
                }
            });
    
        }
    
        function setActiveInput() {
            active_input = this.name;
        }
    
        })(window.jQuery);
    

    Finally, urls.py sends the request back to create_tag()

    # urls.py
    from .views import create_tag
    ...
    url(r'^create_tag', create_tag, name = 'tag-form')
    ...