How to Modulentwicklung [Teil 4]

Wie definiere ich eine eigene Datenbanktabelle, designe die Admin Templates und hinterlege die Funktionalität?

Eigene Datenbanktabellen

Für unser Modul haben wir bereits ein eigenes Datenbanktabellenfeld in der Artikeltabelle angelegt.

Wir benötigen nun noch eine eigene Datenbanktabelle für die Speicherung der Fragen und Antworten und eine Verknüpfungstabelle für die Zuordnung.

 

Unsere eigenen Datenbanktabellen

  • bisquestionanswer
  • oxobject2bisquestionanswer

Damit diese Datenbankanpassungen bei Modulaktivierung vorgenommen werden – überarbeiten wir unsere Eventklasse bisQuestionAnswerEvents des Moduls.

Um sicherzustellen, dass bei Reaktivierung des Moduls keine Fehler auftreten ergänzen wir eine Prüfung ob unser Datenbanktabellenfeld bisinneedofexplanation bereits existiert und erzeugen unsere eigenen Datenbanktabellen nur wenn diese nicht bereits existieren.

<?php

class bisQuestionAnswerEvents
{

    public static function onActivate()
    {
        if( !self::_bisCheckIfDatabaseFieldExists() ) {
            self::bisAlterArticlesTable();
        }
        self::bisCreateBisQuestionAnswerTable();
        self::bisCreateBisQuestionAnswerJoinTable();
    }

    public static function bisAlterArticlesTable()
    {
        $sSql = "ALTER TABLE `oxarticles`
                  ADD `BISINNEEDOFEXPLANATION` tinyint(1) unsigned NOT NULL DEFAULT '0'
                  COMMENT 'Enable in need of explanation tab';";

        oxDb::getDb()->execute( $sSql );
    }

    public static function bisCreateBisQuestionAnswerTable()
    {
        $sSql = "CREATE TABLE IF NOT EXISTS `bisquestionanswer` (
          `OXID` char(32) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL COMMENT 'Entry id',
          `OXACTIVE` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is active',
          `BISQUESTION` text NOT NULL COMMENT 'User question',
          `BISANSWER` text NOT NULL COMMENT 'Admin answer',
          `OXTIMESTAMP` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Timestamp',
            PRIMARY KEY (`OXID`)
          ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Questions and answers entries';";

        oxDb::getDb()->execute( $sSql );
    }

    public static function bisCreateBisQuestionAnswerJoinTable()
    {
        $sSql = "CREATE TABLE IF NOT EXISTS `oxobject2bisquestionanswer` (
          `OXID` char(32) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL COMMENT 'Record id',
          `OXOBJECTID` char(32) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL DEFAULT '' COMMENT 'Article id (oxarticles)',
          `BISQUESTIONANSWER` char(32) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL DEFAULT '' COMMENT 'Bisquestionanswer id (bisquestionanswer)',
          `OXPOS` int(11) NOT NULL DEFAULT '0' COMMENT 'Sorting',
          `OXTIMESTAMP` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Timestamp',
            PRIMARY KEY (`OXID`),
            KEY `OXOBJECTID` (`OXOBJECTID`),
            KEY `BISQUESTIONANSWER` (`BISQUESTIONANSWER`)
          ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Shows many-to-many relationship between bisquestionanswer and articles';";

        oxDb::getDb()->execute( $sSql );
    }

    protected static function _bisCheckIfDatabaseFieldExists()
    {
        $blBisDatabaseFieldExists = false;

        $sTable = 'oxarticles';
        $sColumn = 'bisinneedofexplanation';
        $oDbHandler = oxNew( "oxDbMetaDataHandler" );
        $blBisDatabaseFieldExists = $oDbHandler->tableExists( $sTable ) && $oDbHandler->fieldExists( $sColumn, $sTable );

        return $blBisDatabaseFieldExists;
    }

}

Admin Templates

Ein Admin Template im Detail zu verstehen ist nicht einfach. Bei der Entwicklung orientiere ich mich an einen bestehenden Template und passe dieses an.

