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.



Dienstag, 30. November 2010

Django Email send testing

Sometimes you want to test / debug Emails and dont want to send emails at all.

The smtp-lib provides an email-testserver, wich prints incomming email to stdout.


django-doc: link

You can start the smtp-debugserver with this command.
python -m smtpd -n -c DebuggingServer localhost:1025


in your settings.py you need to add the attributes:
EMAIL_HOST = "localhost"
EMAIL_PORT = 1025

now email you send via django will print out to stdout.

Python / Django remove *.pyc files

User this to remove recursively all pyc files from the current directory

find . -type f -name "*.pyc" -exec rm -f {} \;

Django 1.2 with Jinja2 and template from Database

Sometimes you need to store your templates or content block in Database.
Jinja provides a function "render_"from_string, to use this we need to write a minimal Loader and hook this up to django.


django_jinja2.py

import jinja2

from django.template.loader import BaseLoader
from django.template import TemplateDoesNotExist

from yourproject.yourapp.models import DBTemplate

class JinjaDBLoader(BaseLoader):
is_usable = True

env = jinja2.Environment(
loader=jinja2.FileSystemLoader(settings.JINJA2_TEMPLATE_DIRS),
extensions=(
'jinja2.ext.i18n',
),
)

env.template_class = Template

def load_template(self, template_name, template_dirs=None):
# getting template/content from Database and cached it
object_manager = DBTemplate.objects.filter(name = template_name)

if object_manager.count() > 0:
# accessing cached object
return self.env.from_string( object_manager[0] ), None
else:
# if TemplateDoesNotExist is raised
# django go to the next Loader in TEMPLATE_LOADERS List
raise TemplateDoesNotExist( template_name )

Hook the Loader into settings.TEMPLATE_LOADERS link.
And use he default django render_to_response shortcut link

Django 1.2.x mit Jinja2

django_jinja2.py @ http://www.rosslawley.co.uk/2010/07/django-12-and-jinja2-integration.html

"""
Using Jinja2 with Django 1.2
Based on: http://djangosnippets.org/snippets/2063/

To use:
* Add this template loader to settings: `TEMPLATE_LOADERS`
* Add template dirs to settings: `JINJA2_TEMPLATE_DIRS`

If in template debug mode - we fire the template rendered signal, which allows
debugging the context with the debug toolbar. Viewing source currently doesnt
work.

If you want {% url %} or {% csrf_token %} support I recommend grabbing them
from Coffin (http://github.com/dcramer/coffin/blob/master/coffin/template/defaulttags.py)
Note for namespaced urls you have to use quotes eg:
{% url account:login %} => {% url "account:login" %}
"""
import jinja2

from django.template.loader import BaseLoader
from django.template import TemplateDoesNotExist, Origin
from django.core import urlresolvers
from django.conf import settings


class Template(jinja2.Template):
def render(self, context):
# flatten the Django Context into a single dictionary.
context_dict = {}
for d in context.dicts:
context_dict.update(d)

if settings.TEMPLATE_DEBUG:
from django.test import signals
self.origin = Origin(self.filename)
signals.template_rendered.send(sender=self, template=self, context=context)

return super(Template, self).render(context_dict)


class Loader(BaseLoader):
"""
A file system loader for Jinja2.

Requires the following setting `JINJA2_TEMPLATE_DIRS`
"""
is_usable = True

# Set up the jinja env and load any extensions you may have
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(settings.JINJA2_TEMPLATE_DIRS),
extensions=(
'django_jinja2.extensions.URLExtension',
'django_jinja2.extensions.CsrfExtension',
)
)
env.template_class = Template

# These are available to all templates.
env.globals['url_for'] = urlresolvers.reverse
env.globals['MEDIA_URL'] = settings.MEDIA_URL

def load_template(self, template_name, template_dirs=None):
try:
template = self.env.get_template(template_name)
return template, template.filename
except jinja2.TemplateNotFound:
raise TemplateDoesNotExist(template_name)

settings.py

JINJA2_TEMPLATE_DIRS = (
    # your template dirs
)

TEMPLATE_LOADERS = (
# Use this to hook the loader into django
'django_jinja2.Loader',

# use this to run the admin with django-default-template-engine
'django.template.loaders.app_directories.load_template_source',
...
)


TEMPLATE_CONTEXT_PROCESSORS = [
"django.core.context_processors.auth",

# access request
"django.core.context_processors.request",

# access MEDIA_URL
"django.core.context_processors.media",
...
]

views.py

from django.shortcuts import render_to_response

def index(request):
return render_to_response( "index.html",
{ },
# this is important to access TEMPLATE_CONTEXT_PROCESSORS
context_instance = RequestContext( request )
)