ห้องทดลอง Drupal

ศึกษาและทดลอง Drupal ในห้องนี้ครับ

drupal: ติดตั้งมอดูล และการแก้ปัญหา

บันทึกการติดตั้งมอดูล และการแก้ปัญหา

Topic: 

drupal5: สร้างเอกสารภายในและภายนอก

ถ้าเราต้องการให้ผู้อ่านมีสิทธิ์ที่ไม่เท่ากัน โดยให้กำหนดด้วย role ในการเข้าถึงเอกสารใน node ต่าง ๆ สามารถทำได้โดยใช้มอดูล Taxonomy Access Control Lite ดังนี้

  1. ติดตั้งมอดูล Taxonomy Access Control Lite
    $ cd /var/www/drupal/sites/all/modules
    $ wget http://ftp.drupal.org/files/projects/tac_lite-5.x-1.1.tar.gz
    $ tar xfz tac_lite-5.x-1.1.tar.gz
  2. เปิดใช้งานผ่าน URL:admin/build/modules
  3. สร้าง Vocaburary ใหม่ ด้วย Taxonomy ผ่าน URL:admin/content/taxonomy
    โดยเลือกแท็บ Add vocaburaly ตั้งชื่อว่า Privacy
    สร้าง terms ชื่อว่า Public และ Private ตามลำดับ โดยอย่าให้เป็นฟรีแท็กส์
  4. เปิดให้ admin สามารถใช้งานมอดูลได้ ผ่าน URL:admin/user/access
  5. เมื่อสร้างเสร็จจะเกิดเมนูย่อยชื่อ Access control by taxonomy ให้เลือก Vocaburary ว่า Privacy
  6. เลือกแท็บ Role based privileges และเล็อกกำหนดสิทธิ์ให้แต่ละ role ได้ตามต้องการ เช่น anonymous เป็น Public และ authenticated เป็นทั้ง Public และ Private เป็นต้น

เสร็จแล้ว หลังจากนี้ ในขั้นตอนของการสร้างเอกสาร จะมีที่เลือก Privacy ในหัวข้อ Categories ให้เราเลือกว่าจะให้เป็น Public หรือ Private

อ้างอิง - Content Type access permisions

หมายเหตุ

ถ้าใช้ Drupal-5.5 และ Drupal-5.6 และใช้ฐานข้อมูล Postgresql อาจมีข้อผิดพลาดเกิดขึ้น ต้องปรับแก้ Core ที่ไฟล์ includes/database.pgsql.inc ดังนี้
$ vi includes/database.pgsql.inc

...
function db_distinct_field($table, $field, $query) {
    if (!preg_match('/FROM\s+\S+\s+AS/si', $query)
      && !preg_match('/DISTINCT\s+ON\s*\(\s*('. $table .'\s*\.\s*)?'. $field .'\s*\)/si', $query)
      && preg_match('/(.*FROM\s+)(.*?\s)(\s*(WHERE|GROUP|HAVING|ORDER|LIMIT|FOR).*)/Asi', $query, $m)) {
        $query = $m[1];
        $query .= preg_replace('/([\{\w+\}]+)\s+(' . $table . ')\s/Usi', '(SELECT DISTINCT ON (' . $field . ') * FROM \1) \2 ', $m[2]);
        $query .= $m[3];
    }

    return $query;
}
...

อ้างอิง - Nodes are sorted incorrectly with PostgreSQL

Topic: 

drupal6: ทดลองใช้งานมอดูล geshifilter

