+use Storable qw/nstore retrieve/;
+# https://github.com/perl-entrance-org/workshop-2018/blob/master/5th/slide.md#%E6%9C%80%E7%B5%82%E5%95%8F%E9%A1%8C
+my $data_file = app->home->rel_file('bbs_custom_storable');
+# APIの負荷を避けるため、一度だけ全短歌を取得しておく
+my $POEMS = get_hundred_poems();
+app->secrets( ['lkjiji!&F'] );
+ nstore $entries, $data_file;
+ return retrieve($data_file);
+# ランダムな数字をもとに、数字英語大文字小文字入りの文字列を作る
+ = ( 0 .. 9, 'a' .. 'z', 'A' .. 'Z', 'Z' .. 'A', 0 .. 9, 'a' .. 'z' );
+ # 引数のランダムな数字を2桁ずつ配列に入れる
+ my @divided_num = $num =~ /.{2}/g;
+ for my $n (@divided_num) {
+ $strings .= $secret[$n];
+ my ( $sec, $min, $hour, $mday, $month, $year, $wday, $stime )
+ my @wdays = ( "日", "月", "火", "水", "木", "金", "土" );
+ my $formated_time = sprintf(
+ "%04d/%02d/%02d(%s) %02d:%02d:%02d",
+ $month + 1, $mday, $wdays[$wday], $hour, $min, $sec
+ # MojoliciousのWebユーザーエージェントを使う
+ my $ua = Mojo::UserAgent->new;
+ GET => 'http://api.aoikujira.com/hyakunin/get.php?fmt=json' );
+ if ( my $res = $tx->success ) {
+# Cookie内のtrackingIDで利用するランダムな数字を生成する
+ return int( rand() * 10**14 );
+# http://d.hatena.ne.jp/perlcodesample/20140412/1396426029
+ # 引数のコントローラーオブジェクトを受け取る
+ # クッキーにID登録が無い場合には、IDを付与する
+ unless ( exists $c->session->{tracking_id} ) {
+ $c->session->{tracking_id} = random_number();
+# 百人一首の中から、ランダムに一つをJSON返す
+get '/select_poem' => sub {
+ my $index = int( rand(100) );
+ my $select_poem = $POEMS->[$index];
+ # JSON形式で返す(返した先ではajaxで解釈する)
+ number => $select_poem->{no},
+ name => $select_poem->{sakusya},
+ mail => '@example.com',
+ body => $select_poem->{kami} . ' ' . $select_poem->{simo},
+ # 引数のコントローラーオブジェクトを受け取る
+ # サブルーチンload_fileから書き込み内容を
+ my $entries = load_file();
+ # 配列リファレンスをテンプレート部で利用するために
+ $c->stash( entries => $entries, admin => $c->session->{admin} );
+ # 引数のコントローラーオブジェクトを受け取る
+ my $entries = load_file();
+ # mail欄にageとあった場合には書き込み一覧から
+ if ( $c->param('mail') eq 'age' ) {
+ # 管理者ログインだった場合にはCookieに
+ elsif ( $c->param('name') eq 'admin' && $c->param('mail') eq 'perl' ) {
+ $c->session->{admin} = 1;
+ elsif ( $c->param('name') eq 'admin' && $c->param('mail') ne 'perl' ) {
+ delete $c->session->{admin};
+ # そうでない場合には書き込み内容を配列に追加する
+ row_id => $c->session->{tracking_id},
+ id => rundom_strings( $c->session->{tracking_id} ),
+ name => $c->param('name'),
+ mail => $c->param('mail'),
+ body => $c->param('body'),
+ jtime => formated_time($time),
+# ハッシュをリファレンス化して、書き込み一覧の配列リファレンスの
+ push @{$entries}, \%entry;
+ for my $entry ( @{$entries} ) {
+ $entry->{serial} = $serial;
+# Cookieの中のadminフィールドを0にする
+ $c->session->{admin} = 0;
+ my $index = $c->param('serial');
+ # サブルーチンload_fileから書き込み内容を
+ my $entries = load_file();
+ # 配列リファレンスから、特定のインデックスを持つ要素を削除する
+ splice( @{$entries}, $index, 1 );
+ # 削除処理が終わった配列リファレンスをファイルに保存する
+% title 'Perl入学式 2018 第5回 最終問題';
+ divタグを使ってページをいくつかの要素に分割し、
+ class要素にCSSフレームワークBootstrap4の
+ htmlのタグ、例えば<h1>hogehoge</h1>などを直接
+ テンプレート部に書くこともできるが、練習として全て
+%# begin から end までがタグで囲まれた状態になる
+%= t div => (class => 'display-1') => begin
+ %# タグヘルパーを利用。 <h1>Perl入学式 2018 第5回 最終問題</h1> と同義
+ %= t h1 => (class => 'align-middle') => 'Perl入学式 2018 第5回 最終問題'
+ %# 元の問題文へのリンクをBootstrapでボタン風デザインにする
+ <%= link_to 問題文 => 'https://github.com/perl-entrance-org/workshop-2018/blob/master/5th/slide.md#%E6%9C%80%E7%B5%82%E5%95%8F%E9%A1%8C'
+ class => 'btn btn-info float-right mx-auto' ,
+ %# 管理者ログイン状態によってボタンの表示を制御する
+ %= form_for '/logout' =>( method => 'POST') => begin
+ %= submit_button 'ログアウト' => (class => 'btn btn-warning float-right mx-auto')
+%= t div => (class => 'display-2') => begin
+%= form_for '/post' =>( method => 'POST') => begin
+ %# name,mail,body 3つのinputタグをfor文でまとめて描画する
+ % for my $item ( qw(name mail body) ){
+ %= t div => (class => 'form-group') => begin
+ %= text_field $item => ( id => $item , placeholder => $item, class => 'form-control')
+ %= t div => (class => 'form-group') => begin
+ %= t button => (type =>'submit' ,class => 'btn btn-info',id => 'submitButton' , disabled => '' ) => '投稿する'
+ %# テスト投稿を楽にするボタンをJavascriptで実装した
+ %= t button => (type =>'button',class => 'btn btn-info',id => 'getPoemButton') => 'ダミーデータ入力用ボタン(JavaScript)'
+ %= t button => (type =>'button',class => 'btn btn-info',id => 'clearButton') => '入力内容を消す'
+%= t div => (class => 'display-2') => begin
+ コントローラ部でstashに格納した $entriesを
+ デリファレンスして配列に戻し、for文で取り出して表示する
+% for my $entry ( @{$entries} ){
+ %= t div => (class => 'card mb-2') => begin
+ %= t div => (class => 'card-header row m-0') => begin
+ %= t div => (class => 'col-sm card-text') => begin
+ %= t span => (class => 'badge badge-secondary') => '名前'
+ %= t div => (class => 'col-sm card-text') => begin
+ %= t span => (class => 'badge badge-secondary') => '日時'
+ %= t div => (class => 'col-sm card-text') => begin
+ %= t span => (class => 'badge badge-secondary') => 'ID'
+ %# 管理者ログイン時には削除ボタンを表示する
+ %= form_for '/delete' =>( method => 'POST') => begin
+ %= hidden_field 'serial' , $entry->{serial}
+ %= submit_button '削除' => (class => 'btn btn-danger float-right mx-auto')
+ %= t div => (class => 'card-body card-text') => $entry->{body}
+@@ layouts/default.html.ep
+ <title><%= title %></title>
+ %# bootstrapに必要となるな外部リンク
+ <%= stylesheet 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css' => (
+ integrity => 'sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO',
+ crossorigin => 'anonymous'
+ // div { border: 1px solid red;}
+ <body class="container">
+ %# bootstrapに必要となるな外部リンク
+ <%= javascript 'https://code.jquery.com/jquery-3.3.1.slim.min.js' => (
+ integrity => 'sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo',
+ crossorigin => 'anonymous'
+ %# bootstrapに必要となるな外部リンク
+ <%= javascript 'https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js' => (
+ integrity => 'sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49',
+ crossorigin => 'anonymous'
+ %# bootstrapに必要となるな外部リンク
+ <%= javascript 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js' => (
+ integrity => 'sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy',
+ crossorigin => 'anonymous'
+ %# ページ表示が完了後、イベントリスナーを設定
+ const inputName = document.getElementById('name');
+ const inputMail = document.getElementById('mail');
+ const inputBody = document.getElementById('body');
+ const submitButton = document.getElementById('submitButton');
+ const getPoemButton = document.getElementById('getPoemButton');
+ const clearButton = document.getElementById('clearButton');
+ %# 画面が表示されたらイベントリスナーを登録する
+ window.onload = () => {
+ %# getPoemButton:ダミーデータを入力欄に入れる
+ getPoemButton.addEventListener('click',fillPoem,false);
+ %# clearButton:入力欄を空欄にする
+ clearButton.addEventListener('click',clearInputString,false);
+ %# inputName:投稿ボタンの有効/無効化
+ inputName.addEventListener('change',submitButtonOnOff,false);
+ %# inputName:nameに admin と入力された際にmail入力欄のtypeを変更
+ inputName.addEventListener('change',adminLogin,false);
+ %# inputMail:投稿ボタンの有効/無効化
+ inputMail.addEventListener('change',submitButtonOnOff,false);
+ %# inputBody:投稿ボタンの有効/無効化
+ inputBody.addEventListener('change',submitButtonOnOff,false);
+ %# /select_poemにアクセスし、poemデータを取得
+ %# 取得したpoemデータをfillPoemData関数に渡す
+ ajax('select_poem', (poem)=>{fillPoemData(poem)} );
+ %# 投稿ボタンを有効にする(disabledをremoveする)
+ submitButton.removeAttribute('disabled');
+ %# 引数のpoemデータをもとに入力欄を埋める
+ function fillPoemData(poem){
+ inputName.value = poem.name;
+ inputMail.value = poem.mail;
+ inputBody.value = poem.body;
+ %# 非同期でwebページにアクセスしてJSONを取得し、
+ function ajax(url,callback){
+ let xhr = new XMLHttpRequest();
+ xhr.open('GET',url,true);
+ callback(JSON.parse(xhr.responseText));
+ %# 「入力内容を消す」ボタンを押したときの挙動
+ %# 引数のpoemデータをもとに入力欄を削除する
+ function clearInputString(){
+ %# submitButton.setAttribute('disabled','');
+ %# adminログイン試行後だったらメール欄の状態passwordから戻す
+ inputMail.type = 'text';
+ inputMail.placeholder = 'mail';
+ inputBody.removeAttribute('disabled');
+ function submitButtonOnOff(){
+ %# name,mailの入力欄の文字数が双方とも0以上であれば有効
+ const nameLength = inputName.value.length;
+ const mailLength = inputMail.value.length;
+ const bodyLength = inputBody.value.length;
+ if ( nameLength > 0 && mailLength > 0 && bodyLength > 0 ){
+ submitButton.removeAttribute('disabled');
+ submitButton.setAttribute('disabled','');
+ %# name欄に admin と入力されたらmail欄のtypeをpasswordにする
+ %# mail欄のplaceholderをpasswordに変える
+ if (inputName.value === 'admin'){
+ inputMail.type = 'password';
+ inputMail.placeholder = 'password';
+ inputBody.value = 'log in?';
+ submitButton.removeAttribute('disabled');
+ inputBody.setAttribute('disabled','');
+ inputMail.type = 'text';
+ inputMail.placeholder = 'mail';
+ %# admin専用の名前とハッシュをinput欄に入れるとか