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

サイボウズLiveを作る-第5回-グループへ参加

とりあえず一旦グループにメンバーを追加する機能を進めてみた。

メールアドレスは今のところ登録してほしくないし、とりあえずそれ無しでできる部分だけ進めた。

具体的には本家と同じで、招待URLを使ってそこからアクセスしてログインすればグループ申請となる形。

というか、本当にほとんどそれくらいなので何も書くことがない。 とりあえず、ただそのまま処理を書いてるだけなので何の役にも立たないがソースでも貼っておく。

  def join_request(conn, %{"id" => id, "invitation_hash" => invitation_hash}) do
    user = Auth.get_user(conn)
    group = Groups.get_group!(id)
    cond do
      invitation_hash != Group.invitation_hash(group) ->
        redirect(conn, to: "/")
      user ->
        Groups.create_invitation(user, group)
        redirect(conn, to: group_path(conn, :index))
      true ->
        conn
        |> put_layout(false)
        |> render("join_request.html", group: group, invitation_hash: invitation_hash)
    end
  end

ログインしていたらそのままメッセージもなく申請データが登録されて自分のページに戻るので、 非常にわかりづらい。さすがに直した方がいいかもしれない。

承認。

  def approve(conn, %{"group_id" => group_id, "id" => id}) do
    user = Auth.get_user(conn)
    group = Groups.get_group!(group_id)
    invitation = Groups.get_invitation!(id, group_id)
    invitation_params = %{"closed_at" => Timex.now}

    result = Repo.transaction(fn ->
      Groups.update_invitation!(invitation, invitation_params)
      Groups.create_group_user!(invitation.user, group)
    end)

    case result do
      {:ok, _changes} ->
        conn
        |> put_flash(:info, "承認しました。")
        |> redirect(to: invitation_path(conn, :requests, group_id))
      {:error, _any} ->
        conn
        |> put_flash(:error, "エラーが発生しました。")
        |> redirect(to: invitation_path(conn, :requests, group_id))
    end
  end

あとはページ上に表示されているユーザーのリンクは単純なusersのshowだったが、 関係ないグループのユーザーも表示できてしまうので全てGroupUserのリンクに変更し、 同じグループのユーザーしかアクセスできないものにした。 自分の編集画面などはid等のパラメータなどもなしのURLに変更。

考察

自分の知っている人を新たなグループに招待する機能もサイボウズLiveにはあり、 それは便利なので必要かなと思う。 今回は申請、許可的な機能だがそちらは招待なので参加する側が許可すれば参加できる、 今回とは逆の機能となる。 招待テーブルを作るのか、フラグで分けるのか、また気が向いた時に本家の画面を見て決めたりなどが必要。

次はイベント機能を進めようかと思う。 それが終わったら今テストを全く触っておらず、自動生成されたままのためエラー出まくりなので、 そっちを一旦整備もしたい。修正したり要らないものを捨てて絞ったり。 (なんとなくそっちの方が書くことがあるような気がする)

あとはイベント機能に伴い、今まで放置してたタイムゾーン問題も少し時間をかけて調べる時間を取ろうと思う。

個人開発のだいたいの流れの例

個人開発 Advent Calendar 2017 - Qiita

の5日目。

概要

普段から時間があればプログラミングで遊びつつ何か作成している。 せっかくなので誰も使わなくてもリリースしたりしていて、 それらは特に仕事でやっているのとかけ離れているものではないので、 適当に箇条書き程度で紹介。

今作っているもの

ちょっと前にサイボウズLiveが終了すると発表された。 自分も丁度お客さんが使っていてそこでやり取りをしていた。 丁度Elixir & Phoenixを使って色々遊んでいたところに飛び込んできたニュースで、 丁度別のアプリケーションをキリのいいところまで作り終えたところだったので、 サイボウズLiveをコピーして作ってみようと思い作成中。

成果物は下記。

Copying live

