Ein interessantes Konzept für eines vielleicht würdigen Lomo-Nachfolgers. Würde ich mir auch ein paar Euro kosten lassen…
Der Fall Perlentaucher gegen FAZ und SZ beschäftigt schon seit 2005 die deutschen Gerichte. Die Verhandlung vor dem Bundesgerichtshof findet heute ab 12 Uhr statt. CARTA hat eine sehr gute Zusammenfassung zu diesem Verfahren veröffentlicht. Jeder, der sich auch nur ansatzweise für die aktuellen Diskussionen zum Urheberrecht interresiert, wird den Ausgang des Verfahrens mit Spannung verfolgen. Ich bin vor allem auf die Urteilsbegründung gespannt und hoffe dass auch weiterhin nach Perlen getaucht werden darf.
Unsere geehrte Kanzlerin hat sich gestern mit Herrn Sarkozy getroffen um eine bessere Zusammenarbeit in Wirtschaftspolitik innerhalb der Europäischen Union voran zu bringen. Unter anderem gab es dabei diesen Vorschlag:
Einig zeigten sich beide Seiten darin, dass Euro-Staaten, die häufiger gegen das Drei-Prozent-Defizitkriterium des Maastricht-Vertrags verstoßen, künftig vorübergehend das Stimmrecht entzogen werden soll. Quelle
Finde ich sehr viel sinnvoller, als die bisherige Regelung, dass einem überschuldeten Staat noch mehr Geld in Form von Strafzahlungen abgezweigt wird. Ob "Mutti" allerdings diese Grafik aus dem eigenen Hause kennt mag man fast in Zweifel ziehen.
Nur zu Erinnerung: Die Maastricht-Kriterien legen für den Schuldenstand ein Maximum von 60% des BIP fest.
Bei meinem Turbogears-Projekt verwende ich zur Formularanzeige und -validierung die empfohlenen ToscaWidgets. Die Dokumentation dazu ist spärlich, aber wenn man verstanden hat, dass es sehr einfach ist eigene Templates zu verwenden, kommt man sehr gut mit ihnen zurecht.
Nur eines hat mir gestern und heute Kopfzerbrechen bereitet: die Validierung bei der Verwendung des FormFieldRepeater-Widgets. Dieses wird benötigt, wenn man wiederholende Formularbereiche – wie z.B. mehrere Email-Adressen pro Kontakt – verwenden möchte. Hier ein kleines Code-Beispiel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class EmailSubForm(twf.TableFieldSet): template = "…" hover_help = True class fields(WidgetsList): id = twf.HiddenField(validator=twv.Int(not_empty=True)) email = twf.TextField( label_text='Email-Address', validator=twv.Email(not_empty=True)) remarks = twf.TextField( label_text='Remarks', validator=twv.UnicodeString(not_empty=True)) class ContactForm(twf.TableForm): hover_help = True template = "…" submit_text = "Update Contact" class fields(WidgetsList): name = twf.TextField( label_text='Name', validator=twv.UnicodeString(not_empty=True)) […] emails = twf.FormFieldRepeater( widget=EmailSubForm(), repetitions=0) |
So weit, so gut. Das Formular ´ContactForm´ wird auf der Website angezeigt inklusive der vorhandenen Email-Adressen im "Unterformular" ´EmailSubForm´. Fehler bei der Eingabe im Hauptformular werden auch angezeigt – z.B. keine Eingabe im Feld ´name´. Jedoch wird auf Eingabefehler im Unterformular nicht geprüft!
Ich habe Herrn Google gefragt, viel gelesen, nichts gefunden was mir
weitergeholfen hätte. Die Lösung kam durch ausprobieren. Setzt man den Wert für
repetitions (letzte Zeile) auf 1 funktioniert alles. Warum? keine Ahnung.
Die korrigierte Fassung des ContactForms sieht so aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class ContactForm(twf.TableForm): hover_help = True template = "…" submit_text = "Update Contact" class fields(WidgetsList): name = twf.TextField( label_text='Name', validator=twv.UnicodeString(not_empty=True)) […] emails = twf.FormFieldRepeater( widget=EmailSubForm(), repetitions=0) |
Ich hoffe ich vergesse es nicht und es hilft jemandem weiter :-)
Der gestrige Rücktritt unseres Bundespräsidenten hat auch mich sehr überrascht – vor allem jedoch die Begründung, die er dafür anführt. Natürlich wird man nicht geliebt wenn man unpopuläre Warheiten zum Besten gibt, aber gerade als Amtsinhaber muss man meiner Meinung nach Kritik aushalten können.
Die erste Frage, die ich mir jedoch stelle ist, ob dies der einzige Grund für diese Entscheidung ist. Eine Antwort darauf ist in naher Zukunft wohl nicht zu erwarten.
Wahrscheinlich spinne ich mir etwas zusammen, aber nach dem überraschenden Rücktritt von Roland Koch vor rund einer Woche bleibt ein mulmiges Gefühl. Bin gespannt, wer als Nächster an der Reihe ist…
Schockschwerenot! Georg Schramm verlässt die Anstalt. Finde ich sehr schade. Ich hoffe sehr, dass die Anstalt nach der Sommerpause wieder öffnen wird. Hier noch das Inteview der Süddeutschen mit Urban Priol.
Zudem möchte ich den Aufruf des Schockwellenreiters unterstützen.
Ich stand vor dem Problem, wie ich bei Turbogears einen Test auf ein
repoze.what.predicate in einem Template durchführen kann. Ok, klingt
ein bisschen blöde :-). Eigentlich geht es nur darum, dass ich z.B. für die
Admininstratoren einen Link anzeigen möchte, für andere Benutzer nicht.
Für die Überprüfung innerhalb eines Controllers bietet Turbogears eine solche
Beschränkung durch den @require-Decorator an. Aber wie funktioniert es in
einem Template? Google sei Dank bin ich auf einige Diskussionen rund um das
Thema gestoßen und schließlich bei diesem Eintrag gelandet, welcher mich
auf das Changeset 6347 verwiesen hat.
Turbogears stellt über tg.predicates die entsprechenden tests zur Verfügung.
Und hier ein kleines Beispiel dazu:
1 | <p>${"OK" if tg.predicates.has_permission("ein_recht") else "BLOCKED"}</p> |
Ich habe heute mir halb den Kopf zerbrochen, wie ich bei TurboGears Tests anhand von Daten in einer Datenbank durchführen kann.
Das erste Problem ist eigentlich keines, aber es hat doch lange gedauert, bis ich darauf kam: Wie lade ich Test-Daten in eine separate Test-Datenbank?
Überall stand einfach nur, dass man zum Start der Anwendung benötigte Daten in
websetup.py ablegen sollte. Mein Denkfehler war einfach nur, dass nosetests
die SQL-Einstellungen von test.ini verwendet und nicht von developement.ini.
Logisch! Aber hat bei mir etwas gedauert…
So, musste also einfach in der test.ini den Wert für sqlalchemy.url
anpassen. Voreingestellt ist eine Memory-Datenbank über SQLite. Da ich jedoch
Gebrauch von (MySQL)-spezifischen Enum-Feldern mache, musste ich diese ändern.
Das zweite Problem war die Integration von Sqlalchemy.migrate, welches ich zur
Entwicklung der Datenbank verwende. Zuersteinmal habe ich die Basisklasse für
den Test von Modellen rausgeworfen. Bei jedem Test hätte sich sonst das
Datenbankschema komplett neu aufgebaut, Daten importiert und nach dem Test
wieder die Datenbank zerstört. Und nein, einfach nur alle Modelle zu löschen
geht bei mir nicht so einfach, da es einige zirkuläre Fremdschlüsselbeziehungen
gibt.
Da nosetest verwendet wird, gibt es die Möglichkeit, in einem test-package
für alle Tests verwendete setup und teardown Funktionen bereitzustellen.
Die Setup-Funktion bringt zuerst die Datenbank auf die aktuelle Version und lädt
dann über websetup.py die Testdaten. Die Teardown-Funktion löscht wieder alle
Tabellen und somit alle Daten. Das schöne daran ist, dass dies nur einmal pro
Testreihe erfolgt.
Und falls ich doch zwischen den Tests mal wieder alles neu brauche, reicht ein einfacher Aufruf der restart() Funktion.
Als drittes sollte nun noch in TestController der Aufruf von websetup.py
unterbunden werden. Haben wir ja schon :-)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | """custom unit and functional test suite """ import os from tg import config from paste.deploy import loadapp from paste.script.appinstall import SetupCommand from routes import url_for import migrate.versioning.api from your_tg_app.model import DBSession conf_dir = config.here sqlalchemy_url = config['app_conf']['sqlalchemy.url'] repository = os.path.join(conf_dir, "migration") def setup_package(): """ loads the database fixture in websetup.py """ # get the directory of the config file # setup the database migrate.versioning.api.upgrade(sqlalchemy_url, repository) # setup the data test_file = os.path.join(conf_dir, 'test.ini') cmd = SetupCommand('setup-app') cmd.run([test_file]) def teardown_package(): """ removes all data in the database by downgrading via migrate """ migrate.versioning.api.downgrade(sqlalchemy_url, repository, 0) def restart(): teardown_package() setup_package() class TestController(object): """ Base functional test case for the controllers. The delphi2 application instance (``self.app``) set up in this test case (and descendants) has authentication disabled, so that developers can test the protected areas independently of the :mod:`repoze.who` plugins used initially. This way, authentication can be tested once and separately. Check delphi2.tests.functional.test_authentication for the repoze.who integration tests. This is the officially supported way to test protected areas with repoze.who-testutil (http://code.gustavonarea.net/repoze.who-testutil/). """ application_under_test = 'main_without_authn' def setUp(self): """Method called by nose before running each test""" # Loading the application: conf_dir = config.here wsgiapp = loadapp('config:test.ini#%s' % self.application_under_test, relative_to=conf_dir) self.app = TestApp(wsgiapp) # database setup done with setup_package() def tearDown(self): """Method called by nose after running each test""" # Cleaning up the database: DBSession.rollback() |
Momentan bin ich dabe eine Datenbank für ein Orchester zu programmieren. Ok, genauer gesagt eine schon in die Jahre gekommene Datenbank neu umzusetzen; und hierzu verwende ich Turbogears
Der Turbogears-Quickstart enthält schon eine vorgefertigte Benutzer- und Rechteverwaltung. Leider stimmt diese nicht mit der Struktur der Datenbank überein, wie ich sie gerne hätte. Da mit vielen Freiwilligen gearbeitet wird, die sich dazu noch von Projekt zu Projekt unterscheiden können, war die Idee die bereits in der Datenbank registrieten Personen einfach zu Benutzern machen zu können. Da sich später vielleicht auch Mitspieler anmelden sollen um z.B. die eigenen Telefonnummern zu ändern oder eine Zusage für ein Projekt zu geben wollte ich die vorhanden Email-Adressen als Benutzernamen verwenden.
Um einen Benutzer anhand seiner Email-Adresse anmelden zu können – ohne dass
diese Adresse doppelt in der Datenbank abgelegt wird – muss ich eine Abfrage
über mehrere Tabellen machen: users->persons->emails (in Wirklichkeit noch
ein wenig komplizierter, aber lassen wir es dabei :-)
Der erste Versuch war die repoze.who und repoze.what Konfiguration
unangetastet zu lassen und mit SQLAlchemy-Comparators zu arbeiten – ich
bin dabei kläglich gescheitert. Und spätestens wenn die oben genannte Funktion
für die Musiker hinzukommen sollte, werden eigene Plugins für repoze.who und /
oder repoze.what benötigt – also fange ich doch gleich damit an…
Leider ist die Dokumentation dazu auf der Turbogears-Seite etwas spärlich und verstreut. Teilweise mit Links auf das repoze-Projekt, das wiederum auf Turbogears verweist. Auch die such im allwissenden Netz hat keine einfaches Tutorial hervorgebracht. Daher will ich hier beschreiben werden welche Schritte nötig sind, um die Integration eigener Plugins in Turbogears 2 zu ermöglichen.
Nach dem ich mich durch das Setup von Turbogears gewühlt habe und ich endlich mit den richtigen Begriffen die Google-Treffer so weit einschränken konnte, dass sie mir etwas sinnvolles liefern bin ich über diese Seite gestolpert: http://blog.axant.it/archives/tag/turbogears. Darin wird zwar beschrieben, wie man eine Authentifizierung über mongodb hinbekommt, war aber trotzdem sehr hilfreich.
Die wichtigsten Erkenntnisse kurz zusammengefasst:
config.app_cfg der Wert
base_config.auth_backend = 'sqlalchemy' gesetzt ist, wird die
repoze.who-middleware in der richtigen Reihenfolge von Turbogears
eingebunden. tg.configuration.AppConfig in der
Methode make_base_app wiederum eine Methode self.add_auth_middleware()
aufgerufen. Dies ist kann nicht konfiguriert werden. tg.configuration.AppConfig.add_auth_middleware() letztendlich konfiguriert
repoze.what durch repoze.what.quickstart.setup_sql_auth(). Daher muss man "einfach" nur die add_auth_middleware-Methode überschreiben,
um eigene Plugins zu verwenden.
tg.configuration.AppConf ableitenZuerst muss eine eigene Klasse für die AppConf geschaffen werden, die sich aus
dieser ableitet. Die einzige Methode die überschrieben werden muss ist
add_auth_middleware(). Hierbei wurde nur der Aufruf
app = setup_derived_auth(app, skip_authentication=skip_authentication, […]
geändert um eine eigen Funktion aufzurufen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | import logging from copy import copy from tg.configuration import AppConfig class DerivedAppConfigAuth(AppConfig): """ Own App-Config class to implement custom repoze.(who|what) plugins The class tg.configuration.AppConfig is derived to implement custom plugins for authentification and authorization. """ def add_auth_middleware(self, app, skip_authentication): """ Configure authentication and authorization. :param app: The TG2 application. :param skip_authentication: Should authentication be skipped if explicitly requested? (used by repoze.who-testutil) :type skip_authentication: bool """ from repoze.what.plugins.quickstart import setup_sql_auth from repoze.what.plugins.pylonshq import booleanize_predicates # Predicates booleanized: booleanize_predicates() # Configuring auth logging: if 'log_stream' not in self.sa_auth: self.sa_auth['log_stream'] = logging.getLogger('auth') # Removing keywords not used by repoze.who: auth_args = copy(self.sa_auth) pprint(auth_args) if 'password_encryption_method' in auth_args: del auth_args['password_encryption_method'] if not skip_authentication: if not 'cookie_secret' in auth_args.keys(): msg = "base_config.sa_auth.cookie_secret is required " \ "you must define it in app_cfg.py or set " \ "sa_auth.cookie_secret in development.ini" print msg raise ConfigurationError(message=msg) app = setup_derived_auth(app, skip_authentication=skip_authentication, **auth_args) return app |
repoze.what.quickstart.setup_sql_auth()Als nächstes muss man eine Funktion erstellen, welche die gleichen Aufgaben
übernimmt wie repoze.what.quickstart.setup_sql_auth(). Hierzu habe ich einfach
die Funktion kopiert, umbenannt und in die gleiche Datei eingefügt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | from repoze.what.plugins.quickstart import find_plugin_translations from repoze.what.plugins.sql import configure_sql_adapters from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin from repoze.who.plugins.friendlyform import FriendlyFormPlugin from repoze.what.middleware import setup_auth def setup_derived_auth(app, user_class, group_class, permission_class, dbsession, form_plugin=None, form_identifies=True, cookie_secret='secret', cookie_name='authtkt', login_url='/login', login_handler='/login_handler', post_login_url=None, logout_handler='/logout_handler', post_logout_url=None, login_counter_name=None, translations={}, **who_args): """ Configure :mod:`repoze.who` and :mod:`repoze.what` with SQL-only authentication and authorization, respectively. […] """ plugin_translations = find_plugin_translations(translations) if group_class is None or permission_class is None: group_adapters = permission_adapters = None else: source_adapters = configure_sql_adapters( user_class, group_class, permission_class, dbsession, plugin_translations['group_adapter'], plugin_translations['permission_adapter']) group_adapters = {'sql_auth': source_adapters['group']} permission_adapters = {'sql_auth': source_adapters['permission']} # Setting the repoze.who authenticators: # using custom class here, removed translations for this class if 'authenticators' not in who_args: who_args['authenticators'] = [] orchestra_auth = OrchestraDbTeamUserAuthenticatorPlugin(user_class, dbsession) who_args['authenticators'].append(('orchestra_auth', Orchestraauth)) cookie = AuthTktCookiePlugin(cookie_secret, cookie_name) # Setting the repoze.who identifiers if 'identifiers' not in who_args: who_args['identifiers'] = [] who_args['identifiers'].append(('cookie', cookie)) if form_plugin is None: form = FriendlyFormPlugin( login_url, login_handler, post_login_url, logout_handler, post_logout_url, login_counter_name=login_counter_name, rememberer_name='cookie') else: form = form_plugin if form_identifies: who_args['identifiers'].insert(0, ('main_identifier', form)) # Setting the repoze.who challengers: if 'challengers' not in who_args: who_args['challengers'] = [] who_args['challengers'].append(('form', form)) # Setting up the repoze.who mdproviders: # using custom class here, removed translations for this class orchestra_user_md = OrchestraDbTeamUserMDPlugin(user_class, dbsession) if 'mdproviders' not in who_args: who_args['mdproviders'] = [] who_args['mdproviders'].append(('orchestra_user_md', orchestra_user_md)) middleware = setup_auth(app, group_adapters, permission_adapters, **who_args) return middleware |
Die wichtigsten Änderungen betreffen die Bereiche für den
repoze.who.Authenticator und repoze.who.metadataprovider. Die neuen
repoze.who-Plugins OrchestraDbTeamUserAuthenticatorPlugin und
OrchestraDbTeamUserMDPlugin müssen natürlich noch erstellt werden.
repoze.who-PluginsBei den eigenen Plugins folge ich ganz der Linie von repoze.who.plugins.sa
Analog zu repoze.who.plugins.sa wird zuerst eine Basisklasse definiert, die
vor allem eine Methode bietet den Benutzer anhand der Email-Adresse abzufragen.
Hierzu verwende ich eine Klassenmethode in meinem Model, da ich diese öfters
brauche, z.B. für eine "Passwort vergessen" Funktion. Ich habe dabei darauf
geachtet, dass die Abfrage im User-Model die SQLAlchemy-Funktion .one() benutzt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound class _BaseRepozeWhoOrchestraDdPlugin(object): """ Baseclass for the custom authenticator and metadata provider """ def __init__(self, user_class, dbsession): """ Setup the plugin. :param user_class: The SQLAlchemy/Elixir class for the users. :param session: The SQLAlchemy/Elixir session. """ self.user_class = user_class self.dbsession = dbsession def get_user(self, email): try: return self.user_class.by_email_address(email) except (NoResultFound, MultipleResultsFound): # As recommended in the docs for repoze.who, it's important to # verify that there's only _one_ matching userid. return None |
Als nächstes kommt das Plugin zur Authentifizierung. Die einzig benötigte
Methode hierbei ist authenticate.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | from zope.interface import implements from repoze.who.interfaces import IAuthenticator class OrchestraDbTeamUserAuthenticatorPlugin(_BaseRepozeWhoOrchestraDdPlugin): """ :mod:`repoze.who` custom authenticator for OrchestraDb """ implements(IAuthenticator) # IAuthenticator def authenticate(self, environ, identity): if not ('login' in identity and 'password' in identity): return None user = self.get_user(identity['login']) if user: if user.validate_password(identity['password']): return identity['login'] |
Nun noch das Plugin für den Metadata-Provider:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | from repoze.who.interfaces import IMetadataProvider class OrchestraDbTeamUserMDPlugin(_BaseRepozeWhoOrchestraDdPlugin): """ :mod:`repoze.who` custom metadata provider that loads the SQLAlchemy-powered object for the current user in the OrchestraDb. It loads the object into ``identity['user']``. """ implements(IMetadataProvider) def add_metadata(self, environ, identity): identity['user'] = self.get_user(identity['repoze.who.userid']) |
app_cfg.py anpassenIm letzten Schritt muss man natürlich noch dafür sorgen, dass die abgeleitete AppConfig auch verwendet wird:
1 2 3 | # from tg.configuration import AppConfig from orchestra.config.derived_app_config_auth import DerivedAppConfigAuth base_config = DerivedAppConfigAuth() |
So, ich hoffe das hilft jemandem weiter, der vor dem gleichen Problem stand. Anbei noch ein Zip der finalen Datei.
Da ich es bestimmt früher oder später wieder einmal brauche: Um bei meinem Surfstick das darauf befindliche Image zu aktivieren oder deaktivieren muss mann wiefolgt vorgehen:
Systemsteuerung -> Netzwerk -> Modem -> Nullmodem einstellen
Bei den PPP-Optionen muss man "PPP-Echopakete versenden" und "Über eine Terminalfentser verbinden" aktivieren. Nun nur noch auf verbinden klicken.
Im Terminalfenster zuerst echo eingeben, damit eine Ausgabe erfolgen kann.
Nicht erschrecken, man sieht noch nichts. Über den Befehl ATI erhält man
Firmewareversion und andere Angaben
CD Funktion deaktivieren at^u2diag=0 CD Funktion aktivieren at^u2diag=1
Quelle: http://mobile-surfstick.de/
Ich liebe SQLAlchemy um über Python auf Datenbanken zuzugreifen. Für ein
neues Projekt wollte ich so weit wie möglich auch das Datenbank-Schema mit etwas
mehr Disziplin angehen. Hierfür bietet sich natürlich
sqlalchemy-migrate an. Leider fand ich die Dokumentation an einigen
Stellen etwas schwach. Es wird gezeigt, wie man Scripte zur Migration erstellt
und es werden auch Fehlerquellen beleuchtet, aber es fehlt eine einfache
Anleitung in der z.B. aufgezeigt wird wie man am Besten Spalten ändert,
hinzufügt oder löscht. Google sei Dank bin ich dann auf dieses Tutorial
gestoßen. Ist schon etwas älter, es war für mich das fehlende Steinchen.
Folgt man beim Installieren von Turbogears 2 der Anleitung
erhält man unter Mac OS X 10.6 (Snow Leopard) eine Fehlermeldung bei dem
Modul Extremes. Der Workaround ist eigentlich sehr einfach: man installiert
zuerst die neueste Version von Extremes bevor man Turbogears installiert.
1 2 3 4 | easy_install Extremes […] easy_install -i http://www.turbogears.org/2.0/downloads/current/index tg.devtools […] |
Das letzte "große" Modul der static blog engine ist workbench.py. Die darin
enthaltene Klasse Workbench könnte man als die Ablaufsteuerung der sbe
bezeichen. Sie ruft nach bedarf die einzelnen vorgestellten Module bzw. Klassen
auf und ist für die Ausgabe der statischen HTML-Seiten sowie der Atom-XML-Datei
zuständig.
Die wichtigsten Methoden sind dabei:
update():
Sucht nach neuen und geänderten Artikeln und gibt diese ausrebuild_lastest_year():
erneute Ausgabe aller Artikel des zuletzt angelegten Jahresrebuild_year(year_as_string):
erneute Ausgabe eines bestimmten JahresIn jeder dieser Methoden wird zuerst ein Inventar erstellt, die betroffenen
Artikel gesucht und zwischengespeichert. Durch den Aufruf von deploy() wird
zunächst ein RenderingSet erstellt und alle darin enthaltenden
Elemente als HTML ausgegeben. Zur Ausgabe als HTML zählt natürlich auch das
Kopieren verknüpfter Dateien.
Bei Aufruf der Methode update wird im zusätzlich noch die Index-Datei
ausgegeben, der Atom-Feed neu aufgebaut und die Tag-Seiten aktualisiert. Daher
sind die Methoden rebuild_lastest_year und rebuild_year dazu gedacht
Änderungen an einem Template anzuwenden.
Für die Tag-Seiten gibt es noch besondere Methoden, um diese bei Fehlern oder
ähnlichem zu aktualisieren: rebuild_tags_of_latest_year(),
rebuild_tags_of_year(year_as_string) und rebuild_all_tags() die eigentlich
selbsterklärend sein sollten.
Als Todo steht eigentlich noch an, dass ich die Tag-Verwaltung in ein eigenes Modul auslagern wollte, ich mich aber noch nicht dazu aufraffen konnte. Mahl sehen.
Bei xkdc gibt es den Albtraum eines jeden Gamers zu sehen.

Machte mir große Freude.
So, habe es heute endlich geschafft die Kommentarfunktion einzubauen. War gar nicht so schwer - hatte ich auch nicht erwartet ;-). Danke geht natürlich an disqus.
Wie letzte Woche erwähnt, werde ich jetzt kurz etwas zu inbox.py
schreiben. Wie schon am Namen zu vermuten ist, handelt es sich hierbei um ein
Modul, dass neue Artikel bearbeitet. Hierzu stehen folgende Methoden der
Inbox-Klasse bereit:
scan()
listet alle Dateien im Eingangsverzeichnis auf und teilt diese in
Artikel-Dateien (enden mit einer Erweiterung wie "mkd" oder "html") und
andere Dateien auf, die Artikel werden anschließend anhand ihres
Erstellungsdatums sortiert: _sort_artciles_by_time_created()dispatch()
is das eigentliche Arbeitstier in diesem Modul. Zuerst wird ein Artikel
eingelesen. Dadurch werden auch alle Metaangaben in der Datei überprüft
beziehungsweise ergänzt. Der Eintrag wird dann anhand seines
Erstellungsdatums in das Artikelarchiv kopiert – inklusive der ergänzten
Angaben. Um die Sortierung später zu erleichtern erhält jeder Artikel eine
zweistellige Nummer vorangestelt, sofern er noch keine besitzt. Danach
werden die lokalen Verknüpfungen (siehe article.py) ermittelt
und die entsprechenden Dateien auch in das Artikelarchiv kopiert.cleanup()
Zu guter Letzt werden die bisher nur kopierten Dateien gelöscht.inbox.py und inventory.py spielen gut zusammen, sofern man die
Reihenfolge beachtet. Zuerst sollten die Einträge im Eingangskorb bearbeitet
werden und erst danach die Bestandsaufnahme erfolgen. Da eine Suche nach
geänderten Dateien durch inventory.py das Änderungs- und nicht das
Erstellungsdatum beachtet werden dadurch neue Artikel automatisch gefunden, ohne
das eine Datenübergabe zwischen den beiden Modulen erfolgen muss. Des weiteren
entfällt durch diese Reihenfolge das Einsortieren neuer Artikel (bzw. deren
Elternknoten) in die gefundenen Knoten.
Joel Spolsky hat eine nette Einführung zu Mercurial – einem "distributed source control management" geschrieben. Eine wirklich gute praktischeEinführung in das Thema. Ich habe mich vor einiger Zeit für die Konkurenz Bazaar entschieden – einfach weil es damals das beste Tutorial hatte :-). Als drittes System im Bunde sollte natürlich auch git erwähnt werden. Ihr sollt schließlich die Wahl haben.
Ich wundere mich etwas über den von unserem Gesundheitsminister Rösler (FDP) Vorschlag, dass die Medikamentenpreise nicht mehr alleine von der Pharma-Unternehmen festgelegt werden sollen. Nicht, dass ich eine Beschränkung der Medikamentenkosten zu Lasten der Industrie ablehnen würde, im Gegenteil. Gerade im internationalen Vergleich haben wir recht hohe Preise für Medikamente, nicht nur für neue sondern auch für "alteingesessene".
Wenn mich mein Gedächtnis nicht täuscht – kann ja passieren, werde auch immer älter – war doch gerade die FDP die Partei, die sich für eine freie, uneingeschränkte Marktwirtschaft eingesetzt hat. Jahrelang galt für sie die staatliche Einflussnahme auf die Märkte als unerwünscht – sofern damit keine "systemrelevanten" Betriebe gerettet wurden. Alles frei nach dem Motto "Der Markt wirds schon richten".
Was mich daher verwundert ist, dass sich gerade der "Shootingstar" der FDP für einen staatlichen Eingriff in einen bisher unregulierten Markt eintritt. Man könnte nun behaupten, dies sei eigentlich kein staatlicher Eingriff, da die Preise zwischen den Unternehmen und den Krankenkassen ausgehandelt werden sollen. Da dies jedoch nicht freiwillig geschieht – sonst hätten es die Pharmabranche längst selbst zur Rettung des Gesundheitssystem vorgeschlagen – sondern von der Politik vorgegeben wird lasse ich dieses Argument nicht gelten.
Auch der Hinweis die Pharmaunternehmen haben ein Monopol auf neue Medikamente und daher muss ein Eingriff erfolgen halte ich für etwas "anrüchig". Natürlich hat ein Unternehmen ein Monopol auf neu eingeführte Produkte – und dieses ist auch staatlich garantiert. Das ist für Medikamente ebenso vorhanden wie z.B. für die "Goldbären".
Was die Branche mit neuen Medikamenten jedoch (meistens) nicht hat ist ein Therapiemonopol. Daher finde ich vor allem ein Aspekt der Initiative von Herrn Rösler sehr gut: Die Pharmaunternehmen sollen während der einjährigen Verhandlungsphase einen Beweis für die (verbesserte) Wirksamkeit des Medikamentes vorlegen. Aber halt, hat sich nicht aufch die FDP gegen die Einführungeiner Positivliste ausgeprochen, die somit durch die Hintertür teilweise eingeführt wird?
Das größte Manko des Vorschlages ist jedoch, dass Verhandlungen nur über neue Medikamente geführt werden sollen. In wie weit sich die Krankenkassen gegenüber der Industrie durchsetzen kann und wie schlagkräftig das Schiedsgericht sein wird sich erst dann herausstellen, wenn die Initiative Wirklichkeit werden sollte.
Eigentlich wollte ich nach dem Modul inventory.py mit dem Modul
inbox.py weitermachen, da diese beiden zusammen gehören. Allerdings werde ich
einen Ausflug zur Artikelklasse einschieben, da einige Funktionen bei inbox.py
eine Rolle spielen.
Wie der Name schon vermuten lässt, handelt es sich bei der Klasse Article um
einen Eintrag im Blog. Anders jedoch als bei ArticleNodes handelt es
sich nicht um die logische Beziehung zu Elternelementen oder nachvolgenden
Artikel sondern um den Inhalt und entsprechende Meta-Angaben.
Ein Objekt der Klasse liest eine Datei (oder ein Datei-ähnliches Objekt) ein, stellt sicher dass alle Metaangaben stimmig sind, kann den eigentlichen Inhalt des Eintrages durch einen Parser schicken um eine HTML-Ausgabe zu erhalten und nach lokalen Verknüpfungen suchen. Aber der Reihe nach:
Der Aufbau eines Eintrages sieht wie folgt aus: Zuerst kommen Zeilen bei denen die Metaangaben stehen. Jeweils ein Schlüsselwort gefolgt durch einen Doppelpunkt und danach der jeweilige Wert der dem Schlüssel zugeordnet ist. Danach erfolgt eine Leerzeile und darauf der eigentliche Blog-Eintrag.
1 2 3 4 | Title: static blog engine – v – article.py Tags: sbe, python Eigentlich wollte ich nach dem Modul […] |
Die folgenden Metaangaben werden momentan verstanden. Groß-/Kleinschreibung ist nicht relevant und fehlende Angaben werden anhand von Einstellungen oder Dateieigenschaften gesetzt.
| Schlüssel | Beschreibung |
|---|---|
| Title | der Titel des Eintrages |
| Author | der Autor |
| Created | Zeitpunkt, wann der Eintrag erstellt wurde, Format: 2010-03-25T11:49:46 |
| Tags | Tags, durch ein Komma getrennt |
| Comments | On oder Off, schaltet Kommentare an oder aus, noch nicht verwendet |
| Trackback | On oder Off, schaltet Trackbacks an oder aus, noch nicht verwendet |
| TagURI | eindeutige Identifizierung des Eintrages, wird automatisch ermittelt |
Ist kein Titel für den Eintrag vorhanden wird innerhalb des Textes nach einer
Überschrift gesucht und die erste die gefunden wird verwendet. Wurde keine
ermittelt wird ein Standardwert aus der Konfiguration benutzte. Für den
Schlüssel Created wird das Erstellungsdatum abgefragt, sofern es sich um ein
Dateiobjekt handelt.
Momentan werden nur zwei Parser unterstützt: Markdown und einen Pseudoparser für HTML – dabei wird einfach der "Text" der Datei unverändert zurückgegeben. Die Unterstützung für andere Parser ist einfach zu bewerkstelligen, sie müssen einfach nur (X)HTML zurückgeben können. Den erstellten HTML-Code wird zwischengespeichert, da es sich meistens um eine relativ aufwendige Prozedur handelt.
Zuerst muss ich klären was ich als "Lokale Links" verstehe. Falls ein Bild mit
einem Eintrag verknüpft wird sollte dies auch einfach in die Inbox gelegt werden
und mit dem Artikel zusammen an den richtigen Ort verschoben werden. Allerdings
gibt es auch Verknüpfungen – z.B. zu einem immer wiederkehrenden Element – die
absolut sind (http://example.com/a_picture.jpg) oder sich auf ein anderes
Verzeichnis verweisen (../previous_day/a_picture.jpg). Nach diesen sollte
nicht gesucht werden. Ich habe für diese den Namen "Lokale Links" gewählt und
sie haben ein recht einfaches Unterscheidundsmerkmal: sie enthalten keinen
Slash /. Eine Art von Links – die in ein Unterverzeichnis
(photos/a_picture.jpg) fällt dabei unter den Tisch obwohl diese durchaus
kopierenswert wären. Da dies meiner Ansicht nach nicht allzuoft vorkommt, kann
man ein Unterverzeichnis auch mal von Hand kopieren oder verschieben. Ein
Artikelobjekt stellt eine Methode zur Verfügung, welche die lokalen Links
extrahiert und zurückgibt.
Die Scanner-Klasse im Modul inventory.py ist eine der wichtigsten Grundlagen
für die "static blog engine". Zuerst sucht sie alle bereits vorhandenen Artikel
und erstellt danach die Jahr/Monat/Tag-Struktur anhand der Artikel. Warum habe
ich diese Weg gewählt und nicht gleich beim Scannen auch Jahr, Monat und Tag
miterstellen lassen? Ich stand vor dem Problem, dass falls der letzte Artikel
eines Tages (Monates / Jahres) gelöscht würde, ich die komplette Struktur
nochmals durchsuchen müsste. Da übergeordnete Objekte nur von vorhandenen
Artikeln abgeleitet werden entfällt dieses.
Des weiteren bietet die Scanner-Klasse die Möglichkeit alle Artikel eines
Jahres oder alle geänderten Artikel seit einem Zeitpunk (Unixtimestamp) als ein
Python set zurückzugeben.
Das Moul enthält noch eine weitere Klasse RenderingSet. Übergibt man dieser
ein Python-set mit Artikeln ermittelt diese alle Knoten (siehe nodes.py)
die ausgegeben werden müssen. Dies sind nicht nur die Artikel und übergeordnete
Objekte selbst, sondern auch die vorherigen und nachfolgenden Artikel – es
könnte sich um einen neuen Artikel handeln oder der Titel könnte geändert sein.
Desweiteren müssen auch vorheriger und nachfolgender Tag / Monat / Jahr neu
ausgegben werden, da es sich um einen neuen Tag / Monat / Jahr handeln könnte.
Das Ausgeben von vorherigen bzw. nächsten Objekten müsste nicht sein, wenn man überprüfen würde, ob der Tag / Monat / Jahr schon existiert. Allerdings wäre dann eine Art Index der bereits vorhandenen Objekten nötig, gegen die man einen Vergleich anstellt. So finde ich es viel einfacher.
Mit die erste und schwierigste Frage war, wie ich einzelne Jahre, Monate, Tage und Artikel repräsentieren sollte. Auf der einen Seite wollte ich den jeweils vorhergehenden und nachfolgenden Artikel (oder Jahr oder Monat oder Tag) einfach abfragen können, auf der anderen Seite gibt es natürlich eine Art Baumstruktur. Jeder Artikel ist einem Tag zugeordnet, dieser wiederum einem Monat und dieser natürlich einem Jahr.
Die erste Anforderung würde für eine Linked List sprechen, die zweite für eine Baum-ähnliche Struktur. Da beide Lösungen nicht wirklich optimal für meine Zwecke sind, habe ich mich für eine einfache Zwitterlösung entschieden – zudem hatte ich Lust zum programmieren :-)
Noch eine kleine Randnotiz: Da Artikel in Ordnern mit der Struktur
Jahr/Monat/Tag abgelegt werden, wird einfach der relative Pfad (z.B.
2010/03/23/02-sbe-zum-dritten.mkd zur Identifizierung benutzt. Dies
vereinfacht auch die Ermittlung der Bestandteile des Pfades Dank des os.paht
Moduls.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class ScannerNode(object): """ Basic Node object for hierarchical linked list """ def __init__(self, id): self.id = id self.prev = None self.next = None self.parent = None self.children = [] def append_child(self, child_node): """ appends a child node to the children list and links the child to the parent """ self.children.append(child_node) child_node.parent = self def get_deploy_path(self): """ returns the path to wich the node shall be deployed """ if self.id.endswith(os.path.sep): return self.id return self.id + "/" def get_name_parts(self): """ returns the parts of the identifier """ return self.id.split(os.path.sep) def get_base_name(self): """ returns the basename of the identifier """ return os.path.basename(self.id) |
Dies stellt die Basisklasse der einzelnen Einträge (Nodes) dar. Über die
Attribute .prev und .next lässt sich einfach auf den vorherigen bzw. nächsten
Eintrag zugreifen; .parent ermöglicht den Zugriff auf das Elternelement und
in .children[] können die entsprechenden Kinder-Knoten abgelegt werden.
Es gibt für Artikel, Tage, Monate und Jahre einzelne Klassen die alle von
ScannerNode abgeleitet sind und weiter Methoden enthalten die den Zugriff auf
Kinderelemente erleichtern oder im Falle von ArticleNode den entsprechenden
Blog-Artikel einlesen und zwischenspeichern können.
Bei dieser Struktur handelt es sich um keine klassische Linked List, da die einzelnen Knoten in einer Liste gesammelt werden und nur untereinander eine Verknüpfung erhalten. Eine klare Baumstruktur ist es auch nicht, da die Verknüpfung von Kinderknoten untereinander Elternknoten überspringen können. Ok, etwas verwirrend, vielleicht eine Grafik dazu?

Nachdem ich im ersten Teil den Ablauf und die Funktionsweise der static blog enginge kurz erläutert habe, gehe will ich nun etwas mehr auf die Umsetztung eingehen. Wie bereits erwähnt, dient dies auch als Dokumentation für mich.
Fangen wir doch einfach mal mit einer Auflistung der vorhandenen module an:
| Modul | Kurzbeschreibung |
|---|---|
| article.py | Modul für einzelne Blog-Artikel |
| cli.py | Command Line Interface |
| commons.py | in mehreren Modulen verwendete Funktionenen |
| inbox.py | zur Bearbeitung des Eingangs |
| inventory.py | Erfassung der vorhandenen Blog-Artikel |
| nodes.py | Repräsentation von Jahren, Monaten, Tagen und Artikeln |
| progress.py | Darstellung eines Fortschrittsbalken |
| settings.py | Ermittlung der Einstellungen |
| workbench.py | Bringt alles obrige zusammen |
Zugegeben, nur mit der Kurzbeschreibung kann man nicht viel anfangen. Ganz grob
lassen sich die Module in "Arbeitstiere" (article.py, inbox.py,
inventory.py, nodes.py, workbench.py) und "Hilfsmodule" (cli.py,
commons.py, progress.py, settings.py) einteilen.
Ich in den nächsten Artikeln die einzelnen "Arbeitstiere" näher beleuchten. Ob ich auch mit den Hilfsmodulen so verfahre oder diese irgendwo mit einflechte, wird sich noch zeigen.
Ich bin wieder zurück von einem sehr anstrengenden aber auch sehr schönem Wochenende in Berlin,an dem ich ORSO – The Rock Symphony Orchestra im Friedrichstadpalast tatkräftig zur Seite stand. Es gibt einen sehr netten, wenn auch kurzen (Vorab-)Bericht darüber beim RBB
Das Stopschild für Kirchen wirft eine Frage auf: muss jeder Kirchenbesucher seine Adresse hinterlassen sobald die Vorratsdatenspeicherung wieder kommt? Oder stellt man jeden, der Kirchensteuer zahlt unter Pauschalverdacht?
Die Idee zu einer Möglichkeit Webseiten statisch zu erstellen ist nicht neu und hat bei mir auch lange "gegärt". Ich wusste von Anfang an, was ich erreichen wollte, habe mich aber beim Wie doch etwas schwer getan. Ich glaube es ist nun der fünfte oder sechste Versuch, bei dem ich soweit zufrieden bin, dass ich ihn nun auch wirklich einsetze.
Dies hier sind die Vorgaben, die ich mir selbst gestellt hatte: - keine Datenbank-Anbindung, nach Möglichkeit nur Text-Dateien und Ordner - einfache CLI-Anwendung, kein Web-Interface zum erstellen der Artikel - Artikel sollen in Markdown (ich liebe es) und / oder HTML geschrieben werden - neue Artikel sollen zusammen mit verknüpften Dateien wie Bilder oder PDFs in einem Ordner abgelegt werden und durch die Anwendung überprüft und an ihrem Zielort verschoben werden. - geänderte Artikel sollen automatisch erkannt werden - die Möglichkeit Tags zu setzten sollte von Anfang an vorhanden sein
Natürlich sollten auch Kommentare und Trackbacks bzw. Pingbacks eingebunden werden, aber das habe ist nicht die höchste Priorität. Da es sich bei Kommentaren und Pingbacks um dynamische Elemente handelt werde ich diese über einen externen Service einbinden, doch dazu später mehr.
Gut, nun aber zum praktischen Ablauf: Zuerst erstellt man einen neuen Artikel in einem Eingangs-Ordner. Der Artikel enthält eine Bild-Verknüpfung und ist mit dem Tag "Beispiel" versehen.

Durch den Aufruf sbe update oder die Kurzform sbe (update ist das
Standard-Kommando) werden zuerst die Artikel im Eingangs-Ordner bearbeitet. Es
wird dabei überprüft ob alle Angaben wie Autor, Tags, Titel, etc. und
Verknüpfungen vorhanden sind. Der Artikel und das verknüpfte Bild wird dann in
den entsprechenden Ordner verschoben. Dabei bekommt der Artikel eine Nummer
vorgesetzt um später die Reihenfolge innerhalb eines Tages einfacher ermitteln
zu können.

Nun werden alle Artikel gesucht, die sich seit der letzten Aktualisierung geändert haben. Zugegeben in diesem Beispiel tut sich da nicht viel. Alle geänderten und neue Artikel werden nun als HTML-Datei ausgegeben. Natürlich wird dabei auch das Bild mit kopiert.

Danach werden die Dateien für den Tag, den Monat und das Jahr erstellt bzw. geändert.

Es fehlt noch die Startseite, sowie der Atom-Feed.

Als letztes werden die Tag-Dateien ausgegeben. Dazu wird für jeden gefundenen Tag eine extra Liste gespeichert um nicht jedes mal alle Artikel einlesen zu müssen.

Der deploy-Ordner kann man nun einfach veröffentlichen, in dem man ihn z.B. mit FTP auf einen Webspace lädt. Über folgende Pfade kann man auf die "Einzelteile" des Blogs zugreifen – vorausgesetzt der Server ist so konfiguriert, dass er beim Aufruf eines Ordners automatisch die "index.html"-Datei ausliefert:
| Pfad | Beschreibung |
|---|---|
| / | Startseite des Blogs |
| /2010/ | Übersichtsseite für das Jahr 2010 |
| /2010/03/ | Übersichtsseite für den März 2010 |
| /2010/03/13/ | Übersichtsseite für den 13. März 2010 |
| /2010/03/13/01-ein-artikel.html | Unser erstellter Artikel |
| /tags/ | Übersichtsseite der Tags |
| /tags/beispiel.html | Übersichtsseite des Tags "beispiel" |