tag:crieit.net,2005:https://crieit.net/tags/Phoenix/feed 「Phoenix」の記事 - Crieit Crieitでタグ「Phoenix」に投稿された最近の記事 2022-05-19T23:55:46+09:00 https://crieit.net/tags/Phoenix/feed tag:crieit.net,2005:PublicArticle/18194 2022-05-19T23:55:46+09:00 2022-05-19T23:55:46+09:00 https://crieit.net/posts/php-phoenix-migrate-db-depend-other-db-20220521 (PHP) Phoenix でデータ関係が依存しているDBをマイグレーションする <p><a target="_blank" rel="nofollow noopener" href="https://github.com/lulco/phoenix">lulco/phoenix</a> でデータ関係が依存しているDBをマイグレーションする方法をメモ。</p> <p>ドキュメントが全然ないので地道にやっていきます……。</p> <h2 id="コード"><a href="#%E3%82%B3%E3%83%BC%E3%83%89">コード</a></h2> <p>早速コードを。</p> <h3 id="phoenix.php"><a href="#phoenix.php">phoenix.php</a></h3> <pre><code class="php">return [ 'migration_dirs' => [ 'first' => __DIR__ . '/migrations/first', 'second' => __DIR__ . '/migrations/second', ], 'environments' => [ 'local' => [ 'adapter' => 'mysql', 'host' => $_ENV['MYSQL_HOST'], 'port' => (int)$_ENV['MYSQL_PORT'], // optional 'username' => $_ENV['MYSQL_USER'], 'password' => $_ENV['MYSQL_PASSWORD'], 'db_name' => $_ENV['MYSQL_DBNAME'], 'charset' => 'utf8mb4', ], 'production' => [ 'adapter' => 'mysql', 'host' => $_ENV['MYSQL_HOST'], 'port' => (int)$_ENV['MYSQL_PORT'], // optional 'username' => $_ENV['MYSQL_USER'], 'password' => $_ENV['MYSQL_PASSWORD'], 'db_name' => $_ENV['MYSQL_DBNAME'], 'charset' => 'utf8mb4', ], ], 'default_environment' => 'local', 'log_table_name' => 'phoenix_log', ]; </code></pre> <p>肝は <code>migration_dirs</code> で <code>first</code> と <code>second</code> でそれぞれ対応するディレクトリを指定しているところ。</p> <h3 id="/migrations/first/hoge.php"><a href="#%2Fmigrations%2Ffirst%2Fhoge.php">/migrations/first/hoge.php</a></h3> <pre><code class="php"><?php namespace migrations; use Phoenix\Database\Element\Index; use Phoenix\Migration\AbstractMigration; class HogeMigration extends AbstractMigration { protected function up(): void { $this->table('hoge') ->addColumn('create_date', 'datetime') ->addColumn('name', 'string') ->create(); // insert $hogeData = [ [ 'name' => 'foo' ], [ 'name' => 'bar' ], [ 'name' => 'buz' ], // 略 ]; $rows = []; foreach ($hogeData as $key => $val) { $rows[] = [ 'create_date' => date('Y-m-d H:i:s'), 'name' => $val['name'], ]; } $this->insert('hoge', $rows); } protected function down(): void { $this->table('hoge') ->drop(); } } </code></pre> <p>まずは最初に <code>hoge</code> というDBを作成し、そこにデータを流し込みます。</p> <h3 id="/migrations/second/fuga.php"><a href="#%2Fmigrations%2Fsecond%2Ffuga.php">/migrations/second/fuga.php</a></h3> <pre><code class="php"><?php namespace migrations; use Phoenix\Database\Element\Index; use Phoenix\Migration\AbstractMigration; class FugaMigration extends AbstractMigration { protected function up(): void { $this->table('fuga') ->addColumn('create_date', 'datetime') ->addColumn('name', 'string') ->addColumn('hoge_id', 'integer') ->create(); // select hoge data $hogeRows = $this->select('SELECT id, name FROM hoge'); // insert $fugaData = [ [ 'name' => 'un' ], [ 'name' => 'deux' ], [ 'name' => 'trois' ], // 略 ]; $rows = []; foreach ($fugaData as $key => $val) { $id = 0; // hoge の name と fuga の name が一致する要素を array_filter() で抽出し、 array_values() で番号を詰める $hogeArray = array_values( array_filter( $hogeRows, function($hogeRow) use ($val) { $needle = mb_strlen(mb_strtolower($val[0])) > 0 ? mb_strtolower($val[0]) : 'NOTHING'; return mb_strpos(mb_strtolower($hogeRow['name']), $needle) !== false; } ) ); // $hogeArray の要素が1つ (一意に定まる) 場合はその値を、そうでない場合はデフォルト値をセット $id = count($hogeArray) === 1 ? (int)$hogeArray[0]['id'] : 0; $rows[] = [ 'create_date' => date('Y-m-d H:i:s'), 'name' => $val[0], 'hoge_id' => $id, // 上述でセットした id を使用 ]; } $this->insert('fuga', $rows); } protected function down(): void { $this->table('fuga') ->drop(); } } </code></pre> <p>次に <code>fuga</code> を作成し、そこに徐に <code>$this->select()</code> で SQL文 を発行、先程流し込んだデータを抽出します。</p> <p>その抽出したデータと <code>fuga</code> に流し込みたいデータを突き合わせて初期データを生成し、それを <code>fuga</code> に流し込む……という算段。</p> <p>これで意図したデータをマイグレーションすることができました。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/lulco/phoenix">GitHub - lulco\/phoenix: Framework agnostic database migrations for PHP.</a> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/lulco/phoenix/blob/master/src/Database/Adapter/PdoAdapter.php">phoenix\/PdoAdapter.php at master · lulco\/phoenix · GitHub</a></li> </ul></li> </ul> <p>ドキュメントがないのでコードを読んで普通に <code>select</code> とか使えそう、と思って試したりしていました。</p> arm-band tag:crieit.net,2005:PublicArticle/15424 2019-09-25T09:50:26+09:00 2019-09-25T09:50:26+09:00 https://crieit.net/posts/Elixir-Phoenix-Heroku-Docker Elixir+PhoenixのサービスをHeroku+Dockerで動かす <p>Elixir & PhoenixのサービスをDockerイメージでHerokuにデプロイした時の備忘録です。</p> <p>背景として、元々GCE上で動作させていましたが、GCEのIPが有料になるかもという情報があったのでHerokuに移行することにしました。ただ、無料枠で運用している限りだとIPも有料にはならないかもということなので意味はなかったかもしれません。</p> <p>ただ、もう誰も使っていないサービスでデータも少ないですし、ElixirとかNodeのようなビルドがある言語のサービスは本番サーバーでビルドするとサーバーが止まってしまうため本来行うべきではないため、気分転換にDockerでのリリース形式への変更を試してみるのには良かったかなと思いました。</p> <p>ちなみにHerokuではよく使われているElixir用のBuildpackがあるため、本来はそちらを利用したほうが簡単だと思います。しかし、今回のサービスは結構バージョンが古く、ちらっと試したところ動かないようでした。何か設定がおかしかったのかもしれませんが、とにかく試行錯誤するよりは元々Linuxサーバー上で動いているものなのでDocker化してデプロイするようにした方が早くて確実そうだったのでそちらの形にしました。</p> <h2 id="環境"><a href="#%E7%92%B0%E5%A2%83">環境</a></h2> <ul> <li>Elixir 1.6.0</li> <li>Phoenix 1.3.0</li> </ul> <h2 id="Dockerfileをつくる"><a href="#Dockerfile%E3%82%92%E3%81%A4%E3%81%8F%E3%82%8B">Dockerfileをつくる</a></h2> <p>できたのがこんな感じです。</p> <pre><code class="dockerfile">FROM elixir:1.6.0-slim RUN apt-get update && \ apt-get install -y dbus git curl RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - RUN apt-get install -y nodejs inotify-tools RUN apt-get install -y --reinstall build-essential RUN apt-get clean && \ rm -rf /var/lib/apt/lists/* ENV MIX_HOME=/root/.mix MIX_ENV=prod COPY . /app WORKDIR /app RUN mix local.hex --force && \ mix local.rebar --force && \ mix deps.get && \ mix compile EXPOSE 4000 CMD mix phx.server </code></pre> <p>開発もDockerで行っており、本番もLinuxでデプロイ方法はメモしていたのでここは特に問題ありませんでした(余分な処理は混じっているかもしれませんが)。JavaScriptのビルドもありますがこれはローカルでビルドしてそのまま使っています。今回はもう更新しないサービスのDocker化のため処理を入れていませんが、必要であればここでビルドもしておくと良いかもしれません。</p> <p>細かい部分をメモがてら解説しておきます。</p> <h3 id="MIX_HOMEを指定"><a href="#MIX_HOME%E3%82%92%E6%8C%87%E5%AE%9A">MIX_HOMEを指定</a></h3> <p>環境変数としてMIX_HOMEを指定しています。</p> <pre><code class="dockerfile">ENV MIX_HOME=/root/.mix MIX_ENV=prod </code></pre> <p>HerokuのDockerデプロイではrootユーザーではなく、その場で適当なユーザーが作られそちらで起動などのコマンドが実行されます。そのため、Dockerイメージビルド時にhexやrebarを追加した時にrootのディレクトリにインストールされたものが認識されず、サーバー起動コマンド実行時にまたインストール&ビルドが行われてしまいます。すると時間が経ちすぎてデプロイに失敗します。そのためイメージビルド時に処理したものを正常に認識させるため、MIX_HOMEを指定しています。</p> <p>dockerfile内でユーザーの作成&指定も可能ですが、これは無効となります。</p> <h3 id="EXPOSEは不要"><a href="#EXPOSE%E3%81%AF%E4%B8%8D%E8%A6%81">EXPOSEは不要</a></h3> <p><code>EXPOSE 4000</code>を書いていますがこれは不要です。後述しますが、デプロイ時に<code>PORT</code>の環境変数が渡されるため、そちらでサーバーを起動するようにPhoenix側で設定する必要があります。</p> <h2 id="Phoenixの設定"><a href="#Phoenix%E3%81%AE%E8%A8%AD%E5%AE%9A">Phoenixの設定</a></h2> <p><code>config/prod.exs</code>を設定します。元々のサーバーの設定から変えたところはとりあえずこんな感じになりました。</p> <pre><code class="elixir">config :profile, ProfileWeb.Endpoint, load_from_system_env: false, url: [host: System.get_env("APP_HOST"), scheme: "https", port: 443], http: [port: System.get_env("PORT")], cache_static_manifest: "priv/static/cache_manifest.json" config :profile, Profile.Repo, adapter: Ecto.Adapters.MySQL, username: System.get_env("DB_USERNAME"), password: System.get_env("DB_PASSWORD"), database: System.get_env("DB_DATABASE"), hostname: System.get_env("DB_HOST"), charset: "utf8mb4", ssl: true, pool_size: 5 </code></pre> <h3 id="環境変数を使う形にする"><a href="#%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0%E3%82%92%E4%BD%BF%E3%81%86%E5%BD%A2%E3%81%AB%E3%81%99%E3%82%8B">環境変数を使う形にする</a></h3> <p>下記のように、可能な限り環境変数を使う形にします。</p> <pre><code class="elixir"> username: System.get_env("DB_USERNAME"), password: System.get_env("DB_PASSWORD"), database: System.get_env("DB_DATABASE"), hostname: System.get_env("DB_HOST"), </code></pre> <p>今はどうか知りませんが、Phoenixのプロジェクトを初期化すると<code>prod.secret.exs</code>を作ってそれがprod.exsから読み込まれることで本番の設定をする、という形になっていましたのでついついその形でやってしまいがちかもしれませんが、Herokuなどで利用することを考えると全部環境変数を利用する形にしておく必要がありますし、そもそも設定も環境毎で別にする必要もなくなるため環境変数を利用する方がシンプルだと思います。</p> <h3 id="DBのpool_sizeを減らす"><a href="#DB%E3%81%AEpool_size%E3%82%92%E6%B8%9B%E3%82%89%E3%81%99">DBのpool_sizeを減らす</a></h3> <pre><code class="elixir"> pool_size: 5 </code></pre> <p>Herokuの無料DBはいくつか種類があります。今回はJawsDBを使っていますが、どれにしろ最大接続数が決まっていますので、それより低くpool_sizeを設定しておかないと接続エラーで起動できなくなります。各DBの仕様を見て最大数よりもちょっと少なめにしておきましょう。</p> <h3 id="ポートの設定"><a href="#%E3%83%9D%E3%83%BC%E3%83%88%E3%81%AE%E8%A8%AD%E5%AE%9A">ポートの設定</a></h3> <p>前述したとおり、ポート番号は環境変数で指定されるため、それでListenする必要があります。そのため下記のように指定します。</p> <pre><code class="elixir"> http: [port: System.get_env("PORT")], </code></pre> <h3 id="URLの設定"><a href="#URL%E3%81%AE%E8%A8%AD%E5%AE%9A">URLの設定</a></h3> <p>通常は不要かもしれませんがURLの設定もしています。</p> <pre><code class="elixir"> url: [host: System.get_env("APP_HOST"), scheme: "https", port: 443], </code></pre> <p>OGPを利用しているため、Routerのヘルパーで正しい完全なURLをテンプレート上に出力する必要がありました。正しく設定をしないと<code>http://localhost:11242</code>みたいなURLが出力されてしまうため、上記のような設定を追加しています。<code>port: 443</code>は不要かなと思いましたが、設定しておくとURLのポート表記を消すことができます。</p> <h2 id="デプロイ"><a href="#%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4">デプロイ</a></h2> <p>諸々の設定ができたらデプロイします。これはHerokuのダッシュボードのDeployページで全部確認できますので細かくは書きませんが、とりあえず</p> <pre><code class="sh">heroku container:push web </code></pre> <p>で手元のDockerfileのイメージがビルド&pushされ、</p> <pre><code class="sh">heroku container:release web </code></pre> <p>でpushしたイメージでリリースされます。あとは<code>heroku run</code>でマイグレーションを行ったり、<code>heroku logs</code>でログを確認しておかしなところを解決すれば動くと思います。下記は実際に動いているものです。(絵文字が文字化けしていますが他のところでは正常に表示されているので単にここの不具合のようです。もう修正はしませんが…)</p> <p><a target="_blank" rel="nofollow noopener" href="https://torima.appllis.net/users/dala00">https://torima.appllis.net/users/dala00</a></p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d8ab79a0cbf3.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d8ab79a0cbf3.png?mw=700" alt="" /></a></p> <h2 id="独自ドメイン"><a href="#%E7%8B%AC%E8%87%AA%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3">独自ドメイン</a></h2> <p>独自ドメインは解約したかったのでついでに適当なサブドメインにURLを変更しました。ただどちらにしろ独自ドメインの設定をHeroku側で行わなければなりません。ただ、どうもHerokuでの独自ドメインは有料プランでないとできないようでした。</p> <p>しかし、Cloudflare経由にして無理やりSSLにすることもできるようですので、今回はそのようにしました。~~.herokuapp.comをCNAMEとしてDNSレコードを追加すればよいようです。Herokuの画面で一緒に<code>DNS Target</code>というものも表示されますが、これはCloudflare経由の場合は関係ないようです。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>Buildpackがあればそれを使えば楽ですが、Dockerを使えば特殊なパッケージをインストールしたサービスも動かす事ができますので、必要に応じてHerokuのDockerデプロイを使ってみると良さそうです。CI上でビルド&pushすればGitHubにpushするだけでも可能そうです。</p> <p>容量が少ないですが言語問わず無料でRDBが使えて簡単にデプロイできるのはHerokuだけだと思いますので、ユーザーやアクセスの少ない初期のサービスなどはやっぱりここが良さげだなと感じました。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14548 2018-09-21T20:04:31+09:00 2018-10-18T07:36:18+09:00 https://crieit.net/posts/Elixir-ExTwitter ElixirのExTwitterでツイートする <p>Elixirには<a target="_blank" rel="nofollow noopener" href="https://github.com/parroty/extwitter">ExTwitter</a>というTwitter用のライブラリがある。<br /> それでツイートしてみた。</p> <h2 id="設定"><a href="#%E8%A8%AD%E5%AE%9A">設定</a></h2> <p>ExTwitterの説明通り。access_tokenとaccess_token_secretはユーザーのものを使うので空にしている。</p> <h2 id="ツイート"><a href="#%E3%83%84%E3%82%A4%E3%83%BC%E3%83%88">ツイート</a></h2> <pre><code class="elixir"> def tweet(token, secret, body) do ex_twitter_configure(token, secret) ExTwitter.API.Tweets.update(body) end defp ex_twitter_configure(token, secret) do conf = [ consumer_key: Application.get_env(:extwitter, :oauth)[:consumer_key], consumer_secret: Application.get_env(:extwitter, :oauth)[:consumer_secret], access_token: token, access_token_secret: secret ] ExTwitter.configure(:process, conf) end </code></pre> <p>ログイン中のユーザーのアクセストークンを使うために、API実行前に設定をしている。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14227 2018-04-03T04:36:07+09:00 2018-10-18T07:32:11+09:00 https://crieit.net/posts/Phoenix1-3-ex-admin Phoenix1.3でex_adminを使う <p>Phoenixではex_adminという管理画面作成パッケージを導入することで簡単に管理画面が作成できる。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/smpallen99/ex_admin">smpallen99/ex_admin: ExAdmin is an auto administration package for Elixir and the Phoenix Framework</a></p> <p>しかし依存関係やフォルダ構成の違いの問題で最新のPhoenixではうまく動かない。ただ、よくよく調べてみると下記のphx-1.3ブランチを使えばできるっぽいので試してみた。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/smpallen99/ex_admin/tree/phx-1.3">https://github.com/smpallen99/ex_admin/tree/phx-1.3</a>(https://github.com/smpallen99/ex_admin/tree/phx-1.3)</p> <h3 id="導入方法"><a href="#%E5%B0%8E%E5%85%A5%E6%96%B9%E6%B3%95">導入方法</a></h3> <p>phx-1.3ブランチを指定して入れる。ビルドに失敗するのでgettextのバージョンも上げる。</p> <p>mix.exs</p> <pre><code class="elixir">defp deps do ... {:gettext, "~> 0.13.1"}, {:ex_admin, github: "smpallen99/ex_admin", branch: "phx-1.3"}, ... end </code></pre> <p>configを追加。</p> <p>config.exs</p> <pre><code class="elixir">config :ex_admin, repo: MyProject.Repo, module: MyProjectWeb, modules: [ MyProject.ExAdmin.Dashboard, ] </code></pre> <p>後は通常通りのインストールと同じ。</p> <h3 id="brunch-configの修正"><a href="#brunch-config%E3%81%AE%E4%BF%AE%E6%AD%A3">brunch-configの修正</a></h3> <p>そのままだとex_adminのcssとかが混ざってしまう。brunch-config.jsに修正方法が追記されているので、そのとおりに修正。</p> <p>またその際、node_modules内のcssをインポートしている場合はcssの設定だけ下記に修正が必要。</p> <pre><code class="js"> "css/app.css": /^(css|node_modules)/, </code></pre> <h3 id="モデル追加"><a href="#%E3%83%A2%E3%83%87%E3%83%AB%E8%BF%BD%E5%8A%A0">モデル追加</a></h3> <pre><code class="sh">mix admin.gen.resource User </code></pre> <p>これは特に変わりはない。ただ、1.3だと<code>phx.gen.html</code>を使っている場合、context module以下にモデルを入れていると思う。例えば<code>Accounts.User</code>等。</p> <p>なので生成されたadmin/user.ex内のモデルの指定だけ修正しておく。</p> <p>admin/user.ex(before)</p> <pre><code class="elixir"> register_resource MyProject.User do </code></pre> <p>admin/user.ex(after)</p> <pre><code class="elixir"> register_resource MyProject.Accounts.User do </code></pre> <p>これで動作した。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14233 2017-12-26T06:09:15+09:00 2018-10-29T23:50:59+09:00 https://crieit.net/posts/Wercker-Phoenix-CI WerckerでPhoenixアプリケーションのCI <p>※1.6.0にしてフォーマットを追記</p> <p>WerckerにてPhoenixアプリケーションのCIをするためのwercker.yml。<br /> DBもservicesで追加できるので専用のコンテナを準備する必要がない。</p> <pre><code class="yaml">box: shufo/phoenix:1.6.0 services: - id: mariadb name: mysql username: root password: "" tag: latest env: MYSQL_ALLOW_EMPTY_PASSWORD: "true" build: steps: - script: name: mix format --check-formatted code: mix format --check-formatted - script: name: mix deps.get code: mix deps.get - script: name: mix test code: mix test </code></pre> <p>ほんとはbrunchのビルドもしたかったのだがこのboxだとnpmコマンドが見つからない。<br /> 自分で作っているboxを使えば良いのだが、面倒だったのでとりあえずelixir側だけにした。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14235 2017-12-21T06:19:58+09:00 2018-10-31T19:05:36+09:00 https://crieit.net/posts/Live-6 サイボウズLiveを作る-第6回-イベント作成 <p>あと一つ大きなメイン機能であるイベント機能が残っていたのでそちらを作成した。</p> <p>色々見てみた結果、とりあえず全部FullCalendarに置き換えればいいだろうと言う結論に至った。</p> <p><a target="_blank" rel="nofollow noopener" href="https://fullcalendar.io/">FullCalendar - JavaScript Event Calendar</a></p> <p>期間や範囲切り替えもあるし、これだけで一通りまかなえる気がする。</p> <p><a href="https://crieit.now.sh/upload_images/84d7f7b7f8ae772890ba098f4217f40a5b0d186bce9d0.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/84d7f7b7f8ae772890ba098f4217f40a5b0d186bce9d0.png?mw=700" alt="" /></a></p> <h3 id="イベント予定メニューマスタ"><a href="#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E4%BA%88%E5%AE%9A%E3%83%A1%E3%83%8B%E3%83%A5%E3%83%BC%E3%83%9E%E3%82%B9%E3%82%BF">イベント予定メニューマスタ</a></h3> <p>本家にはない(?)が、予定メニューにも色を付けられるようにした。</p> <p><a href="https://crieit.now.sh/upload_images/3092bacb48ffa3436e5dda227b23a98f5b0d186c323ad.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/3092bacb48ffa3436e5dda227b23a98f5b0d186c323ad.png?mw=700" alt="" /></a></p> <pre><code class="html"><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> </code></pre> <p>一覧も<a target="_blank" rel="nofollow noopener" href="https://github.com/SortableJS/Vue.Draggable">vuedraggable</a>を使ってドラッグ&ドロップで簡単に並び替えできるようにした。</p> <p>面倒かと思ったが、元々のテンプレートをとりあえずコピーから始められるのでそれほどでもなかった。</p> <pre><code class="html"><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> </code></pre> <p>保存側。</p> <pre><code class="elixir"> 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 </code></pre> <h3 id="カレンダー"><a href="#%E3%82%AB%E3%83%AC%E3%83%B3%E3%83%80%E3%83%BC">カレンダー</a></h3> <p>こちらも面倒かと思ったが、データの取得はコールバックに作ればいいだけだったので非常に楽だった。</p> <pre><code class="html"><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> </code></pre> <p>取得側。<br /> こういう重そうな範囲検索は、大規模サービスだとどう実装してるのか気になる。<br /> 日毎にデータを分けてるのかな。(日を子データにしてるとか)大変そう。</p> <pre><code class="elixir"> 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 </code></pre> <h3 id="不足点"><a href="#%E4%B8%8D%E8%B6%B3%E7%82%B9">不足点</a></h3> <p>リピートの予定とか放置してる。</p> <p>あと、設備とかどこで使ってるんだろうと思ったら、アクセスの方法によって登録できたりするっぽい。<br /> 完全に抜けてる。</p> <p>わかりづらいなここは。急にグループ関係なしの画面になるし意味が分からない。</p> <h3 id="次"><a href="#%E6%AC%A1">次</a></h3> <p>テスト放置なので整備しようかと思う。<br /> そっちの方が面白くて書くこと多かったりするかもしれない。<br /> ソース書いてるのVueばっかりだし。</p> <p><a target="_blank" rel="nofollow noopener" href="https://live.alphabrend.com">Copying live</a></p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14237 2017-12-19T06:24:49+09:00 2018-10-24T21:37:00+09:00 https://crieit.net/posts/Systemd-Phoenix Systemdを使ったPhoenixの本番デプロイ詳細例 <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/advent-calendar/2017/elixir">Elixir Advent Calendar 2017 - Qiita</a></p> <p>19日目。</p> <p>サーバーを準備し、コンテナを使わずに運用できるところまでの準備まで一通りまとめてみた。<br /> Systemdで動作させる。</p> <p>(以前残したログをまとめているだけなので正確でない可能性あり)</p> <h3 id="前提"><a href="#%E5%89%8D%E6%8F%90">前提</a></h3> <ul> <li>Elixir1.5</li> <li>Phoenix1.3</li> <li>Debian</li> </ul> <h3 id="初期設定"><a href="#%E5%88%9D%E6%9C%9F%E8%A8%AD%E5%AE%9A">初期設定</a></h3> <p>最初だけ本番で調整したものをコミットしたりもするのでgitもちょっと設定。</p> <pre><code class="sh">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" </code></pre> <h3 id="Elixir &amp; Phoneix &amp; Node.js"><a href="#Elixir+%26amp%3B+Phoneix+%26amp%3B+Node.js">Elixir & Phoneix & Node.js</a></h3> <pre><code class="sh">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 </code></pre> <h3 id="DB"><a href="#DB">DB</a></h3> <p>MySQLとかPostgreSQLとか。<br /> 普通は別サーバーだろうし省略。</p> <h3 id="ビルド"><a href="#%E3%83%93%E3%83%AB%E3%83%89">ビルド</a></h3> <p>ssh-keygenかtokenかでcloneできるように準備。<br /> 適当なので環境変数とかユーザーは適宜いい感じにしてもらった方が良いかもしれない。</p> <pre><code class="sh">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 </code></pre> <p>とりあえずここまでで動くか確認する。</p> <h3 id="Let's Encrypt"><a href="#Let%27s+Encrypt">Let's Encrypt</a></h3> <p>省略。下記の記事に一応詳細は書いてある。</p> <p><a target="_blank" rel="nofollow noopener" href="http://alphabrend.hatenablog.com/entry/2017/10/10/214222">PhoenixでLet's EncryptによるSSL - アルファブレンド プログラミングチップス</a></p> <h3 id="Systemd"><a href="#Systemd">Systemd</a></h3> <pre><code class="sh">sudo vi /etc/systemd/system/myapp.service </code></pre> <pre><code class="ini">[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 </code></pre> <pre><code class="sh">sudo systemctl enable cybozulive sudo systemctl start cybozulive </code></pre> <h3 id="運用"><a href="#%E9%81%8B%E7%94%A8">運用</a></h3> <p>pull, compile, brunch build, digest, migrate, restart</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14238 2017-12-06T06:25:29+09:00 2018-10-18T07:16:11+09:00 https://crieit.net/posts/Live-5 サイボウズLiveを作る-第5回-グループへ参加 <p>とりあえず一旦グループにメンバーを追加する機能を進めてみた。</p> <p>メールアドレスは今のところ登録してほしくないし、とりあえずそれ無しでできる部分だけ進めた。</p> <p>具体的には本家と同じで、招待URLを使ってそこからアクセスしてログインすればグループ申請となる形。</p> <p>というか、本当にほとんどそれくらいなので何も書くことがない。<br /> とりあえず、ただそのまま処理を書いてるだけなので何の役にも立たないがソースでも貼っておく。</p> <pre><code class="elixir"><br /> 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 </code></pre> <p>ログインしていたらそのままメッセージもなく申請データが登録されて自分のページに戻るので、<br /> 非常にわかりづらい。さすがに直した方がいいかもしれない。</p> <p>承認。</p> <pre><code class="elixir"> 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 </code></pre> <p>あとはページ上に表示されているユーザーのリンクは単純なusersのshowだったが、<br /> 関係ないグループのユーザーも表示できてしまうので全てGroupUserのリンクに変更し、<br /> 同じグループのユーザーしかアクセスできないものにした。<br /> 自分の編集画面などはid等のパラメータなどもなしのURLに変更。</p> <h3 id="考察"><a href="#%E8%80%83%E5%AF%9F">考察</a></h3> <p>自分の知っている人を新たなグループに招待する機能もサイボウズLiveにはあり、<br /> それは便利なので必要かなと思う。<br /> 今回は申請、許可的な機能だがそちらは招待なので参加する側が許可すれば参加できる、<br /> 今回とは逆の機能となる。<br /> 招待テーブルを作るのか、フラグで分けるのか、また気が向いた時に本家の画面を見て決めたりなどが必要。</p> <h3 id="次"><a href="#%E6%AC%A1">次</a></h3> <p>次はイベント機能を進めようかと思う。<br /> それが終わったら今テストを全く触っておらず、自動生成されたままのためエラー出まくりなので、<br /> そっちを一旦整備もしたい。修正したり要らないものを捨てて絞ったり。<br /> (なんとなくそっちの方が書くことがあるような気がする)</p> <p>あとはイベント機能に伴い、今まで放置してたタイムゾーン問題も少し時間をかけて調べる時間を取ろうと思う。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14239 2017-12-05T07:03:13+09:00 2018-10-31T17:45:36+09:00 https://crieit.net/posts/e379835214e697a4af50e463d7a73400 個人開発のだいたいの流れの例 <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/advent-calendar/2017/private-development">個人開発 Advent Calendar 2017 - Qiita</a></p> <p>の5日目。</p> <h3 id="概要"><a href="#%E6%A6%82%E8%A6%81">概要</a></h3> <p>普段から時間があればプログラミングで遊びつつ何か作成している。<br /> せっかくなので誰も使わなくてもリリースしたりしていて、<br /> それらは特に仕事でやっているのとかけ離れているものではないので、<br /> 適当に箇条書き程度で紹介。</p> <h3 id="今作っているもの"><a href="#%E4%BB%8A%E4%BD%9C%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E3%82%82%E3%81%AE">今作っているもの</a></h3> <p>ちょっと前にサイボウズLiveが終了すると発表された。<br /> 自分も丁度お客さんが使っていてそこでやり取りをしていた。<br /> 丁度Elixir & Phoenixを使って色々遊んでいたところに飛び込んできたニュースで、<br /> 丁度別のアプリケーションをキリのいいところまで作り終えたところだったので、<br /> サイボウズLiveをコピーして作ってみようと思い作成中。</p> <p>成果物は下記。</p> <p><a target="_blank" rel="nofollow noopener" href="https://live.alphabrend.com">Copying live</a></p> <p>ちなみに当ブログで連載中。</p> <p><a target="_blank" rel="nofollow noopener" href="http://alphabrend.hatenablog.com/archive/category/%E3%82%B5%E3%82%A4%E3%83%9C%E3%82%A6%E3%82%BALive%E3%82%92%E4%BD%9C%E3%82%8B">サイボウズLiveを作る カテゴリーの記事一覧 - アルファブレンド プログラミングチップス</a></p> <h3 id="進め方"><a href="#%E9%80%B2%E3%82%81%E6%96%B9">進め方</a></h3> <p>とりあえずどんどん出来ていくのが楽しいので、細かいところは無視してどんどん自分のやりたいところを先に進める。<br /> 細かいところは後回し。<br /> 今もデータ登録が中心で削除等の細かい機能は放置している。</p> <h3 id="デザイン"><a href="#%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3">デザイン</a></h3> <p>デザイナーじゃないのでやっぱりBootstrap。<br /> 特に</p> <p><a target="_blank" rel="nofollow noopener" href="https://fezvrasta.github.io/bootstrap-material-design/">Bootstrap</a></p> <p>がおしゃれで何にでも使えるので良いと思う。<br /> JavaScriptのフレームワーク毎にMaterial Desginのライブラリがあったりもするのでそういうのでもいい。</p> <h3 id="とりあえずの画面"><a href="#%E3%81%A8%E3%82%8A%E3%81%82%E3%81%88%E3%81%9A%E3%81%AE%E7%94%BB%E9%9D%A2">とりあえずの画面</a></h3> <p>掲示板</p> <p><a href="https://crieit.now.sh/upload_images/2d38283e660a7c0117faeae103135a165b0d18736371a.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/2d38283e660a7c0117faeae103135a165b0d18736371a.png?mw=700" alt="" /></a></p> <p>Todo</p> <p><a href="https://crieit.now.sh/upload_images/a028542c2b31eaf44e1964a5fff3a0ab5b0d1873d7096.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/a028542c2b31eaf44e1964a5fff3a0ab5b0d1873d7096.png?mw=700" alt="" /></a></p> <h3 id="エディタ"><a href="#%E3%82%A8%E3%83%87%E3%82%A3%E3%82%BF">エディタ</a></h3> <p>Visual Studio Code</p> <p>今のところ知っている中では軽量&高機能&設定手軽なものでベストだと思う。</p> <h3 id="開発環境"><a href="#%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83">開発環境</a></h3> <p>docker-composeを使う。Linux Mintなのでただインストールして使うだけ。</p> <p>Elixir & Phoenixだと下記のような感じ。<br /> 見てのとおりだがPhoenix & MySQL & phpMyAdmin。<br /> Phoenixのイメージは誰かがどこかで公開していたのを参考にしただけのもの。</p> <pre><code class="yaml">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 </code></pre> <h3 id="ソース管理"><a href="#%E3%82%BD%E3%83%BC%E3%82%B9%E7%AE%A1%E7%90%86">ソース管理</a></h3> <p>自分の場合、ソースを公開していいならGitHubに置くし、<br /> 多少大きくてあまり公開したくないものはBitbucketに置いている。<br /> (Bitbucket最近重い気がするけど)</p> <h3 id="フロント"><a href="#%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88">フロント</a></h3> <p>Vueを使っている。<br /> Phoenixは最初からbrunchが入っているのでVueを入れたらそのまま使える。<br /> Phoenixの開発サーバーに元々ウォッチ機能がついていて、htmlもcssもJavaScriptも全部保存時に勝手に画面を更新してくれるので何も考える必要がない。<br /> そのためjQueryを使う意味すら無いしもう全員Vueで良いと思う。<br /> なにより簡単に適当に一部のみコンポーネント化できるのが最高。</p> <p>PHPならLaravelも最初からVueが使える。<br /> もしフレームワークに何もついてなくてもwebpack導入してwatchするだけなのでとりあえずもうVueを使っておけばいいと思う。</p> <h3 id="公開"><a href="#%E5%85%AC%E9%96%8B">公開</a></h3> <p>せっかく作ったならぐちゃぐちゃでも公開していったらいいと思う。</p> <p>僕の場合、PHPならさくらのスタンダードを1つ借りてるのでそちらにアップして公開する。<br /> (バージョン上げると動かなくなるものも入っているのでもうひとつ借りたくなるところ…)</p> <p>それ以外の場合、VPSとかでも1アプリケーションごとに500円とか千円とかかかってしまい、<br /> 積み重なっていくとモチベーションに影響してしまうので、最近だと全部GCEの無料のプラン。<br /> Herokuでも良いかもしれないがどうもDBの接続が安定しないようなのでちょっと不安。</p> <p>Phoenixのデプロイ方法はAdvent Calendarの別の記事で紹介する予定。</p> <p>あとSSLは例えしょうもないアプリでももう必須だと思う。<br /> 焦らずにリリース前の、失敗してサーバーをぐちゃぐちゃにしても問題ないタイミングでちゃんと設定しておきたい。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14241 2017-11-30T06:52:24+09:00 2018-10-21T09:30:26+09:00 https://crieit.net/posts/Phoenix-brunch-Vue PhoenixでbrunchのままVueを使う <p>Vueといえばwebpackというイメージだが、Phoenixを使うと最初からbrunchが入っている。<br /> webpackを使うこともできるが色々と設定するのも面倒なので、簡単にbrunchのまま使うための方法。</p> <p>ちなみに下記で紹介されている方法と同じ。</p> <p><a target="_blank" rel="nofollow noopener" href="https://baroni.tech/blog/phoenix-brunch-vuejs-part2/">Phoenix, Brunch and VueJS: Part 2 - Baroni Tech</a></p> <p>とりあえずVueをインストール</p> <pre><code class="bash">npm install vue </code></pre> <p>そしてbrunchで使えるようにするためのライブラリもインストール。<br /> 両方無いと動かない。</p> <pre><code class="bash">npm install --save-dev babel-plugin-transform-runtime vue2-brunch </code></pre> <p>サンプルのGithubのbrunch-config.jsを見てみると、下記のようになっているので追記。<br /> (aliasesを追記)</p> <pre><code class="javascript"> npm: { enabled: true, aliases: { 'vue': 'vue/dist/vue.common.js' } } </code></pre> <p>あとは元々あったapp.jsに下記を追記するだけでもう動く。</p> <pre><code class="javascript">import 'vueify/lib/insert-css' import Vue from 'vue' import Hello from './components/hello.vue' new Vue({ el: '#container', components: { Hello } }); </code></pre> <p>上記であればhtml上のid=container要素内のhelloタグがあれば動作する。<br /> 勝手にcontainer内が全部置きかわってしまったりなどはしないので、<br /> 一部だけ実装したいとかでも可能。<br /> 非常に簡単で便利。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14242 2017-11-28T07:21:48+09:00 2018-10-23T13:38:35+09:00 https://crieit.net/posts/Live-4-Todo サイボウズLiveを作る-第4回-Todoをざっと <p>掲示板をざっと作成後、次は次に簡単そうなToDoを作成することにした。<br /> とりあえずざっと下記を作成した。</p> <h3 id="ToDoの新規登録、編集、コメント追加"><a href="#ToDo%E3%81%AE%E6%96%B0%E8%A6%8F%E7%99%BB%E9%8C%B2%E3%80%81%E7%B7%A8%E9%9B%86%E3%80%81%E3%82%B3%E3%83%A1%E3%83%B3%E3%83%88%E8%BF%BD%E5%8A%A0">ToDoの新規登録、編集、コメント追加</a></h3> <p>特に目新しいこともなく、コメントなどはほとんど掲示板と同じ。<br /> 黙々とシンプルに作成したので、特筆することはなかった。</p> <h3 id="担当者選択UI"><a href="#%E6%8B%85%E5%BD%93%E8%80%85%E9%81%B8%E6%8A%9EUI">担当者選択UI</a></h3> <p>本家だとselectのマルチセレクトで複数の担当者を選択できるように実装されている。<br /> 昔は良く使われていた気がする。ちゃちゃっとjavascriptで作成できる。</p> <p>ただ、今の時代はそういったUIはnpmでインストールするだけ。<br /> 丁度良さそうなものを見つけたので導入した。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/SortableJS/Vue.Draggable">GitHub - SortableJS/Vue.Draggable: Vue component allowing drag-and-drop sorting in sync with View-Model. Based on Sortable.js</a></p> <p>READMEを見ると分かるように、ドラッグで並び替えも、左右のボックスで入れ替えもできる。<br /> Vueのコンポーネントを作成してhiddenタグを自動的に更新するだけで実装できる。</p> <p><a href="https://crieit.now.sh/upload_images/37464f594c64ce314978c655a879670b5b0d1877e77b1.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/37464f594c64ce314978c655a879670b5b0d1877e77b1.png?mw=700" alt="" /></a></p> <p>コンポーネントの実装も非常にシンプル。</p> <pre><code class="html"><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"> <span>{</span><span>{</span>user.name<span>}</span><span>}</span> <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"> <span>{</span><span>{</span>user.name<span>}</span><span>}</span> </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> </code></pre> <p>呼び出しも簡単。</p> <pre><code class="html"> <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> </code></pre> <h3 id="締め切り日時選択"><a href="#%E7%B7%A0%E3%82%81%E5%88%87%E3%82%8A%E6%97%A5%E6%99%82%E9%81%B8%E6%8A%9E">締め切り日時選択</a></h3> <p>これもよくあるのはinputタグをクリックすると日付選択UIが現れるもの。<br /> ただ、inputタグを使って直接入力させる必要性も感じなかったので完全にDatepickerとTimepickerで選択させるようにした。</p> <p>Datepicker。左下のリンクをクリックで表示される。ゴミ箱クリックでnullとなる。</p> <p><a href="https://crieit.now.sh/upload_images/961ef32b2d18db8213c1b029c5b324755b0d18786630e.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/961ef32b2d18db8213c1b029c5b324755b0d18786630e.png?mw=700" alt="" /></a></p> <p>Timepicker。</p> <p><a href="https://crieit.now.sh/upload_images/e3d7b78c81b911b9da6790256b7372ef5b0d1878cab53.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/e3d7b78c81b911b9da6790256b7372ef5b0d1878cab53.png?mw=700" alt="" /></a></p> <p>実装も特筆することはなくシンプル。Timepickerもほとんど同じ。</p> <pre><code class="html"><template> <span> <a ref="toggle" href="#" @click.prevent="toggle()"> <span>{</span><span>{</span>showDate()<span>}</span><span>}</span> </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> </code></pre> <p>マークダウン</p> <pre><code class="html"><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> </code></pre> <h3 id="次"><a href="#%E6%AC%A1">次</a></h3> <p>次はスケジュールを作成、としたいところだが、グループウェアなのにユーザーが自分しかいないのが意味不明なので、<br /> とりあえず招待とかを作ろうかと思う。<br /> 本当はメールアドレスのような個人情報は登録したくはないのだが…非ユーザーを招待するならそれしかなさそう。<br /> (なんかあるのかな)</p> <p><a target="_blank" rel="nofollow noopener" href="https://live.alphabrend.com">Copying live</a></p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14244 2017-11-21T06:49:27+09:00 2017-11-21T06:49:27+09:00 https://crieit.net/posts/Live-3 サイボウズLiveを作る-第3回-トピック登録まで <p>グループは作成できたので次は実際のコンテンツを作成していく。<br /> とりあえず仕様的にシンプルそうな掲示板を作ってみることにした。<br /> (もしかすると細かい機能が多くあるのかもしれないが)</p> <p>処理的に特筆するところは特に何もなかったが、<br /> 投稿に関してはwysiwygエディタを入れた。</p> <p>最終的に画像のアップロードも必要だと思うので有料になるCKEditorは無し。<br /> 最近のスタンダードがよく分からなかったのでStarやForkが非常に多い下記を入れてみた。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/quilljs/quill">GitHub - quilljs/quill: Quill is a modern WYSIWYG editor built for compatibility and extensibility.</a></p> <p>昔のwysiwygエディタといえば、textareaをターゲットにして起動すれば勝手にぜんぶやってくれたが、<br /> これは多分SPA等も考慮されていると思うので勝手にPOSTまで出来るようにはなっていない。<br /> そのため自前でハンドリングしてhiddenタグに入れる。</p> <pre><code class="javascript">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)) }) </code></pre> <pre><code class="html"> <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> </code></pre> <p>上記は元々jQueryで書いていたが下記はVueで書きなおしたもの。</p> <pre><code class="html"><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> </code></pre> <p>呼び出しも下記で良いので非常に簡単。新しい時代が来てるなぁという感じ。</p> <pre><code class="html"> <editor name="board_post[body]" value="<%= Ecto.Changeset.get_field(@changeset, :body) %>"></editor> </code></pre> <p>htmlを取る方法も特に無いようなので、<br /> issueを探ってみたら.ql-editor内をそのまま使えばいいとの事だったのでそのようにした。</p> <p>現在までの完成分はこちら。<br /> とりあえずコメント投稿まで。カテゴリも設定、絞り込みできるようにした。<br /> その他の処理はほぼエラー。<br /> あとはタイムゾーンの設定をしていないので時刻がおかしい。</p> <p><a target="_blank" rel="nofollow noopener" href="https://live.alphabrend.com">Copying live</a></p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14245 2017-11-14T07:04:08+09:00 2017-11-14T07:04:08+09:00 https://crieit.net/posts/Live-2 サイボウズLiveを作る-第2回-グループ登録まで <p>アイコンは出来たので引き続きグループの作成機能。</p> <p>本家だととりあえず一番簡単なパターンでは、グループ名だけ入力すれば登録できる。<br /> とりあえずそこまでを作った。<br /> アイコンも選択できるようにしている。</p> <p>実装は非常にシンプルで、まずモデルに所属メンバー用のアソシエーションを設定。マイグレーションなどもマニュアル通り。</p> <pre><code class="elixir"> many_to_many :users, Cybozulive.User, join_through: "groups_users" </code></pre> <p>登録処理もシンプル。</p> <pre><code class="elixir"> def create(conn, %{"group" => group_params}) do user = Auth.get_user(conn) changeset = Ecto.build_assoc(user, :groups) |> Group.changeset(group_params) |> Ecto.Changeset.put_assoc(:users, [user]) case Repo.insert(changeset) do {:ok, group} -> conn |> put_flash(:info, "グループを作成しました。") |> redirect(to: group_path(conn, :show, group)) {:error, changeset} -> icons = IconRepo.get_select_icons(user.id) render(conn, "new.html", changeset: changeset, icons: icons, show_group: false) end end </code></pre> <p>とりあえずここまでを公開した。</p> <p>Copying live</p> <p><a target="_blank" rel="nofollow noopener" href="https://live.alphabrend.com">https://live.alphabrend.com</a></p> <p>GCEのf1-micro、1台にDBまで全部詰め込み。<br /> Let's EncryptでSSL対応。<br /> 体裁とか未完成の部分はぐちゃぐちゃ。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14247 2017-11-08T06:42:30+09:00 2017-11-08T06:42:30+09:00 https://crieit.net/posts/Phoenix-Task-DB PhoenixのTaskでDBにアクセスする <p>PhoenixでTaskを試した。</p> <p>実際にはPhoenixではなくmix自体のtaskを作成して実行するだけ。</p> <p>ただ、単にtask内でRepoを使ってDBアクセスしようとすると下記のようなエラーが出る。</p> <pre><code>repo App.Repo is not started, please ensure it is part of your supervision tree </code></pre> <p>どうもRepoはちゃんとスタートさせなければならないらしい。</p> <p>最終的に下記の様にRepoの初期化を入れることで動いた。</p> <pre><code class="elixir">defmodule Mix.Tasks.Aiue.Oooo do use Mix.Task alias App.Repo alias App.User import Mix.Ecto @shortdoc "aiueo" @moduledoc """ This is aiuoe """ def run(args) do Mix.shell.info "=== Active user ===" ensure_repo(Repo, args) ensure_started(Repo, []) user = Repo.one!(User) changeset = User.changeset(user, %{email: "[email protected]"}) Repo.update!(changeset) IO.inspect(user) end end </code></pre> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14248 2017-11-07T06:28:04+09:00 2018-10-18T07:17:26+09:00 https://crieit.net/posts/Live-1 サイボウズLiveを作る-第1回-アイコン登録まで <h3 id="概要"><a href="#%E6%A6%82%E8%A6%81">概要</a></h3> <p>サイボウズLiveが終了するとのこと。</p> <p><a target="_blank" rel="nofollow noopener" href="https://topics.cybozu.co.jp/news/2017/10/24-4407.html">無料グループウェア「サイボウズLive」サービス終了のお知らせ | サイボウズ株式会社</a></p> <p>丁度他のアプリケーション作成が一区切りついたところだったので、今度はサイボウズLiveのコピーを作ってみようと思う。<br /> ざっと見てみたらそんなにページ数も多くなさそうな気もするし。</p> <p>一通り作ってはみようと思うが特に代替、移行先として呼びこむつもりはない。<br /> そんなアクセスが来たら止まるだろうし。</p> <h3 id="仕様"><a href="#%E4%BB%95%E6%A7%98">仕様</a></h3> <ul> <li>Elixir1.5</li> <li>Phoenix1.3</li> <li>Bootstrap Material Design</li> </ul> <p>JavaScriptはAngularかVueを使おうかと思ったが、面倒だったことを思い出すと嫌になったのでやめた。<br /> ソロだし付属のbrunchでES6を使ってjQueryで綺麗に書けば十分。</p> <p>※PhoenixでVueも簡単に使えるようなので置き換え中。</p> <h3 id="とりあえず作ったところ"><a href="#%E3%81%A8%E3%82%8A%E3%81%82%E3%81%88%E3%81%9A%E4%BD%9C%E3%81%A3%E3%81%9F%E3%81%A8%E3%81%93%E3%82%8D">とりあえず作ったところ</a></h3> <h4 id="認証"><a href="#%E8%AA%8D%E8%A8%BC">認証</a></h4> <p>とりあえずTwitterでログインできるようにした。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/ueberauth/ueberauth_twitter">GitHub - ueberauth/ueberauth_twitter: Twitter Strategy for Überauth</a></p> <p>ユーザー作成やログイン部分は自分で勝手にやりたかったので、<br /> ログインURLの発行とcallbackのパラメータ構築だけ任せ、<br /> 取得したパラメータであれこれやっている。</p> <pre><code class="elixir">defmodule Cybozulive.AuthController do use Cybozulive.Web, :controller alias Cybozulive.UserRepo plug Ueberauth def request(conn, _params) do Ueberauth.Strategy.Helpers.callback_url(conn) end def callback(%{assigns: %{ueberauth_failure: _fails<span>}</span><span>}</span> = conn, _params) do conn |> put_flash(:error, "Failed to authenticate.") |> redirect(to: "/") end def callback(%{assigns: %{ueberauth_auth: auth<span>}</span><span>}</span> = conn, _params) do user = UserRepo.find_or_create!(auth) conn |> Cybozulive.Auth.set_user(user) |> redirect(to: "/") end end </code></pre> <h4 id="アイコンの登録"><a href="#%E3%82%A2%E3%82%A4%E3%82%B3%E3%83%B3%E3%81%AE%E7%99%BB%E9%8C%B2">アイコンの登録</a></h4> <p>とりあえず掲示板とか作りたいところだが、</p> <ul> <li>掲示板作るにはグループが要る</li> <li>グループ作るにはアイコンが要る</li> </ul> <p>ということで面倒だがとりあえずアイコンを登録できる機能を作った。</p> <p>phoenix.gen.htmlとarc_ectoでちゃちゃっと作れた。<br /> そのままだと画像は公開されないので、</p> <p><a target="_blank" rel="nofollow noopener" href="https://medium.com/@Stephanbv/elixir-phoenix-uploading-images-locally-with-arc-b1d5ec88f7a">Elixir / Phoenix — Uploading images locally (With ARC)</a></p> <p>にあるとおり、Plug.Staticの設定で公開できる。</p> <pre><code class="elixir"> plug Plug.Static, at: "/uploads", from: Path.expand("./uploads"), gzip: false </code></pre> <p><a href="https://crieit.now.sh/upload_images/92c96a11d1b8ae1106aa55e70234451e5b0d187e62b80.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/92c96a11d1b8ae1106aa55e70234451e5b0d187e62b80.png?mw=700" alt="" /></a></p> <p>(公開側はBootstrap Material Designだが、管理画面は面倒だし本当は作りたくなかったのでPhoenixデフォルトのBootstrap)</p> <p>全部これで登録はやってられないのでスクリプトで一括登録で作成予定。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14249 2017-10-26T06:27:36+09:00 2017-10-26T06:27:36+09:00 https://crieit.net/posts/Phoenix Phoenixで作った請求書作成システムをリリース <p>Elixir & Phonenixで作った請求書作成システムをリリースした。</p> <p>元々Misocaを使っていたが、弥生の傘下に入ってからフリープランでは5通までしか作成できなくなってしまった。</p> <p>別に良いかと思っていたが、ちっちゃい請求が続いたりするとどうも超えてしまうのではないかと怖くなることが多くなった。<br /> そのため、ちょうどElixir & Phoenixで遊んでいたので自分で作ったら良いんじゃないかと思い作成してみたところうまくいった。</p> <h3 id="請求書作成サービス"><a href="#%E8%AB%8B%E6%B1%82%E6%9B%B8%E4%BD%9C%E6%88%90%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9">請求書作成サービス</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://bill.alphabrend.com">Bill Builder</a></p> <p>基本的には自分で使う用なので質素でシンプル。<br /> 別に使いたい人がいたら使ってもらっていいと思う。<br /> まだステージングサーバーもなくDBバックアップもしてないけど、<br /> 自分で使っているのでそのうちする予定。</p> <p>安定するかは不明だけど、自分で使わないといけないから使える程度にはメンテナンス&拡張する予定。</p> <h3 id="本番環境"><a href="#%E6%9C%AC%E7%95%AA%E7%92%B0%E5%A2%83">本番環境</a></h3> <ul> <li>Elixir 1.5</li> <li>Phoenix 1.3</li> <li>MySQL 5.7</li> <li>Google Compute Engine f1-micro</li> <li>Systemctlによるサービス実行</li> <li>SendGrid</li> <li>Let's Encrypt</li> <li>bootstrap material design</li> </ul> <h3 id="開発環境"><a href="#%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83">開発環境</a></h3> <p>docker-composeに下記のようなサービスを入れている。</p> <ul> <li>Phoenix</li> <li>MySQL</li> <li>phpMyAdmin</li> <li>PDF作成</li> </ul> <h3 id="Elixir &amp; Phoenixで開発してみた感想"><a href="#Elixir+%26amp%3B+Phoenix%E3%81%A7%E9%96%8B%E7%99%BA%E3%81%97%E3%81%A6%E3%81%BF%E3%81%9F%E6%84%9F%E6%83%B3">Elixir & Phoenixで開発してみた感想</a></h3> <p>非常に良い。とにかくphoenix.gen.htmlでモデル、コントローラ、テンプレートまでざっと作成してくれるのが良い。<br /> それだけで参考になるしちょっといじれば動くのでとてもスムーズに色々進む。<br /> テンプレートがデフォルトでbootstrapなのもいい。<br /> phpだとCakePHP3も同様に最高だった。Laravelはコントローラのメソッドが空なのでやる気をなくす。</p> <p>Elixir自体が今まで使っていた言語とちょっと違うので癖があり躓いたりしたが、<br /> 慣れたら本当に気持ちよく迅速に開発できそう。</p> <h3 id="デプロイ"><a href="#%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4">デプロイ</a></h3> <p>色々Elixirのデプロイ方法もあるみたいだが、とりあえず今は手動。<br /> 頻繁なリリースもないし、今のところ誰も使ってないのでとめちゃいけないということもないし。</p> <p>とはいえ、手動でもデプロイは非常に簡単。</p> <ul> <li>gitでpull</li> <li>compile</li> <li>cssやjsいじってればそちらもビルド</li> <li>マイグレーション追加してたらmigrate</li> <li>準備出来たらsystemctlでrestart</li> </ul> <p>多分restartの瞬間くらいしかダウンタイムも無いと思うのでのんびりできる。<br /> Elixir自体が自分でサービス稼働させているからこその利点。</p> <h3 id="運用費"><a href="#%E9%81%8B%E7%94%A8%E8%B2%BB">運用費</a></h3> <p>とりあえずf1-microだし誰も使ってない限りは無料。<br /> (1サーバーにElixirもMySQLも全部入っている)<br /> だれか使い始めたら適当に広告でも張るがあまり意味なさそう。</p> <h3 id="今後の拡張"><a href="#%E4%BB%8A%E5%BE%8C%E3%81%AE%E6%8B%A1%E5%BC%B5">今後の拡張</a></h3> <p>毎月同じ金額の請求をメールで送るとかできたら良いな、と思ってたけど、<br /> 実は届いていなかった、とかがあると怖いので実装するかどうか検討中。<br /> SendGrid使っているためログは見やすいので問題ないのかもしれない。</p> <p>領収書は使わないけど、体裁ほとんど同じでいいなら作ろうかなとも思う。</p> <h3 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h3> <p>またなんかPhoenixで作りたい。</p> <p><a target="_blank" rel="nofollow noopener" href="https://bill.alphabrend.com">Bill Builder</a></p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14251 2017-10-11T06:42:22+09:00 2017-10-11T06:42:22+09:00 https://crieit.net/posts/Phoenix-Let-s-Encrypt-SSL PhoenixでLet's EncryptによるSSL <h3 id="前提"><a href="#%E5%89%8D%E6%8F%90">前提</a></h3> <p>PhoenixでLet's Encryptにより無料でSSL対応を行う。</p> <ul> <li>Elixir 1.5.2</li> <li>Phoenix 1.3.0</li> </ul> <h3 id="手順"><a href="#%E6%89%8B%E9%A0%86">手順</a></h3> <p>基本的には</p> <p><a target="_blank" rel="nofollow noopener" href="https://medium.com/@a4word/phoenix-app-secured-with-let-s-encrypt-469ac0995775">Phoenix/Elixir App Secured with Let’s Encrypt – Andrew Forward – Medium</a></p> <p>で書かれている通り。</p> <p>とりあえずサーバー起動。</p> <pre><code class="bash">MIX_ENV=prod mix phx.server </code></pre> <p>サーバーを起動したまま.well-knownフォルダを更新しなければならないのでそのための設定を行う。</p> <p>まず.well-knownフォルダ以下をそのままアクセスできるようにするための設定。</p> <p>lib/プロジェクト名/web/endpoint.ex<br /> にてPlug.Staticの設定に.well-knownを追加する。</p> <pre><code class="elixir">plug Plug.Static, at: "/", from: :yourproject, gzip: false, only: ~w(css fonts images js favicon.ico robots.txt .well-known) </code></pre> <p>これで<br /> _build/prod/lib/プロジェクト名/priv/static/.well-known<br /> 以下がそのまま公開され、<br /> <a target="_blank" rel="nofollow noopener" href="http://yourdomain.com/.well-known/****.html">http://yourdomain.com/.well-known/****.html</a><br /> のような感じでアクセスできるようになる。</p> <p>ちなみに、設定を変更したらアプリケーションを再起動する必要があると思う。<br /> また、再起動すると作っていた.well-knownは消えるので混乱しないよう注意。</p> <p>うまくいったらあとはcertbotで証明書を発行する。</p> <p>そして設定を更新。</p> <pre><code class="elixir">config :yourproject, Yourproject.Web.Endpoint, http: [port: 80], https: [port: 443, url: [host: "yourdomain.com", port: 443], keyfile: "/etc/letsencrypt/live/yourdomain.com/privkey.pem", cacertfile: "/etc/letsencrypt/live/yourdomain.com/chain.pem", certfile: "/etc/letsencrypt/live/yourdomain.com/cert.pem"], force_ssl: [hsts: true] </code></pre> <p>これで完了。更新などもそのまま普通にできるようになる。</p> <blockquote> <p>Now your site is being served up only through SSL directly through Phoenix (no Nginx required).</p> </blockquote> <p>と書かれているので別のWEBサーバーを使う冗長な方法もあったのだろうか。<br /> とはいえ_build以下を使う方法なので今後のバージョンによってはできなくなる可能性などもあるのかもしれない。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14252 2017-10-03T06:19:23+09:00 2017-10-03T06:19:23+09:00 https://crieit.net/posts/Phoenix-render Phoenixで他のフォルダの共通テンプレートをrenderする <p>自動生成されたedit.html.eexのformテンプレート読み込み部分を見ると、下記のようになっている。</p> <pre><code class="elixir"><%= render "form.html", changeset: @changeset, action: post_path(@conn, :update, @post) %> </code></pre> <p>ファイル名しか指定されていないので他の共通フォルダなどに入れている場合は指定が出来そうもない。</p> <p>共通フォルダなどのファイルを指定したい場合どうすればよいかというと、<br /> よくよく調べるとrender関数は第1引数にViewを指定することもできる。</p> <p>また、自動生成されたプロジェクトのViewを見てみると、LayoutView等、コントローラに連動していないView等もある。</p> <p>つまり、まず勝手に使用したいフォルダのViewを作成し、</p> <pre><code class="elixir">defmodule App.CommonView do use App.Web, :view end </code></pre> <p>それを使ってrenderを呼べばいい。これでtemplates/common/test.html.eexが表示される。</p> <pre><code class="elixir"><%= render App.CommonView, "test.html" %> </code></pre> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14253 2017-08-20T06:28:37+09:00 2018-08-30T15:48:46+09:00 https://crieit.net/posts/Phoenix-many-to-many Phoenixでmany_to_manyのフォームを対応 <p>Phoenixでmany_to_manyを設定してDBからデータを取得して表示するのは非常に簡単。<br /> ではformで新規登録したり更新したりする際に一緒にmany_to_manyのデータを更新するのはどのようにするのか一通り試してみた。</p> <h3 id="form"><a href="#form">form</a></h3> <p>例として、Postに複数のTagが紐付いているパターンで考える。<br /> PostsTagのモデルはなく、join_throughにて文字列でposts_tagsのテーブルを指定しているだけ。</p> <p>formではTagをカンマ区切りで設定できるという仕様。</p> <p>とりあえず入力欄として使用するためのtag_namesというvirtualフィールドを作っておく。</p> <pre><code class="elixir"> field :tag_names, :string, virtual: true </code></pre> <p>そしてフォームの入力欄を追加。</p> <pre><code class="elixir"><br /> <div class="form-group"> <%= label f, :tag_names, class: "control-label" %> <%= text_input f, :tag_names, class: "form-control" %> </div> </code></pre> <p>ここまではシンプルで難しいことはない。</p> <h3 id="既に存在するデータを入力欄のデフォルトとして表示"><a href="#%E6%97%A2%E3%81%AB%E5%AD%98%E5%9C%A8%E3%81%99%E3%82%8B%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E5%85%A5%E5%8A%9B%E6%AC%84%E3%81%AE%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E3%81%A8%E3%81%97%E3%81%A6%E8%A1%A8%E7%A4%BA">既に存在するデータを入力欄のデフォルトとして表示</a></h3> <p>既に登録されているデータを更新する際に、上記の入力欄に表示を行うための処理。<br /> とくに難しいことはなく、tag_namesにカンマ区切りの値を入れておくだけ。</p> <p>モデルに値をセットする関数を追加。</p> <pre><code class="elixir"> def prepare_form(changeset) do tag_names = Enum.map(get_field(changeset, :tags), fn(tag) -> tag.name end) |> Enum.join(",") put_change(changeset, :tag_names, tag_names) end </code></pre> <p>これをedit時に呼び出すだけ。</p> <pre><code class="elixir"> changeset = Post.changeset(post) |> Post.prepare_form </code></pre> <h3 id="新規登録、編集時にTagとtags_postsを登録する"><a href="#%E6%96%B0%E8%A6%8F%E7%99%BB%E9%8C%B2%E3%80%81%E7%B7%A8%E9%9B%86%E6%99%82%E3%81%ABTag%E3%81%A8tags_posts%E3%82%92%E7%99%BB%E9%8C%B2%E3%81%99%E3%82%8B">新規登録、編集時にTagとtags_postsを登録する</a></h3> <p>とりあえず、Tagを登録するためのRepoを作った。<br /> (Tagのモデル内に実装しても良いのかもしれないが、<br /> デフォルトでモデルにはRepoがaliasされていないことからモデル内ではRepoを使用しない方が良い想定なのかということも考慮し、<br /> 専用のRepoを作る形とした。<br /> 実際どういった形が望ましいのかは不明)</p> <p>文字列でtag_namesを渡すと保存したTagの配列を取得する関数がメイン。</p> <pre><code class="elixir">defmodule App.TagRepo do import App.Repo import Ecto.Changeset alias App.Tag def save_tags(tag_names) do tags = tag_names_to_tags(tag_names) |> Enum.map(fn(tag) -> case get_by(Tag, name: tag.name) do nil -> Tag.changeset(tag) |> insert! saved_tag -> saved_tag end end) end def tag_names_to_tags(tag_names) do String.split(tag_names, ",") |> Enum.map(fn(name) -> %Tag{name: name} end) end end </code></pre> <p>これをcreateとupdateで呼び出すだけ。</p> <p>新しく入力されたタグは新しいTagとして保存され、posts_tagsも登録される。<br /> タグが減った場合はposts_tags(のみ)も減る。</p> <pre><code class="elixir"> post = Repo.get!(Post, id) |> Repo.preload(:tags) tags = TagRepo.save_tags(post_params["tag_names"]) changeset = Post.changeset(post, post_params) |> Ecto.Changeset.put_assoc(:tags, tags) </code></pre> <p>updateの方のみ、上記のようにpreloadが必要となる。</p> <p>また、タグが減った場合エラーになるので、モデルに下記のon_replaceの追記も必要。</p> <pre><code class="elixir"> many_to_many :tags, App.Tag, join_through: "posts_tags", on_replace: :delete </code></pre> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14254 2017-08-05T06:26:08+09:00 2017-08-05T06:26:08+09:00 https://crieit.net/posts/Phoenix-admin Phoenixでadminルーティングの認証 <p>Phoenixでadminルーティングしてそこだけ認証を入れる。</p> <h3 id="仕様"><a href="#%E4%BB%95%E6%A7%98">仕様</a></h3> <ul> <li>/admin/articles<br /> のように最初にadminを含むURLは管理画面</li> <li>管理画面は管理者ユーザーで認証が必要</li> <li>管理画面もgen.htmlで自動生成。自分で頑張って作ったりしない</li> <li>コントローラやアクション毎に全部認証チェックを書いたりしない</li> </ul> <h3 id="モデルを作る"><a href="#%E3%83%A2%E3%83%87%E3%83%AB%E3%82%92%E4%BD%9C%E3%82%8B">モデルを作る</a></h3> <p>mix phoenix.gen.model<br /> でモデルだけ先に作る。もちろんgen.htmlでルーティングのない箇所のCRUDと一緒にモデルを作ってもいい。</p> <h3 id="管理者側ページを作る。"><a href="#%E7%AE%A1%E7%90%86%E8%80%85%E5%81%B4%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%92%E4%BD%9C%E3%82%8B%E3%80%82">管理者側ページを作る。</a></h3> <p>下記でadmin向けページを作成できる。</p> <pre><code class="sh">mix phoenix.gen.html Admin.Article articles --no-model title:string .... </code></pre> <p>これでコントローラやテンプレートも全部adminフォルダに分けて作ってくれる。<br /> モデルのaliasにAdminが含まれているのでそこだけ削除。</p> <h3 id="認証チェックを行う"><a href="#%E8%AA%8D%E8%A8%BC%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%82%92%E8%A1%8C%E3%81%86">認証チェックを行う</a></h3> <p>認証されていない場合にログイン画面へリダイレクトする。まず下記のようなPlugを作る。<br /> (下記のようにAuthモジュールを作るなり直接セッションで見るなりする)</p> <pre><code class="elixir">defmodule App.Plug.Prefix do import Plug.Conn alias App.Auth def init(default), do: default def call(conn, params) do [prefix] = params if !Auth.get_user(conn, prefix) do Phoenix.Controller.redirect(conn, to: "/admins/login") |> halt end conn end end </code></pre> <p>(調べたらどこもhaltしてたんだけどほんとにこれでいいんだろうか)</p> <h3 id="ルーティングの設定"><a href="#%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E3%81%AE%E8%A8%AD%E5%AE%9A">ルーティングの設定</a></h3> <p>ルーティングで先程のPlugを導入。pipelineで実装。ついでにレイアウトはadmin.html.eexを使用するようにもしておく。</p> <pre><code class="elixir"> pipeline :admin do plug :put_layout, {App.LayoutView, :admin} plug App.Plug.Prefix, [:admin] end </code></pre> <p>URLのルーティング。pipelineは複数設定できるらしい。</p> <pre><code class="elixir"> scope "/admin", App, as: :admin do pipe_through [:browser, :admin] get "/admins/logout", Admin.AdminController, :logout resources "/admins", Admin.AdminController end </code></pre> <h3 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h3> <p>これで管理画面側の処理を別に出来た。通常のユーザーのマイページなども同様にしてatomを変えるだけで実装できる。</p> だら@Crieit開発者