Es folgen die überarbeiteten Admin Views.

  • View für OXID Admin Listenansicht
    list_of_questions_list.tpl

    [{include file="headitem.tpl" title="GENERAL_ADMIN_TITLE"|oxmultilangassign box="list"}]
    [{assign var="where" value=$oView->getListFilter()}]
    
    [{if $readonly}]
        [{assign var="readonly" value="readonly disabled"}]
    [{else}]
        [{assign var="readonly" value=""}]
    [{/if}]
    
    <script type="text/javascript">
        <!--
        window.onload = function ()
        {
            top.reloadEditFrame();
            [{if $updatelist == 1}]
                top.oxid.admin.updateList('[{$oxid}]');
            [{/if}]
        }
        //-->
    </script>
    
    
    <div id="liste">
        <form name="search" id="search" action="[{ $oViewConf->getSelfLink() }]" method="post">
            [{include file="_formparams.tpl" cl="bisquestionanswer_listofquestions_list" lstrt=$lstrt actedit=$actedit oxid=$oxid fnc="" language=$actlang editlanguage=$actlang}]
            <table cellspacing="0" cellpadding="0" border="0" width="100%">
                <colgroup>
                    <col width="3%">
                    <col width="10%">
                    <col width="45%">
                    <col width="30%">
                    <col width="2%">
                </colgroup>
                <tr class="listitem">
                    <td valign="top" class="listfilter first" align="right">
                        <div class="r1">
                            <div class="b1">
                                &nbsp;
                            </div>
                        </div>
                    </td>
                    <td valign="top" class="listfilter" align="left">
                        <div class="r1">
                            <div class="b1">
                                <input class="listedit" type="text" size="9" maxlength="128" name="where[oxarticles][oxartnum]" value="[{ $where.oxarticles.oxartnum}]">
                            </div>
                        </div>
                    </td>
                    <td height="20" valign="middle" class="listfilter" nowrap>
                        <div class="r1">
                            <div class="b1">
                                <input class="listedit" type="text" size="20" maxlength="128" name="where[bisquestionanswer][bisquestion]" value="[{ $where.bisquestionanswer.bisquestion}]" [{include file="help.tpl" helpid=searchfieldoxdynamic}]>
                            </div>
                        </div>
                    </td>
                    <td valign="top" class="listfilter" colspan="2" nowrap>
                        <div class="r1">
                            <div class="b1">
                                <div class="find">
                                    <input class="listedit" type="submit" name="submitit" value="[{ oxmultilang ident="GENERAL_SEARCH" }]" onClick="Javascript:document.search.lstrt.value=0;">
                                </div>
                                <input class="listedit" type="text" size="25" maxlength="128" name="where[bisquestionanswer][bisanswer]" value="[{ $where.bisquestionanswer.bisanswer}]" [{include file="help.tpl" helpid=searchfieldoxdynamic}]>
                            </div>
                        </div>
                    </td>
                </tr>
                <tr class="listitem">
                    <td class="listheader first" height="15" width="30" align="center"><a href="Javascript:top.oxid.admin.setSorting( document.search, 'bisquestionanswer', 'oxactive', 'asc');document.search.submit();" class="listheader">[{oxmultilang ident="GENERAL_ACTIVTITLE"}]</a></td>
                    <td class="listheader"><a href="Javascript:top.oxid.admin.setSorting( document.search, 'oxarticles', 'oxartnum', 'asc');document.search.submit();" class="listheader">[{oxmultilang ident="GENERAL_ARTNUM"}]</a></td>
                    <td class="listheader" height="15">&nbsp;<a href="Javascript:top.oxid.admin.setSorting( document.search, 'bisquestionanswer', 'bisquestion', 'asc');document.search.submit();" class="listheader">[{oxmultilang ident="BIS_LIST_OF_QUESTION_LIST_QUESTION"}]</a></td>
                    <td class="listheader" colspan="2"><a href="Javascript:top.oxid.admin.setSorting( document.search, 'bisquestionanswer', 'bisanswer', 'asc');document.search.submit();" class="listheader">[{oxmultilang ident="BIS_LIST_OF_QUESTION_LIST_ANSWER"}]</a></td>
                </tr>
    
                [{assign var="blWhite" value=""}]
                [{assign var="_cnt" value=0}]
                [{foreach from=$mylist item=listitem}]
                [{assign var="_cnt" value=$_cnt+1}]
                <tr id="row.[{$_cnt}]">
                    [{if $listitem->blacklist == 1}]
                        [{assign var="listclass" value=listitem3}]
                    [{else}]
                        [{assign var="listclass" value=listitem$blWhite}]
                    [{/if}]
                    [{if $listitem->bisquestionanswer__oxid->value == $oxid}]
                        [{assign var="listclass" value=listitem4 }]
                    [{/if}]
                    <td valign="top" class="[{ $listclass}][{ if $listitem->bisquestionanswer__oxactive->value == 1}] active[{/if}]" height="15"><div class="listitemfloating">&nbsp</a></div></td>
                    <td valign="top" class="[{ $listclass}]"><div class="listitemfloating"><a href="Javascript:top.oxid.admin.editThis('[{ $listitem->bisquestionanswer__oxid->value }]');" class="[{ $listclass}]">[{ $listitem->bisquestionanswer__oxartnum->value }]</a></div></td>
                    <td valign="top" class="[{ $listclass}]" height="15"><div class="listitemfloating">&nbsp;<a href="Javascript:top.oxid.admin.editThis('[{ $listitem->bisquestionanswer__oxid->value }]');" class="[{ $listclass}]">[{ $listitem->bisquestionanswer__bisquestion->value }]</a></div></td>
                    <td valign="top" class="[{ $listclass}]"><div class="listitemfloating"><a href="Javascript:top.oxid.admin.editThis('[{ $listitem->bisquestionanswer__oxid->value }]');" class="[{ $listclass}]">[{ $listitem->bisquestionanswer__bisanswer->value }]</a></div></td>
                    <td class="[{ $listclass}]">
                        [{if !$readonly}]
                            <a href="Javascript:top.oxid.admin.deleteThis('[{ $listitem->bisquestionanswer__oxid->value }]');" class="delete" id="del.[{$_cnt}]"title="" [{include file="help.tpl" helpid=item_delete}]></a>
                        [{/if}]
                    </td>
                </tr>
                [{if $blWhite == "2"}]
                    [{assign var="blWhite" value=""}]
                [{else}]
                    [{assign var="blWhite" value="2"}]
                [{/if}]
                [{/foreach}]
    
                [{include file="pagenavisnippet.tpl" colspan="5"}]
            </table>
        </form>
    </div>
    
    [{include file="pagetabsnippet.tpl"}]
    
    <script type="text/javascript">
        if (parent.parent)
        {   parent.parent.sShopTitle   = "[{$actshopobj->oxshops__oxname->getRawValue()|oxaddslashes}]";
            parent.parent.sMenuItem    = "[{ oxmultilang ident="GENERAL_MENUITEM" }]";
            parent.parent.sMenuSubItem = "[{ oxmultilang ident="BIS_LIST_OF_QUESTION_LIST_MENUSUBITEM" }]";
            parent.parent.sWorkArea    = "[{$_act}]";
            parent.parent.setTitle();
        }
    </script>
    </body>
    </html>
  • View für OXID Admin Detailansicht
    list_of_questions_main.tpl

    [{include file="headitem.tpl" title="GENERAL_ADMIN_TITLE"|oxmultilangassign}]
    
    <script type="text/javascript">
        <!--
        function editThis( sID )
        {
            var oTransfer = top.basefrm.edit.document.getElementById( "transfer" );
            oTransfer.oxid.value = sID;
            oTransfer.cl.value = top.basefrm.list.sDefClass;
    
            //forcing edit frame to reload after submit
            top.forceReloadingEditFrame();
    
            var oSearch = top.basefrm.list.document.getElementById( "search" );
            oSearch.oxid.value = sID;
            oSearch.actedit.value = 0;
            oSearch.submit();
        }
        window.onload = function ()
        {
            [{if $updatelist == 1}]
                top.oxid.admin.updateList('[{$oxid}]');
            [{/if}]
            top.reloadEditFrame();
        }
        //-->
    </script>
    
    [{if $readonly}]
        [{assign var="readonly" value="readonly disabled"}]
    [{else}]
        [{assign var="readonly" value=""}]
    [{/if}]
    
    <form name="transfer" id="transfer" action="[{$oViewConf->getSelfLink()}]" method="post">
        [{$oViewConf->getHiddenSid()}]
        <input type="hidden" name="oxid" value="[{$oxid}]">
        <input type="hidden" name="cl" value="bisquestionanswer_listofquestions_main">
        <input type="hidden" name="editlanguage" value="[{$editlanguage}]">
    </form>
    
    <form name="myedit" id="myedit" action="[{$oViewConf->getSelfLink()}]" method="post" onSubmit="copyLongDesc( 'bisquestionanswer__bisquestion' );copyLongDesc( 'bisquestionanswer__bisanswer' );">
        [{$oViewConf->getHiddenSid()}]
        <input type="hidden" name="cl" value="bisquestionanswer_listofquestions_main">
        <input type="hidden" name="fnc" value="">
        <input type="hidden" name="oxid" value="[{$oxid}]">
        <input type="hidden" name="editval[bisquestionanswer__oxid]" value="[{$oxid}]">
        <input type="hidden" name="editval[bisquestionanswer__bisquestion]" value="">
        <input type="hidden" name="editval[bisquestionanswer__bisanswer]" value="">
    
        <table cellspacing="0" cellpadding="0" border="0" width="98%">
            <tr>
                <td valign="top" class="edittext">
                    <table cellspacing="0" cellpadding="0" border="0">
                        <tr>
                            <td class="edittext" width="120">
                                [{oxmultilang ident="ARTICLE_MAIN_ACTIVE"}]
                            </td>
                            <td class="edittext">
                                <input type="hidden" name="editval[bisquestionanswer__oxactive]" value="0">
                                <input class="edittext" type="checkbox" name="editval[bisquestionanswer__oxactive]" value='1' [{if $edit->bisquestionanswer__oxactive->value == 1}]checked[{/if}] [{$readonly}]>
                            </td>
                        </tr>
                        <tr>
                            <td class="edittext" colspan="2"><br><br>
                                <input type="submit" class="edittext" name="save" value="[{oxmultilang ident="BIS_LIST_OF_QUESTION_MAIN_SAVE"}]" onClick="Javascript:document.myedit.fnc.value='save'" [{$readonly}]>
                            </td>
                        </tr>
                    </table>
                </td>
                <!-- Anfang rechte Seite -->
                <td valign="top" class="edittext" align="left" width="40%">
                    <strong>[{oxmultilang ident="BIS_LIST_OF_QUESTION_LIST_QUESTION"}]</strong><br /><br />
                    [{$bisquestion}]
                </td>
                <td valign="top" class="edittext" align="left" width="5%">
                    &nbsp;
                </td>
                <td valign="top" class="edittext" align="left" width="40%">
                    <strong>[{oxmultilang ident="BIS_LIST_OF_QUESTION_LIST_ANSWER"}]</strong><br /><br />
                    [{$bisanswer}]
                </td>
            </tr>
        </table>
    </form>
    
    [{include file="bottomnaviitem.tpl"}]
    
    [{include file="bottomitem.tpl"}]

