北海道ラボ

iPhoneアプリ開発ではUITableViewを利用する場合が多々あります。そこでちょっと変わったUITableViewの使い方を紹介します。

TableViewを回転させる

標準ではセルが上から下にかけて並んでいますが、TableViewを回転させることで任意の方向にセルを配置することが可能です。

通常のTableView

90度回転させたTableView

90度回転させたものは中に入っているTexLabelも回転させています。Viewを回転させるにはCGAffineTransform構造体を利用します。

1
2
3
4
5
- (void)viewDidLoad {
CGAffineTransform rotate = CGAffineTransformMakeRotation(-90.0f * (M_PI / 180.0f));
[self.view setTransform:rotate];
[super viewDidLoad];
}

CGAffineTransformMakeRotationで回転行列を設定できます。ここでは、90度左に回転させるために−90度を設定しました。CGAffineTransformMakeRotationで指定できる角度はラジアンです。また、その値をUIViewのsetTransformというクラスメソッドに渡すことによって、回転させることが出来ます。また、90度以外にも中途半端に回転させることも可能です。

中途半端に回転させた図

加速度センサとの融合

これだけだとちょっとつまらないので、加速度センサと組み合わせてみました。iPhoneを傾けると、次のページ(セル)に移動するサンプルを作成してみました。

傾きを検知してスライドするサンプル

このプログラムはXcodeのデフォルトのUITableViewControllerのものを利用しております。RootViewController.mの実装は以下のとおりです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
//RootViewController.h
#import
@interface RootViewController : UITableViewController {
NSMutableArray *cellList;
int rotateRad;
int indexRow;
UIAccelerationValue speedX_;
UIAccelerationValue speedY_;
UIAccelerationValue speedZ_;
}
@end

//RootViewController.m
#import "RootViewController.h"

@implementation RootViewController
- (void)viewDidLoad {
cellList = [[NSMutableArray alloc] initWithObjects:
@"P1",
@"P2",
@"P3",
@"P4",
@"P5",
@"P6",
@"P7",
@"P8",
@"P9",
@"P10",
nil];

indexRow = 0;
CGAffineTransform rotate = CGAffineTransformMakeRotation(-90.0f * (M_PI / 180.0f));
[self.view setTransform:rotate];
[self.tableView setScrollEnabled:NO];
[super viewDidLoad];
}

- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
speedX_ = speedY_ = 0.0;
UIAccelerometer *accelemeter = [UIAccelerometer sharedAccelerometer];
accelemeter.updateInterval = 0.5;
accelemeter.delegate = self;
}

- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
UIAccelerometer *accelemeter = [UIAccelerometer sharedAccelerometer];
accelemeter.delegate = nil;
}

- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
speedX_ = acceleration.x;
speedY_ = acceleration.y;
speedZ_ = acceleration.z;
rotateRad = 90+atan2(speedZ_,speedX_)*180/M_PI;

if(rotateRad>30) indexRow++;
else if(rotateRad<-30) indexRow--;
if(indexRow<0)indexRow=0;    if(indexRow>=[cellList count]) indexRow = [cellList count]-1;
NSIndexPath* indexPath = [NSIndexPath indexPathForRow:indexRow inSection:0];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
}

// Override to allow orientations other than the default portrait orientation.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Return YES for supported orientations.
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

// Customize the number of sections in the table view.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}

// Customize the number of rows in the table view.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [cellList count];
}

// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

static NSString *CellIdentifier = @"Cell";

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}

// Configure the cell.
CGAffineTransform rotate = CGAffineTransformMakeRotation(90.0f * (M_PI / 180.0f));
[cell.textLabel setTransform:rotate];
cell.textLabel.text = [cellList objectAtIndex:indexPath.row];
return cell;
}

- (void)dealloc {
[cellList release];
[super dealloc];
}

@end

ポイントは加速度センサから得られた値をもとに傾きを検出しています。今回はiPhoneを左右に30度傾けた時にセルの移動をするようにしました。また、色んなものと組み合わせれば面白いものができそうですね。

SmartBrain 改善予告

CATEGORIES SmartBrain, 北海道ラボby.yasu.tanaka1 Comments2011.02.03

学習管理システムSmartBrainには、コース内の進捗が分かるようにグラフが表示されているのですが、次回のアップデート時に、グラフが伸縮するようになります。iPhoneなど、表示領域が限られたモバイル端末が増えています。こういった端末でも快適に利用していただけるように、これからも様々な工夫を取り入れています。

幅が広い場合

幅が広い場合

幅が狭い場合

幅が狭い場合

記事数統計へのリンクを表示

CATEGORIES WordPress, 北海道ラボby.yasu.tanaka2 Comments2011.02.03

キバンインターナショナルでは、ブログでの情報発信を推進しています。

情報発信の状況を定量的に把握するための集計ページまで作ったのですが、そのページの表示方法が分からない!との意見を多数いただきましたので、FollowMeタブの↓にリンクを設置致しました。