ちなみに当ブログで連載中。

サイボウズLiveを作る カテゴリーの記事一覧 - アルファブレンド プログラミングチップス

進め方

とりあえずどんどん出来ていくのが楽しいので、細かいところは無視してどんどん自分のやりたいところを先に進める。 細かいところは後回し。 今もデータ登録が中心で削除等の細かい機能は放置している。

デザイン

デザイナーじゃないのでやっぱりBootstrap。 特に

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

がおしゃれで何にでも使えるので良いと思う。 JavaScriptフレームワーク毎にMaterial Desginのライブラリがあったりもするのでそういうのでもいい。

とりあえずの画面

掲示板

f:id:dala:20171202000125p:plain

Todo

f:id:dala:20171202000146p:plain

エディタ

Visual Studio Code

今のところ知っている中では軽量&高機能&設定手軽なものでベストだと思う。

開発環境

docker-composeを使う。Linux Mintなのでただインストールして使うだけ。

Elixir & Phoenixだと下記のような感じ。 見てのとおりだがPhoenix & MySQL & phpMyAdminPhoenixのイメージは誰かがどこかで公開していたのを参考にしただけのもの。

version: '2'
volumes:
  mysql_data:
    driver: 'local'
services:
  mysql:
    image: mysql:8.0
    volumes:
      - mysql_data:/var/lib/mysql
    ports:
      - "3310:3306"
    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

  cybozulive:
    image: dala00/phoenix:1.3.0
    env_file: .docker-env
    volumes:
      - .:/var/opt/app
    ports:
      - "4000:4000"
    tty: true
    stdin_open: true

ソース管理

自分の場合、ソースを公開していいならGitHubに置くし、 多少大きくてあまり公開したくないものはBitbucketに置いている。 (Bitbucket最近重い気がするけど)

フロント

Vueを使っている。 Phoenixは最初からbrunchが入っているのでVueを入れたらそのまま使える。 Phoenixの開発サーバーに元々ウォッチ機能がついていて、htmlもcssJavaScriptも全部保存時に勝手に画面を更新してくれるので何も考える必要がない。 そのためjQueryを使う意味すら無いしもう全員Vueで良いと思う。 なにより簡単に適当に一部のみコンポーネント化できるのが最高。

PHPならLaravelも最初からVueが使える。 もしフレームワークに何もついてなくてもwebpack導入してwatchするだけなのでとりあえずもうVueを使っておけばいいと思う。

公開

せっかく作ったならぐちゃぐちゃでも公開していったらいいと思う。

僕の場合、PHPならさくらのスタンダードを1つ借りてるのでそちらにアップして公開する。 (バージョン上げると動かなくなるものも入っているのでもうひとつ借りたくなるところ…)

それ以外の場合、VPSとかでも1アプリケーションごとに500円とか千円とかかかってしまい、 積み重なっていくとモチベーションに影響してしまうので、最近だと全部GCEの無料のプラン。 Herokuでも良いかもしれないがどうもDBの接続が安定しないようなのでちょっと不安。

Phoenixのデプロイ方法はAdvent Calendarの別の記事で紹介する予定。

あとSSLは例えしょうもないアプリでももう必須だと思う。 焦らずにリリース前の、失敗してサーバーをぐちゃぐちゃにしても問題ないタイミングでちゃんと設定しておきたい。

AtomからVisual Studio Codeに乗り換えた

Visual Studio Code Advent Calendar 2017 - Qiita

の4日目。

最近、長らく使っていたAtomからVisual Studio Codeに乗り換えた。

理由はただひとつ、「重すぎる」

なんだかんだでずっとCodeに乗り換える機会を伺っていた。

まずAtomのいいところ

Atomはとても良い。 拡張機能で何でも揃えられるし、どんな言語でも扱えるし、 一度起動してしまえばさくさく動いて快適。

使っているパッケージに不満があればプルリクを送って改善していけるし、 みんなで作り上げていくとても良いアプリケーション。