ขั้นตอนตั้งแต่ติดตั้งคือ
  1. ดาวน์โหลดมอดูล GeSHi Filter
    สำหรับรุ่น 6-rc2 คือ [code language="bash"]$ cd sites/all/modules
    $ wget http://ftp.drupal.org/files/projects/geshifilter-6.x-1.0-rc2.tar.gz
    $ tar cfz geshifilter-6.x-1.0-rc2.tar.gz[/code]
  2. ดาวน์โหลดไลบรารี GeSHi (Generic Syntax Highlighter) มาไว้ในไดเรกทอรี่ geshi
    [code language="bash"]$ cd geshifilter/geshi
    $ wget http://nchc.dl.sourceforge.net/sourceforge/geshi/GeSHi-1.0.7.20.tar.bz2
    $ tar cfj GeSHi-1.0.7.20.tar.bz2[/code]
  3. เปิดใช้มอดูลจาก URL: admin/builds/modules
  4. เปิดการอนุญาตจาก URL: admin/user/permissions
  5. ตั้งค่าการใช้งาน URL: admin/settings/geshifilter
    เพื่อให้กระทบต่อการแสดงผลของธีมให้น้อยที่สุด เลือกเปลี่ยนแปลงสามตัวคือ
    • Generic syntax highlighting tags: เพิ่มแท็ก <pre>
    • Tag style: เลือก [foo] (ลดข้อจำกัดการแสดงผล HTML)
    • Code container: เลือกใช้ Use <div> container (enables automatic line wrapping)
    ส่วนภาษา สามารถเพิ่มลดได้จากแท็บ Languages
  6. ตรงนี้สำคัญนิดนึง คือ Input Format ต้องเข้าไปเปิดให้ใช้งาน GeSHi ด้วย จาก URL: admin/settings/filters จะใช้ตัวไหนบ้าง ก็ต้องเข้าไปเปิดในทุก ๆ ตัว
  7. เวลาสร้างเนื้อหา จะใช้รูปแบบคือ
    [pre language="php"] ...CONTENT...
    ...CODE...
    
    [/pre] ตัวจริงให้เปลี่ยนจาก < เป็น [
  8. เสร็จแล้ว วิธีนี้น่าจะกระทบต่อธีมน้อยที่สุด ทดลองใช้งานดูสักระยะนึง อ้างอิง
Topic: 

drupal6: ปรับปรุงการใช้งานมอดูล geshifilter

โพสต์นี้ อย่าเชื่อมาก เป็นแค่การทดลองเท่านั้น พบว่ามอดูล geshifilter เขาแปลงรหัส entity ให้โดยอัตโนมัติ
ทำให้การโพสต์เนื้อความ ไม่ยืดหยุ่นเท่าที่ควร
เที่ยวนี้เราจะตั้งค่าใหม่ ให้การใช้งานมีความยืดหยุ่นสูงสุด
  • ตั้งค่าการใช้งาน geshifilter ผ่าน URL: admin/settings/geshifilter
    • Generic syntax highlighting tags: เพิ่มแท็ก pre
    • Code container: เลือก Use <div> container
  • แก้มอดูลเล็กน้อย เพื่อยกเลิกการแปลง entities อัตโนมัติ
    $ vi sites/all/modules/geshifilter/geshifilter.pages.inc เพิ่มการ decode สองครั้ง (โดยเพิ่มบรรทัดที่เป็นตัวหนา)
    :129
    ...
      $source_code = decode_entities($source_code);
      $source_code = decode_entities($source_code);
    ...
    
เสร็จแล้ว เสร็จแล้ว แต่ยังงง ๆ กับผลลัพธ์อยู่ รอดูอีกสักพัก เริ่มเข้าใจแล้ว
แท็กที่เราจะทำพิเศษเพิ่มในบล๊อกของโค๊ด ไม่สามารถทำได้ เพราะ GeSHi จะแปลงเป็น entities ทั้งหมด
ต้องหาทางอื่นในการเน้นประโยคแทน เช่นทำบรรทัดคอมเมนต์เป็นต้น
เช่น
...
  $source_code = decode_entities($source_code);
  $source_code = decode_entities($source_code);  //ADD THIS LINE
...
ตัวอย่างข้างต้น ต้องกรอกในฟอร์มว่า
<div class="geshifilter">
<pre language="drupal6">
...
  $source_code = decode_entities($source_code);
  $source_code = decode_entities($source_code);  //ADD THIS LINE
...
</pre>
</div>
มอดูลนี้ใหญ่และซับซ้อนมาก แฮ็กไม่ไหว เอาเท่าที่ได้แล้วกันครับ update
ลองใช้งาน geshifilter แล้วมันทำให้โค๊ดในงานเก่าดูเพี้ยนไปหมด ทางแก้คือสร้าง Input format ใหม่ ผ่าน admin/settings/filters
ตั้งให้สามารถใช้งาน GeSHi ได้ (โดยยกเลิกการใช้งาน GeSHi กับ Input Format อันเก่า เพื่อให้ไม่ต้องตามไปแก้งานเก่า)
ตั้งชื่อว่า GeSHi Filtered HTML
สำหรับงานใหม่ที่จะต้องการใช้ Syntax Highlight ก็ค่อยมาเลือกใช้ Input format แบบนี้
Topic: 

drupal: ทำบล๊อกจากมอดูล tracker

ต้องแฮกเล็กน้อย คือ เอาเนื้อความจาก Add a "Recent posts" block to the tracker module
ไปต่อท้ายไฟล์ modules/tracker/tracker.module

แล้วจะสามารถใช้มอดูล tracker แสดง Recent posts เป็นบล๊อกได้เลย
(แจ๋ว เพราะเขียน Recent Week เอง มีปัญหากับ Drupal-6)

Topic: 

drupal: ลองติดตั้งโมดูล Import HTML

จากคราวก่อนที่ทดลองใส่โค๊ดตัวอย่างลงในเอกสารต้นฉบับเรื่อง python: Adodb Function Reference with example รู้สึกว่าอ่านง่ายดี คล้ายเอกสารของ php ที่เขามีตัวอย่างประกอบตลอด
เลยคิดว่า ถ้าเราสามารถเอาเอกสาร HTML ต้นฉบับ ที่เป็นคู่มือไพธอน เอามาใส่ตัวอย่างเล็ก ๆ น้อย ๆ ด้วยก็น่าจะดี
ลองศึกษา Drupal ดู พบว่ามีโมดูลนึงที่เข้าข่าย คือโมดูล Import HTML

โมดูลนี้ยังไม่สามารถใช้กับ Drupal-5 ได้ และดูท่าว่ายังไม่มีวีแววพัฒนาต่อ
เลยจะทดลองตั้งไซต์ใหม่ เป็น Drupal-4.7 และ import เอกสารเข้ามายังไซต์นี้ และหาทาง Export มายัง Drupal-5 ต่อไป

ทดลองทำดู ยังไม่สำเร็จ แต่ต้องบันทึกเอาไว้ก่อน ไม่งั้นลืมหมด
ผลที่ยังไม่สำเร็จคือ

  • หน้าที่อิมพอร์ตเข้ามา เป็น page ได้อย่างเดียว จริง ๆ เราต้องการเป็นแบบ book มากกว่า เพื่อคราวหลังจะได้ export ไปยังไซต์อื่นด้วย
  • การอิมพอร์ตพวกไฟล์รูปภาพและ css ยังไม่ค่อยสมบูรณ์

เริ่มงาน
ไล่ดูตามเอกสารและค้นกูเกิล ได้ผลดังนี้

ติดตั้ง apache2-dev
# aptitude install apache2-dev libapr1-dev libaprutil1-dev libapache2-modxslt php5-xsl php5-tidy

แก้ php.ini ให้รับ xslt (เพราะ php5 ไม่มี xslt)
# vi /etc/php5/apache2/php.ini

...
extension=domxml.so
extension=xsl.so
...

เริ่ม apache2 ใหม่
# /etc/init.d/apache2 restart

จะข้ามขั้นตอนติดตั้ง Drupal-4.7 ไปถึงตอนติดตั้งโมดูลเลย

เปลี่ยนเป็น webmaster และติดตั้งโมดูล
# su webmaster
$ cd /var/www/drupal/modules
$ wget http://ftp.osuosl.org/pub/drupal/files/projects/import_html-4.7.x-1.x-dev.tar.gz
$ tar xfz import_html-4.7.x-1.x-dev.tar.gz
$ cd import_html

ต้องแก้โค๊ดนิดนึง ตามเอกสาร Drupal - Fatal Error on Line 1225
$ vi import_html.module
:1225

...
//dsm($node);
drupal_set_message($node);
...

เสร็จแล้ว

ตั้งค่าโมดูลด้วย
administer -> modules -> เปิดให้ import_html ทำงาน

ตั้งค่าการใช้งานด้วย
administer -> access control -> เลือก role ที่จะเปิดให้ใช้

ตรวจสอบและตั้งค่าการอิมพอร์ต
administer -> settings -> import_html
ถ้าไม่ฟ้องข้อผิดพลาด ก็แสดงว่าใช้ได้แล้ว

เรียกใช้งานได้ทางเดียว
administer -> Import HTML Site

drupal: แก้ไขงานของโมดูล Import HTML

บันทึกการแก้ไขด้วยมือ
จากครั้งก่อน ผมได้สร้างหน้าเปล่าขึ้นมา แล้วแปลงให้เป็น book เตรียมไว้
แล้วจึงนำเข้าเอกสาร ด้วยโมดูล import HTML ดังนั้น หน้าเปล่าของเราจะเป็น node/1 และเอกสารที่นำเข้า จะเริ่มต้นตั้งแต่ node/2 เป็นต้นไป จนถึงหน้าสุดท้าย สมมุติว่าเป็น node/100 แล้วกัน

เมื่อทำการอิมพอร์ตเข้ามาแล้ว จะพบจุดผิดพลาดมากมาย ขอบันทึกการแก้ไขไว้นิดนึง เผื่อจะขยายขอบเขตไปถึงการแก้ไขโค๊ดต้นฉบับ

ตอนอิมพอร์ต ผมเลือกที่จะไม่เพิ่มรายการในเมนูแบบอัตโนมัติ
เราจึงต้องยกเลิกหัวข้อ Add each page to menu

ตอนนี้ศึกษาเรื่อง adodb อยู่ เลยใช้ไพธอนในการทำงานไปด้วยเลย

เริ่มต้น
$ python

Python 2.4.4 (#2, Oct 20 2006, 00:23:25) 
[GCC 4.1.2 20061015 (prerelease) (Debian 4.1.1-16.1)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import adodb
>>> driver = 'mysql'
>>> host = 'localhost'
>>> user = 'user'         # MYSQL USER
>>> password = 'password' # MYSQL PASSWORD
>>> db = 'testdrupal47'   # DRUPAL-4.7 DATABASENAME
>>> conn = adodb.NewADOConnection(driver)
>>> cur = conn.Connect(host,user,password,db)

งานที่ทำคือ

  • เปลี่ยน page เป็น book
  • แก้ไข title ล้น โดยใช้ฟังก์ชั่น string.strip()
  • เรียงลำดับหน้าหนังสือใหม่ ด้วยมือ

ขั้นตอน

เพิ่มหมายเลข node ลงในตาราง book ตั้งแต่ node/2 ถึง node/100
>>> for i in range(2, 101, 1):
...   cur = conn.Execute('INSERT INTO book (vid, nid, parent, weight) VALUE (%s, %s, 0, 0)' % (i, i))
เปลี่ยนฟิลด์ format ในตาราง node_revisions เป็น 1
>>> cur=conn.Execute('UPDATE node_revisions SET format=1 WHERE nid>1')
เปลี่ยนฟิลด์ type ในตาราง node เป็น 'book'
>>> cur=conn.Execute('UPDATE node SET type="book" WHERE nid>1')
เปลี่ยนให้ทุก node ที่เพิ่งอิมพอร์ตเข้ามา เข้าอยู่ภายใต้หน้าว่างคือ node/1
>>> cur=conn.Execute('UPDATE book SET parent=1 WHERE nid>1')
เปลี่ยนฟิลด์ comment ในตาราง node เป็น 2 (ไม่รู้ว่าคืออะไร)
>>> cur=conn.Execute('UPDATE node SET comment=2 WHERE nid>1')
ไม่ต้อง Promoted to front page โดยให้ฟิลด์ promote ในตาราง node เป็นศูนย์
>>> cur=conn.Execute('UPDATE node SET promote=0 WHERE nid>1')
แก้ไข title โดยใช้ฟังก์ชั่น string.strip()
ตาราง node
>>> for i in range(2, 101, 1):
...   cur=conn.Execute('SELECT title FROM node WHERE nid=%s' % (i))
...   title=cur.fields[0].strip()
...   cur=conn.Execute('UPDATE node SET title="%s" WHERE nid=%s' % (title, i))

ตาราง node_revisions

>>> for i in range(2, 101, 1):
...   cur=conn.Execute('SELECT title FROM node_revisions WHERE nid=%s' % (i))
...   title=cur.fields[0].strip()
...   cur=conn.Execute('UPDATE node_revisions SET title="%s" WHERE nid=%s' % (title, i))

เรียงลำดับหน้าหนังสือใหม่ ด้วยมือ
อันนี้นานที่สุด แต่ก็คุ้มกว่าการนำเข้าด้วยมือทั้งหมด

drupal: แก้เรื่องเนื้อความยาว ๆ

มีความจำเป็นที่จะต้องโพสต์เนื้อความยาว ๆ ในเนื้อหาของฟอรั่ม
พบว่า ถ้าข้อความยาวมากเกินไป จะไม่สามารถแสดงผลได้ คือแสดงผลออกมาเหมือนกับไม่มีเนื้อหา
ค้นไปค้นมา พบว่าเป็นกับโมดูล filter
ถ้าเราเลือกใช้ Input format แบบที่ใช้ Line break converter จะทำให้เกิดข้อผิดพลาดดังกล่าว

ทางแก้ชั่วคราวคือ
ให้สร้าง Input format ขึ้นมาใหม่ ไม่ให้ใช้ Line break converter แล้วเลือกใช้ Input format ใหม่นี้
ส่วนตัวเนื้อหาเองนั้น เนื่องจากไม่มีการขึ้นบรรทัดใหม่อัตโนมัติแล้ว เราจึงต้องมาสร้างแท็ก HTML ให้ขึ้นบรรทัดใหม่เอง
อาจทำใน Gedit ก็ได้ โดยใช้การ Find&Replace ธรรมดาครับ

ทางแก้ถาวรคงต้องแจ้ง Bug ไปที่ Drupal ต่อไป

Topic: 

drupal: บันทึก postgresql

ลองใช้งานกับ postgresql

ดัมป์ข้อมูล
$ pg_dump -dc -U user database > dumpfile.sql
  • -d หรือ --inserts คือให้ใช้คำสั่ง INSERT มาตรฐาน ทำให้ช้าแต่แลกเปลี่ยนข้อมูลกับฐานข้อมูลแบบอื่นได้ง่ายกว่า
  • -c หรือ --clean คือให้ล้างตารางและ Sequence ก่อนสร้างตารางใหม่ ดีสำหรับเวลาซ่อมฐานข้อมูล
สร้างตารางกลับคืน
$ psql -f dumpfile.sql -U user database
นำเข้าข้อมูลจาก text file
$ psql -c "COPY tablename (column,...) FROM filename DELIMITER E\t database"
ในที่นี้ ตัวแยกฟิลด์คือ TAB
ดูเนื้อที่การใช้งาน
$ psql -U user dbname
$ SELECT relname, relpages FROM pg_class ORDER BY relpages DESC limit line; /* tables size */
$ SELECT pg_database_size('dbname'); /* database size */
$ \q
แก้ไขค่าในตารางเป็น UTF-8
ตัวอย่างเป็นการแก้เครื่องหมาย ' และ `
UPDATE table SET field=convert_to(field,'UTF8') WHERE field != cast(convert_to(field,'UTF8') AS field_type);

UPDATE table SET field=replace(field,'\\302\\222','\'') WHERE cast(field as bytea) LIKE '%\\302\\222%';

UPDATE table SET field=replace(field,'\\302\\221','`') WHERE cast(field as bytea) LIKE '%\\302\\221%';

drupal5: บันทึกการย้ายจาก Mysql เป็น PostgreSQL

ต้องการใช้ฐานข้อมูล postgresql เพราะงานส่วนใหญ่ลงใน postgresql
ตอนศึกษา Drupal เลือก mysql เพราะหลายมอดูลใช้กับ postgresql ไม่ได้ เลยใช้ mysql มาเรื่อย
ตอนนี้ต้องการเปลี่ยนกลับแล้ว เลยศึกษาวิธีโอนข้อมูลเตรียมไว้

เที่ยวนี้ต้องทำด้วยมือเป็นส่วนใหญ่ เพราะลองค้นจากกูเกิลแล้ว สคริปต์ต่าง ๆ ที่ค้นได้ ส่วนใหญ่จะเขียนด้วย perl และล้าสมัยหมดแล้ว รันแล้วเกิดข้อผิดพลาดทุกอัน

บันทึกการย้ายฐานข้อมูล Drupal รุ่น 5.6 จาก MySQL-5.0.32 มาเป็น PostgreSQL-8.1.11 บนเดเบียน Etch

  1. คัดลอกไดเรกทอรี่เก่ามายังอันใหม่ทั้งหมด
    $ cd /var/www/
    $ cp -xa drupal newdrupal
  2. ปรับตั้ง drupal ของไดเรกทอรี่ใหม่ ให้เป็นฐานข้อมูลใหม่
    $ cd newdrupal
    $ vi sites/default/setting.php
    ...
    //$db_url = 'mysql://myuser:mypassword@localhost/drupal-mysql';
    $db_url = 'pgsql://pguser:pgpassword@localhost/newdrupal-pgsql';
    ...
  3. ดิดตั้งโดยเข้าไปที่ URL: http://www.example.com/install.php ติดตั้งพร้อมเพิ่มผู้ใช้คือ admin ให้เรียบร้อย
  4. ดูใน drupal เก่า ว่าเปิดใช้มอดูลอะไรบ้าง ก็ให้เปิดใช้ให้เหมือนเดิม ทั้งนี้เพื่อให้ Drupal สร้างตารางขึ้นมารองรับการโอน โดยใช้ URL: admin/build/modules เลือกกาทุกมอดูล
  5. ล้างข้อมูลในตารางทั้งหมด แต่ไม่ต้องลบตาราง (งานนี้ลบด้วยมือ โดยใช้ phppgadmin ประมาณสัก 10 ตารางเห็นจะได้
  6. นำข้อมูลจากฐานข้อมูลเก่ามา โดยใช้ mysqldump
    $ mysqldump -c -t --skip-opt -u myuser -p drupal-mysql > mydrupal.sql
    -c คือ complete insert เพื่อให้เหมาะกับการนำเข้าไปยัง postgresql
    -t คือ --no-create-info ไม่ต้องเขียนการสร้างตารางใหม่
    --skip-opt คือไม่ต้องเขียนการล๊อกตาราง
  7. ปรับแต่งข้อมูลให้เหมาะกับ postgresql โดยการลบ backquote (`) ทิ้ง
    $ vi mydrupal.sql
    :1,$ s/`//g
    :wq
  8. ถึงตรงนี้อาจนำเข้าข้อมูลได้แล้ว แต่จะมีข้อผิดพลาดจากโค๊ดของ Drupal ตรงตาราง node_revision หน่อยนึง ตรงสดมถ์ log ที่ mysql ยอมให้เป็น NULL แต่ postgresql ไม่ยอม
    ต้องไปแก้ตาราง node_revision สดมถ์ log ให้เป็น NULL ได้
    งานนี้ทำผ่าน phppgadmin
  9. นำเข้าฐานข้อมูลใหม่ โดยใช้ psql
    $ psql -f mydrupal.sql -U pguser newdrupal-pgsql
  10. เข้าไซต์ใหม่ แล้วอัปเดตครั้งนึง ถ้ายังมีรายงานข้อผิดพลาด อาจต้องไปลบ Constraints ของตาราง locales_source ออก (ตรงนี้ลืมจดชื่อ Constraints) โดยใช้ phppgadmin

เสร็จแล้ว

แต่หากยังมีข้อผิดพลาดแบบเล็กน้อย เราอาจเริ่มสร้างตารางใหม่ทั้งหมดผ่าน pg_dump ได้ คือ

  • ดัมป์ข้อมูลแบบสั่งให้สร้างตารางใหม่ทั้งหมด
    $ pg_dump -c -U pguser newdrupal-pgsql > newpg.sql
    -c ให้สร้างคำสั่งสร้างตารางด้วย
  • นำเข้าข้อมูลอีกครั้ง
    $ psql -f newpg.sql -U pguser newdrupal-pgsql

เสร็จแล้ว

update 51-01-22
ยังไม่เสร็จจริง พอตรวจจริงแล้วเกิดข้อผิดพลาดคือ postgresql ไม่ยอมปรับค่า sequence ให้ตามจริง ไม่รู้ว่าเป็นเพราะอะไร ต้องมานั่งปรับประโยคใน newpg.sql ให้เลข sequence เป็นตัวเลขล่าสุด ต้องทำสักประมาณ 20 ตาราง
ขั้นตอนตั้งแต่เริ่ม (นับตั้งแต่ผ่านขั้นตอนก่อนหน้านี้มาแล้ว) คือ

  1. สั่งดัมป์จาก postgresql โดยให้มีคำสั่งสร้างตารางด้วย
    $ pg_dump -c -U pguser newdrupal-pgsql > newpg.sql
  2. แก้ไขข้อมูล sql โดยเอาแต่ส่วนหัว ตัดส่วน INSERT ทิ้งไป คือดูตั้งแต่คำสั่ง COPY ... เป็นต้นไป
    $ vi newpg.sql
    /COPY
    dG
    :wq
  3. ไล่ดูว่าตารางไหนมีฟีลด์ที่เป็น SERIAL ให้ดูค่าที่มากที่สุดของฟีลด์นั้น แล้วนำไปปรับแก้ในข้อมูล sql
    เช่นสมมุติว่า ตาราง accesslog มีฟีลด์ที่เป็น SERIAL คือ aid โดยมีค่าที่มากที่สุดเป็น 12345 (ดูจาก phppgadmin) ก็ให้ไปปรับแก้ประโยค sql ตรงส่วนปรับ sequence ให้เป็นตัวเลขนี้ด้วย เช่น
    $ vi pgnew.sql
    ...
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('accesslog', 'aid'), 12345, true);
    ...

    ทำให้ครบทุกตารางที่มีฟิลด์เป็น SERIAL

  4. นำเข้าข้อมูลจาก sql ของ postgresql ที่เพิ่งปรับแก้เมื่อสักครู่ เพื่อนำไปสร้างตาราง
    $ psql -f newpg.sql -U pguser newdrupal-pgsql
  5. นำเข้าข้อมูลจาก sql ของ mysql ที่ได้จากคราวก่อน เพื่อนำไปเป็นข้อมูล
    $ psql -f mydrupal.sql -U pguser newdrupal-pgsql

เสร็จจริง ๆ แล้ว

ยุ่งเป็นลิงถุง สงสัยเที่ยวหน้าทำเป็นสคริปต์ดูจากข้อมูลดัมป์ของ mysql แล้วโยนไปสร้างสดที่ postgresql เลยดีกว่า

drupal5: ปรับปรุงวิธีย้ายจาก Mysql เป็น PostgreSQL

ปรับปรุงจากครั้งก่อน แต่เพื่อให้สมบูรณ์ในตัวและอ้างอิงภายหลังได้ เลยเขียนใหม่ทั้งหมด

บันทึกการย้ายฐานข้อมูล Drupal รุ่น 5.6 จาก MySQL-5.0.32 มาเป็น PostgreSQL-8.1.11 บนเดเบียน Etch

  1. สร้างฐานข้อมูลใหม่
    $ createdb --encoding=UNICODE -U PG_USER NEW_DRUPAL_PG_DB
  2. คัดลอกไดเรกทอรี่เก่ามายังอันใหม่ทั้งหมด เพราะต้องการเอาไดเรกทอรี่ของมอดูลเก่ามาด้วย เพื่อนำมาเป็นข้อมูลทั้งหมดในการสร้างฐานข้อมูลใหม่
    $ cd /var/www/
    $ cp -xa OLD_DRUPAL_DIR NEW_DRUPAL_DIR
  3. ปรับตั้ง drupal ของไดเรกทอรี่ใหม่ ให้เป็นฐานข้อมูลใหม่
    $ cd NEW_DRUPAL_DIR
    $ vi sites/default/setting.php
    ...
    //$db_url = 'mysql://MY_USER:MY_PASSWORD@localhost/OLD_DRUPAL_MY_DB';
    $db_url = 'pgsql://PG_USER:PG_PASSWORD@localhost/NEW_DRUPAL_PG_DB';
    ...
  4. ดิดตั้งโดยเข้าไปที่ URL: http://WWW.EXAMPLE.COM/install.php ติดตั้งพร้อมเพิ่มผู้ใช้คือ admin ให้เรียบร้อย (create new user)
  5. ดูใน drupal เก่า ว่าเปิดใช้มอดูลอะไรบ้าง ก็ให้เปิดใช้ให้เหมือนเดิม ทั้งนี้เพื่อให้ Drupal สร้างตารางขึ้นมารองรับการโอน โดยใช้ URL: admin/build/modules เลือกกาทุกมอดูล (enable all modules)
  6. ล้างข้อมูลในตารางทั้งหมด รวมทั้งเป็นการสำเนาส่วนหัวของตารางใหม่ไปในตัว โดยใช้ pg_dump
    $ pg_dump -cs -U PG_USER NEW_DRUPAL_PG_DB > NEW_PG.SQL
  7. -c คือ --clean ล้างตารางเก่า ก่อนสร้างตารางใหม่
    -s คือ --schema-only เอาแต่ส่วนหัวของการสร้างตาราง ไม่เอาข้อมูล เพราะเราต้องการล้างอยู่แล้ว

  8. สร้างตารางใหม่จาก sql เมื่อกี้นี้ ด้วย psql
    $ psql -f NEW_PG.SQL -U PG_USER NEW_DRUPAL_PG_DB > temp.log
    ตรงนี้ไม่ควรมีข้อผิดพลาดขึ้น
  9. นำข้อมูลจากฐานข้อมูลเก่ามา โดยใช้ mysqldump
    $ mysqldump -c -t --skip-opt -u MY_USER -p OLD_DRUPAL_MY_DB > OLD_MY.SQL
    -c คือ complete insert เพื่อให้เหมาะกับการนำเข้าไปยัง postgresql
    -t คือ --no-create-info ไม่ต้องเขียนการสร้างตารางใหม่
    --skip-opt คือไม่ต้องเขียนการล๊อกตาราง
  10. ปรับแต่งข้อมูลให้เหมาะกับ postgresql โดยการลบ backquote (`) ทิ้ง
    $ vi OLD_MY.SQL
    :1,$ s/`//g
    :wq
  11. ถึงตรงนี้อาจนำเข้าข้อมูลได้แล้ว แต่จะมีข้อผิดพลาดจากโค๊ดของ Drupal ตรงตาราง node_revisions หน่อยนึง ตรงสดมถ์ log ที่ mysql ยอมให้เป็น NULL แต่ postgresql ไม่ยอม
    ต้องไปแก้ตาราง node_revisions สดมถ์ log ให้เป็น NULL ได้
    งานนี้ทำผ่าน phppgadmin (enable NULL in node_revisions.log)
  12. นำเข้าฐานข้อมูลใหม่ โดยใช้ psql
    $ psql -f OLD_MY.SQL -U PG_USER NEW_DRUPAL_PG_DB > temp.log
    ขั้นตอนนี้อาจมีข้อผิดพลาดรายงาน ไม่ต้องสนใจ
  13. แก้เรื่อง sequence ไม่ตรง โดยนำข้อมูลจาก phppgadmin ดูจากหน้า public/Sequences เอาทั้งหมดมาสร้างเป็น sql สมมุติตั้งชื่อว่า fixseq.sql
    $ vi fixseq.sql
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('access', 'aid'), (SELECT MAX(aid) FROM access), false);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('accesslog', 'aid'), (SELECT MAX(aid) FROM accesslog), true);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('aggregator_category', 'cid'), (SELECT MAX(cid) FROM aggregator_category), false);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('aggregator_feed', 'fid'), (SELECT MAX(fid) FROM aggregator_feed), false);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('aggregator_item', 'iid'), (SELECT MAX(iid) FROM aggregator_item), false);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('authmap', 'aid'), (SELECT MAX(aid) FROM authmap), false);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('boxes', 'bid'), (SELECT MAX(bid) FROM boxes), false);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('client', 'cid'), (SELECT MAX(cid) FROM client), false);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('comments', 'cid'), (SELECT MAX(cid) FROM comments), false);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('contact', 'cid'), (SELECT MAX(cid) FROM contact), false);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('files', 'fid'), (SELECT MAX(fid) FROM files), false);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('filter_formats', 'format'), (SELECT MAX(format) FROM filter_formats), true);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('locales_source', 'lid'), (SELECT MAX(lid) FROM locales_source), false);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('menu', 'mid'), (SELECT MAX(mid) FROM menu), true);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('node_comment_statistics', 'nid'), (SELECT MAX(nid) FROM node_comment_statistics), false);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('node', 'nid'), (SELECT MAX(nid) FROM node), false);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('node_revisions', 'vid'), (SELECT MAX(vid) FROM node_revisions), false);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('poll_choices', 'chid'), (SELECT MAX(chid) FROM poll_choices), false);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('profile_fields', 'fid'), (SELECT MAX(fid) FROM profile_fields), false);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('"role"', 'rid'), (SELECT MAX(rid) FROM role), true);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('term_data', 'tid'), (SELECT MAX(tid) FROM term_data), false);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('url_alias', 'pid'), (SELECT MAX(pid) FROM url_alias), false);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('users', 'uid'), (SELECT MAX(uid) FROM users), true);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('vocabulary', 'vid'), (SELECT MAX(vid) FROM vocabulary), true);
    SELECT pg_catalog.setval(pg_catalog.pg_get_serial_sequence('watchdog', 'wid'), (SELECT MAX(wid) FROM watchdog), true);

    สั่งรันด้วย psql ให้มาซ่อมเรื่อง sequence
    $ psql -f fixseq.sql -U PG_USER NEW_DRUPAL_PG_DB > temp.log

    ขั้นตอนนี้ไม่ควรมีข้อผิดพลาด

  14. อันนี้สำหรับถ้าติดตั้ง Drupal ไว้ในไดเรกทอรี่รองจากโดเมนหลัก (OPTIONAL: IF INSTALL DRUPAL IN SUBDIR) เช่น http://www.example.com/drupal และเปลี่ยนชื่อไดเรกทอรี่ด้วย
    ต้องปรับแก้ข้อมูลในตาราง locales_source ให้ข้อมูลเป็นอันใหม่
    เช่นอันเก่าชื่อ www.example.com/OLDDRUPAL อันใหม่ชื่อ www.example.com/NEWDRUPAL ต้องเขียน sql ดังนี้
    $ vi fixlocale.sql
    UPDATE locales_source SET location = '/NEWDRUPAL/' || substr(location,12) WHERE location LIKE '/OLDDRUPAL/%'

    ตัวเลข 12 ของฟังก์ชั่น substr(location, NUM) เป็นความยาวของอักขระไดเรกทอรี่เก่า คือ OLDDRUPAL บวก 3 คือเครื่องหมาย '/' หน้าหลังและบวกกับตำแหน่งถัดไป
    สั่งรันได้เลย
    $ psql -f fixlocale.sql -U PG_USER NEW_DRUPAL_PG_DB > temp.log

    ต้องไม่มีข้อผิดพลาด