Ich habe 4 neue Sprachkonstanten in den Templates definiert. Die 4 Sprachkonstanten müssen in der Sprachdatei ergänzt werden.

<?php

$sLangName  = "Deutsch";
// -------------------------------
// RESOURCE IDENTIFIER = STRING
// -------------------------------
$aLang = array(
    'charset'                                       => 'ISO-8859-15',
    'bisquestionanswer'                             => 'Fragen und Antworten',
    'tbclbisquestionanswer_listofquestions_main'    => 'Stamm',
    'BIS_LIST_OF_QUESTION_LIST_MENUSUBITEM'         => 'Fragen und Antworten',
    'BIS_LIST_OF_QUESTION_LIST_QUESTION'            => 'Frage',
    'BIS_LIST_OF_QUESTION_LIST_ANSWER'              => 'Antwort',
    'BIS_LIST_OF_QUESTION_MAIN_ACTIVE'              => 'Aktiv',
    'BIS_LIST_OF_QUESTION_MAIN_SAVE'                => 'Speichern',
);

/*
[{ oxmultilang ident="GENERAL_YOUWANTTODELETE" }]
*/

Eine Detailaufschlüsselung der Template Dateien würde diesen Artikel sprengen. Zum besseren Verständnis der Admin Templates plane ich kleinere Vorlagen und Hilfestellungen zu veröffentlichen.

