一人もくもく会 α verでサービス開始しました。
請求書作成システム α verでサービス開始しました。

brunchでBootstrap Material Design4を使う

めでたい事についにBootstrap4が正式リリースされた。

blog.getbootstrap.com

Bootstrap3はscriptタグで読み込んで使うのがメインだったと思うが、Bootstrap4は大体npmでビルドして使うのがメインだとおもう。

Webpackで導入するサンプルは多分検索すればいくらでも出てくると思うが、brunchだとあまり見つからないかもしれないのでメモしておく。

ちなみに、よく使っているのが

Bootstrap Material Design · The most popular HTML, CSS, and JS Material Design library in the world.

なので、それについての詳細を書く。ベースはBootstrapなので多分ほとんどBootstrapと導入方法は同じだと思う。

CSS読み込み

brunch-config.jsでcssを読み込む。

  npm: {
    enabled: true,
    globals: {
      $: 'jquery',
      jQuery: 'jquery'
    },
    styles: {
      "bootstrap-material-design": [
        "dist/css/bootstrap-material-design.min.css"
      ]
    },
  }

npmでインストールはできるけどnpmには対応していない、というライブラリとかも同じ方法で読み込める。

JS読み込み

JSの最初のほうで読み込む。

import "jquery"
import "bootstrap-material-design"

どっかで

    $('body').bootstrapMaterialDesign();

DockerでPHP5.5のLAMP環境を作成

DockerでPHP5.5のLAMP環境を作成した。

元々ローカルのPHPのままだましだまし動かしてたのだが、 シェルだとCakePHP2のObjectクラスがコンフリクトしてついに動かなくなってしまったのでやむなく作成した。

Dockerfile

FROM nyanpass/php5.5:5.5-apache

RUN echo 'date.timezone = "Asia/Tokyo"' > /usr/local/etc/php/conf.d/timezone.ini
RUN a2enmod rewrite
RUN docker-php-ext-install pdo_mysql mysqli mbstring

docker-composer.yml

version: '2'
volumes:
  mysql_data:
    driver: 'local'
services:
  mysql:
    image: mysql:5.5
    volumes:
      - mysql_data:/var/lib/mysql
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: "true"

  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    environment:
      - PMA_ARBITRARY=1
      - PMA_HOST=mysql
      - PMA_USER=root
      - PMA_PASSWORD=
    ports:
      - 8100:80

  zenkokutenkai:
    image: Dockerfileでビルドしたイメージ名
    volumes:
      - .:/var/www/html
    ports:
      - "8050:80"
    tty: true
    stdin_open: true

こんな古いプロジェクトのために作りたくない…とは思うがこういう状況だからこそDockerが役立つんだよなぁ…。

WerckerでPhoenixアプリケーションのCI

WerckerにてPhoenixアプリケーションのCIをするためのwercker.yml。 DBもservicesで追加できるので専用のコンテナを準備する必要がない。

box: shufo/phoenix:1.5.2

services:
  - id: mariadb
    name: mysql
    username: root
    password: ""
    tag: latest
    env:
      MYSQL_ALLOW_EMPTY_PASSWORD: "true"

build:
  steps:
    - script:
        name: mix deps.get
        code: mix deps.get

    - script:
        name: mix test
        code: mix test

ほんとはbrunchのビルドもしたかったのだがこのboxだとnpmコマンドが見つからない。 自分で作っているboxを使えば良いのだが、面倒だったのでとりあえずelixir側だけにした。

Goのglide環境にてWerckerのCI導入

Go3 Advent Calendar 2017 - Qiita

23日目

GoのアプリケーションのリポジトリをBitBucketに作っているので、WerckerでのCIを試した。

シンプルで綺麗なパスでアプリケーションを作っている場合、 多分Werckerにてアプリケーションを登録する際に表示されるサンプルそのままのwercker.ymlでそのまま動くのではないかと思う。

ただ、今回のアプリケーションはパッケージ管理にglideを使っているのでそのままでは動かない。 ちなみにフォルダ構成としては、下記に降りたところにアプリケーションが入ってる。

