ratatwisker(.py)
OXSmoontool cdcopy Musik-Server

ratatwisker(.py)

das (mod_)Python-Web-Application-Framework, mit dem diese Site erstellt wurde

Motivation

2006 begann ich ein Framework mit ähnlicher Struktur in PHP zu erstellen und kam während der Arbeiten zu dem Schluß, daß PHP sehr ineffizient ist. (Wenn ich mal viel Zeit habe, schreibe darüber einen Artikel)
Für die Web-Auftritte, die ich neben meiner Arbeit erstelle, benötige ich ein Framework, das mir einen Großteil der Routinearbeiten abnimmt. Ich habe einfach nicht die Zeit, immer die gleichen SQL-Queries zu schreiben, um dann die Daten in Arrays zu kopieren oder aus ihnen, in immer den gleichen Arbeitsschritten, Objekte zu erzeugen.
Mir fehlt die Geduld, mich durch hunderte Zeilen lange switch/case if/elseif-Dschungel zu kämpfen. Oder unendlichen include/require_once-Orgien hinterherzuhecheln.

Ich möchte Lösungen für Aufgaben erstellen.

oder warum ist ratatwisker(.py) das 42. Web-Applications-Framework:

Bevor ich mit den Arbeiten an ratatwisker(.py) begann, habe ich mir die Platzhirsche angeschaut. Jeder der Hirsch hat seine Stärken, TurboGears, Zope, Ruby on Rails, Django, TYPO3 oder Joomla. Interessant ist auch dieser Artikel.
Ja, ich werfe hier Frameworks und CMSe in einen Topf, weil für mich nur interessant ist, wie groß der Aufwand ist, um zum Ergebnis zu kommen.
Daneben habe ich mir die Systeme, mit denen in meiner beruflichen Praxis Web-Anwendungen erstellt werden und wurden, angeschaut.

Was ich mit ratatwisker(.py) gemacht habe, ist, daß ich aus allen diesen Projekten die besten Ideen und Strukturen genommen und gewichtet habe, um daraus ein System zusammenzubauen, mit dem sich Standardauftritte wie z.B. www.siemion.de, www.karate-sv-mainz.de, www.synergetik-zentrum-mainz.de mit einem Mindestmaß an Programmierung erstellen lassen.
Neben dieser Vereinfachung der Arbeit ist es mir genauso wichtig, daß das System so flexibel und klar strukturiert ist, daß sich mit ihm auch "Sonder"-Anfertigungen, mit möglichst geringem Aufwand erstellen lassen.
Daß Änderungen an bestehenden Auftritten nicht in Katastrophen enden, ist ein angenehmer Nebeneffekt.

und welche Konzepte machen es zum "idealen" Web-Application-Framework

Konfiguration statt Programmieren

Super, gleich der erste Punkt stiftet Verwirrung!
Wenn ich http://de.wikipedia.org/wiki/Ruby_On_Rails lese, dann macht ratatwisker(.py) genau das Gegenteil von "Konfiguration statt Programmieren".
Was dieser Punkt sagen soll, ist: ratatwisker(.py) bietet viele Funktionen, die ich nicht immer neu programmieren muß. Ich kann sie nutzen oder nicht, und entscheide dies über eine Konfiguration.
Über Selbstverständlichkeiten, wie daß man über den Namen des Controllers auf den Namen des Datenmodels schließen kann, habe ich nie nachgedacht. Das ist halt einfach so. ;-)

URL-Mapping

Zwei definierbare Teile des Pfades der URL bestimmen, welche Methode in welcher Klasse / Controller aufgerufen wird.
Die restlichen Teile des Pfades werden zu Parametern des Requests gemapt.

Parameterdefinition

In der Konfiguration können alle Parameter definiert und mit Standardwerten versehen werden, so daß Abfragen auf das Vorhandensein eines Parameters nicht notwendig sind und von definierten default-Werten ausgegangen werden kann.

Objekt-Relationales Mapping

Um die immer wiederkehrenden SQL-Querys, das immer gleiche Erzeugen von Arrays oder Objekten und den Paradigmenwechsel zu vermeiden.
siehe auch ...

MVC-Pattern

Trenne die Aufgabe in ihre Teile auf und vermeide die Ballung von Komplexität.
siehe auch ...