記事数統計へのリンクを

記事数統計へのリンクを

集計ページのトップに、投稿件数のグラフを表示してほしい。という要望も頂いておりますので、近日中にさらにアップデートいたします。また、Wordpressのプラグインで、画面の端にアイコンを表示する方法についての記事も近日中に公開いたします。

アイコンの提供元 
無料素材倶楽部 > uターン矢印アイコン フリー素材

FAQ 「ユーザを自動ログインさせたい」を追加いたしました。ユーザごとの個別のURLを作成することで、ユーザを自動ログインさせることができます。この方法で作成したURLを他人に知られてしまうと、不正利用の原因になるのでご注意ください。

自動ログイン

自動ログイン

グループに教材割当時に期間設定

CATEGORIES SmartBrain, 北海道ラボby.yasu.tanaka1 Comments2011.01.31

学習管理システムSmartBrainの、グループに対する教材割当に、期間設定機能が実装されました。この機能を使うと、部署やクラス単位での受講期間制御が簡単にできます。この機能は、2011年2月にリリース予定の、バージョン1.14で利用できるようになる見込みです。

無期限割当 期間制限割当
個人単位で割当 2009年1月リリース 2010年7月リリース
グループに対して割当 2009年8月リリース 2011年1月リリース予定

スクリーンショット

グループに割り当て

グループに割り当て

期間設定

期間設定

期間設定の概念

期間設定は、「設定しない」「開始日時だけ設定」「終了日時だけ設定」「両方設定」の4パターンがあります。

期間設定パターン

期間設定パターン

SmartBrainマニュアル改定/FAQ

CATEGORIES SmartBrain, 北海道ラボby.yasu.tanaka1 Comments2011.01.29

SmartBrainのマニュアルを改訂いたしました。
FAQの説明が抜けておりましたので、以下の4ページを追加しました。

