Models, controllers and views

Preview:

Citation preview

The Three line MVC application

and introducing Giotto

Table on contents

First part: Why high level code organization schemes are important

Second part: All about MVC

Third Part: Giotto!

My experiences

1. Started web development in 20072. Wrote code without any kind of architectural pattern at all3. This was very frustrating, but I didn't know any better4. Realized it's taking too long to get stuff fixed and its not fun5. Learned first hand that not using MVC is a pain

Non-MVC code

Flightlogg.in'

View: HTML/JavascriptController: Standard HTTP GET/POSTModel: Flight Storage and flight data analysis.

Was originally PHP (non-MVC)Now is django (mostly MVC)

Why?

1. Flexibility 2. Organization

Imagine...

1 app1 django view, 9000 lines of code

Imagine...

We want to fix this. Refactor!!

step 1: 1000 functions, 9 lines each

Imagine...

step 2: 100 classes, 10 functions each

And then...

App Models class class Views class class Controllers class class

Overview

1. Models - The application2. Controllers - The interface a. ties your application (model) to the outside world3. Views - The presentation of the output to the user

Models

1. Usually the biggest part of your application2. Business Logic3. Not just database tables4. Should be completely controller independent

Views

1. All about formatting output from model.2. Templates/HTML3. Serializers4. Should be independent of any controllers (templates are portable)

Controllers

1. How the data gets to and from the user2. Apache, nginx, varnish, django middleware, mod_wsgi are all technically part of the controller.3. What goes into my controller? a. High level Model code b. High level View code c. final interface level operations

An example controller

def new_flight_controller(request): total_time = request.POST['total_time'] landings = request.POST['landings'] user = request.user flight = Flight.new_flight(user, total_time, landings) try: flight.save() except: raise HttpResponseError("invalid flight data") return render_to_response( context={'flights': Flight.objects.filter(request.user)} template='flights.html')

An example controller

def new_flight_controller(request): total_time = request.POST['total_time'] landings = request.POST['landings'] user = request.user flight = Flight.new_flight(user, total_time, landings) try: flight.save() except: raise HttpResponseError("invalid flight data") return render_to_response( context={'flights': Flight.objects.filter(request.user)} template='flights.html')

Interface operations

High level model code

High level view code

Another example controllerclass NewFlightCommand(BaseCommand): option_list = BaseCommand.option_list + ( make_option('--total_time', '-t', dest='total_time'), make_option('--landings', '-l', dest='landings'), make_option('--user', '-u', dest='user') ) def handle(self, *args, **options): flight = Flight.new_flight(**options) try: flight.save() except: print "Invalid flight data" print "Flight saved!"

Don't put non-controller code inside a controller!def to_decimal(input): """ >>> to_decimal('3:30') 3.5 >>> to_decimal('3.5') 3.5 >>> to_decimal('3:12') 3.2 """

This is not a controller function! Not high level model code, not high level view code, and not interface specific!!!

This code is not controller code!

def controller(request): total_time = to_decimal(request.GET['total_time']) # bad! landings = request.GET['landings'] user = request.user flight = Flight.new_flight(user, total_time, landings) try: flight.save() except: raise HttpResponseError("invalid flight data") return render_to_response( context={'flights': Flight.objects.filter(request.user)} template='flights.html')

The three line MVC application!

def mini_controller(request): return {total_time: request.GET['total_time'], landings: request.GET['landings'], user: request.user}

def new_flight(request): args = mini_controller(request) flight = Flight.new_flight(*args).save() return render_to_response('view_flight.html', {'flight': flight})

The MVC color-wheelModel

ViewController

ModelViews / Forms

Context processors

Middleware

ModelViews

1. Projection of a Model (subclass) intended for use in a set of views2. Atomic elements that should not hinder the 'real' view's ability to do its job.

ModelViews

class HTMLFlight(Flight): def as_tr(self): """ >>> HTMLFlight.objects.get(pk=234321).as_tr() '<tr id="flight_234321"><td class="total_time">3.5</td>... """

class JSONFlight(Flight): def as_json(self): """ >>> JSONFlight.objects.get(pk=56216).as_json() '{id: 56216, plane: {tailnumber: "N63NE", type: "SA-227"... """

