Vývoj modulů pro CMS systém Drupal 6.x (4. díl)

17. prosince 2009

V tomto čtvrtém a (zatím) posledním pokračování seriálu o vývoji modul pro publikační systém Drupal dokončíme náš první modul. V této části článku budeme implementovat soubor hlasovani.modul.

Implementace souboru hlasovani.modul

Nyní se pustíme do nejobsáhlejšího souboru, který bude již obsahovat samotnou logiku modulu. Jedná se o soubor hlasovani.modul. Začneme se základními hooky, které by měly být součástí snad každého modulu:

Jako první implementujeme hook_perm. Hook určuje, jaké oprávnění musí mít daný uživatel (přesněji role, která je danému uživateli přidělena), aby mohl pracovat s daným modulem. V administraci potom můžete přidělovat/odebírat tyto oprávnění jednotlivým uživatelským rolím. My budeme požadovat oprávnění s názvem hlasovani. Po instalaci a aktivaci modulu se toto oprávnění objeví jako volitelné u všech uživatelských rolí (viz administrace Drupalu po tom, co aktivujete/instalujete náš modul).

V administraci můžeme uživatelským rolím přidat práva, které definujeme ve svém modulu.

/**
  * Implementace hook_perm
  * @return array se opravneni
  ;*/
function hlasovani_perm() {
  return array('hlasovani');
}

Obecněji může být vráceno více oprávnění, které mají k modulu přístup. Ilustračně:

return array('access onthisdate content', 'administer onthisdate');

My si však v tomto modulu vystačíme s jedním.

V implementaci budeme pokračovat hookem hook_menu, v němž jsou obecně definovány callbacky a prvky menu. My budeme definovat pouze jeden callback a žádný prvek menu.

Definice callbacku je opět ve formě pole (přesněji vnořených polí), jak jsme se s ním již seznámili ve Scheme API. S tímto způsobem definování různých prvků systému je třeba si zvyknout, protože je v Drupalu (a v PHP) obvyklý.

Asociativní pole může obsahovat položky, které jsou vypsány v referenci k hooku hook_menu. O nejdůležitějších položkách se zmíníme:

  • title – název prvku menu
  • description – popis prvku
  • page callback – název funkce, která bude obsluhovat danou URL
  • access arguments – pole se seznamem oprávnění, kteří mají k danému callbacku/prvku menu přístup
  • type – viz dále

Položka type může nabývat několika možných hodnot. Zmíníme se o dvou nejdůležitějších (nejčastějších):

  • MENU_NORMAL_ITEM – bude se zobrazovat jako prvek v menu, kliknutí na prvek tohoto menu obslouží funkce uvedená v definici prvku
  • MENU_CALLBACK – nebude se zobrazovat v menu, pouze se registruje URL a funkce, která toto URL obslouží

Vraťme se k našemu kódu. Budeme chtít zaregistrovat URL hlasovani/zahlasuj s funkcí, kterou nazveme hlasovani_zahlasuj. Název URL se uvádí jako klíč, se kterým jsou spojeny položky pole:

/**
* Implementace hook_menu.
*
*  @return
*    struktura (array) s definici prvku menu (v nasem
*    pripade nedefinujeme ani jeden prvek menu) a callbacku
*/
function hlasovani_menu(){

  $items['hlasovani/zahlasuj'] = array(
    'page callback' => 'hlasovani_zahlasuj',
    'type' => MENU_CALLBACK,
    'title' => 'Zpracovani hlasu',
    'access arguments' => array('hlasovani'),
  );
  return $items;
}

Nyní implementujeme funkci, kterou voláme v callbacku. Připomenu, který prvek pole spojuje callback s názvem:

'page callback' => 'hlasovani_zahlasuj',

Tato funkce bude obsluhovat volání URL vase_site/hlasovani/zahlasuj. Funkce má argument $nid. Kde se tento argument vezme? Vždyť jsme naznačovali, že funkce je volána automaticky! Jak je tedy tento parametr předán? Parametr je předán automaticky jako další část URL: vase_site/hlasovani/zahlasuj/hodnota_promenne. Tento princip funguje v Drupalu vždy, když se jedná o callback funkce.

Vlastní funkce hlasovani_zahlasuj:

/**
 * Implementace callbacku hlasovani/zahlasuj.
 *
 * @param $nid
 *   id nodu, pro ktery je hlasovano
 */