Vermeidung von nicht zur Lösung führender Komplexität

ratatwisker(.py) nutzt eine Template-Engine, einen OR-Mapper und einen Web-Server und benötigt in dessen Konfiguration 5 Zeilen.

Erfinde das Rad nicht immer wieder neu

und nutze gute und erprobte Tools.

Welche Tools nutzt ratatwisker(.py)

um die beschriebenen Aufgaben möglichst effektiv erledigen zu können?

ratatwisker(.py), siteObject, siteData. Die Strukturen des Systems

ratatwisker(.py)

siteObject

ist die Basisklasse, mit der alle Controller-Klassen erstellt werden können, die Funktionalitäten für die Site zur Verfügung stellen, und bildet den Controller.
siteObject stellt, in Zusammenarbeit mit siteData, die Grundfunktionalitäten für eine Administrationsoberfläche zur Verfügung: ohne daß eine Zeile Programmcode notwendig ist.

siteData

bildet das Daten-Modell eines siteObjects.
siteData benutzt als Basis die ColClasses von SQLObject und stellt Daten-Spalten-Objekte der verschiedenen benötgten Datenarten zur Verfügung, z.B. Ein siteData-Object prüft, ob die Daten eines Formulars gültige Daten für das siteData-Object enthalten.
Auch hier: Standardaufgaben sind entweder ohne oder mit ganz wenigen Zeilen Programmcode erstellt.

Als Beispiel

der Programmcode, der für eine Newsletter-Anmeldung mit Double-Optin notwendig ist:

TODO: aktuelle Umsetzung veröffentlichen ...

Die siteData-Klasse

Hier werden nur die Datenfelder eines Newsletter-Abonnementen definiert.

# coding=utf-8
from sqlobject import *
import sys, os
from datetime import datetime
from time import *
from siteData import *
from siteObject import *

class newsletterUserSiteData(siteData):
    
    anrede           = sdfString (defVal = '', defLen=10)
    vorname          = sdfString (defVal = '', defLen=255)
    name             = sdfString (defVal = '', defLen=255)
    email            = sdfEmail  (defLen=255, required = True)
    ipAnmeldung      = sdfString (defVal = '', defLen=20)
    datumAnmeldung   = sdfInteger()
    doubleOptIn      = sdfInteger(defVal = 0)
    datumDoubleOptIn = sdfInteger()
    ipDoubleoptIn    = sdfString (defLen = 20)

Die siteObject-Klasse

Hier steckt die Funktionalität der Klasse.
Die Methode signUp stellt das Anmeldeformular dar
self.objRatat.assignToView ( 'contentInclude',  self.objRatat.tpl ( False, 'newsletterUser', 'cont_', signUpFormName))
prüft die User-Eingaben
dictError = self.callSiteDataClassMethod('validateInputData', arg)
und schreibt den neuen Abonnementen in die Datenbank
siteDataNewsletterUser = self.newSiteDateInstance()
	...
siteDataNewsletterUser.setDataFromArg(argNew)
siteDataNewsletterUser.sync()
# coding=utf-8
from siteObject import *
from siteMessage import *
import siteBasic
import sys
import time
import smtplib
from email.MIMEText import MIMEText
import md5

