機能としては簡単だけど名前の付け方で悩んでいる。どうでもいいことかも知れないけど数日悩んでいる。

ブログ(Lokka)に記事のドラフト機能を追加したい。つまりデータとしては存在するけどおもてには表示しない記事。そこで下記のようにフラグを用意し、その一覧をとってくるメソッドを用意する。

class Post
  property :title, String
  property :draft, Boolean

  def self.draft # 重複!
    all(:draft => true)
  end

  def self.not_draft
    all(:draft => false)
  end
end
>> Post.draft
>> Post.not_draft.all(:title.like => '%foo%')

draftがフラグ名だとメソッド名とかぶるので使えない。この例はDataMapperだけどActiveRecordのnamed_scopeでも同様だ。

こういう場合、今までの俺は、

フラグ名:publish
メソッド名:published, unpublished

みたいな名前を付けてたんだけど、draftというフラグ名を見てこっちの方が具体的な名前だからいいなと思った。できればdraftを採用したい。publishは動詞でdraftは名詞だから上手くいかないのかな。こういうモノを作る場合のRuby/Rails界隈の簡単ルールみたいなものがあったら知りたい・・・。

Railsを追いきれる自信が無かったから。Rails文化に引っ張られてアプリが一生完成しない気がしたから。あとアプリとしては問題無いのにベースのRailsのバージョンが低いだけで残念っぽくなってるアプリ(Redmineとか)を見たから。

半年やってみてSinatra面倒クセー!っていっぱいあったけど、(Sinatra本体の)ソースが短いので完全把握できる掌握感は独自のOSS作る上で心強かった。

public/theme/(Mac版の場合はLokka.app/Contents/Resources/public/theme/) ディレクトリにテーマ名でディレクトリを作ります。

既存のテーマのディレクトリをコピーして修正しても構いませんが、ゼロからexampleというテーマ作ってみます。

テーマディレクトリの作成

exampleディレクトリの作成

theme

テーマのディレクトリの中にテンプレートファイルを作成します。テンプレートの形式は複数から選べますが、一番簡単なerbで作ってみます。

Lokkaのテーマに最低限必要なテンプレートは下記の二つだけです。

  • entry.erb --- 個別(Individual)ページのテンプレート
  • entries.erb --- 一覧(List)ページのテンプレート

個別ページのテンプレートの作成

まずは個別ページのテンプレートを書いてみます。

entry.erb — untitled

exampleテーマを使うように設定します。themeディレクトリにexampleディレクトリを作成した時点でテーマが認識されているので、管理画面の"テーマ"から"example"のテーマを選択します。

Test Site - Lokka

