にほ録

作業記録です。

blogger bookmarklet(2)

2009-11-16 05:09:44 | Weblog
blogger bookmarkletでカテゴリー(別名:ラベル、いわゆるタグ。Bloggerの中では『Category』(a.k.a. Label)というのが正式名称。)を付けられるように改良した。

Developer's Guideを見ても、十分な情報が無くて、API Specification for GData JavaScript Clientを見る必要があるんだが、ここにたどり着くのに時間がかかった。

すでに使用済みのカテゴリーは、var list = blog.getCategories()して、list[i].termで取得できる。
記事へのカテゴリーの設定は、var c = new google.gdata.atom.Category({scheme: 'http://www.blogger.com/atom/ns#', term: '新カテゴリ')して、blogPostEntry.addCategory(c)。

完成したコードは長くなったので、Google Codeに置いた。

はまったというか困ったのは、複数のブログがある場合に、getBlogFeed()のあと、feedRoot.feed.getEntries()の中にブログが入る順番が、その時々で変わること。どうも、最近投稿があったブログが先頭に来るみたい。そんなわけで、毎回getTitle()でブログのタイトルを見て、何番目に入っているか調べる必要がある。順番固定しておいてくれれば、インストール時に番号設定するだけで済むのに。

blogger bookmarklet

2009-11-13 13:12:00 | Weblog
以前にblogger.comのgdata APIで作ったブックマークレットが、一昨日突然何もしてないのに動かなくなったが、昨日突然動くようになった。なんだったんだろう?
で、使っているうちに、Tunblrみたいな引用機能がほしくなったので、ちょっと改良してみた。テキストの引用しかできないけど。

これ↓をブックマークに登録し、(Firefox限定。IEは知らん、というか、ちょっと直すだけでいいはず。)
javascript:void%20window.open('https://www.example.com/blogthis.html?url='+encodeURIComponent(location.href)+'&title='+encodeURIComponent(document.title)+'&selection='+encodeURIComponent(document.getSelection()),'blogthis');

これ↓をどこかのサーバに置く。(localhostでも可)
<html>
<head>
<title>blog this page</title>
<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load("gdata", "1.x");
google.setOnLoadCallback(init);
var BLOGGER_AUTH_URL = 'http://www.blogger.com/feeds';
var BLOGGER_LIST_URL = 'http://www.blogger.com/feeds/default/blogs';
var BlogPress =  {
  service: null,
  blogs: [],
  cuurrentBlogNum: 0  // modify this according your blog settings.
};

function el(id) { 
  return document.getElementById(id);
}

function changeStatus(status) {
  el('status').innerHTML = 'Status: ' + status;
};

function loginOrOut() {
  var token = google.accounts.user.checkLogin(BLOGGER_AUTH_URL);
  if(token) {
    google.accounts.user.logout();
    init();
  }
  else {
    google.accounts.user.login(BLOGGER_AUTH_URL);
  }
}

function init() {
  var param = new Array();
  var query = window.location.search.substring(1);
  var params = query.split('&');
  for(var i = 0; i < params.length; i++) {
    var pos = params[i].indexOf('=');
    if(pos > 0) {
      param[params[i].substring(0, pos)] = params[i].substring(pos + 1);
    }
  }
  el('title').value = decodeURIComponent(param['title']);
  el('url').value = decodeURIComponent(param['url']);
  if(undefined != param['selection'] && '' != param['selection']) {
    el('comment').value = '『' + decodeURIComponent(param['selection']) + '』';
  }
  var token = google.accounts.user.checkLogin(BLOGGER_AUTH_URL);
  if(token) {
    el('login').value = "logout";
    BlogPress.service = new google.gdata.blogger.BloggerService('GoogleInc-bloggerOffline-1');
    getBlogs();
    el('submit').disabled = false;
  }
  else {
    el('login').value = "login";
    el('submit').disabled = true;
  }
}

function getBlogs() {
  var query = new google.gdata.blogger.BlogQuery(BLOGGER_LIST_URL);
  BlogPress.service.getBlogFeed(query, handleBlogsFeed, handleError);
}

function handleBlogsFeed(resultsFeedRoot) {
  var blogsFeed = resultsFeedRoot.feed;
  BlogPress.blogs = blogsFeed.getEntries();
  var blog = BlogPress.blogs[BlogPress.cuurrentBlogNum];
  var title = blog.getTitle().getText();
  el('postTo').innerHTML = 'Post to ' + title;
}

function handleError(error) {
  changeStatus(error.cause ? error.cause.statusText : error.message);
};

function submit() {
  var title = el('title').value;
  var link = '<a href="'+el('url').value+'">'+el('url').value+'</a>';
  var content = link + "</br>\n" + el('comment').value;
  var blogPostEntry = new google.gdata.blogger.PostEntry();
  blogPostEntry.setTitle(google.gdata.atom.Text.create(title));
  blogPostEntry.setContent(google.gdata.atom.Text.create(content, 'html'));
  var blog = BlogPress.blogs[BlogPress.cuurrentBlogNum];
  var blogPostHref = blog.getEntryPostLink().getHref();
  BlogPress.service.insertEntry(blogPostHref, blogPostEntry, handleEntryPost, handleError);
}

function handleEntryPost(entryRoot) {
  changeStatus('Successfully added.');
}

</script>
</head>
<body>
<img src="pumpkin_64.png" alt="An image of the same domain is needed somehow." />
<div><input id="login" type="button" value="login" onClick="loginOrOut()" /></div>
<hr>
<div id="postTo"></div>
<div>Title: <input type="text" id="title" size=70 /></div>
<div>URL: <input type="text" id="url" size=70/></div>
<div>Commnet: <br/><textarea id="comment" rows=20 cols=50></textarea></div>
<div><input id="submit" type="button" value="submit" onClick="submit()" /></div>
<div id="status">Not posted yet.</div>
</body>
</html>

my location

2009-11-13 04:33:28 | Weblog
以前にblogger.comのgdata APIで作ったプログラムが、何もしてないのに昨日から動かなくなって、解析開始。

Firebugでエラー見てたら、時々エラーが変わるんだが、結局、認証でサーバから良くない値が返ってくるみたい。
サーバ側で何かしてるのかも。Googleのapiの中の話なので解析面倒そう。
http://www.google.com/jsapiを見てみた。
緯度経度が取れるのね。
という訳で、自分の現在位置を表示するプログラムをちょっと作ってみた。
my location
緯度経度の取り方は、このソース見れば一目瞭然だが、要するに、google.loader.ClientLocation.latitudeとgoogle.loader.ClientLocation.longitudeに入ってるということ。
位置はあまり正確じゃないな。何の位置だこれ?

で、元の問題のプログラムはまだ動かないまま。

Google Closure Compiler

2009-11-10 11:16:50 | Weblog
Google Closure Compilerちょっと使ってみた。

元のコードが、サーバからJSONを受け取って処理するこんなコードの場合、
var json;
eval('json = ' + request.responseText);
if(json['key'] == 'data') {
  // 何かの処理
}
if文のところで、jsonの型が合わないって、「JSC_TYPE_MISMATCH: only arrays or objects can be accessed」ってWarningがでる。まあ、たしかにこれはサーバ側の処理を見ないとわからないので、仕方ない。


あと、元のコードの先頭(何もない地の部分)に書いてあったグローバル変数が、なぜかfor文の中に入れられてた。こんな感じで。
for(var b=[],c=[],d="",e=[],g,h="",i=0;i<32;i++)h+=Math.floor(Math.random()*16).toString(16);これは、何か効率よくなるの?


あとは、空白を無くすとか、変数名/関数名を短くするとか、当たり前の内容。
驚くほど賢いわけではないが、外部のファイル(prototype.js)の関数名は変えないとか、ちゃんとできてる。

FileUploadHandler.new_fileのcontent_length

2009-11-07 11:50:42 | Weblog
Django v1.0 documentation カスタムのアップロードハンドラを書く

FileUploadHandler.new_fileの説明で、content_lengthは『ブラウザがファイルサイズを提供しないこともあり、その場合は None です』とあるが、実際のところオイラの環境では、FirefoxもIEもサイズを提供しない。ちぇっ。

IEに限定すれば、任意のファイルのファイルサイズを取得する方法でクライアント側でファイルサイズが取れるんで、IE限定でもいい?

django FILE_UPLOAD_HANDLERS

2009-11-04 12:55:54 | Weblog
djangoで一つのフォームから複数のファイルをアップロードした場合に、ハンドラがどう呼ばれるのか調べた。 Pythonのloggingを使用。
  1. __init__
  2. handle_raw_input
  3. new_file  (一個目のファイル)
  4. receive_data_chunk  (chunkの分だけ繰り返し)
  5. file_complete
  6. new_file  (二個目のファイル)
  7. receive_data_chunk  (chunkの分だけ繰り返し)
  8. file_complete
  9. upload_complete
という訳で、当然、複数のファイルはシーケンシャルに処理される。

Flickrで複数のファイルを並行してアップロードしてるように見えるのはどうやってんだろう?
YouTubeも複数のファイルを並行してアップロードできるけど、あれは単にフォームが別なんだと思う。

djangoでファイルアップロードのプログレスバー

2009-11-02 15:50:46 | Weblog
たかがプログレスバーと思ってたら、意外と面倒。
基本的な戦略としては、formのsubmitボタンにfunctionを貼り付けて、submitされると同時に、別のもう一つのセッションを走らせる。このセッションで定期的にサーバに進捗状況を問い合わせて、プログレスバーをアップデートする。サーバ側では、FILE_UPLOAD_HANDLERSにハンドラを追加して、ファイルのchunkが来るたびに受け取ったサイズを加算して、それを「IPアドレス+Session-ID」をkeyにして、memcachedに保存する。もう一方のセッションが同じkeyでmemcachedを参照して進捗状況をクライアントに返す。

手順1: memcachedをインストール。ついでにMemCacheD Managerも
手順2: python-memcached-1.44をインストール。setuptoolsも必要
こんな感じで動作確認。
>>> import memcache
>>> mc = memcache.Client(['localhost:11211'])
>>> mc.flush_all()
>>> mc.set('key', 'value')
True
>>> mc.get('key')
'value'
手順3: http://www.djangosnippets.org/snippets/678/のハンドラをインストール
手順4: http://www.fairviewcomputing.com/blog/2008/10/21/ajax-upload-progress-bars-jquery-django-nginx/にあるuploaddemo.zipをインストール
手順5: 上記のファイルに些細なバグが2つくらいあるので、エラーメッセージを見ながら直す。Firebugとか無いと、バグ見つけるの難しいかも

以上で完成。




11月2日追記: サンプルの中にハンドラが入ってたので、手順3はいらないみたい。手順4はインストールのあと、manage.py syncdbも必要。

Google Reader Full Feedの修正

2009-10-25 07:20:28 | Weblog
Google Readerでもgをぺちって全部全文読んじゃおう。
これ便利なんだが、検索のフィールドでキー入力中に反応していしまうのが玉にキズ。
というわけで修正した。

まず、スクリプトの適当などこかにこの処理を入れる。
<code>
var full_feed = true;
var timer2 = setTimeout(function boo() {
    if(document.getElementById('search-input')) {
      document.getElementById('search-input').addEventListener('focus', stop_full_feed, false);
      document.getElementById('search-input').addEventListener('blur', start_full_feed, false);
    }
    else {
      setTimeout(boo, 500);
    }
  }, 500);

function stop_full_feed() {
  full_feed = false;
}

function start_full_feed() {
  full_feed = true;
}
</code>あとは、キー入力の判定のところの
<code>if (e.keyCode == w.KeyEvent.DOM_VK_Z) {</code>

<code>if (e.keyCode == w.KeyEvent.DOM_VK_Z && full_feed) {</code>
に修正する。

はまった点①
Google Readerを開いた時点では、document.getElementById('search-input')が存在しない。なので、これが出来るまでタイマーで待つことにした。

はまった点②
Greasemonkeyでは、document.getElementById('search-input').onfocus = function() {hoge();}のような書き方が出来ない。これをやると、「"Component is not available" nsresult: "0x80040111」になる。
この点については、以下の記事が参考になった。
304 - narucissus is Not Modified: Greasemonkeyで遊ぶ(その1)

item_list

2009-10-23 07:07:27 | Weblog
from moving.models import Item
from django.contrib.auth.decorators import login_required

@login_required
def item_list(request):
  item_list = Item.objects.all()
  return render_to_response("item_list.html",
    {
      "item_list": item_list,
    }
  )

コーディングはさておき、プロジェクトのadminのパスワードを忘れて、プロジェクトから作り直し(ToT)

blogger.comのgdata API

2009-10-22 01:29:57 | Weblog
今開けているページをブックマークからBloggerに投稿する。
(a.k.a. はてブのブックマークレットもどき)

これをブックマークに登録する。
javascript:void%20window.open('http://path_to_the_file/blogthis.html?url='+encodeURIComponent(location.href)+'&title='+encodeURIComponent(document.title),'blogthis');

下記のファイルを、どこかのhttpサーバに置く。localhostでもよい。
<html>
<head>
<title>blog this page</title>
<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load("gdata", "1.x");
google.setOnLoadCallback(init);
var BLOGGER_AUTH_URL = 'http://www.blogger.com/feeds';
var BLOGGER_LIST_URL = 'http://www.blogger.com/feeds/default/blogs';
var BlogPress =  {
  service: null,
  blogs: [],
  cuurrentBlogNum: 0  // modify this according to your blog settings.
};

function el(id) { 
  return document.getElementById(id);
}

function changeStatus(status) {
  alert(status);
};

function loginOrOut() {
  var token = google.accounts.user.checkLogin(BLOGGER_AUTH_URL);
  if(token) {
    google.accounts.user.logout();
    init();
  }
  else {
    google.accounts.user.login(BLOGGER_AUTH_URL);
  }
}

function init() {
  var param = new Array();
  var query = window.location.search.substring(1);
  var params = query.split('&');
  for(var i = 0; i < params.length; i++) {
    var pos = params[i].indexOf('=');
    if(pos > 0) {
      param[params[i].substring(0, pos)] = params[i].substring(pos + 1);
    }
  }
  el('title').value = decodeURIComponent(param['title']);
  el('url').value = decodeURIComponent(param['url']);
  var token = google.accounts.user.checkLogin(BLOGGER_AUTH_URL);
  if(token) {
    el('login').value = "logout";
    BlogPress.service = new google.gdata.blogger.BloggerService('GoogleInc-bloggerOffline-1');
    getBlogs();
    el('submit').disabled = false;
  }
  else {
    el('login').value = "login";
    el('submit').disabled = true;
  }
}

function getBlogs() {
  var query = new google.gdata.blogger.BlogQuery(BLOGGER_LIST_URL);
  BlogPress.service.getBlogFeed(query, handleBlogsFeed, handleError);
}

function handleBlogsFeed(resultsFeedRoot) {
  var blogsFeed = resultsFeedRoot.feed;
  BlogPress.blogs = blogsFeed.getEntries();
  var blog = BlogPress.blogs[BlogPress.cuurrentBlogNum];
  var title = blog.getTitle().getText();
  el('postTo').innerHTML = 'Post to ' + title;
}

function handleError(error) {
  changeStatus(error.cause ? error.cause.statusText : error.message);
};

function submit() {
  var title = el('title').value;
  var link = '<a href="'+el('url').value+'">'+el('url').value+'</a>';
  var content = link + "</br>\n" + el('comment').value;
  var blogPostEntry = new google.gdata.blogger.PostEntry();
  blogPostEntry.setTitle(google.gdata.atom.Text.create(title));
  blogPostEntry.setContent(google.gdata.atom.Text.create(content, 'html'));
  var blog = BlogPress.blogs[BlogPress.cuurrentBlogNum];
  var blogPostHref = blog.getEntryPostLink().getHref();
  BlogPress.service.insertEntry(blogPostHref, blogPostEntry, handleEntryPost, handleError);
}

function handleEntryPost(entryRoot) {
  changeStatus('Successfully added or updated post.');
}

</script>
</head>
<body>
<img src="pumpkin_64.png" alt="An image of the same domain is needed somehow." />
<div><input id="login" type="button" value="login" onClick="loginOrOut()" /></div>
<hr>
<div id="postTo"></div>
<div>Title: <input type="text" id="title" size=70 /></div>
<div>URL: <input type="text" id="url" size=70/></div>
<div>Commnet: <br/><textarea id="comment" rows=20 cols=50></textarea></div>
<div><input id="submit" type="button" value="submit" onClick="submit()" /></div>
</body>
</html>

はまった点①
認証のため同じドメインの画像を表示する必要があるんだって。何?その仕様。

はまった点②
handleBlogsFeed()が呼び出されるのは、getBlogs()を抜けてinit()が終わったあと。getBlogs()の直後に、handleBlogsFeed()で設定したグローバル変数をチェックしたら何もなくてはまった。