学習者マニュアル

  • FAQ
  • 管理者マニュアル

  • FAQ
  • FAQ設定
  • 回答入力
  • マニュアル更新作業が、システムのアップデートに追いついておらず申し訳ありませんでした。

    PPT2Flash / 文字化けの問題について

    CATEGORIES 北海道ラボby.yasu.tanaka1 Comments2011.01.27

    PPT2Flash Professionalを用いた変換の際に、文字化けが発生する場合があります。

    PPT2Flashでは下記に挙げるフォントに対応しています。

    • MS Pゴシック
    • MS P明朝
    • MS UI Gothic
    • MS ゴシック
    • MS 明朝

    しかし、これらのフォントが混在しているPPT資料を変換すると文字化けが発生することがあります。
    できる限りひとつのPPT資料内ではひとつのフォントに統一することをお勧めします。

    ひとつのフォントに統一していたつもりでも、
    『意図せずにフォントが切り替わってしまう』
    という場面もあります。

    その最も多い例としては、
    『箇条書きの入力中に「Shiftキー + Enterキー」で改行を行なって、複数行入力した』
    という、次ような場合が考えられます。

    箇条書きの一項目に複数の行がある場合

    始めの行は「MS Pゴシック」となっています。

    最初の行は「MS Pゴシック」

    そして、改行を行なって入力した行も同様であることが分かります。

    改行して入力した行も「MS Pゴシック」となっている

    しかし、行頭部分を選択すると、「MS ゴシック」に変わってしまっています。

    行頭部分は「MS ゴシック」に変わっている

    このような場合に、次の画像に示すような文字化けが発生することがあります。

    箇条書きの3行目で文字化けが発生している

    解決策としては、先ほども述べたとおり
    『ひとつのフォントに統一する』
    ということが一番効果的だと思われます。

    今回のように、意図せずにフォントが変わってしまうこともありますので、
    文字化け発生の際には入力ボックス内を全て選択してフォントの再指定を行なってみてください。

    入力ボックス内をすべて選択し、フォントを「MS Pゴシック」に再指定します

    すると、文字化けが解消されました。

    3行目の文字化けが解消されました

    第2回 SCORM技術者資格試験・講習会開催案内

    第2回のSCORM技術者資格試験・講習会が開催されるそうです。申込締切は、2011年2月1日(火)となっていますが、先着20名までしか受講できないそうなので、興味のあるかたは早めに申し込みましょう。私は、講習日程周辺も函館で開発業務を行っている予定なので今回の参加は見送ることになりそうです。

    Pythonを使う理由と作った物(ソース付)

    CATEGORIES 北海道ラボby.yasu.tanaka98 Comments2011.01.21

    みなさん、Pythonってご存知でしょうか?
    ニシキヘビ。。ではないです。
    プログラミング言語のPythonです。

    ↓Pythonのロゴ。蛇が2匹です。

    このPython。日本国内では、それほど知名度は高くないのですが、2010年で最も成長したプログラミング言語にも選ばれるなど、急速に人気の高まりつつある言語です。2011年にはPHPとC++を抜かして3位になれそうな勢いです!では、このPython、どうして急に人気がでてきたのでしょうか?理由は大きくわけて3つあると思います。

    Pythonが急に普及し始めた理由

    1. GAEで動作する

      これが最大の理由だと思います。Googleのサーバを使って簡単にサービスを提供できます。1日あたり1GBまでの転送なら、料金は一切かかりません ※1。無料利用分を超えて使った場合も、課金設定(1日当たりの支払い額の上限)を設定するだけで、簡単にサーバを増強できます。ロードバランス、データベースのレプリケーション、そんなことは一切考える必要がありません。全部自動でやってくれます。固定費が不要なので、AmazonEC2なんかより、よっぽど敷居は低いです。

    2. 理解しやすさ

      これには異論が多いと思います。C、Java、PHPなどとは似ても似つかないソースコード。でも、Python、慣れてしまえば読みやすいですよ。Pythonはインデントに縛られた言語です。プログラムの構造と見た目(インデント)が必ず一致しているので、誰が書いても似たようなソースコードになります。慣れないうちは思うように書き進められないかもしれませんが、しばらく使い込めば「読みやすいコード」が自然にかけるようになっているはずです。

    3. 遅さは問題ではなくなった

      Pythonはスクリプト言語です。コンパイル言語と比べると桁違いに遅いです。でも、Webアプリケーションのボトルネックって、言語ではないですよね?多くの場合、データベースアクセスや、ネットワーク通信、マルチメディアファイルのダウンロードがボトルネックです。スクリプト言語の遅さが問題になっていたのは10年前の話です。今は、PHPだって、RoRだって、Pythonだって実用上問題ない速度で動作します。1番にもつながりますが、言語の速度ではなく、スケールアウトするかどうか、つまり、サーバの台数に応じてきちんと性能が伸びるかが問題なのです。

    ※1 CPUの使用時間(無料分: 6.5時間/日)なども課金対象ですが、通信量制限(無料分: 1GB/日)が最も課金対象になりやすいです。

    で、使ってみた。

    Pythonを使う○○個の理由とか並べてても、実際に使ってみないと説得力がないので、実際につかってみた。ソースコードも全文貼り付けていますが、初心者が書いたコードなのであまり信用しないで頂けると幸いです。

    1. サーバ監視

    サーバ監視

    サーバ監視

    GAEからサーバの監視をしてみた。HTTPリクエストの応答時間を計るだけのシンプルなサービスだが、安定して稼動している。社内のサーバは、Zabbixなどを活用してデータセンタ内からきっちり監視しているが、データセンタと外部との回線の障害などに関しては、外部からの監視が有用だ。(スクリーンショットに掲載したデータは私が個人的に借りているサーバものです)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    #!/usr/bin/env python

    import cgi
    import datetime
    import wsgiref.handlers
    import urllib
    import appengine_utilities.sessions
    import os

    from datetime import datetime
    from datetime import timedelta
    from google.appengine.api.urlfetch import fetch
    from google.appengine.ext import db
    from google.appengine.api import users
    from google.appengine.ext import webapp
    from google.appengine.api import images
    from google.appengine.api.labs import taskqueue

    class Site(db.Model):
      date = db.DateTimeProperty(auto_now_add=True)
      name = db.StringProperty()
      url  = db.URLProperty()

    class Log(db.Model):
      date = db.DateTimeProperty(auto_now_add=True)
      response = db.FloatProperty()
      url = db.URLProperty()

    class MainPage(webapp.RequestHandler):
      def get(self):
        session = appengine_utilities.sessions.Session()
        if "admin" in session:
          self.response.out.write('<html><head><title>Response Monitor!!</title></head><body>')
          self.response.out.write('<form method="post" action="/"><input type="submit" value="logout"/></form>')
          self.response.out.write('[ <a href="http://blog.elearning.co.jp?s=add&amp;search_404=1">+</a> ]<br />')
          sites = db.GqlQuery("SELECT * FROM Site ORDER BY date DESC LIMIT 10")
          for site in sites:
            self.response.out.write('[ <a href="#" onclick="if(confirm(\'Are you sure to delete it?\'))location.href=\'/delete?id=%s\'">delete</a> ] ' % (site.key()))
            self.response.out.write('<a href="/detail/%s">%s</a> [ %s ]<br />' % (site.url, site.name, site.url))
          self.response.out.write("</body></html>")
        else:
          self.response.out.write('<html><head><title>Response Monitor!!</title></head><body>')
          self.response.out.write('<form method="post" action="/"><input type="password" name="password"/><input type="submit" value="login"/></form>')
          sites = db.GqlQuery("SELECT * FROM Site ORDER BY date DESC LIMIT 10")
          for site in sites:
            self.response.out.write('<a href="/detail/%s">%s</a> [ %s ]<br />' % (site.url, site.name, site.url))
          self.response.out.write("</body></html>")
         
      def post(self):
        session = appengine_utilities.sessions.Session()
        if self.request.get("password") == "hakodate2010":
          session["admin"] = 1
        else:
          if "admin" in session:
            del session["admin"]
        self.redirect('/')

    class Detail(webapp.RequestHandler):
      def get(self, key):
        self.response.out.write('<html><head><title>Log for %s</title></head><body>' % urllib.unquote(key))
        self.response.out.write("Log for '%s'" % urllib.unquote(key))
        self.response.out.write("<br />")
        responseMap = {};
        for i in range(0,4):
          query = db.GqlQuery("SELECT * FROM Log WHERE url = :1 ORDER BY date DESC LIMIT %s, 1000" % (1000 * i) , urllib.unquote(key) );
          for log in query:
            log.date+=timedelta(hours=9)
            day = str(log.date)[0:10]
            hour = str(log.date)[11:13]
            if day not in responseMap:
              responseMap[day] = {}
            if hour not in responseMap[day]:
              responseMap[day][hour] = {}
            responseMap[day][hour][log.date] = log.response

        self.response.out.write('<table border=1 style="border-style:solid ">')
        self.response.out.write("<tr><td></td>")
        for day in sorted(list(responseMap)):
          self.response.out.write("<td>")
          self.response.out.write(day)
        self.response.out.write("</td></tr>")

        for hour in range(0,24):
          hour = "%02d" % hour
          self.response.out.write("<tr>")
          self.response.out.write("<td>%s</td>" % hour)
          for day in sorted(list(responseMap)):
            self.response.out.write('<td valign="top">')
            if hour in responseMap[day]:
              limitter = 12
              for log in sorted(list(responseMap[day][hour])):
                limitter = limitter - 1
                if limitter < 0: break
                if responseMap[day][hour][log] == 0:
                  self.response.out.write("<font color=red>")
                  self.response.out.write(str(log)[14:19] + " ERR ")
                elif responseMap[day][hour][log] < 1.5 and responseMap[day][hour][log] != 0:
                  self.response.out.write("<font>")
                  self.response.out.write(str(log)[14:19] + " %.3f" % responseMap[day][hour][log])
                elif responseMap[day][hour][log] < 3:
                  self.response.out.write('<font color="#FF8135">')
                  self.response.out.write(str(log)[14:19] + " %.3f" % responseMap[day][hour][log])
                else:
                  self.response.out.write('<font color="#FFBF00">')
                  self.response.out.write(str(log)[14:19] + " %.3f" % responseMap[day][hour][log])
                self.response.out.write("</font><br />")
            self.response.out.write("</td>")
          self.response.out.write("</tr>")
        self.response.out.write("</table>")
        self.response.out.write('<div><a href="/">back</a></div>')
        self.response.out.write('</body></html>')

    class Cron(webapp.RequestHandler):
      def get(self):
        size = 5
        sites = db.GqlQuery("SELECT * FROM Site ORDER BY date DESC")
        for i in range(0, sites.count(), size):
          params = {}
          for j in range(0, size):
            if i + j >= sites.count():
              break
            params["url"+str(j)] = sites[i+j].url
          taskqueue.add(url='/worker', params = params)

    class Worker(webapp.RequestHandler):
      def post(self):
        if(int(self.request.headers.environ['HTTP_X_APPENGINE_TASKRETRYCOUNT']) > 0):
          return
        for key in self.request.arguments():
          try:
            url = self.request.get(key)
            fetchAndLog(url)
          except:
            pass
     
    class Add(webapp.RequestHandler):
      def get(self):
        self.response.out.write("""
          <form action="/add" method="post" enctype='multipart/form-data'>
            <div>Name: <input type="text" name="name" /></div>
            <div>URL: <input type="text" name="url" /></div>
            <div><input type="submit" value="submit"/></div>
          </form>"""
    )
      def post(self):
        if self.request.get('name') != '' or self.request.get('url') != '':
          site = Site()
          site.url = self.request.get('url')
          site.name = self.request.get('name')
          site.put();
        self.redirect('/')

    class Delete(webapp.RequestHandler):
      def get(self):
        Site.get(self.request.get('id')).delete()
        self.redirect('/')

    def fetchAndLog(url):
      log = Log()
      log.url = url
      log.response = 0.0
      log.put()
      start = datetime.now()
      ret = fetch(url = url, deadline = 30)
      end = datetime.now()
      diff = end - start
      log.response = diff.seconds + diff.microseconds / 1000000.0
      log.put()

    application = webapp.WSGIApplication([
      ('/', MainPage),
      ('/add', Add),
      ('/delete', Delete),
      ('/cron', Cron),
      ('/worker', Worker),
      ('/detail/(.*)', Detail),
    ], debug=True)

    def main():
      wsgiref.handlers.CGIHandler().run(application)

    if __name__ == '__main__':
      main()

    2. ZIPのアップローダー

    GAEって静的ファイルおけないんでしょ?ってよく言われるので、アップロードしたZIPファイルを展開して公開するサービスを作ってみた。ファイルはデータストアに保存しているので厳密な意味では静的ファイルではない。(静的ファイルをデプロイすることもできますが、その場合、GAEの管理ツールを使わないとファイルを更新できません。)

    zipアップローダー

    zipアップローダー

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    #!/usr/bin/env python

    import cgi
    import datetime
    import wsgiref.handlers

    from google.appengine.ext import db
    from google.appengine.api import users
    from google.appengine.ext import webapp
    from google.appengine.api import images

    class Entry(db.Model):
        content = db.StringProperty(multiline=True)
        date = db.DateTimeProperty(auto_now_add=True)
        data = db.BlobProperty()
        contentType = db.StringProperty()

    class MainPage(webapp.RequestHandler):
        def get(self):
            self.response.out.write('<html><body>[ <a href="http://blog.elearning.co.jp?s=add&amp;search_404=1">+</a> ]<br />')
            entries = db.GqlQuery("SELECT * FROM Entry ORDER BY date DESC LIMIT 10")
            for entry in entries:
                self.response.out.write('[<a href="#" onclick="if(confirm(\'Are you sure to delete it?\'))location.href=\'/delete?id=%s\'">d</a>][<a href="http://blog.elearning.co.jp?s=edit?id=%s&amp;search_404=1">e</a>] %s<br />' % (entry.key(), entry.key(), cgi.escape(entry.content)))
                if entry.data:
                    self.response.out.write('<a href="/image/%s/data"><img src="/image/%s/thumnail"/></a><br />' % (entry.key(),entry.key()))
            self.response.out.write("</body></html>")

    class Get(webapp.RequestHandler):
        def get(self, key):
            image = db.get(key)
            if type == 'thumnail':
                self.response.headers['Content-Type'] = 'image/jpeg'
                self.response.out.write(image.thumnail)
            else:
                if image.contentType:
                    self.response.headers['Content-Type'] = image.contentType.encode('utf-8')
                else:
                    self.response.headers['Content-Type'] = 'image/jpeg'
                self.response.out.write(image.data)

    class Add(webapp.RequestHandler):
        def get(self):
            self.response.out.write("""
                <form action="/add" method="post" enctype='multipart/form-data'>
                    <div><textarea name="content" rows="3" cols="60"></textarea></div>
                    <div><input type="file" name="file"/></div>
                    <div><input type="submit" value="submit"/></div>
                </form>"""
    )
        def post(self):
            if self.request.get('content') != '' or self.request.get('file') != '':
                entry = Entry()
                entry.content = self.request.get('content')
                if self.request.get('file'):
                    entry.data = self.request.POST.get('file').file.read()
                    img = images.Image(entry.data)
                    img.resize(60, 100)
                    entry.contentType = self.request.body_file.vars['file'].headers['content-type']
                entry.put();
            self.redirect('/')

    class Edit(webapp.RequestHandler):
        def get(self):
            post = Entry.get(self.request.get('id'))
            self.response.out.write("""
                <form action="/edit?id=%s" method="post">
                    <div><textarea name="content" rows="3" cols="60">%s</textarea></div>
                    <div><input type="submit" value="submit"/></div>
                </form>"""
    % (self.request.get('id'), post.content))
        def post(self):
            entry = Entry.get(self.request.get('id'))
            entry.content = self.request.get('content')
            entry.put()
            self.redirect('/')

    class Delete(webapp.RequestHandler):
        def get(self):
            Entry.get(self.request.get('id')).delete()
            self.redirect('/')

    application = webapp.WSGIApplication([
        ('/', MainPage),
        ('/add', Add),
        ('/get/([-\w]+)', Get),
    ], debug=True)

    def main():
        wsgiref.handlers.CGIHandler().run(application)

    if __name__ == '__main__':
        main()

    3. ブログシステム

    ブログシステムを作ってみた。一覧表示ページ、個別記事ページ、タグページぐらいしかないが、一般的な用途ならこれぐらいでいい気がする。RSSを配信しているので、FeedTweetなどを使ってTwttterに更新履歴を流したりもできる。Wordpressとなどと比べると機能も少ないし、デザインもテンプレート化できていないが、レスポンスはよいし、満足して使っている。

    python-blog

    python-blog

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    from google.appengine.ext import webapp
    from google.appengine.ext.webapp import util
    from google.appengine.ext import db
    from google.appengine.api import users
    import urllib, datetime, re
    step = 10
    title = "Python blog system"

    class AuthHandler(webapp.RequestHandler):
      def get(self, key = ""):
        if users.get_current_user() == None:
          self.write("<a href=\"%s\">Sign in or register</a>." % users.create_login_url("/admin"))
        elif users.is_current_user_admin() != True:
          self.write('Your account %s is not admin. <a href=\"%s\">Log out</a> and log in with an admin account.' % (users.get_current_user(), users.create_logout_url("/admin")))
        else:
          if key:
            self.get2(key)
          else:
            self.get2()
      def post(self, key = ""):
        if users.is_current_user_admin():
          if key:
            self.post2(key)
          else:
            self.post2()
      def write(self, str):
        self.response.out.write(str)

    class MainHandler(AuthHandler):
      def get(self, pageStr):
        try:
          page = int(pageStr)
        except ValueError:
          page = 0
        printHeader(self, title)
        self.write('<h1><a href="/">%s</a></h1>' % title)
        if users.is_current_user_admin():
          self.write('<h2>[<a href="http://blog.elearning.co.jp?s=&amp;search_404=1">New</a>] [<a href=\"%s\">Log out</a>]</h2>'  % users.create_logout_url("/"))
        entries = Entry.all().order("-datetime").fetch(step + 1, page * step)
        for entry in entries[:step]:
          printEntry(self, entry)
        if len(entries) > step:
          self.write('[ <a href="/%d"> Next %d</a> ]' % (page + 1, step))
        if page > 0:
          self.write('[ <a href="/%d"> Prev %d</a> ]' % (page - 1, step))
        printFooter(self)

    class RSSHandler(AuthHandler):
      def get(self, pageStr):
        self.write(
    u"""< ?xml version="1.0" encoding="utf-8"?>
    <rdf:rdf xmlns:dc="http://purl.org/dc/elements/1.1/"
      xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
      xmlns="http://purl.org/rss/1.0/">
      <channel rdf:about="http://python-blog-system.appspot.com/">
        <title>%(title)s</title>
        <link>http://python-blog-system.appspot.com</link>
        <description>%(title)s</description>
        <dc:language>ja</dc:language>
        <dc:creator>N/A</dc:creator>
        <dc:date>%(now)s</dc:date>
      </channel>"""
    % {'now' : '2010-12-31T16:57:12+09:00', 'title' : title})
        for entry in Entry.all().order("-datetime").fetch(30):
          self.write("""  <item rdf:about="http://python-blog-system.appspot.com/entry/%(key)s">
        <title>%(title)s</title>
        <link>http://python-blog-system.appspot.com/entry/%(key)s</link>
        <description>%(body)s</description>
        <dc:date>%(datetime)s</dc:date>
      </item>
    """
    % {"key" : entry.key(), "title" : h(entry.title), "body" : h(entry.body), "datetime" : entry.formattedDatetimeInJST})
        self.write("</rdf:rdf>")

    class AdminHandler(AuthHandler):
      def get2(self, key):
        self.redirect("/")

    class PostHandler(AuthHandler):
      def get2(self,key = ""):
        entry = Entry.get(key) if key != '' else Entry()
        title = "Edit Entry" if key!= '' else "New Entry"
        deleteButton = """<input type="button" value="delete" onclick="if(confirm('Are you sure to delete it?'))location.href='/delete/%s'"/>""" % key if key!= '' else ''
        printHeader(self, title)
        self.write("<h1>%s</h1>" % title)
        self.write(u"""<form method="post" action="/post/%(key)s">
    <h2>タイトル</h2><input type="text" name="title" value="%(title)s" style="width:400px"/>
    <h2>本文</h2><textarea name="body" style="width:400px;height:300px;">%(body)s</textarea>
    <h2>タグ</h2><input type="text" name="tags" value="%(tags)s" style="width:400px"/>
    <h2>画像 [<a href="http://blog.elearning.co.jp?s=uploader&amp;search_404=1" target="_blank">uploader</a>]</h2>
    <div>%(deleteButton)s<input type="submit" value="submit"/></div></form>"""
    % {"key" : key, "title" : entry.title, "body" : entry.body, "tags" : entry.tagStr(), "deleteButton":deleteButton})
        printFooter(self)
      def post2(self, key = ""):
        if self.request.get("title") != '' and self.request.get("body") != '':
          entry = Entry.get(key) if key != '' else Entry()
          entry.title = self.request.get("title")
          entry.body = self.request.get("body")
          entry.tags = []
          for tagStr in self.request.get('tags').replace(u' ',' ').replace('  ',' ').replace(',',' ').split(' '):
            tag = Tag.all().filter("tag =", tagStr).get()
            if tag == None:
              tag = Tag(tag = tagStr)
              tag.put()
            entry.tags.append(tag.key())
          entry.put()
        self.redirect('/')

    class PostCommentHandler(AuthHandler):
      def post(self, key):
        if self.request.get("comment") != '':
          Comment(
            entry = Entry.get(key),
            comment = self.request.get("comment"),
            delpass = self.request.get("delpass"),
            nickname = self.request.get("nickname")
          ).put()
        self.redirect("/entry/%s" % key)

    class DeleteHandler(AuthHandler):
      def get2(self, key):
        db.delete(Entry.get(key))
        self.redirect('/')

    class DeleteCommentHandler(AuthHandler):
      def post(self, key):
        comment = Comment.get(key)
        entry_key = comment.entry.key()
        self.write(self.request.get("delpass"))
        self.write(comment.delpass)
        if self.request.get("delpass") == comment.delpass:
          db.delete(comment)
        self.redirect('/entry/%s' % entry_key)

    class TagHandler(AuthHandler):
      def get(self, key):
        tagStr = urllib.unquote(key).decode('utf-8')
        title = "Python blog system / %s" % tagStr
        printHeader(self, title);
        tag = Tag.all().filter("tag =", tagStr).get()
        self.write('<h1><a href="/">Python blog system</a> / %s</h1>' % h(tagStr))
        if tag:
          for entry in tag.entries:
            printEntry(self, entry)
        else:
          self.write("<h2>Tag %s does not exist</h2>" % h(tagStr))
        printFooter(self)

    class EntryHandler(AuthHandler):
      def get(self, key):
        entry = Entry.get(key)
        printHeader(self, "%s / %s" % (title, entry.title));
        self.write('<h1><a href="/">%s</a></h1>' % title)
        printEntry(self, entry, commentDetail = True)
        printFooter(self)

    class UploaderHandler(AuthHandler):
      def get2(self, key):
        printHeader(self, "Uploader")
        self.write("<h1>Uploader</h1>")
        for image in Image.all():
          self.write('<h2><img src="/image/%(key)s"/><br /><input type="text" value="[img:%(key)s]" style="width:300px;font-size:x-small"/><input type="button" value="delete" onclick="location.href=\'/deleteImage/%(key)s\'"/></h2>' % {"key":image.key()})
        self.write(u'<h2><form action="/uploader" enctype="multipart/form-data" method="post"><input type="file" name="file"/><input type="submit" value="Upload"/></form></h2>')
        printFooter(self)
      def post2(self, key):
        if self.request.get('file'):
          self.write("hoge")
          image = Image()
          image.image = self.request.POST.get('file').file.read()
          image.contentType = self.request.body_file.vars['file'].headers['content-type']
          image.put()
        self.redirect('/uploader')

    class DeleteImageHandler(AuthHandler):
      def get2(self, key):
        Image.get(key).delete()
        self.redirect('/uploader')

    class ImageHandler(AuthHandler):
      def get(self, key):
        image = Image.get(key)
        self.response.headers['Content-Type'] = image.contentType.encode('utf-8')
        self.response.out.write(image.image)

    def printEntry(self, entry, commentDetail = False):
      user = users.get_current_user()
      if user:
        editLink = '[<a href="/post/%s">edit</a>]' % entry.key()
      else:
        editLink = ''
      self.write('<div class="entry">\n<div class="entryHeader">\n')
      self.write('<h2 class="title"><a href="/entry/%(key)s">%(title)s</a> %(editLink)s</h2> <div class="entryDate">%(datetime)s</div>'
        % {"key" : entry.key(), "title" : h(entry.title), "editLink" : editLink, "datetime" : entry.formattedDatetimeInJST})
      self.write('</div>\n')#header
      self.write(replaceImages(linkURLs(nl2br(h(entry.body)))));
      self.write('\n<div class="entryFooter">tag:\n')
      for tag in entry.tags:
        tagObj = Tag.get(tag)
        self.write('<a href="/tag/%s"><span class="tag">%s</span></a>\n' % (urllib.quote_plus(tagObj.tag.encode('utf-8')) ,h(tagObj.tag)))
      if commentDetail:
        self.write(u'<h2>コメント</h2><div class="comments"><a name="comments">\n')
        for comment in entry.comments.order('datetime'):
          if comment.nickname == None or comment.nickname == "":
            comment.nickname = "Anonymous"
          delbutton = u"""
            <div style="float:right">
              <form method="post" name="form" action="/deleteComment/%(key)s">
              <input type="text" name="delpass" class="delcommentpassword"/>
              <input type="button" onclick="if(confirm('本当に削除しますか?'))form.submit()" value="削除" class="delcommentbutton"/>
              </form>
            </div><br clear="all"/>"""
    % {"key":comment.key()}
          self.write('<h3 class="comment">%s: %s %s</h3>' % (comment.nickname, comment.comment, delbutton))
        self.write(u'<div style="width:84px;float:left;font-size:xx-small;position:relative;top:6px;">名前</div>')
        self.write(u'<div style="width:184px;float:left;font-size:xx-small;position:relative;top:6px;">コメント</div>')
        self.write(u'<div style="width:100px;float:left;font-size:xx-small;position:relative;top:6px;">削除パス</div>')
        self.write(u'<br clear="all"/>')
        self.write(u'<form action="/postComment/%s" method="post" style="padding:0">' % entry.key())
        self.write(u'<input type="text" name="nickname" style="width:80px;"/>')
        self.write(u'<input type="text" name="comment" style="width:180px;"/>')
        self.write(u'<input type="text" name="delpass" style="width:50px;"/>')
        self.write(u'<input type="submit" value="投稿" style="width:50px"/>')
        self.write("</form></a></div>\n")
      else:
        self.write(u'<a href="/entry/%s#comments">コメント(%s)</a>\n' % (entry.key(), entry.comments.count()))
      self.write("</div>\n")#footer
      self.write("</div>\n")#entry

    def urlReplacer(match, limit = 45):
      return '<a href="%s" target="_blank">%s</a>' % (match.group(), match.group()[:limit] + ('...' if len(match.group()) > limit else ''))

    def linkURLs(str):
      return re.sub(r'([^"]|^)(https?|ftp)(://[\w:;/.?%#&=+-]+)', urlReplacer, str)

    def replaceImages(str):
      return re.sub(r'\[img:(.*)\]', r'<img src="http://blog.elearning.co.jp?s=\1&amp;search_404=1" style="max-width:400px"/>', str)

    def printHeader(self, title):
      self.write("""< ?xml version="1.0" encoding="UTF-8"?>
    < !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
    <head>
      <link rel="alternate" type="application/rss+xml" title="RSS" href="rss"/>
      <meta http-equiv="content-script-type" content="text/javascript"/>
      <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
      <title>%s</title>
      <link rel="stylesheet" type="text/css" href="/css/style.css"/>
      <meta name = "viewport" content = "width=420"/>
      <script type="text/javascript">
      var _gaq = _gaq || [];
      _gaq.push(['_setAccount', 'UA-20245912-2']);
      _gaq.push(['_trackPageview']);
      (function() {
        var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
        ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
        var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
      })();
      </script>"""
    % (h(title)))
      self.write("</head>\n<body>\n");

    def printFooter(self):
      self.write(u'<div>developed by <a href="http://php6.jp/python/">python練習帳</a></div>\n</body>\n</html>')

    def nl2br(str):
      return str.replace('\r\n','\n').replace('\n','<br />\n')

    class Entry(db.Model):
      title = db.StringProperty(default = "")
      body = db.TextProperty(default = "")
      tags = db.ListProperty(db.Key)
      datetime = db.DateTimeProperty(auto_now_add = True)
      @property
      def formattedDatetimeInJST(self):
        return (self.datetime + datetime.timedelta(hours=9)).strftime("%Y-%m-%d %H:%M:%S")
      def tagStr(self):
        return " ".join([Tag.get(x).tag for x in self.tags])

    class Tag(db.Model):
      tag = db.StringProperty()
      @property
      def entries(self):
        return Entry.all().filter('tags', self.key()).order('-datetime')

    class Comment(db.Model):
      comment = db.TextProperty(default = "")
      entry = db.ReferenceProperty(Entry, collection_name = 'comments')
      user = db.UserProperty()
      datetime = db.DateTimeProperty(auto_now_add = True)
      delpass = db.TextProperty()
      nickname = db.TextProperty()

    class Image(db.Model):
      image = db.BlobProperty()
      contentType = db.StringProperty()

    def main():
      application = webapp.WSGIApplication([
        ('/tag/(.*)', TagHandler),
        ('/entry/(.*)', EntryHandler),
        ('/admin/?(.*)', AdminHandler),
        ('/postComment/?(.*)', PostCommentHandler),
        ('/post/?(.*)', PostHandler),
        ('/rss/?(.*)', RSSHandler),
        ('/deleteComment/?(.*)', DeleteCommentHandler),
        ('/deleteImage/(.*)', DeleteImageHandler),
        ('/delete/?(.*)', DeleteHandler),
        ('/uploader/?(.*)', UploaderHandler),
        ('/image/(.*)', ImageHandler),
        ('/(.*)', MainHandler)
      ], debug=True)
      util.run_wsgi_app(application)

    def h(html):
      return html.replace('&','&amp;').replace('< ','&lt;').replace('>','&gt;').replace('"','&quot;')

    if __name__ == '__main__':
      main()

    おわりに

    Pythonの良さをざっと紹介しましたが、どのように感じられましたでしょうか?C、Java、PHPなどに慣れた人にとっては異質なソースコードだと思います。でも、一度慣れてしまうと、すごく合理的で分かりやすい言語だと思います。このブログにPython関連の記事を書き続けるのは気が引けますので、興味を持たれた方はPython練習帳をご覧いただければと思います。

    SmartBrain/サムネイルアップロード

    CATEGORIES SmartBrain, 北海道ラボby.yasu.tanaka1 Comments2011.01.19

    SmartBrainには、コースのサムネイルを自動生成する機能があるのですが、教材の種類によってはうまくサムネイルを生成できない場合がありました。そこで、SmartBrain1.14(2011年2月中旬リリース予定)で、サムネイル画像のアップロード機能が追加される予定です。以下の画像は、開発環境で撮影したスクリーンショットなので、サムネイルの自動生成がうまく動作していませんが、手動でアップロードしたキバンのロゴはちゃんと表示されています。

    サムネイル

    サムネイル