เสร็จจริง ๆ แล้ว

จะมีรายงานข้อผิดพลาด ที่หน้า Administer หนึ่งครั้ง
และตอนสร้างเนื้อหาใหม่อีกหนึ่งครั้ง ก็สามารถใช้งานได้แล้ว

drupal6: ย้ายจาก MySQL เป็น PostgreSQL

บันทึกการย้ายฐานข้อมูล Drupal รุ่น 6.13 จาก MySQL-5.0.51a-24+lenny1 มาเป็น PostgreSQL-8.3.7-0lenny1

เที่ยวนี้ได้สคริปต์ดีจาก Drupal 6.x. site Migration from MySQL to PostgreSQL (howto) จึงสามารถทำตรงกับฐานข้อมูลได้เลย

  1. แบ๊คอัปกันเหนียวเสียก่อน
    $ mysqldump -u my_admin -p my_drupal_db > drupal.mysql.sql
  2. แก้ไข sites/default/settings.php ให้มาใช้ postgres
    $ vi sites/default/settings.php
    ...
    #$db_url = 'mysql://my_admin:my_password@localhost/my_drupal_db';
    $db_url = 'pgsql://pg_admin:pg_password@localhost/pg_drupal_db';
    ...
  3. เข้าไซต์ที่ใช้ฐานข้อมูลใหม่ สั่ง install ล๊อกอินเป็น admin พร้อมทั้งเปิดมอดูลทุกอัน
    http://www.example.com/install.php
    http://www.example.com/?q=user/login
    http://www.example.com/?q=admin/build/modules

    อย่าลืมเปิดมอดูลทุกอัน

  4. ดาวน์โหลดสคริปต์มาใช้งาน แตกไฟล์ และแก้ไขชื่อ admin พร้อมรหัสผ่านให้เรียบร้อย
    $ wget http://blog.lexa.ru/files/drupal-mysql2pgsql.pl.gz
    $ gunzip drupal-mysql2pgsql.pl.gz
    $ vi drupal-mysql2pgsql.pl
    ...
    #my $srcdb = DBI->connect("dbi:mysql:database=$ARGV[0]") or die;
    my $srcdb = DBI->connect("dbi:mysql:database=$ARGV[0]","my_admin","my_password") or die;
    #my $destdb = DBI->connect("dbi:Pg:dbname=$ARGV[1]") or die;
    my $destdb = DBI->connect("dbi:Pg:dbname=$ARGV[1]","pg_admin","pg_password") or die;
    ...
  5. สั่งรันสคริปต์
    $ ./drupal-mysql2pgsql.pl my_drupal_db pg_drupal_db

เสร็จแล้วครับ เข้าไซต์ที่ใช้ฐานข้อมูลใหม่ได้เลย

drupal: บันทึกการอัปเกรด 5.2 -> 6.0-beta-1

สมมุติว่าเว็บคือ example.com

ไปที่ไดเรกทอรี่ทำงาน
$ cd /var/www/example.com
สมมุติไดเรกทรอรี่เก่าคือ drupal ฐานข้อมูลก็ชื่อ example_com_drupal

แบ็กอัพไฟล์
$ tar cfz drupal-old.tar.gz drupal

แบ็กอัพฐานข้อมูล
$ mysqldump -u webmaster -p example_com_drupal > example_com_drupal.mysql.bak

ดาวน์โหลด 6.0-beta-1 และแตกไฟล์
$ wget http://ftp.drupal.org/files/projects/drupal-6.0-beta1.tar.gz
$ tar xfz drupal-6.0-beta1.tar.gz