ずっと死ぬまで使い続けていくのだろうなと、あの頃は思っていた。

Atomの悪いところ

とにかく重い。

週1の常駐業務で32bitのWindows7Atomを使っていたが起動と終了が重すぎる。 複数のプロジェクトを同時進行しようとすると話にならない。

自分のPCは昔ゲーム用に買ったスペックの良いものなのでそれほど気にはならなかったが、 時々PC自体が完全にフリーズするのでおかしいと思ったらAtomを開きすぎるとそうなるっぽかった。

また、オートコンプリート機能などのパッケージは、裏で解析を始めるので途端に重くなる。 キー入力も遅延が生じ始める。仕方がないのでそういったパッケージは使用を断念する。

最近IDE機能が追加されたので期待して試してみたが、期待はずれだった。 このあたりで完全にAtomへの想いが冷めたのだと思う。

Visual Studio Codeへの乗り換えの懸念

Codeがやけに速くて快適なのはちょくちょく試していたので気づいていた。 乗り換え直前等はAtomの重さを最大限に感じていた時なので、天と地の差くらいには感じていた。

ではなぜ乗り換えなかったかというと、Vimパッケージへの不満だった。

AtomVimパッケージは、元々あったVimパッケージを日本人開発者の方が別パッケージにして強化して主導権を奪い取ったもので、非常に快適。

CodeのVimは、下記が出来なくて日常的に使うのは厳しいなと感じていた。

  • Insert modeの時もCtrl+Fを奪ってしまって検索、置換が面倒
  • modeによってはCtrl+PgUp, PgDnでタブ切り替えが出来ない
  • システムのClipboardを使う設定にすると一部操作ができなくなる不具合がある(現在改善されている)

たったこれだけではあるがスピード感が失われてストレスが半端ないので使用できなかった。

解決方法

解決したので即Codeに乗り換えた。

キーバインディングは自由に設定できるので自分で勝手に設定すれば良いだけだった。 今使っているキーバインディングは下記のような感じ。

[
    {
        "key": "ctrl+f",
        "command": "actions.find",
        "when": "editorTextFocus && vim.active && vim.use<C-f> && vim.mode == 'Insert' && !inDebugRepl"
    },
    {
        "key": "ctrl+pageup",
        "command": "workbench.action.terminal.focusPrevious",
        "when": "terminalFocus"
    },
    {
        "key": "ctrl+pagedown",
        "command": "workbench.action.terminal.focusNext",
        "when": "terminalFocus"
    },
    {
        "key": "ctrl+pageup",
        "command": "workbench.action.previousEditor",
        "when": "editorTextFocus && vim.active && vim.use<C-pageup> && !inDebugRepl"
    },
    {
        "key": "ctrl+pagedown",
        "command": "workbench.action.nextEditor",
        "when": "editorTextFocus && vim.active && vim.use<C-pagedown> && !inDebugRepl"
    },
    {
        "key": "tab",
        "command": "editor.action.indentLines",
        "when": "editorFocus && vim.active && !inDebugRepl && vim.mode != 'Insert'"
    }
]

乗り換えて

非常に快適。とにかく速い。 詳しくはわからないが、Language Serverというのを使っているせいか、オートコンプリートなどの重い機能もやけに速くストレスが無い。

何気にGitの簡単な操作もCode上でできてしまうので便利。

Visual Studioを使っており、いつも重いなぁと思いながら使っていたが、 もしかするとMicrosoftが作っていなかったらもっと遅いIDEになっていたのかもしれないとさえ思った。

とにかくVisual Studio Codeおすすめ。

PhoenixでbrunchのままVueを使う

Vueといえばwebpackというイメージだが、Phoenixを使うと最初からbrunchが入っている。 webpackを使うこともできるが色々と設定するのも面倒なので、簡単にbrunchのまま使うための方法。

