Snippets

sironekotoro Perl入学式 2018 第5回 最終問題で色々やってみる

Created by sironekotoro

File bbs_custom.pm Added

  • Ignore whitespace
  • Hide word diff
+#!/usr/bin/env perl
+use Mojolicious::Lite;
+use Storable qw/nstore retrieve/;
+
+# Perl入学式 2018 第5回 最終問題
+# 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();
+
+# Cookie暗号化
+app->secrets( ['lkjiji!&F'] );
+
+# 書き込み内容をファイルに保存する
+sub save_file {
+    my $entries = shift;
+
+    nstore $entries, $data_file;
+}
+
+# ファイルから書き込み内容を取り出す
+sub load_file {
+
+    my $entries;
+
+    # ファイルが存在するとき
+    if ( -e $data_file ) {
+        return retrieve($data_file);
+    }
+}
+
+# ランダムな数字をもとに、数字英語大文字小文字入りの文字列を作る
+sub rundom_strings {
+    my $num = shift;
+
+    # 秘密の配列を作る
+    my @secret
+        = ( 0 .. 9, 'a' .. 'z', 'A' .. 'Z', 'Z' .. 'A', 0 .. 9, 'a' .. 'z' );
+
+    # 引数のランダムな数字を2桁ずつ配列に入れる
+    my @divided_num = $num =~ /.{2}/g;
+
+    # 返り値用のスカラー変数
+    my $strings;
+
+    for my $n (@divided_num) {
+
+        # 秘密の配列から2桁の数字を添え字にして
+        # 探し当てたものを文字列として連結
+        $strings .= $secret[$n];
+    }
+
+    return $strings;
+
+}
+
+# 書き込み表示用の日時・曜日表示
+sub formated_time {
+    my $time = shift;
+    my ( $sec, $min, $hour, $mday, $month, $year, $wday, $stime )
+        = localtime($time);
+
+    my @wdays = ( "日", "月", "火", "水", "木", "金", "土" );
+
+    my $formated_time = sprintf(
+        "%04d/%02d/%02d(%s) %02d:%02d:%02d",
+        $year + 1900,
+        $month + 1, $mday, $wdays[$wday], $hour, $min, $sec
+    );
+
+    return $formated_time;
+
+}
+
+# 百人一首をAPIから取得する
+sub get_hundred_poems {
+
+    # MojoliciousのWebユーザーエージェントを使う
+    my $ua = Mojo::UserAgent->new;
+
+    my $tx
+        = $ua->build_tx(
+        GET => 'http://api.aoikujira.com/hyakunin/get.php?fmt=json' );
+
+    $tx = $ua->start($tx);
+
+    if ( my $res = $tx->success ) {
+        return $res->json();
+    }
+}
+
+# Cookie内のtrackingIDで利用するランダムな数字を生成する
+sub random_number {
+    return int( rand() * 10**14 );
+}
+
+# トラッキングIDの有無を確認する
+# underサブルーチンを利用
+# http://d.hatena.ne.jp/perlcodesample/20140412/1396426029
+under sub {
+
+    # 引数のコントローラーオブジェクトを受け取る
+    my $c = shift;
+
+    # クッキーにID登録が無い場合には、IDを付与する
+    unless ( exists $c->session->{tracking_id} ) {
+        $c->session->{tracking_id} = random_number();
+    }
+
+};
+
+# 百人一首の中から、ランダムに一つをJSON返す
+get '/select_poem' => sub {
+    my $c = shift;
+
+    # 0〜99のランダムな数字を求める
+    my $index = int( rand(100) );
+
+    # 百人一首から一つ選ぶ
+    my $select_poem = $POEMS->[$index];
+
+    # JSON形式で返す(返した先ではajaxで解釈する)
+    $c->render(
+        json => {
+            number => $select_poem->{no},
+            name   => $select_poem->{sakusya},
+            mail   => '@example.com',
+            body   => $select_poem->{kami} . ' ' . $select_poem->{simo},
+        }
+    );
+};
+
+get '/' => sub {
+
+    # 引数のコントローラーオブジェクトを受け取る
+    my $c = shift;
+
+    # サブルーチンload_fileから書き込み内容を
+    # 配列リファレンスとして取得
+    my $entries = load_file();
+
+    # 配列リファレンスをテンプレート部で利用するために
+    # stashに保存する
+    $c->stash( entries => $entries, admin => $c->session->{admin} );
+
+    # indexテンプレートを描画する
+    $c->render('index');
+};
+
+# 書き込み投稿時に呼ばれるpostメソッド
+post '/post' => sub {
+
+    # 引数のコントローラーオブジェクトを受け取る
+    my $c = shift;
+
+    # ファイルから書き込み一覧を取り出して、
+    # 配列リファレンスに格納する
+    my $entries = load_file();
+
+    # mail欄にageとあった場合には書き込み一覧から
+    # shiftする
+    # (先頭の書き込みを削除する)
+    if ( $c->param('mail') eq 'age' ) {
+
+        shift @{$entries};
+    }
+
+    # 管理者ログインだった場合にはCookieに
+    # adminフィールドを追加して画面に戻る
+    # 書き込みは行わない
+    elsif ( $c->param('name') eq 'admin' && $c->param('mail') eq 'perl' ) {
+        $c->session->{admin} = 1;
+        $c->redirect_to('/');
+    }
+    # 管理者ログイン失敗したら画面に戻る
+    elsif ( $c->param('name') eq 'admin' && $c->param('mail') ne 'perl' ) {
+        delete $c->session->{admin};
+        $c->redirect_to('/');
+    }
+
+    # そうでない場合には書き込み内容を配列に追加する
+    else {
+        # 書き込み内容をコントローラから取得し
+        # ハッシュに格納する
+        my $time  = time();
+        my %entry = (
+            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'),
+            time   => $time,
+            jtime  => formated_time($time),
+        );
+
+# ハッシュをリファレンス化して、書き込み一覧の配列リファレンスの
+# 末尾に追加する
+        push @{$entries}, \%entry;
+    }
+
+    # 書き込みに通し番号をつける
+    my $serial = 0;
+    for my $entry ( @{$entries} ) {
+        $entry->{serial} = $serial;
+        $serial++;
+    }
+
+    # サブルーチンsave_fileを用いて、
+    # 配列をファイルに記録する
+    save_file($entries);
+
+    # 投稿時の処理が終わったので、/ に
+    # リダイレクトする
+    $c->redirect_to('/');
+
+};
+
+# ログアウト時の処理
+# Cookieの中のadminフィールドを0にする
+post '/logout' => sub {
+    my $c = shift;
+    $c->session->{admin} = 0;
+    $c->redirect_to('/');
+
+};
+
+post '/delete' => sub {
+    my $c = shift;
+
+    # 削除する書き込みの配列インデックス
+    my $index = $c->param('serial');
+
+    # サブルーチンload_fileから書き込み内容を
+    # 配列リファレンスとして取得
+    my $entries = load_file();
+
+    # 配列リファレンスから、特定のインデックスを持つ要素を削除する
+    splice( @{$entries}, $index, 1 );
+
+    # 削除処理が終わった配列リファレンスをファイルに保存する
+    save_file($entries);
+
+    $c->redirect_to('/');
+};
+
+app->start;
+
+__DATA__
+
+@@ index.html.ep
+% layout 'default';
+% 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' ,
+          target => '_blank'
+       )
+  %>
+
+  %# 管理者ログイン状態によってボタンの表示を制御する
+  % if ( $admin ){
+    %= form_for '/logout' =>( method => 'POST') => begin
+      %= submit_button 'ログアウト' => (class => 'btn btn-warning float-right mx-auto')
+    % end
+  % }
+
+% end
+
+%= t div => (class => 'display-2')  =>  begin
+  %= t h2 => '新規投稿'
+% end
+
+%= 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')
+    % end
+  % }
+
+  %= 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') => '入力内容を消す'
+
+  % end
+
+% end
+
+%= t div => (class => 'display-2')  =>  begin
+  %= t h2 => '投稿一覧'
+% end
+
+<%#
+ コントローラ部で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') => '名前'
+        %= $entry->{name}
+      % end
+
+      %= t div => (class => 'col-sm card-text') => begin
+        %= t span => (class => 'badge badge-secondary') => '日時'
+        %= $entry->{jtime}
+      % end
+
+      %= t div => (class => 'col-sm card-text') => begin
+        %= t span => (class => 'badge badge-secondary') => 'ID'
+        %= $entry->{id}
+      % end
+
+      %# 管理者ログイン時には削除ボタンを表示する
+      % if ($admin) {
+        %= form_for '/delete' =>( method => 'POST') => begin
+        %= hidden_field 'serial' , $entry->{serial}
+          %= submit_button '削除' => (class => 'btn btn-danger float-right mx-auto')
+        % end
+      % }
+
+    % end
+
+    %= t div => (class => 'card-body card-text') => $entry->{body}
+
+  % end
+
+% }
+
+
+@@ layouts/default.html.ep
+<!DOCTYPE html>
+<html>
+  <head>
+    <title><%= title %></title>
+
+    %# bootstrapに必要となるな外部リンク
+    <%= stylesheet 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css' => (
+      integrity => 'sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO',
+      crossorigin => 'anonymous'
+      ) %>
+
+    %= stylesheet begin
+      // div { border: 1px solid red;}
+    % end
+
+  </head>
+  <body class="container">
+    <%= content %>
+
+    %# 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'
+    ) %>
+
+
+
+
+    %= javascript begin
+    %# ページ表示が完了後、イベントリスナーを設定
+    'use strict';
+
+    %# JSで操作する対象となるパーツ
+    const inputName = document.getElementById('name');
+    const inputMail = document.getElementById('mail');
+    const inputBody = document.getElementById('body');
+
+    %# JSで操作する対象となるボタン
+    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);
+    }
+
+    %# 入力欄に短歌を記入する関数
+    function fillPoem(){
+      %# /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を取得し、
+    %# コールバックに渡すajax関数
+    function ajax(url,callback){
+      let xhr = new XMLHttpRequest();
+      xhr.open('GET',url,true);
+      xhr.onload = () => {
+        callback(JSON.parse(xhr.responseText));
+      };
+      xhr.onerror = () => {
+        alert('error');
+      };
+      xhr.send(null);
+    }
+
+    %# 「入力内容を消す」ボタンを押したときの挙動
+    %# 引数のpoemデータをもとに入力欄を削除する
+    function clearInputString(){
+      inputName.value = '';
+      inputMail.value = '';
+      inputBody.value = '';
+      %# 投稿ボタンを無効にする
+      %# submitButton.setAttribute('disabled','');
+      submitButtonOnOff();
+
+      %# 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');
+      }else{
+        submitButton.setAttribute('disabled','');
+      }
+
+    }
+
+    %# name欄に admin と入力されたらmail欄のtypeをpasswordにする
+    %# mail欄のplaceholderをpasswordに変える
+    %# 投稿ボタンを押せるようにする
+    function adminLogin(){
+      if (inputName.value === 'admin'){
+        inputMail.type        = 'password';
+        inputMail.placeholder = 'password';
+        inputBody.value       = 'log in?';
+        submitButton.removeAttribute('disabled');
+        inputBody.setAttribute('disabled','');
+
+      }else{
+        inputMail.type = 'text';
+        inputMail.placeholder = 'mail';
+      };
+    }
+
+
+    %# adminログインしたら投稿欄消すとか
+    %# admin専用の名前とハッシュをinput欄に入れるとか
+    %# adminは投稿できないほうが良い?
+
+    % end
+  </body>
+</html>
HTTPS SSH

You can clone a snippet to your computer for local editing. Learn more.