คัดลอกไฟล์ไปยังไดเรกทอรี่เก่า
$ cp -xa drupal-6.0-beta1/* drupal

แก้ไขตามจำเป็น
$ vi drupal/site/default/setting.php

...

ล้างธีมและโมดูลเก่า พร้อมทั้งคัดลอกมาใหม่
ธีม
$ for i in drupal-6.0-beta1/themes/*; do aaa=`basename $i`; rm -rf drupal/themes/$aaa; cp -xa $i drupal/themes; done
โมดูล
$ for i in drupal-6.0-beta1/modules/*; do aaa=`basename $i`; rm -rf drupal/modules/$aaa; cp -xa $i drupal/modules; done

อัปเดตที่บราวเซอร์

http://www.example.com/update.php

ตรวจสอบว่าธีมและโมดูลที่เราติดตั้งเพิ่มเติมในครั้งก่อน ได้ถูกปรับปรุงขึ้นมาเป็นรุ่น 6 ทั้งหมดหรือยัง

ถ้ายังไม่ครบ ก็ดาวน์โหลดรุ่น 6 มาติดตั้งให้เรียบร้อย

เสร็จแล้ว

drupal: สร้างมอดูลเอง

บันทึกการทำงานเกี่ยวกับการสร้างมอดูล

Topic: 

drupal: มอดูล Thai Search

คุณสมบัติ

ค้นภาษาไทยได้ โดยมีการให้น้ำหนักคำค้น และสามารถใส่คำค้นได้หลายคำ

drupal5
thaisearch-5.x-0.1.tar.gz
drupal6
thaisearch-6.x-0.2.tar.gz
drupal7
thaisearch-7.x-0.3.tar.gz

ประวัติ ...

Topic: 

drupal5: ทำมอดูล Thai Search

แปลง มอดูล Thai Search ของรุ่น 6 มาเป็น ของรุ่น 5
ความสามารถเหมือนกันคือ ค้นคำไทยได้ ทำบล๊อกได้ ค้นได้หลายคำค้นพร้อมกัน
ติดตั้งปกติเหมือนมอดูลของ Drupal ทั่วไป

ดาวน์โหลดได้ที่นี่ครับ

หมายเหตุ

  • ยังไม่ได้ตรวจสอบแบบจริงจัง แต่คงใช้งานได้ปกติโดยไม่ก่อให้เกิดความเสียหายครับ เพราะไม่ได้สร้างหรือเขียนลงฐานข้อมูลเลย
  • ของรุ่น 6 พบว่าใช้ประโยค SQL แบบมีคำสั่ง GROUP BY กับฟังก์ชั่น pager ไม่ได้ แต่กับรุ่น 5 นี้ ยังไม่ได้ทดลองครับ
Topic: 

drupal6: ทำมอดูล Thai Search

มีโจทย์เรื่องต้องค้นข้อมูลใน Drupal เป็นภาษาไทยในไซต์ที่เป็นอินทราเน็ตให้ได้

ปัญหา

การค้นข้อมูลใน Drupal เขาทำเป็น Full Text Search โดยทำเป็น cron ในการจัดเรียงดัชนีข้อมูล เวลามาค้นก็จะหาได้รวดเร็ว
แต่ปัญหาคือ

  • ภาษาไทยใช้แทบไม่ได้เลย เพราะการทำ Full Text Search ต้องอาศัยการตัดคำเพื่อนำไปจัดเรียงเป็นดัชนี ซึ่งตอนนี้ของเรายังด้อยเรื่องนี้อยู่ โดยเฉพาะกับ php (แต่เริ่มมีคนทำบ้างแล้ว เช่น ที่ pecl (เดาว่าจะออกกับ php-5.3 ดูรายละเอียดที่ ICU+PHP=love และ agavi.org ที่ผมยังไม่ได้ศึกษา)
  • เนื่องจากเป็น Full Text Search อีกเหมือนกัน ที่การบางส่วนของคำ ใช้ไม่ได้ เช่น ไม่สามารถค้น "iffi" จากคำว่า "difficult" ได้ หรือไม่สามารถค้น "stand" จากคำว่า "understandable" ได้

สำหรับไซต์ที่เป็นอินเตอร์เน็ต คือข้อมูลออกสู่โลกภายนอก สามารถแก้ได้โดยใช้กูเกิล โดยพิมพ์ต่อท้ายคำค้นว่า site:example.com หรือสร้าง custom search engine เอง

สำหรับไซต์ที่เป็นอินทราเน็ต คือข้อมูลอยู่แต่ภายใน หรือไม่อนุญาตให้ crawler เข้ามาค้นข้อมูลในไซต์ ทางแก้คือลงมอดูล solr (ท่าทางจะลงยาก เป็นจาวา) หรือมอดูลอื่น (ที่อาจยุ่งพอกัน)

ทางออก

ทางออกคือทำเองแบบง่าย ๆ พอใช้งานได้ดีกว่า โดยพยายามทำคือ

  • ทำให้ง่ายที่สุด (เพราะทำยากไม่เป็น ;D)
  • ไม่ต้องสร้างตารางใหม่ เวลาอัปเกรดแล้วข้อมูลไม่รก
  • ให้ค้นบางส่วนของคำได้ จะได้เป็นตัวเสริมของมอดูล search จริง ๆ (ในอนาคตอันใกล้นี้ search.module จะต้องทำงานนี้ได้แน่ ๆ)
  • ไม่ไปยุ่งกับ Core Module

ดูตัวอย่างจาก page_example.module และตัวอย่างจาก Core Module ได้ออกมาดังนี้

ติดตั้ง

ตามขั้นตอนปกติคือ

  • ติดตั้งไว้ที่ site/all/modules/ ตั้งชื่อมอดูลว่า thaisearch
  • เปิดใช้งานจาก admin/build/modules
  • เปิดข้ออนุญาตจากผู้ใช้ amdin/user/permissions

การใช้งานและข้อจำกัด

เนื่องจากเป็นรุ่นแรก ทำพอใช้งานได้ จึงทุลักทุเลพอควร

  • ให้ใช้งานด้วยการพิมพ์ลงไปใน URL ตรง ๆ ว่า thaisearch/keywords
  • สามารถเว้นวรรคในคำค้นได้ แต่โปรแกรมจะตัดทิ้งหมด เหลือแค่คำค้นคำแรก แก้การงงของ query ที่ซ้อนหลายชั้น
  • ไปค้นที่ หัวเรื่องของ node เนื้อความของ node และเนื้อตวามของ comment เท่านั้น
  • ตอนเขียน พบว่าฟังก์ชั่น pager_query ซึ่งต้องใช้ในการแยกหน้า กลับมีปัญหากับ query GROUP BY เลยตัดทิ้งหมดเลย และจำกัดข้อมูลค้นแค่หน้าเดียว 50 รายการ
  • ยังปรับตั้งค่าอะไรไม่ได้เลย
  • เนื่องจากใช้วิธีการค้นข้อมูลแบบโง่ ๆ ที่สุด จึงไม่ควรใช้กับไซต์ขนาดใหญ่ ควรใช้ในงานอินทราเน็ตเท่านั้น

หวังว่าคงจะมีเวลาศึกษา SQL และ PHP เพิ่มเติมเพื่อนำมาปรับปรุงในรุ่นหน้า หรือรอท่านผู้ใจบุญปรับปรุงแล้วแจกจ่ายต่อไป

เริ่มปรุง

สร้างไฟล์ info
$ cd /var/www/drupal
$ mkdir -p sites/all/modules/thaisearch
$ cd sites/all/modules/thaisearch
$ vi thaisearch.info

; $Id$
name = Thai search
description = Search Thai words in node/comment.
core = 6.x

version = "6.0-rc2"

สร้างไฟล์ module
$ vi thaisearch.module

<?php
// $Id: thaisearch.module,v 1.13 2007/10/17 19:38:36 litwol Exp $
// wd's: modify to thaisearch.module
/**
 * @file
 * This is an example outlining how a module can be used to display a
 * custom page at a given URL.
 */

/**
 * Implementation of hook_help().
 *
 * Throughout Drupal, hook_help() is used to display help text at the top of
 * pages. Some other parts of Drupal pages get explanatory text from these hooks
 * as well. We use it here to illustrate how to add help text to the pages your
 * module defines.
 */
function thaisearch_help($path, $arg) {
  switch ($path) {
    case 'thaisearch':
      // Here is some help text for a custom page.
      return t('Search thai words. Type in URL: <code><strong>thaisearch/<em>WORD1 WORD2 ...</em></strong></code> separated by space.');
  }
}

/**
 * Implementation of hook_perm().
 *
 * Since the access to our new custom pages will be granted based on
 * special permissions, we need to define what those permissions are here.
 * This ensures that they are available to enable on the user role
 * administration pages.
 */
function thaisearch_perm() {
  return array('access thaisearch');
}
/**
 * Implementation of hook_menu().
 *
 * You must implement hook_menu() to emit items to place in the main menu.
 * This is a required step for modules wishing to display their own pages,
 * because the process of creating the links also tells Drupal what
 * callback function to use for a given URL. The menu items returned
 * here provide this information to the menu system.
 *
 * With the below menu definitions, URLs will be interpreted as follows:
 *
 * If the user accesses http://example.com/?q=foo, then the menu system
 * will first look for a menu item with that path. In this case it will
 * find a match, and execute thaisearch_foo().
 *
 * If the user accesses http://example.com/?q=bar, no match will be found,
 * and a 404 page will be displayed.
 *
 * If the user accesses http://example.com/?q=bar/baz, the menu system
 * will find a match and execute thaisearch_baz().
 *
 * If the user accesses http://example.com/?q=bar/baz/1/2, the menu system
 * will first look for bar/baz/1/2. Not finding a match, it will look for
 * bar/baz/1/%. Again not finding a match, it will look for bar/baz/%/2. Yet
 * again not finding a match, it will look for bar/baz/%/%. This time it finds
 * a match, and so will execute thaisearch_baz(1, 2). Note the parameters
 * being passed; this is a very useful technique.
 */
function thaisearch_menu() {

  // By using the MENU_CALLBACK type, we can register the callback for this
  // path but not have the item show up in the menu; the admin is not allowed
  // to enable the item in the menu, either.
  //
  // Notice that the 'page arguments' is an array of numbers. These will be
  // replaced with the corresponding parts of the menu path. In this case a 0
  // would be replaced by 'thaisearch', and 1 will be Thai words to search.
  // These will be passed as arguments to the thaisearch_thaisearch() function.
  $items['thaisearch/%'] = array(
    'title' => 'Thai Search',
    'page callback' => 'thaisearch_thaisearch',
    'page arguments' => array(1),
    'access arguments' => array('access thaisearch'), 
//    'type' => MENU_CALLBACK,
  );
 
  return $items;
}
 
/**
 * A more complex page callback that takes arguments.
 * 
 * The arguments are passed in from the page URL. The in our hook_menu
 * implementation we instructed the menu system to extract the last two
 * parameters of the path and pass them to this function as arguments.
 */
function thaisearch_thaisearch($words) {
  $max_item = 50;
  $keys = explode(" ", $words);
   
  $qnt = "SELECT 2 AS score, n.nid, 0 AS cid, n.title, n.body FROM {node_revisions} n WHERE n.title LIKE '%%%s%%' ";
  $qnb = "SELECT 1 AS score, n.nid, 0 AS cid, n.title, n.body FROM {node_revisions} n WHERE n.body LIKE '%%%s%%' ";
  $qc = "SELECT 1 AS score, c.nid, c.cid, n.title, c.comment AS body FROM {comments} c INNER JOIN {node_revisions} n ON n.nid = c.nid WHERE c.comment LIKE '%%%s%%' ";
  
  $q = 'SELECT SUM(score) AS score, nid, cid, title, body FROM ('.$qnt.' UNION ALL '.$qnb.' UNION ALL '.$qc.' ) t GROUP BY t.nid, t.cid, t.title, t.body ORDER BY score, nid DESC';
  
  $query = db_query_range($q, $keys[0], $keys[0], $keys[0], 0, $max_item);

  $sql_count = 'SELECT COUNT(*) AS num FROM ('.$q.') t';
  $count = db_fetch_object(db_query($sql_count,  $keys[0], $keys[0], $keys[0]));
  $found = $count->num;

  $return = thaisearch_help('thaisearch','').'<br />';
  $return .= t('Search words: <strong>'.$words.'</strong>, found: <strong>'.$found.'</strong><br />');

  $dlist = '';
  $max_words = 20;
  $words_between = 10;
  while ($links = db_fetch_object($query)) {
    // highlight words
    $ar = explode($keys[0], $links->body);
    if ($ar[0]) {
      $len_left = drupal_strlen($ar[0]);
      $ar[0] = ($max_words > $len_left) ? $ar[0] : '...'.drupal_substr($ar[0], $len_left-$max_words, $max_words);
    }
    for ($i = 1; $i < count($ar); $i++) {
      $len_right = drupal_strlen($ar[$i]);
      if ($i == count($ar)-1) {
        $ar[$i] = ($max_words > $len_right) ? $ar[$i] : drupal_substr($ar[$i], 0, $max_words).'...';
      } else {
        $ar[$i] = ($max_words > $len_right) ? $ar[$i] : drupal_substr($ar[$i], 0, $words_between).'...'.drupal_substr($ar[$i], $len_right-$word_between, $word_between);
      }
    }

    if ($links->cid != 0) {
      $dlist .= '<dt>'.l($links->title, 'node/'.$links->nid, array('fragment' => 'comment-'.$links->cid)).'</dt>';
    } else {
      $dlist .= '<dt>'.l($links->title, 'node/'.$links->nid).'</dt>';
    }
    $dlist .= '<dd>'.implode('<strong>'.$keys[0].'</strong>', $ar).'</dd><br />';
  }
  
  if ($dlist) {
    $return .= theme('box', t('Search results'), '<br />'.$dlist, 10, 0);
  } else {
    $return .= theme('box', t('Your search yielded no results'));
  }
  return $return;
} 
?>

ผลการใช้งาน

รันได้รวดเร็วดีพอควร

ต้องปรับปรุง

(รอผู้ใจบุญ)

  • ทำเป็นบล๊อกไว้พิมพ์คำค้นง่าย ๆ
  • เขียน SQL ดี ๆ ให้ใช้กับฟังก์ชั่น pager_query ได้
  • ใช้กับเนื้อความได้ทุกแบบ ไม่จำกัดแค่ node กับ comments
  • เขียนเพิ่มเรื่อง uppercase/lowercase/Capitalize
  • ค้นได้ทีละหลายคำค้น โดยมีการให้น้ำหนัก (คำแรกเยอะหน่อย) แล้วจับมารวมกัน พอเรียงแบบ DESC มันน่าจะขึ้นหัวข้อที่เราต้องการที่สุดไว้ต้น ๆ คล้าย ๆ กูเกิล

อ้างอิง

เพื่อความสุขสวัสดี อย่าลืมรัน update.php ด้วย ไม่งั้นอาจมีปัญหาเรื่อง HTTP request failed

Topic: 

drupal6: ปรับปรุงมอดูล Thai Search

ยังไม่เข้าใจการทำงานมอดูลอย่างจริงจัง ตอนนี้ใช้วิธีตัดแปะจาก Core Module ไปก่อน

  • ปรับปรุงให้มีฟอร์มสำหรับใส่คำค้นในหน้าหลัก ซึ่งจะทำให้สามารถเปิดใช้เมนู Thaisearch ได้ด้วย
  • จัดเรียงตามลำดับน้ำหนัก, และเวลา
  • ใส่คำค้นได้หลายคำ โดยจัดน้ำหนักให้ด้วย - ทุกคำ น้ำหนัก10, คำแรกน้ำหนัก 3, คำที่เหลือ น้ำหนัก 1 และเพิ่มน้ำหนักให้กับส่วนที่ค้น คือ node.title น้ำหนัก 10, node.body น้ำหนัก 2, comments.comment น้ำหนัก 1
  • 50-01-25 เพิ่ม block เพิ่ม css เพิ่ม watchdog
  • รู้สึกจะมีปัญหากับ Drupal เรื่อง HTTP request ยังไม่แน่ใจว่าเป็นที่ใคร วิธีแก้คือ ย้ายทุกมอดูลออกจาก sites/all/modules -> สั่ง update.php -> สั่งอัปเดตด้วยมือ admin/reports/updates/check -> ย้ายมอดูลกลับ

