Difference between revisions of "Extensible Calendar"

From CSE330 Wiki
Jump to navigationJump to search
m (Adding "Former Content" category tag)
 
Line 448: Line 448:
  
 
[[Category:Module 6]]
 
[[Category:Module 6]]
 +
[[Category:Former Content]]

Latest revision as of 08:08, 25 August 2014

This page documents a tutorial for using the Extensible Calendar Pro API with Django as the back end.

REST

The Extensible Calendar is going to connect to your server via AJAX calls. These calls conform to a web application paradigm known as REST, or REpresentational State Transfer. In particular, the application will automatically send requests with the following HTTP methods:

  • GET when it wants to read something
  • POST when it wants to create something
  • PUT when it wants to change something
  • DELETE when it wants to remove something

You recognize GET and POST from Module 2. All along, there were actually more than just these two request methods. REST simply takes advantage of all of them.

Note: In practice, since older browsers do not support the additional HTTP request methods required by REST, the actual implementation of PUT and DELETE will most likely be POST with an additional header defining the request type. The frameworks takes care of this difference under the hood.

The Back End

First, we will set up the back end.

Creating the App with Source Control

Start by making a new Django project and app.

$ django-admin.py startproject cse330calendar
$ cd cse330calendar
$ python manage.py startapp cal

Right now would be a good time to set up your IDE and source control. Create a new Komodo project in the "cse330calendar" directory, and set up a repo on the "cse330calendar" in SourceTree. Make frequent commits after each step of the process.

Initial Configuration

Go into settings.py and configure your database.

Enable the admin panel, which requires editing both settings.py and urls.py. Add the cal application to the admin panel by creating cal/admin.py with the following content:

from django.contrib import admin

# We will create these models in the next step
from cal.models import Cal, Event

admin.site.register(Cal)
admin.site.register(Event)

Finally, add "cal" to the INSTALLED_APPS list in settings.py.

Creating the Models

Set up the models required for the Extensible Calendar. We have already done this work for you. Copy the following code into your cal/models.py file.

from django.db import models

# Extensible wants two models: a "calendar" model and an "event" model.
# The "calendar" model represents a collection of events.  Note that the end
# application supports multiple calendars in the same view, with the events
# in each calendar having a different background color.
# 
# First we will define our calendar model, `Cal`, and then we will define
# our event model, `Event`.

class Cal(models.Model):
	# Title of the calendar
	title = models.CharField(max_length=50)

	# Text description of the calendar
	description = models.CharField(max_length=200)

	 # Color ID (1-32)
	color = models.IntegerField(default=1)

	# Boolean for whether the calendar is hidden by default
	hidden = models.BooleanField(default=False)

class Event(models.Model):
	# Declare a one-to-many relationship with Cal
	cal = models.ForeignKey(Cal)

	# Title of the event
	title = models.CharField(max_length=50)

	# DateTime of event start
	start = models.DateTimeField()

	# DateTime of event end
	end = models.DateTimeField()

	# Additional information that can be associated with an event:
	loc = models.CharField(max_length=50) # Location
	notes = models.CharField(max_length=200) # Notes
	url = models.CharField(max_length=100) # URL
	ad = models.BooleanField(default=False) # Is this an all-day event
	rem = models.CharField(max_length=200) # Reminder

Finally, sync the db. After you do this, run the Django server and play around with the admin panel to make sure everything is in working order.

$ python manage.py syncdb

Tastypie

Django does not come with REST support out of the box, so we need to install an additional framework. I have so far had success with Tastypie, and I will be using Tastypie throughout this tutorial.

Start by installing Tastypie. There are instructions on the above link.

Your cal/api.py file can contain simply:

from tastypie.resources import ModelResource
from tastypie.paginator import Paginator
from cal.models import Cal, Event
from tastypie.fields import ToOneField

class CalResource(ModelResource):
	class Meta:
		queryset = Cal.objects.all()
		paginator_class = Paginator

class EventResource(ModelResource):
	cid = ToOneField(CalResource, "cal", full=True)
	class Meta:
		queryset = Event.objects.all()
		paginator_class = Paginator
		always_return_data = True
	def dehydrate_cid(self, bundle):
		return bundle.obj.cal.id

In the above snippet, we also are installing the Paginator utility. The Extensible Calendar requires that your data have pagination.

You will be modifying this file later on in this tutorial.

I also recommend making JSON the default output format for Tastypie. To do this, add the following line to your settings.py file:

TASTYPIE_DEFAULT_FORMATS = ['json']

While you're inside settings.py, go ahead and add the following line as well. It may save you headaches down the road.

APPEND_SLASH=True
TASTYPIE_ALLOW_MISSING_SLASH=False

Routing

Let's create an empty index.html for our site. We will be modifying it later. Create the file in cal/templates/cal/index.html. Then add it to cal/views.py:

