Annotations Tutorial: Wiring up back and frontend

In the last segment of our per-paragraph commenting app using Django, let us now wire the backend up with our frontend.

Our form was made long ago and we just finished coding the backend for this feature. Now all we have to do is plug the wires in right places and our entire app should be ready to use.

Django forms need a CSRF Token to prevent cross site scripting. This token can be retrieved by two methods: By using the {% csrf_token %} template tags when the form is being rendered from the backend. Or, the same CSRF token and also available in the Site cookie. Since we are not rendering any forms from the backend, the latter is our preferred method. So, we just use the function available on Django Documentation Page:

var getCookie = function(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;
      };

Now again, this CSRF token can be sent as a part of our actual payload, or in the request headers. Since sending it beforehand keeps my data formatting neat and clean, I'll do that. For that, we also need to tell that a CSRF token is not required for a few operations (like GET). To tell that:

  var csrfSafeMethod = function(method) {
    // these HTTP methods do not require CSRF protection
    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
  };

Now, we can replace our fixture we created before with the following Ajax call:

  data = {
    body: body,
    paragraph: id,
    content_type: "9",
    object_id: "1",
    privacy: "3",
    privacy_override:"0",
    shared_with: [],
    };

/* POST it now! */
//Form Validation goes here....
$.ajaxSetup({
  beforeSend: function(xhr, settings) {
    if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
      xhr.setRequestHeader("X-CSRFToken", annotations.csrftoken);
    }
  }
});
//Save Form Data........
$.ajax({
  cache: false,
  url : "http://127.0.0.1:8000/annotations/annotations/",
  type: "POST",
  dataType : "json",
  contentType: "application/json;",
  data : JSON.stringify(data),
  context : this,
  success : renderSingleAnnotation,
  error : function (xhRequest, ErrorText, thrownError) {
                //alert("Failed to process annotation correctly, please try again");
                console.log('xhRequest: ' + xhRequest + "\n");
                console.log('ErrorText: ' + ErrorText + "\n");
                console.log('thrownError: ' + thrownError + "\n");
              }
});        

Here, we create a dictionary of our data, which we will stringify before sending it to the backend. (Though I am told that .ajax() method does it implicitly, I haven't yet tried it and hence, I don't know; I know, I know, it doesn't take much time!).

Now, if the request is successful, the renderSingleAnnotation() method will be called. This is where I had trumped up a bit. If you remember, I had originally designed renderAnnotations() to take care of this, but then, while testing the functionality, which was failing obviously, I realized that when a single object is being returned, it is not a single element in the list; there is no list. Hence, to make our renderAnnotations() method more usable, this renderSingleAnnotation() just takes the incoming data, and puts it in a list before passing it to the renderAnnotation method.

Not quite! While annotations were posting well and good, and console said the response was also good, (except of course, we were not passing in the shared_with data, which would have caused the 400 BAD REQUEST response), the annotations made were not being made visible on the page but only on subsequent reload. A quick examination of our renderAnnotations function shows that we are operating on a variable called annotationCopy (which was initially the clone of our container). We originally had created a clone of our content container, and then replaced it with the formatted version after we were done processing. Now, that object does not exist. So, what we can do is, reset the variable and if it has been reset, reinitialize it in the function itself. This code segment:

  currentObject = annotationCopy.children('[data-section-id="'+data[i]['paragraph']+'"]');

does not do its job properly. We can fix it simply, by finding our element again, before we get here. So, before our for loop we add the following code:

if (annotationCopy == null){
  /* Create a fresh copy of the variable*/
  console.log('It is null. Make a new one');
  annotationCopy = $('#commentable-container');
  console.log(annotationCopy);
}

And when does it become NULL? Once the clone has been put into its right place.

loadAnnotations();
$('#commentable-container').replaceWith(annotationCopy);
annotationCopy = null;

Also put the variable back to sleep once we are done rendering new annotations, by setting it to null at the end of renderAnnotation.

Now, our annotations load just fine, and we are also able to create new one. Obviously, we haven't checked if the user is allowed to post them, but that can be easily implemented. So, it is homework for you for the time being. (look at the available URLs in REST and you'll find a link to fetch data about current user. If the user id is 0, then the user is not logged in, hence, disable posting of comments.) Also, our backend takes care of that for a while, it throws back a FORBIDDEN. Its just that error handling must be done on the front end.

With that done, we'd also like to give the user ability to delete his own annotations. Now, we could bind a delete method to the close button on each container (which we will just create) when it renders.

