Drupal

Seit dem 01.04.2008 beschäftige ich mich mit Drupal, Version 5.7. Auf diesen Seiten werden meine Erfahrungen und Projekte protokolliert.
Für einen Drupal-Anfänger ist das System nicht unbedingt einfach zu verstehen. Standardfunktionen sind ausreichend gut beschrieben, aber was über die Standard-Konfiguration hinausgeht ist in Teilen problematisch.

Zur "Einstimmung" habe ich das Buch "Drupal. Community-Websites entwickeln und verwalten mit dem Open Source-CMS", Verlag: Addison-Wesley, München (März 2006), ISBN 3827323215, von Hergen Graf gelesen.
Das Buch steht auch schon als freier Download auf Addison-Wesley zur Verfügung. !! Das Buch beschreibt die Administration von Drupal 4.7, ist aber ein guter Einstieg. !!
Da die Standardfunktionen für meine Anwendungszwecke nicht ausreichen, stand auch gleich die Entwicklung eigener Module mit auf dem Programm. Hierfür habe ich mit dem Buch "Pro Drupal Development", von John K. VanDyk und Matt Westgate, Computer Bookshops (Taschenbuch - 23. April 2007), eingearbeitet. Dabei habe ich festgestellt das die Dokumentation von Drupal und die Beschreibungen in diesem Buch, insbesondere für Anfänger, nicht immer eindeutig und leicht zu verstehen sind.
Beim Entwickeln tauchen immer Fragen auf, die (wovon ich ausgehe) sich auch andere Entwickler schon gestellt und beantwortet haben. Voller Zuversicht wendet man sich dann an das deuschprachige Forum und wird dort allerdings nicht immer fündig.

Komplexere Fragen werden hier kaum beantwortet. Die folgenden Seiten sind also eher ein Protokoll meiner eigenen Bemühungen mich in Drupal einzuarbeiten.

Erfahrungen mit Drupal

Hier werde ich meine Erfahrungen niederlegen, die ich bein einarbeiten in Drupal sammel.
Mein Hauptaugenmerk liegt dabei auf der Programmierung von Erweiterungen (neue Module).

Die Drupal-Philosophie und wie ich sie sehe
Vorweg - dies ist absolute meine eigene Meinung und soll keine Kritik an anderen Personen sein.
Drupal bietet eine Menge Möglichkeiten in das System einzugreifen, dafür gibt es Programmierschnittstellen die Hook's genannt werden. Nach meinen bisherigen Erfahrungen kann jede Aktion über solche Schnittstellen gemacht werden und es braucht direkt nicht in bestehende Scripte eingegriffen werden.
Vorgefertigte Module können einem einiges an Arbeit abnehmen oder Personen die des Programmierens nicht mächtig sind, neue Möglichkeiten eröffnen. Jedes Modul bringt aber auch unnötigen Balast mit und oft erfüllt es nicht zu 100 % meine Anforderungen. Von daher programmiere ich vieles neu, für das es vorgefertigte Module gibt. Die meisten hier vorgestellten Erklärungen beziehen sich auf Drupal in der Version 5.7.

Ich bemühe mich auch die Unterschiede zu Drupal 6 herauszuarbeiten. Diese sind dann durch eine andere Hintergrundfarbe gekennzeichnet.

Das erste eigene Modul

Auch für Drupal 6.x

Um ein eigenes Modul für Drupal zu erzeugen braucht es nicht viel. Grundsätzlich sollen eigene Module im Drupal-Verzeichnissystem in sites/all/modules/eigene_Bezeichnung erstellt werden. Wenn das Verzeichnis modules nicht existiert, lege es einfach an.

Wenn Module nicht für alle Seiten (bei Multisite-Systemen) benötigt werden, kann auch für eine spezielle Seite ein eingenes Verzeichnis angelegt werden. Dazu wird im Verzeichnis sites ein Unterverzeichnis mit der Bezeichnung der neuen Seite eingerichtet, z. B. sites/webauftritt2.de. Für ein eigenes Modul werden folgende Dateien benötigt:

*.txt
optional - Zur Beschreibung der Funktionen die das Modul ausführt. Zudem sollten hier nochmal Installationsvoraussetzungen und Besonderheiten erklärt werden.
*.info
zwingend - Das Info-File ist notwendig um von Drupal erkannt zu werden. Es beschreibt die Abhängigkeiten zu anderen Modulen, die Versionsnummern, Ersteller, usw.. Der Name muss innerhalb der Drupal-Anwendung eindeutig sein.
*.install
optional - Das Install-File wird bei der Erst-Installation oder bei der Deinstallation ausgeführt. In der Regel werden hierüber Datenbanktabellen bzw -inhalte angelegt, die vom Modul benötigt werden. Es können aber genauso gut System-Variablen gesetzt bzw. entfernt werden. Auch Updates können hier, als Funktion, eingefügt werden.
*.module
zwingend - Das Module-File stellt die eigentliche Funktionalität des Modules zur Verfügung. Ein Modul kann über mehrere Module-Files verfügen.

Zusätzlich können weitere Dateien notwendig sein. So können ohne Probleme .inc-Dateien oder eigene CSS-Dateien eingebunden werden. Schaut man sich das Modulverzeichnis an:

Wie man sieht ist hier nur eine Standard-Installation ohne zusätzlich Module. Im ersten Versuch geht es nur darum, ein Modul zu schreiben das in das Admin-Menü eingeblendet wird und eine Ausgabe generiert.
Das Modul soll noch nichts großartiges veranstalten. Das erste Modul soll "ersterinhalt" heißen.

Das .info-File

Auch für Drupal 6.x

Das .info-File kann mit jedem Editor geschrieben werden. Ich benutze die Freeware-Version von PHP-Designer von MK-Soft. Damit erstelle ich auch alle anderen Scripte (wenn nicht weise ich darauf hin). Das .info-File folgt der .ini-Syntax. Genaueres kann man unter php.net erfahren. Die genaue Definition ist unter Drupal-Info-File-Beschreibung nachzulesen. Hier das wichtigste: Das File beginnt mit der Beschreibung ; $Id$ Das ungewöhnliche daran ist das die Komentare mit ein ; eingeleitet werden. Nach dem ; folgt die erste Erklärung, diese beginnt und endet mit einem $. Hier ein Beispiel:

; $ Id: ersterinhalt.info, UwBach, 2008/04/14 $
name = ersterinhalt
description = Erzeugt einen Eintrag im Admin-Menue
package = "AA"
version = "$Name$"
project = "Uwe_Test"
datestamp = "1207381502"

Nun noch einige Erklärungen zu den einzelenen Punkten:

name
Der Name des Modules - Pflichtangabe. Hier muss eine "computerfreundliche" Schreibweise gewählt werden. Die Bezeichnung wird sich durch alle weiteren Code-Bestandteile ziehen, wählt daher eine eindeutige und einfache Bezeichnung. Es dürfen keine Leerschritte, Zahlen oder Sonderzeichen drin vorkommen, Ausnahme der Unterstrich. Nur der erste Buchstabe darf groß geschrieben werden.
description
Die Beschreibung in einer Zeile (max. 255 Zeichen) - Pflichtangabe. Vermeide bitte Anführungsstriche, egal ob einfache oder doppelte
dependencies
Abhängigkeiten - optional. Hier können Abhängigkeiten zu anderen Modulen beschrieben werden. Wichtig ist das um die Funktionen eigener Module zu gewährleisten, falls man diese im eigenen Code aufruft. Das Modul läßt sich dann erst aktivieren, wenn die beschriebenen Module im Administrationsbereich zur Verfügung stehen und aktiviert sind.
package
Zuweisen des eigenen Moduls zu einem bestimmten Package - optional. Im Beispeil ist das Package "AA" gewählt worden um es in der Administrationsobfläche ganz oben zu positionieren.
version
Versionsangabe des Moduls. Die richtige Versionsnummer wird von "Drupal.org" vergeben, wenn das Modul hochgeladen wird. Dann sollte der Name $Name$ sein. Wird das Modul nicht hochgeladen kannst du machen was du willst.
project
nur für "Drupal.org". Das bitte nicht verwenden. Bei bedarf wird Drupal.org diese Bezeichnung für den Update bestimmter Packages verwenden.
datestamp
Der Zeitpunkt der Erstellung (Unix-Timestamp)
Für Drupal 6 ist ein weiterer Eintrag notwendig. Es muss noch das System angegeben werden für das das Modul gedacht ist. Es ist dabei erlaubt die nur die Hauptversion zu kennzeichnen und die Unterversion als x anzugeben, z. B. core = 6.x. ; $ Id: ersterinhalt.info, UwBach, 2008/04/14 $
name = ersterinhalt
description = Erzeugt einen Eintrag im Admin-Menue
package = "AA"
version = "$Name$"
project = "Uwe_Test"
datestamp = "1207381502"
core = 6.x

Jetzt ist alles bereit und wir können ein Verzeichnis anlegen - sites/all/modules/ersterinhalt. Darin wird unsere Datei unter dem Namen ersterinhalt.info abgespeichert. Das war der erste Schritt ...

Das .module-File

Auch für Drupal 6.x

Der nächste Schritt ist das .module-File zu erstellen. In diesem File sind alle Funktionen abgelegt. Um einen Eintrag im Administration-Menü zu erstzeugen brauchen wir nicht viel: <?php // $ID$
/**
* @file
* Irgendeine Beschreibung
*/  
Den Snipsel speichern unter ersterinhalt.module und fertig. Das Script muss natürlich wieder im Verzeichnis sites/all/modules/ abgespeichert werden.

