はかせだけど博士じゃない

無職が就活しないでプログラミングとかする

N予備校のプログラミング入門メモ18

はじめに

中途半端なところで中断してしまった。

4章 実践サーバーサイドプログラミング

20.コメントの表示の更新

昨日はjQueryを使ってAJAXで出欠の更新を実装した。

コメントを取得する部分を実装する。予定にアクセスした際に予定に関するコメントを全件取得する。

コメントのRouterオブジェクトを実装。Applicationオブジェクトに登録する。

テストを書く。スタブを使ってログインしてスケジュールを作ってからパスにコメントのオブジェクトを渡してコメントを作成、レスポンスのステータスの確認とDBの確認を書いた。

テンプレートの方にコメントの表示とボタンを書いて、jQueryapp/entry.js)でコメントをPOSTするクリックイベントハンドラを実装。

ここまででwebpackしてからサーバを再起動して確認したところ、テンプレートのインデントを間違えて候補ごとにコメントが出てしまってちょっと笑った。

Promiseのチェインについて。そういえば以前の職場ではthenがすごい入れ子になってて直さないのかなぁと気になっていた。講座ではネストが深い関数についてチェインでリファクタリングした。

21.予定の編集と削除

編集機能を作る。まず編集画面のテンプレートを作成。ちなみに予定作成画面と要素はほぼ同じなので、作成モードと編集モードみたいなパラメータで同じテンプレートを使うようにも作れそうな気がする。どっちがいいんだろうか。

Routerオブジェクトにパスのハンドラを登録。アクセスして表示されるか確認する。

POSTメソッドの処理を実装。対象のスケジュールを取得して自分のIDとの一致を確認、update関数でスケジュールを更新したら、候補を追加する。この際同じ処理を切り出して関数化したりした。

削除機能を作る。編集画面に削除ボタンを追加し、テストで作成した削除関数を持ってきて削除機能は完成。

これで要件はすべて満たせた。

22.デザインの改善

Bootstrapを使ってかっちょいい感じにした。

グローバルオブジェクトを取得するには以下のようにする。

const global = Function('return this;')();

23.セキュリティ対策と公開

脆弱性一覧を見て対策が必要なものを考える。

csurfを使って対策する。

const csrfProtection = csrf({ cookie: true });

router.get('/new', authenticationEnsurer, csrfProtection, (req, res, next) => {
  res.render('new', { user: req.user, csrfToken: req.csrfToken() });
});

