Naomi's notebook

Naomi's notebook

GASで自己紹介スライド

Google Formでなんらかの情報を記入するとGoogleスプレッドシートにその結果を出力することができますね. その情報をまとめたスライドを自動で生成するようなスクリプトをGASで書いたのでメモしておきます.

スプレッドシートの情報読み込み

スプレッドシートから情報を読み込み,学籍番号(8カラム目)でソートします.これは調べれば簡単でした.

 let spread_sheet = SpreadsheetApp.openById("id")
 let sheet = spread_sheet.getActiveSheet()
 let range = sheet.getRange(2,2,sheet.getLastRow()-1,sheet.getLastColumn()-1)
 let profiles = range.getValues()
 profiles.sort(function(a,b){//番号の逆順でソート->番号順にスライドができます.
  if(a[7] < b[7]) return +1;
  if(a[7] > b[7]) return -1;
  return 0;
 })

スライドの作成

今回はデザインが楽に作れるように,1枚目にタイトル,2枚目にテンプレートを手で作り,2枚目のスライドをコピーしてその値を変更することで各ページを作っていきます.

あまり調べてもスライドをいじっている人がいなかったので,ここからは公式ドキュメント

developers.google.com とにらめっこして作りました.

どうやら,

Presentation
└──Slide
      ├──Shape
      ├──Image
      └──.....

という構造になっているようです.textなどは全てShapeとして取得できます.

どの要素がどの番号のShapesなのかは,少しずつ番号をずらすなどして調べました.(なんらかの法則があるんですかね?)スライドをduplicateしてもshapesの順番は保持されるっぽいので,この番号を用いて要素を編集し,文字を書き換えることにしました.

//スライドの作成
 let presentation = SlidesApp.openById("id")
 Logger.log(presentation.getName())
 //最初の2ページ以外を削除
 let oldslides=presentation.getSlides()
 for(let i=2;i<oldslides.length;i++){
   oldslides[i].remove()
 }
 let template=oldslides[1]
 template.getImages()[0].remove()
 for(let i=0;i<profiles.length;i++){
   //新しいページの作成
   let newslide=template.duplicate()
   let shapes=newslide.getShapes()
   shapes[5].getText().setText(profiles[i][0]);
   shapes[4].getText().setText(profiles[i][2]);
   shapes[15].getText().setText(profiles[i][3]);
   shapes[13].getText().setText(profiles[i][4]);
   shapes[14].getText().setText(profiles[i][5]);//Twitter ID
   shapes[14].setLinkUrl("https://twitter.com/"+profiles[i][5])
   //画像
   var url = profiles[i][1]
   var eqPos = url.indexOf("=")
   var id = url.substring(eqPos+1)
   var img = DriveApp.getFileById(id).getBlob()
   newslide.insertImage(img,25,125,200,200)
 }
 //テンプレの削除
  template.remove()

このようなスライドができます. 文字数が多すぎるとスライドに入りきりませんが,今回は見栄えの良い文字数までフォームの方で文字数制限をすることでなんとかしました.

結果

この関数を実行すると,以下のようなスライドが入力した人数の枚数生成されます. f:id:Naomi_Lilienthal:20200919201403p:plain

Docker+reactでhello world

dockerとdocker-composeはインストールしてある状況から始めます.

環境の準備

dockerを使ってreactの環境を作ります.まず以下のようにファイルを作ります.

react_portfolio
├── Dockerfile
└── docker-compose.yml

Dockerfileに,コンテナの構成情報を書き込みます.

FROM node:14.9.0-alpine3.10 
WORKDIR /usr/src/app

docker-compose.ymlに,dockerビルドやコンテナ起動のオプションを書き込みます.サーバーで確か3000とかは既に使ってしまっていると思うので,適当なポートを指定します.

version: '3'
services:
  node:
    build:
      context: .
      dockerfile: Dockerfile
    tty: true
    environment:
      - NODE_ENV=production
    volumes:
    - ./:/usr/src/app
    command: sh -c "cd react_portfolio &&PORT=7984 yarn start"
    ports:
    - "7984:7984"

以下のコマンドを実行します.

docker-compose build

これにより,dockerのreact開発用コンテナがビルドされます. 次に,reactの新規アプリの雛形を作ります.

docker-compose run --rm node sh -c 'npm install -g create-react-app && create-react-app react_portfolio'

こうすると以下のようになったはずです

react_portfolio
├── Dockerfile
└── docker-compose.yml
└──react_portfolio
          ├──README.md
          ├──package.json
          ├──node_modules
          ├──src
          ├──public
          └──yarn.lock