Ruft man jetzt den "Administer -> Site building -> Modules" (h t t p://.../admin/build/modules) auf, sieht man schon das neue Modul.

Natürlich haben wir noch keine Funktionalitäten zur Verfügung und guter Stiel ist das auch nicht.
Es gibt hier aber schon einige Besonderheiten zu sehen. So wird die Datei, wie bei PHP-Scripten üblich mit <?php eröffnet, allerdings fehlt das schließende Tag ?>. Der erste Dokumentationsblock /** muss eine Leerzeile zur //$Id$ haben.
Des weiteren soll genau ein Leerschritt in den folgenden Zeilen zwischen dem Zeilenbeginn dem * und ein Leerschritt zur Beschreibung eingehalten werden.
Das ist wieder nur dann erforderlich, wen ggf. mal ein Script an "Drupal.org" gegeben werden soll. Hierfür ist auch das Tag @file gedacht. Es sollte schon darauf geachtet werden, es kann sonst zu Problemen mit bestimmten Includes führen (siehe Drupal.org).

Aber nun soll es darum gehen Inhalte in das .module-File zu bringen. Dafür muss ich etwas weiter ausholen ...

Zum Nachmachen ist das Beschriebene im Anhang ..

AnhangGröße
ersterinhalt.zip630 Bytes

Das hook-System in Drupal

Drupal sollte von Anfang an modular aufgebaut werden. Die Schwierigkeit bei einem solchen System sind die Schnittstellen oder wie bekommt man die unterschiedlichsten Module in ein Ganzes integriert, inbesondere unter dem Aspekt das man die Funktionalitäten ja noch gar nicht kennt die dort abgebildet werden sollen.

Drupal hat dafür das hook-System (engl. Hook = Haken oder auch Greifer). Im Gegensatz zu vielen anderen Systemen werden dabei die Funktionen in Drupal registriert und nicht über "Listener", die praktisch im in Lauerstellung sind, aufgerufen.
In Drupal werden die Funktion aktiv aufgerufen. Alle verfübaren Hooks sind in der Drupal API - Hook Beschreibung beschrieben. Die Problematik für Neueinsteiger ist die Vielzahl der Hooks. Zum Teil sind diese leider auch nicht besonders gut beschrieben.
Um die Hooks zu verwenden, werden in den eigenen .module-Files Funktionen eingefügt die die Hooks verwenden. Die Funktionen werden dabei immer mit dem eigenen Scriptnamen und der Hook-Bezeichnung versehen (z. B. ersterinhalt_menu(), ....).

Wir werden uns die Arbeitsweise und die Implementierung an einigen Hooks genauer betrachten. Anfangen werden wir mit dem Menu-Hook ...

Das Menü-System

Den Hook hook_menu() als reines Menü-System zu bezeichnen trifft es nicht genau. Das hook_menu() ist einiges mehr. Jedes Script braucht einen Punkt über den es aufgerufen werden kann. Dies wird über das hook_menu() erreicht. Zudem wird hier auch festgelegt welche Funktion, im eigenen Script, die Antwort auf einen Request der Seite zurückgibt. Weiter können die Berechtigungen festgelegt werden, die es erlauben bestimmte Funktionen bzw. Menüpunkte aufzurufen. Dabei ist jeder Menüpunkt ein eigenes Array. Das Array besteht aus folgenden Komonenten:

path
Der Pfad über den der Punkt zu erreichen ist. Pflichtangabe
title
Der übersetzte Titel im Menü. Pflichtangabe
callback
Die Funktion die aufgerufen wird, wenn der User den Menüpunkt aufruft.
callback arguments
An array of arguments to pass to the callback function.
access
Ein Boolean-Wert der angibt ob der Menüpunkt angezeigt werden soll. Gibt eine Entscheidung an user_access(). If omitted and "callback" is also absent, the access rights of the parent menu item will be used instead.
weight
Ist ein Integerwert der bestimmt an welcher Stelle im Menü der Punkt erscheint (Wertebereich -10 bis 10; Default 0. Wenn hier keine Angabe gemacht wurde, wird alphabetisch sortiert.
type
Gibt an in welchem Menü der Punkt erscheinen soll. A bitmask of flags describing properties of the menu item. Many shortcut bitmasks are provided as constants in menu.inc:
MENU_NORMAL_ITEM
Anzeige im normalen Menü-Baum. Die Position kann vom Administrator verändert werden.
MENU_ITEM_GROUPING
Item groupings are used for pages like "node/add" that simply list subpages to visit.
MENU_CALLBACK
Callbacks simply register a path so that the correct function is fired when the URL is accessed.
MENU_DYNAMIC_ITEM
Dynamic menu items change frequently, and so should not be stored in the database for administrative customization.
MENU_SUGGESTED_ITEM
Modules may "suggest" menu items that the administrator may enable.
MENU_LOCAL_TASK
Erzeugt einen lokalen Tab.
MENU_DEFAULT_LOCAL_TASK
Alle Seiten mit Tab's benötigen einen "default" Tab. Dieser wird immer zu Anfang aufgrufen.

Fehlt der "type", wird MENU_NORMAL_ITEM angenommen.

Jetzt aber ein wenig Code:

/**
* Einhängen in das Menue
*/
function ersterinhalt_menu($may_chache)
{
$items = array();  
if(!$may_chache)
{
$items[] =
array(
'path' => 'ersterinhalt',
'title' => t('Erster Inhalt'),
'callback' => 'ersterinhalt_seiteninhalt',
'callback arguments' => array(''),
'access' => TRUE,
// user_access('anonymous user'),
'type' => MENU_NORMAL_ITEM );
}  
return $items;
}  
 
/**
* Die Callback-Funktion
*/
function ersterinhalt_seiteninhalt()
{
return t('Hallo   Mein erster Seiteninhalt.   '.date('d.m.Y H:i:s', mktime()));
}
Jetzt sollte im User-Menü der Menüpunkt erscheinen und die Callback-Funktion ersterinhalt_seiteninhalt() sollte einen Inhalt rausgeben.

Über das Administrationsmenü kann der Punkt dann gesondert verschoben werden, so das er an der richtigen Stelle erscheint:

Dabei werden die Informationen in der Datenbank gespeichert und beim nächsten Aufruf wieder ausgelesen. Es ist gut zu wissen wie die internen Abläufe zum Menü-Aufbau sind, daher eine kleine Übersicht:
Damit ist auch klar das MENU_DYNAMIC_ITEM nicht in der Datenbank gespeichert werden können. Diese Funktion ist wichtig, wenn Menüs aus eigenen Datenbeständen erstellt werden sollen - z. B. Produktkatalogen. Ferner ist es möglich für Menüpunkte Unterpunkte zu definieren. Dazu wird in der Funktion hook_menu(), in unserem Beispiel function erster_inhalt() ein weiterer Menüpunkt definiert. function ersterinhalt_menu($may_chache)
{
$items = array();
 
if(!$may_chache)
{
$items[] = array
(
'path' => 'ersterinhalt',
'title' => t('Erster Inhalt'),
'callback' => 'ersterinhalt_seiteninhalt',
'callback arguments' => array(''),
'access' => TRUE,
'type' => MENU_NORMAL_ITEM,
);
// Hier wird der erste Unterpunkt definiert
$items[] = array
(
'path' => 'ersterinhalt/upunkt',
'title' => t('Erster Unterpunkt'),
'callback' => 'ersterinhalt_ersterupunktinhalt',
'callback arguments' => array(''),
'access' => TRUE,
'type' => MENU_NORMAL_ITEM
);  
}  
 
return $items;
}  
 
/**
* Die Callback-Funktion des ersten Seiteninhaltes
*/
function ersterinhalt_seiteninhalt()
{ return t('Hallo
Mein erster Seiteninhalt.
'.date('d.m.Y H:i:s', mktime()));
}  
 
/**
* Die Callback-Funktion für den ersten Unterpunkt
*/
function ersterinhalt_ersterupunktinhalt()
{
return t('Hallo
Mein erster Seiteninhalt im Unterpunkt.
'.date('d.m.Y H:i:s', mktime()));
}

Sollte das Menü über die Administrationsoberfläche geändert worden sein, werden die neuen Werte in der Datenbank (Tabelle menu) gespeichert. Werden dann Änderungen im Code gemacht, die die Sichtbarkeit oder Bezeichnung beeinflussen, kann es passieren das diese nicht korrekt umgesetzt werden. Hier einfach ein Update der Struktur durchführen (Verwalten -> Strukturieren -> Modul => Link update.php oder ./update.php). Drupal überprüft dann alle Module auf Änderungen und trägt diese neu in die Datenbank ein.

Seiten mit Tab's

Eine besondere Form des Menüs sind Seiten mit Tabs. Tabs können in zwei Varianten erstellt werden; einmal als Reiter und einmal als Link-Unterpunkte.

(Zur besseren Sichtbarkeit wurde das Themes gewechselt) Hier erst einmal der Code:

/**
* Einhängen in das Menue
*/
function ersterinhalt_menu()
{
$items = array();
 
if(!$may_chache)
{
// Der Hauptmenüpunkt
$items[] = array
(
'path' => 'ersterinhalt',
'title' => t('Erster Inhalt'),
'callback' => 'ersterinhalt_seiteninhalt',
'callback arguments' => array(''),
'access' => TRUE,
'type' => MENU_CALLBACK,
);
 
// Der erste Reiter
$items[] = array
(
'path' => 'ersterinhalt/task',
'title' => t('Erster Unterpunkt'),
'access' => TRUE,
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -1,
);
 
// Der zweite Reiter
$items[] = array
(
'path' => 'ersterinhalt/nr1',
'title' => t('Erster Tab'),
'callback' => 'ersterinhalt_upunkt_nr1',
'access' => TRUE,
'type' => MENU_LOCAL_TASK,
);
 
// Der dritte Reiter
$items[] = array
(
'path' => 'ersterinhalt/nr2',
'title' => t('Zweiter Tab'),
'callback' => 'ersterinhalt_upunkt_nr2',
'access' => TRUE,
'type' => MENU_LOCAL_TASK,
);
 
// Der erste Unterunterpunkt als Link
$items[] = array
(
'path' => 'ersterinhalt/task/upunkt1',
'title' => t('Erster Unterunterpunkt'),
'access' => TRUE,
'type' => MENU_LOCAL_TASK,
'callback' => 'upunkt1',
);
 
// Der zweite Unterunterpunkt als Link
$items[] = array
(
'path' => 'ersterinhalt/task/upunkt2',
'title' => t('Zweiter Unterunterpunkt'),
'access' => TRUE,
'type' => MENU_LOCAL_TASK,
'callback' => 'upunkt2',
);
}
 
return $items;
}  
 
function ersterinhalt_seiteninhalt()
{
return t('Hallo
Mein erster Seiteninhalt.
'.date('d.m.Y H:i:s', mktime()));
}  
 
function ersterinhalt_ersterupunktinhalt()
{
return t('Hallo
Mein erster Seiteninhalt im Unterpunkt (TAB).
'.date('d.m.Y H:i:s', mktime()));
}  
 
function ersterinhalt_upunkt_nr1()
{
return t('Hallo
Mein erster Tab-Inhalt.
'.date('d.m.Y H:i:s', mktime()));
}

 
function ersterinhalt_upunkt_nr2()
{
return t('Hallo
Mein zweiter Tab-Inhalt.
'.date('d.m.Y H:i:s', mktime()));
}  
 
function upunkt1()
{
return 'Erster Unter-Unterpunkt';
}  
 
function upunkt2()
{
return 'Zweiter Unter-Unterpunkt';
}

In den ersten beiden Arrays, wird zum einen der Menüpunkt definiert und zum anderen der Default-Tab festgelegt.
// Der Hauptmenüpunkt
$items[] = array
(
'path' => 'ersterinhalt',
'title' => t('Erster Inhalt'),
'callback' => 'ersterinhalt_seiteninhalt',
'callback arguments' => array(''),
'access' => TRUE,
'type' => MENU_CALLBACK
);
// Der erste Reiter
$items[] = array
(
'path' => 'ersterinhalt/task',
'title' => t('Erster Unterpunkt'),
'access' => TRUE,
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -1,
);

Die Werte des Hauptmenüpunktes unterscheiden sich nur in der Definition des "type", der hier auf MENU_CALLBACK gesetzt. Das zweite Array hat einen erweiterten path, kein definiertes callback und der type ist auf MENU_DEFAULT_LOCAL_TASK gesetzt. Damit wird die Tab als Default-Tab definiert. Die nächsten beiden Tabs sind wie bisher üblich definiert. Der path ist als Unterpfad des Menüpunktes definiert - der type ist auf MENU_LOCAL_TASK gesetzt. Beide Tabs haben jeweils eigene Callback-Funktionen. Die beiden Link-Tabs ("Erster Unterunterpunkt", "Zweiter Unterunterpunkt") unterscheiden sich nur durch die path-Angabe von den echten Tabs. Wie auch sonst im Menü kann die Reihenfolge der Einträge durch weight beeinflußt werden (siehe "Erster Unterpunkt"). Fehlt der Wert wird alphabetisch sortiert.

Block mit ungelesenen Nodes erstellen

Ich habe versucht mit Views einen Block mit ungelesenen Nodes eines Users zu erstellen. Das scheiterte an zwei Dingen:

  1. die Tabelle {history} ist nicht in Views integriert und
  2. die Berechtigungen einen Node zu lesen können mit Views nicht korrekt abgefragt werden.

Daher habe ich mich entschlossen ein eigenes Modul zu schreiben, dass einen Block erzeugt der die ungelesenen Nodes anzeigen kann. Die Vorgaben sind:

  • Konfiguration der einzubeziehenden Node-Types
  • Angabe von zurückliegenden Zeiten der Node-Erstellung
  • Berücksichtigung von Berechtigungen (View) aus dem Modul "node_privacy_byrole"
  • Rollenbasierte Berechtigung den Block anzuzeigen

Herausgekommen ist ein eigenes Modul "unreaded_nodes":

<?php //; $ Id: unreaded_nodes.info, UwBach, 2008/08/04 $  
 
/**
* @file Erstellen eines Blockes mit nocht gelesenen Nodes
* Version 0.1
*
* Erstellen von Rechten
* Block-Zuweisung
* Block-Konfiguration
*/
  
/**
* Funktion zum erstellen eines Blockes zu anzeigen noch nicht gelesener Nodes
*/
function unreaded_nodes_block($op = 'list', $delta = 0, $edit = array())
{
switch ($op)
{
case 'list':
$blocks[0]['info'] = t('ungelesen');
return $blocks;
break;  
 
case 'configure':
// Holen der möglichen Node Types
$node_types = node_get_types($op = 'types');
// DEFAULT-Variable
$node_type_option_list = array();
// Auslesen der Node Types
foreach($node_types as $node_typ)
$node_type_option_list[$node_typ -> type] = $node_typ -> name;
$node_desc_option_list = array
(
'1' => t('aufsteigend'),
'0' => t('absteigend')
);  
 
$form['notreaded_nodes_count'] = array
(
'#type' => 'textfield',
'#title' => 'Anzahl der unglesenen Nachrichten',
'#default_value' => variable_get('notreaded_nodes_count', 10),
'#description' => t('Anzahl der anzuzeigenden Mitteilungen')
);  
 
$form['notreaded_nodes_nodetypes'] = array
(
'#type' => 'checkboxes',
'#title' => t('Welche Node-Types sollen berücksichtigt werden?'),
'#default_value' => variable_get('notreaded_nodes_nodetypes', ''),
'#options' => $node_type_option_list,
'#description' => t('Wenn keine Node Types ausgewählt werden, werden alle Node Types abgefragt.')
);  
 
$form['notreaded_nodes_time'] = array
(
'#type' => 'textfield',
'#title' => t('Zeitbeschränkung'),
'#default_value' => variable_get('notreaded_nodes_time', 0),
'#description' => t('In Sekunden die zurückliegende Zeit angeben')
);  
 
$form['notreaded_nodes_desc'] = array
(
'#type' => 'select', '#title' => t('Reihenfolge '),
'#default_value' => variable_get('notreaded_nodes_desc', ''),
'#options' => $node_desc_option_list,
'#description' => t('Wenn keine Node Types ausgewählt werden, werden alle Node Types abgefragt.')
);  
 
return $form;
break;  
 
case 'save':  
$node_types = array();
 
if(!count($edit['notreaded_nodes_nodetypes'])) ;
else
foreach($edit['notreaded_nodes_nodetypes'] as $key => $node_typ)
if($node_typ)
$node_types[$key] = $node_typ;  
 
variable_set('notreaded_nodes_count', (int)$edit['notreaded_nodes_count']); variable_set('notreaded_nodes_nodetypes', $node_types); variable_set('notreaded_nodes_time', (int)$edit['notreaded_nodes_time']); variable_set('notreaded_nodes_desc', $edit['notreaded_nodes_desc']);
 
break;  
 
case 'view':
if(user_access(t('notreaded_zugriff')))
{
global $user;  
 
// falls ein User nicht angemeldet ist
if($user -> uid == 0) return ;  
 
$subquery_nodetype = '';  
// Holen der Anzahl der anzuzeigenden Node Types
$notreaded_nodes_count = (int)variable_get('notreaded_nodes_count', 10);
// Holen der Zeiten
$notreaded_nodes_time = (int)variable_get('notreaded_nodes_time', 0);  
// Holen der betroffenen Node-Types
$betroffene_nodetypes = variable_get('notreaded_nodes_nodetypes', array());
// Wenn keine Node-Types gespeichert sind alle Node-Types zu holen
// sonst hole nur betroffene Node-Types
if(count($betroffene_nodetypes))
{
$mehr_types = '';
 
foreach($betroffene_nodetypes as $node_typ => $wert)
{
$subquery_nodetype .= $mehr_types.' node.type = "'.$node_typ.'"';
$mehr_types = ' OR';
}
 
}
else
$subquery_nodetype .= '1';  
 
$query = 'SELECT node.nid, node.title, node.created FROM {node} AS node WHERE node.status = 1 AND node.nid NOT IN (SELECT history.nid FROM {history} AS history WHERE history.uid = %d GROUP BY history.nid)';
 
if($notreaded_nodes_time)
$query = $query.' AND node.created BETWEEN (UNIX_TIMESTAMP() - '.$notreaded_nodes_time.') AND UNIX_TIMESTAMP()'; if($subquery_nodetype)
 
$query = $query.' AND ('.$subquery_nodetype.')';  
$query = $query.' ORDER BY node.created '.((variable_get('notreaded_nodes_desc', 0)?'DESC ':'ASC '));
 
if($notreaded_nodes_count)
$query = $query.' LIMIT '.$notreaded_nodes_count.'';  
 
$result = db_query($query, $user -> uid);  
 
$url = drupal_get_destination();  
 
$module = module_list();
$node_privacy_byrole = false;
$node_privacy_byrole_list = array();  
 
// Prüfen ob Modul node_privacy_byrole genutzt wird if($module['node_privacy_byrole'])
{
$node_privacy_byrole = true;
$roles = $user -> roles;
$more_roles = '';
$roles_query = '';  
// Zusammenstellen der User-bezogenen Rollen
foreach($roles as $role => $bezeichnung)
{
$roles_query .= $more_roles.' gid = '.$role;
$more_roles = ' OR ';
}
 
// Holen der Rollen
$node_access_query = 'SELECT nid FROM {node_access} WHERE grant_view = 1 AND ('.$roles_query.') GROUP BY nid';  
 
$ergDB_node_access = db_query($node_access_query);  
 
while($erg_node_access = db_fetch_array($ergDB_node_access))
$node_privacy_byrole_list[$erg_node_access['nid']] = 1;
 
} // END if
 
while($punkt = db_fetch_object($result))
{
// wenn kein node_privacy_byrole genutzt wird
if(!$node_privacy_byrole)
$liste[] = format_date($punkt -> created).'
'.l($punkt -> title, 'node/'.$punkt -> nid, array(), NULL, '');
// ansonsten checken ob der user berechtigt ist die Meldung zu sehen elseif($node_privacy_byrole_list[$punkt -> nid])
$liste[] = format_date($punkt -> created).'
'.l($punkt -> title, 'node/'.$punkt -> nid, array(), NULL, '');
}
 
if(count($liste) == 0) return ;  
$block['subject'] = t('Neue Mitteilungen');
$block['content'] = theme('item_list', $liste);  
return $block;
} // END  case 'view': if

 
} // END switch  
} // END manipulation_block
 
/**
* Funktion zum setzen der Berechtigungen für das BLOCK-Modul
*/
function unreaded_nodes_perm()
{
return array(t('notreaded_zugriff'));
}

Nach der Aktivierung sollten als erstes die Rechte gesetzt werden. Dabei sollte der "Gast" bzw. "anonymous user" ausgenommen werden. Das Modul bricht allerdings bei der UID = 0 ohnehin ab. Im nächsten Schritt wird das Modul einer Area zugewiesen (Block "ungelesen"). In der Konfiguration können folgende Einstellungen gemacht werden:

  • Anzahl der anzuzeigenden Nodes. Dabei macht es an dieser Stelle keinen Sinn einen "mehr"-Link zu setzen (die übliche Einstellung in Views), da eine gelesene Nachricht durch die nächste ersetzt wird.
  • Welche Node-Typen berücksichtigt werden sollen.
  • Wie weit zurückliegende Nodes noch angezeigt werden sollen, z. B. 15552000 für die letzten 180 Tage. Hier werden nur positive Integerwerte eingetragen.
  • Und die Reihenfolge in der die Nodes angezeigt werden sollen, die ältesten zuerst oder die neusten zuerst.

Das Style kann in der common.css angepasst werden. Der Block wird mit .block .block-unreaded_nodes { } angesprochen. Alle Strings die angezeigt werden, können in dem "Lokalisierungs"-Modul (Zeichenketten verwalten" bearbeitet werden. Das war es auch schon ..

AnhangGröße
unreaded_nodes.zip2.53 KB

Debugging mit Drupal

Hier sollten die erfahrenen Drupalbenutzer drüberweglesen, aber für einen unerfahrenen Entwickler ist es in Drupal etwas schwerer ein Debugging zu machen. In PHP wird oft ein print_r($array); benutzt um Daten auszulesen. In Drupal funktioniert das nicht so einfach. Dafür gibt es das Debugging-Tool DEVEL. Dieses bietet eine Fülle an Möglichkeiten, dazu aber später.

Für den Einsteiger ist das allerding zu komplex. Abhilfe schafft hier eine Möglichkeit um in Drupal Meldungen auszugeben- drupal_set_message('Meldung-Text');. Mit dieser Methode können auf einfache Weise Text-Meldungen im System ausgegeben werden. Um auch Array's auslesen zu können kann man eine einfache Funktion implementieren:
/**
* Einfache Funktion zum Auslesen von Arrays
*/
function array_auslesen($array)
{
// Wenn $array ein Array ist
if(is_array($array))
// Das Array auslesen
foreach($array as $key => $wert)
{
if(!is_array($wert))
$uebergabe .= $key.' - '.$wert.'
';
else
{
$uebergabe .= $key.' (
';  
$uebergabe .= array_auslesen($wert);  
$uebergabe .= ')
'; }
}
// Sonst gib nur den Text aus
else $uebergabe = $array;  
return $uebergabe;
} // END array_auslesen

Der Aufruf erfolgt durch drupal_set_message(array_auslesen($array);. Die Ausgabe sieht dann so aus (z. B. das User-Objekt):

Richtiges Debugging ist ein eingenes Thema ...

Scheduler-Modul mit JS-Kalender

Das Modul Scheduler kann den Veröffentlichungszeitpunkt eines Nodes steuern. Man setzt die Option in den Node-Types. Unter Drupal 5 wird eine JS-Kalender aus dem Modul jstools mit eingebettet.

Unter Drupal 6 steht diese Option leider nicht mehr zur Verfügung, da das jstools den JSCalendar nicht mehr implementiert hat. In dem Modul im Anhang befindet sich nun das Scheduler-Modul mit dem JSCalendar aus dem jstools von Drupal 5. Hierzu wurden einige Anpassungen in dem File jscalendar.info, jscalendar.module und scheduler.module gemacht. Das Admin-Menü für den JSCalendar ist nicht implementiert. Damit steht momentan nur die Standard-Ansicht zur Verfügung.
AnhangGröße
scheduler.zip124.26 KB

Arbeiten mit dem User-Objekt

Für dynamische Anwendungen ist es unumgänglich spezielle Bereiche nur bestimmten Usern zugänglich zumachen. Hierfür muss ein User identifiziert und ggf. auch einer bestimmten Rolle zugewiesen werden. Drupal bietet einen komfortablen Registrierungs- und Loginprozess. Um mit diesen Daten zu arbeiten dient das User-Objekt. Auf den nächsten Seiten soll eine, in PHP, geschriebene Anwendung in Drupal imlementiert werden. Bei der Anwendung handelt es sich um ein Anwesenheitsmanagementsystem (Arbeitszeiterfassung). Hierfür wurden zwei zusätzliche Rollen angelegt: - Mitarbeiter - Leiter Verwaltung Der Mitarbeiter soll Zugriff auf seine eigenen Daten haben und einen Gruppenkalender einsehen können. Der Leiter Verwaltung hat die gleichen Rechte und kann zusätzlich auf die Mitarbeiterdaten zugreifen, Mitarbeiter anlegen und bearbeiten, Genehmigungen erteilen, usw... . Die Anwendung verfügt über eine eigene Datenbank und ein eigenes CSS-Design.

Auslesen des User-Objektes

Was steht nun in dem User-Objekt? Dazu lesen wir das Userobjekt erst einmal aus. Dazu verwenden wir erst einmal eine eigene Funktion, besser ist das Debugger-Modul zu verwenden, dazu aber später.

/**
* Startseite des AWS
*/
function aws_start()
{
// Holen des User-Objektes
global $user;  
$uebergabe = 'Start AWS';
$uebergabe .= '';  
$uebergabe .= array_auslesen($user);
 
return $uebergabe;
} // END aws_start  
 
/**
* Einfache Funktion zum Auslesen von Arrays
*/
function array_auslesen($array)
{
// Das Array auslesen
foreach($array as $key => $wert)
{
if(!is_array($wert))
$uebergabe .= $key.' - '.$wert.'
';
else
{
$uebergabe .= $key.' (
';  
$uebergabe .= array_auslesen($wert);  
$uebergabe .= ')
';
}
}  
 
return $uebergabe;
} // END array_auslesen

Ausgabe:
uid - 3
name - Mitarbeiter1
pass - cfe09803c312143de6ef0fe9dc436bc9
mail - mitarbeiter1@xxx.de
mode - 0
sort - 0
threshold - 0
theme -
signature -
created - 1208330988
access - 1208340810
login - 1208339914
status - 1
timezone -
language -
picture -
init -
mitarbeiter1@xxx.de
data - a:0:{}
sid - 8797c3216b5f0032fa1a38bda510e5f6
hostname - 127.0.0.1
timestamp - 1208340810
cache - 0
session -
roles (
2 - authenticated user
3 - Mitarbeiter
)

Hier die Definition der wichtigsten Punkte:

uid
Die User-ID des Users. Das ist der Primary-Key aus der Datenbank
name
Name des Users. Standardmäßig erfasst Drupal nur einen Namen und unterscheidet nicht Vor- bzw. Nachname.
pass
Das Passwort als MD5-Hash.
mail
Die Standard-Email-Adresse des Users.
status
Sollte eine 1 beinhalten, nur wenn der User gesperrt ist steht hier eine 0
data
Enthält ein serialisiertes Array in dem über die Funktion user_save() Daten hinterlegt werden können.
roles
Enthält ein Array mit den zugewiesenen Rollen. Es gibt zwei DEFAULT-Rollen
  1. anonymous user
  2. authenticated user
sid
Session ID
hostname
Die IP-Adresse

Über die Rolle wird im folgenden das weitere Menü gesteuert.

Benutzerabhängiges Menü

Als erstes soll unter dem Startmenü ein Menüpunkt erscheinen, der nur aktiv ist wenn ein User eingeloggt ist. Dafür gibt es zwei Möglichkeiten:

  1. Ein Admin-Menü erstellen in dem die Rechte der Sichtabrkeit über die Administrationsoberfläche in Drupal definiert werden können. Die Berechtigungen werden dann in der Drupal-eigenen Datenbank gespeichert.
  2. Die Berechtigungen in dem eigenen Modul setzen. (Das wird im Folgendem favorisiert)

Um einen ersten Menüpunkt zu erzeugen der nur für eingeloggte Mitarbeiter sichtbar ist, wird folgender Code verwendet:
/**
* Das Menüsystem des AWS
*/
function aws_menu()
{
// Holen des User-Objektes
global $user;
$items = array();
 
if(!$may_chache && ($user -> uid > 0))
{
$items[] = array
(
'path' => 'aws',
'title' =>t( 'Arbeitszeiten'),
'callback' => 'aws_helper',
'access' => true,
'type' => MENU_NORMAL_ITEM,
);
 
$items[] = array
(
'path' => 'aws/start',
'title' =>t( 'Arbeitszeit Start'),
'callback' => 'aws_start',
'access' => true,
'type' => MENU_NORMAL_ITEM,
);
}
 
return $items;
}

Anschließend wird der Menüpunkt in den Startpunkt verschoben (admin/build/menu). Solange der User nicht angemeldt ist, ist der Menüpunkt nicht sichtbar.

Erst nach der Anmeldung wird der neue Menüpunkt sichtbar. Hierzu sind zwei Dinge notwendig

  1. Einbinden des User-Objektes (global $user;).
  2. Abfragen ob ein User eingeloggt ist(if(!$may_chache && ($user -> uid > 0))).

User Login Bypass

Es kommt häufig vor das Daten schon in einer anderen Anwendung oder in einer anderen Datenbank liegen. Hier ist es nicht gewünscht alle Daten in Dupal zu integrieren, zumeist soll die externe Anwendung oder Datenbank genutzt werden. Drupal bietet dafür mehrere Möglichkeiten:

Externers Login via XML-RPC
Dazu verbindet sich Drupal mit einem externen Server und übergibt den Nutzernamen und das Passwort. Die Anwort ist ein TRUE oder FALSE. Bei Misserfolg können auch die Fehlermeldungen des kontaktierten Servers ausgegeben werden. Genaueres ist in der Dokumentation zu drupal_xmlrpc() nachzulesen. Bei Erfolg wird der User in die Tabelle {authmap} eingetragen.
Eigenes Script ohne User-Generierung
Hierbei wird die Authentifizierungsabfrage in Drupal um ein eigenes Script erweitert. Drupal wird dann nur mitgeteilt ob die Authentifizierung erfolgreich ist oder nicht. Drupal legt zwar einen eigenen User dafür an, aktiviert diesen aber nicht.
Eigenes Script mit User-Generierung
Hierbei wird die Authentifizierungsabfrage in Drupal um ein eigenes Script erweitert. Drupal wird dann nicht nur mitgeteilt ob die Authentifizierung erfolgreich ist oder nicht, sondern es wird gleichzeitig ein eigener User angelegt und aktiviert.

Es gibt noch eine Reihe fertiger Module mit denen die Authentifizierrung erledigt werden kann, z. B. das LDAP Integration um direkt auf eine Datenbank zuzugreifen. Generell schaut Drupal nur in der eigenen Datenbanktabelle {users} nach ob dort der User existiert, der sich grade versucht einzuloggen. Wenn dieser existiert wird das Passwort geprüft und geprüft ob der User aktiviert ist. Sind diese Prüfungen erfolgreich, werden die Userdaten geladen und im User-Objekt gespeichert. Damit sind dann auch die Berechtigungen global verfügbar. Es besteht jedoch auch die Möglichkeit einen eigenen Authentifizierungs-Mechanismus einzuhängen - (hook_auth()). Der Ablauf verändert sich dann ein wenig:

  1. Drupal schaut in die Tabelle {users}, findet er hier den aktivierten User - alles klar, dann wird abgebrochen und der User geladen.
  2. Drupal schaut in die Tabelle {authmap}, findet er hier den aktivierten User - alles klar, dann wird abgebrochen und der User geladen.
  3. Drupal schaut ob irgendwelche Module den hook_auth implementiert haben. Wenn nicht ist hier Schluss und der User wird abgewiesen.
  4. Drupal fragt den hook_auth nach seinem Ergebnis. Kommt hier ein FALSE ist Schluss und der User wird abgewiesen.
  5. Drupal legt den User an (falls er noch nicht existiert). An diesem Punkt kann auch in das User-Objekt eingegriffen werden.

Das Ergebnis ist dass der User jetzt im System angemeldet ist. Solange er kein aktiviertes Konto hat, stehen Ihm aber nicht alle Möglichkeiten offen.
/**
* Implementation des Hook_auth
*/
function aws_auth($username, $password, $server = FALSE)
{
drupal_set_message('Authentifikation - user:'.$username.' - pw:'.$password.' ist zugelassen');  
// Hier die Prüfung einbauen  
$return = true;  
 
return $return;
} // END aws_auth

 

Hier also ein "universeller" Bypass, der so natürlich nur lokal und nur zum Test verwendet werden sollte. Zumeist werden die Daten aus einer Datenbank geholt und Drupal mitgeteilt das der User Zugriff hat oder nicht. Wenn der User einen dauerhaften Zugriff auf das Drupal-System haben soll, besteht die Möglichkeit direkt einen Drupal-User anzulegen.
/**
* Implementation des Hook_auth
*/
function aws_auth($username, $password, $server = FALSE)
{
drupal_set_message('Authentifikation - user:'.$username.' - pw:'.$password.' ist zugelassen');  
 
// Hier die Prüfung einbauen und die Daten holen  
user_save('',
array
(
'name' => $username,
'pass' => $password,
'mail' => 'user@email.org',
'roles' => array
(2 => 'authenticated user', 3 => 'beliebige rolle'),
'status' => 1,
'init' => 'user@email.org',
)
);
 
$return = true;  
return $return;
} // END aws_auth

 

Mit der Methode user_save können die Daten direkt an Drupal gegeben und der User dort angelegt werden. Wird der erste Parameter mit der $user -> uid angegeben, wird kein neuer Account angelegt sondern geprüft ob ein Account vorhanden ist und dieser bei Bedarf upgedatet. Der zweite Parameter sind die Daten aus dem User-Objekt die als Array zurückgegeben werden. Man sollte sich allerdings immer die Frage stellen ob das sinnvoll ist, denn für diesen User existieren ja dann zwei Datensätze an unterschiedlichen Orten die auch gepflegt werden müssen.

User-Profilseite verändern

Die normale Profilseite eines Users in Drupal gibt nicht viel her. Man kann die Profile mit Drupal-Mitteln recht einfach erweitern und diese zusätzlichen Angaben auch ausgeben lassen. Dazu muss das Modul Profile (Dupal-Standardumfang) aktiviert werden und unter Verwalten -> Benutzerverwaltung können die Profile dann angepasst werden. Die Augabensteuerung und die Sichtbarkeiten für andere User sind alldings beschränkt. Daten aus externen Datenbanken oder Informationen die nicht im Profil sind können aber kaum ausgegeben werden.
Drupal Default-Userprofil
Hierfür kann aber über den hook_user($op, &$edit, &$account, $category = NULL) direkt die Inhalte zugegriffen werden. Der Zugriff erfolgt wieder in einem eigenen Modul. Es soll in diesem Beispiel erst einmal eine Adresse eingeblendet werden, die nicht im Drupal-System gespeichert ist. Es soll weiterhin eine Prüfung erfolgen ob der User auch die Berechtigung hat die Daten zu sehen.
<?php //; $ Id: manipulation.module, UwBach, 2008/05/14 $
 
/**
* @file Menümanipulation der Anwendung
* Erster Versuch
*/
 
/**
* Funktion um Usereinstellungen zu maipulieren
* Hier wird der gespeicherte Benutzername durch den richtigen Namen ersetzt, soweit er erfasst ist
*/
function manipulation_user($op, &$edit, &$user, $category = NULL)
{
// Wenn der User ein Profil aufruft
if($op == 'view')
{
// Der nachgefragte User
$requested_user = $user;
 
// Falls der User keine Berechtigung hat
if(!$user -> uid)
// Prüfung für Zugangsberechtigung einfügen oder weglassen
return ;
else
{
// hole irgendwelche Fremddaten
$externe_daten = array
(
'strasse' => 'Musterstrasse',
'hausnummer' => '12a',
'plz' => '12345',
'ort' => 'Testhausen'
);
}
 
// Der angemeldete User
global $user;  
 
// DEFAULT-Werte für die Sichtbarkeit der Angaben
$sichtbarkeit_adresse = FALSE;
 
// Falls der User seine eigenen Daten nachfragt - alles freigeben if($requested_user -> uid == $user -> uid)
{
$sichtbarkeit_adresse = TRUE;
} // Falls ein fremder Account nachgefragt wird - prüfen was freigegeben werden kann
else
{
// eigene Prüfung einfügen
} // Prüfen Ob die Adresse ausgeben werden soll
 
if($sichtbarkeit_adresse)
{
$items[] = array
(
'title' => t('Strasse'),
'value' => $externe_daten['strasse'],
'class' => 'adresse',
);  
 
$items[] = array
(
'title' => t('Hausnummer'),
'value' => $externe_daten['hausnummer'],
'class' => 'adresse',
);
 
$items[] = array
(
'title' => t('Postleitzahl'),
'value' => $externe_daten['plz'],
'class' => 'adresse',
);
 
$items[] = array
(
'title' => t('Ort'),
'value' => $externe_daten['ort'],
'class' => 'adresse',
);
 
return array
(
t('Adresse') => $items);
} // END  if($sichtbarkeit_adresse)
} // END if($op == 'view')  
} // END manipulation_user()

Die Ausgabe hat sich jetzt verändert:
geändertes Profil
Im Modul User Funktion user_user($type, &$edit, &$user, $category = NULL) wird die Default-Ausgabe generiert und kann ggf. überschrieben werden. Die Umsetzung des Arrays erfolgt in der Funktion function theme_user_profile($account, $fields) ebenfalls in dem Modul User. Interessant ist noch die Angabe 'class' mit der CSS-Klassen erzeugt werden. Der Quelltext sieht dann so aus:
<div class="profile">
<h2 class="title">Verlauf</h2>
<dl>
<dt class="user-member">Mitglied seit</dt>
<dd class="user-member">1 Woche 35 Minuten</dd>
</dl>
 
<h2 class="title">Adresse</h2>
<dl>
<dt class="manipulation-adresse">Strasse</dt>
<dd class="manipulation-adresse">Musterstrasse</dd>
<dt class="manipulation-adresse">Hausnummer</dt>
<dd class="manipulation-adresse">12a</dd>
<dt class="manipulation-adresse">Postleitzahl</dt>
<dd class="manipulation-adresse">12345</dd>
<dt class="manipulation-adresse">Ort</dt>
<dd class="manipulation-adresse">Testhausen</dd>
</dl>
</div>
Damit kann durch die Einbindung einer einfachen CSS-Datei das eigene Design schnell umgesetzt werden (siehe Kapitel "CSS einbinden").

Arbeiten mit Formularen

Das Arbeiten mit Formularen erfolgt in Drupal grundsätzlich in drei Schritten.

  1. Das Formular erstellen
  2. Die zurückgegebenen Werte validieren
  3. Die validierten Werte verarbeiten

Dazu müssen mindestens folgende Funktionen eingebunden werden:

.. _form()
Initialisierung der einzelnen Form-Elemente
.. _form_validate()
Validierung
.. _form_submit()
Verarbeitung

Dabei können Formulare in jeder Phase abgefangen und bearbeitet werden. Das ist praktisch um Änderungen an Formularen vorzunehmen die von fremden Modulen erzeugt werden, z. B. an dem User-Registrierungsformular. Es gibt daher fast immer verschiedene Wege ein Formular zu ändern. Ein Formular kann vor der Erzeugung verändert werden, d. h. einer Funktion werden zusätzliche Informationen mitgegeben indem man den Hook in ein eigenes Modul integriert. Es kann während der Erzeugung geändert werden indem man den Hook _form_alter integriert und es kann nach der Erzeugung durch die Funktion _after_build angepasst werden.

Formularelemente 1 von 2

Formularelemente sind immer als Array aufgebaut, z. B.
$form['textfield_bsp'] = array
(
'#type' => 'textfield',
'#title' => t('Der Titel'),
'#description' => t('Machen Sie mal eine Eingabe.'),
'#required' => false,
'#default_value' => 'hier eingeben',
);
Dabei sind nur die Bezeichnung (['textfield_bsp']) und der '#type' für die meisten Elemente zwingend erforderlich. Zur Verfügung stehen folgende Elemente:

  • Textfield
  • Password
  • Textarea
  • Select
  • Radio
  • Checkboxes
  • Value
  • Hidden
  • Date
  • Weight
  • File Upload
  • Fieldset
  • Submit
  • Button
  • Markup
  • Item

Die wichtigsten sollen genauer betrachtet werden. Dafür benötigen wir erst einmal ein Testformular das dann immer erweitert werden kann. Das .info-File /site/all/testform/testform.info ; $Id$ name = testform description = Test eines Formulars version = "$Name$" und das .module-File /site/all/testform/testform.module
<?php // $Id$  
 
/**
* @file
* Testen von Formularelementen
*/  
 
/**
* Ein eigenes Menü muss her
*/
function testform_menu()
{
$items = array();  
 
$items[] = array
(
'path' => 'testform',
'title' => t('Beispielsformular'),
'callback' => 'testform_page',
'access' => TRUE,
'type' => MENU_NORMAL_ITEM
);
 
return $items;
} // END testform_menu()
 
/**
* Rückgabe der Seite
*/
function testform_page()
{
$uebergabe = t('Das ist ein Beispielsformular');  
$uebergabe .= drupal_get_form('testform_bspform');
 
return $uebergabe;
} // END testform_page()
 
/**
* Hier wird das Formular angelegt
*/
function testform_bspform()
{
$form = array();
 
return $form;
} // END testform_bspform()  
 
/**
* Validierung der Formulareingaben
*/
function testform_bspform_validate($form_id, $form_values)
{
 
} // END testform_bspform_validate()
 
/**
* Das kommt jetzt nach erfolgreicher Validierung
*/
function testform_bspform_submit($form_id, $form_values)
{
return 'ersterinhalt';
} // testform_bspform_submit()

Damit hat man eine Grundform die es erlaubt mit eigenen Formularen zu arbeiten. In der Funktion testform_bspform_submit($form_id, $form_values) wird erst einmal auf eine x-beliebige Seite verwiesen. Später wird hier noch eine Meldung für den User in einer eigenen Seite ausgegeben.

Formularelemente 2 von2

Textfield
Fieldset
Submit
Das Textfield
Das am Häufigsten benötigte Elemen ist wohl das "textfield". Die Definition ist einfach:
$form['dein_vorname'] = array
(
'#title' => t('Dein Vorname'),
'#type' => 'textfield',
'#description' => t('Hier gib bitte deinen Vornamen ein')
);
Mit $form['dein_vorname'] wird dem $form-Array ein neues Element hinzugefügt. Die Mindestangabe im Array ist der '#type' der einen definierten Wert enthält. Mit '#title' wird über dem Formularelement ein Titel und mit '#description' unter dem Formularelement eine Beschreibung ausgegeben. Die Elemente werden durch die Drupal-Funktion form_builder($form_id, $form) in HTML erstellt und die Formatierung der ausgegebenen Elemente wird durch das verwendete Theme bestimmt. Für Textfields stehen noch einige weitere Möglichkeiten offen:

'#default_value'
STRING - Setzen eines Default-Values. Kann durch eine neue Eingabe überschrieben werden.
'#value'
STRING - Setzen eines festen Wertes.
'#size'
INTEGER - Größe des dargestellten Elementes (Default-Wert 60)
'#maxlength'
INTEGER - Angabe der Zeichen die aufgenommen werden können (Default-Wert 128)
'#weight'
INTEGER - Die Position im Formular (Default-Wert 0)
'#autocomplete_path'
STRING - Erstellen von AJAX-baierten Funktionen zur Autosuche nach passenden Werten.
'#reqired'
BOOLEAN - Gibt an ob ein Wert zwingend erforderlich ist. Drupal übernimmt die Überprüfung ob ein Wert übergeben wurde, der eigentliche Wert muss dann allerdings noch überprüft werden.
'#field_prefix'
STRING - Ausgabe direkt vor dem Formularfeld.
'#field_suffix'
STRING - Ausgabe direkt hinter dem Formularfeld.
'#prefix'
STRING - Ausgabe vor dem Formularelement, z. B. '#prefix' => '<div class="dein_nachname">', um eine eigene CSS-Klasse hinzuzufügen.
'#suffix'
STRING - Ausgabe nach dem Formularelement, z. B. '#suffix' => '</div>',

um den DIV-Block von '#prefix' abzuschließen.

'#access'
BOOLEAN - Gibt an ob ein Formularelement angezeigt werden soll oder nicht.

Alle hier aufgeführten Werte sind für alle weiteren Formularelemente zu verwenden.

Das Fieldset
Formularelemente können auch sehr einfach in Gruppen organisiert werden. Dazu wird ein Fieldset erzeugt und die zugeordneten Formularelemente diesem in einem mehrdimensionalen Array zugeordnet.
/**
* Hier wird das Formular angelegt
*/
function testform_bspform($form_values)
{
global $user;
 
$form['name_fieldset'] = array
( '#title' => t('Dein Name'),
'#type' => 'fieldset',
'#description' => t('Wie heißt du?')
);

 
$form['name_fieldset']['vorname'] = array
(
'#title' => t('Dein Vorname'),
'#type' => 'textfield',
'#description' => t('Hier gib bitte deinen Vornamen ein'),
'#field_suffix' => 'Grossbuchstaben',
'#default_value' => 'Vorname',
);
 
$form['name_fieldset']['name'] = array
(
'#title' => t('Dein Nachname'),
'#type' => 'textfield',
'#description' => t('Hier gib bitte deinen Nachnamen ein'),
'#size' => 30,
'#maxlength' => 30,
'#value' => (isset($form_values['name']))? $form_values['name'] : '',
'#weight' => -1,
'#prefix' => '<div class="dein_nachname">',
'#suffix' => '</div>',
'#required' => true
);
 
$form['optionalesFeld'] = array
(
'#title' => t('Zusaetzliche Angaben'),
'#type' => 'fieldset',
'#description' => t('Ausfuellen wenn du magst?'),
'#collapsible' => TRUE,
'#collapsed' => True,
'#access' => ($user -> uid) ? true : false,
);
 
$form['optionalesFeld']['textfeld'] = array
(
'#title' => 'Irgendwas',
'#type' => 'textfield',
'#description' => t('hier können Sie irgendwas eingeben'),
);
 
$form['submit'] = array
(
'#type' => 'submit',
'#value' => t('Senden')
);
 
return $form;
} // END testform_bspform()

Die Fieldsets können auch mit verschieden Attributen versehen werden. Die wichtigsten sind hier:

'#collapsible'
BOOLEAN - gibt an ob ein Fieldset verkleinerbar sein soll. Bei keine Angabe wird der DEFAULT-Wert FALSE angenommen.
'#collapsed'
BOOLEAN - gibt an ob ein Fieldset verkleinert ausgeliefert werden soll oder nicht.

Über das Attribut '#access' im Fieldset lassen sich schnell angepasste Formulare entwickeln. In diesem Fall wird das zweite Fieldset nur angemeldeten Usern angezeigt ('#access' => ($user -> uid) ? true : false,).

Submit
Der Submit-Button kann noch zusätzlich mit dem Attribut '#button_typ' belegt werden. Hierfür wird noch eine Pfadangabe zur verwendeten Grafik benötigt.

Grundsätzliche Arbeitsweise bei der Formularerstellung

Drupal erzeugt Formulare indem es zunächst die Informationen die ein Formular enthält sammelt und in einem Array speichert. Dabei werden die einzelnen Module abgefragt ob Sie etwas zu dieser Funktion beitragen möchten, daher ist es wichtig sich bei eigenen Modulen an die Konventionen zu halten.

Eine Funktion setzt sich immer aus dem Modul-Namen, Funktionsbezeichnung und ggf. dem Hook zusammen. Dabei kann jedes Modul als eigener Hook dienen, z. B. User. So ist es möglich in eigenen Funktionen (meinModul_user) ein fremdes Modul zu integrieren und in dem fremden Modul eine bestimmte Funktion abzufangen (user_register_validate()).

Sind alle Informationen gesammelt wird überprüft ob es eine entsprechende Validierungsfunktion gibt. Es kann eine einzige Funktion zur Validierung des gesammten Formulars genutzt werden oder es können einzelnen Elementen gesonderte Validierungsfunktionen zugewiesen werden. Das erspart einem Programmierer in so fern Arbeit das vorhandene Validierungsfunktionen, z. B. die Validierung eines Passwortes oder einer Email-Adresse, genutzt werden können. Ein weiterer Vorteil ist das eine Validierungsfunktion nur an einer Stelle upgedatet muss und nicht jedes Formular einzeln angepasst wird. Nach der Prüfung der Validierung wird geprüft ob eine Submit-Funktion zur Verarbeitung der Rückgaben vorhanden ist. Erst wenn auch diese gefunden wurde wird das Formular erzeugt und übermittelt (selbst hier kann das Formular nochmals abgefangen werden - empfiehlt sich allerdings nicht da die Prüfungen umgangen werden). Kommen die Formularwerte wieder zurück werden die Ergebnisse an die entsprechende Valierungsfunktion(en) übergeben. Fehler die während der Validerung auftreten werden mit der Funktion form_set_error() an das Drupal-System gemeldet und dort gesammelt. Sind Fehler aufgetreten wird das Formular erneut aufgerufen und die entsprechenden Fehlermeldungen eingeblendet. Sind keine Fehler während der Validierung aufgetreten, werden die Formularwerte and die _submit()-Funktion übergeben und dort verarbeitet.

Hinzufügen einer Einverständniserklärung bei der Standardregistrierung

Anhand des Beispiels soll beispielhaft erkärt werden wie über hook_user($op, &$edit, &$account, $category = NULL) Veränderungen an einem Formular vorgenommen werden können. Hierfür wird ein eigenes Modul erzeugt: Die Datei /site/all/einverstaendniserklaerung/einverstaendniserklaerung.info

; $Id$ name = Einverstaendniserklaerung
description = Zeigt eine Einverstaendniserklaerung bei der Registrierung
version = "$Name$"

Die Datei /site/all/einverstaendniserklaerung/einverstaendniserklaerung.module
<?php // $id$
 
/**
* @file
* Enthält eine Einverständniserklärung während des Registrierungsprozesses
*/
 
/**
* Implementation des hook_users()
*/
function erklaerung_user($op, &$edit, &$user, $category = NULL)
{
switch ($op)
{
// Wenn der User registriert ist
case 'register' :
// Hinzufügen eines Radio-Button im Registrierungsformular $fields['einverstaendniserklaerung'] = array
(
'#type' => 'fieldset',
'#title' => t('Einverständniserklärung'),
);
 
$fields['einverstaendniserklaerung']['decision'] = array
(
'#type' => 'radios',
'#description' => 'Ich bin einverstanden',
'#default_value' => 0,
'#options' => array(t('nicht OK'), t('OK'))
);
 
return $fields;  
 
case 'validate' :  
if(isset($edit['decision']) && $edit['decision'] != 1)
{
form_set_error('decision', t('Die Registrierung kann ohne Ihr Einverstaendnis nicht abgeschlossen werden.'));
}
return;
 
case 'insert' :  
watchdog('user', t('User %user hat zugestimmt', array('%user' => $user -> name)));
return;
 
}
}

Erweiterung für das Modul Webform - eigene Token /dynamische Listen erstellen

Für ein internes Projekt war es gefordert das User eigene Formulare erstellen können. Damit fiel die Wahl schnell auf das Modul webform. Das Modul ist einfacher zu händeln als z. B. CCK. Allerdings waren einige Anpassungen notwendig um die Anforderungen zu erfüllen. Anforderungen waren:

  • Einbindung von bestehenden Web-Services
  • Automatisierte Weiterverarbeitung von Formularen
  • Zugriff auf die Auswertungen durch User

Insbesondere bei Listen-Elementen wurden hier schnell Einschränkungen im Gebrauch deutlich. Webform kann zwar Schlüssel und Werte abspeichern,
Syntax:
Schlüssel1|Wert1
Schlüssel2|Wert2
gibt aber bei der Ausgabe nur den gespeicherten Wert zurück - also den Schlüssel. Das ist zwar für die Weiterverarbeitung praktisch, eignet sich allerdings nur bedingt für einen Enduser. Für die Weiterverarbeitung von Formularen, wurde zunächst das Modul Workflow getestet, aber schnell als ungeeignet, da nicht flexibel genug, abgetan. Webform hat allerdings die Möglichkeit nach erfolgreicher Formulareingabe den User auf eine definierte Seite weiterzuleiten und diese wird genutzt um in einem eigenen Modul den Workflow abzuwickeln. Dazu wird ein einfaches MENU_CALLBACK definiert das auf eine bestimmte Funktion zugreift.
/**
* Implementierung des Hook Menü
*/
function dev_form_menu()
{
$items = array();
 
// erstellen einer Callbackfunktion für die Weiterverarbeitung von eigenen Formularen
// Dieser Pfad kann in Webform Feld: Bestätigungsnachricht oder Weiterleitungs-URL - gesetzt werden
$items['formular_intern/_step_1'] = array
(
'title' => 'Step 1',
'type' => MENU_CALLBACK,
'description' => 'erster Schritt zur Weiterverarbeitung von eigenen Formularen',
'access arguments' => array('Step 1'),
'access callback' => 'user_access',
'page callback' => '_step_1',
'page arguments' => array(1),
'load arguments' => array(1),
);
 
return $items;
} // END dev_form_menu

Diese Funktionalität erforderte keinen Eingriff in das Webform-Modul. Bei den anderen Punkten ließen sich aber Anpassungen direkt im Modul nicht umgehen. Im Detail mußte im Script webform.module und webform_report.inc je zwei Anpassungen gemacht werden. In webform.module wurde die Help-Funktion angepasst um eigen definierte Token mit aufführen zu können.
function theme_webform_token_help()
{
$tokens = array
(
'%username',
'%useremail',
....
 
if (module_exists('profile'))
{
$output .= ' '. t('If you are using the profiles module, you can also access all profile data using the syntax %profile[form_name]. If you for example have a profile value named profile_city, add the variable %profile[profile_city].');
}
 
// Anpassung START
if(module_exists('dev_form'))
$output .= ' '.dev_form_token_help();
// Anpassung ENDE

 
$fieldset = array
(
'#title' => t('Token values'),
'#type' => 'fieldset',
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#children' => '<div>'. $output .'</div>',
);
 
return theme('fieldset', $fieldset);
}

In der Funktion dev_form_token_help() werden die Anzeigetexte für die User geschrieben.

Um auf diese selbst definierten Token zugreifen zu können wurden in der Funktion webform_load($node) eine Anpassung gemacht:
/**
* Implementation of hook_load().
*/
function webform_load($node)
{
module_load_include('inc', 'webform', 'webform_components');
 
...
// Ab hier werden die Token geladen
$additions->webform['components'] = array();
$additions->webform['additional_emails'] = array();
$result = db_query('SELECT * FROM {webform_component} WHERE nid = %d ORDER BY weight, name', $node->nid);
while ($c = db_fetch_array($result))
{
$component => $additions->webform['components'][$c['cid']]; $component['nid'] = $node->nid;
$component['cid'] = $c['cid'];
$component['form_key'] = $c['form_key'] ? $c['form_key'] : $c['cid'];
$component['name'] = t($c['name']);
$component['type'] = $c['type'];
$component['value'] = $c['value'];
$component['extra'] = unserialize($c['extra']);
$component['mandatory'] = $c['mandatory'];
$component['email'] = $c['email'];
$component['pid'] = $c['pid'];
$component['weight'] = $c['weight'];
 
if (isset($component['extra']['email']) && $component['extra']['email'])
{
$additions->webform['additional_emails'][$c['cid']] = $c['cid'];
} // Modifikation START
 
if(module_exists('dev_form'))
{
$get = explode('/', $_GET['q']);
 
// um das Ersetzen von Token während des Editiernes zu vermeiden
if($get[2] != 'edit')
{
// Prüfen ob Defaultwerte gebraucht werden
if($dev_token = strstr($component['value'], '%dev'))
{
$fun = explode('[', $dev_token);
$fun2 = explode(']', $fun[1]);
 
if($daten = dev_form_token($fun2[0], $component['type'], true))
$component['value'] = $daten;
}
 
// Prüfen ob Listen abgefragt werden sollen
if($component['type'] == 'select')
if($dev_token = strstr($component['extra']['items'], %dev'))
{
$fun = explode('[', $dev_token);
$fun2 = explode(']', $fun[1]);
 
if($daten = dev_form_token($fun2[0], $component['type'], false))
$component['extra']['items'] = $daten;
}
}
} // Modifikation ENDE
 
webform_component_defaults($component);
}
....
return $additions;
}
Mit dem Funktionsaufruf dev_form_token($fun2[0], $component['type'], false) werden die Werte aus der eigenen Funktion abgerufen. Diese Funktion besteht im wesentlichen nur aus einem switch .. case. In dieser Funktion werden die Werte aus festen Werten oder wiederum durch Funktionsaufrufe übergeben.
/**
* Eigene Token definieren
*
* @param $token_name - Bezeichnung des Token
* @param $type - Art des Formularfeldes (normalerweise textfield, select)
* @param $default - Der Angefrate Wert ist ein Defaultwert für das Formularfeld
*/
function dev_form_token($token_name = '', $type = '', $default = false)
{
switch ($token_name):
 
case 'wert1':
return 'Token Test';
break;

 
// Gibt eine Liste aller registrierten User zurück
// Aufbau uid| Name
case 'userliste':
if(!$default)
return _get_token_userlist();
else
return '5';
break;
/*
// Weitere Funktionen
case '':
return 0;
break;
// Weitere Funktionen
case '':
return 0;
break;
*/
 
default :
return false;
endswitch;
} // END dev_form_token
Somit ist es jetzt möglich eigene Werte zu übergeben. Die Grenzen liegen hier nur noch in den Möglichkeiten des Programmierers. Insbesondere für SELECT-Typen kann noch die Option angegeben werden ob es sich um ein Default-Wert handelt. Somit kann mit dem gleichen Token sowohl die Liste als auch ein Default-Wert generiert werden. Dabei ist zu beachten das Default-Werte aus dem Schlüssel bestehen, nicht aus dem Wert, falls die Liste als Schlüssel/Wert-Paar übergeben werden. Unter case 'userliste': wird eine Funktion aufgerufen die die entsprechenden Werte generiert.
/**
* Liefert eine Liste mit allen registrierten Mitarbeitern
* Listenaufbau: uid | Benutzername
*/
function _get_token_userlist()
{
// Holt alle aktiven User
$query = 'SELECT uid, name FROM {users} WHERE status = 1';
$result = db_query($query);
 
if(!$result)
return false;
 
$uebergabe = '';
 
while($erg = db_fetch_array($result))
{
$uebergabe .= $erg['uid'].'|'.$erg['name'].' ';
}
return $uebergabe;
} // END _get_token_userlist()

Damit sind jetzt alle notwendigen Schritte erledigt und die Formulare können erstellt werden.

Wie man sieht ist hier jetzt eine User-Liste erstellt worden, die nicht zum normalen Funktionsumfang von Webform gehört. Allerdings werden bei der Ausgabe nur die Schlüssel angezeigt. Um diese wieder mit den Werten zu ersetzen mußten in webform_report.inc zwei Funktionen angepasst werden. Mit dem ersten Update werden die Werte in der Tabelle geparst und das zweite Update ersetzt die Werte in den Downloads. /**
* Create a table containing all submitted values for a webform node.
*/
function webform_results_table($node, $pager_count = 0)
{
// Load Components.
webform_load_components();
 
if (isset($_GET['results']) && is_numeric($_GET['results']))
{
$pager_count = $_GET['results'];
}
 
// Get all the submissions for the node.
$header = theme('webform_results_table_header', $node);
$submissions = webform_get_submissions($node->nid, $header, NULL, $pager_count);
$total_count = webform_get_submission_count($node->nid);
 
// Manipulation START
$submissions = dev_form_ersetze_listen_keys($node, $submissions);
// Manipulation ENDE
 
$output = theme('webform_results_table', $node, $node->webform['components'], $submissions, $total_count, $pager_count);
 
if ($pager_count)
{
$output .= theme('pager', NULL, $pager_count, 0);
}
 
return $output;
}

und
function webform_results_download($node, $format = 'delimiter', $options = array())
{
module_load_include('inc', 'webform', 'webform_export');
 
....
// Get all the submissions for the node.
$submissions = webform_get_submissions($node->nid);
// manipulation START
$submissions = dev_form_ersetze_listen_keys($node, $submissions);
// manipulation START
 
....
 
$export_name = _webform_safe_name($node->title);
$exporter->set_headers($export_name);
@readfile($file_name);
// The @ makes it silent.
@unlink($file_name);
// Clean up, the @ makes it silent.
 
exit();
}

Beidemal wird die Funktion dev_form_ersetze_listen_keys($node, $submissions); aufgerufen. In dieser werden die $submissions analysiert und ggf. ersetzt. /**
* Funktion zum ersetzen von Listenwerten
*/
function dev_form_ersetze_listen_keys($node, $submissions)
{
// prüfen ob ein Node übergeben wurde
if(!$node or !$submissions)
return $submissions;
 
$submissions_org = $submissions;
 
// Holen der Webform-Komponenten (Datenfelder, etc.)
$components_array = $node -> webform['components'];
// Prüfen ob Daten veändert wurden
$components_array_changed_global = FALSE;
$components_array_global = array();
 
// Für jedes Element überprüfen ob Daten ersetzt werden müssen
// Ersetzungen sind nur notwendig bei Datentyp SELECT
foreach($components_array as $component_key => $component_value)
{
// prüfen ob in diesem Typ geändert werden muss
$components_array_changed = FALSE;
 
// Prüfen ob es sich um ein Select-Feld handelt
if($component_value['type'] == 'select')
{
// Prüfen ob ein Array für die Listenelemente vorhanden ist
if(array_key_exists('items', $component_value['extra']))
{
// Trennen der einzelnen Listenelemente
$extra_items = explode('
', nl2br($component_value['extra']['items']));
 
// Vorbereiten des Arrays zur Aufnahme der einzelnen Werte
$extra_items_array = array();
// Jedes Array-Element bearbeiten
foreach($extra_items as $extra_item)
{
// Eine Trennung erfolgt nur für Listenelemente die aus Key und Value bestehen
$extra_item2 = explode('|', $extra_item);
 
// prüfen ob beide Werte übergeben werden
if(trim($extra_item2[0]) != '' and trim($extra_item2[1]) != '')
{
$extra_items_array[trim($extra_item2[0])] = trim($extra_item2[1]);
$components_array_changed_global = TRUE;
$components_array_changed = TRUE;
$components_array_global[$component_key] = $component_key;
}
}
 
// Wenn Änderungen gemacht wurden, diese zur späteren Verwendung speichern
if($components_array_changed)
$node -> webform['components'][$component_key]['extra'] = array('replace' =>$extra_items_array);
}
}
}
 
// Prüfen ob Änderungen vorgenommen wurden
if($components_array_changed_global)
// Jede Übermittlung überprüfen
foreach($submissions as $key => $submission)
// Jeden zu ändernden Wertebereich abgehen
foreach($components_array_global as $key_changed => $value)
// Ersetze die gefundene ID durch den Wert
$submissions[$key] -> data[$key_changed]['value'][0] = $node -> webform['components'][$key_changed]['extra']['replace'][$submissions[$key] -> data[$key_changed]['value'][0]];
 
// die geänderten Werte zurückgeben
if($components_array_changed_global)
return $submissions;
else
return $submissions_org;
} // END dev_form_ersetze_listen_keys($node, $submissions)

Im $node stehen alle Werte aus den Listen und müssen entsprechend nur noch ersetzt werden. Der Weg die Werte aus den Nodes herauszulesen ist etwas umständlicher als die dev_form-Funktion aufzurufen und hier die Werte gleich als Array zu bekommen, hat aber den Vorteil das auch Werte ersetzt werden können die nicht über die dev_form-Funktionen eingefügt wurden. Falls das jemand mal testet, wäre ich für ein Feedback dankbar.

AnhangGröße
webform__dev_form.zip264.19 KB

Attachment erst nach Formular downloadbarbar

Das Modul "Attachment via Form" (Durpal 6.x) ermöglicht es Attachments eines Nodes erst nach dem passieren eines Formulars herunterladbar zu machen.
Der Hintergrund ist, das man so erfahren kann wer Interesse an weiterführenden Informationen bzw. wer welche Informationen erhalten hat. Das war bisher weder bei "Gästen" noch angemeldeten Besuchern für Attachments möglich.
Bei dieser Version ist das Formular statisch implementiert. Eine weiterführende Version soll eine Anbindung an das Modul Webform und CCK ermöglichen. Weiterhin ist kann den Formularfeldern automatisch eine Zuweisung zu Profil-Feldern hinzuprogrammiert werden.

Auf diese Weise kann nachgewiesen werden, wer wann welches Attachment gelesen hat (für den internen Gebrauch) und extern können Angaben von Interessenten gesammelt werden. Eine Anpassung des Formulars oder eine Erweiterung der Zugriffsrechte (z. B. nur 3 Downloads erlauben) ist jederzeit möglich => Sie können mich gerne kontaktieren.

Leistungen Die Konfiguration erlaubt bestimmte Formularfelder verpflichtend zu machen, eine Einverständniserklärung für weiterführende Kontakte (per Mail oder Telefon) zu bekommen und ggf. das Formular zu überspringen.
Dass Modul wird auf bestimmte Node-Types eingestellt und fügt dann das Formular ein. Einmal abgefragt werden bei weiteren Downloads die vorher eingetragenen Daten übernommen (das ist im Modul auch anpassbar).
Alle Daten sind in einer eigenen Tabelle einsehbar. Die Daten können als XML-Excel_Liste herunter geladen werden. _______________________________________________________________________________________________

The module "Attachment via Form" (Durpal 6.x) allows to make a node attachment downloadable until after pass a form.
The background is that you can learn so who are interested in further information or who has received the information.
This has been possible so far either for "guests" have registered visitors for the attachments. In this version the form is implemented statically.
An extended version is to allow a connection to the module Webform and CCK.
It is also intended to form fields automatically an assignment to profile fields to enable.
In this way it can be shown who has read an attachment (for internal use) and external addresses of prospects can be collected. An adaptation of the form or an extension of access rights (eg only allow three downloads) at any time => you are welcome to contact me.

Services
The specific configuration allows form fields to make mandatory a consent form for further contacts (by mail or telephone to get) and if necessary, the form skip.
That module is adjusted to specific node type and then inserts the form.
Once accepted to be interrogated for further downloads, the previously entered data (which is in the module also customizable).
All data are stored in a separate table. The data can be downloaded as XML Excel_Liste down.

AnhangGröße
attachment_via_form.zip12.56 KB

Arbeiten mit der Datenbank

Drupal ist in der Lage mit verschiedenen Datenbanken zu arbeiten. Die Auswahl wird in der Regel bei der Instalation getroffen. Ferner ist Drupal in der Lage auch weitere Datenbanken (exterene Datenbanken) einzubinden. Die Einbindung der Datenbank ist im Script ./sites/default/settings.php niedergelegt.
$db_url = 'mysql://username:password@localhost/databasename';
oder
$db_url = 'mysqli://username:password@localhost/databasename';
oder
$db_url = 'pgsql://username:password@localhost/databasename';
 
$db_prefix = '';

Mit dem Prefix $db_prefix = ''; kann ein Datenbank-Prefix, z. B. drupal_tablename, festgelegt werden. Diese Option ist immer dann zu empfehlen, wenn man nicht nur mit Standard-Modulen arbeiten möchte oder mehrere Distributionen in einer Datenbank laufen lassen möchte. Hierbei ist einzig zu beachten das die Table in den Querys entsprechend gekennzeichnet sind SELECT * FROM {table_name};. Durch den Einsatz der geschweiften Klammer kann Drupal die entsprechenden Table-Prefixe ersetzen und parst die Query zu SELECT * FROM prefix_table_name (Funktion db_prefix_tables($spl) in Script ./includes/database.inc). Sobald Module mit ander User geteilt werden soll muss man darauf achten, man weiß ja nicht wie der andere User die Datenbank anlegt. Drupal stellt dann ein Reihe von Funktionen zur Verfügung mit denen die Datenbankoperationen ausgeführt werden können. Leider stehen nicht alle Funktionen aus dem PHP-Manual MySQL-Funktionen zur Verfügung. Diese können aber auch direkt aufgerufen werden, hier sollte man die Sicherheit der Abfragen beachten. Hier die wichtigsten Funktionen für MySQL (aus dem Script ./includes/datase.mysql.inc):

db_query($query)
Die Grundfunktion der Query. Hier nutzt Drupal die "Prepared Statements"-Syntax, bei der die Query "nackt", d. h. ohne Daten, übergeben wird und die Daten erst nach Prüfung eingefügt werden (siehe Erklärung nächste Seite).
db_affected_rows()
Gibt die Anzahl der betroffenen Zeilen einer Query zurück (für UPDATE, INSERT und DELETE).
db_error()
Gibt die Fehlermeldung einer Anfrage zurück. Leider fehlt hier die Funktion mysql_errno().
db_escape_string($text)
Setzt vor bestimmten Zeichen ein "/".
db_fetch_array($result)
Liefert ein Array mit dem Ergebnis einer Zeile der Abfrage; nur für SELECT-Anweisungen.
db_fetch_objekt($result)
Liefert ein Objekt mit dem Ergebnis. db_fetch_object() ähnelt db_fetch_array(), mit einem Unterschied - ein Objekt wird zurück geliefert anstatt eines Arrays. Indirekt bedeutet dies, dass Sie die Daten nur mit ihren Feldnamen und nicht mit dem Offset ansprechen können (Nummern sind ungültige Namen für Eigenschaften).
db_next_id($name)
Da Drupal keine auto_increment in den Datenbanken verwendet, wird über die Funktion db_next_id($name) der nächste frei Key geholt. Normalerweise ist das der größte Key + 1. Drupal kann etwas durcheinanderkommen, wenn Schlüssel manuel in der Datenbank gelöscht werrden - also Vorsicht. Die Syntax ist einfach $name = {table_name}_gewünschteID, also z. B. die nächste freie UID $naechsterUser = db_next_id('{users}_uid');.
db_num_rows($result)
Anzahl der Treffer eines SELECTS.
db_table_exists($table)
Abfrage ob eine bestimmte Tabelle vorhanden ist.

Für MySQLI und PostgreSQL stehen ähnliche Scripte mit gleichen Funktionen unter ./includes/datase.mysqli.inc und ./includes/datase.pgsql.inc bereit.

Datenbankabfragen als Prepared Statements

Die Syntax ist eigentlich simpel; es wird eine normale Query formuliert und die Werte werden nur mit Platzhaltern versehen. Diese Platzhalter werden dann nach Prüfung durch die eigentliche Werte ersetzt. $user->uid = 12;
 
$result = db_query('SELECT r.rid, r.name FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid WHERE ur.uid = %d', $user->uid);
Erstellt wird daraus die Query 'SELECT r.rid, r.name FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid WHERE ur.uid = 12'. Werden mehrere Platzhalter verwendet, müssen diese in der richtigen Reihenfolge angegeben werden. Gleiche Werte sind dabei auch mehrfach nazugeben. Sieht einfach aus - aber warum der Aufwand? Ganz einfach: So können SQL-Injections verhindert werden und vorbereitete Statements können schneller reprudziert werden (werden schneller ausgeführt). Wichtig ist dabei die korrekte Syntax der Daten anzugeben, d. h. wie sollen sie Daten geprüft werden. Es gibt fünf Datentypen die entsprechend geprüft werden können:

%s
Prüft ob es sich um einen STRING handelt.
%d
Prüft ob es sich um einen INTERGER handelt.
%f
Prüft ob es sich um einen FLOAT handelt.
%b
Prüft ob es sich um einen BINARAY DATA handelt. Diese dürfen nicht in '' gekapselt werden.
%%
Prüft ob es sich um einen LITERAL handelt.

Insbesondere bei Strings sollte man allerdings sehr vorsichtig sein und ggf. noch mal eine gesonderte Prüfung einbauen bevor man die Daten mit der Query weitergibt. Als kleines Beispiel für eine schlechte Passwort-Abfrage:
$user = 'wackelpudding' OR uid = 1; --';
$pass = 'abc';
SELECT uid FROM users WHERE name = '.$username.' AND pass = MD5('.$password.')   // daraus wird:
// SELECT uid FROM users WHERE name = "wackelpudding" or uid = 1
// Das Ergebnis ist immer die UID des 1 Drupal-User (Administator)
Um solche Injections abzuwähren reicht ein einfaches Ersetzen bestimmter Strings, in diesem Fall '--' um die Auskommentierung eines Teils der Query zu verhindern. Auch sollten Strings immer escape-ed werden bevor sie an die Datenbank weitergegeben werden (Funktion db_escape_string($text). An dieser Stelle soll das reichen, es soll ja keine Anleitung zum Hacken werden, aber das macht deutlich wie gefährlich bestimmte Abfragen sein können. Für Litterale gilt eine gesonderte Regelung:
$suchbegriff = 'mann';
 
$query_suche1 = 'SELECT id_member AS id FROM member WHERE vorname like "%%%s%%" OR name LIKE "%%%s%%";';  
$ergDB_suche1 = db_query($query_suche1, $suchbegriff, $suchbegriff);
Die Query wird dann zu SELECT id_member AS id FROM member WHERE vorname like "%mann%" OR name LIKE "%mann%"; umgebaut.

Ergebnisse aus der Datenbank auslesen

Anhand eines Beispiels zur Einbindung einer externen Mitarbeiterdatenbank, soll das deutlich gemacht werden.

/**
* Prüft ob es einen Mitarbeiter mit der UID gibt
*
* @param User-ID des Drupal-System
* @return Array - 0 => 0 = user nicht registriert, 1 = User registriert
* 1 => Integer = Berechtigung des Users
*/
function check_ma($uid)
{
// Prüfen ob der User in der AWS-Datenbank registriert ist und holen der Berechtigung
$query = ' SELECT m.berechtigung, m.name FROM mitarbeiter AS m, abteilungzugehoerig AS a WHERE m.uid = %d AND m.eintrittsDatum < now() AND (m.austrittsDatum >= now() OR m.austrittsDatum = "0000-00-00") AND a.uid = %d AND a.active = 1';  
 
$ergDB = db_query($query, $uid, $uid);  
$anzahl_erg = db_num_rows($ergDB);

 
// Wenn kein Ergebniss gefunden wurde
if($anzahl_erg == 0)
return array(0 => 0, 1 => 0);
 
// Darf nicht passieren - dann ist etwas gewaltig schief gelaufen elseif($anzahl_erg > 1)
{
drupal_set_message('Suchen Sie den Programmierer und erschlagen Sie ihn.');
 
return array(0 => 0, 1 => 0);
}
 
// holen der Ergebnisse
$erg = db_fetch_array($ergDB);
 
// Rückgabe wenn nichts gefunden wurde
return array(0 => 1, 1 => $erg['berechtigung']);
} // END check_ma()
Weitere Beispiele folgen ..

Wechseln zu einer externen Datenbank

Um die Inhalte von Drupal und externen Anwendungen getrennt zu halten bietet es sich an verschiedene Datenbanken zu nutzen. Der Zugriff auf die externe Datentank kann auch über das Drupal-eigene Datenbankscript erfolgen. Hierfür ist es es erforderlich Drupal die Zugangsdaten mitzuteilen und die Datenbank dann jeweils umzuschalten. Die Einbindung der externen Datenbank ist auch im Script ./sites/default/settings.php niedergelegt.
// Orginaldatenbank
$db_url['default'] = 'mysql://user:password@localhost/drupal_db';
// externe Datenbank
//$db_url['externe_db'] = 'mysql://user:password@localhost/externe_db';

Jetzt hat Drupal die notwendigen Daten und Drupal kann mitgeteilt werden, wann Drupal eine andere Datenbank nutzen soll.
// Drupal db auf die extere Datenbank umstellen
function meinModul_xyz()
{
db_set_active('externe_db');
 
/**
*
* Diverse Datenbankoperationen *
*/
 
// Die Datenbank wieder auf Default-Wert setzen
db_set_active('default');
} // END meinModul_xyz()
Beim arbeiten mit externen Datenbank muss man allerrdings sehr vorsichtig sein, sobald eine Fehlermeldung aus einer Datenbankoperation generiert wird, versucht Drupal diese auszugeben - hierfür sind allerdings Datenbankoperationen auf der Drupaleigenen Datenbank notwendig und diese kann zu diesem Zeitpunkt nicht erreicht werden, da Drupal ja momentan auf der externen Datenbank steht. Die Folge ist eine Fehlermeldung die in einer BLANK-Seite ausgegeben wird, da Drupal auch für den Seitenaufbau seine eigene Datenbank benötigt. Es sollten alle Operationen daher doppelt abgesichert werden, z. B. eine Passwortabfrage auf eine externe Datenbank:
// Drupal db auf die externe Datenbank umstellen
db_set_active('externe_db');  
// Query mit den notwendigsten Daten holen
$query = 'SELECT externerUser, uid FROM externe_db AS mm WHERE mm.benutzername = LOWER("%s") AND mm.password = LOWER("%s") AND mm.status_id > 1;';
 
// Query ausführen
$result = db_query($query, $username, $password);
 
// Ermittel der Anzahl der Datensätze
$anzahl_datensaetze = db_num_rows($result);
 
// kein treffender User gefunden
if($anzahl_datensaetze == 0)
{
drupal_set_message('Es ist keine User mit Ihren Daten registriert.');
 
// Zurücksetzen zur Drupal-Datenbank
db_set_active('default');  
 
return false;
} // hier gibt es mehrere Datensätze
elseif($anzahl_datensaetze > 1)
{
drupal_set_message('Es gibt Probleme mit Ihrem Account, bitte wenden Sie sich an den Administrator - Fehlernummer d556.');
 
// Zurücksetzen zur Drupal-Datenbank
db_set_active('default');
 
return false;
}
elseif(db_error() != '')
{
drupal_set_message('Es gibt Probleme mit Ihrem Account, bitte wenden Sie sich an den Administrator - Fehlernummer d556.');
// Zurücksetzen zur Drupal-Datenbank
db_set_active('default');
 
return false;
}  
 
// holen der Mitglieds-Daten
$mitglied = db_fetch_array($result);  
// Zurücksetzen zur Drupal-Datenbank
db_set_active('default');

Ansonsten ist das Arbeiten mit externen Datenbanken problemlos.

Drupal 6 - Neues Statistik-Modul accesslog_statistics

!! BETA-VERSION !!
Hierbei handelt es sich um eine Erweiterung für das vorhandene Statistik-Modul. Die Seite soll einen schnellen Überblick über die einzelnen Bereiche einer Website geben. Weiterhin besteht die Möglichkeit die Ergebnisse als Excel-File herunterzuladen. Der Download wurde vom mir mit Open-Office Calc (2.4), MS Excel (2003) und MS Excel (2007) getestet. In Excel 2007 kommt es momentan noch zu einer Meldung in der man die Vertrauenswürdigkeit bestätigen muss, funktioniert aber tadellos.
Insbesondere die "Suche-basierte"-Übersicht könnte interessant sein. Sie haben hier eine Übersicht über die abgefragten Suchbegriffe und die Anzahl der gelieferten Ergebnisse. Somit haben Sie die Möglichkeit auf nachgefragte Informationen schnell reagieren zu können, wenn die Anzahl der Ergebnisse nicht ausreichend ist.

Bei Seiten mit vielen Suchabfragen und sehr viel Content kann es hier möglicherweise Probleme geben. Die Suchabfragen sind momentan auf 1000 Begriffe limitiert. Sollte die Abfrage hierfür zu lange dauern, kann das Limit manuell verändert werden. File accesslog_statistics.module - Zeile 765 function _get_statistik_search($search_limit = 1000) das $search_limit auf den gewünschten Wert ändern.

Getestet wurde die Suchabfrage mit knapp 800 Suchbegriffen und die Seite wurde innerhalb von 4 Sekunden generiert, der Download brauchte nur 5 Sekunden. Geplant ist noch ein Bereich zur Administration der Seite und das herausfiltern der Suchmaschinen-Connects.
Für ein Feedback bin ich dankbar.

AnhangGröße
accesslog_statistics.zip12.39 KB

Änderungen im Content (Node)

Drupal bietet viele Möglichkeiten die Darstellung im Content zu beeinflussen. Manchmal möchte man aber bestimmte Informationen zusätzlich einblenden oder Standard-Informationen ausblenden bzw. beeinflussen. In den folgenden Seiten werden einige Möglichkeiten aufgeführt. Je nach Anforderungen muss man sich die geeigneten Eingriffsmöglichkeiten heraussuchen. Dieser Bereich wird in einige Unterbereiche gesplittet:

  • Wie arbeitet ein Node
  • Wo kann ich eingreifen
  • Content ändern
  • Eigenen Node-Type programmieren
  • Nodes mit Modulen erstellen

Wie arbeitet ein Node

Grundsätzlich läuft alles über das Modul node. Die Daten eines Nodes werden in der Datenbank abgelegt. Ein Node hat immer eine eindeutige ID (nid). Die Bestandteile eines Node:

nid
Eindeutige ID des Nodes
vid
Eindeutige Revisions-ID des Node. Damit ist es möglich einen Node zu einem beliebigen Zeitpunkt wieder herzustellen.
type
STRING - Bezeichnung des Node. Standardmäßig ist der Node-Type page und story aktiviert. Der Node book ist im Core enthalten muss aber aktiviert werden.
title
Der Titel des Nodes als 128 Zeichen String. Diese kann deaktiviert werden (benötigt einen Eingriff über Programierung - siehe "Eigenen Node programmieren")
uid
Die UID des Erstellers.
status
0
Unveröffentlicht
1
Veröffentlicht

Der Status kann durch andere Module beeinflußt werden, z. B. "Node Privacy By Role"

created
Das Erstellungsdatum als UNIX_TIMESTAMP
changed
DAS Bearbeitungsdatum als UNIX_TIMESTAMP
comment
Integerfeld das die Möglichkeiten für Comments beschreibt
0
Keine Kommentare erlaubt
1
Vorhanden Kommentare lesen aber keine neuen erstellen
2
Lesen und schreiben von Kommentaren erlaubt

Die Berechtigungen hierfür werden über das Comment-Modul gesetzt. Siehe auch "NoComment"-Modul zur Kommentar-Steuerung einzelner Seiten als Beispiel zur Einflussnahme auf Nodes.

promote
Gibt an ob ein Node auf der Startseite bzw. unter domain/nodes erscheinen soll. 1 auf Startseite - 0 nicht auf Startseite
moderate
Gibt an ob ein Node unter Moderation steht. Im Core von Drupal ist hierfür allerdings kein Modul enthalten. Steht standardmäßig auf 0
sticky
Gibt an ob ein Node immer oben auf der Liste stehen soll. Ansonsten werden mehrere Nodes nach Datum ausgegeben.

    Ein Node kann in verschieden Stadien abgefangen werden. Die Stadien können über die Variable $op abgefragt werden. Die Stadien sind:

delete
The node is being deleted.
delete revision
The revision of the node is deleted. You can delete data associated with that revision.
insert
The node is being created (inserted in the database).
load
The node is about to be loaded from the database. This hook can be used to load additional data at this time.
prepare
The node is about to be shown on the add/edit form.
search result
The node is displayed as a search result. If you want to display extra information with the result, return it.
print
Prepare a node view for printing. Used for printer-friendly view in book_module
update
The node is being updated.
submit
The node passed validation and will soon be saved. Modules may use this to make changes to the node before it is saved to the database.
update index
The node is being indexed. If you want additional information to be indexed which is not already visible through nodeapi "view", then you should return it here.
validate
The user has just finished editing the node and is trying to preview or submit it. This hook can be used to check the node data. Errors should be set with form_set_error().
view
The node content is being assembled before rendering. The module may add elements $node->content prior to rendering. This hook will be called after hook_view(). The format of $node->content is the same as used by Forms API.
alter
the $node->content array has been rendered, so the node body or teaser is filtered and now contains HTML. This op should only be used when text substitution, filtering, or other raw text operations are necessary.
rss item
An RSS feed is generated. The module can return properties to be added to the RSS item generated for this node. See comment_nodeapi() and upload_nodeapi() for examples. The $node passed can also be modified to add or remove contents to the feed item.

Wo kann man eingreifen?

Ein Node kann an verschiedenen Stellen abgefangen werden. Die häufigste wird dabei wohl der _nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) sein. Dazu wird in einem eigenen Modul die Funktion node_api() integriert. Und um deutlich zu machen was übermittelt wird, soll jeder Node einmal ausgelesen werden.

<?php //; $ Id: nodetest.module, UwBach, 2008/05/29 $  
 
/**
* @file Zugriff auf den hook_nodeapi
* Erster Versuch
*/
 
/**
* Erlaubt Inhalte bestimmten Seiten hinzuzufügen oder zu bearbeiten
*/
function nodetest_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL)
{
drupal_set_message('Status - '.$op.''.array_auslesen((array)$node));
} // node_test_nodeapi()
 
/**
* Einfache Funktion zum Auslesen von Arrays
*/
function array_auslesen($array)
{ $uebergabe = '
';
if(is_array($array) or is_object($array))
// Das Array auslesen
foreach($array as $key => $wert)
{
if(!is_array($wert))
$uebergabe .= $key.' - '.$wert.'
';
else
{
$uebergabe .= $key.' (
';
$uebergabe .= array_auslesen($wert);
$uebergabe .= ')
';
}
}
else
$uebergabe = $array;
return $uebergabe;
}// END array_auslesen

Die Funktion array_auslesen() kann entweder auskommentiert werden oder durch eine print_r() ersetzt werden. In der Funktion nodetest_nodeapi() wird die Variable &$node als Objekt übergeben, daher muss sie zu auslesen gecarstet werden. Schaut man sich die Ausgabe an, fällt auf das ein normaler Node dreimal aufgerufen wird. Im Status load, view und alter. Beim Erstellen oder Verändern werden die Stati validate, submit, prepare, und ggf. update durchlaufen. In jedem Statdium kann nochmals auf den Node zugegriffen werden. Modul nodetest als Download

AnhangGröße
nodetest.zip952 Bytes

Eigene Box im Contentbereich

Auf einer bestimmten Seite (Startseite) soll eine zusätzliche Box mit den neusten Meldungen im Content-Bereich eingeblendet werden. Hier sollen nur die Titel und das Erstellungs- bzw. Änderungsdatum angezeigt werden. Die Meldungen können vom Typ 'story' und von einen Node-Typen stammen. Mit einer Box auf der rechten Seite konnten die gewünschten Ergebnisse nicht erreicht werden. Daher wird bei dem Aufruf der Startseite die hook_nodeapi() "angezapft" und ein eigener Bereich definiert. Dafür wird ein eigenes Modul manipulation.module angelegt. Mit der Funktion manipulation_nodeapi(&$node, $op, $teaser, $page) können dann die Informationen des Node abgefragt werden. Für diese Erweiterung ist nur das Stadium 'alter' interessant. /**
* Erlaubt Inhalte bestimmten Seiten hinzuzufügen
*/
function manipulation_nodeapi(&$node, $op, $teaser, $page)
{
// Änderungen für die Startseite
// Hinzufügen einer BOX mit den neusten Meldungen
if($_GET['q'] == 'node/18' and $op == 'alter')
{
// Angabe welche Nodes gelistet werden sollen
$node_type = 'story';
 
// Die anderen Nodes entfernt
// Anzahl der gelisteten Elemente
$list_no = 10;
 
// Select aus Node
$sql = 'SELECT node.title, node.type, node.nid, node.changed FROM {node} AS node WHERE node.type = "'.$node_type.'" node.promote = 1 ORDER BY node.changed DESC LIMIT '.$list_no.'';
 
// Inhalte als Liste erstellen
// Weitere Formatierungen in manipulation.css
$inhalt .= '<ul>';
$result = db_query($sql);
 
while ($anode = db_fetch_object($result))
{
$inhalt .= '<li>'.l($anode->title, 'node/'.$anode->nid).'
'.format_date($anode -> changed, 'small').'</li>';
}
 
$inhalt .= '</ul>';


// Die Box als erstes einfügen
// Weitere Formatierungen in manipulation.css
$node -> body = '<div class="frontpage_box" ><b>Die letzten Meldungen:</b>'.$inhalt.'</div>'.$node -> body;
 
// Hinzufügen eines eigenen CSS-Designs
drupal_add_css('sites/all/modules/manipulation/manipulation.css', 'theme', 'screen', TRUE);
}
//
} // END manipulation_nodeapi()
Mit if($_GET['q'] == 'node/18' and $op == 'alter') wird ein bestimmter Node abgefangen. Das funktioniert auch mit aktiviertem Clean-URL. In der Query wird bei den Nodes der Status abgefragt und zusätzlich, mit node.promote = 1 , ob der Eintrag auf der Startseite angezeigt werden soll. Dann werden die Ergebnisse aus der Datenbanktabelle {node} geholt. Die Ergebnisse werden gleich als Objekt ausgelesen und in einen Link umgewandlt. Um das Datum mit den lokalen Einstellungen auszugeben wird die Drupal-Funktion format_date($timestamp, $type = 'medium', $format = '', $timezone = NULL) verwendet. Schließlich werden die gesammelten Inhalte vor den eigentlichen Seiteninhalt gestellt. Die eigenen Inhalte werden dabei in einem eigenen DIV-Container (<div class="frontpage_box"> ) verpackt und anschließend mit einer eigenen CSS-Datei formatiert. Die CSS-Datei ist recht einfach gehalten:   .frontpage_box
{
width:250px;
float:right;
padding-left:10px;
padding-bottom:10px;
}
 
.frontpage_box li
{
list-style-type:none;
list-style-position:outside;
margin-left: -2.5em;
}

Damit wird die DIV-Box an den rechten Bereich des Content-Bereichs geschoben, eine Breite und ein Abstand unten und links definiert. Der Text der Seite umfließt den erstellten Container.

Sichten auf Teaser einschränken

In diesem Modul sollen für bestimmte Node-Typen die Sichten auf Rollenbasis eingeschränkt werden. Alle User sollen den Teaser sehen, aber die Vollanzeige soll auf Rollenbasis beschränkt werden. Statt dessen wird ein Hinweistext angezeigt. Zu beachten ist das nicht alle Node-Types beschränkt werden sollten, da sonst auch Seiten wie das Impressum, AGB's, usw. nicht mehr vollständig angezeigt werden. Ggf. sollte man sich neu Node-Typen als Clone anlegen. Das Modul hat den Namen "eingeschaenktesicht".

Das .info-File
; $ Id: eingeschraenktesicht.info, UwBach, 2008/06/06 $
name = eingeschraenktesicht
description = Schränkt die Sichten auf Artikel und Seiten ein
package = "Node-Sicht"

Das .module-File
<?php // $ Id: eingeschraenktesicht.info, UwBach, 2008/06/06 $  
/**
* @file
* Dieses Modul gibt nur beistimmte Seiten zur Vollansicht frei
*/
 
/**
* Erlaubt Inhalte bestimmten Seiten hinzuzufügen oder zu bearbeiten
*/
function eingeschraenktesicht_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL)
{
global $user;
 
$ersatz_text = t('Sie haben zum weiterlesen keine Berechtigung, machen Sie drei Purzelbäume und hüpfen auf einem Bein um weiter zu lesen.');
 
if($op == 'alter' && !user_access('Teasersicht '.$node -> type))
{
$node -> body = t($ersatz_text);
//$node -> links = '';
$node -> comment = 0;
//$node -> readmore = 0;
}
} // eingeschraenktesicht_nodeapi()
 
/**
* Zum Administrationsmenü hinzufügen
*/
function eingeschraenktesicht_perm()
{
$result = db_query('SELECT type FROM {node_type} WHERE module = "node"');
 
while($erg = db_fetch_array($result))
$node_types[] = 'Teasersicht '.$erg['type'];
 
return $node_types;
} // END eingeschraenktesicht_perm
 
/**
* Hook user_access() imlementieren
*/
function eingeschraenktesicht_access($op, $node)
{
return user_access('Teasersicht '.$node -> type);
} // END eingeschraenktesicht_access($op, $node)
Der Ersatz-Text in den Funktion eingeschraenktesicht_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) muss angepasst werden. Nach der Aktivierung des Moduls können die Zugriffsrechte über die Benutzerverwaltung gesetzt werden.

Eigenen Node-Type programmieren

Der Vorteil einer Eigenprogrammierung liegt in dem exakten Funktionsumfang der den zusätzlichen Aufwand oftmals lohnt. Ein eigener Node setzt immer auf dem Standard-Node in Drupal auf. Man kann über diverse Hook's seine eigenen Anforderungen umsetzen. Dabei programmiert man praktisch immer ein Add-On auf die Default-Funktionen. Zusätzlich zum .info und .module-File wird meist noch ein .install-File benötigt. Hierin wird eine Anpassung der Datenbank gemacht. Es gibt i. R. eine Funktion zur Installierung und eine Funktion zum Deinstallieren. Im .module-File sollten folgende Funktionen ausprogrammiert werden:

_node_info()
Gibt "Grundeinstellungen" des eigenen Nodes bekannt.
_menu($may_chache)
Einhängen des Nodes in das Menü.
_perm()
Einstellungen für die Erteilung von (Benutzer-)Rechten. Hier werden die unterschiedlichen Bearbeitungsformen definiert, z. B. 'edit own nodetype', 'view nodetype'.
_access($op, $node)
Rückgabe der Benutzerrechte. Man kann hier sehr genau steuern wer was machen kann und darf.
_form($node)
Definition der eigenen Formularfelder zu Nodeerfassung / -bearbeitung.
_validate($node)
Validierung der Daten aus den zusätzlichen Formularfeldern
_insert($node)
Speicherung der zusätzlichen Daten.
_update($node)
Speicherung nach der Bearbeitung.
_delete(&$node)
Löschfunktion des Node's.
_load($node)
Holt die zusätzlichen Daten des Node's, z. B. für Updates.
_view($node, $teaser = FALSE, $page = FALSE)
Hinzufügen der Daten zu dem Node für die Anzeige im Browser.
theme_xx()
Möglichkeit die Ausgabe mit eigenen Formatierungen zu versehen.

Es müssen nicht unbedingt alle Funktionen ausprogrammiert werden. Vor der Programmierung muss man sich also Gedanken machen:

  • was soll dargestellt werden?
  • wie soll etwas dargestellt werden
  • welche Aktionen müssen durchgeführt werden?
  • wer soll die Berechtigung für bestimmte Aktionen bekommen?

Aber jetzt der Reihe nach:

Planung der Beispielsanwendung

Es soll eine Möglichkeit geschaffen werden intern Stellenanzeigen zu schalten. Es gibt in dem System 10 Usergruppen (Rollen).

  • Die Stellenanzeigen sollen Bereichen zugeordnet werden.
  • Es sollen Interessentengruppen ausgewählt werden können.
  • Sehen sollen diese nur angemeldete User.
  • Die berechtigten User sollen die Möglichkeit haben sich über Neueinträge per Mail informieren zu lassen.
  • Angebote einstellen sollen nur dafür berechtigte Usergruppen. Sie können nur eigene Angebote bearbeiten.
  • Es soll möglich sein Attachments (PDF) an das Angebot anzuhängen.
  • Besonders berechtigte Usergruppen können alle Angebote bearbeiten.
  • Stellenangbote haben eine Laufzeit (von - bis). Zusätzlich kann der Ersteller die Stellenangebote auch im Archiv anzeigen lassen (Checkbox).
  • Es soll eine Archiv der Angebote geben.

Das Berechtigungssystem ist recht einfach aufgebaut. Rolle 1 und 2 sind die Standardrollen aus (anonymus user und authenticated user) Drupal die keine Berechtigungen haben. Ansonsten verteilen sich die Berechtigungen nach folgender Tabelle:

Rolle View Create Update Delete Email
Rolle 1 - - - - -
Rolle 2 - - - - -
Rolle 3 - - - - -
Rolle 4 + - - - +
Rolle 5 + + own node - +
Rolle 6 + + + + +
Rolle 7 + + own node - +
Rolle 8 + - - - +
Rolle 9 + + + - +
Rolle 10 + + + + +

  Diese Berechtigungen werden in der Funktion _perm() gesetzt, in Verwalten -> Benutzerverwaltung -> Zugriffskontrolle ausgewählt und in der Tabelle {permission} per Rolle abgespeichert. Als Datenfelder werden benötigt:

  • Der Titel (Varchar)
  • Die Beschreibung (Text)
  • Für wen interessant (Checkbox)
  • Anzeige von (Date)
  • Anzeige bis (Date)
  • Im Archiv anzeigen (Boolean)
  • Insert-Date (Timestamp)
  • Update-Date (Timestamp)
  • Erfasser (Integer (UID))
  • Letzter Bearbeiter (Integer (UID))
  • Bereich (Checkbox)
  • File
  • Aktiviert (Boolean)

  Die Datenfelder Titel ({node}::title, ({node_revisions}::title)), Text (({node_revisions}::body)), Insert-Date ({node}::created), Update ({node}::changed, ({node_revisions}::timestamp)), Erfasser ({node}::uid, ({node_revisions}::uid)), File ({files},{file_revisions}) und Aktiviert ({node}::status) sind Standard-Felder im Node und können so genutzt werden. Alle anderen Datenfelder müssen angelegt werden. Ferner wird noch eine Datenbanktabelle für die Interessenten und die Bereiche benötigt, diese könnten alternativ auch statisch programmiert werden. Das .info-File ist wieder sehr einfach:
; $ Id: stellenangebote.info, UwBach, 2008/06/02 $
name = stellenangebot
package = "Stellenangebot"
version = "$Name$"
datestamp = "1212412515"
description = "Über dieses Modul können Stellenangebote aufgesetzt werden."

Möchte man das Modul abhängig von dem Modul "views" machen, kann man hier dependencies = views einfügen. Damit läßt sich dass Modul nur dann aktivieren wenn auch das Modul "views" aktiviert ist.

Das .install-File

Um eigene Daten zu speichern bietet Drupal die Möglichkeiten eigene Datenbanktabellen zu erstellen. Hierfür ist die Implementierung zweier Funktionen nötig:

  • Der hook_install()
  • und der hook_uninstall()

In beiden Funktionen sollte darauf geachtet werden, dass die drei unterschiedlichen Datenbanken "MySQL", "MySQLi" und "PostgreSQL" unterstützt werden. Auf den Einsatz von AUTO_INCREMENT sollte verzichtet werden, da diese Funktion kein Standard in allen betroffenen Datenbanken ist. Für Nodes verwendet Drupal zum einen die nid (Node-ID) als globale ID für Nodes und die vid (Versions-ID) um die aktuelle Version zu markieren. Die beiden ID's werden auch hier als PRIMARY KEY verwendet.
<?php // $Id$  
 
/**
* Einbinden des Hook_install()
*/
function stellenangebot_install()
{
// Überprüfen welches DB-System genutzt wird
switch($GLOBALS['db_type'])
{
// Für Mysql und MySqli
case 'mysql':
case 'mysqli':
// Speichern der zusätzlichen Daten für Stellenangebote
db_query( 'CREATE TABLE {stellenangebot} (
nid int(10) unsigned NOT NULL DEFAULT "0",
vid int(10) unsigned NOT NULL DEFAULT "0",
anzeige_von date NOT NULL,
anzeige_bis date NOT NULL,
archiv tinyint(1) NOT NULL DEFAULT "1",
update_uid int(10) unsigned DEFAULT NULL,
bereich1 tinyint(1) DEFAULT NULL,
bereich2 tinyint(1) DEFAULT NULL,
bereich3 tinyint(1) DEFAULT NULL,
active tinyint(1) DEFAULT NULL,
PRIMARY KEY (nid,vid),
UNIQUE KEY vid_uniquie (vid)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;' );
 
// Speichern der Interessenten-Bezeichnungen
db_query( 'CREATE TABLE {stellenangebot_interessent} (
idstellenangebot_interessent int(10) unsigned NOT NULL,
bezeichnung varchar(45) NOT NULL,
weight int(10) unsigned NOT NULL DEFAULT "0" COMMENT "bestimmt die Reihenfolge der Ausgabe",
active tinyint(3) unsigned NOT NULL DEFAULT "1" COMMENT "gibt an ob der Eintrag aktiviert ist",
PRIMARY KEY (idstellenangebot_interessent) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;' );
 
// Speicherung der Zuordnungen zwischen Stellenangeboten und Interessenten
db_query( 'CREATE TABLE {stellenangebot_zuordnung} (
vid int(10) unsigned NOT NULL,
idstellenangebot_interessent int(10) unsigned NOT NULL,
PRIMARY KEY (vid,idstellenangebot_interessent) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;' );  
 
break;  
 
// für Postgree
case 'pgsql' :
 
// Speichern der zusätzlichen Daten für Stellenangebote
db_query( 'CREATE TABLE {stellenangebot} (
nid int(10) unsigned NOT NULL DEFAULT "0",
vid int(10) unsigned NOT NULL DEFAULT "0",
anzeige_von date NOT NULL,
anzeige_bis date NOT NULL,
archiv tinyint(1) NOT NULL DEFAULT "1",
update_uid int(10) unsigned DEFAULT NULL,
bereich1 tinyint(1) DEFAULT NULL,
bereich2 tinyint(1) DEFAULT NULL,
bereich3 tinyint(1) DEFAULT NULL,
active tinyint(1) DEFAULT NULL,
PRIMARY KEY (nid,vid) ) ' );
 
// Speichern der Interessenten-Bezeichnungen
db_query( 'CREATE TABLE {stellenangebot_interessent} (
idstellenangebot_interessent int(10) unsigned NOT NULL,
bezeichnung varchar(45) NOT NULL,
weight int(10) unsigned NOT NULL DEFAULT "0" COMMENT "bestimmt die Reihenfolge der Ausgabe",
active tinyint(3) unsigned NOT NULL DEFAULT "1" COMMENT "gibt an ob der Eintrag aktiviert ist",
PRIMARY KEY (idstellenangebot_interessent) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;'
);
 
// Speicherung der Zuordnungen zwischen Stellenangeboten und Interessenten
db_query( 'CREATE TABLE {stellenangebot_zuordnung} (
vid int(10) unsigned NOT NULL,
idstellenangebot_interessent int(10) unsigned NOT NULL,
PRIMARY KEY (vid,idstellenangebot_interessent) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;' );  
 
db_query("CREATE SEQUENCE {stellenangebot_interessent}_seq;");  
db_query("CREATE INDEX {stellenangebot_interessent}_content_type_id_idx ON {stellenangebot_interessent} (idstellenangebot_interessent);");
 
break;
}  
 
// Für alle Datenbanken
// Einfügen der Default-Daten
db_query('INSERT INTO {stellenangebot_interessent} (
idstellenangebot_interessent,
bezeichnung,
weight,
active)
VALUES
(2, "Gruppe 1", 0, 1),
(3, "Gruppe 2", 0, 1),
(4, "Gruppe 3", 0, 1),
(5, "Gruppe 4", 0, 1),
(6, "Gruppe 5", 0, 1);'
);
 
// Updaten der Tabelle sequences
db_query('UPDATE {sequences} SET id = 6 WHERE name = "stellenangebot_interessent_idstellenangebot_interessent"');
 
} // END stellenangebot_install()
 
/**
* Einbinden des Hook_uninstall()
*/
function stellenangebot_uninstall()
{
// Löschen der speziellen Datenbank
db_query('DROP TABLE {stellenangebot};');
db_query('DROP TABLE {stellenangebot_interessent};');
db_query('DROP TABLE {stellenangebot_zuordnung};');
 
// Nur zur Sicherheit - in einigen Drupalversionen funktioniert das nicht richtig db_query('DELETE FROM {sequences} WHERE name = "stellenangebot_interessent_idstellenangebot_interessent"');
db_query('DELETE FROM {node_revisions} WHERE vid IN (SELECT vid FROM {node} WHERE type = "stellenangebot")');
db_query('DELETE FROM {node} WHERE type = "stellenangebot"');
} // END stellenangebot_uninstall()
 
 

In der Funktion stellenangebot_install() wird zunächst abgefragt welches Datenbanksystem genutzt wird; switch($GLOBALS['db_type']). Die MySQL und MySQLI Anweisungen unterscheiden sich nicht und können daher in einem Block abgehandelt werden. Weiterhin werden noch INSERT-Anweisungen ausgeführt, die außerhalb der SWITCH-Anweisung stehen, die auch für alle Systeme gleich sind. Die Default-Werte für die Tabelle {stellenangebot_interessent} sind von der Anweisung her für alle verwendeten Datenbanksysteme wieder gleich. Da die Tabelle eine ID verwendet muss diese auch in {sequences} upgedatet werden. Im .install-File kann auch noch der hook_update_N() implementiert werden. Bei der Erstinstallation eines Modules ruft Drupal die _install()-Funktion auf; ist ein Modul schon installiert wird die _update_N()-Funktion mit der höchsten Zahl aufgerufen. N steht dabei für die Nummerierung des Updates. Man sollte alte Update-Anweisungen in die neue Update-Anweisung mit aufnehmen, da man sich nicht sicher sein kann welche Updates im Vorfeld schon ausgeführt wurden.

Das .module-File 1

Nun geht es daran die Funktionalitäten zu programmieren. Als erstes wird ein stellenangebot.module-File erzeugt und unter dem Ordner stellenangebot abgespeichert.   Jetzt werden die Funktionen der Reihe nach ausprogrammiert.

 

Die Funktion stellenangebot_node_info()
Mit _node_info() werden Informationen auf der Seite "Inhalt erstellen" (/node/add) gesetzt. Zudem wird Drupal mitgeteilt ob der DEFAULT-Title und -Body genutzt und wie er bezeichnet werden soll. Pflichtangaben sind nur 'name' und 'module', ab Drupal 6.x ist auch 'description' eine Pflichtangabe.
/**
* Implementierung hook_node_info()
*/
function stellenangebot_node_info()
{
return array(
'stellenangebot' => array( 'name' => t('Stellenangebot'),
'module' => 'stellenangebot',
'description' => t('Mit diesem Modul können Sie Stellenangebote schalten.'),
'has_title' => TRUE,
'title_label' => t('Stellenangebot-Titel'),
'has_body' => TRUE,
'body_label' => t('Stellenangebot-Beschreibung'),
'min_word_count' => 2,
'locked' => TRUE) );
} // END stellenangebot_node_info()

Die Funktion stellenangebot_node_menu()
Hier wird vorerst nur der Inhaltstyp zu dem Link "Inhalt erstellen" (node/add) hinzugefügt. Eine CALLBACK-Funktion muss hier nicht definiert werden, da die Standard-Funktionen vom Modul Node genutzt werden.
/**
* Implementierung des Hook_menu()
*/
function stellenangebot_menu($may_chache)
{
$items = array();  
 
if(!$may_chache)
{
// zunächst einmal ausgeben in "Inhalt erstellen"
$items[] = array(
'path' => 'node/add/stelleangebot',
'title' => t('Stellenangebot'),
'access' => user_access('Stellenangebot erstellen'),
);
}
 
return $items;
} // END stellenangebot_menu($may_chache)
Die Funktion stellenangebot_perm()
Diese Funktion liefert ein Array mit allen möglichen Berechtigungen zurück. Die Berechtigungen ergeben sich aus der Analyse der Anforderungen (siehe "Planung der Beispielsanwendung"). Hier werden die Berechtigungen nur übergeben und können unter Verwalten->Benutzerverwaltung->Zugriffskontrollle (admin/user/access) den einzelnen Rollen zugeordnet werden. Die Berechtigungsstufen werden dann in der Datenbank-Tabelle {permission} für jede Rolle gespeichert. Die Rückgabe der Berechtigungen wird auf User-Basis, zur Laufzeit, durch die nächste Funktion function stellenangebot_access($op, $node) geregelt.
/**
* Einstellungen für die Erteilung von (Benutzer-)Rechten
*/
function stellenangebot_perm()
{
return array('Stellenangebote einsehen', 'Email-Information', 'Stellenangebot erstellen', 'Eigene Stellenangebote bearbeiten', 'Alle Stellenangebote bearbeiten', 'Stellenangebote loeschen');
} // stellenangebot_perm()
Die Funktion stellenangebot_access($op, $node)
Diese Funktion prüft die Berechtigung des einzelen Users. Der User mit der UID = 1 umgeht die Prüfung und hat daher auf alle Funktionen Zugriff. Für alle anderen User gilt: Die Prüfung wird über die Funktion user_access($string, $account = NULL) durchgeführt. Der Parameter $string muss übergeben werden, in diesem Beispiel die Benutzerrechte aus der Funktion _perm() ('Stellenangebote einsehen', 'Email-Information', 'Stellenangebot erstellen', 'Eigene Stellenangebote bearbeiten', 'Alle Stellenangebote bearbeiten', 'Stellenangebote löschen'). Wird $account (User-Account) nicht übergeben, wird der abfragende User genutzt. In diesem Fall wird die Prüfung nach den Optionen aus der Tabelle mit den Berechtigungen (View, Create, Update, Delete) duchgeführt.
/**
* Rückgabe der Benutzerrechte
*/
function stellenangebot_access($op, $node)
{
// holen des aktuellen User-Objekts
global $user;
 
// Prüfen ob der User Stellenangebote einsehen darf
if($op == 'view')
return user_access('Stellenangebote einsehen');
 
// prüfen ob der User Stellenangebot erstellen darf
if ($op == 'create')
return user_access('Stellenangebot erstellen');
 
// Prüfen ob der User Stellenangebote bearbeiten darf
if($op == 'update')
{
// eigene Stellenangebote dürfen immer bearbeitet werden
if(user_access('Eigene Stellenangebote bearbeiten') && $user -> uid == $node -> uid)
return TRUE;
 
// wenn es kein eigenes Stellenagebot ist - prüfen ob erweiterte Rechte vorliegen
return user_access('Alle Stellenangebote bearbeiten');
}
 
// prüfen ob der User Stellenangebote Löschen darf
if($op == 'delete')
return user_access('Stellenangebote loeschen');
}
 
// stellenangebot_access($op, $node)

Die zentrale Abfrage der Funktion user_access('STRING') ist ein Query auf die Datenbanktabelle {permission} - $result = db_query("SELECT DISTINCT(p.perm) FROM {role} r INNER JOIN {permission} p ON p.rid = r.rid WHERE r.rid IN ($placeholders)", $rids);. Diese Funktion muss nicht genutzt werden, es kann auch ein eigenes Überprüfungssystem genutzt werden, z. B. Berechtigungen die in einer anderen Datenbank niedergelegt sind. Ansonsten ist diese Funktion sehr schön, einmal eingestellt braucht man sich im Rest der Anwendung keine Gedanken mehr um die Berechtigungen der User machen. Von hier an kann man das Modul schon aktivieren und die Berechtigungen setzen. Der neue Node wird schon im Menü angezeigt und kann ausgewählt werden. Die Seite ist allerdings noch leer da noch keine Formularinhalte zurückgegeben werden. Das ändert sich mit der nächsten Funktion.
Die Funktion stellenangebot_form($node)
In dieser Funktion werden alle Formularinhalte definiert, die zur Erfassung des eigenen Nodes erforderlich sind. Der Node kann schon aufgerufen werden (das erleichtert auch die Entwicklung) es sollten aber noch keine Daten abgespeichert werden. Die Funktionen für die Validierung, Submit und View sind noch nicht angelegt. Hilfreich ist es auch sich schon mal mit Formularen befasst zu haben.
/**
* Erstellen eines Formular für stellenangebot
*/
function stellenangebot_form($node)
{
// Prüfen ob ein Node bearbeitet werden soll
// wenn ja aufbereiten den DEFAULT-Daten
if($node -> nid && $node -> vid)
{
// aufbereiten des Bereichs
if($node -> bereich1 && $node -> bereich2 && $node -> bereich3)
$node -> bereich = 0;
elseif($node -> bereich1)
$node -> bereich = 1;
elseif($node -> bereich2)
$node -> bereich = 2;
elseif($node -> bereich3)
$node -> bereich = 3;
 
// Aufbereiten der Interessenten
$node -> interessent = array();
 
foreach($node -> interessenten as $key => $wert)
$node -> interessent[$key] = $key;
 
// Aufbereiten des Datums
$datum_von = explode('-',$node -> anzeige_von);
$node -> date_von = array(
'month' => (int)$datum_von[1],
'day' => (int)$datum_von[2],
'year' => (int)$datum_von[0]);
 
// Aufbereitn des Datums
$datum_bis = explode('-',$node -> anzeige_bis);
$node -> date_bis = array(
'month' => (int)$datum_bis[1],
'day' => (int)$datum_bis[2],
'year' => (int)$datum_bis[0]); }
 
// holen der Informationen die durch die Funktion hook_node_info() gesetzt wurden $type = node_get_types('type', $node);
// holen der Bereiche
$bereich = array(
0 => t('alle Bereiche'),
1 => t('Bereich 1'),
2 => t('Bereich 2'),
3 => t('Bereich 3'), );
 
// Array zur Aufnahme der Interessenten
$interessent = array();
// Holen der Interessenten
$ergDB_interessent = db_query('SELECT idstellenangebot_interessent, bezeichnung FROM {stellenangebot_interessent} WHERE active = 1 ORDER BY weight');
 
// Auslesen der Interessenten
while($erg_interessent = db_fetch_array($ergDB_interessent)) $interessent[$erg_interessent['idstellenangebot_interessent']] = t($erg_interessent['bezeichnung']);
 
// Standard-Title der in _node_info() gesetzt wurde
$form['title'] = array(
'#type' => 'textfield',
'#title' => check_plain($type -> title_label),
'#required' => TRUE,
'#default_value' => $node -> title, );
 
$form['bereich'] = array(
'#title' => t('Bereich'),
'#type' => 'select',
'#options' => $bereich,
'#required' => true,
'#default_value' => $node -> bereich,
'#description' => t('Sie können das Stellengebot einem Bereich zuordnen.'), );  
 
$form['interessent'] = array(
'#type' => 'checkboxes',
'#title' => 'Interessentbereich',
'#description' => t('Welche Personengruppe wollen Sie ansprechen'),
'#default_value' => $node -> interessent,
'#options' => $interessent,
'#prefix' => '<div class="form_stellenangebot_checkbox">',
'#suffix' => '</div>' );
 
$form['body_filter']['body'] = array(
'#type' => 'textarea',
'#title' => check_plain($type -> body_label),
'#default_value' => $node -> body,
'#rows' => 7,
'#required' => TRUE );
 
// diese Funktion wird nur dann benötigt, wenn kein Editor installiert ist
// $form['body_filter']['filter'] = filter_form($node -> format);  
 
$form['date_von'] = array(
'#type' => 'date',
'#title' => t('anzeigen von'),
'#default_value' => (($node -> von) ? $node -> von : array('year' => date('Y'), 'month' => (int)date('m'), 'day' => (int)date('d'))), '#prefix' => '<div class="form_stellenangebot_date">', );
 
$form['date_bis'] = array(
'#type' => 'date',
'#title' => t('anzeigen bis'),
'#default_value' => (($node -> bis) ? $node -> bis : array('year' => date('Y'), 'month' => (int)date('m'), 'day' => (int)date('d'))), '#suffix' => '</div>', );  
 
$radio = array('0' => 'nein', '1' => 'ja');
 
$form['archiv'] = array(
'#type' => 'radios',
'#title' => t('Im Archiv dauerhaft anzeigen'),
'#options' => $radio,
'#default_value' => (($node -> archiv)? $node -> archiv : '1'),
'#prefix' => '<div class="form_stellenangebot_radio">',
'#suffix' => '</div>', );
 
$form['activ'] = array(
'#type' => 'radios',
'#title' => t('Das Stellenangebot ist aktiviert'),
'#options' => $radio,
'#default_value' => ((array_key_exists('status', $node) or $node -> status == '0')? $node -> status : '1'),
'#prefix' => '<div class="form_stellenangebot_radio">',
'#suffix' => '</div>', );
} // END stellenangebot_form($node)

In der Funktion stellenangebot_node_info() wurden Drupal einige Standard-Einstellungen für den neuen Node-Type übermittelt. Diese werden jetzt wieder geholt und in das Formular übergeben. Möchte man z. B. keinen Titel ausgeben setzt man in _node_info() die Variable 'has_title' auf FALSE und übergibt das Formularfeld 'title' nicht (macht zwar keinen Sinn, ist aber möglich). Mit $form['body_filter']['filter'] = filter_form($node -> format); können die DEFAULT-Formatierungen angezeigt werden, im aktuellen System ist allerdings eine Editor installiert so das diese Angaben nicht benötigt werden. Einige Formularelemente wurden noch zusätzlich in DIV-Container "verpackt" (z. B. '#prefix' => '<div class="form_stellenangebot_radio">',) um diese gesondert formatieren zu können. Screenshot stellenangebot Hier ist das Stellenangebot schon mal vollständig, allerdings noch nicht formatiert. Zu beachten ist auch die unterschiedliche Anzeige der erweiterten Einstellungsmöglichkeiten für Menüeinstellungen, Kommentare, Autoren und Veröffentlichungseinstellungen. Hier werden die Standardeinstellungen für Nodes geladen und bei Berechtigung angezeigt. Im Kopf der Funktion wird überprüft ob ein Node bearbeitet werden soll, if($node -> nid && $node -> vid), nur dann sind die nid und vid vorhanden. Es müssen dann ein paar Daten aufbereitet werden, damit diese als Default-Werte übernommen werden (siehe Funktion stellenangebot_load($node)).
Die Funktion stellenangebot_validate($node)
Die Validierung aller Standard-Elemente kann Drupal (Modul Node) überlassen werden. Dazu gehören die beiden Elemente 'title' und 'body', die im _node_info() angelegt wurden, und die Datumswerte die mit dem Form-Element 'date' automatisch auf Gültigkeit geprüft werden. Auch für das 'file'-Element wird die Standardfunktion genutzt. Die eigenen Formularinhalte werden nur auf Gültigkeit geprüft, vor allem ob der Übergabewert aus $_POST stimmen kann. Für das Datum "anzeigen von" wird eine Prüfung eingefügt ob das Datum sich nicht auf die Vergangenheit bezieht und das Datum "anzeigen bis" größer als das Datum "anzeigen von" ist. Da diese Funktion auch für das Updaten des eigenen Nodes verwendet wird, muss ggf. darauf geachtet werden ob es Unterschiede zwischen der Ersterfassung und dem Updaten vorhandener Nodes gibt. Exemplarisch ist das in der Validierung des Datums "anzeigen von" umgesetzt.
/**
* Validation von stellenangebot
*/
function stellenangebot_validate($node)
{
// Alle Default-Werte werden vom Modul node ausgewertet
// Checken des Bereichs
// prüfen ob bereich gesetzt ist
if(!isset($node -> bereich)
// prüfen ob wirklich ein nummerischer Wert übergeben wurde
or !is_numeric($node -> bereich))
// hier stimmt der Quellcode dann nicht mehr (POST wurde verändert) form_set_error('bereich', t('Bitte laden Sie das Formular neu.'));
// prüfen der Interessenten
// prüfen ob Interessenten gesetzt ist
if(!isset($node -> interessent))
form_set_error('interessent', t('Bitte laden Sie das Formular neu.'));
else
foreach($node -> interessent as $key => $wert)
// prüfen ob wirklich ein nummerischer Wert übergeben wurde
if(!is_numeric($key) or !is_numeric($wert))
// hier stimmt der Quellcode dann nicht mehr (POST wurde verändert) form_set_error('interessent', t('Bitte laden Sie das Formular neu.'));
 
// prüfen date_von
// Bei Neueingaben muss das Datum >= jetzt sein
if(!$node -> nid && (strtotime($node -> date_von['year'].'-'.$node -> date_von['month'].'-'.$node -> date_von['day']) < mktime(0,0,0)))
form_set_error('date_von', t('Bitte geben Sie ein Datum ein das größer als das heutige ist.'));
 
// prüfen date_bis
// das eingegebene Datum darf nicht kleiner sein als das Datum date_von
if((strtotime($node -> date_bis['year'].'-'.$node -> date_bis['month'].'-'.$node -> date_bis['day']) < strtotime($node -> date_von['year'].'-'.$node -> date_von['month'].'-'.$node -> date_von['day'])))
form_set_error('date_bis', t('Das Datum "anzeigen bis" darf nicht kleiner sein als das Datum "anzeigen von".'));
 
// prüfen ob wirklich ein nummerischer Wert übergeben wurde
if(!is_numeric($node -> archiv))
form_set_error('archiv', t('Bitte laden Sie das Formular neu.'));
 
// prüfen ob wirklich ein nummerischer Wert übergeben wurde
if(!is_numeric($node -> activ))
form_set_error('activ', t('Bitte laden Sie das Formular neu.'));
 
} // END stellenangebot_validate($node)

Werden bei der Prüfung Fehler entdeckt, werden diese mit der Funktion form_set_error() zurückgegeben. Drupal ruft dann das Stellenangebot-Formular erneut auf, blendet die Fehler ein und makiert die entsprechenden Elemente.
Die Funktion stellenangebot_insert($node)
Diese Funktion wird nur dann aufgerufen wenn es bei der Valierung keinen Fehler gegeben hat. Der neue Node ist mittlerweile im Drupal-System eingetragen, d. h. alle Standard-Werte sind in der Datenbank, das Objekt Node hat jetzt eine $node -> nid und eine $node -> vid. Jetzt müssen nur noch die zusätzlichen Informationen eingetragen werden, hierfür gibt es einen Insert in die Datenbanktabellen {stellenangebot} und {stellenangebot_zuordnung}.
/**
* Speichern der Objekte
* Dabei muss man sich nur um die angepassten Formualrfelder kümmern,
* der Rest wird von Drupal erledigt
*/
function stellenangebot_insert($node)
{
global $user;
 
// speichern der Daten in die Tabelle stellenangebote
db_query('INSERT INTO {stellenangebot}
(nid, vid, anzeige_von, anzeige_bis, archiv, update_uid, winf, wing, mwj, active)
VALUES
(%d, %d, "%s", "%s", %d, %d, %d, %d, %d, %d)',
$node -> nid,
$node -> vid,
($node -> date_von['year'].'-'.$node -> date_von['month'].'-'.$node -> date_von['day']),
($node -> date_bis['year'].'-'.$node -> date_bis['month'].'-'.$node -> date_bis['day']),
$node -> archiv,
$user -> uid,
(($node -> bereich == 0 or $node -> bereich == 2)?1:0),
(($node -> bereich == 0 or $node -> bereich == 3)?1:0),
(($node -> bereich == 0 or $node -> bereich == 1)?1:0),
$node -> activ
);
 
// Query für stellenangebot_zuordnung aufbauen
$query_zuordnung = 'INSERT INTO {stellenangebot_zuordnung} VALUES ';
 
$index = false;
 
foreach($node -> interessent as $key => $wert)
if($wert)
{
$komma = '';
 
if($index)
$komma = ',';
 
$query_zuordnung .= $komma.'('.$node -> vid.', '.$key.')';
 
$index = true;
}
 
// speichern der Daten in die Tabelle stellenangebot_zuordnung
db_query($query_zuordnung);
 
// Einstellungen in Tabelle node verändern
if(!$node -> activ)
db_query('UPDATE {node} SET status = 0 WHERE vid = %d', $node -> vid);
else
db_query('UPDATE {node} SET status = 1 WHERE vid = %d', $node -> vid);
 
// Funktion für weiterführende Aktionen
stellenangebot_information($node -> vid);
} // END stellenangebot_insert($node)
Die neuen Stellenangebote sollen nicht auf der Startseite veröffentlicht werden (promote = 0) und es sollen keine Kommentare (comment = 0) zugelassen werden, die Einstellungen hierfür können unter Startseite › Verwalten › Inhaltsverwaltung › Inhaltstypen Stellenangebot (/admin/content/types/stellenangebot) gemacht werden. Da jetzt das Stellenangebot vollständig erfasst ist, kann an dieser Stelle auch ein abschließende Aktion mit eingefügt werden, stellenangebot_information($node -> vid);. In dieser Funktion wird eine Datenbankabfrage auf eine externe Datenbank gemacht und ein Webservice angestoßen (diesen Code werde ich hier nicht veröffentlichen).
/**
* In dieser Funktion können Tätigkeiten durchgeführt werden die nach Inserts oder Updates notwendig sind
*/
function stellenangebot_information($vid = 0)
{
// Mach was nötig ist
return 1;
} // END stellenangebot_information($vid)
Die Funktion stellenangebot_load($node)
Der nächste Schritt ist die zusätzlichen Daten in der Funktion _load($node) zur Verfügung zu stellen. Diese Funktion wird immer dann aufgerufen, wenn der Node zum anzeigen oder bearbeiten geladen wird.
/**
* Implementierung des hook_load
* Hier werden die zusätzlichen Bestandteile eines Nodes für das Objekt Node geholt
*/
function stellenangebot_load($node)
{
// Holen der zusätzlichen Daten
$stellenangebot = db_fetch_object(db_query(' SELECT * FROM {stellenangebot} WHERE vid = %d', $node -> vid));  
 
$interessent = array();
 
// Holen der Interessenten
$interessent_db = db_query(' SELECT idstellenangebot_interessent, bezeichnung FROM {stellenangebot_interessent} WHERE idstellenangebot_interessent IN (SELECT idstellenangebot_interessent FROM {stellenangebot_zuordnung} WHERE vid = %d)', $node -> vid);
 
// Wenn Ergebnisse gefunden wurden
if(db_num_rows($interessent_db))
while($interessent_erg = db_fetch_array($interessent_db))

$interessent[$interessent_erg['idstellenangebot_interessent']] = $interessent_erg['bezeichnung'];
 
// zuweisen der Interessenten
$stellenangebot -> interessent = $interessent;
 
return $stellenangebot;
} // stellenangebot_load($node)

Wichtig ist zu beachten das die Daten nicht zu 100 % Prozent dem Datensatz entsprechen der aus dem Formular des Nodes übergeben wurde. Für das bearbeiten des Nodes können die Daten entweder in der Funktion _form($node) oder in _load($node) umgestellt werden. Da ein Node wahrscheinlich öfter gelesen als bearbeitet wird, ist in diesem Beispiel die Bearbeitung in die Funktion _form($node) verlegt worden. Im günstigsten Fall speichert man die Daten gleich so das diese so weiterverarbeitet werden können (naja ist meine erster Versuch).
Die Funktion stellenangebot_view($node, $teaser = FALSE, $page = FALSE)
Da jetzt die Daten alle vorhanden sind geht es an die Darstellung. Dabei sind grundsätzlich zwei Darstellungen zu unterscheiden, "Der Teaser" und "Die Page". Hier ist man relativ frei wie man den Node dargestellt haben möchte. In diesem Beispiel soll unter dem Titel das Erstellungsdatum und der Ersteller angezeigt werden (Drupal-Standard), darunter der Bereich an den sich das Angebot richtet, die Interessentengruppen, die Gültigkeit und schließlich der Text. Ruft eine berechtigte Person das Angebot auf sollen zusätzliche Informationen eingeblendet werden. Alle Angaben sollen mit einem eigenen CSS-Design versehen werden. Anmerkung: In dem vorliegenden System ist das User-Objekt erweitert worden, es wird u. a. der reale Name (real_name) gespeichert. Daher wird ein User-Objekt des Node-Erstellers geladen.
/**
* Implementierung Hook_view
* Zur Node die zusätzlichen Inhalte hinzufügen
*/
function stellenangebot_view($node, $teaser = FALSE, $page = FALSE)
{
// Holen des Erstellers als User-Objekt
$user_update = user_load(array('uid' => $node -> update_uid));
 
// Bei vorhandenen "real_name" name überschreiben
$node -> name = ($user_update -> real_name)? $user_update -> real_name : $node -> name;
 
// Wenn der Node angefordert wird
if(!$teaser)
{
$node = node_prepare($node, $teaser);
// hier werden die zusätzlichen Angaben ausgegeben
$node -> content['zusatzinfo'] = array(
'#value' => theme('stellenangebot_zusatzinfo', $node),
'#weight' => -1 );
 
// prüfen ob noch weitere Angben notwendig sind
global $user;
 
if((user_access('Eigene Stellenangebote bearbeiten') && $node -> uid == $user -> uid)|| user_access('Alle Stellenangebote bearbeiten') || user_access('Stellenangebote löschen'))
$node -> content['sichtbarkeit'] = array(
'#value' => theme('stellenangebot_sichtbarkeit', $node),
'#weight' => 10 );
}
 
// Wenn nur der Teaser angefordert wird
if($teaser)
{
stellenangebot_bereichwaehlen($node);
stellenangebot_interessenten($node);  
$node -> teaser = 'Bereich: '.$node -> bereich.' Interessengruppen:'.$node -> interessent;  
$node = node_prepare($node, $teaser);
}
 
return $node;
} // END stellenangebot_view($node, $teaser = FALSE, $page = FALSE)

Die darzustellenden Inhalte werden mit $node -> content zugewiesen. Die Standard-Inhalte werden mit folgenden Einstellungen gespeichert: $node->content['body'] = array( '#value' => $teaser ? $node->teaser : $node->body, '#weight' => 0, ); Man kann das Array content beliebig erweitern. Die Reihenfolge der Ausgbae kann über das Attribut '#weight' bestimmt werden. Die Inhalte können direkt in content übergeben werden, es ist aber auch möglich das in einer gesonderten Funktion zu machen. Bei komplexeren Darstellungen empfiehlt sich diese Methode. Für die Page-Darstellung werden zwei Theme-Funktionen benötigt. In der ersten werden Angaben die für alle sichtbar sind aufgesetzt, in der zweiten die Angaben die nur den berechtigten Personen zugänglich sind. Zur Ermittlung der Berechtigungen if((user_access('Eigene Stellenangebote bearbeiten') && $node -> uid == $user -> uid)|| user_access('Alle Stellenangebote bearbeiten') || user_access('Stellenangebote löschen')) wird die user_access()-Funktion genutzt.
Das fertige Stellenangebot
Wie man sehen kann werden die Teaser (links) wie beabsichtigt angezeigt. Den Stellenangeboten selber fehlt noch das CSS-Design. Auch die unterschiedlichen Sichten sind schon verfügbar.
Die Funktion theme_stellenangebot_zusatzinfo($node)
In den Theme-Funktionen werden die einzelnen Bereiche in DIV's mit einen Classes gesetzt.
/**
* Ausgabe im Theme steuern
*/
function theme_stellenangebot_zusatzinfo($node)
{
// Funktion zur Ermittlung der Bereiche auswählen stellenangebot_bereichwaehlen($node);
 
// function zur Ermmitlung der Interessenten auswählen stellenangebot_interessenten($node);
 
// Falls die Vorschau angefordert wird, Datum aus Formular übernehmen
if($node -> preview)
{
$node -> anzeige_von = $node -> date_von['year'].'-'.$node -> date_von['month'].'-'.$node -> date_von['day'];
$node -> anzeige_bis = $node -> date_bis['year'].'-'.$node -> date_bis['month'].'-'.$node -> date_bis['day'];
}
 
$output = '<div class="stellenangebot-bereich">Das Stellenangebot richtet sich an den Bereich: '.$node -> bereich.'</div>';
 
$output .= '<div class="stellenangebot-interessent">Gesucht wird:'.$node -> interessent.'</div>';
 
$output .= '<div class="stellenangebot-gueltig">Gültig vom: '.date('d.m.Y', strtotime($node -> anzeige_von)).' bis '.date('d.m.Y', strtotime($node -> anzeige_bis)).'</div>';
 
return $output;
} // END theme_stellenangebot_zusatzinfo($node)
 
/**
* Ausgabe im Theme steuern
*/
function theme_stellenangebot_sichtbarkeit($node)
{
$output = '';
 
if($node -> active)
$output .= '<div class="stellenangebot-active">Das Stellenangebot ist <b>aktiviert</b></div>';
else
$output .= '<div class="stellenangebot-active">Das Stellenangebot ist <b> nicht aktiviert</b></div>';
 
if($node -> archiv)
$output .= '<div class="stellenangebot-active">Das Stellenangebot wird im Archiv <b>angezeigt</b></div>';
else
$output .= '<div class="stellenangebot-active">Das Stellenangebot wird im Archiv <b>nicht angezeigt</b></div>';
 
return $output;
} // END theme_stellenangebot_sichtbarkeit($node)

Diese Funktionen sind nur zur Vereinfachung ausgegliedert.
/**
* Diese Funktion setzt der gewählten Bereich in das Node-Objekt
*/
function stellenangebot_bereichwaehlen($node)
{
$bereich = array( 0 => t('alle Bereiche'), 1 => t('Bereich 1'), 2 => t('Bereich 2'), 3 => t('Bereich 3'), );
 
// Wenn die Vorschau abgerufen wird sind die Interessenten noch nicht gesetzt

if($node -> preview && !($node -> bereich1 || $node -> bereich2 || $node -> bereich3))
{
if($node -> bereich == 0)
$node -> bereich = $bereich[0];
elseif($node -> bereich == 1)
$node -> bereich = $bereich[1];
elseif($node -> bereich == 2)
$node -> bereich = $bereich[2];
elseif($node -> bereich == 3)
$node -> bereich = $bereich[3];
}
 
// aufbereiten des Bereichs
if($node -> bereich1 && $node -> bereich2 && $node -> bereich3) $node -> bereich = $bereich[0]; elseif($node -> bereich1) $node -> bereich = $bereich[1]; elseif($node -> bereich2)
$node -> bereich = $bereich[2];
elseif($node -> bereich3)
$node -> bereich = $bereich[3];
}// END stellenangebot_bereichwaehlen($node)
 
/**
* Diese Funktion setzt eine Liste mit allen gewählten Interessentengruppen ein
*/
function stellenangebot_interessenten($node)
{
// Wenn die Vorschau abgerufen wird sind die Interessenten noch nicht gesetzt
if($node -> preview && !$node -> interessenten)
{
// Übernehmen der Einträge aus dem Formular
$interessent_preview = $node -> interessent;
 
// Abfrage der gewählten Interessenten vorbereiten
$query = 'SELECT idstellenangebot_interessent, bezeichnung FROM {stellenangebot_interessent} WHERE active = 1';
 
$sub_query = '0';
 
// Auslesen der gewählten Einträge
foreach($interessent_preview as $key => $wert)
if($wert)
$sub_query .= ' OR idstellenangebot_interessent = "'.$wert.'"';   $query .= ' AND ('.$sub_query.')';
 
$ergDB = db_query($query);
 
while($erg = db_fetch_array($ergDB)) $interessent_pre[$erg['idstellenangebot_interessent']] = $erg['bezeichnung'];   $node -> interessenten = $interessent_pre;
}
 
// Ausfiltern der Interessenten
foreach($node -> interessenten as $key => $wert)
$interessent .= '<li>'.check_plain($wert).'</li>';
 
$node -> interessent = '<ul>'.$interessent.'</ul>';
} // END stellenangebot_interessenten($node)

Die Funktion stellenangebot_update($node)
Die Funktion wird aufgerufen, wenn ein Update des Nodes gemacht wird. In diesem Fall können die Daten gelöscht und neu eingetragen werden.
/**
* Die updatefunktion für den neuen Node
*/
function stellenangebot_update($node)
{
// Alle vorherigen Einträge in stellenangebot_zuordnung löschen
db_query('DELETE FROM {stellenangebot_zuordnung} WHERE vid IN (SELECT vid FROM {stellenangebot} WHERE nid = %d)', $node -> nid) ;
 
// Alle Nodes mit der NID löschen
db_query('DELETE FROM {stellenangebot} WHERE nid = %d', $node -> nid);
 
// Setze die Einträge neu stellenangebot_insert($node);
} // stellenangebot_update($node)
Im Anschluss wird der Node neu eingetragen.
Die Funktion stellenangebot_delete($node)
Das löschen der Nodes ist nur berechtigten Personen vorbehalten, hier greift auch wieder die user_access()-Funktion und sorgt dafür dass nur berechtigte Personen diese Funktion aufrufen können.
/**
* Einfügen einer DELETE Funktion
*/
function stellenangebot_delete(&$node)
{
// Alle vorherigen Einträge in stellenangebot_zuordnung löschen
db_query('DELETE FROM {stellenangebot_zuordnung} WHERE vid IN (SELECT vid FROM {stellenangebot} WHERE nid = %d)', $node -> nid) ;
 
// Alle Nodes mit der NID löschen
db_query('DELETE FROM {stellenangebot} WHERE nid = %d', $node -> nid);
} // END stellenangebot_delete(&$node)
Damit sind alle Standard-Funktionen ausprogrammiert und es müssen nur noch die Verknüpfungen auf das Menü gesetzt werden. Natürlich fehlt noch die CSS-Datei. Diese kann am besten in die Funktion stellenangebot_load($node) intgriert werden. An dieser Stelle ist sichergestellt das immer wenn ein Node vom Type 'stellenangebot' geladen wird auch die dazugehörige CSS vorhanden ist.

Das .module-File 2

Im Node "stellenangebot" sind zwei Kategorien erfasst, der Bereich und die Interessentengruppen. Beide Bereiche könnte auch über das Modul Taxonomy eingerichtet werden. Dies würde die Gruppierung ein wenig erleichtern, trifft aber im vorliegenden Beispiel nicht 100%ig die Vorgaben. Da jetzt der Node "stellenangebot" erstellt ist, geht es an die Darstellung. Man kann den Node direkt im Menü verlinken oder sich eine Funktion zur Darstellung selber schreiben. Die gängigste und einfachste Methode wird allerdings über das Modul "views" sein. Über "views" kann schon jetzt der Node-Type "stellenangebot" ausgewählt werden. Es fehlen allerdings die zusätzlich gespeicherten Angaben. Es sind noch die Anzeigekriterien und die Ordnungskriterien zu berücksichtigen, die in der Tabelle {stellenangebot}, {stellenangebot_interessent} und {stellenangebot_zuordnung} gespeichert sind. Als Anzeigekriterium der aktuellen Stellenangebote gilt für das Modul "views" der Reihe nach:

  • Node-Type = stellenangebot
  • Aktiviert = 1
  • Anzeige_von = <= now()
  • Anzeige_bis = >= now()

Als Anzeigekriterium des Stellenangebot-Archivs gilt für das Modul "views" der Reihe nach:

  • Node-Type = stellenangebot
  • Aktiviert = 1
  • Archiv = 1
  • Anzeige_bis = > now()

Zusätzlich sollen die Angebote noch sortiert werden können. Die Sortierkriterien sind

  • der Bereich
  • die Interessentengruppen

Um dem Modul "views" Zugriff auf diese Informationen zu ermöglichen müssen die einzelnen Datenfelder in dem Modul "angemeldet" werden. Das kann über zwei Wege gemacht werden, über eine Funktion innerhalb des .module-Files oder über ein .inc-File. Dabei muss man bei Views Felder, Argumente, Filter, Hervorgehobene Filter und Sortierkriterien unterscheiden. Filter sind Ausschlusskriterien bzw. Anzeigekriterien, d. h. das Nodes die das Kriterium treffen bzw. nicht treffen nicht angezeigt werden. Bei der Implementierung des Nodes in das Modul Views muss man jedes gewünschte Kriterium für jede gewünschte Funktion anmelden. Man sollte sich also vorher genau überlegen welche Funktionalität man benötigt. In diesem Beispiel sind die Werte Archiv, Activ, Anzeige von und Anzeige bis Ausschlusskriterien und werden als Filter angelegt. Ein Node der nicht aktiviert ist soll nie angezeigt werden. Nun aber los:
/**
* Implementierung der eigenen Tabellen in das Modul Views
*/
function stellenangebot_views_tables()
{  
// Anlegen der Tabelle stellenangebot
$tables['stellenangebot'] = array (
'fields' => array( 'archiv' => array ( 'name' => t('Stellenangebot: archiv'),
'handler' => array( 'auction_views_field_archiv' => t('Normal') ),
'option' => array(),
'sortable' => true,
'help' => t('Display the Stellenangebot-Settings.'), ),
'active' => array ( 'name' => t('Stellenangebot: Aktiv'),
'handler' => array( 'auction_views_field_active' => t('Normal') ),
'option' => array(),
'sortable' => true,
'help' => t('Display the Stellenangebot-Settings.'),
)
),
 
'filters' => array (
'archiv' => array(
'handler' => 'auction_views_filter_bid',
'help' => t('Bitte hier ein 1 als Wert eintragen.'),
'name' => 'Stellenangebot: Archiv',
'operator' => 'views_handler_operator_gtlt',
),
'active' => array(
'handler' => 'auction_views_filter_bid',
'help' => t('Bitte hier ein 1 als Wert eintragen.'),
'name' => 'Stellenangebot: Active',
'operator' => 'views_handler_operator_gtlt',
),
'anzeige_von' => array(
'handler' => 'views_handler_filter_stellenangebot_date',
'help' => t('Bitte hier ein "now" als Wert eintragen. Es werden die Werte immer vom heutigen Zeitpunkt ausgewertet.'),
'name' => 'Stellenangebot: Anzeigen von',
'operator' => 'views_handler_operator_gtlt',
),
'anzeige_bis' => array(
'handler' => 'views_handler_filter_stellenangebot_date',
'help' => t('Bitte hier ein "now" als Wert eintragen. Es werden die Werte immer vom heutigen Zeitpunkt ausgewertet.'),
'name' => 'Stellenangebot: Anzeige bis',
'operator' => 'views_handler_operator_gtlt',
),
'bereich' => array(
'handler' => 'stellenangebot_views_filter_bid',
'help' => t('Bereich anzeigen'),
'name' => 'Stellenangebot: Bereich',
'operator' => 'views_handler_operator_yesno',
'value' => array(
'#type' => 'select',
'#options' => 'views_handler_get_bereich',
'#multiple'=> true,
)
),
),  
'join' => array(
'left' => array(
'field' => 'nid',
'table' => 'node' ),
'right' => array(
'field' => 'nid',
)
)
); // Anlegen der Tabelle stellenangebot_zuordnung
 
$tables['stellenangebot_zuordnung'] = array(
'fields' => array(
'idstellenangebot_interessent' => array (
'name' => t('Stellenangebot: Zuordnung der Interessenten'),
'handler' => array(
'auction_views_field_archiv' => t('Normal')
),
'option' => array(),
'sortable' => true,
'help' => t('Display the Stellenangebot-Settings.'),
),
'vid' => array (
'name' => t('Stellenangebot: Zuordnung'),
'handler' => array(
'auction_views_field_archiv' => t('Normal')
),
'option' => array(),
'sortable' => true,
'help' => t('Display the Stellenangebot-Settings.'),
),
),
 
// verknüpfen der Tabelle Node mit Interessentengruppen
'filters' => array (
'idstellenangebot_interessent' => array(
'handler' => 'auction_views_filter_bid',
'help' => t('Ermoeglicht die Asuwahl der Interessentengruppen'),
'name' => 'Stellenangebot: Zuordnung der Interessenten',
'operator' => 'views_handler_operator_eqneq',
'value' => array(
'#type' => 'select',
'#options' => 'views_handler_get_interessentengruppen',
'#multiple'=> true,
)
),
 
),
 
// Verknüpfung mit der Tabelle Node
'join' => array (
'left' => array(
'field' => 'vid',
'table' => 'node'
),
'right' => array(
'field' => 'vid',
)
)
);  
 
return $tables;
} // END stellenangebot_views_tables()
 
/**
* Holen der Interssentgruppen aus der Tabelle stellenangebot_interessent
*/
function views_handler_get_interessentengruppen()
{
// Anlegen des Rückgabe-Arrays
$interessenten = array();
// Abfrage der Interessenten
$ergDB = db_query('SELECT * FROM {stellenangebot_interessent} WHERE active = 1');
 
// Wenn keine Gruppen gefunden wurden
if(db_num_rows($ergDB) == 0)
// Dann ein DEFAULT-Array zurückgeben
return array(0 => 'Keine Auswahl möglich');
 
// Auslesen der Ergebnisse
while($erg = db_fetch_array($ergDB))
$interessenten[$erg['idstellenangebot_interessent']] = $erg['bezeichnung'];

return $interessenten;
} // END views_handler_get_interessentengruppen()
 
/**
* Erstellen der Bereiche
*/
function views_handler_get_bereich()
{
// Anlegen des Rückgabe-Arrays
$bereich = array(
0 => t('alle Bereiche'),
1 => t('Bereich 1'),
2 => t('Bereich 2'),
3 => t('Bereich 3'),
);
 
return $bereich;
} // END views_handler_get_bereich()
 
/**
* Eigener Handler um die Tabellenspalten Bereiche anzusprechen
*/
function stellenangebot_views_filter_bid($op, $filter, $filterinfo, &$query)
{
// Alle Bereiche abgefragt
if($filter['value'] == 0)
{
// Query braucht nicht angepasstzu werden
}
elseif($filter['value'] == 1)
{
$query -> ensure_table('stellenangebot');
$query -> add_where('stellenangebot.bereich1 = 1');
}
elseif($filter['value'] == 2)

{
$query -> ensure_table('stellenangebot');
$query -> add_where('stellenangebot.bereich2 = 1');
}
elseif($filter['value'] == 3)
{
$query -> ensure_table('stellenangebot');
$query -> add_where('stellenangebot.bereich3 = 1');
}
} // END stellenangebot_views_filter_bid
 
/**
* Handler für Views um Datentype mit DATE zu verarbeiten
*/
function views_handler_filter_stellenangebot_date($op, $filter, $filterinfo, &$query)
{
// Wenn now eingeben wurde
if($filter['value'] == 'now')
{
$query -> ensure_table('stellenangebot');
$query -> add_where('UNIX_TIMESTAMP('.$filter['field'].') '.$filter['operator'].' UNIX_TIMESTAMP()');
}
} // END views_handler_filter_stellenangebot_date()
Als erstes müssen die Tabellen in views "angemeldet" werden. Das geschieht durch die Übernahme der Table-Bezeichnung in den Array-Key $tables['stellenangebot'] = array() bzw. $tables['stellenangebot_zuordnung'] = array(). Im nächsten Schritt können die benötigten Komponenten der View angelegt werden, in diesem Beispiel "fields", "filter" und "join" um die Verknüpfung der ID zu kennzeichnen. "fields" werden nur benötigt, wenn Listen oder Tabellen gebildet werden sollen. Über 'active' und 'archiv' soll später ein Liste erstellt werden. "filter" werden benötigt um die Ergebnisse anzupassen. Am einfachsten ist das wenn einfache Datenformate verwendet werden.
'filters' => array (
'archiv' => array(
'handler' => 'auction_views_filter_bid',
'help' => t('Bitte hier ein 1 als Wert eintragen.'),
'name' => 'Stellenangebot: Archiv',
'operator' => 'views_handler_operator_gtlt',
),
),
Die Bezeichnung des Arrays 'archiv' gibt die Tabellen-Spalte an. Als Musskriterium sind 'name', 'help' und 'operator' zu setzen. Eine Übersicht über die wichtigsten Merkmale findet man unter der "Views API". Die 'operator' kann man als Array (array('=', '<=')) übergeben oder man bedient sich der vorgefertigten Operatoren:

function views_handler_operator_andor()
Returns an array of standard and/or/nor operators.
function views_handler_operator_or()
Returns an array of or/nor operators, but does not include AND.
function views_handler_operator_eqneq()
Returns an array of simple equality (equal/not equal to) operators.
function views_handler_operator_gtlt()
Returns an array of the typical numeric =/> operators
function views_handler_operator_yesno()
Returns yes/no values. (This is misnamed as an operator).
function views_handler_operator_like()
Returns an array of the typical string operators.
function views_handler_filter_like($op, $filter, $filterinfo, &$query)
A filter handler to handle the string operators.
function views_handler_filter_timestamp($op, $filter, $filterinfo, &$query)
Returns a 'value' array for timestamps that can pop up a timestamp chooser.
function views_handler_filter_date_value_form()
A handler for the timestamp value above; supports 'from_unixtime' as an arbitrary $filterinfo key -- if true, will transform the date from a standard unix date into a database date.

Leider kennt Views das DATE-Format nicht und von daher muss hierfür ein eigener Handler definiert werden. Als Handler wird 'views_handler_filter_stellenangebot_date' eingesetzt. Drupal erwartet das eine entsprechende Funktion zur Verfügung steht.
'anzeige_von' => array(
'handler' => 'views_handler_filter_stellenangebot_date',
'help' => t('Bitte hier ein "now" als Wert eintragen. Es werden die Werte immer vom heutigen Zeitpunkt ausgewertet.'),
'name' => 'Stellenangebot: Anzeigen von',
'operator' => 'views_handler_operator_gtlt',
),

Die Funktion
/**
* Handler für Views um Datentype mit DATE zu verarbeiten
*/
function views_handler_filter_stellenangebot_date($op, $filter, $filterinfo, &$query)
{
// Wenn now eingeben wurde
if($filter['value'] == 'now')
{
$query -> ensure_table('stellenangebot');
$query -> add_where('UNIX_TIMESTAMP('.$filter['field'].') '.$filter['operator'].' UNIX_TIMESTAMP()');
}
} // END views_handler_filter_stellenangebot_date()

ist erst einmal nur auf den Wert 'now' ausgelegt. In der Funktion wird die Query, die das Modul "views" erstellt, direkt verändert. Leider gibt es nur eine Liste mit möglichen Funktionen die auf das QUERY-Objekt angewendet werden können, diese sind leider nicht dokumentiert.

  • function _views_query($views_get_title_table = 'node', $views_get_title_field = 'nid',
  • function add_field($field, $table = '$$', $alias = '') {
  • function set_distinct() {
  • function clear_fields() {
  • function set_count_field($field) {
  • function add_where($clause) {
  • function add_orderby($table, $field, $order, $alias = '') {
  • function add_groupby($clause) {
  • function ensure_table($table) {
  • function add_table($table, $ensure_path = false, $howmany = 1, $joininfo = NULL) {
  • function queue_table($table) {
  • function set_header($header) {
  • function ensure_path($table, $traced = array(), $add = array()) {
  • function get_table_name($table, $table_num, $alias_prefix = '') {
  • function query($getcount = false) {

Eine weitere interesannte Besonderheit ist dieser Bereich:
'bereich' => array(
'handler' => 'stellenangebot_views_filter_bid',
'help' => t('Bereich anzeigen'),
'name' => 'Stellenangebot: Bereich',
'operator' => 'views_handler_operator_yesno',
'value' => array(
'#type' => 'select',
'#options' => 'views_handler_get_bereich',
'#multiple'=> true,
)
),
In der Datenbank gibt es keine Spalte 'bereich' (die ja eigentlich so deklariert wird), statt dessen werden Boolean-Werte für die Spalten 'bereich1', 'bereich2' und 'bereich3' erfasst. Es soll aber für die User ermöglicht werden durch eine Drop-Down-Liste eine entsprechende Auswahl zu machen. Um dieses zu erreichen wird eine Liste für den Wert 'value' generiert.
/**
* Erstellen der Bereiche
*/
function views_handler_get_bereich()
{
// Anlegen des Rückgabe-Arrays
$bereich = array( 0 => t('alle Bereiche'), 1 => t('Bereich 1'), 2 => t('Bereich 2'), 3 => t('Bereich 3'), );
 
return $bereich;
} // END views_handler_get_bereich()
Die Abfrage-Werte werden dann abgefangen und auf die tatsächlichen Datenbank-Spalten angepasst. Dies geschieht wieder durch die direkte Anpassung der Query.
/**
* Eigener Handler um die Tabellenspalten Bereiche anzusprechen
*/
function stellenangebot_views_filter_bid($op, $filter, $filterinfo, &$query)
{
// Alle Bereiche abgefragt
if($filter['value'] == 0)
{
// Query braucht nicht angepasstzu werden
}
elseif($filter['value'] == 1)

{
$query -> ensure_table('stellenangebot');
$query -> add_where('stellenangebot.bereich1 = 1');
}
elseif($filter['value'] == 2)
{
$query -> ensure_table('stellenangebot');
$query -> add_where('stellenangebot.bereich2 = 1');
}
elseif($filter['value'] == 3)
{
$query -> ensure_table('stellenangebot');
$query -> add_where('stellenangebot.bereich3 = 1');
}
} // END stellenangebot_views_filter_bid

Auch die Verküpfung der Tabellen 'stellenangebot_zuordnung' und 'stellenangebot_interessent' wird durch ein eigene Funktion unterstützt:
/**
* Holen der Interssentgruppen aus der Tabelle stellenangebot_interessent
*/
function views_handler_get_interessentengruppen()
{
// Anlegen des Rückgabe-Arrays
$interessenten = array();
// Abfrage der Interessenten
$ergDB = db_query('SELECT * FROM {stellenangebot_interessent} WHERE active = 1');
// Wenn keine Gruppen gefunden wurden
if(db_num_rows($ergDB) == 0)
// Dann ein DEFAULT-Array zurückgeben
return array(0 => 'Keine Auswahl möglich');

// Auslesen der Ergebnisse
while($erg = db_fetch_array($ergDB))
$interessenten[$erg['idstellenangebot_interessent']] = $erg['bezeichnung'];  
return $interessenten;
} // END views_handler_get_interessentengruppen()
Die Werte für die Select-Box wird aus der Tabelle 'stellenangebot_interessent' ausgelsen und als 'value' übergeben. In der Tabelle 'stellenangebot_zuordnung' kann dann die Verküpfung mit der Node-VID und der ID aus 'stellenangebot_interessent' ausgelesen werden. Jetzt stehen alle geforderten Funktionen zur Verfügung und es können über das Modul views entsprechende Einrichtungen gemacht werden. Bei der Entwicklung der Funktionen für views sollte man das Modul DEVEL aktiviert haben, das Views sehr viele Daten im Chache speichert. Nach jeder Veränderung sollte auch der entsprechende Punkt aus der View entfernt und auf jeden Fall einmal gespeichert werden. Sonst wundert man sich das die gemachten Änderungen nicht übernommen werden.

Einstellungen und Download aller Files für Stellenangebote

Jetzt muss das Modul noch installiert und aktiviert werden. Vorher sollte das Modul "views" installiert werden. Anschließend die Benutzerrechte setzen und die Views einrichten. In den Views sind zwei Seiten eingerichtet, STELLENANGEBOT mit dem link '/stellenangebot' und ARCHIV STELLENANGEBOT mit dem Link 'stellenangebot/archiv'. Das Importscript für Views:

$view = new stdClass();
$view->name = 'stellenangebot';
$view->description = 'Stellenangebot';
$view->access = array ( 0 => '1', 1 => '2', );
$view->view_args_php = '';
$view->page = TRUE;
$view->page_title = 'Aktuelle Stellenangebote'; $view->page_header = ''; $view->page_header_format = '1';
$view-page_footer = '';
$view->page_footer_format = '1';
$view->page_empty = 'Es sind momentan keine Stellenangebote vorhanden';
$view->page_empty_format = '1';
$view->page_type = 'teaser';
$view->url = 'stellenangebot';
$view->use_pager = TRUE;
$view->nodes_per_page = '10';
$view->sort = array ( );
$view->argument = array ( );
$view->field = array ( );
$view->filter = array (
array (
'tablename' => 'node',
'field' => 'type',
'operator' => 'OR',
'options' => '',
'value' => array (
0 => 'stellenangebot',
),
),
array (
'tablename' => 'stellenangebot_zuordnung',
'field' => 'idstellenangebot_interessent',
'operator' => '=',
'options' => '',
'value' => '',
),
array (
'tablename' => 'stellenangebot',
'field' => 'bereich',
'operator' => '1',
'options' => '',
'value' => '0',
),
array (
'tablename' => 'stellenangebot',
'field' => 'anzeige_von',
'operator' => '<=',
'options' => '',
'value' => 'now',
),
array (
'tablename' => 'stellenangebot',
'field' => 'anzeige_bis',
'operator' => '>=',
'options' => '',
'value' => 'now',
),
);
 
$view->exposed_filter = array (
array (
'tablename' => 'stellenangebot_zuordnung',
'field' => 'idstellenangebot_interessent',
'label' => 'Interessentengruppen',
'optional' => '1',
'is_default' => '0',
'operator' => '1',
'single' => '1',
),
array
(
'tablename' => 'stellenangebot',
'field' => 'bereich',
'label' => 'Bereich',
'optional' => '0',
'is_default' => '0',
'operator' => '1',
'single' => '1',
),
);
 
$view->requires = array(node, stellenangebot_zuordnung, stellenangebot); $views[$view->name] = $view;

Für das Archiv
$view = new stdClass();
$view->name = 'stellenangebot_archiv';
$view->description = 'Archiv der Stellenangebote';
$view->access = array ( 0 => '1', 1 => '2', );
$view->view_args_php = '';
$view->page = TRUE;
$view->page_title = '';
$view->page_header = '';
$view->page_header_format = '1';
$view->page_footer = '';
$view->page_footer_format = '1';
$view->page_empty = 'Es steht unter diese Auswahl kein Ergebnis zur Verfügung.';
$view->page_empty_format = '1';
$view->page_type = 'teaser';
$view->url = 'stellenangebot/archiv';
$view->use_pager = TRUE;
$view->nodes_per_page = '10';
$view->sort = array ( );
$view->argument = array ( );
$view->field = array ( );
$view->filter = array (
array (
'tablename' => 'node',
'field' => 'type',
'operator' => 'OR',
'options' => '',
'value' => array ( 0 => 'stellenangebot', ),
),
array (
'tablename' => 'stellenangebot',
'field' => 'active',
'operator' => '=',
'options' => '',
'value' => '1',
),
array (
'tablename' => 'stellenangebot',
'field' => 'archiv',
'operator' => '=',
'options' => '',
'value' => '1',
),
array (
'tablename' => 'stellenangebot_zuordnung',
'field' => 'idstellenangebot_interessent',
'operator' => '=',
'options' => '',
'value' => '',
),
array (
'tablename' => 'stellenangebot',
'field' => 'anzeige_bis',
'operator' => '<',
'options' => '',
'value' => 'now',
),
array (
'tablename' => 'stellenangebot',
'field' => 'bereich',
'operator' => '1',
'options' => '',
'value' => '0',
),
);
 
$view->exposed_filter = array (
array (
'tablename' => 'stellenangebot_zuordnung',
'field' => 'idstellenangebot_interessent',
'label' => 'Interessentengruppen',
'optional' => '1',
'is_default' => '0',
'operator' => '1',
'single' => '1',
),
array (
'tablename' => 'stellenangebot',
'field' => 'bereich',
'label' => 'Bereich',
'optional' => '1',
'is_default' => '0',
'operator' => '1',
'single' => '1', ),
);
 
$view->requires = array(node, stellenangebot, stellenangebot_zuordnung); $views[$view->name] = $view;
Mehr ist nicht zu machen.

AnhangGröße
stellenangebot.zip7.34 KB