ちなみに下記で紹介されている方法と同じ。

Phoenix, Brunch and VueJS: Part 2 - Baroni Tech

とりあえずVueをインストール

npm install vue

そしてbrunchで使えるようにするためのライブラリもインストール。 両方無いと動かない。

npm install --save-dev babel-plugin-transform-runtime vue2-brunch

サンプルのGithubのbrunch-config.jsを見てみると、下記のようになっているので追記。 (aliasesを追記)

  npm: {
    enabled: true,
    aliases: {
      'vue': 'vue/dist/vue.common.js'
    }
  }

あとは元々あったapp.jsに下記を追記するだけでもう動く。

import 'vueify/lib/insert-css'
import Vue from 'vue'
import Hello from './components/hello.vue'

new Vue({ 
  el: '#container',
  components: { Hello }
});

上記であればhtml上のid=container要素内のhelloタグがあれば動作する。 勝手にcontainer内が全部置きかわってしまったりなどはしないので、 一部だけ実装したいとかでも可能。 非常に簡単で便利。

サイボウズLiveを作る-第4回-Todoをざっと

掲示板をざっと作成後、次は次に簡単そうなToDoを作成することにした。 とりあえずざっと下記を作成した。

ToDoの新規登録、編集、コメント追加

特に目新しいこともなく、コメントなどはほとんど掲示板と同じ。 黙々とシンプルに作成したので、特筆することはなかった。

担当者選択UI

本家だとselectのマルチセレクトで複数の担当者を選択できるように実装されている。 昔は良く使われていた気がする。ちゃちゃっとjavascriptで作成できる。

ただ、今の時代はそういったUIはnpmでインストールするだけ。 丁度良さそうなものを見つけたので導入した。

GitHub - SortableJS/Vue.Draggable: Vue component allowing drag-and-drop sorting in sync with View-Model. Based on Sortable.js

READMEを見ると分かるように、ドラッグで並び替えも、左右のボックスで入れ替えもできる。 Vueのコンポーネントを作成してhiddenタグを自動的に更新するだけで実装できる。

f:id:dala:20171127220507p:plain

コンポーネントの実装も非常にシンプル。

<template>
  <div class="row">
    <div class="col-6 col-sm-3">
      <div class="card">
        <draggable v-model="selectedUsers" :element="'ul'" :options="{group:'users'}" @start="drag=true" @end="drag=true" class="list-group list-group-flush">
          <li class="list-group-item" v-for="(user, index) in selectedUsers" :key="user.id">
            <img :src="user.avatar">
            {{user.name}}
            <input type="hidden" :name="`todo_task[todo_tasks_users][${index}][user_id]`" :value="user.id">
            <input type="hidden" :name="`todo_task[todo_tasks_users][${index}][display_order]`" :value="index + 1">
          </li>
        </draggable>
      </div>
    </div>
    <div class="col-6 col-sm-3">
      <div class="card text-secondary">
        <draggable v-model="allUsers" :element="'ul'" :options="{group:'users'}" @start="drag=true" @end="drag=true" class="list-group list-group-flush">
          <li class="list-group-item" v-for="user in allUsers" :key="user.id">
            <img :src="user.avatar">
            {{user.name}}
          </li>
        </draggable>
      </div>
    </div>
    <input v-if="selectedUsers.length == 0" type="hidden" name="todo_task[todo_tasks_users]">
  </div>
</template>

<style scoped>
li {
  cursor: pointer;
}

img {
  width: 24px;
}
</style>

<script>
import draggable from 'vuedraggable'

export default {
  components: {draggable},
  props: ['name', 'value', 'users', 'selected'],

  data () {
    const users = JSON.parse(this.users);
    const selected = JSON.parse(this.selected);
    return {
      allUsers: users.filter(user => selected.indexOf(user.id) === -1),
      selectedUsers: users.filter(user => selected.indexOf(user.id) !== -1),
    }
  },

  methods: {
  }
}
</script>