from django.shortcuts import render

def index(request):
    return render(request, 'cal/index.html', {})

Also add it to cal/urls.py:

from django.conf.urls import patterns, url

from cal import views

urlpatterns = patterns('',
    url(r'^$', views.index, name='index')
)

Finally, add cal to your core urls.py file, which by now should look something like this:

from django.conf.urls import patterns, include, url
from cal.api import CalResource, EventResource

# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()

# RESTful setup:
cal_resource = CalResource()
event_resource = EventResource()

urlpatterns = patterns('',
    # Admin page
    url(r'^admin/', include(admin.site.urls)),

    # Templates
    url(r'^', include('cal.urls')),

    # RESTful URLs
    url(r'^', include(cal_resource.urls)),
    url(r'^', include(event_resource.urls)),
)

Try launching http://localhost:8000/cal/ in your browser. If everything is working properly, you should see a JSON representation of the data in your calendar.

Additional Work on the Back End

In the above example, we have implemented a read-only API for your calendar. You will need to use Django Auth to add users and to only reveal those calendars associated with a user. You will also need to implement the writing site of Tastypie. Almost all of these steps are in the Tastypie documentation.

The Front End

So far, all of our work has been setting up our database, models, and server API. Now, we need to set up the front end that takes the JSON data and displays it in a GUI for the end user.

Installing Extensible

Start by downloading the Extensible library from the official web site. Save the download in cal/static/vendor/extensible.

Extensible Config File

Copy the file at cal/static/vendor/extensible/Extensible-config.js into the cal/static/cal directory. Make the following changes:

  1. Change the Ext JS version on line 59 from 4.2.0 to 4.1.1
  2. Comment out the line that includes the examples.js file (around line 179)

The Core Extensible Code

The examples on the Extensible documentation are helpful, but if you have never used Ext.JS before, it is easy to get lost. We have therefore written an index.html file and an app.js file to get you started.

Put the following code in cal/templates/cal/index.html:

<!DOCTYPE html>
{% load staticfiles %}
<html>
<head>
	<title>Calendar</title>
	<script type="text/javascript" src="{% static 'cal/Extensible-config.js' %}"></script>
	<script type="text/javascript" src="{% static 'cal/app.js' %}"></script>
</head>
<body>

<div id="calbox" style="width:600px; height:400px;"></div>
<span id="app-msg" class="x-hidden"></span>

</body>
</html>

Put the following code in cal/static/cal/app.js:

// Demo Extensible Calendar App
// Created for CSE 330S by Shane Carr
// Washington University in St. Louis


// Ext.JS wants you to use `Ext.define` when making a custom component.
Ext.define("CSE330.Calendar", {

	// List the classes we use inside this component.  This enables Ext.JS to load
	// the files containing the definitions for these classes more efficiently.
	requires: [],

	// Main code for our component
	constructor: function(){
		// Set up the calendar store
		this.calendarStore = Ext.create("Extensible.calendar.data.MemoryCalendarStore", {
			autoLoad: true,
			storeId: "Calendars",
			proxy: {
				type: "rest",
				url: "/cal/",
				startParam: "offset",
				reader: {
					type: "json",
					root: "objects"
				},
				writer: {
					type: "json",
					nameProperty: "mapping"
				}
			}
		});

		// Set up the event store
		this.eventStore = Ext.create("Extensible.calendar.data.MemoryEventStore", {
			autoLoad: true,
			storeId: "Events",
			proxy: {
				type: "rest",
				url: "/event/",
				startParam: "offset",
				reader: {
					type: "json",
					root: "objects",
				},
				writer: {
					type: "json",
					nameProperty: "mapping"
				}
			}
		});

		// Set up the panel (view)
		this.container = Ext.create('Extensible.calendar.CalendarPanel', {
			renderTo: Ext.get("calbox"),
			title: "My CSE330 Calendar",
			width: "100%",
			height: "100%",
			id: "ext-calendar-main",
			eventStore: this.eventStore,
			calendarStore: this.calendarStore,
			monthViewCfg: {
				showHeader: true // show the days of the week
			}
		});

	}
});

Ext.onReady(function(){
	Ext.create("CSE330.Calendar");
});

You should be able to load http://localhost:8000/ and see an empty calendar with all events you have thusfar put in your database. Exciting!

Additional Work on the Front End

Again, right now you have a read-only calendar without any sort of authentication. You need to tie these two pieces together.

If you look at the web inspector, you can see the requests that Extensible sends to your server when you try to create or modify an event using the front-end GUI. You need to make sure that your server and Extensible work together, both sending and receiving compatible formats.

You can implement log in and log out over AJAX via the Tastypie API. There are examples of how to do this in the documentation.

Help with Debugging

