Sonntag, 5. August 2012

django REST tastypie jQuery

Nach dem wir so schön und einfach django um REST erweitert haben befassen wir uns damit diese über JavaScript um konkreter zu sein jQuery aufzurufen.

beim googlen nach "jQuery rest" kommt man nicht um das Script vom "Sasa Jovancic" vorbei, daher habe ich mir erlaubt sein Script als Vorlage zu nehmen und es zu erweitern.

Für dieses Beispiel wird eine funktionieren tastypie-Anwendung benötigt wie z.b. hier.
In der Standartkonfiguration ist die default "Authentication" eingestellt, also keine.
Als autorisation ist "DjangoAuthorization" eingestellt, also kann man nur Anfragen absetzen wenn man sich eingeloggt hat

Hier die JS-Klasse
Danke an Sasa Jovancic für diesen Code-Snippets

/* JS REST.js */
function RestServiceJs(newurl) {
 this.myurl = newurl;

 this.add = function(model, callback) {
  console.log(JSON.stringify(model));
  $.ajax({
   type : 'POST',
   url : this.myurl,
   data : JSON.stringify(model), // '{"name":"' + model.name + '"}',
   dataType : 'text',
   processData : false,
   contentType : 'application/json',
   success : callback,
   error : function(req, status, ex) {
   },
   timeout : 60000
  });
 };

 this.update = function(model, callback) {
  $.ajax({
   type : 'PUT',
   url : this.myurl,
   data : JSON.stringify(model), // '{"name":"' + model.name + '"}',
   dataType : 'text',
   processData : false,
   contentType : 'application/json',
   success : callback,
   error : function(req, status, ex) {
   },
   timeout : 60000
  });
 };

 this.findById = function(id, callback) {
  $.ajax({
   type : 'GET',
   url : this.myurl + id,
   contentType : 'application/json',
   success : callback,
   error : function(req, status, ex) {
   },
   timeout : 60000
  });
 };

 this.filter = function(query, callback) {
  
  $.ajax({
   type : 'GET',
   url : this.myurl + "?" + $.param(query),
   contentType : 'application/json',
   success : callback,
   error : function(req, status, ex) {
   },
   timeout : 60000
  });
 };

 this.findAll = function(callback) {
  $.ajax({
   type : 'GET',
   url : this.myurl,
   contentType : 'application/json',
   success : callback,
   error : function(req, status, ex) {
   },
   timeout : 60000
  });
 };

 this.remove = function(id, callback) {
  $.ajax({
   type : 'DELETE',
   url : this.myurl + id,
   contentType : 'application/json',
   success : callback,
   error : function(req, status, ex) {
   },
   timeout : 60000
  });
 };

 this.loadTmpl = function(turl, callback) {
  $.ajax({
   url : turl,
   success : callback,
   error : function(req, status, ex) {
   },
   timeout : 60000
  });
 }
}



Die REST.js und natürlich jQuery binden wir ein und los geht's.
Nach dem die Seite geladen ist können wir gleich mit der Browser-Konsole spielen.

p = new RestServiceJs("/api/v1/poll/") // mit slash am ende, da sonst kein POST von django akzeptiert wird


// fetche alle Daten
r.findAll(function(data){ 
    console.log(data); 
}) 


// fetch by id
r.findById(1, function(data){ 
    console.log(data); 
})


// django filter mit (i)startswith, (i)endswith, (i)contains, gt, lt ...
r.filter(
    { bezeichnung__istartswith:'1008'}, 
    function(data){ 
        console.log(data); 
})


beim filter kann django-db-filter-notation verwendet werden.

also viel spaß beim ausprobieren!



Freitag, 3. August 2012

Django Objekt Ableitung

Große und komplexe Anwendung mit Businesslogik erfordert auch komplexere Daten-Modelle, in manchen Fällen dürfen Daten nicht phsysikalisch gelöscht werden, sondern werden nur als gelöscht markiert, oder man möchte wissen wann etwas hinzugefügt und wann es geändert wurde.
Sortierung gehört auch dazu.
Bei Anwendungen mit zig Apps und hunderten von Models ist es nicht sinnvoll jedes Model anzufassen und denen immer wieder die gleichen Attribute wie "created", "updated" bzw. "sorting" einzutragen. Abgesehen davon verstößt man da gegen das DRY-Prinzip und man macht sich unnötig viel Arbeit, spätestens wenn weitere Attribute hinzukommen,  gelöscht oder umbenannt werden.