呼び出しも簡単。

    <todo-user-select users="<%= Poison.encode!(@users) %>" selected="[<%= if Map.get(@conn.assigns, :todo_task) do
      Enum.join(Cybozulive.Todo.TodoTask.user_ids(@todo_task), ",")
    end %>]"></todo-user-select>

締め切り日時選択

これもよくあるのはinputタグをクリックすると日付選択UIが現れるもの。 ただ、inputタグを使って直接入力させる必要性も感じなかったので完全にDatepickerとTimepickerで選択させるようにした。

Datepicker。左下のリンクをクリックで表示される。ゴミ箱クリックでnullとなる。

f:id:dala:20171127221425p:plain

Timepicker。

f:id:dala:20171127221628p:plain

実装も特筆することはなくシンプル。Timepickerもほとんど同じ。

<template>
  <span>
    <a ref="toggle" href="#" @click.prevent="toggle()">
      {{showDate()}}
    </a>
    <a href="#" @click.prevent="setNull()"><i class="material-icons">delete_forever</i></a>
    <input type="hidden" :name="name" :value="showValue()">
  </span>
</template>

<style scoped>
.material-icons {
  font-size: 20px;
}
</style>

<script>
import mdDateTimePicker from 'md-date-time-picker'
import moment from 'moment'

const dialog = new mdDateTimePicker({
  type: 'date'
})

export default {
  props: ['name', 'value'],

  data () {
    const currentValue = this.value === '' ? null : moment(this.value);
    if (currentValue !== null) {
      dialog.time = currentValue;
    }
    return {
      currentValue,
    }
  },

  mounted() {
    dialog.trigger = this.$refs.toggle;
    this.$refs.toggle.addEventListener('onOk', () => {
      this.currentValue = dialog.time;
    })
  },

  methods: {
    toggle() {
      dialog.toggle();
    },

    showDate() {
      if (this.currentValue === null) {
        return '(未設定)';
      }
      return this.currentValue.format('YYYY-MM-DD');
    },

    showValue() {
      if (this.currentValue === null) {
        return '';
      }
      return this.currentValue.format('YYYY-MM-DD');
    },

    setNull() {
      this.currentValue = null;
    }
  }
}
</script>

マークダウン

<datepicker name="todo_task[limited_date]" value="<%= Ecto.Changeset.get_field(@changeset, :limited_date) %>"></datepicker>
<timepicker name="todo_task[limited_time]" value="<%= Cybozulive.Todo.TodoTask.limited_time(@changeset.data) %>"></timepicker>

次はスケジュールを作成、としたいところだが、グループウェアなのにユーザーが自分しかいないのが意味不明なので、 とりあえず招待とかを作ろうかと思う。 本当はメールアドレスのような個人情報は登録したくはないのだが…非ユーザーを招待するならそれしかなさそう。 (なんかあるのかな)

Copying live

幼稚園用達成ボードアプリを作成

概要

タブレット向け幼稚園用達成ボードアプリを作成した。 (単なるWEBアプリ)

https://child-achievement.alphabrend.com/

適当に作ったのでBitbucketは使わずGithubで公開している。

GitHub - dala00/child-achievement: Achievement for children

経緯

息子の発達が遅れており、 幼稚園で勝手にうろうろしたり指示を聞けなかったりするので、 少しでも頑張る気持ちを増やせないかと作成した。

特徴

報告画面

f:id:dala:20171117215035p:plain

  • タブレットなので楽しい
  • 簡単タップ操作のみ
  • Google認証のため誰でも使用可能
  • 発達が遅れている子向けのためそうでなければ簡単すぎて楽しくなさそう

集計画面

f:id:dala:20171117215410p:plain

  • 30回達成(1日最大6項目)するとレベルアップするので楽しいかもしれない
  • 週ごとの集計なので早く推移が分かってやる気が出るかもしれない

開発環境

