So You Want To Make Your Own Extension (Part 2)

Alrighty, so you made it through Part 1! Congrats! By now, you should be able to play around with some kind of front-end prototype for your extension. Here comes the hard part: bridging to the back-end. Just a reminder, I’m going to be referring to the extension I wrote, which you can find here for a better reference.

For this, Reviewboard has something nice called the WebResourceAPI. In short it facilitates REST API calls to your back-end. Once you’ve got all your configurations set up, you basically have very little to do except call their API to do the stuff you want, like instantiating new objects, modifying them, or deleting them.

Models

But before we even get to that, we should make the models for our extension. For my case, I only needed one model, which is declared in models.py. It’s pretty simple, because I only needed to keep track of a few things for each checklist. Its methods are also pretty basic; each one implements each functionality that I want the checklist to do.

Here is my models.py file.

from django.contrib.auth.models import User
from django.db import models
from djblets.util.fields import JSONField
from reviewboard.reviews.models import ReviewRequest

class ReviewChecklist(models.Model):
    """A checklist is a list of items to keep track of during a review. """

    user = models.ForeignKey(User)
    review_request = models.ForeignKey(ReviewRequest)
    items_counter = models.IntegerField(default=0)
    checklist_items = JSONField()

    def add_item(self, item_description):
        ...

    def edit_item_desc(self, itemID, item_description):
        ...

    def toggle_item_status(self, itemID):
        ...

    def delete_item(self, itemID):
        ...

Just to avoid cluttering up the tutorial, you can view the full implementation here. As a design decision, I made the checklist items a JSON instead of creating a different class for it. It ended up simpler that way, but of course, you are free to make decisions about your extension’s models. You can make as many as you need.

Notice that my foreign keys for this model is the user id and review request id. This is why I needed {{user.pk}} and {{review_request.id}} from the template. I needed to be able to pass them in as keys when instantiating / accessing my checklist.

WebAPIResource

WebAPIResource Class

Create a python class that subclasses reviewboard.webapi.base.WebAPIResource. For my case, my file is called checklistResource.py. For now, we’ll add these lines:

from reviewboard.webapi.base import WebAPIResource

from checklist.models import ReviewChecklist

class ChecklistResource(WebAPIResource):
    name = 'checklist'
    model = ReviewChecklist
    uri_object_key = 'checklist_id'
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
    fields = {
        'id': {
            'type': int,
            'description': 'The numeric ID of the checklist review.'
        },
        'items_counter': {
            'type': int,
            'description': 'Number of items added in the checklist.'
        },
        'checklist_items': {
            'type': str,
            'description': 'Items in checklist.'
        }
    }

Remember to import your extension as well (and just to clarify, this file should be in the same directory as your models.py). Anyway, here’s a breakdown of what these attributes mean:

  1. name: This is needed for the API calls from the front-end. Your URL would end up being something like this — api/extensions/checklist.extension.Checklist/checklists/. The extension id is checklist.extension.Checklist. You don’t define this. It will be defined for you. The last part is the plural form of the name defined here. See, I had a bug where I thought the name itself had to be plural, and my url ended up having checklistss, and needless to say, it didn’t work. So the URL takes the name and adds an ‘s’ to it.
  2. model: This is the model that you want the API to work with.
  3. uri_object_key: If you’re familiar with the way Django does its regex for the url, you might have seen something like this before: r’^path/(?P<var>\d{3}), or something similar. Basically this allows values in the URL to be captured, but you have to specify that variable. This is what you put on the uri_object_key. For example, I put checklist_id. Basically, I’m telling Django to capture the id in the URL and assign that to checklist_id.
  4. allowed_methods: This defines which REST API calls you would allow through this class.
  5. fields: This dictionary defines the variables that would be sent back to the client side after each API call. So if you have something on the front-end that depends on the values of your object on the back-end, this would be a good place to send those values over.

By the way, these aren’t the only attributes you can define. Make sure you check out djblets.webapi.resources.py for the full documentation.

Next, we add methods.

For GET and DELETE, unless you need to do something really special, getting and deleting just require two functions to be implemented.

    def has_access_permissions(self, request, checklist, *args, **kwags):
        return checklist.user == request.user

    def has_delete_permissions(self, request, checklist, *args, **kwags):
        return checklist.user == request.user

