一人もくもく会 α verでサービス開始しました。

Phoenixでmany_to_manyのフォームを対応

Phoenixでmany_to_manyを設定してDBからデータを取得して表示するのは非常に簡単。 ではformで新規登録したり更新したりする際に一緒にmany_to_manyのデータを更新するのはどのようにするのか一通り試してみた。

form

例として、Postに複数のTagが紐付いているパターンで考える。 PostsTagのモデルはなく、join_throughにて文字列でposts_tagsのテーブルを指定しているだけ。

formではTagをカンマ区切りで設定できるという仕様。

とりあえず入力欄として使用するためのtag_namesというvirtualフィールドを作っておく。

    field :tag_names, :string, virtual: true

そしてフォームの入力欄を追加。

  <div class="form-group">
    <%= label f, :tag_names, class: "control-label" %>
    <%= text_input f, :tag_names, class: "form-control" %>
  </div>

ここまではシンプルで難しいことはない。

既に存在するデータを入力欄のデフォルトとして表示

既に登録されているデータを更新する際に、上記の入力欄に表示を行うための処理。 とくに難しいことはなく、tag_namesにカンマ区切りの値を入れておくだけ。

モデルに値をセットする関数を追加。

  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

これをedit時に呼び出すだけ。

    changeset = Post.changeset(post)
    |> Post.prepare_form

新規登録、編集時にTagとtags_postsを登録する

とりあえず、Tagを登録するためのRepoを作った。 (Tagのモデル内に実装しても良いのかもしれないが、 デフォルトでモデルにはRepoがaliasされていないことからモデル内ではRepoを使用しない方が良い想定なのかということも考慮し、 専用のRepoを作る形とした。 実際どういった形が望ましいのかは不明)

文字列でtag_namesを渡すと保存したTagの配列を取得する関数がメイン。

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

これをcreateとupdateで呼び出すだけ。

新しく入力されたタグは新しいTagとして保存され、posts_tagsも登録される。 タグが減った場合はposts_tags(のみ)も減る。

    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)

updateの方のみ、上記のようにpreloadが必要となる。

また、タグが減った場合エラーになるので、モデルに下記のon_replaceの追記も必要。

    many_to_many :tags, App.Tag, join_through: "posts_tags", on_replace: :delete

Phoenixでadminルーティングの認証

Phoenixでadminルーティングしてそこだけ認証を入れる。

仕様

  • /admin/articles のように最初にadminを含むURLは管理画面
  • 管理画面は管理者ユーザーで認証が必要
  • 管理画面もgen.htmlで自動生成。自分で頑張って作ったりしない
  • コントローラやアクション毎に全部認証チェックを書いたりしない

モデルを作る

mix phoenix.gen.model でモデルだけ先に作る。もちろんgen.htmlでルーティングのない箇所のCRUDと一緒にモデルを作ってもいい。

管理者側ページを作る。

下記でadmin向けページを作成できる。

mix phoenix.gen.html Admin.Article articles --no-model title:string ....

これでコントローラやテンプレートも全部adminフォルダに分けて作ってくれる。 モデルのaliasにAdminが含まれているのでそこだけ削除。

認証チェックを行う

認証されていない場合にログイン画面へリダイレクトする。まず下記のようなPlugを作る。 (下記のようにAuthモジュールを作るなり直接セッションで見るなりする)

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

(調べたらどこもhaltしてたんだけどほんとにこれでいいんだろうか)

ルーティングの設定

ルーティングで先程のPlugを導入。pipelineで実装。ついでにレイアウトはadmin.html.eexを使用するようにもしておく。

  pipeline :admin do
    plug :put_layout, {App.LayoutView, :admin}
    plug App.Plug.Prefix, [:admin]
  end

URLのルーティング。pipelineは複数設定できるらしい。

  scope "/admin", App, as: :admin do
    pipe_through [:browser, :admin]

    get "/admins/logout", Admin.AdminController, :logout
    resources "/admins", Admin.AdminController
  end

まとめ

これで管理画面側の処理を別に出来た。通常のユーザーのマイページなども同様にしてatomを変えるだけで実装できる。

Phoenixのex_adminでueberauthの認証