Wrapping your head around Extensible, Django-Tastypie, and how to make the two frameworks talk to each other is a daunting task. The goal of this section is to help you learn how to debug this process.

Tutorial for Debugging the Create Event Mechanism

Let's first take a look at what happens when you try to add a new event via the Extensible GUI. When you click somewhere on the calendar, a little dialog opens, and in it you can specify the details of your event. However, when you press "Save", you see the following error in the console:

Extensible-Debugging-Unauthorized.png

Whoopsy daisy, we got a 401 Unauthorized from the server! This means that Django-Tastypie thinks that we don't have enough permissions to edit this resource. We need to tell Tastypie that we do indeed have such permissions.

Let's temporarily open up the event to be globally editable. Refer to this section of the Tastypie guide for an example of how to do this. Try figuring it out on your own before reading on.

You may have figured out that we need to specify these changes inside the cal/api.py file. In particular, if we add the following line to the Meta of the EventResource class, we in effect open up the resource to be globally editable.

# Add to the Event meta
authorization = Authorization()

# don't forget to put an import statement at the top of your file!
#      from tastypie.authorization import Authorization

Great! Now let's try sending our request again. You'll see that you get a 400 Bad Request error. That sounds scary! What's happening?

Let's head on over to the "Network" panel of the web inspector. Scroll to the bottom of the page to find the latest network request. Click it, and then choose the "Response" tab. You'll see something like this:

Extensible-Debugging-Response-Error.png

Hey, look! There's an error that we can fix. Let's look at it in more detail.

The 'cid' field was given data that was not a URI, not a dictionary-alike and does not have a 'pk' attribute: 1.

It sounds like Tastypie doesn't like whatever we're sending as the cid field. Let's go check on what we're actually sending. To do this, click on the "Headers" tab in the web inspector, and scroll down to "Request Payload".

Extensible-Debugging-Request-Payload.png

So we're clearly sending an integer calendar ID in the cid field. Why wouldn't Tastypie like this? I don't know; let's google the error!

Note that when we google something, we need to take specific field names out of our search. In our case, this is the "cid" field name as well as the value of "1". With that in mind, here are the hits:

Extensible-Debugging-Google-Hits.png

Look at that! We're clearly not the first people to run into this error.

The first Stack Overflow question looks verbose. Let's try the second one.

Hey, you find something that might provide you a clue!

Extensible-Debugging-StackOverflow-Answer.png

So we need to make our data field resemble one of those two options: either a dictionary with a "pk" field or a resource URL.

But how can we change the data format? We could do it from the Extensible side, or we could do it from the Tastypie side. Since we have greater control over Tastypie than Extensible, let's do it on the Tastypie side.

The Resources guide for Tastypie seems like a reasonable place to start. Read it and try to find the method that enables you to modify incoming data before Tastypie puts it into the model.

If you pinpointed the hydrate_FOO method, you were right! We simply need to define this method to transform the incoming data into the format that Tastypie wants.

Based on the "simple example" under hydrate_FOO, the following definition seems reasonable.

# Part of the EventResource class

	def hydrate_cid(self, bundle):
		bundle.data["cid"] = "/cal/%d/" % bundle.data["cid"]
		return bundle

Here, we are transforming the incoming cid number to a resource URL. For example, 3 will be transformed into /cal/3/, the format that Tastypie wants according to the Stack Overflow post.

Okay, great! Let's save and see where we stand. Try creating an event through the GUI.

Hey, it looks like something happened! We received a 201 request back from the server! Let's take a peek at our database. Enter a Django shell and enter the following commands.

# Run these commands inside a Django shell
from cal.models import Cal, Event
map(lambda v: "%d %s" % (v.id, v.title), Event.objects.all())

The first line imports your modules into the shell, and the second line queries them via ORM and displays a list of the events with an ID and a title.

What do you see that's interesting? Hey, the "Banana Event" that I just added has ID 0! That's not cool. How do we fix it?

You may remember from the Django tutorial that Django will automatically assign the next available ID if you leave the id field empty in the new model. So, let's go ahead and transform all 0 IDs in Tastypie to None.

Try writing the necessary hydrate_id method. You should get something like the following:

# Part of the EventResource class

	def hydrate_id(self, bundle):
		if bundle.data["id"] == 0:
			bundle.data["id"] = None
		return bundle

Save and try again. What do you know, it works now! Each new event is now assigned a unique ID.

Next Steps

So hopefully you now have the knowledge you need to complete the module. In summary, when you need help, do the following:

  1. Look at the web inspector console
  2. Look at the network activity
  3. Look at the server log
  4. When you find an error that looks useful, google it (remove application-specific terms)
  5. When fixing the problem, refer to documentation
  6. When the problem is fixed, commit your code!

If you still need help, ask in class or come to office hours.