ほんとはElixir & Phoenixで作りたかったが、デプロイに時間がかかる可能性があった。 作っている間に幼稚園を追い出されたら意味がないのでとにかく早く作ることを目的とし、 ぱぱっと設置できるphp5.6のさくらレンタルにLaravel5.4で作って入れた。 (それなのに本質とは関係ない認証導入でちょっとてまどってしまったが…)

まとめ

息子にやらせていたら2歳の次男もやりたそうに騒いで大変だった。

マルチアカウントなのでやらせることは可能なのだが、 そもそも幼稚園向けの項目しかないのであまり意味がない。

すると妻がちゃちゃっとコピペみたいな感じで流用できるんだから用意してあげたら、 みたいなことを言ってきて顧客にはしたくないなと思った。

最終的にアカウントIDで項目を変更できるよう修正した。(設定機能はない)

サイボウズLiveを作る-第3回-トピック登録まで

グループは作成できたので次は実際のコンテンツを作成していく。 とりあえず仕様的にシンプルそうな掲示板を作ってみることにした。 (もしかすると細かい機能が多くあるのかもしれないが)

処理的に特筆するところは特に何もなかったが、 投稿に関してはwysiwygエディタを入れた。

最終的に画像のアップロードも必要だと思うので有料になるCKEditorは無し。 最近のスタンダードがよく分からなかったのでStarやForkが非常に多い下記を入れてみた。

github.com

昔のwysiwygエディタといえば、textareaをターゲットにして起動すれば勝手にぜんぶやってくれたが、 これは多分SPA等も考慮されていると思うので勝手にPOSTまで出来るようにはなっていない。 そのため自前でハンドリングしてhiddenタグに入れる。

import Quill from 'quill';

$(function() {
  $('div.richtext').each(function() {
    const $this = $(this);
    const quill = new Quill(this, {
      theme: 'snow',
    })
    $this.data('quill', quill);
    quill.on('text-change', function(delta, oldDelta, source) {
      const html = $this.find('.ql-editor').html();
      this.next('input[type=hidden]').val(html);
    }.bind($this))
})
  <div class="form-group">
    <%= label f, :body, "本文", class: "control-label" %>
    <div class="richtext">
      <p><%= raw(Ecto.Changeset.get_field(@changeset, :body)) %></p>
    </div>
    <input type="hidden" name="board_topic[body]" value="<%= Ecto.Changeset.get_field(@changeset, :body) %>">
    <%= error_tag f, :body %>
  </div>

上記は元々jQueryで書いていたが下記はVueで書きなおしたもの。

<template>
  <div>
    <div ref="richtext">
      <p v-html="value"></p>
    </div>
    <input type="hidden" :name="name" :value="currentValue">
  </div>
</template>

<script>
import Quill from 'quill'

export default {
  props: ['name', 'value'],

  data () {
    return {
      currentValue: this.value,
    }
  },

  mounted() {
    const quill = new Quill(this.$refs.richtext, {
      theme: 'snow',
    });
    this.$refs.richtext.querySelector('input[type=text]').classList.add('form-control');
    quill.on('text-change', (delta, oldDelta, source) => {
      const html = this.$refs.richtext.querySelector('.ql-editor').innerHTML;
      this.currentValue = html;
    })
  },

  methods: {
  }
}
</script>

呼び出しも下記で良いので非常に簡単。新しい時代が来てるなぁという感じ。

 <editor name="board_post[body]" value="<%= Ecto.Changeset.get_field(@changeset, :body) %>"></editor>

htmlを取る方法も特に無いようなので、 issueを探ってみたら.ql-editor内をそのまま使えばいいとの事だったのでそのようにした。

現在までの完成分はこちら。 とりあえずコメント投稿まで。カテゴリも設定、絞り込みできるようにした。 その他の処理はほぼエラー。 あとはタイムゾーンの設定をしていないので時刻がおかしい。

Copying live