tag:crieit.net,2005:https://crieit.net/magazines/dala00/%E3%82%B5%E3%82%A4%E3%83%9C%E3%82%A6%E3%82%BALive%E3%82%92%E4%BD%9C%E3%82%8B/feed [連載] サイボウズLiveを作るの投稿 - Crieit Crieitで連載「[連載] サイボウズLiveを作る」の最近の投稿 2018-10-31T19:05:36+09:00 https://crieit.net/magazines/dala00/%E3%82%B5%E3%82%A4%E3%83%9C%E3%82%A6%E3%82%BALive%E3%82%92%E4%BD%9C%E3%82%8B/feed 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/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/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開発者