In den 5 Jahren (2007) Django Entwicklung habe ich einige Strategien untersucht und eine hat sich als am praxistauglichsten herausgestellt, Abstrakte-Klassen.
Dabei ist django sehr freundlich was das Ableiten von abstrakte-Klassen angeht, es verhält sich exakt so wie man es von eine objekt-orientierte-sprache erwartet und überträgt dieser durch den ORM in die Datenbank.

Hier die Klassen.

Die Klasse "DjObject" ist unsere Basis, sie besitzt die Attribute, hidden (falls ein Datensatz nur als unsichtbar markiert werden soll), deleted (wenn ein Datensatz gelöscht wird - "Mülltonne", es ist noch da wird aber nirgends angezeigt).
Da kommt der DjObjectManager ins Spiel, solange man den Manager über XX.objects.visible() aufruft werden nur nicht gelöschte Datensätze gefiltert und das für ALLE Klassen die von DjObject ableiten - ist das nicht cool?.

Jetzt gehen wir ein Schritt weiter und fügen die Klasse "Sortable" hinzu, diese hat nur das Attribut "sorting", gleichzeitig hat es auch alle Attribute und Funktionen von DjObject.
Damit das Sorting automatisch für alle abgeleiteten Klassen hochzählt müssen wir die Save-Funktion erweitern.
Beim Sortieren hilft uns Django wieder aus, mit der Meta-Klasse kann der django-default-manager angewiesen werden nach dem Attribut "sorting" zu sortieren.

from django.db import models

class DjObjectManager(models.Manager):
    def visible(self):
        return self.filter(hidden = False, deleted = False)
    

class DjObject(models.Model):
    hidden = models.BooleanField( default = False )
    deleted = models.BooleanField (default = False )
    created = models.DateTimeField(auto_now_add = True )
    updated = models.DateTimeField( auto_now = True )
    objects = DjObjectManager()

    def hide(self):
        self.hidden = True
        self.save()
    
    def show(self):
        self.hidden = False
        self.save()
    
    def remove(self):
        self.hidden = True
        self.deleted = True
        self.save()
    
    class Meta:
        abstract = True
        
        

class Sortable(DjObject):
    sorting = models.IntegerField( blank = True, null = True )
    sorting_increment = 10    

    def save(self, *args, **kwargs):
        self.sorting = self.__class__.objects.count() + self.sorting_increment 
        super(Sortable, self).save(*args, **kwargs)
    
    class Meta:
        ordering = ("sorting", )
        abstract = True


Jetzt leiten wir mal eine konkrete Klasse von den Abtraktenklassen ab.


class Country(Sortable):
    name = models.CharField(max_length=255)
    
    def __unicode__(self):
        return self.name

class Address(DjObject):
    name = models.CharField(max_length = 255)
    descrription = models.TextField(blank = True, null = True)
    
    def __unicode__(self):
        return self.name


Der django-orm-Mapper löst die Klasse so auf
Die Attribute der Abtrakten-Klassen werden auf die Konkrete Klasse übertragen, in diesem Falle sind es die Attribute von DjObject und Sortable, zuletzt werden die Klassen-attribute hinzugefügt, in diesem Falle "name".



CREATE TABLE app_country
(
  id serial NOT NULL,
  hidden boolean NOT NULL,
  deleted boolean NOT NULL,
  created timestamp with time zone NOT NULL,
  modified timestamp with time zone NOT NULL,
  sorting integer,
  name character varying(255) NOT NULL,
  CONSTRAINT app_country_pkey PRIMARY KEY (id )
)


CREATE TABLE app_address
(
  id serial NOT NULL,
  hidden boolean NOT NULL,
  deleted boolean NOT NULL,
  created timestamp with time zone NOT NULL,
  modified timestamp with time zone NOT NULL,
  name character varying(255),
  description text
)


Jetzt können wir auch auf die Funktionen der Abstrakte-klassen zugreifen:

Company.objects.all() # django-default-manager

Company.objects.visible() # djobject-manager

company = Company.objects.create(name="XXX") # aufruf von save - zaehlt sorting hoch

company.hide() # das Objekt unsichtbar machen

company.show() # object wieder sichtbar machen

company.remove() # als geloescht markieren 


Hiermit kommt man schon ziemlich weit, es gibt noch komplexere Anwendungsfälle die komplexere Strukturen benötigen, das hier reicht für 99% der Anwendungen aus.

dann viel spaß beim Testen

und gerne könnt ihr mir einen Kommentar hinerlassen.