function hlasovani_zahlasuj($nid){
  //dostan objekt do lokalniho scopu
  global $user;
  
  //autori nemohou hlasovat pro svuj vlastni clanek
  $je_autorem = db_result(db_query('SELECT uid FROM {node} WHERE nid = %d 
    AND uid = %d', $nid, $user->uid));
  
  //muze dany uzivatel skutecne pro tento clanek hlasova?
  if ($user->uid && ($nid > 0) && !$je_autorem) {
    $hlasoval = hlasovani_hlasoval($nid, $user->uid);
    if(!$hlasoval){
      hlasovani_hlasuj($nid, $user->uid);
    }
  }
  
  //presmeruj se na adresu z niz jsme prisli
  drupal_goto("node/".$nid);
}

Funkce je hodně jednoduchá. Na prvním řádku dostaneme do lokálního scopu objekt user (který se vytvoří během zaváděcího procesu Drupalu), který nese informace o uživateli, který zaslal požadavek (který si právě prohlíží stránku). Na tomto objektu nás momentálně zajímá hodnota user->uid, což je id uživatele. Na dalším řádku jednoduchým SQL dotazem zjišťujeme, zda daný uživatel je autorem článku (nodu).

//autori nemohou hlasovat pro svuj vlastni clanek
$je_autorem = db_result(db_query('SELECT uid FROM {node} WHERE nid = %d 
  AND uid = %d', $nid, $user->uid));

Následuje struktura if, která rozhodne, zda daný uživatel může nodu přidat hlas. Podmínkou je, že není autorem a ješťe nehlasoval. O přidání hlasu se stará funkce hlasovani_hlasuj (napíšeme ji vzápětí).

Poslední příkaz drupal_goto se stará o přesměrování zpět na node, ze kterého hlasující uživatel přišel. Tento způsob řešení: tj. přechod na jinou URL a znovu přesměrování již dávno není v Drupalu obvyklý. Používá se AJAX (s knihovnou JQuery), který dovoluje tyto věci řešit mnohem elegantněji: bez přenačítání stránky.

Připomínám, že funkce je volána pouze ve chvíli, kdy váš systém dostane URL vase_site/hlasovani/zahlasuj/cislo_nodu. Jak se volání této URL má k hlasování? Za pár okamžiků napíšeme kód, který bude generovat HTML s odkazem na právě tuto URL, přičemž cislo_nodu bude skutečně nahrazeno číselnou hodnotu, která odpovídá id (nid) daného nodu. Klikem na odkaz dojde k přechodu na tuto URL a tím k vykonání funkce hlasovani_hlasuj a tedy i zahlasování a přesměrování zpět.

Ve funkci voláme funkce hlasovani_hlasuj a hlasovani_hlasoval. Od funkce hlasovani_hlasuj očekáváme, že provede uložení potřebných dat do databáze:

/**
* Zahlasuj pro dany clanek - uloz jeden sloupec do tabulky.
*
* @param $nid
*   nid nodu
* @param $uid
*   uid uzivatele
*/
function hlasovani_hlasuj($nid, $uid){
  db_query('INSERT INTO {hlasovani} (nid, uid) VALUES (%d, %d)', $nid, $uid);
}

Funkce hlasovani_hlasoval bude vracet, zda daný uživatel již pro článek hlasoval:

/**
 * Hlasoval uzivatel s danym uid pro nod s danym nid?
 *
 * @param $nid
 *   id nodu
 * @param $uid
 *   id uzivatele
 * @return
 *   pocet hlasu pro dany clanek od daneho uzivatele 
 *   (pretypovavame na boolean)    
 */
function hlasovani_hlasoval($nid, $uid){
  return (bool) db_result(db_query('SELECT COUNT(*) FROM {hlasovani} 
    WHERE nid = %d AND uid = %d', $nid, $uid));
}

Jak připojíme výstup modulu (tj. HTML kód s počtem hlasů pro článek a s odkazem, kterým lze zahlasovat) na konec každého nodu? Implementujeme hook_nodeapi.

V tomto hooku můžeme modifikovat obsahu nodu, který je právě sestavován, aby byl zobrazen uživateli. Hook je však volán kdykoli se s nějakým nodem děje něco zajímavého, například ve chvíli, kdy je mazán, editován apod. V hooku budeme chtít obsloužit dvě události: příprava nodu pro zobrazení a mazání nodu. V případě přípravy pro zobrazení budeme chtít „přilepit“ hlasovací prvek. V případě mazání nodu budeme i my chtít smazat k němu relevantní data v tabulce hlasovani.

Node je zde reprezentován jako objekt, který je předáván referencí. Druhým argumentem funkce je op, který říka, jaká událost s hookem nastává. Význam dalších dvou argumentů hooku je popsán v komentáři:

/**
 * Implementace hook_nodeapi().
 * Modifikuje obsahu nodu, ktery je predan parametrem. 
 *
 * @param &$node
 *   referenci je predan node, ktereho se udalost tyka
 * @param $op
 *   informace o tom, co se vlastne deje: zda je clanek mazan, 
 *   prohlizen apod. Mozne hodnoty viz http://api.drupal.org/api/function/hook_nodeapi/6
 * @param $teaser
 *   informace, zda se jedna o renderovani teaseru
 * @param $page
 *   informace o tom, zda se jedna o renderovani cele stranky
 */
function hlasovani_nodeapi(&$node, $op, $teaser, $page) { 
  switch($op){
    case 'view':
    //zobraz hlasovani
    if(!$teaser){
      $node->content['hlasovani'] = array(
      '#value' => hlasovani_renderer($node->nid),
      '#weight' => 90);
    }
    break;
  	
    case 'delete':
      db_query('DELETE FROM {hlasovani} WHERE nid = %d', $node->nid);
    break;
  }
} 

Podle hodnoty parametru op tedy buď k obsahu nodu ($node->content) připojíme nový prvek, jehož hodnota je dána návratovou hodnotu funkce hlasovani_renderer, nebo vymažeme z tabulky hlasovani všechny řádky s daným nid.

Pokračujeme funkcí hlasovani_renderer. Ta bude již vracet HTML kód, který budeme chtít na konec nodu přilepit.

Funkce hlasovani_renderer:

/**
 * Vytvor HTML kod, ktery se bude zobrazovat pod nodem
 * jako hlasovaci prvek.
 *
 * @return
 *   HTML kód, který se zobrazí na konci nodu
 */
function hlasovani_renderer($nid) {
  //nahraj CSS soubor
  drupal_add_css(drupal_get_path('module', 'hlasovani') .'/hlasovani.css');
  	
  //ziskej objekt user do lokalniho scope
  global $user;
  	
  //ziskej skore
  $pocethlasu = hlasovani_pocethlasu($nid);
  //je aktualni ctenar autorem clanku?
  $je_autor = db_result(db_query('SELECT uid FROM {node} WHERE nid = %d 
     AND uid = %d', $nid, $user->uid));
  //hlasoval jiz pro dany clanek?								 
  $hlasoval = hlasovani_hlasoval($nid, $user->uid);
  //vrat HTML kod hlasovaciho prvku
  return hlasovani_output($nid, $je_autor, $pocethlasu, $hlasoval);
}

Funkce drupal_add_css přidává referenci na soubor se styly, který je v adresáři s ostatními soubory modulu. Funkce drupal_get_path vrací cestu k danému modulu.

Jako návratová hodnota je předána návratová hodnota funkce hlasovani_output. Ta již na základě vstupních parametrů generuje HTML kód, který se pod nodem zobrazí. Podle toho, zda uživatel je autorem nodu a hlasoval či nehlasoval, se zobrazí buď link na callback (který automaticky volá funkci hlasovani_zahlasuj, která se postará o započtení hlasu), který jsme vytvořili, nebo text: „Již jste hlasoval.“ případně „Nelze hlasovat pro vlastní článek.“

Budeme pokračovat funkcí hlasovani_output:

/**
 * Vytvor HTML kod, samotneho hlasovaciho prvku.
 *
 * @param $nid
 *   nid nodu
 * @param $je_autor
 *   je uzivatel s danym uid autorem clanku?
 * @param $pocethlasu
 *   kolik hlasu dany clanek ziskal?
 * @param $hlasoval
 *   hlasoval jiz dany uzivatel pro clanek?
 */
function hlasovani_output($nid, $je_autor, $pocethlasu, $hlasoval){	
  $output = '<div class="hlasovani">';
  $output .= '<div class="pocethlasu">';
  $output .= $pocethlasu; 
  $output .= '</div>';
  $output .= '<div class="hlas">';	
  	
    if(!$hlasoval && !$je_autor){ 
      //hlasovat muze jen ten, kdo neni autor a kdo
      //jeste nehlasoval
      $output .= l("Hlasuj", "hlasovani/zahlasuj/".$nid);
    }
    elseif ($hlasoval) {
      //jiz hlasoval, vypis pouze hlasku
      $output .= 'Již jste hlasoval.';
    }
    elseif ($je_autor) { 
      //nelze hlasovat pro vlasnit clanek - vypis
      //pouze hlasku
    $output .= 'Nelze hlasovat pro vlastní článek';
  }
  
  $output .= '</div>';
  $output .= '</div>';
  
  // vrat HTML kod
  return $output;
}

Poslední funkcí, kterou musíme implementovat je funkce hlasovani_pocethlasu, která počítá počet hlasů pro daný článek. Obsahuje pouze jeden SQL dotaz, jehož výsledek je vrácen jako návratová hodnota:

/**
 * Spocti hlasy pro dany clanek.
 *
 * @param $nid
 *   nid nodu
 * @return
 *   pocet hlasu pro dany clanek
 */
function hlasovani_pocethlasu($nid){
  return (int) db_result(db_query('SELECT COUNT(*) FROM {hlasovani} 
    WHERE nid = %d', $nid));
}

Implementace souboru hlasovani.css

Zbývá soubor hlasovani.css. CSS kód nechávám na vaší představivosti, zde uvádím pouze návrh, jak by mohl soubor vypadat.

div.hlasovani {
  width: 150px;
  text-align: center;
  margin-bottom: 5px;
}

div.hlasovani .hlas {
  padding: 3px 5px;
  border: 1px solid white;
  margin-top: 2px;
  background-color: white;
}

div.hlasovani .pocethlasu{
  font-size: 175%;
  padding: 10px;
  border: 1px solid black;
  background-color: white;
}

Zbývá připomenout, jak je soubor hlasovani.css připojen k našemu modulu. O to se stará funkce. Viz řádek ve funkci hlasovani_renderer:

drupal_add_css(drupal_get_path('module', 'hlasovani') .'/hlasovani.css');

Nyní zbývá dát vše dohromady a pochopit kód v celku. Ale to je již práce pro vás. Přeji hodně štěstí.

Závěr

V tomto seriálu jsme začali obecným popisem Drupalu a seznámili se s jeho „vnitřním chodem“ a základní terminologií, jejž znalost je nezbytná při studiu reference. Napsali jsme společně relativně jednoduchý, ale použitelný modul, který umožní uživatelům s daným právem hlasovat pro nody. Seznámili jsme se se základními hooky (hook_perm, hook_menu), které je třeba implementovat prakticky v každém modulu, a máme přehled o tom, jaké hooky existují (a jaké události lze tedy ovlivnit). Seznámili jsme se s funkcemi drupal_add_css a drupal_goto, jejichž použití je při vývoji modulů časté. Získali jsme přehled o databázové vrstvě (včetně základních funkcí a jejich použití) a Scheme API.

Dokončili jsme první modul! Po instalaci by měl spolehlivě fungovat. Lze mu vytknout mnoho vlastností. Nejzávažnějším nedostatkem je nutnost načtení celé stránky po hlasování. Tuto nemilou vlastnost lze snadno odstranit pomocí AJAXu a jQuery, a tím se budeme zabývat v další sérii článků o Drupalu.

Štítky: Články

Mohlo by vás také zajímat

Nejnovější

9 komentářů

  1. Jakub Suchy

    Pro 17, 2009 v 12:14

    Dobry den,

    jak jsem psal v komentari k minulemu clanku, primlouval bych se za Coding standards. Pokud prijmu vas nazor, ze cestina neni proti coding standards, tento zapis komentaru vsak proti nim je.

    Dale absolutne nesouhlasim s pouzitim funkce hlasovani_output(). Musim rict, ze nevim, kde jste ji nasli, nicmene takto se to nedela a primlouval bych se za opravu v clanku, protoze to je VELMI nebezpecny precedens. Spravne by mela existovat theme funkce, ktera HTML kod vrati. Pokud je to jinak, je to obrovskym zpusobem spatne.

    Spravne tedy:

    function hlasovani_theme() {
    return array(
    ‚hlasovani_output‘ => array(
    ‚arguments‘ => array(‚nid‘, ‚je_autor‘, ‚pocethlasu‘, ‚hlasoval‘),
    ),
    }

    funkci hlasovani_output prejmenovat na theme_hlasovani_output.

    ve funkci renderer pak misto hlasovani_output zavolat:

    return theme(‚hlasovani_output‘, $nid, $je_autor, $pocethlasu, $hlasoval);

    Pokud by bylo mozne v poslednim dile zminit neco o bezpecnosti a vystupu, budu rad. Pokud si uzivatele takove veci neprectou, jsem si JIST, ze v jejich obdobach (spatnych) funkci hlasovani_output bude samy Cross Site Scripting

    Odpovědět
  2. Jakub Suchy

    Pro 17, 2009 v 12:15

    (V mem komentari odsazovani bylo, ale formatovani intervalu to asi neprijalo :-)

    Odpovědět
  3. Marek Sotak

    Pro 17, 2009 v 12:20

    Predstava, ze tento modul pouziju na dvou webech a kazdy by mel mit odlisny vystup, budu se uz muset starat o dve odlisne verze.
    Pouziti hook_theme by zde bylo vice nez na miste, takze by se tento vystup dal ovlivnit pomoci sablony, jak pise Jakub, tim padem pokud tento modul vyuziju na dalsim webu, zmenim vystup v sablone. Kodu v modulu bych se ani nedotkl -> stejna verze na vsech webech.

    Odpovědět
  4. Jakub Suchy

    Pro 17, 2009 v 12:41

    Dalsi chybou je v hook_menu nepouziti %node v samotnem menu item. Spravne:

    $items[‚hlasovani/zahlasuj/%node‘]

    Protoze Drupal pote automaticky pozna, ze v argumentu musi byt NID, tedy cislo a zajisti vse potrebne. Tak jak je to nyni by bylo mozne do argumentu dat cokoliv, modul by to „sezral“ a nekde dole by nastala chyba, protoze node s cislem „pokus“ asi neexistuje (Resp PHP by to asi pretypovalo na nulu).

    Odpovědět
  5. Marek Sotak

    Pro 17, 2009 v 12:44

    Nekdo by mel vzdycky projet ten kod, co se tyce coding standards. Doporucil bych modul coder (http://drupal.org/project/coder), ktery dokaze kod projet a najit coding standards a klasicke chyby.

    drupal_goto(„node/“.MEZERA$nid);
    v _nodeapi spatne odsazeni v case
    _output -> $output .= l(„Hlasuj“, „hlasovani/zahlasuj/“.MEZERA$nid); + spatne odsazeni

    Jinak nechapu michani cesko-anglickych slov, pochopil bych, kdyby jste pouzival bud jen cestinu nebo anglictinu, ale veci typu:
    //dostan objekt do lokalniho scopu
    function hlasovani_output, hlasovani_renderer, atd… nebylo by uz rovnou lepsi to psat vse v anglictine?

    Odpovědět
  6. Jan Sova

    Pro 17, 2009 v 16:21

    Dekuji vam za vase komentare a nepresnosti/chyby v clanku uvedu na pravou miru.

    Zamenenim hlasovani_theme jsem se dopustil zjednoduseni, ktere bylo z „pedagogickeho“ hlediska nepresne.

    Dekuji vam za zpetnou vazbu.

    Honza Sova

    Odpovědět
  7. Martin

    Pro 19, 2009 v 16:43

    Zdravím,
    prosím Vás, bojuji s vytvářením vlastního modulu a vyskytl se mi následující problém:
    v administraci webu na „hlavní stránce modulu“ (/admin/settings/prihlas) vypisuji určitou tabulku z databáze a ke každému záznamu přidávám odkaz na zobrazení dalších detailů daného záznamu, které by se měly zobrazit na stránce zanořené (admin/settings/prihlas/detail/1). Výpis tabulky na té hlavní straně je v pohodě, dokonce se mi tam správně zobrazí odkazy na ten detail (za posledním / je ID toho záznamu), ale jakmile na odkaz kliknu, adresa se sice změní, ale obsah stránky se načte znovu ten z hlavní strany…
    Pro zobrazení používám drupal_get_form a vše to cpu do formulářů. Četl jsem v dokumentaci API, že ta funkce drupal_get_form buď načte poslední načtenou stránku, nebo nově předávanou, ale jsem si téměř jist, že mu předávám správně tu novou…
    Nevíte, čím to může být způsobeno, případně jak to vyřešit? Už nevím jak na to :(
    Moc děkuji za pomoc,
    Martin

    Odpovědět
  8. Martin

    Pro 19, 2009 v 23:51

    Tak už jsem na to došel… :)

    Odpovědět
  9. Anonym

    Bře 18, 2010 v 13:55

    spravil som vsetko ako to bolo napisane a ked chcem hlasovat za clanok a kliknem na link hlasuj, tak mi vypise, ze nemam opravnenie na pristup k tej stranke. Neviete kde by mohol byt problem?

    Odpovědět

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *