Login
  Days of Liris

2007/02/20

TurboGearsで全文検索

TurboGearsで全文検索するにはどうすればいいでしょうか?というお話。ここでは、SQLObjectに限定して、全文検索はHyperEstraierです。

全文検索するときに、コラムごとにインデックスを張って、検索した結果をモデルクラスのオブジェクトにする方法もありますが、めんどいのでもう少し単純化します。

モデルは、こんな感じ

class MyModel(SQLObject):
    
title = UnicodeCol()
    
body = UnicodeCol()

という物です。

まずは、インデックスへの登録です。データを保存するときにコントローラの中で明示的にindexingするのもありですが、もし、複数の経路からデータを保存しないといけない場合は、コードが重複して嫌です。なので、MyModelクラスをいじります。データを保存するときやデータの更新時(僕はデータの更新は、MyModel.setを使っているのでそれ以外は動かないかも)に自動でやってほしいです。なので、setをホゲホゲします。こんな感じ。

import hyperestraier
node = hyperestraier.Node()
node.set_url("http://localhost:1978/node/mynode")
node.set_auth("admin", "admin")

class MyModel(SQLObject):
    title = UnicodeCol()
    body = UnicodeCol()

    def set(self, **kwd):
        super(MyModel, self).set(**kwd)
        # indexへ登録する文書の準備
        doc = hyperestraier.Document()
        doc.set_attr("@uri", "MyModel://" + str(self.id))
        doc.set_attr("@title", self.title)
        for line in self.body.splitlines():
            doc.add_text(line)
        node.put_doc(node)

と言う感じです。データをセットしたあとに、インデックスへ登録しています。

それじゃ、検索はどうするじゃ、と言うことで、それは、

class MyController(Controller):
    
@expose(template="myproj.templates.searchresult")
    
def search(self, keyword):
        
cond = hyperestraier.Condition()
        
cond.set_phrase(keyword)
        
sres = node.search(cond)
        
return dict(searchResult = sres.docs)

という感じにして、あとは、テンプレートのお仕事。@uriの属性を直接WebのURLにしてもよいし、今回のようにSQLObjectのidにしてテンプレートの中でURLの組み立てを頑張ってもいい。

あと、@paginate("searchResult")をしてあげればPaged Listになりますた。


2007/02/07

captcha widgetの作り方(適当編)

Tag: turbogears

10分で作るCaptchaの作り方です。Captchaが何なのか、と言うのは、すでに知っていると言う前提で。

作り方としては、CaptchaWidgetクラスを定義して、テンプレートを書くプロセスと、バリデーションクラスを作るプロセスの二つです。多分、このサイトで使っているものと、ほぼ同じ物になるはず。

Captchaは、 http://captcha.net/ のものを使います。これには、画像の物とオーディオ形式の2種類がありますが、日本人にはオーディオ形式はちょっとね。さて、どちらもユーザ登録して、ユーザIDとパスワードをもらいます。Widget内で直接ユーザIDとパスワードを管理してもいいのですが、何となくいやん。というか、バリデータと共通で使うところがあるので、分けています。なので、ロジックが入りそうなところはCaptchaConfigというクラスを作って、その人にやってもらうことにします。そのクラスがこんな感じ

alphabet = "abcdefghijklmnopqrstuvwxyz"
letters = alphabet + alphabet.upper () + "0123456789"

class CaptchaConfig:
    
def __init__(self, captcha_username, captcha_password,
                 
captcha_width = 240,
                 
captcha_height = 80,
                 
base_url = 'http://image.captchas.net',
                 
captcha_letters = 6):
        
self.base_url = base_url
        
self.captcha_alphabet = alphabet
        
self.captcha_letters = captcha_letters
        
self.captcha_username = captcha_username
        
self.captcha_password = captcha_password
        
self.captcha_width = captcha_width
        
self.captcha_height = captcha_height

    
def getCaptchaCode(self):
        
random_string = ''
        
for i in range(50):
          
random_string += random.choice(letters)
        
return random_string

    
def getGeneratedCaptchaCode(self, random_string):
        
code = self.captcha_password + random_string
        
if self.captcha_alphabet != alphabet or self.captcha_letters != 6:
            
code += ':' + self.captcha_alphabet + ':' + str(self.captcha_letters)

        
code_md5 = md5.new(code).digest()
        
generated_code = ''
        
for p in range(self.captcha_letters):
            
n = ord(code_md5[p]) % len(self.captcha_alphabet)
            
generated_code += self.captcha_alphabet[n]

        
return generated_code

    
def getImageLink(self, random_string):
        