/src/myapp

Dockerコンテナ内では上記のルートをGOPATHとしている。ちょっと変な構成。

とりあえず動くところまでを調整してみた。 とりあえずなので変な感じになっている。

box: golang
build:
  steps:
    - setup-go-workspace

    - script:
        name: install glide
        code: |
          curl https://glide.sh/get | sh

    # Gets the dependencies
    - script:
        name: glide install
        cwd: src/crawler/
        code: |
          glide install

    # Build the project
    - script:
        name: go build
        cwd: src/crawler/
        code: |
          export GOPATH=/go/src/bitbucket.org/myaccount/myapp
          go build

    # Test the project
    - script:
        name: go test
        cwd: src/crawler/
        code: |
          export GOPATH=/go/src/bitbucket.org/myaccount/myapp
          go test $(glide novendor)

細かいところを補足していくと、まずglideのドキュメント通りにインストール

curl https://glide.sh/get | sh

最初に書いたとおりパスがちょっと違うので合わせる。

cwd: src/crawler/
code: |
  glide install

パッケージの位置がずれて読み込めていなかったので、exportでGOPATHを指定している。 (ymlの設定であるかも?)

プロジェクトの環境変数を設定すると今度はglideのインストールとかがうまくいかなくなるのでここで指定している。

cwd: src/crawler/
code: |
  export GOPATH=/go/src/bitbucket.org/myaccount/myapp
  go build

こんな感じで通った。

あとはパイプラインでデプロイの処理などと繋げれば楽だと思う。

サイボウズLiveを作る-第6回-イベント作成

あと一つ大きなメイン機能であるイベント機能が残っていたのでそちらを作成した。

色々見てみた結果、とりあえず全部FullCalendarに置き換えればいいだろうと言う結論に至った。

FullCalendar - JavaScript Event Calendar

期間や範囲切り替えもあるし、これだけで一通りまかなえる気がする。

f:id:dala:20171219224124p:plain

イベント予定メニューマスタ

本家にはない(?)が、予定メニューにも色を付けられるようにした。

f:id:dala:20171217155825p:plain

<template>
  <div class="row">
    <div class="col-12">
      <div class="sample">
        <span class="event-color-sample" v-bind:style="{backgroundColor: currentBgColor.hex, color: currentTextColor.hex}">サンプル</span>
      </div>
    </div>
    <div class="col-12 col-sm-4">
      <div>背景色</div>
      <swatches-picker v-model="currentBgColor"></swatches-picker>
    </div>
    <div class="col-12 col-sm-4">
      <div>文字色</div>
      <swatches-picker v-model="currentTextColor"></swatches-picker>
    </div>
    <input type="hidden" :name="bg_color_name" :value="currentBgColor.hex">
    <input type="hidden" :name="text_color_name" :value="currentTextColor.hex">
  </div>
</template>

<style scoped>
div.row {
  margin-bottom: 20px;
}
</style>

<script>
import { Swatches } from 'vue-color'

export default {
  props: ['bg_color_name', 'text_color_name', 'bg_color', 'text_color'],

  components: {
    'swatches-picker': Swatches,
  },

  data () {
    return {
      currentBgColor: {hex: this.bg_color === undefined ? '#3F51B5' : this.bg_color},
      currentTextColor: {hex: this.text_color === undefined ? '#FFFFFF' : this.text_color},
    }
  },

  methods: {
  }
}
</script>

一覧もvuedraggableを使ってドラッグ&ドロップで簡単に並び替えできるようにした。

面倒かと思ったが、元々のテンプレートをとりあえずコピーから始められるのでそれほどでもなかった。

