ラベル activerecord の投稿を表示しています。 すべての投稿を表示
ラベル activerecord の投稿を表示しています。 すべての投稿を表示

2012/07/15

RedmineとActiveRecordで番組チケット作成

プランとしてはこんな感じで
  1. 各局のEPGデータ取得のため60秒ずつ録画
  2. egpdumpでxmlに変換
  3. ActiveRecordでxmlからredmineチケットを作成
EPG取得用TS録画からxml変換は簡単
$ recfsusb2n -b [channel] 60 [channel].ts
$ epgdump [channel].ts [channel].xml
こんなで十分。これを必要なチャンネル分実行すればいい。

チケット(Issue)作成
ここが試行錯誤した部分。前記事でも書いたがActiveResourceでやろうと思ったけど思うように進まないので、ActiveRecordでやることにした。


番組データをチケットで表現

  • タイトル=>チケット題名
  • 内容=>チケット内容
  • 録画日=>開始日
  • カテゴリ=>チケットカテゴリ
  • 録画時間長さ=>予定工数(ほんとは時間だけど分のまま扱う)

カスタムフィールド(redmineは至る所にカスタムフィールドを追加できる)

  • 録画開始時間(数値)
  • チャンネル(文字列)
  • ジョブ番号(数値):録画ジョブ管理番号

こんな感じで対応させようと思う。


結果から書くと、こんな感じのスクリプトになった。
#!/usr/bin/ruby
# -*- coding: utf-8 -*-
require 'rubygems'
require 'active_record'
require 'active_support/core_ext'

ActiveRecord::Base.establish_connection(
                                        :adapter => 'mysql2',
                                        :host => 'localhost',
                                        :username => 'redmine',
                                        :password => 'redmine',
                                        :database => 'redmine'
                                        )

class Issue < ActiveRecord::Base
end

class IssueCategories < ActiveRecord::Base
end

class CustomValues < ActiveRecord::Base
end

class CustomFields < ActiveRecord::Base
  # 使いたいな
end

$entry_count = 0
$ch_name = ''

#
# チケット登録
#
def entry_issue date, start, duration, title, desc, category, channel
  #  p date, start, duration, title, desc, category, channel
  #  return

  # カテゴリ
  cate = IssueCategories.first( :conditions =>
                                {:project_id => 1, :name => category } )
  cate = IssueCategories.create( :project_id => 1, :name => category ) if cate.nil?

  # マッチするチケットを検索
  if Issue.count( :conditions => { :start_date => date, :subject => title }) > 0
    return #登録済み
  end

  # チケット作成
  issue = Issue.create( :project_id => 1, #'番組プログラム'
                        :tracker_id => 1, #'番組'
                        :lft => 1,
                        :rgt => 2,
                        :status_id => 1,
                        :priority_id => 2,
                        :done_ratio => 0,
                        :author_id => 1,
                        :subject => title,
                        :description => desc,
                        :start_date => date,
                        :due_date => date,
                        :estimated_hours => duration,
                        :category_id => cate.id )

  # root_id 更新
  # NULLのままだとRedmineからのチケット更新でクラッシュする
  # 親がない場合は自分のIDを入れるらしい。自分? 更新しか無い?
  issue.root_id = issue.id

  if issue.save
    # 開始時間
    CustomValues.create( :customized_type => "Issue",
                         :customized_id => issue.id,
                         :custom_field_id => 1, #'開始時間'
                         :value => start )
    # チャンネル
    CustomValues.create( :customized_type => "Issue",
                         :customized_id => issue.id,
                         :custom_field_id => 2, #'チャンネル'
                         :value => channel )
  end

  $entry_count = $entry_count + 1

end # entry_issue

#
# TVプログラム登録
#
def entry_program prog
  start = Time.parse( prog['start'] )
  stop = Time.parse( prog['stop'] )
  duration = (stop - start) / 60

  date = start.to_date
  start = start.strftime("%H%M").to_i

#  channel = prog['channel'].gsub(/ontv.*/,'') + $ch_name
  channel = prog['channel']+ '.'+$ch_name
  category = prog['category'].join(".")
  title = prog['title']
  desc = prog['desc']
  desc = "" if desc.kind_of? Hash

  # 5分以下の短い番組は登録しない :todo: DBに設定できると良い
  return if duration <= 5

  # デスクリプションが無い番組は登録しない
  return if desc == ''

  title.gsub!(/(【二】|【デ】|【S】|【字】)/,"")

  entry_issue date, start, duration, title, desc, category, channel
end

#
# メイン
#
xml_file = ARGV.shift + '.xml'
h = Hash.from_xml( open(xml_file).read )["tv"]

h.each_pair {|key,value|
  if key == 'channel'
    $ch_name = value['display_name']
  elsif key == 'programme'
    value.each {|prog| entry_program prog}
  end
}

puts "%d entried" % $entry_count

xml => hash
xmlからhashへの変換はactive_supportのcore_extにあるHashクラスを使って簡単に変換した。hashになればrubyで簡単にキーワードとデータを扱えるようになる。