そして、個別ページを見てみましょう。(最初から1エントリーある筈なので http://localhost:9646/1 にアクセスします。)

Example Individual Page

単なるHTMLですが、ちゃんと表示されました。

しかし、静的なHTMLを表示するだけではつまらないのでサイトのタイトルを表示してみましょう。

entry.erb — untitled

h1タグの部分にサイトのタイトルを表示するように変更しました。<% と %>を囲うのがerb形式のテンプレートの記法です。<%= %>のように=が最初に付くと内容を表示するという意味になります。(これはphp/wordpressの<?= ?>と同じです。)

また http://localhost:9646/1 にアクセスして確認してみましょう。

Example Individual Page

サイトのタイトルが表示されました。この内容は管理画面の"設定" > "タイトル" で変更することが出来ます。同じようにサイトの詳細も <%= @site.description %>と書けば表示されます。

今度は個別ページなので、エントリーの内容を表示してみましょう。

entry.erb — untitled

http://localhost:9646/1 というURLからわかるように、IDが1のエントリーを表示するためのテンプレート内容になっています。

上記URLから判断して、IDが1のエントリーが@entryという変数の中に自動的に入っているので、@entry.titleや@entry.bodyでそれぞれエントリーのタイトルと本文が取り出せます。

Example Individual Page

表示されました。

一覧ページのテンプレートの作成

一覧ページのテンプレートを作る前に、エントリーが一個だけでは一覧になっているかわからないので管理画面の"投稿" > "登録" から幾つか適当に投稿(Post)を登録しておきます。

entries.erbというファイル名で一覧のページのテンプレートを作成します。

entries.erb — untitled

一覧は個別より少し複雑です。<% @posts.each do |post| %>から<% end %>の書き方に注目して下さい。do ~ endは一組になっていて、@posts.each do ~ end は@postsの個数分だけdo ~ endを繰り返すという構文です。投稿(Post)が3個あれば3回繰り返されます。

結果は下記のように表示されます。

Example List Page

画像やCSS等の外部リソースの使い方

画像やCSS、Javascriptといった外部リソースはテーマフォルダに含めることができます。

例えば、example/logo.png は <img src="/theme/example/logo.png"> のようにテンプレート中で指定できます。下記のようにディレクトリを作っても構いません。

example/
 images/
  logo.png
 stylesheets/
  style.css
 javascripts/
  jquery.js

また、テーマまでのパスは下記のように書くこともできます。

<img src="<%= @theme.path %>/logo.png" />

テーマ作成に最低限の知識はこれだけです。Lokkaにはその他に柔軟なテンプレートタイプとテーマAPIを持っていますが、それは別のエントリーで紹介したいと思います。

Lokka 0.2.0をリリースしました。

Lokka 0.2.0 Released - Lokka

Screen shot 2011-01-30 at 22.52.33

今回からMac版のLokka.appの配布を始めました。

MacのSnow Leopard(多分Leopardも)であれば、真っさらの状態でも、zipを解凍して、ダブルクリックし、http://localhost:9646/ にブラウザからアクセスすればCMSが使えます。

XcodeやMac PortsやTerminalが必要無いのでデザイナーの方でも簡単に使えるようになったと思います。(Lokka.appを右クリックして"パッケージの内容を表示"すればテーマなどの編集が出来ます。)

アプリケーション

LokkaのテーマにSlim使えるようにしました。index.slimみたいな。

HTMLのescapeがデフォルトだったりして実践で使ってみると予想してたよりHamlとの違いがありますね。

アイコン出来たらMac版をリリースしてlokka.orgにもドキュメント書こうと思います。

まだコミットしては無いんですが、LokkaのMac用ビルドを作ってみました。Dropboxに置いてあるので、Macをお持ちの方は記をダウンロード・解凍して起動し、http://localhost:9646/ でLokkaが動いてるかどうか試していただけるとありがたいです。

http://dl.dropbox.com/u/188423/Lokka.zip

特にXcodeをインストールしてない方の情報が知りたい!動いた!とか動かねえ!とかあったら@komagataや#lokkacmsにメッセージいただけるとうれしいです。

多分Snow Leopard、あわよくばLeopardで動くかもしれません。

Snippet

LokkaにSnippet(スニペット)を追加しました。

Snippetとはテンプレート間で共有したい部品を入れておくものです。Pageも同じ用途に使えるのでWordPressではそれが常套手段っぽいですが、@machidaさんが

「PageはURLが存在するので直接アクセスできるのが気持ち悪い」

と言っていたのでSnippetという名前で別に用意しました。

komagata [p0t]

このブログで言えば、右のプロフィールの部分でSnipetを使っています。今まではPageの機能を使っていたので、プロフィール単体がURLを持っているので妙なページが存在することになっていました。

Snippetは好きに名前を付けられるのでテンプレ内では下記のようにかけます。

<%= Snippet.first(:name => 'about').body %>

モデルのユーティリティメソッド

今回、各モデル用にユーティリティメソッドを用意しました。そのユーティリティメソッドとは"そのクラスで一番使いそうなメソッドをクラス名と同名のメソッドとして用意したもの"です。それを使えば上記は下記のように書けます。

<%= Snippet('about').body %>

要はこれです。

Ruby - クラス名と同名の関数にデフォルト動作をさせるパターン - komagata [p0t]

同じように他のモデルにも用意されています。

<h2><%= Page('inquiry').title %></h2>

DataMapperのAPIを使えば何でも取れますが、テンプレートがスッキリするかなと。

SPAMで大変なことになっていたこのブログのコメントを@yagi_さんのAkismetプラグインをきっかけに整理してみた。とりあえず既存のSPAMを消すが大変だった。そもそもSPAM判定されようがされまいが、コメントがDBに投稿される数が多過ぎるので、

%form
  :javascript
    document.write('<input type="hidden" name="check" value="check" />')

と入れて、checkという値が無かったら404を返すようにした。SPAM BOTがJavascript を理解するかどうかというただそれだけだけど、今までは1日に数百個コメントSPAMが来るとかザラだったので大分違う気がします。何か他に簡単で99%ぐらいのスパムを弾ける方法があればいいなと思います。

Conditional Tagの追加

テーマを書くときに、今どのページなのか(カテゴリー別ページなのか、個別ページなのか等)を判断する関数をWordPressではConditional Tagと言うらしいです。@machidaさんからの要望で、個別ページ(entry?)の中でも投稿(Post)なのかページ(Page)なのかが一発で判断できるConditional Tagが欲しいと言っていたので追加しました。

%w[index search category tag yearly monthly daily post page entry entries].each do |name|                                                                     
  define_method("#{name}?") do    
    @theme_types.include?(name.to_sym)
  end
end

post?とpage?というhelperを追加しただけです。

日本語のtruncate

何文字以上だったら"xxxxxxxxxxxxx..."みたいに縮めるtruncateが日本語文字列の途中でぶった切ってしまうのを直しました。微妙に気になってたんです。

def truncate(text, options = {})                                                                                                                              
  options = {:length => 30, :ommision => '...'}.merge(options)
  if options[:length] < text.split(//u).size
    text.split(//u)[0, options[:length]].to_s + options[:ommision]
  else
    text
  end
end

split(//u)で配列にして文字数を判断するようにしました。何か場当たり的な感じで怒られそうだけど・・・。

DataMapperを教えておじいさん | Selfkleptomaniacに回答しようとしたら、既に解決されていた・・・。

require 'rubygems'
require 'dm-core'
require 'dm-migrations'

class Repo
  include DataMapper::Resource
  property :id, Serial
  property :body, String
end

DataMapper.logger.set_log STDERR, :debug, "SQL: ", true
DataMapper.setup(:default, 'sqlite::memory:')
Repo.auto_migrate!                                                                                                                                                

puts Repo.all(:body.like => '%hoge%') |
     Repo.all(:body.like => '%fuga%') |
     Repo.all(:body.like => '%piyo%')

結果:

SQL: (0.000045) SELECT sqlite_version(*)
SQL: (0.000082) DROP TABLE IF EXISTS "repos"
SQL: (0.000014) PRAGMA table_info("repos")
SQL: (0.000393) CREATE TABLE "repos" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "body" VARCHAR(50))
SQL: (0.000079) SELECT "id", "body" FROM "repos" WHERE ("body" LIKE '%piyo%' OR "body" LIKE '%hoge%' OR "body" LIKE '%fuga%') ORDER BY "id"

検索条件の場合、分かりやすく書くとこういう感じでしょうか。検索条件の論理演算がRubyの演算子で出来るのが面白いですね。しかし恐ろしく遅そうなSQLが。

Lokkaでもしちゃんと検索をやるならばLuceneのRuby版であるFerretdm-ferret-adapterで使うのが楽そうです。datamapperのadapterになってるということはindexの置き場所も何でもいいというフリーダムさ。検索式を解析するFerret::QueryParserが日本語対応してるかわからないので面倒かもしれません。

LokkaにAkismetプラグイン来た!これで勝つる!@yagi_さんありがとうございます。

プラグインはみんなlokka-xxxxというgithubリポジトリになってるのでsubmoduleとして本体に取り込んでみました。

$ cd $LOKKA_ROOT
$ git submodule add git://github.com/yagitoshiro/lokka-akisment.git public/plugin/lokka-akismet

lokka本体をcloneした時は

$ git submodule init
$ git submodule update

git submodule initすると.git/configに下記みたいな設定が増える。

[submodule "public/plugin/lokka-akismet"]
        url = git://github.com/yagitoshiro/lokka-akismet.git

submoduleの中がmasterブランチになってないのでmasterにする。

$ cd public/plugin/lokka-akismet
$ git checkout master

しないといけないのは若干敷居が高いけど、リリースするzipには含めるからまあいいかなあ。