These two functions should define the criteria for who / when a GET or a DELETE is acceptable. In my case, I only need to know whether the user who is performing these actions are the owner of the checklist. The actual fetching and deleting are handled by the ResourceAPI.

Now, onto more complicated methods. I’ll only show you how to do one, and hopefully you’ll have enough primer to sink your teeth on your own functions.

    @webapi_request_fields(
        required={
            'user_id': {
                'type': int,
                'description': 'The id of the user creating the checklist.'
            },
            'review_request_id': {
                'type': int,
                'description': 'The id of the review request.'
            }
        }
    )
    @webapi_login_required
    @webapi_check_local_site
    def create(self, request, user_id=None, *args, **kwargs):
        """Creates a new checklist."""
        user = User.objects.get(pk=user_id)
        review_request = resources.review_request.get_object(request,
                                                             args, **kwargs)

        new_checklist, _ = ReviewChecklist.objects.get_or_create(
            user=user,
            review_request=review_request)

        return 201, {self.item_result_key: new_checklist}

So this is a function that creates a new instance of your model. It is automatically mapped to the POST request, so it gets called whenever the front-end does a POST. Now let’s break this down.

@webapi_request_fields
This is a dictionary containing data from the client-side. If any of your function is expecting data from the client-side, you should have this decorator sitting on top of your function. You can see that it’s called required. You can also have a dictionary (inside the same decorator) called optional, and here you can write all the data that you might expect some of the time, but not others.

If you pass in data that’s not declared in either a required or optional dictionary, you would experience some 404 errors. If you try to call a function and you do not pass in one of the fields inside required, you would also get 404 errors. So make sure you define these correctly.

There are other types of decorators like @webapi_login_required and @webapi_check_local_site. These enforce certain things when the function is called. I have to admit I don’t know much about them, but there are a lot of them on hand, so you should check out which ones would ensure the integrity of your data. Don’t be afraid to ask which ones you need or what something means. That’s usually the best way to find out.

You can access these fields either through **kwargs, using the variable name as the key. Or you can include them in the function signature, like I did here for user_id.

Class Registration

Now, we need to connect this resource API to the model and the extension. So at the end of checklistResource.py, instantiate a new resource:

checklist_resource = ChecklistResource()

And then in your extension.py, register the model to your resource. So add these lines:

from checklist.checklistResource import checklist_resource

class Checklist(Extension):
    ...
    resources = [checklist_resource]

    def __init__(self, *args, **kwargs):
        ...
        register_resource_for_model(ReviewChecklist, checklist_resource)

    def shutdown(self, *args, **kwargs):
        unregister_resource_for_model(ReviewChecklist)

So, also notice that I added a new method called shutdown. It’s responsible for unregistering the resource.

BaseResource

There is a corresponding resource for the client side which works with the WebAPIResource. It’s called RB.BaseResource and you can find it in reviewboard/static/rb/js/resources/models/baseResourceModel.js. It’s a very big Backbone.js model, because it does a lot of things for you. But this also means that it might take you a while to learn how to use it. I’ll cover the main things here, but you should definitely still look into it, because I’m sure there would be functionalities you’d need which I won’t cover.

The good news is that after you extend RB.BaseResource, there’s only a few things you have to override.

Your BaseResource File

Create a Backbone.js model that extends RB.BaseResource. i called mine checklistAPI.js, because I’m using it as an interface for my main Backbone.js views. (I’ll show you later how to do this.) For now, we can start off like this:

Checklist.ChecklistAPI = RB.BaseResource.extend({
    rspNamespace: 'checklist',

     url: function () {
        var baseURL = SITE_ROOT + 'api/extensions/checklist.extension.Checklist/checklists/';
        return this.isNew() ? baseURL : (baseURL + this.id + '/');
    },
});

Alright. So in this little tidbit, we define our rspNamespace. This is very important, so don’t forget to define it. I named mine like my extension.

The url function builds the URL that we’re going to be using to get to the server-side. The API Resource normally does this automatically for RB apps, but at the time of writing, this wasn’t yet supported for extensions. So this function just defines the URL that I use, which must follow the pattern api/extensions/extensionID/extensionName + ‘s’. So remember the name we had to define in our WebAPIResource? This is where it’s needed.