<template>
  <table class="table">
    <thead>
      <tr>
        <th></th>
        <th>予定メニュー名</th>

        <th></th>
      </tr>
    </thead>
    <draggable v-model="scheduleCategories" :element="'tbody'" :options="{handle: '.handle'}" @end="onEnd">
      <tr v-for="scheduleCategory in scheduleCategories" :key="scheduleCategory.id">
        <td class="handle"><i class="material-icons">drag_handle</i></td>
        <td>
          <span
            v-text="scheduleCategory.name"
            class="event-color-sample"
            v-bind:style="{backgroundColor: scheduleCategory.bg_color, color: scheduleCategory.text_color}"
          ></span>
        </td>

        <td class="text-right">
          <span><a :href="`/${group_id}/schedule-categories/${scheduleCategory.id}/edit`" class="btn btn-default btn-xs">編集</a></span>
          <span>
            <a
              href="#"
              data-confirm="削除してよろしいですか?"
              :data-csrf="csrf"
              data-method="delete"
              :data-to="`/${group_id}/schedule-categories/${scheduleCategory.id}`"
              rel="nofollow"
              class="btn btn-danger btn-xs"
            >削除<div class="ripple-container"></div></a>
          </span>
        </td>
      </tr>
    </draggable>
  </table>
</template>

<style scoped>
.handle {
  cursor: crosshair;
}
</style>

<script>
import draggable from 'vuedraggable'
import axios from 'axios'

export default {
  props: ['group_id', 'schedule_categories'],

  components: {draggable},

  data () {
    return {
      scheduleCategories: this.schedule_categories,
      csrf: document.querySelector('meta[name=csrf]').getAttribute('content'),
    }
  },

  methods: {
    onEnd() {
      axios.put(`/${this.group_id}/schedule-categories/update-order`, {
          ids: this.scheduleCategories.map(c => c.id),
        });
    }
  }
}
</script>

保存側。

  def update_schedule_categories_order(group_id, ids) do
    Enum.with_index(ids)
    |> Enum.each(fn{id, index} ->
      schedule_category = get_schedule_category!(id, group_id)
      update_schedule_category(schedule_category, %{"display_order" => index + 1})
    end)
  end

カレンダー

こちらも面倒かと思ったが、データの取得はコールバックに作ればいいだけだったので非常に楽だった。

<template>
  <div id="calendar">
  </div>
</template>

<style scoped>
</style>

<script>
import axios from 'axios'
import moment from 'moment'

export default {
  props: ['group_id', 'month'],

  data () {
    return {
      currentEvents: [],
    }
  },

  mounted() {
    if (this.month !== undefined) {

    }
    $('#calendar').fullCalendar({
      locale: 'ja',
      header: {
        right:  'month,agendaWeek,agendaDay today prev,next'
      },
      views: {
        month: {
          titleFormat: 'YYYY年 MMMM',
        },
      },
      buttonText: {
        today: '今日',
        month: '月',
        week: '週',
        day: '日',
      },
      firstDay: 1,
      timeFormat: 'HH:mm',
      defaultDate: this.getDefaultDate(),
      events: this.loadEvents.bind(this),
      dayClick: this.dayClick.bind(this),
      eventClick: this.eventClick.bind(this),
    });
  },

  methods: {
    dayClick(date, jsEvent, view) {
      const dateText = date.format('YYYY-MM-DD');
      if (view.name == 'month') {
        location.href = `/${this.group_id}/schedules/new?date=${dateText}`; 
      } else {
        const timeText = date.format('HH:mm:ss');
        location.href = `/${this.group_id}/schedules/new?date=${dateText}&time=${timeText}`; 
      }
    },

    eventClick(calEvent, jsEvent, view) {
      location.href = `/${this.group_id}/schedule-posts/${calEvent.schedule.id}`;
    },

    loadEvents(start, end, timezone, callback) {
      const startDate = start.format('YYYY-MM-DD');
      const endDate = end.format('YYYY-MM-DD');
      axios.get(`/${this.group_id}/schedules/events/${startDate}/${endDate}`)
        .then(response => {
          this.$emit('GET_AJAX_COMPLETE');
          callback(response.data);
        });
    },

    getDefaultDate() {
      if (this.month !== undefined) {
        return moment(this.month + '-01');
      } else {
        return moment();
      }
    }
  }
}
</script>