ดาวน์โหลดได้แล้ว

ติดตั้งตามวิธีปกติ
$ cd /var/www/drupal/sites/all/modules
$ wget http://www.thaitux.info/files/modules/thaisearch-6.x-0.1.tar.gz
$ tar xfz thaisearch-6.x-0.1.tar.gz

หรือเขียนโค๊ดเอง
เพิ่มไฟล์ css
$ cd /var/www/drupal/sites/all/modules/thaisearch
$ vi thaisearch.css

/* $Id: thaisearch.css,v 1.3 2007/10/31 18:06:38 dries Exp $ */

.block-thaisearch .form-item {
  display: inline;
  margin: 0;
  padding: 0;
}

แก้เฉพาะไฟล์ thaisearch.module
$ vi thaisearch.module

<?php
// $Id: thaisearch.module,v 1.13 2007/10/17 19:38:36 litwol Exp $
// wd's: modify to thaisearch.module, simple search Thai words. v.2008/1/24.
/**
 * @file
 * This is an example outlining how a module can be used to display a
 * custom page at a given URL.
 */

/**
 * Implementation of hook_help().
 *
 * Throughout Drupal, hook_help() is used to display help text at the top of
 * pages. Some other parts of Drupal pages get explanatory text from these hooks
 * as well. We use it here to illustrate how to add help text to the pages your
 * module defines.
 */