コンテナを起動してみます

docker-compose up

http://localhost:7984でreactのデフォルトページを見ることができました.

環境の移設

これをgithubで管理したものをさくらのサーバ側でcloneします. .gitignoreは以下のようにします.

# dependencies
react_portfolio/node_modules
react_portfolio/.pnp
.pnp.js

# testing
react_portfolio/coverage

# production
react_portfolio/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

サーバ側の/var/www/appでgit cloneして,

cd react_portfolio
npm install
sudo docker-compose build

起動できることが確認できます.

本当にこんな感じでいいのかな…?

Hello World

src/App.jsを書き換えます

function App() {
  const name ="Naomi";
  const element=<h1>Hello, {name}!</h1>
  return (
    element
  );
}

ついでにApp.test.jsもこれが通るように書き換えます

test('renders Naomi', () => {
  const { getByText } = render(<App />);
  const linkElement = getByText(/Naomi/i);
  expect(linkElement).toBeInTheDocument();
});

docker-compose upで起動すると"Hello,Naomi!"と表示されます

self._sslobj.do_handshake() OSError: [Errno 0] Error

apache+flask+geventでアプリを公開しようとしているとき、proxypassで指定したアドレスにアクセスするとエラーが起こる (直接:8000にアクセスすると見られる)

Traceback (most recent call last):
  File "src/gevent/greenlet.py", line 854, in gevent._gevent_cgreenlet.Greenlet.run
  File "/home/Naomi/.linuxbrew/Cellar/python@3.8/3.8.5/lib/python3.8/site-packages/gevent/baseserver.py", line 34, in _handle_and_close_when_done
    return handle(*args_tuple)
  File "/home/Naomi/.linuxbrew/Cellar/python@3.8/3.8.5/lib/python3.8/site-packages/gevent/server.py", line 233, in wrap_socket_and_handle
    with _closing_socket(self.wrap_socket(client_socket, **self.ssl_args)) as ssl_socket:
  File "/home/Naomi/.linuxbrew/Cellar/python@3.8/3.8.5/lib/python3.8/site-packages/gevent/_ssl3.py", line 793, in wrap_socket
    return SSLSocket(sock=sock, keyfile=keyfile, certfile=certfile,
  File "/home/Naomi/.linuxbrew/Cellar/python@3.8/3.8.5/lib/python3.8/site-packages/gevent/_ssl3.py", line 311, in __init__
    raise x
  File "/home/Naomi/.linuxbrew/Cellar/python@3.8/3.8.5/lib/python3.8/site-packages/gevent/_ssl3.py", line 307, in __init__
    self.do_handshake()
  File "/home/Naomi/.linuxbrew/Cellar/python@3.8/3.8.5/lib/python3.8/site-packages/gevent/_ssl3.py", line 663, in do_handshake
    self._sslobj.do_handshake()
OSError: [Errno 0] Error

httpsサーバへリバースプロキシするするときはapacheの設定を変える必要があるらしい。 以下の4行(ProxyPass部分を除いて)を追加する。

ProxyRequests Off
SSLProxyEngine On
SSLProxyCheckPeerCN off
SSLProxyCheckPeerName off
ProxyPass /CovidGraph/ https://0.0.0.0:8000/
ProxyPass /LiHabit/ https://0.0.0.0:9292/

こうするとちゃんと動いた。

SSL routines:tls_post_process_client_hello:no shared cipher

Let's encrypt で証明書を取得し、railsアプリを起動しようとしたとき

bundle exec pumactl start -e production

すると

SSL routines:tls_post_process_client_hello:no shared cipher

というエラーが出てページが見られない場合

ちなみにsudoで実行するとちゃんとページが見られるという場合の解決策です

証明書の権限を確認する(シンボリックリンクの方)

これはやっているかと思いますが

ls -l /etc/letsencrypt/live/<ドメイン名>/

を確認し、ユーザがアクセスできないようなら権限を変更しましょう。

証明書本体の権限を確認する

よくわかっていなかったためここで詰まった

ls -lを行なった情報を見ると、 /etc/letsencrypt/live/<ドメイン名>/以下のファイルはシンボリックリンクであり、アクセスすると/etc/letsencrypt/archive/<ドメイン名>/privkey2.pemなどに転送されるようです。(更新のたびにファイルが変わるのを防ぐためと思われる)

こちらのファイルの権限も同様に確認し、変更しましょう。

AWS Hands-on for Beginners 〜 スケーラブルウェブサイト構築編 〜

ハンズオン資料 | AWS クラウドサービス活用資料集 をやってみた

詳しい内容を書くと怒られる可能性があるのではないかと思うため、概要、メモや感想のみ書きます。

概要

WordPressを具体例に、

を利用し、スケーラブルなwebシステムの構築を行う

環境

使っていないAWS Educate Starter Accountを使いました。

メモ

丁寧に説明されたので特につまるところはありませんでした。

Unable to describe the availability zones in this region

私のAWSEducateのアカウントでは東京リージョンだとUnable to describe the availability zones in this regionとなってしまうので、米国(バージニア北部)リージョンを選択しました。AZ(アベイラビリティーゾーン)はus-east-1a, us-east-1cを利用しました。

VPCって何をやるの?

仮想ネットワーク内で AWS リソースを起動するためのもの。この上に配置したサブネット(VPC の IP アドレス範囲のセグメントで、セキュリティや運用上のニーズに基づいてリソースをグループ化するために指定する。)について(インターネットゲートウェイを用いて)サブネットとインターネットを接続したり、サブネット同士で通信したりできるようになる。

セキュリティグループって?

インスタンストラフィックを制御するファイアウォールのルールセット。特定のトラフィックに対してインスタンスへの到達を許可するルールを追加できる。(たとえば、ウェブサーバーをセットアップして、インターネットトラフィックインスタンスへの到達を許可する場合、HTTP および HTTPS ポートに無制限のアクセス権限を与える。)

今回はweb-user1(EC2インスタンス用)とdb-user1(データベース用)とelb-user1(ロードバランサー用)を作成した。たとえば、データベース用セキュリティーグループでは、ソースとしてEC2インスタンス用セキュリティグループを選択して、EC2インスタンスのみMySQLのポートを許可していた。

dbサブネットグループは何のために作るの?

選択した VPC で DB インスタンスが使用できるサブネットと IP 範囲を定義するため。 VPC の DB インスタンスの使用 - Amazon Relational Database Service

ロードバランサーって?

ロードバランサに着信したパケットを複数の実サーバへ振り分ける。webサーバの冗長化や負荷分散に使う。

ターゲットグループって?

ロードバランサ配下に接続するインスタンスを選択するためのグループ。

感想

ハンズオン自体は演習もないのですぐ終わりました(動画の長さは2時間半くらい) AWSを触ることができました。クラウドサービス全般についての知識もなかったのでこれを機に勉強ができてよかったです。

Ruby on Rails tutorial +alpha(8)ストップウォッチの実装

注意:この記事はメモなので、この記事に書いてあることをやっただけでは同じものができません。(大体の雰囲気は書いてあると思います。)

注意:全くのweb開発初心者なので、間違っているところなどがあるかと思いますが、教えていただけると嬉しいです。

naomi-notebook.hatenablog.com

ストップウォッチを使って生活記録をつけられるようにします。開始時刻はデータベースに記録され、画面を閉じてもう一回開いても開始時刻からの時間が表示されているようにします。

まず、普通のタイマーをjavascriptで作ります。コントローラtimerを作り、get '/timer', to: 'timer#show'をルーティングします。このビューにタイマーを作ります。

javascriptでストップウォッチを作ってみる。忘備録 - Qiita これを参考にしました。

starttimeとtimetoaddをuserごとにデータベースに保存すると良さそうです。一つずつしかないので、usersデータベースに保存してしまいましょう。

rails g migration AddTimerToUsers timer_start:datetime timer_add:integer 
rails db:migrate

routesにpost '/timer', to: 'timer#edit'を追加し、editでデータベースに記録します。

【Rails】Rails⇔JavaScript間で時間データを渡す方法 - Qiita ここら辺も参考にしました。

とりあえずこれでタイマー機能ができました。viewとcontrollerを載せておきます。

<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>ストップウォッチで記録をつける</title>
</head>
<body>
    <div id="timer">00時間00分00</div>
    <button id="start">start</button>
    <button id="stop">stop</button>
    <button id="reset">reset</button>
</body>
</html>

<script>
(function(){
    'use strict';
    var timer = document.getElementById('timer');
    var start = document.getElementById('start');
    var stop = document.getElementById('stop');
    var reset = document.getElementById('reset');

    var startTime=<%=@user.timer_start ? @user.timer_start.to_f*1000 : 0 %>;//開始時刻を記録する
    var elapsedTime = 0;//経過時刻
    var timerId;//タイマーを止める用のタイマーのid
    var timeToadd=<%=@user.timer_add ? @user.timer_add : 0%>;
    var started=false;
    <% if @user.timer_start then %>
        var started=true;
        countUp();
    <% elsif @user.timer_add then %>
        elapsedTime = timeToadd;
        updateTimetText();
    <% end %>
    function updateTimetText(){//ミリ秒を時間:分:秒に直す
        var h = Math.floor(elapsedTime / 3600000);
        var m = Math.floor(elapsedTime %3600000 / 60000);
        var s = Math.floor(elapsedTime % 60000 / 1000);

        //2桁ずつ表示させる
        h = ('00' + h).slice(-2);
        m = ('00' + m).slice(-2); 
        s = ('00' + s).slice(-2);
        timer.textContent = h + '時間' + m + '分' + s;
    }


    //再帰的に使える用の関数
    function countUp(){
        timerId = setTimeout(function(){
            elapsedTime = Date.now() - startTime + timeToadd;
            updateTimetText()
            countUp();
        },1000);//一秒おきに実行
    }

    //todo:timer_startにtime_startを、timer_addにtime_addを記録
    function recordState(time_start,time_add){
        $.ajax({
            url: "timer",
            type: "POST",
            data: {timer_start:time_start,timer_add:time_add},
            dataType: "html"
        });
    }

    start.addEventListener('click',function(){
        started=true;
        startTime = Date.now();
        recordState(startTime,timeToadd);
        clearTimeout(timerId);
        countUp();
    });

    stop.addEventListener('click',function(){
        if(started){//タイマーが止まっている時は何もしない
            clearTimeout(timerId);
            timeToadd += Date.now() - startTime;
            recordState("nil",timeToadd);
            started=false;
        }else{
            console.log("stopの押し過ぎはやめてください")
        }
    });

    reset.addEventListener('click',function(){
        clearTimeout(timerId);
        elapsedTime = 0;
        timeToadd = 0;
        recordState("nil","nil");
        updateTimetText();
        started=false;
    });
})();
</script>
class TimerController < ApplicationController
    def show
        @user=current_user
        render "timer/show"
    end
    def edit
        params.permit(:timer_start,:timer_add)
        time=nil
        add=nil
        time=Time.at(params[:timer_start].to_i/1000).in_time_zone if params[:timer_start]!="nil" 
        add =params[:timer_add].to_i  if params[:timer_add]!="nil" 
        current_user.update_attributes(timer_start:time,timer_add:add)
    end
end

ここから、ログインしたユーザしか見られなかったり、生活記録を(今の時間-計測した時間)から(今の時間)までの長さでつけられるように書いていきます。

jQuery内からRailsのActionを叩く - Qiita 【Rails】ajax通信 -> 処理 -> redirect_toのやり方(remote: true/controller) - Qiita

これも参考にしました。

function postEvent(){
        var title=document.getElementById("title_form").value;
        var eventtype=document.getElementById("eventtype_form").value;
        var time=elapsedTime;
        $.ajax({
            url: "timer/new",
            type: "POST",
            data: {title:title,eventtype:eventtype,time:time},
            dataType: "text",
            success: function(data) {
                alert("行動記録が作成されました!");
            },
            error: function(data) {
                alert("エラーにより行動記録が作成されませんでした。");
            }
        });
    }
submit.addEventListener('click',postEvent);
def create_event
        time=(params[:time].to_i/1000).to_i
        params[:title]="睡眠" if params[:eventtype] == "1"
        params[:title]="食事" if params[:eventtype] == "2"
        params[:title]="服薬" if params[:eventtype] == "3"
        event_params={title:params[:title],eventtype:params[:eventtype],starttime:Time.now-time,endtime:Time.now}
        @event = current_user.events.build(event_params)
        if @event.save
            return "true"
        else
            return 
        end
    end

params[:title]="睡眠" if params[:eventtype] == "1"をもう一回書いているのがとても頭が悪い感じですが…(あとでヘルパーか何かにまとめ流べき)

とりあえずこんな感じで完成です。 あとはテストを書けば終わりです。

f:id:Naomi_Lilienthal:20200820184959p:plain
タイマーの様子

Ruby on Rails tutorial +alpha(7)睡眠表の表示

注意:この記事はメモなので、この記事に書いてあることをやっただけでは同じものができません。(大体の雰囲気は書いてあると思います。)

注意:全くのweb開発初心者なので、間違っているところなどがあるかと思いますが、教えていただけると嬉しいです。


fullcalender.ioだと印刷する時にうまくいかないので、表を表示して画像をダウンロードするか印刷できるようにしたいなあと思いました。

どういうものを目指しているかというと、Googleで「睡眠表」を検索すると出てくるようなやつです。そのまま病院などに持って行ってもある程度使えるようにしたいです。(そのためには睡眠の段階をもっと細かく分けて、最低でも「横になっている」「ぼんやりしている」「うとうとしている」「熟睡している」を分けないといけないですが…とりあえず今の状態でグラフを表示してみます。)

ところで入力がめんどくさすぎるので、30分ごとにプルダウンかなんかで作成できるようにしたいなあ…


usersコントローラのevents_tableアクションと、表が入っているビューを用意します。 表は横が30分刻みで、縦が1日1行2週間です。 もちろんevents_tableアクションには、対象をフォローしているユーザーしかアクセスできません。

  def events_table
    params.permit(:endtime)
    @title = "睡眠表の印刷"
    @user  = User.find(params[:id])
    @end_date=Date.today
    if params[:endtime] &&  params[:endtime]!=""then
      @end_date=Date.parse(params[:endtime])
    end
    @start_date=@end_date-14
    @events = @user.events.where("starttime <= ? and endtime >= ?",(@end_date+1.day).to_time,@start_date.to_time)
    render 'events_table'
  end

view

<aside class="col-md-4">
<%= form_with(url:events_table_user_path,method: :get, local: true) do |f| %>
    <%= f.label :endtime, "最後の日付" %>
    <%= f.date_field :endtime%>
    <%= f.submit "表示", class: "btn btn-primary" %>
<% end %>
</aside>

<div class="col-md-8">

<table border=1 id="events_table" style="table-layout:fixed;width: 100%;">
<tr>
<th>記録</th>
<% (24*2).times do |j|%>
    <td >
    <% if j%2==0 then%>
        <%=j/2%>
    <%end%>
    </td>
<% end%> 
<td width="200">メモ</td>
</tr>
<% (@start_date..@end_date).each do |date|%>
    <tr>
    <td height="50">
    <%=date.month%>/<%=date.mday%>
    </td>
    <% (24*2+1).times do |j|%>
        <td> </td>
    <% end%> 
    </tr>
<% end%>  
</table>
睡眠は★、食事は●、服薬は○、その他は先頭の一文字で示します。<br>
あまりに生活記録の時間帯が重複していると表示がおかしくなる場合があります。
</div>
<script>
let table = document.getElementById('events_table');
<% @events.each do |event|%>
    start_duration=<%=(event.starttime.to_time-@start_date.to_time)%>;
    end_duration=<%=(event.endtime.to_time-@start_date.to_time)%>;
    event_name="<%=event.title%>";
    event_type=<%=event.eventtype%>;
    counterx=0;
    if(event_type!=0){
        for (let i=0;i<14;i++) {
            row=table.rows[i+1]
            console.log(counterx)
            let countery=0;
            for(let j=0;j<24*2;j++){
                cell=row.cells[j+1];
                celltime=counterx*60*60*24+countery*60*30;
                if(celltime+15*60>start_duration && celltime-15*60<=end_duration){
                    if(cell.innerText==" ")cell.innerText="";
                    if(event_type==1){
                        cell.innerText+="★\n";
                    }else if(event_type==2){
                        cell.innerText+="●\n";
                    }else if(event_type==3){
                        cell.innerText+="○\n";
                    }else{
                        cell.innerText+=event_name[0]+"\n";
                    }
                    
                }
                countery+=1;
            }
            counterx+=1;
        }
    }else{
        for (let i=0;i<14;i++) {
            row=table.rows[i+1]
            celltime=counterx*60*60*24;
            if(start_duration-24*60*60<=celltime&& celltime<=end_duration ){
                console.log(counterx);
                row.cells[ 49 ].firstChild.data=event_name;
            }
            counterx+=1;
        }
        
    }
<%end%>
</script>

表の部分はjavascriptで書きました。もっといい方法がある気がするんですけど、目的が目的なのでなかなかぴったりくるものが見つからず… 見つけた方は教えてください。

とりあえずこのアルゴリズムだと(2週間のevent)242*14の計算量がかかりますが、(2週間のevent)はそんなに大きくないので大丈夫でしょう。ここで使ったwhichの条件、eventページにも使いたいですね。

テストを書いたら終わりです。

できました。(せっかくなので印刷する画面でみてみます。実用に耐えなさそうな気がします…まあ今後改良していきます)

f:id:Naomi_Lilienthal:20200819232451p:plain
印刷画面