class newsletterUser(siteObject):
    
    showFieldsInAdminListView = ['name', 'vorname', 'email']
    
    # registers a new newsletter reciver
    def signUp(self, arg, signUpFormName, successTplName):

        if(arg['do'] == '0'):
            # no parameter do, so deliver the registerform
            self.objRatat.assignToView ( 'contentInclude',  self.objRatat.tpl ( False, 'newsletterUser', 'cont_', signUpFormName))
            return True
        else:
            # the register form is submitted by the user
            # so check the input
            dictError = self.callSiteDataClassMethod('validateInputData', arg)
            
            if dictError:
                # error
                self.objRatat.dictMessage = dictError
                self.objRatat.assignToView('contentInclude',  self.objRatat.tpl ( False, 'newsletterUser', 'cont_', signUpFormName))
                return False
            
            # no error in the form, check if the email is not yet registed
            argNew = {'email': arg['email'].lower()}
            entry = self.callSiteDataClassMethod('selectBy', argNew)
            
            if entry.count() > 0:
                # email already registed
                self.objRatat.dictMessage['newsletterUser_email'] = [smLookUp('newsletterUser', 'email', 'emailAlreadyRegisted')]
                self.objRatat.assignToView('contentInclude',  self.objRatat.tpl ( False, 'newsletterUser', 'cont_', signUpFormName))
                return False
            else:
                # create a new newsletterUser siteData-Object
                siteDataNewsletterUser = self.newSiteDateInstance()
                
                # fill arg with the futher informations
                argNew['ipAnmeldung']      = self.objRatat.req.get_remote_host(apache.REMOTE_NOLOOKUP)
                argNew['datumAnmeldung']   = time.time()
                argNew['doubleOptIn']      = 0
                argNew['datumDoubleOptIn'] = 0
                argNew['ipDoubleoptIn']    = ''
                argNew['anrede']           = ''
                argNew['vorname']          = ''
                argNew['name']             = ''
                
                siteDataNewsletterUser.setDataFromArg(argNew)
                siteDataNewsletterUser.sync()
                
                self.sendDoubleOptInEmail(siteDataNewsletterUser)
                self.objRatat.assignToView('contentInclude',  self.objRatat.tpl ( False, 'newsletterUser', 'cont_', successTplName))
                return True
            
        self.objRatat.assignToView('contentInclude',  self.objRatat.tpl ( False, 'newsletterUser', signUpForm))
        self.objRatat.assignToView('dictError', dictError)
        return True

    
    # sends the doubleOptIn Email to the Users in listUser
    def sendDoubleOptInEmail(self, objNewsletterUser):
        
        hashStr = '%s%s%d' % (objNewsletterUser.email, objNewsletterUser.ipAnmeldung, objNewsletterUser.datumAnmeldung)
        hash = md5.new(hashStr).hexdigest()
        
        self.objRatat.assignToView('newsletterUser_date', time.strftime('%a, %d %b %Y %X +0100'))
        self.objRatat.assignToView('newsletterUser_from', 'newsletter@email.de')
        self.objRatat.assignToView('newsletterUser_to',   objNewsletterUser.email)
        self.objRatat.assignToView('newsletterUser_id',   objNewsletterUser.id)
        self.objRatat.assignToView('newsletterUser_hash', hash)
        
        emailTxt = self.objRatat.tpl(True, 'newsletterUser', 'email_', 'doubleOptIn')
        
        s = smtplib.SMTP('xxxxxx')
        s.login('xx', 'xxxx')
        s.sendmail('xxxx@test.de', [objNewsletterUser.email], emailTxt.encode('utf-8'))
        s.quit()
        
        return True;
    
    
    # handles the doubleOptIn
    def doubleOptIn(self, userId, hash, successTpl, errorTpl):
        
        # read the user by id
        try:
            objUser = self.getEntryById(userId)
        except:
            self.objRatat.dictMessage['newsletterUser_email'] = [smLookUp('newsletterUser', 'doubleOptIn', 'entryNotFound')]
            self.objRatat.assignToView('contentInclude',  self.objRatat.tpl ( False, 'newsletterUser', 'cont_', errorTpl))
            return False
        
        # rebuild the hash
        hashStr = '%s%s%d' % (objUser.email, objUser.ipAnmeldung, objUser.datumAnmeldung)
        if md5.new(hashStr).hexdigest() != hash:
            self.objRatat.dictMessage['newsletterUser_email'] = [smLookUp('newsletterUser', 'doubleOptIn', 'entryNotFoundHash')]
            self.objRatat.assignToView('contentInclude',  self.objRatat.tpl ( False, 'newsletterUser', 'cont_', errorTpl))
            return False
        
        # set user to doubleOptIn
        arg = {}
        arg['doubleOptIn']      = 1
        arg['datumDoubleOptIn'] = time.time()
        arg['ipDoubleoptIn']    = self.objRatat.req.get_remote_host(apache.REMOTE_NOLOOKUP)
        
        objUser.setDataFromArg(arg)
        objUser.sync()
        
        self.objRatat.assignToView('contentInclude',  self.objRatat.tpl ( False, 'newsletterUser', 'cont_', successTpl))
        return True;

(c) 2012 by thomas siemion