ModelView

def list_flights_controller(request, format): if format == 'json': return JSONFlight, 'flights.json' elif format == 'html': return HTMLFlight, 'flights.html'

def list_flights(request, format): Flight, view = list_flights_controller(request, format) flights = Flight.objects.filter(user=request.user) return render_to_response({'flights': flights}, view)

ModelView

flights.html: <table class="whatever"> {{ Flight.header }} {% for flight in flights %} {{ flight.as_tr }} {% endfor %} </table>flights.json:{user: {{ request.user }}, flights: {% for flight in flights %} {{ flight.as_json }}, {% endfor %}}

Good models are easy to test

class BaseFlightFailedTest(object): exc = Flight.InvalidFlightData def test(self): for kwargs in self.kwarg_set: self.assertRaises(Flight.new_flight(**kwargs), self.exc)

class TotalGreatestTest(TestCase, BaseFlightFailedTest): exc = Flight.TotalMustBeGreatest kwarg_set = [{'total_time': '3:50', 'pic': 9.6}, {'total_time': 1.0, 'night': 2.3}]

class NoNightTime(TestCase, BaseFlightFailedTest) kwarg_set = [{'total_time': 1, 'night': 0, 'night_landings': 5}]

Tips:

1. Don't pass request objects into the model a. It couples your model to HTTP requests b. Models should only work with raw data

2. Try to avoid putting business logic into a controller a. It makes it hard to reuse models

3. Pass in only one model object to a view. a. if you have trouble doing this, your models may be wrong b. helps keep the templates reusable.

4. Make an attempt to re-write all your controllers to be exactly 3 lines.

Giotto!

- New python web development framework!!- Absolutely nothing has been started yet.- Doesn't let you violate MVC.- There should be one-- and preferably only one --obvious way to do it.- "MV" framework. (micro controllers)- Completely automatic urls- plugins for features- plugins for controller backends. (commandline, http-get, etc)

Giotto Feature

@interfaces('http-get', 'commandline')class ShowFlightsForUser(Feature): """ Show flights for a given user """ controller = {'user': Input.data.user} model = Flight.objects.show_for_user view = ShowFlights

url for feature: {% http-get ShowFlightsForUser.html 59 %} ->

Logbook.com/flights/ShowFlightsForUser.html?user=59

Giotto Interfaces

@interfaces('http-put', 'commandline')class NewFlight(Feature): """ Create a new flight """ controller = {'user': Input.data.user, 'total_time': Input.data... model = Logbook.Flight.create view = SingleFlight

Using the feature: $ ./giotto.py logbook NewFlight --user=chris --total_time=3 ...or PUT /new_flight.json HTTP/1.1 user=chris&total_time=3 ...

Giotto Models

class Manager(models.Manager) def show_for_user(self, user): return self.filter(user=user)

def create(self, *args, **kwargs): # logic goes here return Flight(**kwargs)

class Flight(models.Model): attribute = models.Field() objects = Manager()

Accessing features

command line: $ ./giotto.py app feature format [args]

http-get POST app.com/feature.format HTTP/1.1 [args]

sms text "feature format [args]" to 3558526

The controller backend handles transporting data to/from the user

Controllers handle everything for you

@interfaces('http-get', 'commandline')class ShowRouteForFlight(Feature): """ Get the route for a single flight """ controller = Flight.id model = Flight.route view = SingleRouteView

Giotto Views

Take only a single model instance as the only context (obj)

Views can return anything, as long as the controller backend knows how to handle it.

templates make links to application features:

<a href="{% url_get ShowFlightsForUser obj.user %}"> see {{ obj.user }}'s flights!</a>

Giotto Views

class SingleRouteView(View): def png(self, route): "Given a route, return as a png image" return image_file

def kml(self, route): return kml_string

def html(self, route): return jinja2.render({'obj': route}, 'route.html')

{% http-get RouteForFlight.kml 36426 %}{% http-get RouteForFlight.html 36426 %}giotto logbook RouteForFlight png 36426 | file.png

Recommended