Funktionalität

Für die Funktionalität müssen wir ein Model anlegen – da wir eigene Datenbanktabellen verwenden.

Wir legen ein eigenes neues Model an. Im Model machen wir den Klassennamen und Datenbanktabellennamen bekannt.

<?php

class bisQuestionAnswer extends oxI18n
{
    protected $_sClassName = 'bisquestionanswer';

    public function __construct()
    {
        parent::__construct();
        $this->init( 'bisquestionanswer' );
    }
}

Nachdem wir unser Model angelegt haben, müssen wir dieses in der metadata.php bekannt machen.

<?php

$sMetadataVersion = '1.1';

$aModule = array(
    'id'           => 'bisQuestionAnswer',
    'title'        => 'Modul f&uuml;r Fragen und Antworten zum Artikel',
    'extend'       => array(),
    'files'        => array(
        // core
        'bisQuestionAnswerEvents' => 'bisQuestionAnswer/core/bisquestionanswerevents.php',
        // admin controllers
        'bisQuestionAnswer_ListofQuestions' => 'bisQuestionAnswer/application/controllers/admin/bisquestionanswer_listofquestions.php',
        'bisQuestionAnswer_ListofQuestions_List' => 'bisQuestionAnswer/application/controllers/admin/bisquestionanswer_listofquestions_list.php',
        'bisQuestionAnswer_ListofQuestions_Main' => 'bisQuestionAnswer/application/controllers/admin/bisquestionanswer_listofquestions_main.php',
        // models
        'bisQuestionAnswer' => 'bisQuestionAnswer/application/models/bisquestionanswer.php',
    ),
    'blocks'       => array(
        array(
            'template' => 'article_extend.tpl',
            'block' => 'admin_article_extend_form',
            'file' => '/views/blocks/article_extend.tpl'
        ),
    ),
    'settings'     => array(),
    'templates'    => array(
        'list_of_questions.tpl' => 'bisQuestionAnswer/views/admin/tpl/list_of_questions.tpl',
        'list_of_questions_list.tpl' => 'bisQuestionAnswer/views/admin/tpl/list_of_questions_list.tpl',
        'list_of_questions_main.tpl' => 'bisQuestionAnswer/views/admin/tpl/list_of_questions_main.tpl',
    ),
    'events'       => array(
        'onActivate'   => 'bisQuestionAnswerEvents::onActivate',
    ),
);