So, how would we like that? I think I'd like the cross button to appear on the side of an annotation when we hover on top of it. Though, it sounds good (and hence we will implement it), it is poor design in a way that it is not cross platform compliant. The mobile devices do not have a :hover option yet. And continuously visible cross buttons don't sound too appealing to me. So, we could put a button textual link and that hovering button also.

Add the following to the comments body in the script to add the delete method for annotations:

  '<div class="comments-control-box">'+
  '<span class="comments-control comments-delete">Delete</span>'+
  '<span class="comments-control">Shared with</span>'+
  '</div>'+
  '<span class="comments-delete glyphicon glyphicon-remove"></span>'+

Bind a delete method to each button. But for that, we'll need to keep its ID somewhere. I don't want to use a hidden span for this, and hence would use the data-xxx attribute:

  currentComment.find('.comments-delete').attr('data-comment-id', data[i]['id']);
  currentComment.find('.comments-delete').on('click', deleteAnnotation);

The deleteAnnotation() method is:

var deleteAnnotation = function(id){
  id= parseInt($(this).attr('data-comment-id'));
  console.log('Deleting annotation' + id);
  $.ajaxSetup({
    beforeSend: function(xhr, settings) {
      if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
        xhr.setRequestHeader("X-CSRFToken", annotations.csrftoken);
      }
    }
});

//Save Form Data........
$.ajax({
  cache: false,
  url : "http://127.0.0.1:8000/annotations/annotations/"+id+"/",
  type: "DELETE",
  dataType : "json",
  contentType: "application/json;",
  //data : JSON.stringify(data),
  context : this,
  success : removeAnnotation,
  error : function (xhRequest, ErrorText, thrownError) {
          //alert("Failed to process annotation correctly, please try again");
          console.log('xhRequest: ' + xhRequest + "\n");
          console.log('ErrorText: ' + ErrorText + "\n");
          console.log('thrownError: ' + thrownError + "\n");
          }
});  

/* Update annotation count on adjoining container */
console.log($(this).closest('.comments').find('.comments--toggle p'));
commentCount = parseInt($(this).closest('.comments').find('.comments--toggle p').text());
console.log('Comment Count now is '+ commentCount);
if((commentCount -1)==0){
  $(this).closest('.comments').find('.comments--toggle p').text('+');
}
else{
  $(this).closest('.comments').find('.comments--toggle p').text(commentCount -1);
}
/* Remove the annotation from flow */
$(this).closest('.comments-container-item').remove();
};

Here, we are going to remove it from the flow once Ajax call is made. In case some validation is to be run, it must be done before that. In case error occurs (which mustn't if the code is well tested), the comment will reappear on next refresh. What we could have done is saved the object in some global variable and when we received a successful response, do this removal there. But for now, we'll let it be this way. The removeAnnotation method doesn't do anything for now.

The styles:

.comments-control-box{
  position: relative;
  font-size: 0.9rem;
  }
.comments-delete{
  position: absolute;
  top: 0;
  right: 0;
  height: 20px;
  width: 20px;
  display: none;
  font-size: 1.2rem;
  }

This does it!

Creating annotations from a different user also works fine. They are neatly tucked in under the button. I hope your's is also working just the same. Here's a snapshot of how it looks on my screen.

Annotations App, finished screenshot

We'll for now, disallow the shared_with privacy option and only allow 'private and public' because writing code for the other things would involve a little more of backend. So, we'll save that for another day. (i.e. after we have put social networks into our apps). But you can always do it, and send us a pull request. The intimation to the author for now, can simply be done using signals (and sending an email to him). Later, we'll need a more elaborate dispatcher with tunable privacy and subscription settings.


Concluding remarks

With this, we come to the end of our per-paragraph commenting app tutorials. I hope you had fun. Since I am relatively a newbie into this stuff, I had fun, frustration and everything. But I'd like to take it further from here too, and make it more usable, pythonic and elegant. The source code is available under open source license on github and the package is available on PyPi where you can install it by simply calling pip install pi-annotations. I encourage you to report in bugs, fork the repository on github to develop features into it, enhance it (and send me a merge request). Also, I understand that the tutorial is fairly long, and there are many areas where we could improve upon. But we won't know until you tell us what you do not understand in what was written. So, you could send in your suggestions via email, on our Facebook page, in the Facebook group, on twitter and Google Plus, or use the annotations app here itself and let us know. We will improve upon it. :-)

blog comments powered by Disqus