router.post('/', authenticationEnsurer, csrfProtection, (req, res, next) => {
  // 中略

みたいにするとトークンを発行・確認する。あとは

input(type="hidden" name="_csrf" value!="#{csrfToken}")

のようにテンプレートでトークンをPOSTするようにして対策完了。csurfの使い方は README.mdを参照。

認証後にアクセスした画面にリダイレクトされるように実装を変更する。

認証する関数でログイン画面にアクセスする際、元のURLをクエリに含めるようにして、ログインのRouterオブジェクト側でクエリの内容をCookieに保存。OAuthのコールバック関数内でCookieから元のURLを取得してそこにリダイレクトするように実装した。

Herokuへの公開の準備をする。

app.jsonでアプリケーション情報を記述、package.jsonにNode.jsのバージョンを追記、DBのURLもHerokuのものを使用するに変更。ここまでをコミットしておく。

Herokuにログインしてサーバーを作成、URLを控えておく。

以下のようにPostgreSQLのDBを作成。

heroku addons:create heroku-postgresql:hobby-dev

一旦Herokuにpush。ここまででトップページが表示できる。

GitHub認証のためにアプリケーションを登録する。Client IDとClient Secretを控える。

Herokuの環境変数に登録。

heroku config:set HEROKU_URL='https://xxx-xxxxxxx-XXXXXX.herokuapp.com/'
heroku config:set GITHUB_CLIENT_ID='000000000000000000'
heroku config:set GITHUB_CLIENT_SECRET='000000000000000000000000000000000'

アプリケーション側でIDと秘密鍵、コールバックするURLを上記を使うように変更する。

以上をコミットしてHerokuにpushするとアプリ開発は完了。

ちなみにトップページを表示する段階でHerokuのエラー画面が出たのでheroku logsして確認して修正してデプロイしなおすなどした。

練習問題は、何かネタを考えてみよう。ひとまず4章は完了。

N予備校のプログラミング入門メモ17

はじめに

妹とデートしてきた。

4章 実践サーバーサイドプログラミング

18.予定の一覧の表示

講座用のプロジェクトをcloneしてnpm startしたところ、DBのエラーが出た。

Unhandled rejection SequelizeDatabaseError: column "scheduleId" does not exist
    at Query.formatError (/home/vagrant/workspace/schedule-arranger-4018/node_modules/sequelize/lib/dialects/postgres/query.js:357:14)
    at Query.<anonymous> (/home/vagrant/workspace/schedule-arranger-4018/node_modules/sequelize/lib/dialects/postgres/query.js:88:19)
    at emitOne (events.js:96:13)
    at Query.emit (events.js:188:7)
    at Query.handleError (/home/vagrant/workspace/schedule-arranger-4018/node_modules/pg/lib/query.js:108:8)
    at Connection.<anonymous> (/home/vagrant/workspace/schedule-arranger-4018/node_modules/pg/lib/client.js:171:26)
    at emitOne (events.js:96:13)
    at Connection.emit (events.js:188:7)
    at Socket.<anonymous> (/home/vagrant/workspace/schedule-arranger-4018/node_modules/pg/lib/connection.js:109:12)
    at emitOne (events.js:96:13)
    at Socket.emit (events.js:188:7)
    at readableAddChunk (_stream_readable.js:176:18)
    at Socket.Readable.push (_stream_readable.js:134:10)
    at TCP.onread (net.js:547:20)

まだ何もいじっていないがscheduleIdが存在しないという旨のエラーのようなので一応コードを確認するも特に問題がない。ということでDBの方が不整合になっているのかと思ってpsqldrop databaseしてcreate databaseしたところ、今度はちゃんと起動した。前回の写経で変な風にtypoしたくせにつじつまが合って起動できてしまったといったところか。

index.jadeにログイン/ログアウトボタンを追加してRouterオブジェクトのindex.jsでテンプレートにユーザ情報のオブジェクトを渡すように修正。

新規予定作成画面のテンプレート、そこからPOSTを受け取るRouterオブジェクトを実装する。認証を確認するハンドラ関数も実装しておき、Routerのget関数などの第二引数にしていすることで呼び出し時に認証が確認されるようにできる。

'use strict';

function ensure(req, res, next) {
  if (req.isAuthenticated()) { return next(); }
  res.redirect('/login');
}

module.exports = ensure;
const authenticationEnsurer = require('./authentication-ensurer');

router.get('/new', authenticationEnsurer, (req, res, next) => {
  res.render('new', { user: req.user });
});

上記のようにするとログインしていない状態で予定作成画面/newにアクセスするとログイン画面にリダイレクトされる。

予定作成に使うUUIDを実装する。

yarn add node-uuid@1.4.7

インストールしたらv4という関数を呼び出すとUUID文字列を取得できる。

予定と候補を保存する機能を実装する。sequelizeオブジェクトのbulkCreate関数で複数のデータをDBに保存できる。

Promiseについて。そういえば解説されてなかった。非同期でよく見るやつ、というイメージだったけど解説もだいたいそんな感じだった(失礼)。

index.jadeに自分で作った予定の一覧を表示するように修正。each inを使ってテーブルの行をループで表示する。Routerオブジェクトからテンプレートに予定の一覧を渡すのを忘れずにやる。以前もやったfindAll関数を使って自分が作った予定一覧を取得する。

/schedule/:scheduleIdを実装する。:scheduleIdはUUIDの文字列。

RouterオブジェクトのfindOne関数は1行を取得する。

ここでincludeというのが急に出てきた。(忘れてるだけかもしれない)

  Schedule.findOne({
    include: [
      {
        model: User,
        attributes: ['userId', 'username']
      }],

事前に Schedule.belongsTo(User, {foreignKey: 'createdBy'});と従属の設定をしているのでこうやってユーザー情報が取得できる、というもののようだ。たぶん。userプロパティにユーザー情報が設定される。

練習問題で予定が表示されることのテストを書いたが、そこでexpect関数でテストしたあとにend関数内でテストで作成したデータを削除する処理が書かれている。

  it('予定が作成でき、表示される', (done) => {
    User.upsert({ userId: 0, username: 'testuser' }).then(() => {
      request(app)
        .post('/schedules')
        .send({ scheduleName: 'テスト予定1', memo: 'テストメモ1\r\nテストメモ2', candidates: 'テスト候補1\r\nテスト候補2\r\nテスト候補3' })
        .expect('Location', /schedules/)
        .expect(302)
        .end((err, res) => {
          let createdSchedulePath = res.headers.location;
          request(app)
            .get(createdSchedulePath)
            .expect(/テスト予定1/)
            .expect(/テストメモ1/)
            .expect(/テストメモ2/)
            .expect(/テスト候補1/)
            .expect(/テスト候補2/)
            .expect(/テスト候補3/)
            .expect(200)
            .end((err, res) => {
              // テストで作成したデータを削除
              let scheduleId = createdSchedulePath.split('/schedules/')[1];
              Candidate.findAll({
                where: { scheduleId: scheduleId }
              }).then((candidates) => {
                candidates.forEach((c) => { c.destroy(); });
                Schedule.findById(scheduleId).then((s) => { s.destroy(); });
              });
              if (err) return done(err);
              done();
            });
        });
    });
  });

19.出欠の表示と更新

2次元のデータ構造についての話。特別新しい話はなかった気がする。ApplicationオブジェクトにRouterオブジェクトを設定する際、同じパスに対して複数のRouterオブジェクトを設定しているのはこういうのもあるのかという感じ。

20.コメントの表示と更新

出欠の更新、コメントの更新APIを実装して、jQueryとwebpackを入れてAJAXでそれぞれが更新できるようにした。

一時中断してたけど日をまたいだので一旦投稿。

N予備校のプログラミング入門メモ16

はじめに

男子スキークロス見てたら開設の人のニーデラー推しにハマってしまい予定より遅い開始になりました。

4章 実践サーバーサイドプログラミング

10.RDBMSSQL

PostgreSQL って「ポストグレ」だったのか「ポストグル」って読んでた。

sudo su - postgrespostgresユーザを開始してpsql。以前やったのと同じ。

CREATE DATABASE diary_1;diary_1というDBを作成し、\c diary_1で接続。

CREATE TABLE pages (write_date DATE PRIMARY KEY, body TEXT);pagesというテーブルを作成。ここでは列名、型、主キーを設定している。

この辺は以前の講座でsequelize? だったかっていうモジュールを使ってJSで定義を書いたのと同じことを直接やっている。

\dtでテーブル一覧を表示する。何の略だろ。すこーしだけググったけど出てこなかった。

INSERT INTO pages (write_date, body) VALUES ('2016-02-22', '最初の日記');でデータを挿入する。

SELECT body FROM pages;pagesから取得してbodyを表示する。SQL Serverなら触ったことあるので簡単なSELECT文ならたぶん分かる。細かい文法の差とかはあるんだろうけど。

複数のデータを入れたり主キーがダブるように入れたりして挙動を見た。

N予備校のプログラミング入門を始めるにあたって実はUS配列のキーボードも一緒に始めたのだけど、シングルクォーテーション(')の入力がEnterの隣にあっていつか誤ってEnter押しちゃうなーって思ってたらSQLのDELETE文で押しちゃってヒヤッとした。最後にセミコロン(;)を入力するまで実行されないという動きもこれで初めて確認できた。間違ってEnterした時の取り消し(セミコロンを入力してない状態を解除する)ってどうすればいいんだろうか。とりあえず明らかに誤った文法を入力してエラーにして回避した。

DELETE FROM pages WHERE write_date='2016-02-29';してデータを消したりした。

SELECT * FROM pages WHERE body LIKE '%寝た%';さっき簡単なSELECT文ならたぶん分かるって書いたけどLIKE句初めて見た(小声)。

DROP TABLE ${テーブル名};でテーブル自体を削除する。

\qPostgreSQLのフロントエンドを終了。そのままexitpostgresユーザも終了する。

RDBのデータの定義を扱うものをDDL(Data Define Language)、テーブルのデータ自体を扱うものをDML(Data Manipulation Language)、DBをコントロールするものをDCL(Data Control Language)という。試験問題みたいだあ。

'UPDATE pages SET body='三月になった' WHERE write_date='2016-03-01';'で本文を更新した。

11.データモデリング

データモデリングのお話。非正規形であるテーブルを正規形に直したりする。今度ゆっくり読む。

psql diary2diary2に接続した状態でフロントエンドを開く。

CREATE TABLE users (userid SERIAL PRIMARY KEY, name VARCHAR(16), gender CHAR(1));IDを1,2,3...とするためにuseridSERIAL型で定義している。

CHARVARCHARの違いは、CHARはスペースパディングすること。CHAR(5)aを入れたら、a____みたいにになる(アンダーバーはスペース)。

ALTER TABLE ${テーブル名} ADD ${追加する列の名前} ${データ型};で既存のテーブルに列を追加できる。

12.テーブルの結合

SELECT * FROM diaries JOIN users ON diaries.userid = users.userid; JOIN句でテーブルを結合。

内部結合と外部結合がある。内部結合はどちらのテーブルにも存在したものを結果に含める。AND条件みたいな感じ。

INNER JOINと書くと、動作は同じだが内部結合であることが読み取りやすくなる。

外部結合は以下がある。どのテーブルを残すかの違い。 - 左外部結合(LEFT JOIN) - 右外部結合(RIGHT JOIN) - 完全外部結合(FULL JOIN)

SELECT id, body, comment
FROM diaries
LEFT JOIN comments ON diaries.id = comments.diary_id;

diariesが残るような結果になる。さらにORDER BY句を使ってソートする。

SELECT id, body, comment
FROM diaries LEFT JOIN comments
ON diaries.id = comments.diary_id
ORDER BY id;

13.インデックス

RDBのパフォーマンスについて。

EXPLAIN ANALYZE SELECT * FROM scores WHERE score = 100;のようにEXPLAIN ANALYZEをつけると実行時間が計れる。

CREATE INDEX ON scores (score);とするとscoresscore列にインデックスが作成されてscore列に対する処理が早くなる。ほかの列も早くしたい場合は別途インデックスを作成する必要がある。

インデックスがない場合はシーケンシャルスキャン(1行ずつなめるスキャン)をしていたが、インデックスを作成するとすべてを調べる必要がなくなるので早くなる。

B木、二分探索木に似てるなと思って調べたら二分探索木→AVL木→B木のように改良されていった感じだった。

14.集計とソート

AVG``COUNT``MIN``MAX``SUM等が使える。

GROUP BY句でグループ化できる。以下のようにすると名前ごとに集計したデータが表示される。

SELECT
    name,
    MAX(stage),
    AVG(score),
    COUNT(score)
FROM
    scores
    INNER JOIN
        users
    ON  scores.user_id = users.user_id
GROUP BY
    name;

数が10000未満のユーザだけ取得したい、という場合にWHERE句にCOUNT(score) < 10000というようには記述できない。WHRER句は行についての条件を書く句であるため。

グループについての条件を書くHAVING句を使う。

SELECT
    name,
    MAX(stage),
    AVG(score),
    COUNT(score)
FROM
    scores
    INNER JOIN
        users
    ON  scores.user_id = users.user_id
GROUP BY
    name
HAVING
    COUNT(score) < 10000;

15.「予定調整くん」の設計

  • 要件を出す
  • 用語を定義する
    • 英語表記も用意するとコード書くときに便利
  • URL設計
    • ページ構成
    • Web APIのURL設計
  • モジュール設計

自分でアプリケーションを作る時にも参考になりそう。

最後にGitHubでアプリケーションを登録した。

16.認証の実装とテスト

Expressでひな形を準備。

express <project-name>

ディレクトリを移動して一連の作業。

echo "node_modules/" > .gitignore
git init
git add .
git commit -am "first commit"

helmetをインストール。

yarn add helmet@1.1.0

app.jsにhelmetを読み込んでapp.use(helmet());することで有効化する。

./routesにRouterモジュールのファイル群を作成する(login.jsとかlogout.jsとか)

認証に必要なモジュールをインストール

yarn add passport@0.3.2
yarn add passport-github2@0.1.9
yarn add express-session@1.13.0

GitHub認証の実装は以前とほぼ同じ。

Routerオブジェクトをテストする。

yarn add mocha@3.2.0 --dev
yarn add supertest@1.2.0 --dev
yarn add passport-stub@1.1.1 --dev

supertestはRouterオブジェクトをテストするモジュール、passport-stubは認証のスタブ。

supertestと作っているアプリ../appを読み込んでテストを記述する。

'use strict';
const request = require('supertest');
const app = require('../app');

describe('/login', () => {

  it('ログインのためのリンクが含まれる', (done) => {
    request(app)
      .get('/login')
      .expect('Content-Type', 'text/html; charset=utf-8')
      .expect(/<a href="\/auth\/github"/)
      .expect(200, done);
  });

});

上記ではヘッダが含まれているか(文字列を2つ渡す)、GitHubへのリンクが含まれているか(正規表現を渡す)をテストして、最後にステータスコードdone関数を渡す。

認証に関するテストを書く。

const passportStub = require('passport-stub');

describe('/login', () => {
  before(() => {
    passportStub.install(app);
    passportStub.login({ username: 'testuser' });
  });

  after(() => {
    passportStub.logout();
    passportStub.uninstall(app);
  });

// 中略

  it('ログイン時はユーザー名が表示される', (done) => {
    request(app)
      .get('/login')
      .expect(/testuser/)
      .expect(200, done);
  });
});

before``after関数はそれぞれdescribeの前後に処理される。ここではスタブにアプリをインストールしてテストユーザでログイン、終了後にログアウトしてアプリをアンインストールしている。

17.ユーザーの保存

sequelizeモジュールを利用してDBを扱う。

yarn add sequelize@3.25.0
yarn add pg@6.1.0
yarn add pg-hstore@2.3.2

それからschedule_arrangerというDBを作成。

複数のモデルを別ファイルに記述したいのでsequelize読み込み用のmodels/sequelize-loader.jsを用意する。

'use strict';
const Sequelize = require('sequelize');
const sequelize = new Sequelize(
  'postgres://postgres:postgres@localhost/schedule_arranger',
  { logging: true });

module.exports = {
  database: sequelize,
  Sequelize: Sequelize
};

あとはひたすらモデルを作成するけどモデルの写経がなかなかしんどい。どういうときにインデックスを作成して、こんな場合にはインデックスを作成しなくてもよいみたいな話学びがある。

sequelizeはエンティティ同士の関係を定義しておくとテーブルを結合して取得ができるらしい。

// モデルの読み込み
var User = require('./models/user');
var Schedule = require('./models/schedule');
var Availability = require('./models/availability');
var Candidate = require('./models/candidate');
var comment = require('./models/comment');
User.sync().then(() => {
  Schedule.belongsTo(User, {foreignKey: 'createdBy'});
  Schedule.sync();
  Comment.belongsTo(User, {foreignKey: 'userId'});
  Comment.sync();
  Availability.belongsTo(User, {foreignKey: 'userId'});
  Candidate.sync().then(() => {
    Availability.belongsTo(Candidate, {foreignKey: 'candidateId'});
    Availability.sync();
  });
});

各モデルを読み込み、sync関数でモデルに合わせてDBのテーブルを作成する。作成が終わった後に実行したい処理をthenでつないでいる。なのでUserのテーブルを作成→外部キーを設定してテーブルを作成→外部キーを設定して……みたいな流れで各テーブルを作成している。

認証が実行されたときに呼び出される処理のところに以下を記述

    process.nextTick(function () {
      User.upsert({
        userId: profile.id,
        username: profile.username
      }).then(() => {
        done(null, profile);
      });
    });

upsert関数でUPDATE(無ければINSERT)を行う。これでユーザーを永続化できた。

ちょっと疲れてきたので休憩。

N予備校のプログラミング入門メモ15

はじめに

もう少しだけやります。

4章 実践サーバーサイドプログラミング

09.WebSocket

Socket.IOというアプリを利用する。

npm install socket.io@1.4.5 socket.io-client@1.4.5 --save

yarnの場合は特に--saveをつけなくてもpackage.jsonに反映される。npm install --save-devのときはyarn add --devにする必要がある。

bin/wwwを編集する。socket.ioモジュールを読み込んでsocket.emit関数でWebSocketの接続に対してイベントを発行するようにemitServerStatus関数を実装して、socket.ioモジュールのconnectionイベントが発火した際に実装したemitServerStatus関数を定期的に実行する感じのコードを書いた。

function emitServerStatus(socket) {
  socket.emit('server-status', { loadavg: os.loadavg() });
  console.log('server-status event emitted.');
}

io.on('connection', function (socket) {
  setInterval(emitServerStatus, 10, socket);
});

setIntervalで関数を10ミリ秒ごとに呼び出しててマジかってなるけどWebSocketなら大丈夫ってことなんだろうきっと。

ここまで実装してPORT=8000 npm startするとエラーが出て起動しない。追記した部分をコメントアウトして再度サーバを起動したが同じエラーが出た。

node_modulesを削除して再度yarnした後、試しに

yarn add socket.io@1.4.5 socket.io-client@1.4.5

してサーバを起動したらうまくいった。yarnでインストールしても一緒だと思うのでtypoしてたんじゃないかと思っている。

ここまでのコードではまだSocket.IOの恩恵を受けていない。

クライアント側で以下のようにする。

const socket = require('socket.io-client')('http://localhost:8000');
socket.on('server-status', (data) => {
  loadavg.text(data.loadavg.toString());
});

localhost:8000にアクセスすると怒涛の勢いでserver-status event emitted.の文字が表示された。

webpackするのをよく忘れるので忘れないこと。

yarn run webpack./node_modules/.bin/の中のwebpackを実行してくれる。

gitにコミットするとき、コメントを待ちがえたままコミットしてしまった。git commit --amend -m "コメント"とすると直前のコメントを変更できた。

途中で電池が切れて落ちたんだけど、復帰したらここまで書いたところが生きてて感動した。新しいPCありがとう、学習用に買ってよかった。

イラストのトレース練習サイト(仮)メモ

はじめに

以前読んだ「【これは納得】 #0から絵師になる方法論 #ダラダラと絵師になる方法 【従来の労力を軽減】」という記事、何となくイラストを練習したいけど続かないしそもそもどうやって練習すればいいのか分からないという自分にとても合ってる練習方法なのでは? と思って早2年。やらない。知ってたけどやらないんだよなあ。うん。

ということでトレースの練習に特化したアプリでもあれば継続して練習するのでは、しかも自分で作ったアプリならなおさらなのでは!? という感じのモチベーションでアプリを作ってみたいと思います。今はN予備校をやってるけど、終わってからがいいか並行するかは未定。

(たぶんアプリを作ってもイラストの練習はやら)ないです。

必要な機能を書きなぐる

  • イラストをトレースする
    • イラストの上から線を描く
      • 線の太さを変える
      • 線の色を変える
    • イラストの上に書いた線を消す
      • 消しゴムのように消す
      • すべて消す
      • UNDO
    • 書いた線を非表示にする
  • イラストを表示する
    • イラストを半透明にする
    • イラストを非表示にする
    • イラストを横に表示する
    • イラストを左右反転する
    • イラストを上下反転する
  • 書いた線を保存する
    • ローカルに
    • Web上に
  • 書いた線を共有する
  • トレースするイラストを用意する
    • アップロードする?
      • ローカルの画像を表示するだけとかできないかな
    • Web上の画像をもってくる
    • アプリ側であらかじめ用意する?

色々書いたけど、とりあえずパスはログインとお絵かき(トレース)画面くらいがあればいいのかな。基本的な部分以外は作りながら考えよう。

path 機能
/ トップ画面
/login ログイン画面
/logout ログアウト画面
/main トレースする画面

まず最小構成で作るためにお絵かき画面だけでもいいかもしれない。そもそもログインが必要かどうかも怪しい。

お絵かき機能はcanvasで実現できそう。簡単な線が描ける程度のものを想定していて、「canvas お絵かき」とかでググるとそれっぽいコードがたくさん出てくるのでパク参考になるだろう。

画像を表示する部分も、画像をどこから持ってくるのかはさておき、canvasでレイヤーを実現できるかは調べてみるがそもそも要素を2つ重ねるとかすればできそうである。半透明・非表示も並べて表示もなんならCSSで実現できる。

まずはそこまで作ってみよう。