取得側。 こういう重そうな範囲検索は、大規模サービスだとどう実装してるのか気になる。 日毎にデータを分けてるのかな。(日を子データにしてるとか)大変そう。

  def list_schedules_for_range(group_id, start_date, end_date) do
    query = from s in Schedule,
      where: s.group_id == ^group_id
        and (
          (s.start_date < ^start_date and ^end_date < s.end_date)
          or (^start_date <= s.start_date and s.start_date <= ^end_date)
          or (^start_date <= s.end_date and s.end_date <= ^end_date)
        )
        and is_nil(s.deleted_at)
    Repo.all(query)
    |> Repo.preload(:schedule_category)
  end

不足点

リピートの予定とか放置してる。

あと、設備とかどこで使ってるんだろうと思ったら、アクセスの方法によって登録できたりするっぽい。 完全に抜けてる。

わかりづらいなここは。急にグループ関係なしの画面になるし意味が分からない。

テスト放置なので整備しようかと思う。 そっちの方が面白くて書くこと多かったりするかもしれない。 ソース書いてるのVueばっかりだし。

Copying live

Advent Calendarの影響によるアクセス増加?

Visual Studio Code Advent Calendar 2017 - Qiita

上記用に書いた、下記の記事のアクセスがすごかった。

AtomからVisual Studio Codeに乗り換えた - アルファブレンド プログラミングチップス

f:id:dala:20171218090236p:plain

普通Qiitaのカレンダーからアクセスが来るだけではこんなにはアクセスは増えない。

どうもスマホGoogle検索トップのカードに表示されていたらしい。 大した記事でも無いのになんでだろ。 どういうアルゴリズムなのか不明。

ただ、VSCodeのカレンダーは人気だったようなのでそのへんが関係していたのかもしれない。 今までも他のカレンダーはこんなことはなかったので。

なによりすごいのは、これだけアクセスあってスターもブックマークもほとんどない記事のしょぼさ。

Systemdを使ったPhoenixの本番デプロイ詳細例

Elixir Advent Calendar 2017 - Qiita

19日目。

サーバーを準備し、コンテナを使わずに運用できるところまでの準備まで一通りまとめてみた。 Systemdで動作させる。

(以前残したログをまとめているだけなので正確でない可能性あり)

前提

初期設定

最初だけ本番で調整したものをコミットしたりもするのでgitもちょっと設定。

sudo apt-get update
sudo apt-get install -y git dbus
sudo timedatectl set-timezone Asia/Tokyo
git config --global user.name "name"
git config --global user.email "email"

Elixir & Phoneix & Node.js

curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && sudo dpkg -i erlang-solutions_1.0_all.deb
sudo apt-get update
sudo apt-get install -y nodejs inotify-tools esl-erlang elixir
mix local.hex --force
mix local.rebar --force
mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez --force
sudo apt-get install --reinstall build-essential

DB

MySQLとかPostgreSQLとか。 普通は別サーバーだろうし省略。

ビルド

ssh-keygenかtokenかでcloneできるように準備。 適当なので環境変数とかユーザーは適宜いい感じにしてもらった方が良いかもしれない。

git clone ********.git
cd appdir
mix deps.get --only prod
sudo MIX_ENV=prod mix compile
cd assets
npm install
sudo npm install -g brunch
brunch build --production
cd ..
sudo MIX_ENV=prod mix phx.digest
sudo MIX_ENV=prod mix ecto.migrate
sudo MIX_ENV=prod mix phx.server

とりあえずここまでで動くか確認する。

Let's Encrypt

省略。下記の記事に一応詳細は書いてある。

PhoenixでLet's EncryptによるSSL - アルファブレンド プログラミングチップス

Systemd

sudo vi /etc/systemd/system/myapp.service
[Unit]
Description=My app

[Service]
WorkingDirectory=/home/username/appdir

Restart=always
Environment=MIX_ENV=prod HOME=/root
ExecStart=/usr/local/bin/mix phx.server

[Install]
WantedBy=multi-user.target
sudo systemctl enable cybozulive
sudo systemctl start cybozulive

運用

pull, compile, brunch build, digest, migrate, restart