return "%s?client=%s&random=%s&alphabet=%s&letters=%s&width=%s&height=%s" % (
                     
self.base_url, self.captcha_username,
                     
random_string, self.captcha_alphabet,
                     
self.captcha_letters, self.captcha_width,
                     
self.captcha_height)

もともとが PloneCaptchaベースです。次にWidget本体です。

class CaptchaWidget(CompoundFormField):
    
validator = None
    
template = "myproj.templates.captcha"
    
params = ["attrs", "captchaConfig", "captcjaCode"]
    
member_widgets = ["text_field"]
    
attrs = {}
    
width = "240px"
    
height = "80px"
    
captcha_code = ""
    
text_field = TextField(name="text")

    
def display(self, value, **params):
        
params["captchaCode"] = self.captchaConfig.getCaptchaCode()
        
return super(CaptchaWidget, self).display(value, **params)

とこれだけです。Widgetの継承元は、CompoundFormFieldで、一つのWidgetの中に複数のフィールドを含めたい場合などはこれを使うらしい。ここでは、text_fieldというテキストの入力フィールドがあります。テンプレートの中に隠しフィールドが直接あって、これをウィジェットでやると嬉しくないことがあるので、そうなっています。

displayの振る舞いを変えちゃうのはどうなのかな?と言う気もしますが、ここでは、そうなっています。インスタンス化するときにCaptchaConfigのオブジェクトが必要です。mustなんですが、めんどくさいので、上のコードはプログラマの責任で、と言うスタンス。

次にテンプレート。

<div xmlns:py="http://purl.org/kid/ns#"
  
class="${field_class}"
  
id="${field_id}"
  
py:attrs="attrs">
  
<img width="${captchaConfig.captcha_width}"
       
height="${captchaConfig.captcha_height}"
       
src="${captchaConfig.getImageLink(captchaCode)}"
       
alt="Captcha Image" />
  
<input type="hidden" name="${name}.hidden" value="${captchaCode}"/>
  ${text_field.display("", **params_for(text_field))}
</div>

と言う感じ。最初のネームスペースの宣言とか、class,id、属性の指定はそういう物だと思ってください。ウィジェットの初期化のときにしていしたcaptchaConfigとか、displayで指定したcaptchaCodeとかは魔法により使えます。displayを変更せずにtemplateの中でcaptchaCodeを作ると言うのもあり。

これでウィジェットは完成。次は、バリデータ。CompoundFormFieldは普通のバリデータが使えない(使えるんだったら教えてください)ので、とりあえずは、FormValidatorを拡張して、複数のフィールドにまたがってバリデートするようなやつを使います。

class CaptchaValidator(FormValidator):
    
messages = {"notMatch": "not match"}
    
validate_partial_form = True

    
def __init__(self, captchaConfig, fieldId):
        
self.captchaConfig = captchaConfig
        
self.fieldId = fieldId
        
super(CaptchaValidator, self).__init__()

    
def validate_partial(self, field_dict, state):
        
field_names = (self.fieldId + ".hidden", self.fieldId + ".text")
        
for name in field_names:
            
if not field_dict.has_key(name):
                
return
        
self.validate_python(field_dict, state)

    
def validate_python(self, field_dict, state):
        
code_generated = self.captchaConfig.getGeneratedCaptchaCode(field_dict[self.fieldId]["hidden"])
        
if field_dict[self.fieldId]["text"] != code_generated:
            
raise Invalid("notMatch", field_dict, state,
                          
error_dict={self.fieldId: "No Match"})

validate_pythonだけで動くはずなですが、パスワードをバリデートするやつがvalidate_partialも定義していたのでそれに習っています。CompoundFormFieldだと、各フィールドはfieldid.textとか、fieldid.hiddenとか言う感じでポストされて、小人さんがそれらを辞書にしてくれます。バリデータなのにWidgetの知識が必要なのは気に入らないと言う人は、適当に直してみてね。

で、あとは、使うだけ。CompoundFormFieldなので、スキーマにcaptcha = CaptchaValidator(capcha_config, "captcha")とはかけなくって、chained_validators = [CaptchaValidator(captcha_config, "captcha")]のようににしてください。


2007/01/31

captcha widgetを作ってみる(まだ途中かもしれない)

Captcha Widgetを作ってみました。まだ、途中だけど。作成途中の 例のサイト でブログのエントリを表示したら見えるでしょう。作成途中と言うのは、validatorがよくわからない。CaptchaWidgetはCompoundFormFieldなんですが、こやつには普通のvalidatorをセットできないのです。AutoCompleteFieldもCompoundFormFieldでこいつにvalidatorを指定できない。chained_validatorsだと大丈夫なんですが、ちょっと負けた気分かも。もう少し別のやり方があるのかな?

とりあえずはそれなりに動いているっぽい。ローカルだけど。