entry_program
必要な項目を取り出して、タイトルとかちょっと加工する。
5分以下の短い番組は多分観ないし、それまで全部登録するとチケット数が多すぎないか?と思ったので排除した。
それから、メンテナンスや内容が書かれていない不明番組も排除するようにした。

entry_issue
まずカテゴリをチェックして新規なら登録する。
既に登録済みのチケットか確認する。
未登録なら必要項目をセットしてチケットを作成する。

カスタムフィールドの開始時間とチャンネルもチケットとリンクさせて作成する。
大体こんな手順で番組チケット作成が出来た。

注意点は、チケットアトリビュートのlft、rgt、及び root_id だ。
関連チケットや親子関係を表現するためのRailsテクニックらしいが、省略するとredmineがエラーを起こす。特に、root_idは親がない場合は自分のIDを入れておくものらしい。

便利

redmineをフロントエンドにした事で表示部分を全然作らないで済んだ。
チケット検索機能がもともと強力なので、番組検索が思い通り。
しかも検索内容をカスタムクエリーとして保存できるんで自分なりの分類が簡単。

出来てみると思いのほかマッチしそうなので、このまま進めてみようっと。

ActiveResourceかActiveRecordか

Redmineで番組録画チケット管理するために、まず放送番組を自動でチケット化する処理を書かねばならない。RedmineはRubyOnRailsのシステムなので、RailsのActiveResourceでやればいいんだよねって気軽に考えていた。

require 'rubygems'
require 'active_resource'
class Redmine < ActiveResource::Base
    self.site = 'http://localhost'
    self.user = '[login-id]'
self.password = '[password]'
end
class Project < Redmine
end
class Issue < Redmine
end

これだけでチケット(Issue)レコードへ簡単にアクセスできるようになる。
self.site、self.user、self.passwordはRedmine上のアカウント。普通にアクセスして操作するのと同じ事をスクリプトで出来るってことだね。
SQLコマンドを知らなくても変換してくれる便利機能だなので使わない手はないだろうとやってみたが。

project = Project.find(:all)  プロジェクト一覧取得

エラーだよ
/var/lib/gems/1.9.1/gems/activeresource-3.2.6/lib/active_resource/base.rb:929:in `instantiate_collection': undefined method `collect!' for # (NoMethodError)
        from /var/lib/gems/1.9.1/gems/activeresource-3.2.6/lib/active_resource/base.rb:901:in `find_every'
        from /var/lib/gems/1.9.1/gems/activeresource-3.2.6/lib/active_resource/base.rb:813:in `find'
        from project.rb:24:in `
'

ん?なんじゃこれは?
色々調べてみたが、Railsの不具合でもなければRedmineの不具合でもないらしい。
お互いの整合性がちゃんと取れていないのでは?というのが自分の結論だ。

Collection-resources-and-pagination
こちらの記事を読んで、何となくだけど納得した。

回避策?
IssueもProjectと同じエラーがでるので、Issueで説明すると

app/views/issues/index.api.srb ファイルの最初の
api.array :issues, api_meta(:total_count => @issue_count, :offset => @offset, :limit => @limit) do

api.array :issues do
に変更してすることで、エラーがなくなった。Projectも同じ対処で回避できた。
collect!を作ればいいんだよとか、Hashの問題だとか、色々あったけど・・・


検索〜
  issues = Issue.find(:all, :conditions => {:project_id => 1})
ん?全然検索できない・・・
issues = Issue.find(:all, :params => {:project_id => 1})
ActiveResourceだと:paramsなんだと。


この辺で、もういいや。ActiveResourceは、使わない事にした。

Railsからちゃんと習得していれば問題ないかもしれないけど、redmineがRailsだったから使ってみようと思ったんで、その辺の基礎知識が乏しい。
ネット検索してみると、ActiveResourceより、ActiveRecordの方が情報が豊富だし。
なにより、redmineのモデル部分はActiveRecordで記述されているんで参考にしやすい。

ということで、DB周りの自動処理はActiveRecordでやることにしたよ。


require 'rubygems'
require 'active_record'

ActiveRecord::Base.establish_connection(
:adapter => 'mysql2',
:host => 'localhost',
:username => 'redmine',
:password => 'redmine',
:database => 'redmine'
)

class Issue < ActiveRecord::Base
end

ActiveRecordだとこんな感じでredmineDBにアクセスできるようになる。
establish_connectionの中は、redmineのconfig/database.ymlに記述したのと同じ内容を書けばいい。

チケット検索は、Issue.find。作成は、Issue.new で簡単に操作できるね。
こっちの方がDBレコードのイメージそのままな感じなんでフィーリングが合ってる。
初心者の私が色々書いてもしょうがないので、この辺の詳しい事は他を参照してね。

まだ初歩的な使い方しか出来ないけど、それでも目的のものは構築できそうだ。
次回からは、構築関係を書いてみる予定。