function thaisearch_help($path, $arg) {
  switch ($path) {
    case 'thaisearch':
      // Here is some help text for a custom page.
      return t('Search thai words. Type in URL: <code><strong>thaisearch/<em>WORD1 WORD2 ...</em></strong></code><br />Words separated by space, quote or double-quote are allowed.<br /><code>a "b c" d</code> gives <code>"<strong>a</strong>"</code> , <code>"<strong>b c</strong>"</code> , <code>"<strong>d</strong>"</code>');
    case 'search#noresults':
      return t('<ul>
<li>Check if your spelling is correct.</li>
<li>Remove quotes around phrases to match each word individually: <em>"blue smurf"</em> will match less than <em>blue smurf</em>.</li>
<li>Consider loosening your query with <em>OR</em>: <em>blue smurf</em> will match less than <em>blue OR smurf</em>.</li>
</ul>');
  }
}

/**
 * Implementation of hook_perm().
 *
 * Since the access to our new custom pages will be granted based on
 * special permissions, we need to define what those permissions are here.
 * This ensures that they are available to enable on the user role
 * administration pages.
 */
function thaisearch_perm() {
  return array('access thaisearch');
}

/**
 * Implementation of hook_block().
 */
function thaisearch_block($op='list', $delta=0) {
  // listing of blocks, such as on the admin/block page
  if ($op == "list") {
     $block[0]["info"] = t('Thai Search');
     return $block;
  } else if ($op == 'view' && user_access('access thaisearch')) {
    $block['content'] = drupal_get_form('thaisearch_form', $keys, $size=15);
    $block['subject'] = t('Thai Search');
    return $block;
  }
}

/**
 * Implementation of hook_menu().
 *
 * You must implement hook_menu() to emit items to place in the main menu.
 * This is a required step for modules wishing to display their own pages,
 * because the process of creating the links also tells Drupal what
 * callback function to use for a given URL. The menu items returned
 * here provide this information to the menu system.
 *
 * With the below menu definitions, URLs will be interpreted as follows:
 *
 * If the user accesses http://example.com/?q=foo, then the menu system
 * will first look for a menu item with that path. In this case it will
 * find a match, and execute thaisearch_foo().
 *
 * If the user accesses http://example.com/?q=bar, no match will be found,
 * and a 404 page will be displayed.
 *
 * If the user accesses http://example.com/?q=bar/baz, the menu system
 * will find a match and execute thaisearch_baz().
 *
 * If the user accesses http://example.com/?q=bar/baz/1/2, the menu system
 * will first look for bar/baz/1/2. Not finding a match, it will look for
 * bar/baz/1/%. Again not finding a match, it will look for bar/baz/%/2. Yet
 * again not finding a match, it will look for bar/baz/%/%. This time it finds
 * a match, and so will execute thaisearch_baz(1, 2). Note the parameters
 * being passed; this is a very useful technique.
 */
function thaisearch_menu() {

  // By using the MENU_CALLBACK type, we can register the callback for this
  // path but not have the item show up in the menu; the admin is not allowed
  // to enable the item in the menu, either.
  // 
  // Notice that the 'page arguments' is an array of numbers. These will be
  // replaced with the corresponding parts of the menu path. In this case a 0
  // would be replaced by 'thaisearch', and 1 will be Thai words to search.
  // These will be passed as arguments to the thaisearch_thaisearch() function.
  
  $items['thaisearch/%'] = array(
    'title' => 'Thai Search',
    'page callback' => 'thaisearch_thaisearch',
    'page arguments' => array(1),
    'access arguments' => array('access thaisearch'),
//    'type' => MENU_CALLBACK,
  );
 
  $items['thaisearch'] = array( 
    'title' => 'Thai Search',
    'page callback' => 'thaisearch_view',
    'access arguments' => array('access thaisearch'),
    'type' => MENU_SUGGESTED_ITEM,
 
  );
 
  return $items;
}
 
/**
 * Form builder; Output a search form for the search block and the theme's search box.
 * 
 * @ingroup forms
 * @see search_box_form_submit()
 * @see theme_search_box_form()
 */
function thaisearch_form(&$form_state, $form_id, $size=25) {
  // Add CSS
  drupal_add_css(drupal_get_path('module', 'thaisearch') .'/thaisearch.css', 'module', 'all', FALSE);

  $form['keys'] = array(
    '#title' => t('Search Thai words'),
    '#type' => 'textfield',
    '#size' => $size,
    '#default_value' => '',
    '#attributes' => array('title' => t('Enter the terms you wish to search for.')),
  );
  $form['submit'] = array('#type' => 'submit', '#value' => t('Search'));
  $form['#submit'][] = 'thaisearch_form_submit';
  $form['#validate'][] = 'thaisearch_form_validate';

  return $form;
}

/**
 * Process a block search form submission.
 */
function thaisearch_form_submit($form, &$form_state) {
  $keys = $form['keys']['#value'];
  $form_state['redirect'] = 'thaisearch/'. $keys;
}

/**
 * Helper function for grabbing search keys.
 */
function thaisearch_get_keys() {
  static $return;
  if (!isset($return)) {
    // Extract keys as remainder of path
    // Note: support old GET format of searches for existing links.
    $path = explode('/', $_GET['q'], 2);
    $keys = empty($_REQUEST['keys']) ? '' : $_REQUEST['keys'];
    $return = count($path) == 2 ? $path[1] : $keys;
  }
  return $return;
}

/**
 * Menu callback; presents the search form and/or search results.
 */ 
function thaisearch_view() {
  // Search form submits with POST but redirects to GET. This way we can keep
  // the search query URL clean as a whistle:
  // search/type/keyword+keyword
  if (!isset($_POST['form_id'])) {
    
    $keys = thaisearch_get_keys(); 
    // Only perform search if there is non-whitespace search term:
    $results = '';
    if (trim($keys)) { 
      // Collect the search results:
      $results = thaisearch_thaisearch($keys);
  
      if ($results) {
        $results = theme('box', t('Search results'), $results);
      }
      else { 
        $results = theme('box', t('Your search yielded no results'), thaisearch_help('search#noresults', drupal_help_arg()));
      } 
    }
    // Construct the search form.
    $output = drupal_get_form('thaisearch_form', $keys);
    $output .= $results;
 
    return $output;
  }
  
  return drupal_get_form('thaisearch_form', empty($keys) ? '' : $keys);
}   
    
    
/** 
 * Helper function: is word in string 
 */
function _in_string($string, $word) {
  if (strpos($string, $word) === FALSE) { return FALSE; } else { return TRUE; }
}

/**
 * Helper function: get array of wordlist from words
 * 'a "b c" d' => ['a "b c" d', 'a', 'b c', 'd']
 */
function _get_wordlist($string) {
  $temp = trim($string);
  $wordlist = array();
  $wordlist[] = $string;
  while (strlen($temp) > 0) {
    $m = 9999;
    if (_in_string($temp, " ")) {
      $p1 = strpos($temp, " ");
      $m = ($m < $p1) ? $m : $p1;
    }
    if (_in_string($temp, '"')) {
      $p2 = strpos($temp, '"');
      $m = ($m < $p2) ? $m : $p2;
    }
    if (_in_string($temp, "'")) {
      $p3 = strpos($temp, "'");
      $m = ($m < $p3) ? $m : $p3;
    }
    if ($m == $p1) {
      $a = explode(" ",$temp,2);
      $wordlist[] = $a[0];
      $temp = trim($a[1]);
    } elseif ($m == $p2 and substr_count($temp, '"') > 1 and (_in_string($temp, '" ') or substr($temp, -1) == '"')) {
      $a = explode('"',$temp,3);
      $wordlist[] = $a[1];
      $temp = trim($a[2]);
    } elseif ($m == $p3 and substr_count($temp, "'") > 1 and (_in_string($temp, "' ") or substr($temp, -1) == "'")) {
      $a = explode("'",$temp,3);
      $wordlist[] = $a[1];
      $temp = trim($a[2]);
    } else {
      $wordlist[] = $temp;
      $temp = '';
    }
  }
  return($wordlist);
}
 
/**
 * Helper function for array_walk in thaisearch_except.
 */
function _thaisearch_excerpt_replace(&$text) {
  $text = preg_quote($text, '/');
}   
    
/**
 * Helper function for trim text
 */   
function _trim_text($text, $pos = 'first', $length = 50) {
  $l = drupal_strlen($text);
  if ($l > $length) {
    switch ($pos) {
      case 'first':
        return ' ...'.drupal_substr($text, $l-$length, $length);
      case 'middle':
        $b = intval($length / 2);
        return drupal_substr($text, 0, $b).'...'.drupal_substr($text, $l-$b, $b);
      case 'last': 
        return drupal_substr($text, 0, $length).'... ';
    } 
  }   
  return $text;
}

/**   
 * A more complex page callback that takes arguments.
 *  
 * The arguments are passed in from the page URL. The in our hook_menu
 * implementation we instructed the menu system to extract the last two
 * parameters of the path and pass them to this function as arguments.

 * do search from node_revision.title, node_revision.body and comments.comment

 */
function thaisearch_thaisearch($keys) {
 // Log the search keys:
 watchdog('thaisearchsearch', '%keys (@type).', array('%keys' => $keys, '@type' => module_invoke($type, 'thaisearch', 'name')), WATCHDOG_NOTICE, l(t('results'), 'thaisearch/'. $type .'/'. $keys));

  $max_item = 50;
  $wordlist = _get_wordlist($keys);

  $qarray = array();
  $arguments = array();

  for ($iwl = 0; $iwl < count($wordlist); $iwl++) {
    // first argument in full word: allscore*10
    switch ($iwl) {
      case 0:
        $w = 10;
      case 1:
        $w = 2;
      default:
        $w = 1;
    }
    // node.title: score*10
    $qarray[] = " SELECT ".strval($w*10)." AS score, n.nid, 0 AS cid, n.title, n.body, n.timestamp FROM {node_revisions} n WHERE n.title LIKE '%%%s%%' ";
    // node.body: score*2
    $qarray[] = " SELECT ".strval($w*2)." AS score, n.nid, 0 AS cid, n.title, n.body, n.timestamp FROM {node_revisions} n WHERE n.body LIKE '%%%s%%' ";
    // comments.comment: score*1
    $qarray[] = " SELECT ".strval($w*1)." AS score, c.nid, c.cid, n.title, c.comment, c.timestamp AS body FROM {comments} c INNER JOIN {node_revisions} n ON n.nid = c.nid WHERE c.comment LIKE '%%%s%%' ";

    $arguments = array_merge($arguments, array($wordlist[$iwl], $wordlist[$iwl], $wordlist[$iwl]));
  }

  $q = 'SELECT SUM(score) AS score, nid, cid, title, body, timestamp FROM ('.implode(' UNION ALL ',$qarray).') t GROUP BY t.nid, t.cid, t.title, t.body, t.timestamp ORDER BY score DESC, nid DESC, timestamp DESC';
 
  $sql_count = 'SELECT COUNT(*) AS num FROM ('.$q.') t';
  $count = db_fetch_object(db_query($sql_count, $arguments)); 
  $found = $count->num;
  
  $query = db_query_range($q, $arguments, 0, $max_item);
  
  // highlight words in trimmed output
  $dlist = '';
  $max_length= 50;
  while ($links = db_fetch_object($query)) {
    // highlight words
    $text = ' '. strip_tags(str_replace(array('<', '>'), array(' <', '> '), $links->body)) .' '; 
    array_walk($wordlist, '_thaisearch_excerpt_replace');
    $text = preg_replace('/('. implode('|', $wordlist) .')/iu', '<strong>\0</strong>', $text);

    // trim output
    $text_front = '';

    if (_in_string($text, '<strong>')) {
      $item_count = 0;
      $text_array = explode('<strong>', $text, 2);
      $text_front = _trim_text($text_array[0], 'first', $max_length).'<strong>';
      $text_array = explode('</strong>', $text_array[1], 2);
      $text_front .= $text_array[0].'</strong>';
      $text = $text_array[1];
      while (_in_string($text, '<strong>') and $item_count < 5) {
        $item_count++;
        $text_array = explode('<strong>', $text, 2);
        $text_front .= _trim_text($text_array[0], 'middle', $max_length).'<strong>';
        $text_array = explode('</strong>', $text_array[1], 2);
        $text_front .= $text_array[0].'</strong>';
        if (_in_string($text_array[1], '<strong>')) {
          $text = $text_array[1];
        } else {
          $text = _trim_text($text_array[1], 'last', $max_length);
        }
      }
      $text_front .=  _trim_text($text, 'last', $max_length);
    } else {
      $text_front = truncate_utf8($text, $max_length, TRUE);
    }
    $dlist .= '<dt><strong>'.l($links->title, 'node/'.$links->nid).'</strong></dt>';
    $dlist .= '<dd>'.$text_front.'</dd></strong><br />';
  }

  $output = thaisearch_help('thaisearch','').'<br />';
  $output .= drupal_get_form('thaisearch_form', $keys);
  $output .= t('Search words: <strong>'.$keys.'</strong>, found: <strong>'.$found.'</strong><br />');

  if ($dlist) {
    $output .= theme('box', t('Search results'), '<br /><dl>'.$dlist.'</dl>', 10, 0);
  } else {
    $output .= theme('box', t('Your search yielded no results'));
  }
  return $output;
}
?>

Topic: 

drupal6: ปรับปรุงมอดูล Thai Search 2

ผ่านไปนานแล้ว แต่มอดูล Search ของ drupal ก็ยังค้นภาษาไทยได้ไม่ดีขึ้นเลย จึงปรับปรุงของเก่าให้ใช้งานได้ดีขึ้นครับ

ปรับปรุง

  • เพิ่ม pager ให้สามารถแบ่งดูได้หลายหน้า
  • เพิ่มฟิลด์ timestamp ให้ทราบว่าโพสต์เมื่อไหร่

ดาวน์โหลด

ปรับปรุง

  • 20101113 ปรับการแสดงผลเล็กน้อย
Topic: 

drupal7: มอดูล Thai Search

ยังตามมาหลอกหลอนถึง Drupal 7

ปรับปรุง

  • โยกมาใส่ใน Drupal 7
  • เพิ่มการค้นหาใน comment.subject

ดาวน์โหลด

Topic: 

drupal: สร้างมอดูลเอง (Drupal 6 Module Tutorial)

เอามาจาก Creating modules - a tutorial: Drupal 6.x
พยายามเขียนให้เป็นเรื่องเป็นราว แต่ให้สั้น ๆ

ตามตัวอย่างเป็นการสร้างมอดูลชื่อ onthisdate เพื่อจะทำเป็นบล๊อกแสดง "วันนี้ในอดีตเมื่ออาทิตย์ก่อน"

ลิงก์ที่ต้องไป

Topic: 

01. เริ่มต้น (Getting Started)

ควรเขียนไว้ภายใต้ site/all/module/onthisdate เพื่อไม่ให้ปนกับของ Drupal เอง และปลอดภัยจากการอัปเกรด
$ cd /var/www/drupal
$ mkdir -p site/all/module/onthisdate

โครงสร้างชื่อฟังก์ชั่นในมอดูลจะเป็น

function {modulename}_{hook}

เช่น onthisdate_help เพื่อข่วยเหลือ หรือ onthisdate_menu เพื่อแสดงเมนู เป็นต้น

Topic: 

02. บอกว่ามอดูลเราจะทำอะไร (Telling Drupal about your module)

ต้องสร้างไฟล์ module_name.info สำหรับบอก Drupal ตัวอย่างนี้คือ
$ vi onthisdate.info

; $Id$
name = On this date
description = A block module that lists links to content such as blog entries or forum discussions that were created one week ago.
core = 6.x

ต้องมีหัวข้อดังนี้

name
บอกชื่อมอดูล
description
บอกคนอื่นให้รู้ว่ามอดูลทำอะไร สั้น ๆ 1 บรรทัด ถ้ามีอักขระแปลก ๆ ต้องเขียนด้วย HTML entities เพื่อให้แสดงผลในเว็บได้ เช่น
description = This is my &quot;crazy@email.com&quot; email address instead of description = This is my "crazy@email.com" email address
core
บอกให้รู้รุ่น Drupal เพราะเขาใช้เป็นตัวแยกว่าอันไหนจะเปิดให้ใช้งานบ้าง เวลาอัปเกรด

อาจมีหัวข้อดังนี้

ดีเพนเดนซี (dependencies)
บอกให้รู้ว่ามอดูลเราต้องการมอดูลไหนเป็นฐานบ้าง
    dependencies[] = taxonomy
    dependencies[] = comment
แพกเกจ (package)
บอกให้รู้ว่ามอดูลเรามีเพื่อนร่วมมอดูลอะไรบ้าง
package = "ชื่อมอดูลที่ร่วม"

ลองฟังก์ชั่นแรกคือ help
$ vi onthisdate.module

<?php
/**
* Display help and module information
* @param path which path of the site we're displaying help
* @param arg array that holds the current path as would be returned from arg() function
* @return help text for the path
*/
function onthisdate_help($path, $arg) {
  $output = '';
  switch ($path) {
    case "admin/help#onthisdate":
      $output = '<p>'.  t("Displays links to nodes created on this date") .'</p>';
      break;
  }
  return $output;
} // function onthisdate_help
?>

ตัวแปร $path แทนพาธว่า help ของเราจะไปอยู่ตรงไหน

ดูเพิ่มที่

Topic: 

03. มอดูลมีข้ออนุญาตอะไรบ้าง (Telling Drupal who can use your module)

เขียนชื่อฟังก์ชั่นในรูป hook_perm
รูปแบบฟังก์ชั่นคือ

<?php
function newmodule_perm() {
  return array('access newmodule', 'create newmodule', 'administer newmodule');
} / function newmodule_perm
?>

ตามตัวอย่างนี้คือ

<?php
/**
* Valid permissions for this module
* @return array An array of valid permissions for the onthisdate module
*/

function onthisdate_perm() {
  return array('access onthisdate content', 'administer onthisdate');
} // function onthisdate_perm
?>

ดูเพิ่มที่

Topic: 

04. ทำเรื่องบล๊อก (Declare we have block content)

ถ้ามอดูลเราทำบล๊อกด้วย เราต้องเขียนฟังก์ชั่นในรูป hook_block

ดังนี้

<?php
/**
* Generate HTML for the onthisdate block
* @param op the operation from the URL
* @param delta offset
* @returns block HTML
*/
function onthisdate_block($op='list', $delta=0) {
  // listing of blocks, such as on the admin/block page
  if ($op == "list") {
     $block[0]["info"] = t('On This Date');
     return $block;
  }
} // end onthisdate_block
?>

ตัวแปร $op เรียกว่า operation บอกว่าข้อมูลบล๊อกของเราอยู่ในรูปไหน ในที่นี้เป็น list
ตัวแปร $delta เรียกว่า offset บอกว่าระหว่างการแสดงผลเป็นบล๊อก หรือแสดงผลในรูปอื่นมีข้อแตกต่างกันหรือเปล่า

ดูเพิ่ม

Topic: 

05. สร้างเนื้อให้บล๊อก (Generate the block content)

มอดูลนี้ เราจะสร้างรายการของเนื้อหา (nodes) ของวันนี้ในสัปดาห์ก่อน เวลาเราจะดึงรายการมา เราดูจากเวลาที่เนื้อหาถูกสร้าง โดยเราทำในรูปวินาที (ดู php เรื่องเวลา)

<?php
/**
* Generate HTML for the onthisdate block
* @param op the operation from the URL
* @param delta offset
* @returns block HTML
*/
function onthisdate_block($op='list', $delta=0) {

  // listing of blocks, such as on the admin/block page
  if ($op == "list") {
    $block[0]["info"] = t('On This Date');
    return $block;
  } else if ($op == 'view') {

    // our block content
    // Get today's date
    $today = getdate();

    // calculate midnight one week ago
    $start_time = mktime(0, 0, 0,
                         $today['mon'], ($today['mday'] - 7), $today['year']);

    // we want items that occur only on the day in question, so  
    // calculate 1 day
    $end_time = $start_time + 86400; 
    // 60 * 60 * 24 = 86400 seconds in a day
  }

  //We'll use db_query() to get the records (i.e. the database rows) with our SQL query
  $result =  db_query("SELECT nid, title, created FROM {node} WHERE created >= '%s' AND created <= '%s'", $start_time, $end_time);

  // content variable that will be returned for display   
  $block_content = ''; 
  while ($links = db_fetch_object($result)) {
    $block_content .=  l($links->title, 'node/'. $links->nid) .'<br />';
  }

  // check to see if there was any content before setting up
  //  the block 
  if ($block_content == '') {   
    /* No content from a week ago.  If we return nothing, the block  
     * doesn't show, which is what we want. */
    return;
  }

  // set up the block 
  $block['subject'] = 'On This Date'; 
  $block['content'] = $block_content;
  return $block;
}
?>
  • สร้าง query ใช้ฟังก์ชั่น db_query() โดยให้ชื่อตารางในฐานข้อมูลอยู่ในรูป {node} ดูรายละเอียดจาก Table Prefix (and sharing tables across instances)
  • เวลาดึงจริง ใช้ฟังก์ชั่น db_fetch_object()
  • พอดึงมาปุ๊ป ก็สร้างรายการเป็นลิงก์ ด้วยฟังก์ชั่น l() ให้อยู่ในรูปของ <li><a href="node/nid">title</li>
Topic: 

06. ติดตั้ง เปิดใช้ ตั้งค่า และทดสอบ (Installing, enabling and testing the module)

ติดตั้ง
เอาไดเรคทอรี onthisdate (มีไฟล์ onthisdate.info และ onthisdate.module) ไปไว้ที่ sites/all/modules หรือ sites/hostname/modules
เปิดใช้
ผ่านเมนูคือ Administer » Site building » Modules หรือพิมพ์ตรง ๆ ใน URL ว่า admin/build/modules แล้วกาถูก
ตั้งค่า
มอดูลเราเป็นบล๊อก จึงต้องเปิดใช้งานบล๊อกผ่านเมนู Administer » Site building » Blocks หรือ URL ว่า admin/build/block
ทดสอบ
ดูจากตรงบล๊อกที่เราเพิ่งเปิดใช้ ถ้าวันนี้ในสัปดาห์ก่อนไม่มีเนื้อหาอะไร เราก็จะไม่เห็นอะไรเลย แต่ถ้ามีเนื้อหามากหลายหัวข้อ จะเห็นบล๊อกนี้ยาวเหยียด (ซึ่งเราจะไปปรับแต่งต่อไป)
Topic: 

07. เพิ่มส่วนของการตั้งค่า (Create a module configuration (settings) page)

ทำให้ตั้งค่าได้ด้วยฟังก์ชั่น onthisdate_admin
ทำเป็นฟอร์มโดยบรรจุอาเรย์ในรูปของ array( '#name => 'value', ... )
<?php
function onthisdate_admin() {

  $form['onthisdate_maxdisp'] = array(
    '#type' => 'textfield',
    '#title' => t('Maximum number of links'),
    '#default_value' => variable_get('onthisdate_maxdisp', 3),
    '#size' => 2,
    '#maxlength' => 2,
    '#description' => t("The maximum number of links to display in the block."),
    '#required' => TRUE,
  );

  return system_settings_form($form);
}
?>
  • ใช้ฟังก์ชั่น t() ในการแสดงผลอักขระ
  • ใช้ฟังก์ชั่น variable_get('variable_name',default_value) ในการรับค่าตัวแปรจากระบบ ซึ่งในที่นี้เรากำหนดค่าปริยายให้ onthisdate_maxdisp เป็น 3
  • ต้องคืนค่าให้ระบบด้วยฟังก์ชั่น system_settings_form()

เอาส่วนของการตั้งค่านี้ คือตัวเลข onthisdate_maxdisp ไปใส่ในฟังก์ชั่น onthisdate_block() ดังนี้

//--- onthisdate_block function ---
...
  $limitnum = variable_get("onthisdate_maxdisp", 3);

  $query = "SELECT nid, title, created FROM " .
           "{node} WHERE created >= %d " .
           "AND created <= %d";

  $queryResult = db_query_range($query, $start_time, $end_time, 0, $limitnum);
...


ทำให้เรียกใช้งานตั้งค่า ผ่านเมนูได้ด้วยฮุก onthisdate_menu
<?php
function onthisdate_menu() {

  $items = array();

  $items['admin/settings/onthisdate'] = array(
    'title' => 'On this date module settings',
    'description' => 'Description of your On this date settings control',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('onthisdate_admin'),
    'access arguments' => array('access administration pages'),
    'type' => MENU_NORMAL_ITEM,
   );

  return $items;
}
?>
กรองความถูกต้องของการป้อนข้อมูลด้วยฟังก์ชั่น onthisdate_admin_validate
<?php
function onthisdate_admin_validate($form, &$form_state) {
  $maxdisp = $form_state['values']['onthisdate_maxdisp'];
  if (!is_numeric($maxdisp)) {
    form_set_error('onthisdate_maxdisp', t('You must select a number for the maximum number of links.'));
  }
  else if ($maxdisp <= 0) {
    form_set_error('onthisdate_maxdisp', t('Maximum number of links must be positive.'));
  }
}
?>

ดูเพิ่ม

Topic: 

08. เพิ่มส่วนแสดงเนื้อหาในหน้าหลัก (Generate a page content)

โค๊ดหลักมีแต่การแสดงเนื้อในบล๊อกซึ่งมีเนื้อที่จำกัด คราวนี้เรามาเพิ่มให้แสดงเนื้อในหน้าหลักได้ โดยเราสามารถแสดงได้ไม่จำกัดจำนวนหัวข้อ

ทำผ่านฟังก์ชั่น onthisdate_all()

<?php
function onthisdate_all() {
  // content variable that will be returned for display
  $page_content = '';

  // Get today's date
  $today = getdate();

  // calculate midnight one week ago
  $start_time = mktime(0, 0, 0, $today['mon'], ($today['mday'] - 7), $today['year']);

  // we want items that occur only on the day in question,
  // so calculate 1 day
  $end_time = $start_time + 86400;
  // 60 * 60 * 24 = 86400 seconds in a day

  $query = "SELECT nid, title, created FROM " .
           "{node} WHERE created >= '%d' " .
           " AND created <= '%d'";

  // get the links (no range limit here)
  $queryResult =  db_query($query, $start_time, $end_time);
  while ($links = db_fetch_object($queryResult)) {
    $page_content .= l($links->title, 'node/'.$links->nid).'<br />';
  }

  // check to see if there was any content before
  // setting up the block
  if ($page_content == '') {
    // no content from a week ago, let the user know
    $page_content = "No events occurred on this site on this date in history.";
  }
  return $page_content;
}
?>
  • ฟังก์ชั่น onthisdate_all ไม่ใช่ฮุก ถ้ามอดูลอื่นจะเรียกใช้ ต้องเรียกผ่านฟังก์ชั่นระบบ module_invoke()
  • แต่ถ้าเราจะให้ฟังก์ชั่นของเราดูได้เฉพาะภายใน ต้องนำหน้าชื่อฟังก์ชั่นด้วยขีดเส้นใต้ _
Topic: 

09. บอกให้ Drupal รู้ถึงการทำงานใหม่ (Letting Drupal know about the new function)

ถ้าฟังก์ชั่นไหนของเราไม่ใช่ฮุก เราต้องบอกให้ Drupal รับรู้ถึงฟังก์ชั่นเราเสมอ
ทำได้ผ่านฮุก onthisdate_menu() โดยกลับไปแก้ไขงานจากคราวก่อน

<?php
function onthisdate_menu() {

  $items = array();

  //this was created earlier in tutorial 7.
  $items['admin/settings/onthisdate'] = array(
    'title' => 'On this date module settings',
    'description' => 'Description of your On this date settings control',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('onthisdate_admin'),
    'access arguments' => array('access administration pages'),
    'type' => MENU_NORMAL_ITEM,
   );

  //this is added for this current tutorial.
  $items['onthisdate'] = array(
    'title' => 'On this date',
    'page callback' => 'onthisdate_all',
    'access arguments' => array('access onthisdate content'),
    'type' => MENU_CALLBACK
  );

  return $items;
}
?>
  • เมื่อเรียกผ่าน URL ว่า onthisdate ฟังก์ชั่น onthisdate_all() จะทำงาน
  • ชนิด (type) ของเมนู คือ
    • MENU_NORMAL_ITEM เป็นเมนูปกติที่ผู้ใช้มองเห็น
    • MENU_CALLBACK ไม่แสดงในเมนูจริง ๆ แต่จะถูกเรียกใช้ผ่านทาง URL เท่านั้น

    ดูเพิ่ม

    Topic: 

    10. เพิ่ม 'more' ให้บล๊อก (Adding a 'more' link and showing all entries)

    เพิ่มลิงก์ 'more' หรือ 'มีต่อ' ในการแสดงหัวข้อเพิ่มเติมจากที่ล้นเนื้อบล๊อก
    กลับไปแก้งานในส่วนของ onthisdate_block

    <?php
    // add a more link to our page that displays all the links
      $block_content .=
        "<div class=\"more-link\">".
        l(
          t("more"),
          "onthisdate",
          array(
            "title" => t("More events on this day.")
          )
        )."</div>";
    ?>

    ดูเพิ่ม

    Topic: 

    11. จับมารวมกัน

    ได้ดังนี้
    $ cd /var/www/drupal
    $ mkdir -p sites/all/module/onthisdate

    ไฟล์ info
    $ vi onthisdate.info

    ; $Id$
    name = On this date
    description = A block module that lists links to content such as blog entries or forum discussions that were created one week ago.
    core = 6.x

    ไฟล์ module
    $ vi onthisdate.module

    <?php
    /**
    * Display help and module information
    * @param path which path of the site we're displaying help
    * @param arg array that holds the current path as would be returned from arg() function
    * @return help text for the path
    */
    function onthisdate_help($path, $arg) {
      $output = '';
      switch ($path) {
        case "admin/help#onthisdate":
          $output = '<p>'.  t("Displays links to nodes created on this date") .'</p>';
          break;
      }
      return $output;
    } // function onthisdate_help
    
    /**
    * Valid permissions for this module
    * @return array An array of valid permissions for the onthisdate module
    */
    function onthisdate_perm() {
      return array('access onthisdate content', 'administer onthisdate');
    } // function onthisdate_perm
    
    /**
    * Generate HTML for the onthisdate block
    * @param op the operation from the URL
    * @param delta offset
    * @returns block HTML
    */
    function onthisdate_block($op='list', $delta=0) {
      // listing of blocks, such as on the admin/block page
      if ($op == "list") {
        $block[0]["info"] = t('On This Date');
        return $block;
      } else if ($op == 'view') {
    
        // our block content
        // Get today's date
        $today = getdate();
    
        // calculate midnight one week ago
        $start_time = mktime(0, 0, 0,
                             $today['mon'], ($today['mday'] - 7), $today['year']);
    
        // we want items that occur only on the day in question, so  
        // calculate 1 day
        $end_time = $start_time + 86400; 
        // 60 * 60 * 24 = 86400 seconds in a day
      }
    
      //We'll use db_query() to get the records (i.e. the database rows) with our SQL query
      $limitnum = variable_get("onthisdate_maxdisp", 3);
    
      $query = "SELECT nid, title, created FROM " .
               "{node} WHERE created >= %d " .
               "AND created <= %d";
    
      $queryResult = db_query_range($query, $start_time, $end_time, 0, $limitnum);
    
      // content variable that will be returned for display   
      $block_content = '<ul>';
      while ($links = db_fetch_object($queryResult)) {
        $block_content .=  '<li>'.l($links->title, 'node/'. $links->nid) .'</li>';
      }
    
      // check to see if there was any content before setting up
      //  the block
      if ($block_content == '<ul>') {
        /* No content from a week ago.  If we return nothing, the block
         * doesn't show, which is what we want. */
        return;
      }
    
      $block_content .= '</ul>';
    
      // add a more link to our page that displays all the links
      $block_content .=
        "<div class=\"more-link\">".
        l(
          t("more"),
          "onthisdate",
          array(
            "title" => t("More events on this day.")
          )
        )."</div>";
    
      // set up the block 
      $block['subject'] = 'On This Date'; 
      $block['content'] = $block_content;
      return $block;
    } // end onthisdate_block
    
    function onthisdate_admin() {
      $form['onthisdate_maxdisp'] = array(
        '#type' => 'textfield',
        '#title' => t('Maximum number of links'),
        '#default_value' => variable_get('onthisdate_maxdisp', 3),
        '#size' => 2,
        '#maxlength' => 2,
        '#description' => t("The maximum number of links to display in the block."),
        '#required' => TRUE,
      );
    
      return system_settings_form($form);
    }
    
    function onthisdate_menu() {
      $items = array();
    
      //this was created earlier in tutorial 7.
      $items['admin/settings/onthisdate'] = array(
        'title' => 'On this date module settings',
        'description' => 'Description of your On this date settings control',
        'page callback' => 'drupal_get_form',
        'page arguments' => array('onthisdate_admin'),
        'access arguments' => array('access administration pages'),
        'type' => MENU_NORMAL_ITEM,
       );
    
      //this is added in tutorial 9.
      $items['onthisdate'] = array(
        'title' => 'On this date',
        'page callback' => 'onthisdate_all',
        'access arguments' => array('access onthisdate content'),
        'type' => MENU_CALLBACK,
      );
    
      return $items;
    }
    
    function onthisdate_admin_validate($form, &$form_state) {
      $maxdisp = $form_state['values']['onthisdate_maxdisp'];
      if (!is_numeric($maxdisp)) {
        form_set_error('onthisdate_maxdisp', t('You must select a number for the maximum number of links.'));
      }
      else if ($maxdisp <= 0) {
        form_set_error('onthisdate_maxdisp', t('Maximum number of links must be positive.'));
      }
    }
    
    function onthisdate_all() {
      // content variable that will be returned for display
      $page_content = '';
    
      // Get today's date
      $today = getdate();
    
      // calculate midnight one week ago
      $start_time = mktime(0, 0, 0, $today['mon'], ($today['mday'] - 7), $today['year']);
    
      // we want items that occur only on the day in question,
      // so calculate 1 day
      $end_time = $start_time + 86400;
      // 60 * 60 * 24 = 86400 seconds in a day
    
      $query = "SELECT nid, title, created FROM " .
               "{node} WHERE created >= '%d' " .
               " AND created <= '%d'";
    
      // get the links (no range limit here)
      $queryResult =  db_query($query, $start_time, $end_time);
      while ($links = db_fetch_object($queryResult)) {
        $page_content .= l($links->title, 'node/'.$links->nid).'<br />';
      }
    
      // check to see if there was any content before
      // setting up the block
      if ($page_content == '') {
        // no content from a week ago, let the user know
        $page_content = "No events occurred on this site on this date in history.";
      }
      return $page_content;
    }
    ?>
    

    ติดตั้ง
    ติดอยู่แล้ว

    เปิดใช้

    • URL admin/build/module กาถูกหน้า onthisdate
    • URL admin/user/permissions เลือกกาถูกข้ออนุญาตที่เกี่ยวข้องกับ onthisdate

    ตั้งค่า

    • URL admin/settings/onthisdate ตั้งจำนวนหัวข้อ
    • ทำเป็นบล๊อกผ่าน URL admin/build/block เลือกกาถูก On this date

    ทดสอบ
    ดูที่บล๊อก On this date ตามต้องการ

    Topic: 

    drupal6: ตัวอย่างการสร้าง node, page

    มีตัวอย่างการสร้าง node และ page แอบอยู่ที่ api.drupal.org คือ

    สามารถดูซอร์ส แล้วเอามาทดลองสร้างมอดูลเองได้

    Topic: 

    drupal6: แปลงมอดูล onthisdate เป็น recentweek

    จะแปลงมอดูลจากตัวอย่าง คือ On This Date ซึ่งดู "วันนี้ในอาทิตย์ก่อน" มาเป็น Recent Week คือดูหัวข้อใหม่ในสัปดาห์นี้ (เหมือนกับ tracker แต่ทำเป็นบล๊อกได้)

    เริ่มเลย
    $ cd /var/www/drupal/sites/all/modules
    $ cp -xa onthisdate recentweek
    $ cd recentweek
    $ mv onthisdate.info recentweek.info
    $ mv onthisdate.module recentweek.module
    $ sed -i "s/onthisdate/recentweek/g" *
    $ sed -i "s/on this date/recent week/g" *
    $ sed -i "s/On this date/Recent week/g" *
    $ sed -i "s/On This Date/Recent Week/g" *
    $ vi recentweek.module

    <?php
    /**
    * Display help and module information
    * @param path which path of the site we're displaying help
    * @param arg array that holds the current path as would be returned from arg() function
    * @return help text for the path
    */
    function recentweek_help($path, $arg) {
      $output = '';
      switch ($path) {
        case "admin/help#recentweek":
          $output = '<p>'.  t("Displays links to nodes created recent week") .'</p>';
          break;
      }
      return $output;
    } // function recentweek_help
    
    /**
    * Valid permissions for this module
    * @return array An array of valid permissions for the recentweek module
    */
    function recentweek_perm() {
      return array('access recentweek content', 'administer recentweek');
    } // function recentweek_perm
    
    /**
    * Generate HTML for the recentweek block
    * @param op the operation from the URL
    * @param delta offset
    * @returns block HTML
    */
    function recentweek_block($op='list', $delta=0) {
      // listing of blocks, such as on the admin/block page
      if ($op == "list") {
        $block[0]["info"] = t('Recent Week');
        return $block;
      } else if ($op == 'view') {
    
        // our block content
        // Get today's date
        $today = getdate();
    
        // calculate midnight one week ago
        $start_time = mktime(0, 0, 0,
                             $today['mon'], ($today['mday'] - 7), $today['year']);
    
    /* wd's mod
        // we want items that occur only on the day in question, so
        // calculate 1 day
        $end_time = $start_time + 86400;
        // 60 * 60 * 24 = 86400 seconds in a day
    */
        #wd#// delete parameter $end_time, $query changed
      }
    
      //We'll use db_query() to get the records (i.e. the database rows) with our SQL query
      $limitnum = variable_get("recentweek_maxdisp", 3);
    
    /* wd's mod
      $query = "SELECT nid, title, created FROM " .
               "{node} WHERE created >= %d " .
               "AND created <= %d";
      $queryResult = db_query_range($query, $start_time, $end_time, $limitnum);
    */
      #wd#// delete parameter $end_time, $query changed
      $query = "SELECT nid, title, created FROM " .
               "{node} WHERE created >= %d ORDER BY created DESC" ;
      $queryResult = db_query_range($query, $start_time, 0, $limitnum);
      
      // content variable that will be returned for display
      $block_content = '<ul>';
      while ($links = db_fetch_object($queryResult)) {
        $block_content .=  '<li>'.l($links->title, 'node/'. $links->nid) .'</li>';
      } 
    
      // check to see if there was any content before setting up
      //  the block
      if ($block_content == '<ul>') {
        /* No content from a week ago.  If we return nothing, the block
         * doesn't show, which is what we want. */
        return;
      } 
        
      $block_content .= '</ul>';
        
      // add a more link to our page that displays all the links
      $block_content .=
        "<div class=\"more-link\">".
        l(
          t("more"),
          "recentweek",
          array(
            "title" => t("More events on this day.")
          )
        )."</div>";
      
      // set up the block
      $block['subject'] = 'Recent Week'; 
      $block['content'] = $block_content;
      return $block;
    } // end recentweek_block
    
    function recentweek_admin() {
      $form['recentweek_maxdisp'] = array(
        '#type' => 'textfield',
        '#title' => t('Maximum number of links'),
        '#default_value' => variable_get('recentweek_maxdisp', 3),
        '#size' => 2,
        '#maxlength' => 2,
        '#description' => t("The maximum number of links to display in the block."),
        '#required' => TRUE,
      );
    
      return system_settings_form($form);
    }
    
    function recentweek_menu() {
      $items = array();
    
      //this was created earlier in tutorial 7.
      $items['admin/settings/recentweek'] = array(
        'title' => 'Recent week module settings',
        'description' => 'Description of your Recent week settings control',
        'page callback' => 'drupal_get_form',
        'page arguments' => array('recentweek_admin'),
        'access arguments' => array('access administration pages'),
        'type' => MENU_NORMAL_ITEM,
       );
    
      //this is added in tutorial 9.
      $items['recentweek'] = array(
        'title' => 'Recent week',
        'page callback' => 'recentweek_all',
        'access arguments' => array('access recentweek content'),
        'type' => MENU_CALLBACK,
      );
      
      return $items;
    }   
        
    function recentweek_admin_validate($form, &$form_state) {
      $maxdisp = $form_state['values']['recentweek_maxdisp'];
      if (!is_numeric($maxdisp)) {
        form_set_error('recentweek_maxdisp', t('You must select a number for the maximum number of links.'));
      }
      else if ($maxdisp <= 0) {
        form_set_error('recentweek_maxdisp', t('Maximum number of links must be positive.'));
      }
    } 
    
    function recentweek_all() {
      // content variable that will be returned for display
      $page_content = '';
        
      // Get today's date
      $today = getdate();
        
      // calculate midnight one week ago
      $start_time = mktime(0, 0, 0, $today['mon'], ($today['mday'] - 7), $today['year']);
      
    /* wd's
      // we want items that occur only on the day in question,
      // so calculate 1 day
      $end_time = $start_time + 86400;
      // 60 * 60 * 24 = 86400 seconds in a day
    
      #wd#// delete parameter $end_time, $query changed
      $query = "SELECT nid, title, created FROM " .
               "{node} WHERE created >= '%d' " .
               " AND created <= '%d'";
      $queryResult =  db_query($query, $start_time, $end_time);
    */
    
      $query = "SELECT nid, title, created FROM " .
               "{node} WHERE created >= '%d' ORDER BY created DESC";
    
      // get the links (no range limit here)
      $queryResult =  db_query($query, $start_time);
      $page_content = '<ul>';
       while ($links = db_fetch_object($queryResult)) {
        $page_content .= '<li>'.l($links->title, 'node/'.$links->nid).'</li>';
      }
    
      // check to see if there was any content before
      // setting up the block
      if ($page_content == '<ul>') {
        // no content from a week ago, let the user know
        $page_content = "No events occurred on this site recent week in history.";
      } else $page_content .= '</ul>';
      return $page_content;
    }
    ?>
    

    เสร็จแล้ว

    เพื่อความสุขสวัสดี อย่าลืมรัน update.php ด้วย ไม่งั้นอาจมีปัญหาเรื่อง HTTP request failed

    Topic: 

    drupal: แก้และแปลงโมดูล

    บันทึกแก้และแปลงโมดูล

    Topic: 

    drupal: บันทึกแก้โมดูล switch theme

    เพิ่งสังเกตุว่าโมดูล Switch Theme เขาไม่ยอมเปลี่ยนธีมสำหรับผู้ใช้ทั่วไป (anonymous users)

    ลองค้นดู bug ของ switchtheme พบ patch แก้ปัญหาไว้แล้ว แต่ยังไม่มีสำหรับ Drupal-6
    เลยทดลองแก้ไขดู ได้ความแพตช์ดังนี้ครับ

    $ patch -u -p switchtheme.module

    --- switchtheme.module  2007-08-31 21:56:14.000000000 +0700
    +++ switchtheme.module  2007-12-09 19:46:14.000000000 +0700
    @@ -148,11 +148,17 @@ function switchtheme_switch_form_submit(
       // save the setting in the db for logged in users
       // save the setting in the session for all others
       if ($user->uid > 0) {
    -    variable_set('theme_default', $form_values['custom_theme']);
    +    // FIX DEFAULT THEME CHANGED
    +    //variable_set('theme_default', $form_values['custom_theme']);
         if (user_save($user, array('theme' => $form_state['values']['custom_theme']))) {
           $user->theme = $form_state['values']['custom_theme'];
         }
       }
    +  // FIX ANONYMOUS SWITCH THEME
    +  elseif (user_access('switch theme')) {
    +    // save the setting in the variable for all others
    +    $_SESSION['custom_theme'] = $form_state['values']['custom_theme'];
    +  }
     }
    
    
    @@ -184,4 +190,4 @@ function switchtheme_select() {
       }
       asort($select);
       return $select;
    -}
    \ No newline at end of file
    +}
    
    Topic: 

    drupal: แปลงโมดูล On This Date เป็น Recent 3 Days

    ทดลองแปลงโมดูล On This Date ของ Drupal-5 Modules Tutorial

    โมดูลในรุ่น 5 ควรเขียนในไดเรกทอรี่ ./sites/all/modules/
    ดังนั้นต้องสร้างไดเรกทอรี่ให้โมดูลเราก่อน โดยใช้ชื่อ onthisdate เหมือนเดิม
    $ cd /var/www/drupal
    $ mkdir -p sites/all/modules/onthisdate
    $ cd sites/all/modules/onthisdate

    สำหรับรุ่น 5 ต้องมีไฟล์ info
    $ vi onthisdate.info

    ; $Id$
    name = On this date
    description = "A block module that lists links to content such as blog entries or forum discussions that were created one week ago. (wd's: Modify to recent 3 days post)"

    คราวนี้ก็สร้างไฟล์โมดูลจริง ๆ ได้แล้ว
    $ vi onthisdate.module

    <?php
    /**
     * @file: onthisdate.module.5.txt Drupal tutorial on how to write modules
     * @author: Kitt Hodsden (http://kitt.hodsden.org/)
     * @license: GPL:v2, CC:by-nc-sa, contact for commercial uses
     */
    
    /**
     * Display help and module information
     * @param section which section of the site we're displaying help 
     * @return help text for section
     */
    function onthisdate_help($section='') {
    
      $output = '';
    
      switch ($section) {
        case "admin/help#onthisdate":
          $output = '<p>'.  t("Displays links to nodes created on this date"). '</p>';
          break;
      }
    
      return $output;
    } // function onthisdate_help
    
    /**
     * menu hook
     * @return array of menu items
     */
    function onthisdate_menu() {
      
      $items = array();
      
      //this was created earlier in tutorial 7.
      $items[] = array(
        'path' => 'admin/settings/onthisdate',
        'title' => t('On this date module settings'),
        'callback' => 'drupal_get_form',
        'callback arguments' => 'onthisdate_admin',
        'access' => user_access('access administration pages'),
        'type' => MENU_NORMAL_ITEM,
       );
    
      //this is added for this current tutorial.
      $items[] = array(
        'path' => 'onthisdate',
        'title' => t('on this date'),
        'callback' => 'onthisdate_all',
        'access' => user_access('access content'),
        'type' => MENU_CALLBACK
      );
    
      return $items;
    }
    
    function onthisdate_admin() {
    
      $form['onthisdate_maxdisp'] = array(
        '#type' => 'textfield',
        '#title' => t('Maximum number of links'),
        '#default_value' => variable_get('onthisdate_maxdisp', 3),
        '#size' => 2,
        '#maxlength' => 2,
        '#description' => t("The maximum number of links to display in the block.")
      );
    
      return system_settings_form($form);
    }
    
    
    /**
     * Valid permissions for this module
     * @return An array of valid permissions for the onthisdate module
     */
    function onthisdate_perm() {
      
      return array('access onthisdate', 'administer onthisdate');
      
    } // function onthisdate_perm()
    
    /**
    * Generate HTML for the onthisdate block
    * @param op the operation from the URL
    * @param delta offset
    * @returns block HTML
    */
    function onthisdate_block($op='list', $delta=0) {
      // listing of blocks, such as on the admin/block page
      if ($op == "list") {
        $block[0]["info"] = t("On This Date");
        return $block;
      } else if ($op == 'view') {
      // our block content
        // content variable that will be returned for display
        $block_content = '';
    
        // Get today's date
        $today = getdate();
    
        // calculate midnight one week ago
        //$start_time = mktime(0, 0, 0,$today['mon'],
        //                           ($today['mday'] - 7), $today['year']);
        //wd's: MODIFY TO LAST 3 DAYS 
        $start_time = mktime(0, 0, 0,$today['mon'],
                                   ($today['mday'] - 3) , $today['year']);
    
        // we want items that occur only on the day in question, so
        //calculate 1 day
        //$end_time = $start_time + 86400;
        // 60 * 60 * 24 = 86400 seconds in a day
        //wd's EXPAND TO 3 DAYS
        $end_time = $start_time + 259200;
    
        $limitnum = variable_get("onthisdate_maxdisp", 3);
    
        $query = "SELECT nid, title, created FROM " .
               "{node} WHERE created >= %d " .
               "AND created <= %d ORDER BY created DESC";
    
        // get the links
        $queryResult = db_query_range($query, $start_time, $end_time, 0, $limitnum);
    
        while ($links = db_fetch_object($queryResult)) {
          //wd's: MODIFY TO LIST
          //$block_content .= l($links->title, 'node/'.$links->nid) . '<br />';
          $block_content .= '<li>'.l($links->title, 'node/'.$links->nid) . '</li>';
        }
        // check to see if there was any content before setting up the block
        if ($block_content == '') {
          // no content from a week ago, return nothing.
          return;
        } else {
          //wd's: ADD <ul>
          $block_content = '<ul>'.$block_content.'</ul>'; 
        }
        // add a more link to our page that displays all the links
        $block_content .=
          "<div class=\"more-link\">".
          l( t("more"), "onthisdate", array( "title" => t("More events on this day.")))."</div>";
        // set up the block
        //$block['subject'] = 'On This Date';
        //wd's EXPAND TO 3 DAYS
        $block['subject'] = 'These 3 Days';
        $block['content'] = $block_content;
        return $block;
      }
    } // end onthisdate_block
    
    /**
     * Settings for the onthisdate module
     * @return form contents for this module
     */
    function onthisdate_settings() {
    
      // only administrators can access this function
      if (!user_access('access administration pages')) {
        return message_access();
      }
      
      // 4.7 forms API
      $form['onthisdate_maxdisp'] = array('#type' => 'textfield',
                        '#title' => t('Maximum number of links'),
                        '#default_value' => variable_get('onthisdate_maxdisp', 3),
                        '#description' => t("The maximum number  of links to display in the block."),           
                        '#maxlength' => '2', '#size' => '2');
      return $form;
    
    }
    
    /**
     * Render a page listing links to all the content from a week ago
     */
    function onthisdate_all() {
      
      // content variable that will be returned for display
      $page_content = '';
    
      // Get today's date
      $today = getdate();
    
      // calculate midnight one week ago
      //$start_time = mktime(0, 0, 0, $today['mon'], ($today['mday'] - 7), $today['year']);
      //wd's: CHANGE TO LAST 3 DAYS
      $start_time = mktime(0, 0, 0, $today['mon'], ($today['mday'] - 3), $today['year']);
      
      // we want items that occur only on the day in question,
      // so calculate 1 day
      //$end_time = $start_time + 86400;
      // 60 * 60 * 24 = 86400 seconds in a day
      //wd's EXPAND TO 3 DAYS
      $end_time = $start_time + 259200;
      
      $query = "SELECT nid, title, created FROM " .
               "{node} WHERE created >= '" . $start_time .
               "' AND created <= '". $end_time . " ORDER BY created DESC'";
    
      // get the links (no range limit here)
      $queryResult = db_query($query);
      while ($links = db_fetch_object($queryResult)) {
        //$page_content .= l($links->title, 'node/'.$links->nid).'<br />';
        //wd's: ADD LIST
        $page_content .= '<li>'.l($links->title, 'node/'.$links->nid).'</li>';
      }
    
      // check to see if there was any content before
      // setting up the block
      if ($page_content == '') {
        // no content from a week ago, let the user know
        $page_content = "No events occurred on this site on this date in history.";
      } else {
        //wd's: ADD <ul>
        $page_content = '<ul>'.$page_content.'</ul>';
      }
      print theme("page", $page_content);
    }
    
    ?>

    เสร็จแล้ว
    ต่อไปก็ทำตามขั้นตอนคือ

    • ให้โมดูลทำงาน
      admin/build/module -> enable onthisdate
    • ให้ผู้ใช้สามารถใช้งานโมดูลได้
      admin/user/access -> เปิดให้ทำงานตาม roles
    • เปิดให้ใช้งานบล๊อก
      admin/build/block -> enable onthisdate
    Topic: