タスク管理 Doing List作った
タスク管理はどうしていますか?
http://lifehacking.jp/2008/03/doing-list/
を電子化してみたかった。紙とペンが無いときが自分には多いのだ。
org-modeはどうか。良いのだけれど可搬性がわるい。
DTM専用PCにはemacsなど入れたくない。
emacsでファイルを編集しているときはいちいちorg-agendaというのもいやな感じだ。
Google Tasksはどうか。非常に良いと思う。GoogleTask一択でよいとおもった。
だけれども作った。つくってみたかった。
sinatraをcgiで動かす。lisonalは非常に遅いので使い物にならないがローカルで動かすと許容できる速度感にはなる。
制作物
http://modeverv.a.lisonal.com/doinglist/doinglist.rb
thin版
http://modeverv.a.lisonal.com/doinglistP/
貼りつけてあるものとは中身が少し違う。5行分ぐらい。thin速い。
使い方
テキストフィールドに入力してエンターor追加をclickする。
Doing列にタスクが追加される
タスクをダブルクリックするとタスクのステータスが"closed"扱いになる。
裏側でサーバーにタスクは保存されているのでブラウザをとじても環境が変わってもOKだ。
タスクはDoing<=>Pendingの間でドラッグアンドドロップで移動が可能である。
一括してタスクを追加することができる。一括追加を押すとdivが現れるので
テキストエリアに入力して追加ボタンを押す。
一括追加におけるタスク間のデリミタは"\n"である。
タスクを実行した結果を見るにはアウトプットを押す。
preフィールドに実行結果が書き込まれて表示されるので利用してほしい。
"サーバーからTODOデータを削除"を押すとサーバー上からデータが削除される。
ソース
ソースはrbファイル,css,js,テンプレートファイルになる。
タスクのデータはxmlで管理される。
ソースの大部分はコピペである。
# doinglist.rb #! /Users/seijiro/.rvm/rubies/ruby-1.9.2-p290/bin/ruby # -*- coding: utf-8 -*- #require 'rubygems' require 'sinatra' require 'json' require 'nokogiri' require 'cgi/util' require 'digest' require 'kconv' # モデル xmlにする前にいったんTaskクラスで受け取ろう。 class Task attr_accessor :sn,:title,:timestamp def initialize( args={ } ) @timestamp = args[:timestamp] || Time.now @title = args[:title] || "dammy" @sn = args[:sn] || Digest::MD5.hexdigest(title) end def stored_task? Task.get_ds.xml.xpath("//task[@sn='#{@sn}']").length > 0 end def append_to_datastore fragment = <<-EOF <task sn='#{@sn}' status='doing'> <title>#{Task.h(CGI.unescape(@title))}</title> <timestamp>#{@created_at}</timestamp> </task> EOF Task.get_ds.xml.css('tasks').first.add_child(fragment) end def done_task_datasource Task.get_ds.xml.xpath("//tasks/task[@sn='#{@sn}']").each do |node| node.attribute("status").value = "done" node.at("timestamp").remove node.add_child("<timestamp>#{Time.now}</timestamp>") end end class << self #データソースを取得する def get_ds @ds ||= DataStore.new end # def get_tasks_from_ds(status="doing") tasks = [] Task.get_ds.xml.xpath("//tasks/task[@status='#{status}']").each do |node| tasks << Task.new({ :title => Task.h(node.css("title").text), :sn => node.attribute('sn').value, :timestamp => node.css("timestamp").text, }) end return tasks end def h(str) return str. gsub(/&/, "&"). gsub(/"/, """). gsub(/</, "<"). gsub(/>/, ">") end def commit Task.get_ds.write end def dispose_xml old_filename = DataStore.filename new_filename = old_filename.gsub("\.xml","\_archive\_#{Time.new.to_i.to_s}\.xml") FileUtils.cp(old_filename,new_filename) ds = DataStore.new ds.xml.xpath("//tasks/task[@status='done']").each do |node| node.remove end ds.write rescue "ok"#no fileの場合は空振り上等。 end end end # データソース class DataStore attr_accessor :xml def initialize @filename = DataStore.filename unless File.exist?(@filename) File.open(@filename,"w"){|io| io.write("<tasks></tasks>") } end @xml = Nokogiri::XML( File.open(@filename) ,nil,'utf-8') end def write File.open(@filename,"w") { |io| io.write @xml.to_s } end class << self def filename "#{Dir::pwd}/data.xml" end end end # sinatra コントローラー # config configure do settings.run = false end get '/' do @title = "Doing List" @tasks = Task.get_tasks_from_ds("done") @tasks2 = Task.get_tasks_from_ds("doing") @tasks3 = [] erb :index end post '/append_server' do "Server OK. Worked with No Work " unless params["tasks"] require 'json' datas = JSON.parse(params["tasks"]) write_flg = false datas.each do |k,v| t = Task.new({ :title => v.gsub('#<<semicoron>>#',';'), :sn => k, }) unless t.stored_task? t.append_to_datastore write_flg = true end end Task.commit if write_flg "Server OK. Appended." end post '/done_server' do datas = JSON.parse(params["tasks"]) write_flg = false datas.each do |data| t = Task.new({:sn => data["sn"]}) if t.stored_task? t.done_task_datasource write_flg = true end end Task.commit if write_flg "Server Done sn=#{datas[0]['sn']} OK. " end post '/dispose_xml' do Task.dispose_xml "Server OK dispose xml." end Rack::Handler::CGI.run Sinatra::Application
// doinglist.js /* js for doing list */ function escapeHTML(str) { return str.replace(/[&"<>]/g, function(c) { return { "&": "&", '"': """, "<": "<", ">": ">" }[c]; }); } var _server = { append : function(data,mode){ this._data_pool[data["sn"]] = data["title"]; if(mode === 'single'){ this.bulk_end(); } }, bulk_end : function(){ var pdata = JSON.stringify(this._data_pool); this._append_server(pdata); this._data_pool = {}; }, _data_pool : {}, _append_server : function(pdata){ $.ajax({ type: "POST", url: "doinglist.rb/append_server", data: "tasks="+pdata, success: function(msg){ console.log( "Data Saved: " + msg ); } }); }, done : function(data){ var qdata = JSON.stringify([data]); this._done_server(qdata); }, _done_server : function(pdata){ $.ajax({ type: "POST", url: "doinglist.rb/done_server", data: "tasks="+pdata, success: function(msg){ console.log( "Data Saved: " + msg ); } }); }, dispose_xml : function(){ console.log("_server:dispose_xml"); $.ajax({ type: "POST", url: "doinglist.rb/dispose_xml", data: "", success: function(msg){ console.log( "Data Saved: " + msg ); } }); } }; /* タスクを追加する */ function _appender(title,mode){ console.log(title); console.log( title.replace(/\;/g,'\\;') ); if(title.match(/^\s*$/)){ return; } var sn = CybozuLabs.MD5.calc(title + new Date()); var c_date = Math.round(new Date()); var targ = " <li id=\"jquery-ui-sortable-item-<%= t.sn %>\" \n \ class=\"ui-state-default\" \ ondblclick=\"done_task(this);\" > \ <span> \ <span class=\"title\"><%= t.title %></span> \ <span class=\"cdate right\"><%= t.timestamp.to_s %></span> \ </span> \ </li> ". replace('<%= t.sn %>',sn). replace('<%= t.title %>',escapeHTML(title)). replace('<%= t.timestamp.to_s %>',c_date); $("#jquery-ui-sortable-center").append($(targ)); _server.append({ 'sn' : sn , 'title' : title.replace(/;/g,'#<<semicoron>>#') },mode); } /* タスク修了 */ function done_task(targ){ var parent_div_id = $(targ).closest('ul').attr("id"); if(parent_div_id == "jquery-ui-sortable-center"){ // タイムスタンプを変更 var stamp = new Date(); $(targ).find(".cdate").first().html( "" + new Date() ); //move $("#jquery-ui-sortable-left").append(targ); //coookie $(targ).css({ 'background' : 'white' } ); var updateArray = jQuery("#jquery-ui-sortable-center").sortable('toArray').join( ',' ); jQuery.cookies.set ("jquery-ui-sortable-center",updateArray,{ expires: 1 } ); // server var sn = $(targ).attr("id").replace('jquery-ui-sortable-item-',''); _server.done({'sn': sn }); } } /* タスクの出力を書きだす */ function print_statistics(){ var str = "結果\n"; console.log(str); $("#jquery-ui-sortable-left"). find("li"). each(function(){ str += $(this).find('.title').first().html() + "\t"; str += $(this).find('.cdate').first().html(); str += "\n"; }); console.log(str); $("#statistics").html(str); } /* ページ上の doneを非表示 */ function remove_page_done_li_data(){ $("#jquery-ui-sortable-left").find("li"). each(function(){ $(this).css({ "display" : "none" }); }); } /* ボタンからキックされる */ function server_remove_xml(){ remove_page_done_li_data(); console.log("server_remove_xml"); _server.dispose_xml(); _fout_s(); return false; } /* タスク追加 formからキックされる */ function append_task(){ var title = $('#task_title').val(); _appender(title,'single'); $('#task_title').val(''); } /* UI */ function _fout(){$("#floatWindow").fadeOut('fast');return false;} function _fin(){$("#floatWindow").fadeIn('fast');return false;} function _fout_s(){$("#floatWindow2").fadeOut('fast');return false;} function _fin_s(){$("#floatWindow2").fadeIn('fast');print_statistics();return false;} /* text_areaをパースする */ function _textarea_parser(data){ // 今は\nのみ var titles = data.split("\n"); return titles; } /* タスク追加 text_areaから */ function bulk_input(){ var data = $("#bulk_text_area").val(); var titles = _textarea_parser(data); for(var i=0;i<titles.length;i++){ _appender(titles[i],'bulk'); } _server.bulk_end(); _fout(); $("#bulk_text_area").val(''); return false; } /* init */ jQuery( function() { sorter = function(name){ jQuery('#'+ name).sortable({ connectWith: '.sortable' , cursor: 'move', opacity: 0.7, placeholder: 'ui-state-highlight' } ); // jQuery('#'+ name).disableSelection(); jQuery('#'+ name). sortable( { update: function(event,ui) { var updateArray = jQuery('#'+ name).sortable('toArray').join( ',' ); jQuery.cookies.set (name,updateArray,{ expires: 1 } ); } } ); if( jQuery.cookies.get(name) ){ var cookieValue = jQuery.cookies.get(name).split( ',' ).reverse(); jQuery.each( cookieValue, function( index, value ){ jQuery( '#' + value ).prependTo( '#' + name ); }); } }; sorter('jquery-ui-sortable-center'); sorter('jquery-ui-sortable-right'); /* window 表示 */ $("#fin").click(_fin); $("#fout").click(_fout); $("#fin_s").click(_fin_s); $("#fout_s").click(_fout_s); /* ui design */ $(".ui-state-default").mouseover( function(){ $(this).css({'background':'lightyellow'}); } ); $(".ui-state-default").mouseout( function(){ $(this).css({'background':'white'}); } ); } );
/* custom.css */ * { margin:0px;padding:0px} body { font-size:12px; line-height:1.5; } h1 { font-size:3.0em; padding-left:0.6em; } pre { width:100%; font-size:20px; } /* layout */ header { position: fixed; left: 0; top: 0; width: 100%; background-color: lightgreen; z-index: 100; } #wrapper { margin:65px 0px 0px 0px; } #now,#after,#done{ margin-bottom:30px; } .gridl { width: 20%;float:left;} .gridc { width: 50%;float:left;} .gridr { width: 30%;float:left;} footer { position: fixed; left: 0; bottom: 0; clear:both; width: 100%; background-color: lightgreen; z-index: 100; text-align:right; } .left{float:left} .right{float:right} .clear{clear:both} /* design */ h1 { float:left; } h2{ text-align:center; } .roundborder10 { border-radius: 10px; -webkit-border-radius: 10px; -moz-border-radius: 10px; border:1px solid; } #jquery-ui-sortable-left, #jquery-ui-sortable-center, #jquery-ui-sortable-right { clear:both; list-style-type: none; margin: 0; padding: 0; min-height:2em; } #jquery-ui-sortable-left li , #jquery-ui-sortable-center li , #jquery-ui-sortable-right li { margin:0 1em 0.5em 1em; padding: 0.1em; padding-left: 1em; padding-right:1em; background:white; cursor: move; overflow:auto; } #jquery-ui-sortable-center li , #jquery-ui-sortable-right li { border-radius: 5px; -webkit-border-radius: 5px; -moz-border-radius: 5px; border:1px solid; } footer p{ margin:5px 5px 5px 5px; } #floatWindow { background:white; display:none; position:absolute; top:0; z-index:200; width:50%; } #floatWindow2 { width:50%; background:white; display:none; position:absolute; top:0; z-index:200; width:50%; } #taskdiv{ margin:5px 1em 0 1em; float:right; } .text_field { background: none repeat scroll 0 0 lightgray; border-color: -moz-use-text-color -moz-use-text-color #FFF8E8; border-radius: 4px 4px 4px 4px; border-style: none none solid; border-width: medium medium 2px; box-shadow: 0 3px 3px #706751 inset; color: #453000; font-family: Helvetica,Arial,sans-serif; font-size: 24px; margin: 5px 5px 5px 5px; padding: 4px; } .bulk_area{ width:97%;} .submit_button { background: darkgreen; border: 1px solid; border-radius: 4px 4px 4px 4px; box-shadow: 0 1px 1px darkgray inset, 0 1px 1px gray; color: #FFFFFF; cursor: pointer; display: block; font-family: Helvetica,Arial,sans-serif; font-size: 16px; font-weight: bold; margin: 5px 5px 5px 5px; padding: 8px; text-shadow: 0 -1px 1px #250300; } .cdate{ font-size:0.5em; } .title{ font-size:0.5em; text-align:left;} #jquery-ui-sortable-left li .cdate{ display:block; } #jquery-ui-sortable-center li .cdate{ display:none; } #jquery-ui-sortable-right li .cdate{ display:none; } #jquery-ui-sortable-left li .title{ font-size:0.5em; } #jquery-ui-sortable-center li .title{ font-size:1.4em; } #jquery-ui-sortable-center li:first-child .title{ font-size:3.4em; } #jquery-ui-sortable-left li .title{ font-size:0.5em; } .ui-state-highlight { height: 6em; background:lightblue;}
# タスクのデータはxmlで管理される。 <?xml version="1.0" encoding="utf-8"?> <tasks> <task sn="90367bcdc321412d8346ece0d25aec7b" status="done"> <title>リファクタリング:html/css</title> <timestamp>2011-09-19 00:43:05 +0900</timestamp> </task> <task sn="d4eebf3bc73b1d544d21993bec989f2d" status="done"> <title>タスク2</title> <timestamp>2011-09-19 00:43:33 +0900</timestamp> </task> </tasks>
<!-- view ファイル --> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title><%= @title %></title> <link rel="icon" href="http://ja.gravatar.com/userimage/14611836/d5caef2a5366cf647fc8fba3430e5854.png" type="image/png"> <!--[if lt IE 9]> <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link rel="stylesheet" href="custom.css?<%=Time.now.to_i.to_s%>" type="text/css" media="screen" /> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js"></script> <script type="text/javascript" src="http://cookies.googlecode.com/svn/trunk/jquery.cookies.js"></script> <script type="text/javascript" src="http://labs.cybozu.co.jp/blog/mitsunari/2007/07/24/js/md5.js"></script> <script type="text/javascript" src="doinglist.js?<%=Time.now.to_i.to_s%>"></script> </head> <body> <header> <h1><%= @title %></h1> <div id="taskdiv" > <form onsubmit="append_task();return false;" class="left"> <input type="text" id="task_title" class="text_field left" size="24" /> <input type="submit" class="submit_button left" value="追加" /> </form> <div style="float:right"> <input type="button" id="fin" class="submit_button left" value="bulk追加" /> <input type="button" id="fin_s" class="submit_button left" value="アウトプット" /> </div> </div> </header> <div id="wrapper"> <div id="done" class="gridl"> <h2>Closed</h2> <ul id="jquery-ui-sortable-left" class=""> <% @tasks.each do |t|%> <li id="jquery-ui-sortable-item-<%= t.sn %>" class="ui-state-default" ondblclick="done_task(this);"> <span> <span class="title"><%= t.title %></span> <span class="cdate right"><%= t.timestamp.to_s %></span> </span> </li> <% end %> </ul> </div> <div id="now" class="gridc"> <h2>Doing</h2> <ul id="jquery-ui-sortable-center" class="sortable"> <% @tasks2.each do |t|%> <li id="jquery-ui-sortable-item-<%= t.sn %>" class="ui-state-default" ondblclick="done_task(this);"> <span> <span class="title"><%= t.title %></span> <span class="cdate right"><%= t.timestamp.to_s %></span> </span> </li> <% end %> </ul> </div> <div id="after" class="gridr"> <h2>Pending</h2> <ul id="jquery-ui-sortable-right" class="sortable"> <% @tasks3.each do |t|%> <li id="jquery-ui-sortable-item-<%= t.sn %>" class="ui-state-default" ondblclick="done_task(this);"> <span> <span class="title"><%= t.title %></span> <span class="cdate right"><%= t.timestamp.to_s %></span> </span> </li> <% end %> </ul> </div> <div id="floatWindow"> <div><input type="button" id="fout" class="submit_button right" value="閉じる" /></div> <div class="clear"><textarea id="bulk_text_area" class="text_field bulk_area" value="" ></textarea></div> <div><input type="button" id="fout" class="submit_button right" value="追加" onclick="bulk_input();"/></div> </div> <div id="floatWindow2"> <div><input type="button" id="fout_s" class="submit_button right" value="閉じる" /></div> <div class="clear"><pre id="statistics"> </pre></div> <div><input type="button" id="server_remove_xml" class="submit_button right" value="サーバーからTODOデータを削除(destructive)" onclick="server_remove_xml();return false;"/></div> </div> </div> <footer> <p>modeverv@gmai.com</p> </footer> </body> </html>