Im Admin Controller der Liste bisQuestionAnswer_ListofQuestions_List machen wir unser neues Model bekannt und legen eine eigene Select Abfrage an.

Die eigene Select Abfrage ist notwendig da wir gerne die Artikelnummer in unserer Listenansicht mit anzeigen möchten.

<?php

class bisQuestionAnswer_ListofQuestions_List extends oxAdminList
{
    protected $_sListClass = 'bisquestionanswer';

    protected $_sThisTemplate = 'list_of_questions_list.tpl';

    protected function _buildSelectString( $oListObject = null )
    {
        $sSql = parent::_buildSelectString($oListObject);

        $sQuestionAnswer = getViewName( "bisquestionanswer" );
        $sArticle2QuestionAnswer = getViewName( "oxobject2bisquestionanswer" );
        $sArticles = getViewName( "oxarticles" );
        $sSql  = "
          SELECT {$sQuestionAnswer}.*, {$sArticles}.oxartnum FROM {$sQuestionAnswer}
            LEFT JOIN {$sArticle2QuestionAnswer} ON {$sArticle2QuestionAnswer}.BISQUESTIONANSWER = {$sQuestionAnswer}.OXID
            LEFT JOIN {$sArticles} ON {$sArticles}.oxid = {$sArticle2QuestionAnswer}.OXOBJECTID
            WHERE 1
        ";

        return $sSql;
    }

}

Damit in unserem Stamm Tab die Daten des ausgewählten Listeneintrages erscheinen und bearbeitbar sind müssen wir unser Controller für OXID Admin Detailansicht anpassen.

<?php

class bisQuestionAnswer_ListofQuestions_Main extends oxAdminDetails
{
    public function render()
    {
        parent::render();

        $oQuestionAnswer = oxNew('bisquestionanswer');
        $this->_aViewData['edit'] = $oQuestionAnswer;

        $sOxId = $this->getEditObjectId();
        if (  $sOxId && $sOxId != "-1") {
            $oQuestionAnswer->load($sOxId);
        }

        // textareas
        $this->_aViewData["bisquestion"] = $this->_generateTextEditor( "100%", 100, $oQuestionAnswer, "bisquestionanswer__bisquestion", "details.tpl.css");
        $this->_aViewData["bisanswer"] = $this->_generateTextEditor( "100%", 100, $oQuestionAnswer, "bisquestionanswer__bisanswer", "details.tpl.css");

        return 'list_of_questions_main.tpl';
    }

    public function save()
    {
        parent::save();

        $aParams = oxConfig::getParameter( "editval");

        $oQuestionAnswer = oxNew('bisquestionanswer');
        $oQuestionAnswer->assign($aParams);
        $oQuestionAnswer->save();

        // set oxid if inserted
        $this->setEditObjectId( $oQuestionAnswer->getId() );
    }

}

Fortsetzung

Im 5. und letzten Teil der Modulentwicklung geht es weiter mit:
Wie integriere ich meinen neuen Tab “Fragen & Antworten” auf der Produktdetailseite im Frontend?

Key Learnings

  • Eigene Datenbanktabellen angelegt
    • Fragen und Antworten Tabelle
    • Verknüpfungstabelle
  • Groben Aufbau der Admin Listen- und Detailansicht kennen gelernt
  • Zusammenspiel der Admin Listen- und Detailansicht
  • Eigenes Model definieren
  • Model Verwendung innerhalb der Listen- und Detailcontroller

 

 



2 Kommentare
  1. KristianH sagte:

    In dem Template list_of_questions_list.tpl ist folgender Code:

    if (parent.parent)
    { parent.parent.sShopTitle = "[{$actshopobj->oxshops__oxname->getRawValue()|oxaddslashes}]";
    parent.parent.sMenuItem = "[{ oxmultilang ident="GENERAL_MENUITEM" }]";
    parent.parent.sMenuSubItem = "[{ oxmultilang ident="BIS_LIST_OF_QUESTION_LIST_MENUSUBITEM" }]";
    parent.parent.sWorkArea = "[{$_act}]";
    parent.parent.setTitle();
    }


    Dies ist eine Oxid Kopie und sollte nicht verwendet werden, eher so:

    [{capture name="emptyCapture"}]

    [{capture name="d3JavaScript"}]
    if (parent.parent) {
    parent.parent.sShopTitle = "[{$actshopobj->oxshops__oxname->getRawValue()|oxaddslashes}]";
    parent.parent.sMenuItem = "[{ oxmultilang ident="GENERAL_MENUITEM" }]";
    parent.parent.sMenuSubItem = "[{ oxmultilang ident="BIS_LIST_OF_QUESTION_LIST_MENUSUBITEM" }]";
    parent.parent.sWorkArea = "[{$_act}]";
    parent.parent.setTitle()
    }
    [{/capture}]

    [{/capture}]
    [{oxscript add=$smarty.capture.d3JavaScript}]
    [{include file="bottomitem.tpl"}]

    In der Klasse bisQuestionAnswer_ListofQuestions_Main Methode render() Bitte immer das parent::render() zurück geben.
    bspw:

    $sReturn = parent::render();
    // do some stuff
    return $sReturn

    Antworten
  2. indianer3c sagte:

    Hallo Kristian,

    vielen Dank für dein Feebback. Dies freut mich sehr!

    Kannst du bitte deine 2 Verbesserungsvorschläge auch begründen?

    Mich würde dies sehr interessieren. Weil damit die Softwarequalität von OXID Modulen im Allgemeinen erhöht wird, wenn man sich über Best Practices austauscht 🙂

    Antworten

Dein Kommentar

An Diskussion beteiligen?
Hinterlasse uns Deinen Kommentar!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.