Django REST TastyPie helper

Heute habe ich mir django und REST-API angeschaut, nach dem Google ein Dutzen django-rest-projekte ausgespuckt hat, bin ich auf tastypie (git) gestoßen, da das Projekt schon seit einigen Jahren existiert und munter weiter entwickelt wird habe ich mir das näher angeschaut.

Schnell pip install tastypie in die virtualenv eingegeben, ein bisschen konfigurieren und voila! es klappt.

Nur leider ist es doch ein bisschen mühsam für jede ORM-Klasse das Tastypie-Gegenstück (Resource-Klasse) zu schreiben, auch wenn die Klasse trivial aufgebaut ist.

Der Ansatz jede Resource-Klasse in der urls.py zu registrieren erscheint mir nicht sehr elegant, das fürht bei größeren Projekten mit mehreren "Apps" und Klassen zu einem riesen Import-Schlacht (korrigiert mich, falls ich da falsch lege).

Also habe ich mich kurzer Hand entschieden eine API-POOL zu schreiben, der sich drum kümmert triviale Resource-Klassen dynamisch zu erstellen und komplexe Klassen zu sammeln.

#Datei: TastyPiePool
#coding: utf-8
'''
Created on 03.08.2012

@author: trungphanan
@contact: info@level96.de

'''

from tastypie.api import Api
from tastypie.resources import ModelResource, ALL, ALL_WITH_RELATIONS
from tastypie.authentication import Authentication
from tastypie.authorization import DjangoAuthorization
import tastypie.fields



class TastyPiePool(object):
    '''
    '''

    def __init__(self, api_name="v1"):
        self.api = Api(api_name=api_name)
        self.allowed_methods = ("get", "post", "put", "delete")
        

    def register(self, orm_class, resource_name, foreign_keys={}, filtering={}, manager=None, excludes=None, fields=None, allowed_methods=None, authentication=None, authorization=None):
        '''
        Dynamicly creates Resource Class from Django-ORM-Model
        '''
        
        resource_attrs = {"Meta": type(
            "Meta", (), 
            {
                "queryset": orm_class.objects.all() if not manager else manager,
                "resource_name": resource_name,
                "excludes": excludes, 
                "fields": fields, 
                "allowed_methods": allowed_methods if allowed_methods else self.allowed_methods,
                "authentication": Authentication() if not authentication else authentication,
                "authorization": DjangoAuthorization() if not authorization else authorization,
                "filtering": dict([ (f, ALL_WITH_RELATIONS if filtering[f]=="all" else filtering[f]) for f in filtering ])
            }
        )}
        
        for fk in foreign_keys:
            resource_attrs.update({ 
                fk: tastypie.fields.ForeignKey(foreign_keys[fk], 
                getattr(foreign_keys[fk].Meta, "resource_name")) 
           })
            
        
        cls = type(
            "%sResource" % type(orm_class).__name__,
            (ModelResource, object), 
            resource_attrs
        )
        
        self.register_resource(cls())
        
        return cls
    
    
    def register_resource(self, resource, canonical=True):
        self.api.register(resource, canonical)
    
    
    @property
    def urls(self):
        return self.api.urls

        


api_pool = TastyPiePool()


in der models.py importieren wir die API-POOL und registrieren die Klasse und den resource-name, letzteres wird als Slug für die API verwendet.
Die Funktion register generiert dynamisch eine Resource-Klasse mit dem Namen: orm-klasse-name Resource (tastypie namens-konvention) mit der entsprechenden Meta-Klasse.
Dabei kann ein abweichenden Object-Manager, excludes, fields oder allowed_methods angegeben werden (siehe tastypie doku)

Für den Fall das ihr komplexere Resource-Klassen benötigt und geschrieben habt, könnt ihr die Funktion "register_resource(CLASS)" benutzen.

from TastyPiePool import api_pool

class Poll(models.Model):
    name = models.TextField()

api_pool.register(Poll, 'poll' ) # args: orm-class, resource_name


Nach dem die Klasse registiert wurde müssen wir die api_pool nur noch der urls.py registriert werden damit die urls erreichtbar sind.


from django.conf.urls.defaults import *

from TastyPiePool import api_pool


urlpatterns = patterns('',
    (r'^api/', include(api_pool.urls)),
)


danach server neustarren und ihr erreicht die API unter:
http://host:port/api/v1/RESCOURCE-NAME/?format=json

viel spaß beim Testen und Erweitern des Scripts


für jQuery-API klickt ihr hier.