Tagebuch einer Extension-Entwicklung
Hallo!
Ich möchte hier gerne eine Art "Tagebuch" einer Extension-Entwicklung schreiben. Ich besitze das TL-Buch, ich kenne das "Hello, World"-Beispiel, ich kenne das CD-Collection-Beispiel, ich kenne das FlowPlayer-Beispiel. Trotzdem ist mir weiterhin nicht glasklar, welche Schritte man bis zur eigenen Extension gehen muss. Ich bin PHP-erfahren, habe aber noch nie eine TL-Extension geschrieben. Ich möchte mit diesem Tagebuch anderen "Anfängern" die Möglichkeit geben, an einem realen Beispiel zu lernen, und auch meine Entscheidungen und Gedanken entlang des Wegs kennen zulernen. In vielen Tutorials wird nur vorgegeben, was in welche Datei geschrieben wird, und was Zeile X oder Zeile Y dort tut. Der Prozess hin zu Zeile X oder Y bleibt leider zu oft im Dunkeln, und erschwert mir die Übertragung der vorgestellten Tutorial-Inhalte auf meine eigenen Probleme. Ich möchte hier auch Irrwege dokumentieren, wie sie für Extension-Anfänger wahrscheinlich typisch sind, und nicht nur ein Tutorial zum Endprodukt abliefern. Ich hoffe, die Foren-Admin akzeptieren so etwas als "Tutorial", falls nicht, dann bitte ich um Verschiebung in die "Fragen"-Sektion. Wobei ich eher berichten als fragen möchte. Natürlich freue ich mich auch über Hinweise, wenn ich vielleicht ganz in die falsche Richtung denke, oder ich im Rahmen meines "Tutorials" selbst nicht mehr weiter weiß.
Liste der Anhänge anzeigen (Anzahl: 4)
Schritt 3: Backend-Modul registrieren und SQL-Tabellenstruktur anlegen
Jetzt geht es also an die Files, die uns der Extension-Generator im letzten Schritt erzeugt hat. Ich vewende WinSCP unter Windows 7 und PSPad als Editor.
Zunächst will ich mein Backend-Modul in TYPOlight bekannt machen. Dafür öffne ich /system/modules/gw_turnierpaare/config/config.php .
Im Skelett dieser Datei sind schon Abschnitte für die Eintragung von Backend-Modulen, Front-Modulen, Content Elementen, Hooks und noch viel mehr vorgesehen. Ich orientiere mich am cd-collection-Tutorial (http://https://contao.org/projects/t...rialsExtension) und trage im Abschnitt für Backend-Module folgendes ein:
PHP-Code:
$GLOBALS['BE_MOD']['content']['gw_turnierpaare'] = array
(
'tables' => array('tl_gw_turnierpaare','tl_gw_meldungen'),
'icon' => 'system/modules/gw_turnierpaare/icons/turnierpaare.png'
);
Hiermit registriere ich ein Modul mit dem Bezeichner "gw_turnierpaare", das sich auf die Datenbanktabellen "tl_gw_turnierpaare" und "tl_gw_meldungen" stützt (wie im Extension-Generator angegeben). In der Liste der Backend-Module (linke Spalte im Backend) soll ein Icon vor dem Bezeichner angezeigt werden, dessen Pfad ich unter 'icon' angegeben habe. Das Icon, was ich mir ausgesucht habe, ist an diesen Post angehängt. Ich habe mich für ein eigenes Unterverzeichnis für mögliche weitere Icons entschieden, und deshalb manuell das Unterverzeichnis "icons" angelegt und mein Icon dort hochgeladen. Im Backend-Modul soll der Berechtigte (also der Sportwart) die Daten Anlegen/Löschen/Ändern dürfen.
Ich speichere die Datei zunächst, und lade meine Backend-Ansicht (als Administrator!) neu. Nun sehe ich in der linken Spalte unter "Inhalte" den neuen Eintrag "gw_turnierpaare" mit meinem Icon. Ein Klick darauf führt leider noch zu einer Fehlermeldung, da die SQL-Tabellen tl_gw_turnierpaare und tl_gw_meldungen noch nicht angelegt sind.
https://community.contao.org/de/atta...1&d=1266917909
Also, die Tabellen anlegen: Ich öffne /system/modules/gw_turnierpaare/config/database.sql , in der mir der Extension-Generator schon ein Skelett für meine beiden Tabellen vorgegeben hat. In beiden Tabellen sind id, pid, sorting und tstamp vorgegeben, sowie der primary key id und der key pid. Ich vermute, dass pid die "Parent ID" ist. Meine Turnierpaare haben keinen parent, darum lösche ich die Definition von "pid" und die Festlegung von "pid" als Key.
Ich füge meine restlichen Felder hinzu, ohne so recht zu wissen, mit welcher Syntax genau. Ich habe mal was von SQL92-Syntax gelesen, aber die kenne ich nicht. Gibt es BOOLEAN-Datentypen? Char vs. Varchar? Sicherheitshalber halte ich mich erstmal an die MySQL-Syntax. Im Endeffekt sieht der Abschnitt für tl_gw_turnierpaare in database.sql so aus:
Code:
CREATE TABLE `tl_gw_turnierpaare` (
`id` int(10) unsigned NOT NULL auto_increment,
`sorting` int(10) unsigned NOT NULL default '0',
`tstamp` int(10) unsigned NOT NULL default '0',
`partnernachname` varchar(64) NOT NULL default '_',
`partnervorname` varchar(64) NULL,
`partnerinnachname` varchar(64) NULL,
`partnerinvorname` varchar(64) NULL,
`startgruppe` varchar(32) NOT NULL default '_',
`startklasselatein` varchar(12) NULL,
`startklassestandard` varchar(12) NULL,
`aktiv` int(1) NOT NULL default '0',
`aktivseit` int(4) NULL,
`aktivbis` int(4) NULL,
`password` varchar(32) NULL,
`bild` varchar(255) NULL,
`anschrift` text NULL,
`zeigeanschrift` int(1) NOT NULL default '0',
`telefon` varchar(32) NULL,
`zeigetelefon` int(1) NOT NULL default '0',
`fax` varchar(32) NULL,
`zeigefax` int(1) NOT NULL default '0',
`mobil` varchar(32) NULL,
`zeigemobil` int(1) NOT NULL default '0',
`email` varchar(128) NULL,
`zeigeemail` int(1) NOT NULL default '0',
`homepage` varchar(128) NULL,
`zeigehomepage` int(1) NOT NULL default '0',
`beschreibung` text NULL,
PRIMARY KEY (`id`),
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Man kann sich hier jetzt beliebige Gedanken über die Datenstruktur, Abstraktion, Normalisierung usw machen. Ich möchte es jetzt _so_ lösen :D .
Meine Meldungen in tl_gw_meldungen sollen parents haben (nämlich die Turnierpaare), darum lasse ich das Skelett so, und erweitere um meine eigenen Felder. Für meinen Fall kommt das hier heraus:
Code:
CREATE TABLE `tl_gw_meldungen` (
`id` int(10) unsigned NOT NULL auto_increment,
`pid` int(10) unsigned NOT NULL default '0',
`sorting` int(10) unsigned NOT NULL default '0',
`tstamp` int(10) unsigned NOT NULL default '0',
`datum` date NOT NULL default '1900-01-01',
`startgruppe` varchar(32) NOT NULL default '_',
`startklasse` varchar(12) NOT NULL default '_',
`lat_std` char(1) NOT NULL default '_',
`turnierort` varchar(128) NOT NULL default '_',
`turnierart` varchar(64) NULL,
`anzahlpaare` int(4) NULL,
`platz_von` int(4) NULL,
`platz_bis` int(4) NULL,
`bemerkung` text NULL,
PRIMARY KEY (`id`),
KEY `pid` (`pid`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Ich speichere database.sql und rufe /typolight/install.php in meiner TYPOlight-Installation auf. TYPOlight bemerkt, daß die Datenbankstruktur nicht mehr aktuell ist, und schlägt mir vor, meine Tabellen anzulegen. Ich bestätige das.
https://community.contao.org/de/atta...1&d=1266920556
Was dann kommt, erstaunt mich dann aber doch etwas: Die Datenbankstruktur soll weiterhin nicht aktuell sein:
https://community.contao.org/de/atta...1&d=1266920556
Auch wenn ich diese Vorschläge bestätige, ändert sich nichts. Ein Blick in die Datenbank zeigt aber, dass die Tabellen wie von mir gewünscht angelegt wurden. Irgendwie kommt TYPOlight dort ins Schleudern. Ein Klick auf gw_turnierpaare im Backend verläuft jetzt aber ohne Fehlermeldung, auch wenn in diesem Backend-Modul noch nichts "passiert".
Meine Frage an dieser Stelle also an die erfahrenen Entwickler: Was ist an meiner SQL-Definition "falsch", dass die Tabellen zwar richtig angelegt werden, das TYPOlight-Install-Tool aber damit nicht zurecht kommt?
Liste der Anhänge anzeigen (Anzahl: 2)
Schritt 4: Wir wagen uns in das DCA-Land
Jetzt kommt es zu einem (vermutlich) harten Brocken. Mein backend-Modul wird links in der Navigation des Backends angezeigt, aber man kann noch keine Datensätze anlegen oder verändern. Dafür müssen wir einen passenden "DCA-Record" anlegen. Ich werde mich wieder vom CD-Collection-Tutorial und der Referenz zu den DCA-Records leiten lassen. Die Referenz ist schon mal erschlagend-beeindruckend.
Mithilfe der DCA-Records erstellt TYPOlight die Masken, mit denen man im Backend die Tabellen füllen, verändern und löschen kann. In /system/modules/gw_turnierpaare/dca/tl_gw_turnierpaare.php hat der Extension-Generator freundlicherweise schon ein Skelett für einen DCA-Record für die Tabelle tl_gw_turnierpaare angelegt.
PHP-Code:
$GLOBALS['TL_DCA']['tl_gw_turnierpaare'] = array
(
// Config
'config' => array
(
'dataContainer' => 'Table',
'enableVersioning' => true
),
...
Der zweite Array-Key in $GLOBALS ist der Name unserer Tabelle. Im darauffolgenden mehrfach verschachtelten Array gibt es zunächst die "config"-Sektion. Hier wird zunächst festgehalten, dass es sich bei der Datenquelle um eine Tabelle handelt. Laut Referenz sind auch noch File und Folder vorgesehen. Sicherlich sind Tabellen der am häufigsten gebrauchte Datacontainer. enableVersioning erlaubt die Versionierung der Einträge - das ist OK und passt mir ins Konzept. Die Referenz verrät mir, daß ich eine "child Table" angeben kann. Da die Turniermeldungen Childs der Turnierpaare werden soll, ergänze ich also
PHP-Code:
'ctable' => 'tl_gw_meldungen'
Die verbleibenden Optionen in "config" erscheinen mir nicht weiter von Bedeutung. Weiter geht es mit dem Abschnitt "list", und dort mit "sorting":
PHP-Code:
// List
'list' => array
(
'sorting' => array
(
'mode' => 1,
'fields' => array(''),
'flag' => 1
),
Sortierart und Sortierreihenfolge sind für mich erstmal ok, da ich gerne nach Nachnamen von Herrn und Dame sortieren würde, verändere ich die Zeile mit 'fields' auf:
PHP-Code:
'fields' => array('partnernachname','partnerinnachname'),
Als nächstes kommt ein Block "'label":
PHP-Code:
'label' => array
(
'fields' => array(''),
'format' => '%s'
),
Hier scheint es wohl darum zu gehen, was in der Liste der schon bestehenden Tabelleneinträge eingezeigt wird. Ich verändere die Zeilen auf
PHP-Code:
'fields' => array('partnernachname','partnervorname','partnerinnachname','partnerinvorname','startgruppe','startklassestd','startklasselat'),
'format' => '%s, %s und %s, %s - %s %s LAT / %s STD'
Etwas "domain-specific knowledge": Startgruppe ist im Prinzip die Altersklasse, Startklasse ist die Leistungsklasse (Die "Liga"), in der das Paar tanzt, und zwar unterschieden nach lateinamerikanischen und Standardtänzen. Diese Infos sind für den Sportwart interessant und sollten in der Übersichtsliste vorhanden sein. Die "%s" im format-String werden in der Reihenfolge mit Feldinhalten befüllt, wie wir sie obendrüber im Array angegeben haben. Der Aufbau des format-Strings sollte PHP- (oder C-)Programmierern bekannt sein.
Dann kommt ein Abschnitt "global_operations" und "operations", den ich aber gar nicht verändern will:
PHP-Code:
'global_operations' => array
(
'all' => array
(
'label' => &$GLOBALS['TL_LANG']['MSC']['all'],
'href' => 'act=select',
'class' => 'header_edit_all',
'attributes' => 'onclick="Backend.getScrollOffset();"'
)
),
'operations' => array
(
'edit' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['edit'],
'href' => 'act=edit',
'icon' => 'edit.gif'
),
'copy' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['copy'],
'href' => 'act=copy',
'icon' => 'copy.gif'
),
'delete' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['delete'],
'href' => 'act=delete',
'icon' => 'delete.gif',
'attributes' => 'onclick="if (!confirm(\'' . $GLOBALS['TL_LANG']['MSC']['deleteConfirm'] . '\')) return false; Backend.getScrollOffset();"'
),
'show' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['show'],
'href' => 'act=show',
'icon' => 'show.gif'
)
)
),
Laut Referenz sollte "global_operations" ein Unterpunkt von "operations" sein, im Skelett-File des Extension-Generators stehen sie aber auf gleicher Ebene. Bin etwas verwundert, aber wird schon funktionieren.
Nächster Abschnitt im vorgegeben File sind "palettes" und "subpalettes". Leider stehen die nicht in der Referenz, und auch die Seite über "palettes" macht mich nicht so richtig schlauer.
PHP-Code:
// Palettes
'palettes' => array
(
'__selector__' => array(''),
'default' => ''
),
// Subpalettes
'subpalettes' => array
(
'' => ''
),
Ein Blick ins CD-Collection-Tutorial verrät, dass man unter "default" die Felder angeben kann, die in Paletten sortiert werden sollen: Felder innerhalb der Palette mit Komma getrennt, Beginn einer neuen Palette durch ein Semikolon.
Da ich es zeitlich für diesen Post nicht schaffen werde, alle Felder meiner tl_gw_turnierpaare-Tabelle zu definieren, will ich zunächst nur die Namensfelder definieren, und zum Testen 2 Paletten benutzen. Ich editiere den "palettes"-Eintrag also in
PHP-Code:
// Palettes
'palettes' => array
(
'__selector__' => array(''),
'default' => 'partnernachname,partnervorname;partnerinnachname,partnerinvorname'
),
"Subpalettes" lässt mich weiterhin ratlos, also Finger weg davon.
Der letzte Teil der Skelett-Datei (und hier wird es richtig spannend!) ist das "fields"-Array:
PHP-Code:
// Fields
'fields' => array
(
'' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare'][''],
'exclude' => true,
'inputType' => 'text',
'eval' => array('mandatory'=>true, 'maxlength'=>255)
)
)
);
Ich halte mich erstmal an das Skelett, und füge nur die Feldnamen hinzu, und vervielfältige den Block auf insgesamt 4 Stück:
PHP-Code:
// Fields
'fields' => array
(
'partnernachname' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['partnernachname'],
'exclude' => false,
'inputType' => 'text',
'eval' => array('mandatory'=>true, 'maxlength'=>64)
),
'partnervorname' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['partnervorname'],
'exclude' => false,
'inputType' => 'text',
'eval' => array('mandatory'=>false, 'maxlength'=>64)
),
'partnerinnachname' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['partnerinnachname'],
'exclude' => false,
'inputType' => 'text',
'eval' => array('mandatory'=>false, 'maxlength'=>64)
),
'partnerinvorname' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['partnerinvorname'],
'exclude' => false,
'inputType' => 'text',
'eval' => array('mandatory'=>false, 'maxlength'=>64)
)
)
Die Labels müssen wir später noch in den Sprachfiles definieren, "exclude" = true bedeutet, dass nur Admins das Feld sehen können. Da später ein Nicht-Admin die Tebelle pflegen können soll, setze ich es also überall auf "false". Ich hoffe mein Gedankengang ist da richtig. Wir setzen für jedes Feld die Maximallänge auf 64 Zeichen, und nur der Partner-Nachname ist verpflichtend. Warum nicht auch Vorname und der Namen der Partnerin? Ich brauche für meine Anwendung EINE klitzekleine Ausnahme, in der ich gerne eine Mannschaft in die Startliste eintragen würde. Deren Name würde dann in 'partnernachname' stehen, die restlichen Felder wären leer.
Für den nächsten Post wird das alles noch verfeinert, weitere Optionen für die Felder hinzugefügt und vor allem alle Felder der Tabelle im DCA-Record definiert. Aber erstmal ein kleines, bescheidenes Zwischenergebnis zur Motivation:
https://community.contao.org/de/atta...1&d=1267007471
Und man kann auch schon was eingeben:
https://community.contao.org/de/atta...1&d=1267007642
Da Startgruppe und Klasse(n) noch nicht einzugeben sind, bleiben die in der Übersichtsliste noch leer. Aber: Grundlegend funktioniert das schonmal, und auch den Begriff "palette" habe ich jetzt (anhand des Screenshots) verstanden.
Im nächsten Post wird das alles erweitert und "poliert".
Liste der Anhänge anzeigen (Anzahl: 2)
Schritt 4b: Verwirrung im DCA-Land
Nachdem ich einige Testpaare in meine "Minimalmaske" eingetragen habe, stelle ich fest, dass es nicht ganz so aussieht, wie ich es gerne hätte.
https://community.contao.org/de/atta...1&d=1267032082
Für jeden Herrennachnamen gibt es eine eigene Gruppenüberschrift. Das ist irgendwie suboptimal, und verschwendet Platz. Ich hätte gerne keine Gruppenüberschriften, oder nur "A", "B", "C", usw...
Ich vermute, dass das mit dem Eintrag ['list']['sorting']['flag'] zusammenhängt, den ich auf "1" hatte, laut Referenz "Sort by initial letter ascending":
PHP-Code:
// List
'list' => array
(
'sorting' => array
(
'mode' => 1,
'fields' => array('partnernachname', 'partnervorname', 'partnerinnachname', 'partnerinvorname'),
'flag' => 1
),
Ich spiele also etwas mit 'flag' herum, und stelle fest: Irgendwie beeinflusst das garnix. 2 = "Sort by initial letter descending", 3 = "Sort by initial two letters ascending", 4 = "Sort by initial two letters descending", 11 = "Sort ascending" oder 12 = "Sort descending" machen in meiner Auflistung nirgendwo irgendeinen Unterschied. Immer wird stur "ascending" in der Reihenfolge meiner Sortierfelder sortiert, und für jeden "Unique" Herrennachnamen gibt es eine Gruppenüberschrift.
In meiner Verzweifelung setze ich ['list']['sorting']['mode'] auf 0, laut Referenz "Records are not sorted". Das Ergebnis sieht so aus:
https://community.contao.org/de/atta...1&d=1267032797
Immerhin die Gruppenüberschriften weg...und stur "ascending" nach meinen Sortierfeldern sortiert. Fast schon unnötig zu erwähnen, dass 'flag' auch hier keine Wirkung zu haben scheint.
Um das Sortieren vielleicht "von Hand" steuern zu können, füge ich streng nach Referenz die Zeile
PHP-Code:
'panelLayout' => 'search,sort,filter'
hinzu in der Erwartung, dass mir dann in der Übersicht die entsprechenden Optionen angeboten werden. Leider - nichts. Die Übersichtsliste der Turnierpaare verändert sich überhaupt nicht.
Meine ['list']['sorting']-Sektion sieht nun so aus:
PHP-Code:
'sorting' => array
(
'mode' => 0,
'fields' => array('partnernachname', 'partnervorname', 'partnerinnachname', 'partnerinvorname'),
'flag' => 1,
'panelLayout' => 'search,sort,filter'
),
Jemand eine Ahnung, warum das Verhalten so ist, bzw. warum mich die Referenz für die DCA-Records so im Stich lässt?
Danke...
Liste der Anhänge anzeigen (Anzahl: 1)
Schritt 4c: Leichte Entwirrung im DCA-Land
Lösung gefunden! Bei den einzelnen 'field'-Beschreibungen muss noch die Freigabe zum Sortieren, Filtern und Suchen gegeben werden. Das sieht jetzt so aus:
PHP-Code:
// Fields
'fields' => array
(
'partnernachname' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['partnernachname'],
'exclude' => false,
'inputType' => 'text',
'search' => true,
'sorting' => true,
'filter' => true,
'flag' => 1,
'eval' => array('mandatory'=>true, 'maxlength'=>64)
),
'partnervorname' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['partnervorname'],
'exclude' => false,
'inputType' => 'text',
'eval' => array('mandatory'=>false, 'maxlength'=>64)
),
'partnerinnachname' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['partnerinnachname'],
'exclude' => false,
'inputType' => 'text',
'search' => true,
'sorting' => true,
'filter' => true,
'flag' => 1,
'eval' => array('mandatory'=>false, 'maxlength'=>64)
),
'partnerinvorname' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['partnerinvorname'],
'exclude' => false,
'inputType' => 'text',
'eval' => array('mandatory'=>false, 'maxlength'=>64)
)
)
Mit Ergebnis:
https://community.contao.org/de/atta...1&d=1267034180
Schon besser, auch wenn die Dropdown-Liste hinter "Suchen:" noch leer ist. Vielleicht liegt das an den noch fehlenden Feld-Labels in den Sprachdateien. Nur warum man 'flag' bei den einzelnen Fields und nochmal global angeben muss, das will ich noch nicht verstehen...
Ergänzung: Und wenn ich ['sorting']['mode'] auf 2 setze, dann kann ich sogar mein Sortierfeld auswählen....sehr schön...
Liste der Anhänge anzeigen (Anzahl: 3)
Schritt 4d: DCA-Polishing
Nachdem also die leichten Verwirrungen rund um den DCA-Record beseitigt sind, geht es weiter damit, die Backend-"Maske" für die tl_gw_turnierpaare-Tabelle zu definieren und zu "polieren".
Zu jedem Feld lege ich einen Verweis auf den Erklärungs-Text an, der unter dem Eingabefeld angezeigt wird, z.B. für das 'partnernachname'-Feld im Abschnitt ['fields']['partnernachname']:
PHP-Code:
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['partnernachname_explanation'],
Der entsprechende Text muss in den Sprachfiles natürlich noch eingetragen werden - später.
Außerdem ergänze ich den 'eval'-Wert meiner bisherigen 4 Eingabefelder um den Wert 'minlength' => 1, um bei den namen eine Mindestlänge zu erzwingen (Beim Wert 1 wahrscheinlich überflüssig, aber egal).
Bei dem Nachnamen des Partners und der Partnerin ergänze ich außerdem 'tl_class' => 'w50'. Das sorgt dafür, dass zwei Felder nebeneinander dargestellt werden. Das Feld mit der w50-Klasse links, das darauffolgende rechts. Dadurch werden Nachname und Vorname jeder Person nebeneinander in einer Zeile dargestellt.
Meine Einstellungen für das "partnernachname"-Feld sehen jetzt so aus:
PHP-Code:
// Fields
'fields' => array
(
'partnernachname' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['partnernachname'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['partnernachname_explanation'],
'exclude' => false,
'inputType' => 'text',
'search' => true,
'sorting' => true,
'filter' => true,
'flag' => 1,
'eval' => array('mandatory'=>true, 'minlength' => 1, 'maxlength'=>64, 'tl_class' => 'w50')
),
Wo ich gerade noch optisch etwas aufräume, baue ich den Eintrag 'default' unter 'palettes' so um:
PHP-Code:
// Palettes
'palettes' => array
(
'__selector__' => array(''),
'default' => '{name_legend},partnernachname,partnervorname,partnerinnachname,partnerinvorname;'
),
{name_legend} legt die Überschrift für die "Palette" fest (also die Felder bis zum nächsten Semikolon). Der Wert muss später im Sprachenfile definiert werden. Ich habe nun alle 4 Textfelder in einer Palette. Optisches Ergebnis:
https://community.contao.org/de/atta...1&d=1267090609
Wenn man sich "vernünftige" Überschriften aus dem Sprachfile dazu vorstellt, schon mal ganz OK :-).
Nun geht es um die noch fehlenden Tabellenfelder.
Zunächst kommt "startgruppe", das bezeichnet die Altersklasse des Paars. Das Feld soll mandatory sein, aber auch eine "leere Option" erlauben. Ich will als Ausnahme auch eine Mannschaft in die Paarliste eingeben können, und Mannschaften haben keine Altersklasse. Mein Code für das field sieht so aus:
PHP-Code:
'startgruppe' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['startgruppe'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['startgruppe_explanation'],
'exclude' => false,
'inputType' => 'select',
'options' => array('KIN I','KIN II', 'JUN I', 'JUN II', 'JUG', 'HGR', 'HGR II', 'SEN I', 'SEN II', 'SEN III', 'SEN IV'),
'eval' => array('mandatory'=>true, 'includeBlankOption' => true)
),
Ich wähle also ein "select", also eine Drop-Down-Box. In "options" liste ich die möglichen Altersgruppen auf. im "eval"-Bereich gebe ich noch an, dass eine leere Option hinzugefügt werden soll.
Dann kommen startklasselatein und startklassestandard. Inhaltlich kann in beiden Feldern dasselbe drinstehen, darum ist es fast nur Copy&Paste für das zweite Feld. Die Definition sieht so aus:
PHP-Code:
'startklasselatein' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['startklasselatein'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['startklasselatein_explanation'],
'exclude' => false,
'inputType' => 'select',
'options' => array('E','D', 'C', 'B', 'A', 'S', 'PRO', 'LL', 'OL', 'RL', '2. BL', '1. BL'),
'eval' => array('mandatory'=>true, 'includeBlankOption' => true, 'tl_class' => 'w50')
),
Auch hier wieder eine Drop-Down-Box mit Optionen und Möglichkeit der "leeren Option". Durch tl_class => w50 wird die Drop-Down-Box nach links gerückt, so dass rechts daneben noch die gleichartige Box für startklassestandard passt. Die hat natürlich KEIN tl_class => w50!
Eigentlich müsste ich prüfen, dass entweder in startklasselatein oder startklassestandard ein Wert ausgewählt ist (also nicht in beiden Feldern die leere Option gewählt wurde), aber das bürde ich zunächst mal dem User auf, vielleicht ergänze ich hier später eine Validation durch einen Hook.
Zur Motivation will ich meine drei neuen Felder auch in im backend sehen, dazu muss ich sie zur Liste der Paletten hinzufügen. Ich packe sie in eine eigene Palette mit Überschrift.
PHP-Code:
// Palettes
'palettes' => array
(
'__selector__' => array(''),
'default' => '{name_legend},partnernachname,partnervorname,partnerinnachname,partnerinvorname;{classes_legend},startgruppe,startklasselatein,startklassestandard'
),
Ergebnis:
https://community.contao.org/de/atta...1&d=1267091838
Weiter geht, jetzt folgen die Felder aktiv, aktivseit und aktivbis.
PHP-Code:
'aktiv' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['aktiv'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['aktiv_explanation'],
'exclude' => false,
'inputType' => 'checkbox',
'eval' => array('mandatory'=>true, 'isBoolean' => true)
),
Aktiv wird eine Checkbox. Ich weiß zwar nicht, was es für eine Bedeutung hat, aber da eine CheckBox immer "Boolean" ist, setze ich in 'eval' isBoolean => true.
PHP-Code:
'aktivseit' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['aktivseit'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['aktivseit_explanation'],
'exclude' => false,
'inputType' => 'text',
'eval' => array('mandatory'=>false, 'minlength' => 4, 'maxlength' => 4, 'rgxp' => 'digit', 'tl_class' => 'w50')
),
'aktivbis' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['aktivbis'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['aktivbis_explanation'],
'exclude' => false,
'inputType' => 'text',
'eval' => array('mandatory'=>false, 'minlength' => 4, 'maxlength' => 4, 'rgxp' => 'digit')
),
aktivseit und aktivbis sollen nur eine Jahreszahl enthalten. Darum setze ich Minimal- und Maximallänge auf 4 und lasse durch 'rgxp' nur Zahlen zu. Man hätte auch eine Dropdown-Box mit Jahreszahlen drin nehmen können. Ich denke beides hat Vor- und Nachteile. Sich durch DropDown-Boxen zu scrollen, die bei "1900" anfangen, wenn man nach "2004" will, ist auch kein Vergnügen. Das erste Feld setze ich mit tl_class => w50 nach links, um das zweite Feld daneben darstellen zu können.
Ich ergänze die Paletten-Definition um
PHP-Code:
{aktiv_legend:hide},aktiv,aktivseit,aktivbis;
Da die Felder nicht so oft editiert werden, schließe ich die Palette defaultmäßig.
Nun kommt schon ein kleiner Sonderfall: password. Dies soll das Paar-Passwort sein, was zum Eintragen von Turnierergebnissen oder geänderten persönlichen Daten im Frontend dient. Ich will das nur in eigenen PHP-Skripten nutzen, von daher habe ich hier alle Freiheiten, wie ich das realisiere.
Ich möchte gerne, dass der Sportwart in diesem Feld ein Klartextpasswort eingeben kann. In die Datenbank soll aber nur der MD5-Hash des Passworts gelangen. Momentan plane ich, dass in dem Textfeld einfach der MD5-Hash angezeigt wird, wenn man aber etwas in dieses Feld eingibt, dass es dann aber durch einen Hook in den Hash umgewandelt wird, bevor es in der Datenbank gespeichert wird. Um die Realisation kümmere ich mich später. Erstmal soll es ein ganz normales Text-Feld sein:
PHP-Code:
'password' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['password'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['password_explanation'],
'exclude' => false,
'inputType' => 'text',
'eval' => array('mandatory'=>false, 'minlength' => 1, 'maxlength' => 64)
)
Dieses Feld soll (alleine) in einer eigenen Palette stehen, darum ergänze ich die Palettendefinition um:
PHP-Code:
{password_legend:hide},password;
Zwischenstand der Backend-Maske:
https://community.contao.org/de/atta...1&d=1267093219
Und nochmal die gesamte Paletten-Definition:
PHP-Code:
// Palettes
'palettes' => array
(
'__selector__' => array(''),
'default' => '{name_legend},partnernachname,partnervorname,partnerinnachname,partnerinvorname;{classes_legend},startgruppe,startklasselatein,startklassestandard;{aktiv_legend:hide},aktiv,aktivseit,aktivbis;{password_legend:hide},password;'
),
Leider ist damit schon wieder das Ende meiner zur Verfügung stehenden Zeit erreicht (Sorry, wenn es zu langsam voran geht). Bald geht es weiter.
Liste der Anhänge anzeigen (Anzahl: 1)
Schritt 4f: Nochmal anders
Tjaja, wie das bei so einem Tagebuch im Gegensatz zum "durchgeplanten und polierten" Tutorial so ist: Ich habe mir nochmal was anders überlegt.
Ich habe mich entschlossen, die Felder startklasselatein und startklassestandard doch mandatory zu machen, aber als Option eine Leer-Option "-" hinzuzufügen. So ist der Benutzer gezwungen, explizit anzugeben, dass ein Paar keine Startklasse in einer der beiden Sektionen hat, und in meiner Paarübersicht sieht es besser aus, wenn z.B. vor "LAT" noch ein Strich steht, statt einfach garnichts.
Bei beiden Feldern sieht der Eintrag in ['fields'] jetzt also so aus:
PHP-Code:
'options' => array('-', 'E','D', 'C', 'B', 'A', 'S', 'PRO', 'LL', 'OL', 'RL', '2. BL', '1. BL'),
'eval' => array('mandatory'=>true)
Dann habe ich noch Entdeckt, dass man eine Checkbox nicht mandatory machen darf, weil dann MUSS sie nämlich angehakt werden. Ist irgendwie suboptimal. Also nochmal den Eintrag ['fields']['aktiv']['eval'] geändert auf:
PHP-Code:
'eval' => array('mandatory'=>false, 'isBoolean' => true)
Und schließlich habe ich den Format-String für die Turnierpaar-Übersicht nochmal überarbeitet. Damit die relevantesten Elemente hervorstechen gebe ich die Nachnamen der Partner fett aus, ebenso die Startgruppe. Die Startklassen zusätzlich in orange (Standard) und rot (Latein). Die Startpässe der Paare in der jeweiligen Sektion haben die gleichen Farben, so dass dies für den Eingeweihten eine natürliche Assoziation ist.
['list']['label']['format'] lautet jetzt:
PHP-Code:
'format' => '<span style="font-weight: bold;">%s</span>, %s und <span style="font-weight: bold;">%s</span>, %s - <span style="font-weight: bold; margin-left: 5px">%s <span style="color: orange; margin-left: 5px;">%s STD</span> / <span style="color: red;">%s LAT</span></span>'
Und sieht so aus:
https://community.contao.org/de/atta...1&d=1267118000
Aber im nächsten Post geht es endlich mit den restlichen Feldern der tl_gw_turnierpaare-Tabelle weiter.
Liste der Anhänge anzeigen (Anzahl: 2)
Schritt 4g: DCA - Almost there
Schritt 4 scheint kein Ende zu nehmen. Wie erwartet erweist sich das Thema "DCA" als harter Brocken.
Zunächst geht es weiter mit den restlichen Feldern der tl_gw_turnierpaare-Tabelle.
Für Anschrift, Telefonnummer, Fax, Mobilnummer, Email-Adresse und Homepage gibt es jeweils ein Flag, ob es im öffentlichen Profil angezeigt werden soll. Ist es nicht gesetzt, sind die Daten nur im Backend sichtbar. So kann der Sportwart das Paar evtl. erreichen, falls notwendig.
Statt die Anschrift in Straße, PLZ, Ort, usw. aufzusplitten, habe ich hierfür eine Textarea vorgesehen. Im DCA sieht das so aus:
PHP-Code:
'anschrift' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['anschrift'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['anschrift_explanation'],
'inputType' => 'textarea',
'eval' => array('mandatory'=>false, 'cols' => 40, 'rows' => 5)
),
Alles wie gehabt, zusätzlich geben cols und rows die Spalten und Zeilen des Eingabebereichs an.
Die Definition für das Flag, ob die Anschrift öffentlich angezeigt werden soll, ist so wie beim Feld "aktiv":
PHP-Code:
'zeigeanschrift' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['zeigeanschrift'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['zeigeanschrift_explanation'],
'inputType' => 'checkbox',
'eval' => array('mandatory'=>false, 'isBoolean' => true)
),
Nicht Besonderes!
Die Felder für Telefon, Fax, Mobilnummer, EMail und Homepage sind jeweils Textfelder, denen ich je nach Art die passende Regular Expression zur Überprüfung der Inhalte zuweise. Zusätzlich hat jedes Feld die "Anzeigen"-Checkbox:
PHP-Code:
'telefon' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['telefon'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['telefon_explanation'],
'inputType' => 'text',
'eval' => array('mandatory'=>false, 'maxlength' => 32, 'rgxp' => 'phone')
),
'zeigetelefon' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['zeigetelefon'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['zeigetelefon_explanation'],
'inputType' => 'checkbox',
'eval' => array('mandatory'=>false, 'isBoolean' => true, 'tl_class' => 'clr m12 w50')
),
'fax' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['fax'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['fax_explanation'],
'inputType' => 'text',
'eval' => array('mandatory'=>false, 'maxlength' => 32, 'rgxp' => 'phone')
),
'zeigefax' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['zeigefax'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['zeigefax_explanation'],
'inputType' => 'checkbox',
'eval' => array('mandatory'=>false, 'isBoolean' => true, 'tl_class' => 'clr m12 w50')
),
'mobil' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['mobil'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['mobil_explanation'],
'inputType' => 'text',
'eval' => array('mandatory'=>false, 'maxlength' => 32, 'rgxp' => 'phone')
),
'zeigemobil' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['zeigemobil'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['zeigemobil_explanation'],
'inputType' => 'checkbox',
'eval' => array('mandatory'=>false, 'isBoolean' => true, 'tl_class' => 'clr m12 w50')
),
'email' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['email'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['email_explanation'],
'inputType' => 'text',
'eval' => array('mandatory'=>false, 'maxlength' => 32, 'rgxp' => 'email')
),
'zeigeemail' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['zeigeemail'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['zeigeemail_explanation'],
'inputType' => 'checkbox',
'eval' => array('mandatory'=>false, 'isBoolean' => true, 'tl_class' => 'clr m12 w50')
),
'homepage' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['homepage'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['homepage_explanation'],
'inputType' => 'text',
'eval' => array('mandatory'=>false, 'maxlength' => 32, 'rgxp' => 'url')
),
'zeigehomepage' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['zeigehomepage'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['zeigehomepage_explanation'],
'inputType' => 'checkbox',
'eval' => array('mandatory'=>false, 'isBoolean' => true, 'tl_class' => 'clr m12 w50')
),
Die tl_class-Werte sind so gewählt, dass jede "Anzeigen"-Checkbox links in einer Reihe mit dem entsprechenden Textfeld (rechts) in einer Zeile steht. Ich hätte es gerne andersrum gehabt, also Textfeld links, Checkbox rechts, aber trotz viel experimentieren mit den tl_class-Werten ist es mir nicht gelungen, das Layout sah immer "zerschossen" aus.
Den Wert ['palettes']['default'] ergänze ich noch um
PHP-Code:
'{contact_legend:hide},zeigeanschrift,anschrift,zeigetelefon,telefon,zeigefax,fax,zeigemobil,mobil,zeigeemail,email,zeigehomepage,homepage;'
Ergebnis:
https://community.contao.org/de/atta...1&d=1267172233
"Beschreibung" ist eine Textarea, die in eigener Palette angezeigt werden soll. Einzige Besonderheit ist hier, dass ich HTML im Inhalt zulassen will.
PHP-Code:
'beschreibung' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['beschreibung'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['beschreibung_explanation'],
'inputType' => 'textarea',
'eval' => array('mandatory'=>false, 'cols' => 80, 'rows' => 20, 'allowHtml' => true)
),
Schließlich fehlt noch das Bild. Hier wollte ich eine Bilder-Auswahl wie im Content-Element "Bild" haben. Ich habe eine Weile herumexperimentiert, insbesondere mit dem Feldtyp "radioTable" (Der aber ganz falsch ist, wie mir jetzt klar ist). Lösung brachte dann ein Blick in das CD-Collection-Tutorial, wo auch so eine Bilderauswahl drin ist. Der richtige DCA-Eintrag lautet:
PHP-Code:
'bild' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['bild'],
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['bild_explanation'],
'inputType' => 'fileTree',
'eval' => array('mandatory'=>false, 'files'=>true, 'fieldType'=>'radio', 'filesOnly' => true, 'extensions' => 'jpg,jpeg,png,gif', 'path' => 'tl_files/GW/Bilder_Turnierpaare/')
)
Typ ist also ein "fileTree", in dem man mittels Radiobutton EIN File auswählen kann (fieldType => radio), in dem Dateien mit den Erweiterungen jpeg,jpg,png und gif angezeigt werden (extensions), in dem Unterverzeichnisse UND Dateien angezeigt werden (files => true, sonst werden NUR Verzeichnisse angezeigt), und in dem man mittels des Radiobuttons auch ausschließlich Dateien auswählen kann, KEINE Unterverzeichnisse (filesOnly => true). Zusätzlich gebe ich den "Basis-Pfad" an, aus dem man auswählen kann (path). Wobei der natürlich bei Jedem anders heißen kann...Also eigentlich nicht so toll. Muss nochmal drüber nachdenken.
Noch die Palettendefinition erweitern um
PHP-Code:
'{beschreibung_legend:hide},beschreibung;{bild_legend:hide},bild;'
und wir landen hier:
https://community.contao.org/de/atta...1&d=1267172831
Damit bin ich im Prinzip mit der Maskendefinition für diese Tabelle fertig, abgesehen vom Hook für das MD5-Hashing meines Passworts. Das verschiebe ich erstmal auf später :-).
Folgende "Probleme" habe ich noch: Meine Palettendefinition sieht insgesamt so aus:
PHP-Code:
// Palettes
'palettes' => array
(
'__selector__' => array(''),
'default' => '{name_legend},partnernachname,partnervorname,partnerinnachname,partnerinvorname;'
.'{classes_legend},startgruppe,startklasselatein,startklassestandard;'
.'{aktiv_legend:hide},aktiv,aktivseit,aktivbis;{password_legend:hide},password;'
.'{contact_legend:hide},zeigeanschrift,anschrift,zeigetelefon,telefon,zeigefax,fax,zeigemobil,mobil,zeigeemail,email,zeigehomepage,homepage;'
.'{beschreibung_legend:hide},beschreibung;{bild_legend:hide},bild;'
),
Eigentlich erwarte ich, dass nur die oberste Palette geöffnet ist, und alle folgenden geschlossen. Komischerweise sind beim Editieren bestehender Einträge und auch bei der Neuanlage alle geöffnet bis auf "password" und "beschreibung". Ich kann nicht verstehen, wieso. Kann mich jemand schlau machen?
Die Eigenschaft 'exclude' im Abschnitt 'fields' soll steuern, ob das jeweilige Feld in der Usergruppenverwaltung spezifisch für einzelne Gruppen (de)aktivierbar ist. Bei exclude => true soll das Feld in der Usergruppenverwaltung erscheinen, bei false soll es dort nicht erscheinen und immer sichtbar sein (für die Gruppen, die das Backend-Modul überhaupt freigegeben haben).
Das Verhalten scheint aber ein anderes zu sein: Egal ob ich exclude true oder false zuweise, erscheint das Feld in der Usergruppenverwaltung. Nur wenn ich exclude ganz weglasse, erscheint es dort nicht. Ein Bug? Keine Ahnung. Zumindest ist es in der Referenz anders beschrieben. Da ich diese Steuerung auf Feldebene nicht brauche, sondern das ganze Backendmodul nur einer bestimmten Usergruppe freischalten möchte, habe ich die 'exclude'-Eigenschaft aus allen Felddefinitionen entfernt.
Mein "Traum" wäre, dass man, falls ein Bild schon ausgewählt ist im Filetree man sofort dieses Bild sieht, statt erst den Filetree öffnen zu müssen, um zu sehen ob irgendwo der Radiobutton gesetzt ist. So kann man beim Öffnen eines Datensatzes nicht schnell sehen, ob ein Paar ein Bild zugewiesen hat, oder nicht. Aber ich glaube, das ist so (noch) nicht vorgesehen.
So, das sollte das Ende von Schritt 4 gewesen (puh),. Um die Maske zu vervollständigen werde ich mit der Definition der Texte in den Sprachfiles weitermachen.
Liste der Anhänge anzeigen (Anzahl: 1)
Schritt 5: Language-Files
Nun geht es an die Sprachfiles, um das Backend-Modul "hübsch" zu machen.
Ich demonstriere es für die deutschen Sprachfiles im /system/modules/gw_turnierpaare/languages/de/-Verzeichnis. Englisch geht genau analog :-).
Zunächst definieren wir die Namen der Back- und Frontendmodule, und einen kurzen Erklärungstext dazu. Das wird in modules.php gemacht:
PHP-Code:
/**
* Back end modules
*/
$GLOBALS['TL_LANG']['MOD']['gw_turnierpaare'] = array('Turnierpaare', 'Verwaltung der Turnierpaare und der Meldeliste.');
/**
* Front end modules
*/
$GLOBALS['TL_LANG']['FMD']['gw_turnierpaarliste'] = array('Turnierpaarliste', 'Dieses Modul zeigt die Turnierpaarliste an');
$GLOBALS['TL_LANG']['FMD']['gw_meldeliste'] = array('Meldeliste', 'Dieses Modul zeigt die Meldeliste an');
Die Bezeichner hinter 'MOD' und 'FMD' müssen die sein, dir wir im config/config.php der Extension definiert haben. Die entsprechenden Texte für die beiden geplanten Frontendmodule habe ich hier auch schon mal eingetragen, auch wenn es die Module noch nicht gibt...
Die Texte für die Backendfelder sind in tl_gw_turnierpaare.php definiert, entsprechend dem Namen der Datenbanktabelle. Die in der DCA-Record-Definition deklarierten Felder müssen wir mit Text füllen. Das ist ziemlich straight-forward:
PHP-Code:
/**
* Fields
*/
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['partnernachname'] = array('Nachname des Partners', 'Bitte den Nachnamen des (männlichen) Partners eingeben');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['partnervorname'] = array('Vorname des Partners', 'Bitte den Vornamen des (männlichen) Partners eingeben');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['partnerinnachname'] = array('Nachname der Partnerin', 'Bitte den Nachnamen des (weiblichen) Partners eingeben');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['partnerinvorname'] = array('Vorname der Partnerin', 'Bitte den Vornamen des (weiblichen) Partners eingeben');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['startgruppe'] = array('Startgruppe', 'Bitte die Startgruppe (JUG, HGR, SEN, ...) des Paares eingeben');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['startklasselatein'] = array('Startklasse Latein', 'Bitte die Startklasse (Latein) des Paares eingeben. Kein Lateinstartbuch = "-"');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['startklassestandard'] = array('Startklasse Standard', 'Bitte die Startklasse (Standard) des Paares eingeben. Kein Standardstartbuch = "-"');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['aktiv'] = array('Aktiv', 'Bitte angeben, ob das Paar noch aktiv ist');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['aktivseit'] = array('Aktiv seit', 'Bitte Jahreszahl des ersten Starts angeben (z.B. 2005)');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['aktivbis'] = array('Aktiv bis', 'Bitte Jahreszahl des letzten Starts angeben, wenn das Paar nicht mehr aktiv ist (z.B. 2008)');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['password'] = array('Passwort', 'Bitte ein Passwort für das Paar anlegen');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['zeigeanschrift'] = array('Anschrift anzeigen', 'Anschrift in der Visitenkarte sichtbar?');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['anschrift'] = array('Anschrift', 'Bitte Anschrift des Paares eingeben');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['zeigetelefon'] = array('Telefonnummer anzeigen', 'Telefonnummer in der Visitenkarte sichtbar?');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['telefon'] = array('Telefonnummer', 'Bitte Telefonnummer des Paares eingeben');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['zeigefax'] = array('Faxnummer anzeigen', 'Faxnummer in der Visitenkarte sichtbar?');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['fax'] = array('Faxnummer', 'Bitte Faxnummer des Paares eingeben');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['zeigemobil'] = array('Mobilnummer anzeigen', 'Mobilnummer in der Visitenkarte sichtbar?');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['mobil'] = array('Mobilnummer', 'Bitte Mobilnummer des Paares eingeben');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['zeigeemail'] = array('Email-Adresse anzeigen', 'Email-Adresse in der Visitenkarte sichtbar?');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['email'] = array('Email-Adresse', 'Bitte Emailadresse des Paares eingeben');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['zeigehomepage'] = array('Homepage-Adresse anzeigen', 'Homepage-Adresse in der Visitenkarte sichtbar?');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['homepage'] = array('Homepage-Adresse', 'Bitte Homepage-Adresse des Paares eingeben');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['beschreibung'] = array('Beschreibungstext', 'Bitte Beschreibungstext des Paares eingeben');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['bild'] = array('Paarbild', 'Bitte ein Paarbild auswählen');
/**
* Reference
*/
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['name_legend'] = 'Namen';
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['classes_legend'] = 'Startdaten';
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['aktiv_legend'] = 'Aktiv';
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['password_legend'] = 'Passwort';
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['contact_legend'] = 'Kontakt';
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['beschreibung_legend'] = 'Beschreibung';
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['bild_legend'] = 'Bild';
/**
* Buttons
*/
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['new'] = array('Neues Paar', 'Ein neues Turnierpaar anlegen');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['edit'] = array('Editieren', 'Das Turnierpaar editieren');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['copy'] = array('Paar kopieren', 'Das Turnierpaar in die Zwischenablage kopieren');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['delete'] = array('Paar löschen', 'Das Turnierpaar aus der Liste entfernen');
$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['show'] = array('Details', 'Die Detailansicht des Turnierpaars anzeigen');
Unterschieden werden die Texte für die Eingabefelder, die aus Überschrift/Bezeichner für das Feld und dem darunter angezeigten Beschreibungstext bestehen, den Texten für Palettenüberschriften (mittlerer Teil) und den Texten für die Buttons in den Masken (letzter Teil). Umlaute müssen HTML-üblich durch ihre Ersatzcodes (ä = ä usw.) dargestellt werden. Die Beschreibungstexte sollten auch nicht zu lang werden.
Ich habe in der DCA-Definition auch Referenzen auf Sprach-Strings für die Eigenschaft "explanation" angegeben, z.B. so:
PHP-Code:
'explanation' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['partnernachname_explanation'],
allerdings habe ich nicht erkennen können, wo das benutzt wird. Vielleicht nur in bestimmten Situatuionen/Konfigurationen. Ich habe all diese Referenzen bei jedem Feld aus meiner DCA-Konfiguration also wieder entfernt.
Die Backend-Maske sieht jetzt so aus:
https://community.contao.org/de/atta...1&d=1267525875
Liste der Anhänge anzeigen (Anzahl: 2)
Schritt 6: Von Callbacks und Subpaletten
Nachdem auch das englische Sprachfile fertig ist, geht es nun an die letzte fehlende Funktionalität der Backend-Maske. Zunächst gibt es aber noch einige Detailkorrekturen.
Da MySQL keinen "Boolean"-Datentyp bietet, hatte ich die Felder, die nur true/false sein können, als int(1) angelegt, mit den möglichen Werten 0/1. Das klappt auch prinzipiell, die Stati der Checkboxen werden gespeichert und wieder aus der Datenbank ausgelesen, aber ein Problem zeigt sich, wenn man nach so einem Feld filtern will: Ich will nach dem "aktiv"-Feld filtern können. Häufig sind die die "aktiven" Turnierpaare von Interesse, die nicht mehr aktiven verstopfen aber die Liste.
Wählt man dieses Feld nun als Filter-Feld aus, werden in der DropDown-Liste als Filtermöglichkeiten aber nur "Ja" und nochmals "Ja" angezeigt. Das Filtern klappt damit auch, bei dem einen "Ja" werden nur die aktiven Paare angezeigt, beim anderen "Ja" die inaktiven. Aber das ist natürlich nicht so gewollt.
Durch Abschauen bei anderen Extensions bin ich darauf gekommen, die Checkbox-Felder durch "char(1)" statt "int(1)" abzubilden. Das klappt genau so gut, und auch das Filtern funktioniert mit "Ja" und "Nein". Alle int(1)-Felder wurden entsprechend in char(1) verändert. Nach dem Anpassen der database.sql muss natürlich das Install-Tool ausgeführt werden, um die Änderungen in der Datenbank durchzuführen.
Die database.sql sieht jetzt so aus:
Code:
CREATE TABLE `tl_gw_turnierpaare` (
`id` int(10) unsigned NOT NULL auto_increment,
`sorting` int(10) unsigned NOT NULL default '0',
`tstamp` int(10) unsigned NOT NULL default '0',
`partnernachname` varchar(64) NOT NULL default '',
`partnervorname` varchar(64) NULL default NULL,
`partnerinnachname` varchar(64) NULL default NULL,
`partnerinvorname` varchar(64) NULL default NULL,
`startgruppe` varchar(32) NOT NULL default '',
`startklasselatein` varchar(12) NULL default NULL,
`startklassestandard` varchar(12) NULL default NULL,
`aktiv` char(1) NOT NULL default '',
`aktivseit` int(4) NULL default NULL,
`aktivbis` int(4) NULL default NULL,
`resetpassword` char(1) NULL default '',
`password` varchar(64) NULL default NULL,
`bild` varchar(255) NULL default NULL,
`anschrift` text NULL,
`zeigeanschrift` char(1) NOT NULL default '',
`telefon` varchar(32) NULL default NULL,
`zeigetelefon` char(1) NOT NULL default '',
`fax` varchar(32) NULL default NULL,
`zeigefax` char(1) NOT NULL default '',
`mobil` varchar(32) NULL default NULL,
`zeigemobil` char(1) NOT NULL default '',
`email` varchar(128) NULL default NULL,
`zeigeemail` char(1) NOT NULL default '',
`homepage` varchar(128) NULL default NULL,
`zeigehomepage` char(1) NOT NULL default '',
`beschreibung` text NULL,
PRIMARY KEY (`id`),
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Wer aufgepasst hat, dem ist auch noch ein neues Feld aufgefallen:
Code:
`resetpassword` char(1) NULL default '',
Das werde ich gleich für das Aktivieren einer Subpalette benötigen. Der Inhalt des Feldes in der Datenbank wird später nicht gebraucht, aber leider muss das Feld vorhanden sein, um es so nutzen zu können, wie ich es vorhabe.
Für das Passwortfeld habe ich ich vor, dass ein dort eingegebenes Passwort SHA1-gehasht in der Datenbank abgelegt wird, nicht im Klartext. Dabei wird ein eventuell schon vorhandes Passwort natürlich überschrieben. Um Fehleingaben zu verhindern, möchte ich eine Checkbox anzeigen, die defaultmäßig "aus" ist. Erst wenn die Checkbox aktiviert ist, soll per AJAX das Passwortfeld angezeigt werden.
Zunächst fügen die wir Definition für das Checkbox-Feld in die "field"-Sektion des DCA-Records ein:
PHP-Code:
'resetpassword' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['resetpassword'],
'inputType' => 'checkbox',
'default' => '',
'eval' => array('mandatory'=>false, 'isBoolean' => true, 'submitOnChange' => true),
),
submitOnChange bewirkt, dass das Formular neu geladen wird, wenn das Feld angeklickt wird. nur dann wird das Passwortfeld nachgeladen.
Dafür benötigen wir eine sogenannte "Subpalette". Die Checkbox (bei mir "resetpassword") muss als "__selector__" angegeben werden im DCA-Record:
PHP-Code:
// Palettes
'palettes' => array
(
'__selector__' => array('resetpassword'),
...
In der "default"-Sektion sieht die Palettendefinition so aus:
PHP-Code:
.'{aktiv_legend:hide},aktiv,aktivseit,aktivbis;{password_legend:hide},resetpassword;'
Hier steht also der Name der Subpalette. Der Name des Passwortfeldes steht hier nicht mehr. Das wird in der "subpalettes"-Sektion angegeben:
PHP-Code:
// Subpalettes
'subpalettes' => array
(
'resetpassword' => 'password'
),
Dies bedeutet, dass das Feld "password" in die Subpalette "resetpassword" eingeblendet wird, wenn "resetpassword" aktiviert ist. Wird es deaktiviert, verschwindet die Subpalette wieder.
https://community.contao.org/de/atta...1&d=1267814965
---
https://community.contao.org/de/atta...1&d=1267814965
Die Texte hierzu wurden in den Sprachfiles entsprechend erweitert und angepasst.
Für beide Felder, resetpassword und password, benötige ich besondere Funktionalitäten. resetpassword soll bei Öffnen der Backendmaske IMMER deaktiviert, das Passwort-Feld also versteckt sein - egal was in der Datenbank für das Feld steht. Dafür setze ich zunächst
PHP-Code:
'default' => '',
was dafür sorgt, dass beim Anlegen eines neuen Datensatzes die Checkbox deaktiviert ist. Und dann gibt es noch die Option "load_callback", in der eine Funktion angegeben werden kann, die beim Laden des Feldes aufgerufen wird (Zu den Details von Callbacks gleich mehr).
Hier habe ich versucht, durch "return '';" immer den Defaultwert zurückzugeben. Leider klappt das nicht richtig, wenn der Datensatz mit "speichern" gespeichert wird, aber geöffnet bleibt. Obwohl die Checkbox dann deaktiviert dargestellt wird, bleibt das Passwort-Feld trotzdem angezeigt und wird nicht versteckt. Erst durch manuelles Aktivieren und erneutes Deaktivieren verschwindet das Passwortfeld wieder. Ich weiß nicht, ob das ein Bug oder gewollt ist, zumindest gefiel es mir nicht.
Ein weiterer Versuch scheiterte mit dem save_callback: Eine Funktion, die aufgerufen wird, bevor das Feld in die Datenbank gespeichert wird. Hier versuchte ich ebenfalls durch ein "return '';" zu erzwingen, dass immer ein deaktiviertes Feld gespeichert wird, und damit auch beim erneuten Anzeigen des Formulars deaktiviert bleibt. Das funktioniert optisch auch sehr gut. Leider werden dann aber keine Eingaben im Passwort-Feld gespeichert.
Der Grund wird sehr wahrscheinlich sein, dass der save_callback auf dem resetpassword-Feld ausgeführt wird, bevor der Input im password-Feld verarbeitet wird. Mein Save-Callback deaktiviert die Checkbox, und damit auch die Subpalette, und das Feld in der Subpalette wird gar nicht mehr ausgewertet oder gespeichert. Auch die Reihenfolge der Felddefinitionen hat darauf keinen Einfluss, es kommt wohl auf die Reihenfolge in der Palettendefinition an, und die kann ich nicht umdrehen.
An der Stelle war tiefe Frustration angesagt, aber ich habe das Problem anders lösen können. Beim resetpassword-Feld werden jetzt keine Callbacks verwendet.
Dafür aber beim password-Feld, was jetzt in der Definition so aussieht:
PHP-Code:
'password' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['password'],
'inputType' => 'text',
'eval' => array('mandatory'=>false, 'minlength' => 1, 'maxlength' => 64),
'load_callback' => array(array('tl_gw_turnierpaare','password_load_callback')),
'save_callback' => array(array('tl_gw_turnierpaare','password_save_callback'))
),
Durch diese Definition wird beim Laden des Feldes die Funktion "password_load_callback" und beim Speichern "password_save_callback" aufgerufen, die sich in der Klasse "tl_gw_turnierpaare" befinden. Diese Klasse lege ich in der DCA-Definitions-Datei /modules/gw_turnierpaare/dca/tl_gw_turnierpaare.php an.
PHP-Code:
class tl_gw_turnierpaare extends Backend
{
/**
* Import the back end user object
*/
public function __construct()
{
parent::__construct();
$this->import('BackendUser', 'User');
}
public function password_load_callback()
{
...
}
public function password_save_callback($var, $dc)
{
...
}
}
Das Gerüst habe ich mir bei anderen Extensions abgeschaut, es scheint zumindest klug zu sein, von "Backend" zu erben, und den Konstruktur zu überschreiben. Vielleicht ist es auch nicht nötig, ich habe es nicht probiert
Ob der load_callback Parameter übergeben bekommt weiss ich nicht, aber ich benötige keinen Parameter.
Der save_callback erhält den Wert des Feldes, das gespeichert werden soll ($var), und den DataContainer ($dc), der zu dem Formular gehört. Meist (wie auch hier) ist es DC_Table, der DataContainer für Datenbanktabellen.
Mein password-Feld in der Datenbank wird den SHA1-Hash, also einen langen String unverständlicher hexadezimaler Zahlen enthalten. Es nützt nichts, wenn ich den im Backend im Passwort-Feld anzeige. Dort will der Admin Klartext-Passwörter eingeben. Im Load-Callback setze ich den Wert des password-Felds also auf einen leeren String, egal was in der Datenbank steht:
PHP-Code:
public function password_load_callback()
{
// Passwort-Feld immer leer anzeigen (Damit der User SHA1-Hash nicht sieht)
return '';
}
Damit wird der User schon mal nicht vom "Müll" aus der Datenbank belästigt. Umgekehrt müssen wir aber nicht das Klartext-Passwort, sondern den Hash in die Datenbank schreiben. Das macht der save_callback:
PHP-Code:
public function password_save_callback($var, $dc)
{
// Kein neues PW angegeben: Feld nicht ändern
if(strlen($var) < 1) return '';
// Aktuellen Datensatz aus DB holen
$row = $this->Database->prepare("SELECT * FROM tl_gw_turnierpaare WHERE id=?")
->execute($dc->id);
// PW in Passwort und Salt aufspalten
list($strPassword, $strSalt) = explode(':', $row->password);
// Falls kein Salt vorhanden, dann erzeugen
if (!strlen($strSalt))
{
$strSalt = substr(md5(uniqid('', true)), 0, 23);
}
// SHA1-Hash aus Salt+neuem Passwort berechnen, Salt anhängen
$pwd = sha1($strSalt . $var) . ':' . $strSalt;
// Das resetpassword-Feld löschen
$this->Database->prepare("UPDATE tl_gw_turnierpaare SET resetpassword='' WHERE id=?")
->executeUncached($dc->id);
return $pwd;
}
Falls kein Passwort angegeben wurde, macht der save_callback garnichts, und gibt einen leeren String zurück. Das Feld "Id" des DataContainers enthält die id des aktuellen Datensatzes in der Datenbank. Um den Hash berechnen zu können, benötige ich das "alte" Passwort in der Datenbank. Darum hole ich mir erstmal den gesamten Datensatz mit der ID ab.
Die Hash-Erzeugung habe ich mir beim File /system/libraries/User.php abgeschaut und funktioniert genauso wie in der Userverwaltung von TYPOlight: Der Hash wird zusammen mit einem "Salt" erzeugt, der zusammen mit dem Hash (durch Doppelpunkt getrennt) im Passwort-Feld abgespeichert wird. Existiert noch kein Salt, wird er erzeugt. Existiert der Salt schon, wird er weiterverwendet (und dafür muss ich das alte Passwort aus der Datenbank auslesen - um an den evtl. schon vorhandenen Salt zu kommen). Die Hash-Erzeugung läuft dann ziemlich straight-forward ab.
Und fast ganz am Ende nochmal der Knackpunkt: Hier setze ich das resetpassword-Feld in der Datenbank auf ''. Das (und leider nur das) sorgt in allen Fällen dafür, dass beim Öffnen von Datensätzen die Subpalette für das Passwort-Feld geschlossen ist.
Abschließend gebe ich den berechneten Hash zurück. Er wird dann in die Datenbank eingetragen.
Wichtiger Hinweis noch: load_callback und save_callback müssen doppelt geschachtelte Arrays sein, weil es mehrere Callbacks geben kann, die nacheinander aufgerufen werden, der Feldwert wird jeweils durch alle durchgeschleust. Falls man aber z.B. einen onSubmitCallback für das ganze Formular vorgeben möchte, ist das nur ein einfaches Array mit Klassennamen und Methodenname, weil es hier nur einen Callback gibt. Das ist so leider in der Referenz der möglichen Callbacks nicht dokumentiert, und hat mir kurz graue Haare beschert.
Der aktuelle Codestand wird gleich oben im ersten Post aktualisiert.
Damit ist das Backend-Modul für die Turnierpaar-Tabelle erstmal fertig.
Weiter wird es dann (endlich) mit dem Frontendmodul für die Turnierpaarliste gehen.
Liste der Anhänge anzeigen (Anzahl: 4)
Schritt 7: Endlich Frontend!
nach soviel Kampf hinter den Kulissen wird jetzt endlich was im Frontend angezeigt und - um das vorweg zu nehmen - das klappt ziemlich reibungslos.
Zunächst mache ich eine "Simpel-Version" des Frontends, die erstmal was anzeigt. Verfeinerungsschritte kommen dann nach und nach dazu. Erstmal muss das schnelle Erfolgserlebnis her.
Zunächst mal müssen die Frontendmodule in system/modules/gw_turnierpaare/config/config.php "registriert" werden:
PHP-Code:
// Front end module
array_insert($GLOBALS['FE_MOD']['turnierpaare'], 0, array
(
'gw_turnierpaarliste' => 'gwTurnierpaarliste',
'gw_meldeliste' => 'gwMeldeliste'
));
'turnierpaare' scheint die Zwischenüberschrift zu sein, die man in der Dropdownliste der zur Verfügung stehenden Module zu sehen kriegt. Die Keys in dem Array sind einfach nur die Bezeichner, die durch die Languagefiles auch übersetzt werden. Die Values dahinter sind die Namen der Frontendklassen. Zu jeder Frontendklasse existiert /system/modules/gw_turnierpaare/*.php mit der entsprechenden Klassendefinition.
Damit in der Modul-Dropdown-Liste nicht das hässliche 'turnierpaare' steht, muss die Languagedatei erweiter werden, z.b. system/modules/gw_turnierpaare/languages/de/modules.php um:
PHP-Code:
$GLOBALS['TL_LANG']['FMD']['turnierpaare'] = array('Turnierpaare');
Nun kommt das eine große Puzzle-Teil für die Frontendausgabe: Die PHP-Klasse, die unsere Inhalte aus der Datenbank holt, aufbereitet und an das Template weiterreicht:
PHP-Code:
class gwTurnierpaarliste extends Module
{
/**
* Template
* @var string
*/
protected $strTemplate = 'gw_turnierpaarliste';
/**
* Generate module
*/
protected function compile()
{
$arrPaare = array();
$objPaare = $this->Database->execute("SELECT * FROM tl_gw_turnierpaare ORDER BY partnernachname, partnerinnachname");
while ($objPaare->next())
{
$newArr = array
(
'partnernachname' => trim($objPaare->partnernachname),
'partnervorname' => trim($objPaare->partnervorname),
'partnerinnachname' => trim($objPaare->partnerinnachname),
'partnerinvorname' => trim($objPaare->partnerinvorname),
'startgruppe' => $objPaare->startgruppe,
'startklasselatein' => $objPaare->startklasselatein,
'startklassestandard' => $objPaare->startklassestandard,
'aktiv' => $objPaare->aktiv,
'aktivseit' => $objPaare->aktivseit,
'aktivbis' => $objPaare->aktivbis,
);
if(strlen($objPaare->bild) == 0)
{
$newArr['bild'] = '/system/modules/gw_turnierpaare/icons/default.png';
}
else
{
$newArr['bild'] = $this->getImage($objPaare->bild, '30', '30');
}
$arrPaare[] = $newArr;
}
$this->Template->paare = $arrPaare;
}
}
Wir leiten von Module ab, und $strTemplate enthält den Namen des Templates, was wir füllen wollen. Die Funktion, die die Werte für das Template liefert, muss compile() heißen.
Wir holen uns alle Turnierpaardatensätze aus der Datenbank, sortiert nach den Nachnamen (Andere Sortierungen und Filter werden später hinzugefügt!), durchlaufen diese in einer Schleife und füllen pro Eintrag ein Array $newArr mit den Werten der Datenbankfelder.
Das 'bild'-Feld wird anders behandelt: Ist es leer (also kein Bild ausgewählt), wird als Bild-URL ein Default-Bild übergeben, was ich von Hand zum icons-Unterverzeichnis hinzufüge (Das benutzte Bild hänge ich hier an). Falls ein Bild angegeben wurde, hole ich mir über die in TL eingebaute getImage()-Funktion ein Thumbnail des Bildes, was maximal 30 mal 30 Pixel groß ist.
Dann wird das das Array, was die Daten eines Paares enthält zum Array mit allen Paardaten hinzugefügt, und ganz am Ende die Template-Variable 'paare' mit unserem Ergebnis (Also den Daten aller Paare in der Liste) gefüllt.
Nun das Ausgabetemplate system/modules/gw_turnierpaare/templates/gw_turnierpaarliste.tpl:
Code:
<div class="<?php echo $this->class; ?>"<?php echo $this->cssID; ?><?php if ($this->style): ?> style="<?php echo $this->style; ?>"<?php endif; ?>>
<?php if ($this->headline): ?>
<<?php echo $this->hl; ?>><?php echo $this->headline; ?></<?php echo $this->hl; ?>>
<?php endif; ?>
<table cellpadding="4" cellspacing="0" summary="Turnierpaarliste">
<thead>
<tr>
<th> </th>
<th>Name</th>
<th>Startgruppe</th>
<th>Std</th>
<th>Lat</th>
<th>Aktiv seit</th>
<th> Aktiv bis</th>
</tr>
</thead>
<tbody>
<?php foreach ($this->paare as $paar): ?>
<tr<?php if($paar['aktiv'] != '1') { echo ' style="color: #888;"'; } ?>>
<td>
<img src="<?php echo $paar['bild']; ?>">
</td>
<td><?php echo $paar['partnernachname']; ?>
<?php if($paar['partnervorname']): ?>
<?php echo ', '.$paar['partnervorname']; ?>
<?php endif; ?>
<?php if($paar['partnerinnachname']): ?>
<?php echo ' und '.$paar['partnerinnachname']; ?>
<?php if($paar['partnerinvorname']): ?>
<?php echo ', '.$paar['partnerinvorname']; ?>
<?php endif; ?>
<?php endif; ?>
</td>
<td><?php echo $paar['startgruppe']; ?>
</td>
<td><?php echo $paar['startklassestandard']; ?>
</td>
<td><?php echo $paar['startklasselatein']; ?>
</td>
<td><?php echo $paar['aktivseit']; ?>
</td>
<td><?php echo $paar['aktivbis']; ?>
</td>
</tr><?php endforeach; ?>
</tbody>
</table>
</div>
Den Teil bis zum Beginn der Table habe ich aus einem anderen Template übernommen, und enthält den "Standardheader" mit der Möglichkeit, spezielle Klassen, IDs und Styles anzugeben. Auch eine Überschrift wird zugelassen, falls sie gesetzt ist.
In der Tabelle selbst wird ein Kopfbereich definiert mit den Spaltenüberschriften.
Dann werden alle Paare in der Paarliste (Variable $this->paare) durchlaufen. Falls das Paar NICHT aktiv ist, setze ich einen style für die Tabellenzeile (graue Schrift). Das ist so nicht "sauber" und wird auch so nicht bleiben, sondern durch richtiges CSS-Markup ersetzt. Erstmal dient es zur Visualisierung.
Aus den Namen von Herr und Dame wird ein "schöner" Namensstring zusammengebaut und ausgegeben. Die rechtlichen Felder werden ziemlich straight-forward ausgegeben.
Nun wären wir eigentlich "fast" fertig. Aber um unser neues Frondendmodul nutzen zu können, muss es im Backend unter "Module" angelegt werden. Das übernimmt die Backend-Klasse tl_module, und die weiss noch nichts detailliertes über unser neues Frontendmodul.
Wir müssen die DCA-Konfiguration für die Backend-Klasse tl_module so erweitern, dass die richtige Palette angezeigt wird, wenn bei Modul-Typ unser neues Frontendmodul ausgewählt wird. Dafür legen wir in system/modules/gw_turnierpaare/dca/ eine neue Datei tl_module.php an. Dort können wir die Definition erweitern. Das tun wir NICHT im tl_module-Backendmodul selbst. Da die DCA-Definitionen aller Extensions nacheinander eingelesen werden (und auf jeden Fall nach Frontend und Backend) können wir den fehlenden Eintrag für tl_module bei uns im Modul nachholen.
In die tl_module.php kommt:
PHP-Code:
<?php
// Add a palette to tl_module
$GLOBALS['TL_DCA']['tl_module']['palettes']['gw_turnierpaarliste'] = 'name,type,headline;align,space,cssID';
?>
Wenn in der Modulverwaltung in der Drop-Down-Liste der zur Verfügung stehenden Module unseres ('gw_turnierpaare') ausgewählt wird, wird diese Palette aktiviert, und als Modul-Optionen Name, Typ und Überschrift, Ausrichtung, Abstände und css-Optionen angezeigt. Später werden wir hier noch weitere Optionen einfügen.
Der letzte Key der Definition ['gw_turnierpaarliste'] muss die ID unseres Moduls sein, so wie in config/config.php definiert. Hier hatte ich zuerst 'tl_gw_turnierpaare' benutzt - funktioniert nicht!
Jetzt können wir das neue Modul in der Modulverwaltung anlegen:
https://community.contao.org/de/atta...1&d=1268220072
---
https://community.contao.org/de/atta...1&d=1268220072
Hier sehen wir unsere Frontendmodule (das zweite kommt noch) in der Auswahlliste und bei Selektion des oberen Moduls die geänderte Palette (durch die Erweiterung des DCAs von tl_module). "align" scheint aber irgendwie nicht zu funktionieren. Ich ignoriere das erstmal.
Jetzt können wir das Modul einem Artikel hinzufügen, und bei mir sieht es dann so aus:
https://community.contao.org/de/atta...1&d=1268220225
Gut, das Icon ist auf meinem Hintergrund noch nicht "hübsch", aber die zugrunde liegende Funktionalität (Default-Bild wenn keins gesetzt), usw... ist erkennbar. Auch die Thumbnailerstellung eines Paarbildes funktioniert, und nicht-aktive Paare werden grau dargestellt.
Damit genug für heute, ein erstes Ergebnis ist erreicht, aber das Frontendmodul wird noch deutlich aufgebohrt werden. In den nächsten Folgen ;-).
Liste der Anhänge anzeigen (Anzahl: 1)
Schritt 8: Parameter fürs Modul
Die Ausgabe im Frontendmodul möchte ich gerne in der Modulverwaltung parametrisieren können. Einerseits soll man auswählen können, ob das Modul nur aktive oder nur inaktive, oder alle Paare auflisten soll. Andererseits soll der Sortiermodus zwischen "Nachnamen alphabetisch" und "Nach Altersgruppe aufsteigend" wählbar sein.
Dafür ist der etablierte Weg eine Erweiterung der Tabelle tl_module um die benötigten Felder. Man soll bestehende Fehler wenn möglich recyclen, aber man recycelt dann auch die Beschreibungstexte usw mit, das war für mich nicht passend.
Also habe ich neue Felder angelegt. In der config/database.php meiner Extension:
Code:
--
-- Extend table 'tl_module'
--
CREATE TABLE `tl_module` (
`gw_tp_showonlyactive` char(1) NOT NULL default 'B',
`gw_tp_couplesorting` char(1) NOT NULL default 'A',
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Hier füge ich also zwei "CHAR"-Felder hinzu, die Buchstabencodes enthalten (Beim Filter: B = Alle Paare [Both], A = Nur aktive Paare, I = Nur inaktive Paare; Beim Sortieren: A = Alphabetisch, C = Nach Altersklasse [Class]), die ich dann später im Frontend-Ausgabemodul auslesen und interpretieren muss. WICHTIG: Obwohl ich nur eine schon bestehende Tabelle ERWEITERN will, muss ich hier "CREATE TABLE" verwenden. Das Installtool macht das Richtige daraus...
Danach muss ich das Installtool aufrufen, um die neuen Felder anlegen zu lassen.
Dann muss der DCA-Record für tl_module so erweitert werden, dass meine Felder angezeigt werden, wenn in der Dropdown-Liste der zur Verfügung stehenden Module die Turnierpaarliste ausgewählt wird.
Dafür nehmen wir uns system/modules/gw_turnierpaare/dca/tl_module.php vor:
PHP-Code:
$GLOBALS['TL_DCA']['tl_module']['palettes']['gw_turnierpaarliste'] = '{title_legend},name,headline,type;{sort_legend},gw_tp_showonlyactive,gw_tp_couplesorting;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID,space';
In der Subpalette für mein Modul füge ich einen neuen Abschnitt mit Überschrift "sort_legend" ein. Darin werden meine beiden neuen Datenbankfelder angezeigt.
Die müssen noch im DCA definiert werden:
PHP-Code:
$GLOBALS['TL_DCA']['tl_module']['fields']['gw_tp_showonlyactive'] = array
(
'label' => &$GLOBALS['TL_LANG']['tl_module']['gw_tp_showonlyactive'],
'default' => 'B',
'exclude' => true,
'inputType' => 'select',
'options' => array('B','A','I'),
'reference' => &$GLOBALS['TL_LANG']['tl_module']['gw_tp_filteroptions'],
'eval' => array('mandatory'=>true, 'tl_class' => 'w50')
);
$GLOBALS['TL_DCA']['tl_module']['fields']['gw_tp_couplesorting'] = array
(
'label' => &$GLOBALS['TL_LANG']['tl_module']['gw_tp_couplesorting'],
'default' => 'A',
'exclude' => true,
'inputType' => 'select',
'options' => array('A','C'),
'reference' => &$GLOBALS['TL_LANG']['tl_module']['gw_tp_sortoptions'],
'eval' => array('mandatory'=>true)
);
Der Typ ist "select", also eine Dropdownbox. Die "options" sind die Werte, die in die Datenbank geschrieben werden sollen. Die "reference" sind im Gegensatz dazu die Werte, die in der Dropdown-Box angezeigt werden sollen. Hier war mir anfangs unklar, wie genau die Übersetzungen definiert werden müssen. Dazu gleich mehr. Hier setze ich erstmal die Referenz auf ein Array, was in den Sprachfiles definiert wird. Da die Werte in der Modulverwaltung angezeigt werden, habe ich mich dazu entschlossen, sie unter ['tl_module'] einzusortieren, aber mit meinem Präfix "gw_tp", um Namenskollisionen unwahrscheinlich zu machen.
Nun noch die Definition der Strings in der deutschen Sprachdatei (englisch geht genauso). Da die String zur Modulverwaltung gehören, habe ich mich dazu entschlossen, sie in die Datei system/modules/gw_turnierpaare/languages/de/modules.php zu schreiben. Man hätte auch eine neue Datei tl_module.php anlegen können. Ich weiss nicht, was da "best practice" ist.
Wir ergänzen also die modules.php um:
PHP-Code:
/**
* Back end fields for tl_module
*/
$GLOBALS['TL_LANG']['tl_module']['sort_legend'] = 'Filter und Sortierung';
$GLOBALS['TL_LANG']['tl_module']['gw_tp_showonlyactive'] = array('Aktive/Inaktive Paare auflisten?', 'Bitte wählen Sie aus, ob nur aktive, inaktive oder alle Paare gelistet werden sollen');
$GLOBALS['TL_LANG']['tl_module']['gw_tp_filteroptions'] = array('B' => 'Alle Paare', 'A' => 'Nur aktive Paare', 'I' => 'Nur inaktive Paare');
$GLOBALS['TL_LANG']['tl_module']['gw_tp_couplesorting'] = array('Sortiermodus', 'Bitte wählen Sie aus, wie die Liste der Turnierpaare sortiert sein soll');
$GLOBALS['TL_LANG']['tl_module']['gw_tp_sortoptions'] = array('A' => 'Alphabetisch', 'C' => 'Nach Altersklasse aufsteigend');
Hier kommt nun der Knackpunkt der "reference"-Arrays, der mir nicht klar war, und erst durch Abgucken bei anderen Extensions geklärt wurde:
Das reference-Array darf nicht einfach ein aufsteigendes Array der Texte zu den Optionen sein wie das "options"-Array (also "array('text1','text2','text3');"), sondern es muss ein assoziatives Array mit der Beziehung 'option1' => 'Text1' sein, sonst funktioniert es nicht. Die DCA-Referenz bleibt da leider sehr schwammig.
Auch die Abschnittsüberschrift "sort_legend" definieren wir hier.
Damit haben wir dieses Ergebnis:
https://community.contao.org/de/atta...1&d=1268390837
Leider schaffe ich es in diesem Schritt nicht mehr, die neuen Felder in der Frontendausgabe auch noch auszuwerten (Der Kampf mit dem reference-Array hat doch etwas Zeit gekostet), das muss bis zum nächsten Mal (übernächste Woche) warten, sorry.
Liste der Anhänge anzeigen (Anzahl: 1)
Schritt 8b: Parameter für das Modul, die zweite
Nun geht es daran, die Modulparameter aus Schritt 7 im Frontendmodul auch wirklich zu nutzen.
Dafür muss ich sie mir zuerst beschaffen. Das geschieht zu Beginn der compile()-Funktion in /system/modules/gw_turnierpaare/gwTurnierpaarliste.php:
PHP-Code:
$moduleParams = $this->Database->prepare("SELECT * FROM tl_module WHERE id=?")
->limit(1)
->execute($this->id);
$this->id enthält die Modul-ID, und über einen Blick in die Datenbank bekommen wir alle Felder der entsprechenden Zeile und damit auch unsere Parameter.
Die muss ich nun auswerten uns entsprechend reagieren. Zunächst das Flag, das mir nur die aktiven Paare, nur die inaktiven oder beide liefert:
PHP-Code:
$whereClause = '';
if($moduleParams->gw_tp_showonlyactive == 'A')
{
$whereClause = "WHERE aktiv='1'";
}
else
{
if($moduleParams->gw_tp_showonlyactive == 'I')
{
$whereClause = "WHERE aktiv=''";
}
}
Die Variable $whereClause baue ich später in meinen SELECT der Turnierpaare ein.
Für die Sortierung muss ich die alphabetische Sortierung nach Nachname Herr und Nachname Dame einerseits und die Sortierung nach Startgruppe (und dann Startklasse und erst dann Nachnamen) unterscheiden.
Der erste Fall ist einfach ("ORDER BY partnernachname, partnerinnachname"), der andere aber komplizierter, da die Sortierrreihenfolge nicht alphabetisch ist, sondern sich nach den Altersklassen richten soll, die Jüngsten zuerst. Aber ein wenig Googeln hilft, das Problem mit einem SQL-Query zu lösen. Die FIELD()-Funktion liefert mir den Index des Inhalts eines Datenbankfeldes innerhalb einer Liste von Werten. Das kann ich benutzen, um in beliebiger Reihenfolge zu sortieren. Will ich das Feld "field" in der Reihenfolge "BCA" sortieren, leistet das "ORDER BY FIELD(field,'B','C','A')".
PHP-Code:
$orderClause = 'partnernachname, partnerinnachname';
if($moduleParams->gw_tp_couplesorting == 'C')
{
// Nach Altersgruppen sortieren
$fieldstartgruppe="''";
foreach(array('KIN I','KIN II', 'JUN I', 'JUN II', 'JUG', 'HGR', 'HGR II', 'SEN I', 'SEN II', 'SEN III', 'SEN IV') as $gruppe)
{
$fieldstartgruppe .= ",'".$gruppe."'";
}
$fieldstartklasse="''";
foreach(array('-', 'E','D', 'C', 'B', 'A', 'S', 'PRO', 'LL', 'OL', 'RL', '2. BL', '1. BL') as $klasse)
{
$fieldstartklasse .= ",'".$klasse."'";
}
$orderClause = "FIELD(startgruppe,".$fieldstartgruppe."), FIELD(startklassestandard,".$fieldstartklasse."), FIELD(startklasselatein,".$fieldstartklasse."), partnernachname, partnerinnachname";
}
Defaultmäßig wird nach den Nachnamen alphabetisch sortiert. Wenn aber das gw_tp_couplesorting-Feld 'C' ist, dann wird aus den möglichen Werten der Altersgruppe und der Startklasse jeweils ein String zusammengebaut, der dann in $orderClause zum "ORDER BY"-Teil zusammengebaut wird.
So wird zuerst nach der Altersgruppe (in vorgegebener Reihenfolge), dann nach den Startklassen Standard und Latein (in vorgegebener Reihenfolge) und schließlich nach den Nachnamen sortiert. Natürlich hätte ich die Liste der Gruppen und Klassen direkt als String definieren können, statt ein Array anzulegen, mit einer Variable drüberzulaufen, um daraus wieder einen String zu machen. Aber der Gedanke war, das 'options"-Feld der DCA-Definition der entsprechenden Felder im DCA-Record zu nutzen. Eine Änderung der möglichen Optionen dort würde dann auch gleich an dieser Stelle genutzt werden. Leider scheint die DCA-Definition des Backend-Moduls an dieser Stelle (Frontend-Modul!) leider nicht geladen zu sein, $GLOBALS['TL_DCA'] ist zumindest nicht vorhanden. Schade! Aber vielleicht lagere ich die Arrays noch in eine gemeinsame, geteilte Include-Datei aus, die ich in DCA-Definition und im Frontendmodul nutzen kann. Darum bleibt es erstmal bei der Schleife über den Arrayelementen.
Schließlich muss das ursprüngliche SELECT-Statement noch um die neuen Variablen $whereClause und $orderClause erweitert werden:
PHP-Code:
$objPaare = $this->Database->execute("SELECT * FROM tl_gw_turnierpaare " . $whereClause . "ORDER BY " . $orderClause);
Ich definiere nun zwei Module, einmal die Liste der aktiven Turnierpaare, die eben nur die aktiven Paare anzeigt, sortiert nach Startgruppen und Klassen, und eine Liste der inaktiven Paare, alphabetisch sortiert. Beide Module füge in in eine Seite als Inhaltselemente ein.
Das Ergebnis:
https://community.contao.org/de/atta...1&d=1269375406
Oben das Modul, das die aktiven Turnierpaare sortiert nach Altersgruppe anzeigt, unten das Modul, das die inaktiven Paare alphabetisch sortiert anzeigt. Die graue Farbe bei den Inaktiven wird auch noch geändert, und im nächsten Schritt wird es an den "Detail"-Link gehen.
Liste der Anhänge anzeigen (Anzahl: 3)
Schritt 9: Details, Details, Details!
In der Listenübersicht der Turnierpaare ist als Link bereits vorgesehen, dass man beim Klick auf den "Detail"-Link eines Paares zu einer Detail-Seite kommen kann. Ich möchte das mit demselben Modul regeln, also ohne Weiterleitungsseite auf eine (versteckte) Seite mit einem "DetailViewer"-Modul.
Ich habe mir das schamlos beim EFG abgeguckt. Meine Turnierpaarliste liegt unter turnierpaarliste.html. Wenn man als URL z.B. turnierpaarliste/info/8.html verwendet, wird trotzdem die Seite "turnierpaarliste" aufgerufen, aber jetzt hat der GET-Parameter "info" den Wert 8. Sehr praktisch.
Prüft man im Modulcode auf diesen Parameter, kann man dann gegebenenfalls auf das Detail-Template wechseln und anderen Code ausführen.
Nochmal der entsprechende Abschnitt aus dem Template der /system/modules/gw_turnierpaare/templates/gw_turnierpaarliste.tpl:
PHP-Code:
</td>
<td><?php echo '<a href="/turnierpaarliste/info/'.$paar['id'].'.html">Detail</a>'; ?>
</td>
Hier wird aus der Paar-ID der Detail-Link gebaut. Mich stört noch, dass mein Seitenname "turnierpaarliste" hardkodiert ist. Das werde ich noch ändern müssen, habe gerade aber keine Idee, wie ich an diese Information komme. Außerdem wäre es schöner, statt mit der numerischen ID mit einem alphanumerischen Alias zu arbeiten, so wie bei Seiten oder Artikeln. Das werde ich noch in einem zukünftigen Schritt implementieren.
Das Template für die Detailseite /system/modules/gw_turnierpaare/templates/gw_turnierpaarliste_detail.tpl sieht so aus:
PHP-Code:
<div class="<?php echo $this->class; ?> block paarvisitenkarte"<?php echo $this->cssID; ?>
<?php if ($this->style): ?> style="<?php echo $this->style; ?>"<?php endif; ?>>
<h3><?php echo $this->paar['partnernachname']; ?>
<?php if($this->paar['partnervorname']) { echo ", ".$this->paar['partnervorname']; }; ?>
<?php if($this->paar['partnerinnachname']) { echo " und ".$this->paar['partnerinnachname']; }; ?>
<?php if($this->paar['partnerinvorname']) { echo ", ".$this->paar['partnerinvorname']; }; ?>
</h3>
<?php if ($this->paar['bild']): ?>
<div class="left">
<?php if($this->paar['bildfullsize']): ?>
<a href="<?php echo $this->paar['bildfullsize']; ?>" rel="lightbox[bild]">
<?php endif; ?>
<img src="<?php echo $this->paar['bild']; ?>">
<?php if($this->paar['bildfullsize']): ?>
</a>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="right">
<ul>
<?php if ($this->paar['startklassestandard'] != '-'): ?>
<li>
<?php echo $this->paar['startgruppe']." ".$this->paar['startklassestandard']." Standard"; ?>
</li>
<?php endif; ?>
<?php if ($this->paar['startklasselatein'] != '-'): ?>
<li>
<?php echo $this->paar['startgruppe']." ".$this->paar['startklasselatein']." Latein"; ?>
</li>
<?php endif; ?>
<li>
Aktiv: <?php echo $this->paar['aktivseit']; ?> - <?php if($this->paar['aktivbis']) { echo $this->paar['aktivbis']; } else { echo "heute"; }; ?>
</li>
<?php if ($this->paar['anschrift']): ?>
<li>
<?php echo nl2br($this->paar['anschrift']); ?>
</li>
<?php endif; ?>
<?php if ($this->paar['telefon']): ?>
<li>
Tel.: <?php echo $this->paar['telefon']; ?>
</li>
<?php endif; ?>
<?php if ($this->paar['fax']): ?>
<li>
Fax: <?php echo $this->paar['fax']; ?>
</li>
<?php endif; ?>
<?php if ($this->paar['mobil']): ?>
<li>
Mob.: <?php echo $this->paar['mobil']; ?>
</li>
<?php endif; ?>
<?php if ($this->paar['email']): ?>
<li>
EMail: {{email::<?php echo $this->paar['email']; ?>}}
</li>
<?php endif; ?>
<?php if ($this->paar['homepage']): ?>
<li>
Homepage: <a href="<?php echo $this->paar['homepage']; ?>"><?php echo $this->paar['homepage']; ?></a>
</li>
<?php endif; ?>
</ul>
</div>
<div class="twocol">
<?php echo nl2br($this->paar['beschreibung']); ?>
</div>
</div>
Im Prinzip nichts besonderes, ich bastele aus den Namen eine schöne Überschrift, Bild und die anderen Texte werden in DIVs verpackt, die ich per CSS dann "hübsch" anordne. Beim Bild wird unser Modulcode in $paar['bild'] eine verkleinerte Version (180px breit max.) zurückliefern, in $paar['bildfullsize'] befindet sich das Originalbild, das hier im Template dann über eine Lightbox großklickbar ist. Das Array $paar im Template entspricht ansonsten den Datenbankfeldern in tl_gw_turnierpaare.
Die Hauptarbeit passiert nun in der Frontend-Modul-Klasse /system/modules/gw_turnierpaare/gwTurnierpaarliste.php:
PHP-Code:
class gwTurnierpaarliste extends Module
{
/**
* Template
* @var string
*/
protected $strTemplate = 'gw_turnierpaarliste';
protected $strDetailKey = 'info';
Zunächst definiere ich wie üblich den "Default"-Templatenamen, das ist der für die gesamte Liste. Zusätzlich definiere ich hier das URL-Fragment, was meinen "Detail-Link" ausmachen soll.
PHP-Code:
function obj2Arr($objPaar)
{
$newArr = array
(
'partnernachname' => trim($objPaar->partnernachname),
'partnervorname' => trim($objPaar->partnervorname),
'partnerinnachname' => trim($objPaar->partnerinnachname),
'partnerinvorname' => trim($objPaar->partnerinvorname),
'startgruppe' => $objPaar->startgruppe,
'startklasselatein' => $objPaar->startklasselatein,
'startklassestandard' => $objPaar->startklassestandard,
'aktiv' => $objPaar->aktiv,
'aktivseit' => $objPaar->aktivseit,
'aktivbis' => $objPaar->aktivbis,
'id' => $objPaar->id,
'beschreibung' => $objPaar->beschreibung,
);
if($objPaar->zeigeanschrift == '1')
{
$newArr['anschrift'] = $objPaar->anschrift;
}
if($objPaar->zeigetelefon == '1')
{
$newArr['telefon'] = $objPaar->telefon;
}
if($objPaar->zeigefax == '1')
{
$newArr['fax'] = $objPaar->fax;
}
if($objPaar->zeigemobil == '1')
{
$newArr['mobil'] = $objPaar->mobil;
}
if($objPaar->zeigeemail == '1')
{
$newArr['email'] = $objPaar->email;
}
if($objPaar->zeigehomepage == '1')
{
$newArr['homepage'] = $objPaar->homepage;
}
return $newArr;
}
Diese Utility-Funktion füllt mir eine Zeile des Resultsets der Datenbank in ein Array. Ich brauche das an zwei Stellen, darum habe ich das hierhin ausgelagert. Hier werden auch schon die "zeige"-Flags berücksichtigt: Sind die nicht angehakt, landet auch nichts im Array. Damit spare ich mir die Abfragen im Template.
PHP-Code:
protected function compile()
{
if ( strlen($this->Input->get($this->strDetailKey)) )
{
// A "detail"-URL is given -> Output turnierpaarliste_detail template
$this->compileDetailTemplate();
}
else
{
// Output the turnierpaarliste template
$this->compileListTemplate();
}
}
Die alte "Compile"-Funktion wurde angenehm kurz. Falls der GET-Parameter mit dem Namen "info" gesetzt ist (über unsere URL turnierpaarliste/info/8.html), dann wird eine Funktion zur Ausgabe des Detail-Templates aufgerufen, ansonsten eine für das Listentemplate.
PHP-Code:
// Compiles the turnierpaarliste template
protected function compileListTemplate()
{
$moduleParams = $this->Database->prepare("SELECT * FROM tl_module WHERE id=?")
->limit(1)
->execute($this->id);
$whereClause = '';
if($moduleParams->gw_tp_showonlyactive == 'A')
{
$whereClause = "WHERE aktiv='1'";
}
else
{
if($moduleParams->gw_tp_showonlyactive == 'I')
{
$whereClause = "WHERE aktiv=''";
}
}
$orderClause = 'partnernachname, partnerinnachname';
if($moduleParams->gw_tp_couplesorting == 'C')
{
// Nach Altersgruppen sortieren
$fieldstartgruppe="''";
foreach(array('KIN I','KIN II', 'JUN I', 'JUN II', 'JUG', 'HGR', 'HGR II', 'SEN I', 'SEN II', 'SEN III', 'SEN IV') as $gruppe)
{
$fieldstartgruppe .= ",'".$gruppe."'";
}
$fieldstartklasse="''";
foreach(array('-', 'E','D', 'C', 'B', 'A', 'S', 'PRO', 'LL', 'OL', 'RL', '2. BL', '1. BL') as $klasse)
{
$fieldstartklasse .= ",'".$klasse."'";
}
$orderClause = "FIELD(startgruppe,".$fieldstartgruppe."), FIELD(startklassestandard,".$fieldstartklasse."), FIELD(startklasselatein,".$fieldstartklasse."), partnernachname, partnerinnachname";
}
$arrPaare = array();
$objPaare = $this->Database->execute("SELECT * FROM tl_gw_turnierpaare " . $whereClause . "ORDER BY " . $orderClause);
while ($objPaare->next())
{
$newArr = $this->obj2Arr($objPaare);
if(strlen($objPaare->bild) == 0)
{
$newArr['bild'] = '/system/modules/gw_turnierpaare/icons/default.png';
}
else
{
$newArr['bild'] = $this->getImage($objPaare->bild, '', '48');
}
$arrPaare[] = $newArr;
}
$this->Template->paare = $arrPaare;
}
Bei der Ausgabe des Listentemplates hat sich nicht viel gegenüber dem letzten Schritt getan, nur die Utility-Funktion obj2Arr wird jetzt benutzt, um den Großteil der Felder aus der Datenbank ins Array zu packen. Als Paarbild wird ein Thumbnail von max. 48 Pixeln Höhe ausgegeben.
PHP-Code:
// Compiles the data for the turnierparliste_detail template
protected function compileDetailTemplate()
{
$coupleRow = $this->Database->prepare("SELECT * FROM tl_gw_turnierpaare WHERE id=?")
->limit(1)
->execute($this->Input->get($this->strDetailKey));
$this->strTemplate = 'gw_turnierpaarliste_detail';
$this->Template = new FrontendTemplate($this->strTemplate);
$newArr = $this->obj2Arr($coupleRow);
if(strlen($coupleRow->bild) == 0)
{
$newArr['bild'] = '/system/modules/gw_turnierpaare/icons/default.png';
}
else
{
$newArr['bild'] = $this->getImage($coupleRow->bild, '180', '');
$newArr['bildfullsize'] = $coupleRow->bild;
}
$this->Template->paar = $newArr;
}
Hier die Ausgabe des Detail-Templates: Zunächst holen wir uns das Paar aus der Datenbank, dessen Detailseite ausgegeben werden soll. Hier wäre wohl zukünftig noch eine Fehlerabfrage ("ID existiert nicht") notwendig. *Hüstel*
Dann müssen wir das andere Template wählen. Dafür ersetze ich den Namen durch den Neuen. Das alleine reichte aber nicht: Es erschien immer noch das alte Template, weil es schon initialisiert war, als compile() aufgerufen wurde.
Eine Änderung von $strTemplate bleibt dann unbemerkt. Darum muss jetzt in der nächsten Zeile ein neues Frontendtemplate instanziert werden. Ich hätte den Namen auch direkt als Parameter bei der Instanzierung angeben können. Dann fülle ich mit obj2Arr() mein Paar-Array, und gebe (falls vorhanden) das Paarbild-Thumbnail mit maximal 180 Pixel Breite aus, und zusätzlich das Originalbild.
Das Ergebnis ist das Folgende - Turnierpaarliste:
https://community.contao.org/de/atta...1&d=1269594717
Ich habe das Default-Bild noch ausgetauscht, das angezeigt wird, wenn kein Paarbild hinterlegt ist (Ich hänge es auch hier nochmal an). Das "richtige" Paarbild ist übrigens ein freies aus Wikimedia Commons.
Nach Klick auf einen Detaillink:
https://community.contao.org/de/atta...1&d=1269594718
Liste der Anhänge anzeigen (Anzahl: 6)
Schritt 11: Die zweite Tabelle
Ich habe mich lange davor gedrückt, aber jetzt muss es an meine zweite Tabelle gehen: tl_gw_meldungen.
Meldungen sind Teilnahmen von Turnierpaaren an Turnieren. Sie bestehen aus dem Datum der Turnierteilnahme, dem Ort, der Startgruppe (Altersklasse) und der Startklasse (aus Regeltechnischen Gründen müssen die NICHT zwingend mit den entsprechenden Werten, die beim Turnierpaar eingetragen sind übereinstimmen), der Tanzart (Standard/Lateinamerikanisch), der Turnierart (Hier unterscheidet man "offene Turniere", "Einladungsturniere", "Landesmeisterschaften", "Deutsche Meisterschaften" usw), der Anzahl der gestarteten Paare, dem Platz (Da es geteilte Plätze wie 5.-7. geben kann gibt es "platz_von" und "platz_bis"), und einem Freitext als Bemerkung.
Soviel zur Domain specific knowledge.
Schauen wir uns nochmal die SQL-Definition in config/database.sql an:
Code:
--
-- Table `tl_gw_meldungen`
--
CREATE TABLE `tl_gw_meldungen` (
`id` int(10) unsigned NOT NULL auto_increment,
`pid` int(10) unsigned NOT NULL default '0',
`sorting` int(10) unsigned NOT NULL default '0',
`tstamp` int(10) unsigned NOT NULL default '0',
`datum` date NOT NULL default '2000-01-01',
`startgruppe` varchar(32) NOT NULL default '',
`startklasse` varchar(12) NOT NULL default '',
`lat_std` char(32) NOT NULL default '',
`turnierort` varchar(128) NOT NULL default '',
`turnierart` varchar(64) NULL default NULL,
`anzahlpaare` int(4) NULL default NULL,
`platz_von` int(4) NULL default NULL,
`platz_bis` int(4) NULL default NULL,
`bemerkung` text NULL,
PRIMARY KEY (`id`),
KEY `pid` (`pid`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Die ersten 4 Felder sind ja so "default". Hier macht pid (Parent-ID) aber auch Sinn. Die Meldungen sind "Childs" der Turnierpaare. Im Backend wollte ich auch so vorgehen, also bei den Turnierpaaren auf einen Tree-View umstellen. Im DCA-Record für tl_gw_turnierpaare habe ich also unter "sort" den "mode" auf 5 gestellt, und im DCA-Record für tl_gw_meldungen auf 6.
Dann straft mich TYPOlight aber mit der SQL-Fehlermeldung, dass es in der Tabelle tl_gw_turnierpaare kein Feld "pid" gäbe. Richtig, gibt es auch nicht. Da das die Parent-Tabelle ist, braucht die auch kein "pid". Nun gut, offensichtlich erfordert die Logik das so, also erfülle ich den Wunsch und spendiere auch tl_gw_turnierpaare ein Feld "pid".
Beim Betrachten im Backend fällt mir aber auf, dass das unpraktisch ist: Der Sportwart, der das im Backend pflegt, den interessiert nur die nach dem Turnierdatum sortierte Liste, neueste ganz oben. Der will gar nicht erst das Turnierpaar suchen, um dort eine Meldung einzugeben.
Ich entscheide mich, Turnierpaare und Meldungen im Backend getrennt zu verwalten, aber bei den Meldungen dann eine foreign-key-Beziehung über die pid zur Turnierpaartabelle zu haben. Wenn der Sportwart eine neue Meldung anlegt, soll er über ein Dropdown das Turnierpaar auswählen, zu dem diese Meldung gehört, die Liste selbst soll aber nach dem Datum absteigend sortiert sein.
Ich brauche also eine weitere Seite im Backend, und modifiziere /system/modules/gw_turnierpaare/config/config.php auf:
PHP-Code:
// Back end module
$GLOBALS['BE_MOD']['content']['gw_turnierpaare'] = array
(
'tables' => array('tl_gw_turnierpaare'),
'icon' => 'system/modules/gw_turnierpaare/icons/turnierpaare.png'
);
$GLOBALS['BE_MOD']['content']['gw_meldungen'] = array
(
'tables' => array('tl_gw_meldungen'),
'icon' => 'system/modules/gw_turnierpaare/icons/meldeliste.png'
);
Also zwei hier formal getrennte Tabellen mit getrennten Backendseiten und verschiedenen Icons (meldeliste.png hänge ich hier an, das soll ein Siegerpodest darstellen).
Dann gehts an DCA für die Meldungstabelle, in /system/modules/gw_turnierpaare/dca/tl_gw_meldungen.php:
PHP-Code:
/**
* Table tl_gw_meldungen
*/
$GLOBALS['TL_DCA']['tl_gw_meldungen'] = array
(
// Config
'config' => array
(
'dataContainer' => 'Table',
'enableVersioning' => true,
),
Hier keine Besonderheiten, es ist eine Tabelle mit Versionierung...
PHP-Code:
// List
'list' => array
(
'sorting' => array
(
'mode' => 1,
'fields' => array('datum DESC', 'turnierort'),
'panelLayout' => '',
'flag' => 8,
),
Sortierung nach festem Feld, nämlich dem absteigenden Datum, und dann nach dem Turnierort alphabetisch. Das Panel für Sortieren, Filtern usw lasse ich erstmal weg (Das sorgte nämlich für eine kryptische Fehlermeldung - ich kümmere mich später darum). flag 8 ist das absteigende Sortieren nach dem Monat. Zu meinem Problem damit komme ich später.
PHP-Code:
'label' => array
(
'fields' => array('datum', 'turnierort', 'turnierart', 'startgruppe','startklasse','lat_std','pid'),
'format' => '<span style="font-weight: bold;">%s</span>, %s (%s), %s %s %s - %s'
),
Hier bastele ich mir die Ausgabezeile, mit dem Datum in fett, dann dem Ort, der Turnierart, Startgruppe und Klasse und der Info, ob Standard oder Lateinamerikanisch. Dann müsste dort eigentlich noch der Name des Tanzpaares hin, aber der steht ja nicht (direkt) in der Tabelle, sondern nur die pid. Darum nehme ich erstmal die - besser als Nix. Ich vermute, ich muss/kann da mit einem Label-Callback arbeiten und die pid selbst in die Namen auflösen.
PHP-Code:
'global_operations' => array
(
'all' => array
(
'label' => &$GLOBALS['TL_LANG']['MSC']['all'],
'href' => 'act=select',
'class' => 'header_edit_all',
'attributes' => 'onclick="Backend.getScrollOffset();"'
)
),
'operations' => array
(
'edit' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_meldungen']['edit'],
'href' => 'act=edit',
'icon' => 'edit.gif'
),
'copy' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_meldungen']['copy'],
'href' => 'act=copy',
'icon' => 'copy.gif'
),
'delete' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_meldungen']['delete'],
'href' => 'act=delete',
'icon' => 'delete.gif',
'attributes' => 'onclick="if (!confirm(\'' . $GLOBALS['TL_LANG']['MSC']['deleteConfirm'] . '\')) return false; Backend.getScrollOffset();"'
),
'show' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_meldungen']['show'],
'href' => 'act=show',
'icon' => 'show.gif'
)
)
),
Hier bleibt erstmal alles so, wie vom Extension Creator vorgegeben.
PHP-Code:
// Palettes
'palettes' => array
(
'__selector__' => array(''),
'default' => '{couple_legend},pid;{tournament_legend},datum,turnierort,turnierart,startgruppe,startklasse,lat_std;'
.'{result_legend},anzahlpaare,platz_von,platz_bis,bemerkung;'
),
// Subpalettes
'subpalettes' => array
(
'' => ''
),
Auch hier nichts spannendes: Eine normale Palette für alle Felder.
PHP-Code:
// Fields
'fields' => array
(
'pid' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_meldungen']['pid'],
'inputType' => 'select',
'foreignKey' => 'tl_gw_turnierpaare.partnernachname',
'search' => true,
'sorting' => true,
'eval' => array('mandatory'=>true)
),
pid soll ein Foreign Key in die Turnierpaare-Tabelle sein. mir der foreignKey-Option wird das Dropdown-Feld mit den Partnernachnamen aus der Turnierpaar-Tabelle gefüllt. Zum ersten Testen ist das ganz OK, aber eigentlich stelle ich mir das anders vor: Es sollen dort nur AKTIVE Paare auswählbar sein, und ich hätte dort gerne die kompletten Namen des Turnierpaares stehen, nicht nur den Nachnamen des Herrn. Auch da wird wohl ein Callback hermüssen - später.
PHP-Code:
'datum' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_meldungen']['datum'],
'inputType' => 'text',
'search' => true,
'sorting' => true,
'flag' => 11,
'eval' => array('mandatory'=>true, 'datepicker'=>$this->getDatePickerString(), 'tl_class'=>'w50 wizard', 'minlength' => 1, 'maxlength'=>64, 'rgxp' => 'date')
),
Abgeguckt beim DCA-Record für Artikel: Das Datumsfeld. Insbesondere den getDatePickerString() verstehe ich nicht - muss ich aber auch erstmal nicht. Kommt Zeit, kommt Erleuchtung.
PHP-Code:
'turnierort' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_meldungen']['turnierort'],
'inputType' => 'text',
'search' => true,
'sorting' => true,
'flag' => 1,
'eval' => array('mandatory'=>true, 'minlength' => 1, 'maxlength'=>128, 'tl_class' => 'w50')
),
Nichts Besonderes hier...
PHP-Code:
'turnierart' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_meldungen']['turnierart'],
'inputType' => 'select',
'sorting' => false,
'options' => gwTurnierpaarliste::$TurnierArten,
'eval' => array('mandatory'=>false, 'includeBlankOption' => true, 'tl_class' => 'w50')
),
Ähnlich wie bei den Startgruppen und Klassen lagere ich die Optionen für "Turnierart" in meine Frontendklasse aus. Gefällt mir besser so, und ich kann es "Re-usen".
PHP-Code:
'startgruppe' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_meldungen']['startgruppe'],
'inputType' => 'select',
'sorting' => true,
'flag' => 1,
'options' => gwTurnierpaarliste::$StartGruppen,
'eval' => array('mandatory'=>false, 'includeBlankOption' => true, 'tl_class' => 'w50')
),
'startklasse' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_meldungen']['startklasse'],
'inputType' => 'select',
'sorting' => false,
'options' => gwTurnierpaarliste::$StartKlassen,
'eval' => array('mandatory'=>true, 'tl_class' => 'w50')
),
'lat_std' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_meldungen']['lat_std'],
'inputType' => 'select',
'sorting' => false,
'options' => gwTurnierpaarliste::$TanzArten,
'eval' => array('mandatory'=>true, 'tl_class' => 'w50')
),
Wie zuvor, normale Dropdownfelder, deren Optionen ich in der Frontendklasse gwTurnierpaarliste ablege, um sie von verschiedenen Bereichen aus nutzen zu können.
PHP-Code:
'anzahlpaare' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_meldungen']['anzahlpaare'],
'inputType' => 'text',
'eval' => array('mandatory'=>false, 'minlength' => 1, 'maxlength'=>4, 'rgxp' => 'digit')
),
'platz_von' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_meldungen']['platz_von'],
'inputType' => 'text',
'eval' => array('mandatory'=>false, 'minlength' => 1, 'maxlength'=>4, 'rgxp' => 'digit', 'tl_class' => 'w50')
),
'platz_bis' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_meldungen']['platz_bis'],
'inputType' => 'text',
'eval' => array('mandatory'=>false, 'minlength' => 1, 'maxlength'=>4, 'rgxp' => 'digit', 'tl_class' => 'w50')
),
'bemerkung' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_gw_meldungen']['bemerkung'],
'inputType' => 'textarea',
'eval' => array('mandatory'=>false, 'cols' => 80, 'rows' => 20, 'allowHtml' => false)
),
)
);
Auch hier eigentlich "Hausmannskost": 3 Felder für Zahlen und ein Textfeld für die Bemerkung.
/system/modules/gw_turnierpaare/gwTurnierpaarliste.php (das Frontend-Modul) erweitere ich noch um die Optionenlisten für Tanzart und Turnierart:
PHP-Code:
public static $TurnierArten = array('-', 'OT','ET', 'LM', 'DM', 'EM', 'WM');
public static $TanzArten = array('-', 'Std','Lat');
Zum ersten Testen füge ich zwei Testdatensätze in die Meldungstabelle ein. Ergebnis:
https://community.contao.org/de/atta...1&d=1271095947
Gut, die Tabellenheader sehen noch nicht so aus wie gewünscht, aber das ist Feinschliff. Die gewünschten Informationen (mit pid 7) werden angezeigt.
Wenn ich einen neuen Eintrag hinzufüge, sieht das so aus:
https://community.contao.org/de/atta...1&d=1271096091
Durch das Fehlen der Language-Einträge natürlich noch sehr unschön, aber alle Felder sind da. Die Foreign-Key-Beziehung in die Turnierpaar-Tabelle klappt (rudimentär) auch.
Mich wundert der "20.12.2000" als Default im Datumsfeld, aber ich habe auch keinen Default vorgegeben. Es wäre wohl praktisch, durch einen Load-Callback das aktuelle Datum dort als Default vorzugeben.
Ich gebe also mal Daten ein:
https://community.contao.org/de/atta...1&d=1271096301
Und drücke auf Speichern- was sehe ich beim Datum:
https://community.contao.org/de/atta...1&d=1271096393
30.11.1999? Das habe ich im Datepicker aber nicht ausgewählt....auch eine manuelle Eingabe sorgt für das selbe 1999er-Ergebnis.
Und in der Übersichtstabelle dann das:
https://community.contao.org/de/atta...1&d=1271163730
0000-00-00...hm.
Das ist alles nicht so ermutigend. Ich habe die Konfiguration für das Datumsfeld bei tl_article.php abgeschaut, da funktioniert ja auch alles.
Erstmal Pause, demnächst geht es weiter.
Stefan
Liste der Anhänge anzeigen (Anzahl: 1)
Ja, ich vermute sehr stark, dass das das Problem ist. Habe es einfach intuitiv (falsch) gemacht. Leider fehlte mir die Zeit, vorher "im Core" abzugucken. Eins habe ich sowieso schon gelernt: Das Einzige, auf was man sich in Sachen "TL-Entwickler-Doku" verlassen kann, ist der Core-Source ;-).
EDIT: habe es schnell geändert und das Feld "datum" auf varchar(10) umgestellt. Jetzt klappt der Datepicker. Aber jetzt wird das Feld mit einem Unix-Timestamp gefüllt. Gebe ich das Feld also in meinen Tabellenzeilen aus oder als Sortierheader, steht dort nur Zahlenwust statt eines Datums. Ich wusste schon, warum ich lieber "date" haben wollte ;-). Ich kriege graue Haare. Da muss ich mir also definitiv noch was einfallen lassen...
https://community.contao.org/de/atta...1&d=1271100757
Stefan
Herzlichen Dank und Hilfe
Zuerst einmal herzichen Dank für dieses hervorragende Tutorial!!! So etwas habe ich mir schon sehr lange gewünscht.
Ich versuche nun, nach Deiner Vorlage eine Verwaltung für unseren Vereinsbekleidungs-Verleih zu programmieren. Ich habe den Extension-Creator, die Datenbank-Einrichtung und die DCA-Programmierung fürs Erste recht gut hinbekommen und alle kleinen Fehler ziemlich schnell lokalisieren und beheben können. Ich arbeite auf einer lokalen Installation und habe die Fehlermeldung eingeschaltet.
Wenn ich mich richtig erinnere, so kommt seitdem ich das language file "modules.php" angefasst habe, folgende Fehlermeldung:
Zitat:
Warning: Cannot modify header information - headers already sent by (output started at mein_pfad\system\modules\to_bekleidungsverleih\languages\de \modules.php:1) in mein_pfad\system\libraries\Template.php on line 174
#0 [internal function]: __error(2, 'Cannot modify h...', 'mein_pfad...', 174, Array)
#1 mein_pfad\system\libraries\Template.php(174): header('Content-Type: t...')
#2 mein_pfad\system\modules\backend\BackendTemplate.php(135): Template->output()
#3 mein_pfad\typolight\main.php(286): BackendTemplate->output()
#4 mein_pfad\typolight\main.php(102): Main->output()
#5 mein_pfad\typolight\main.php(295): Main->run()
#6 {main}
Leider bin ich nun mit meinem Latein am Ende... Weiß jemand Rat?