Now we’ll need to add a few more attributes and functions.

parseResourceData

    parseResourceData: function(rsp) {
        this.checklist_items = rsp.checklist_items;
        return {
            id: rsp.id,
            links: rsp.links,
            checklist_item_id: rsp.items_counter,
            loaded: true
        };
    }

This function takes the data (denoted by rsp here) you return from your server side, and parses it. Here, I return it as a JSON to my Backbone.js view in a form I can work with. I also assign the checklist_items variable to the checklist_items that was given back to me. Basically this is the function where you can initially get the data the server returns to you, so this is where you get to handle it in any way you like.

toJSON

    toJSON: function () {
        return {
            user_id: this.get('user_id') || undefined,
            review_request_id: this.get('review_request_id') || undefined,
            checklist_item_id: this.get('checklist_item_id') || undefined,
            item_description: this.get('item_description') || undefined,
            toggle: this.get('toggle') || undefined
        };
    },

This function organizes the data you want to send into a JSON that the BaseResource will send over to the back-end. So, remember those required or optional dictionaries we created inside the @webapi_request_fields decorator? The variables here should correspond to the variables there. So, for example, in our create function, we needed user_id and review_request_id. Make sure that the corresponding variables here is not NULL when you do a POST. (I’ll be showing you how to do that shortly.)  Here, I’m assigning them to the values of the class attributes. I set the class attributes in my Backbone.js view.

Just so you know, it’s entirely up to you how you extend BaseResource, whether as a model directly related to your extension, or as an interface. I just saw that treating BaseResource as an interface from my view was a better choice for me. It’s really your call.

How to Use the BaseResource

So let’s take a look at my view. My view is responsible for making the API calls. For example, suppose I want to add a new checklist item to the checklist. Here is my function for it:

    /* Add the new item to the backend. */
    addItemDB: function (event) {
        if (event.keyCode === 13) {
            var item_desc = $('input[name=checklist_itemDesc]').val();
            $('input[name=checklist_itemDesc]').val('');

            if (item_desc === '') {
                alert("Please type a description");
                return;
            }

            this.checklistAPI.set({
                item_description: item_desc,
                checklist_item_id: null,
            });

            var self = this;
            var saveOptions = {
                success: function (data) {
                    var item_id = data.attributes.checklist_item_id;
                    self.addItem(item_id, item_desc, false);
                },
            };

            this.checklistAPI.save(saveOptions, this);
        }
    },

Let’s break this down.

L12 – L15
Here, I refer to a variable called checklistAPIchecklistAPI is just an instance of ChecklistAPI, and I have made it an attribute of my view. I am setting the value of checklistAPI’s instance variables. This is so that when toJSON is called, item_description and checklist_item_id are set properly. (Note that I purposely set checklist_item_id to null; this is to let the server side know that I’m adding a new item, rather than editing the description of an already existing item. Again, designing your API calls is up to you. This is merely procedure stuff.)

L19 – L23
Here we define a saveOptions variable, which we will pass in to our API call. It contains functions that defines what we expect would be done upon completion of the API call. For this example, I have defined a ‘success’ function. The back-end sends me the item id for the new item, so I use that to create my front-end Backbone.js model for the new item. You may also define an error function, which is useful for debugging.

L26
This line is virtually the only line you’ll ever need to fire off your POST or PUT call. For a delete, you can call destroy rather than save. You pass your saveOptions variable, along with the object you want to be referred to as ‘this’ in your success / error functions. In this case, I want the success function to operate on my view whenever it uses ‘this’, so I just pass in ‘this’, because my current object is the view.

…And, that’s about it!

So, so, so. This would be as far as this tutorial would cover. I believe I covered the most major points about using the WebAPIResource and the BaseResource. If there’s anything that’s not clear to you, feel free to leave comments, and I’ll try to help as much as I can; re-explain things in a different way, or clarify some stuff that might not have crossed as well as I hoped. But more importantly, check out the documentation, and never forget to ask for help!! When I was going through this, the help I got from the mentors were so valuable, and it’s mostly because of them that this tutorial is up in the first place.

So I’ll leave you guys with that, and good luck with your extensions!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s