Phoenixでex_adminをueberauthを使って行う。 基本的には

Elixir/Phoenixにおけるueberauth(認証)とex_admin(管理画面)の連携方法 - Qiita

の解説通りでいいのだが、一部情報が古いので現在の対応方法を記載。

current_user_nameはログインしていないとエラーになるので下記のように修正。

  def current_user_name(conn) do
    user = current_user(conn)
    if user do
      user.name
    else
      ''
    end
  end

あと現在はconfigのauthorizeはなくなった。 ExAdmin.Authenticationの書き方と同様にできるようになったので下記をconn.exに追記。 コントローラによって処理を分けたいならAnyの部分を変えれば良い。

defimpl ExAdmin.Authorization, for: Any do
  def authorize_query(_, _, query, _, _), do: query
  def authorize_action(resource, conn, action) do
    App.UserAuth.check_logged_in(conn, action, resource)
  end
end

あとuser_auth.exのcheck_logged_inだが、identityが必要ならつける。

  def check_logged_in(conn, _action, _resource_model) do
    login_url = auth_path(conn, :request, "identity")
    if conn.request_path != login_url && !logged_in?(conn) do
      conn |> redirect(to: login_url)
    end
    true
  end

Phoenixのex_adminを使ってみてうまく動かない時

Phoenixでためしにex_adminを使って管理画面を作ってみようと思ったのだがどうもうまく動かない。 その時に試した時の対策。

コンパイルエラー

ex_adminはまだバージョン1にも行っていないし、Phoenix側に追従しきれていない場合があるようなので丁度ビルド時にエラーが出た。 deps/ex_admin/web/web.exに

import Ecto.Model

となっているところがあるので下記に修正。

import Ecto.Schema

既にこの修正のPRはmergeされているようなので、ex_adminのバージョンが上がった時に反映されると思うので気にせず直接depsのファイルをいじって問題ないと思う。

cssが反映されない

どのサイトを見てもアクセスすれば正しく画面が表示される、とあり、githubの情報にも特に何もないのだが、自分が試した時はcssが404になっていてデザインがなかった。

この辺はよくわからないがadmin.installした時に追加されてたweb/static/vendorの中のファイルをpriv/staticのcss, jsの中にコピーしたら表示された。 何も書かれていなくてもコピーするのが当然なのか、staticの機能で勝手に本来は反映されるべきなのかは不明。

cocos2d-xのfbx-convをlinuxで実行

cocos2d-xはlinuxにもインストールできる。 しかし、一緒にインストールされる3Dモデルのコンバートツールであるfbx-convはWindows用とmac用のものしか付属していない。

wineで動くと良いかと思ったがうまく動かないのでlinuxでビルドを試してみた。OSはLinux Mint18

基本的には下記のとおりだが、順番や不足があるので補足してみる。

Fbx-conv tool source code - Extensions - Cocos2d-x Forums

FBX SDKをインストー

前提としてFBX SDKが必要なので、下記でダウンロード。

Autodesk - Autodesk FBX - FBX Software Development Kit

付属のテキストに書かれているように、インストールするパスを指定して実行するだけ。

./fbx20160_fbxsdk_linux /usr/local/lib/fbx-conv

fbx-convをビルド

下記をclone

github.com

export LD_LIBRARY_PATH=/usr/local/lib/fbx-conv/lib/gcc4/x64/release/libfbxsdk.so
export FBX_SDK_ROOT=/usr/local/lib/fbx-conv/lib/gcc4/x64/release

src/modeldata/Node.hに下記を追加

#include <algorithm>

src/modeldata/FileIO.cppに下記を追加

#include <string.h>

あと実際の実行時にもライブラリへのパスが通っていないといけないので、 /etc/ld.so.conf.d/fbx-conv.confを作成して

/usr/local/lib/fbx-conv/lib/gcc4/x64/release

を書いておく。 このconfファイルを作成したら

ldconfig

で設定を反映させる。

Makefileを作成するため、

./generate_makefile

するとbuild/gmake/fbx-conv.makeが作成されるので、この中の最初のCFLAGSを下記に書き換える。

CFLAGS    += $(CPPFLAGS) $(ARCH) -g -w -std=c++11

あとはbuild/gmakeフォルダでmake。するとcloneしたフォルダの直下にfbx-convが作成される。 ここに実行パスを通しておけば良い。

Linux Mint 18でUnreal Engine

Linux Mint 18にUnreal Engineをインストールして起動するところまでやってみた。

基本的には公式の情報に沿ってビルドだが色々わかりにくいところもあるので補足しつつ。

Building On Linux - Epic Wiki

ソースの取得

wikiをみると、まずgithubからソースをcloneするところから始まる。 しかし、githubを見ても該当のリポジトリが無い。

これは無いのではなく、github上にてEpic Gamesのメンバーにならなければアクセスする権限がない。 下記のようにしてメンバーになる必要がある。

  • Githubのアカウントがない人はまずGithubに登録。
  • Unreal Engineのアカウントがない人はまず登録。(ダウンロードページから登録できる)
  • Unreal Engineのサイトでログインし、自分の情報の編集画面に行くとGithubのIDを設定するところがあるのでそこに設定。 これでEpic GamesのアカウントとGithubのアカウントが関連付けられる。 これで確かメールが送られてくるのでその中のボタンを押せばメンバーになりリポジトリが閲覧できた気がする。

あとはとりあえずcloneしておく。だいぶ時間がかかる。

ビルド前の事前準備

clone後、まずは説明ページの下の方にディストリビューション毎の事前準備の説明があるのでそちらを確認。 たとえば「Setting up on Linux Mint」のような項目がある。

Linux Mintの場合、色々とaptでインストールとシンボリックリンクの作成。 Setup.shだかGenerateProjectFiles.shを行うと勝手に色々インストールしようとするので多分省略しても良いと思う。 自分の場合clang-3.9をインストールしようとして失敗していた。 多分ディストリビューションのバージョンを見て入れるバージョンを変えているのかもしれない。

自分の場合は

LLVM Debian/Ubuntu nightly packages

のYakketyで設定してインストールした。 /etc/apt/sources.listに追記し、apt-get updateしてインストール。

ビルド

あとは書かれているとおりにビルドまで進める。ビルドはすごく時間がかかるし多少容量も使うので注意。

起動

説明通りに起動すると、エラーで停止。 エラーを見るとスプラッシュの画像が読み込めないと出ているので、 実行ファイルのところにcdしてから起動したらそのエラーは消えた。

あとはどうも Could not create OpenGL 4.3 context のようなエラーが出ているので調べてみたら、OpenGL3指定で起動すればいいということが書かれていたので

./UE4Editor -opengl3

としてみたところめでたく起動した。

サンプルがあったのでそれで新規プロジェクトを作成し、エディタ上でプレイ出来たところ。 f:id:dala:20170510220127p:plain

しかし重い…初回だからだろうか。

あとVisual Studioは無いのでちゃんとC++が動くのか不明。

cocos2d-jsでInvalid Native Object

cocos2d-jsにて、androidでrunしてみると、真っ黒で何も表示されない。 FPS表示は動いているのでうごいてはいるらしい。

android studioのログを見てみると下記のようなログが出力されていた。(抜粋)

js_cocos2dx_Node_isRunning : Invalid Native Object
(evaluatedOK == JS_FALSE)
JS: assets/script/jsb_prepare.js:254:Error: js_cocos2dx_Node_isRunning : Invalid Native Object
Evaluating main.js failed (evaluatedOK == JS_FALSE)

main.jsの評価に失敗しているということなので、 もしかすると何かしらwebでしか動かない構文があるのではないかと細かく調べてみたが、見つからない。

次にInvalid Native Objectを調べてみると、どうも実行時だけにでるエラーとのこと。 androidでのJavaScriptの実行時だけ何かエラーが出ているらしい。

最終的に、sceneに定義しているrunningメソッドをgoingという名前に変更したら動いた。 どうもSpriteクラスのメソッドを上書きしていておかしくなっていたらしい。

webだと単にJavaScript上でメソッドを上書きするだけだが、 実機だとc言語側でエラーが出るのでエラー時の挙動が変わってしまうようだ。