Elixir Melbourne
Open for anyone using or interested in Elixir (with a healthy side dose of Erlang). Welcome to Elixir coders of all levels: newbies, dabblers, everyday users, and pros.
Open for anyone using or interested in Elixir (with a healthy side dose of Erlang). Welcome to Elixir coders of all levels: newbies, dabblers, everyday users, and pros.
Data Structures in Elixir
Kevin Yu
Thursday, 19 May 2022
State of Data Science in Elixir
Christopher Grainger
Thursday, 14 April 2022
Build live Q&A using Phoenix LiveView
Jimmy Qiu
Thursday, 14 April 2022
We are Jimmy Qiu and William Kurniawan. We work at Fresh.xyz and We created platform to make it easy for public companies to increase shareholder engagement, know how much capital they can raise, and decreases the cost of raising capital. This is a post about how to build a live Q&A using phoenix LiveView.
Story started a couple of months ago when I reviewed all our companies’ service subscriptions. Then I saw Slido cost us $60 per month. The only scenario we are using this is for a weekly company wrap online Q&A. Meanwhile, I am starting to play around Phoenix LiveView and am instantly attracted by its performance and ability to create a live
experience. So, why not build a simple version of Slido?
I am using Tailwind + LiveView + Ecto. Full setup as below: https://pragmaticstudio.com/tutorials/adding-tailwind-css-to-phoenix
create table(:freshqa_topics) do
add :owner_id, references(:admins_users, on_delete: :nilify_all), null: false
timestamps()
end
create table(:freshqa_questions) do
add :content, :text, null: false
add :creator_id, references(:admins_users, on_delete: :delete_all), null: false
add :topic_id, references(:freshqa_topics, on_delet: :delete_all), null: false
add :resolved_at, :naive_datetime, null: false
timestamps()
end
create table(:freshqa_votes) do
add :creator_id, references(:admins_users, on_delete: :delete_all), null: false
add :question_id, references(:freshqa_questions, on_delete: :delete_all), null: false
timestamps()
end
create unique_index(:freshqa_votes, [:creator_id, :question_id])
We need a topics
table to store the owner user (for the purpose only owner can be able to resolve questions), a questions
table to store who asked what question for what topic and if it has been resolved, a votes
table to see who vote for which question.
So the first step we need to do is create a topic. Let’s add some html first.
def render(%{topic: nil} = assigns) do
~H"""
<div class="flex justify-center items-center">
<button class="btn btn-primary-black" phx-click="create-topic" type="button">
Let's Wrap!
</button>
</div>
"""
end
def render(%{topic: topic} = assigns) do
~H"""
<h1><%= topic.owner_id %></h1>
"""
end
phx-click allow DOM element(in here <button />) binding client-server interaction. For more information, check this
def handle_event("create-topic", _params, socket) do
case FreshQA.create_topic(%{owner_id: socket.assigns.current_admin_user.id}) do
{:ok, topic} ->
{:noreply, socket |> assign(:topic, topic)}
_ ->
{:noreply, socket}
end
end
When button has been clicked, a topic with current user id as owner will be created (user id is already stored in the session).
Ok, before it gets too complex, let’s see how it works. (For more information about life cycle, please check)
When button clicked, a socket message send to server.
["4", "9", "lv:phx-FvNJPHRFpdl5oQjF", "event",…]
0: "4"
1: "9"
2: "lv:phx-FvNJPHRFpdl5oQjF"
3: "event"
4: {type: "click", event: "create-topic", value: {value: ""}}
event: "create-topic"
type: "click"
value: {value: ""}
value: ""
The server create a record in database and send back a message to client.
["4", "9", "lv:phx-FvNJPHRFpdl5oQjF", "phx_reply", {response: {diff: {,…}}, status: "ok"}]
0: "4"
1: "9"
2: "lv:phx-FvNJPHRFpdl5oQjF"
3: "phx_reply"
4: {response: {diff: {,…}}, status: "ok"}
When I first saw this, I was pretty impressed. Different with the normal Restful approach, it is stateful, which means you can send much less information each call. Eg, in here, if you are using restful, you at least need to send request headers and maybe more information, but in here, you only need to send which button has been clicked.
The other cool thing is we don’t need to consider js anymore. Eg, if using React, when a button has been clicked, you need either reload the page(guess no one actually will do that) or using a state to change the layout from the client side. But by using liveview, the server will send diff
to the client, and browsers no longer need to take care of the business logic, only need to base on whatever is sent to the client to render the new layout. Pretty cool ha!
Live
There are many ways to make your web application live updated, including HTTP live streaming, Long Polling, WebSocket. LiveView’s Phoenix Pubsub system makes using WebSocket a really easy and delightful experience. Let’s see how it works.
So the goal here is really simple. When Topic is created, let other people using this to instantly see the created topic without refreshing the page.
First of all, you need to join a topic when page mount.
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(Hades.PubSub, inspect(FreshQA))
end
socket =
socket
|> assign(:menu, :wraps)
|> assign(:page_title, "It's a wrap!")
|> assign(:topic, FreshQA.get_todays_topic())
{:ok, socket}
end
def get_todays_topic() do
date =
Timex.now()
|> Timex.beginning_of_day()
|> Timex.to_naive_datetime()
|> NaiveDateTime.truncate(:second)
Topic
|> where([t], fragment("date_trunc('day', ?)", t.inserted_at) == ^date)
|> Repo.one()
end
As our wrap will never be twice for a single day, to keep it easy, using FreshQA.get_todays_topic function to get today’s topic. (Ideally should add some validation to make sure it is uniq in either schema level or database level).
Once the function mount, it will check if socket is being connected, if not, subscribe to FreshQA.
Next, let’s add one line after the topic successfully being created.
def handle_event("create-topic", _params, socket) do
case FreshQA.create_topic(%{owner_id: socket.assigns.current_admin_user.id}) do
{:ok, topic} ->
Phoenix.PubSub.broadcast(Hades.PubSub, inspect(FreshQA), {:topic_created, topic})
{:noreply, socket |> assign(:topic, topic) |> assign_questions()}
_ ->
{:noreply, socket}
end
end
To broadcast a message to FreshQA topic for every user subscribing to this channel. Then adding a [handle_info](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:handle_info/2)
function to assign the new topic to re-render the changing part of the page.
def handle_info({:topic_created, new_topic}, socket) do
{:noreply, assign(socket, :topic, new_topic)}
end
Ok, let’s see what is happening.
When button clicked, the user who clicked the button still send same message.
And meanwhile if you open another browser with different user, the page will automatically updated, as in socket you can see a message like this:
["4", null, "lv:phx-FvN_Cv6Fv7azb0iB", "diff", {,…}]
0: "4"
1: null
2: "lv:phx-FvN_Cv6Fv7azb0iB"
3: "diff"
4: {,…}
(An interesting factor is that the user who created the topic will not receive such a message although it is also subscribed to the same subscription. That’s because there is no diff
so nothing needs to be changed.)
defmodule HadesWeb.WrapsLive.Index do
@moduledoc false
use HadesWeb, :live_view
alias Gaia.Admins
alias Gaia.FreshQA
alias Gaia.FreshQA.Topic
alias Phoenix.LiveView.Socket
on_mount(HadesWeb.AdminUserLiveAuth)
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(Hades.PubSub, inspect(Admins))
Phoenix.PubSub.subscribe(Hades.PubSub, inspect(FreshQA))
end
socket =
socket
|> assign(:menu, :wraps)
|> assign(:page_title, "It's a wrap!")
|> assign(:topic, FreshQA.get_todays_topic())
|> assign_questions()
{:ok, socket}
end
@impl true
def handle_event("create-topic", _params, socket) do
case FreshQA.create_topic(%{owner_id: socket.assigns.current_admin_user.id}) do
{:ok, topic} ->
Phoenix.PubSub.broadcast(Hades.PubSub, inspect(FreshQA), {:topic_created, topic})
{:noreply, socket |> assign(:topic, topic) |> assign_questions()}
_ ->
{:noreply, socket}
end
end
@impl true
def handle_info({:user_updated, user}, socket)
when user.id == socket.assigns.current_admin_user.id do
{:noreply, socket |> assign(:current_admin_user, user) |> assign_questions()}
end
@impl true
def handle_info({:user_updated, _user}, socket) do
{:noreply, assign_questions(socket)}
end
@impl true
def handle_info({:topic_created, new_topic}, socket) do
{:noreply, assign(socket, :topic, new_topic)}
end
@impl true
def handle_info({:topic_updated, topic}, %Socket{assigns: %{topic: %Topic{}}} = socket)
when topic.id == socket.assigns.topic.id do
{:noreply, assign(socket, :topic, topic)}
end
@impl true
def handle_info(
{:question_created, new_question},
%Socket{assigns: %{topic: %Topic{}}} = socket
)
when new_question.topic_id == socket.assigns.topic.id do
{:noreply, assign_questions(socket)}
end
@impl true
def handle_info({:question_updated, question}, %Socket{assigns: %{topic: %Topic{}}} = socket)
when question.topic_id == socket.assigns.topic.id do
{:noreply, assign_questions(socket)}
end
@impl true
def handle_info({:vote_created, _user}, socket) do
{:noreply, assign_questions(socket)}
end
@impl true
def handle_info({:vote_deleted, _user}, socket) do
{:noreply, assign_questions(socket)}
end
@impl true
def handle_info(_msg, socket), do: {:noreply, socket}
defp assign_questions(%Socket{assigns: %{topic: %Topic{} = topic}} = socket) do
assign(socket, :questions, FreshQA.get_questions_by_topic_id(topic.id))
end
defp assign_questions(socket) do
assign(socket, :questions, [])
end
@impl true
def render(%{topic: nil} = assigns) do
~H"""
<div class="flex justify-center items-center">
<button class="btn btn-primary-black" phx-click="create-topic" type="button">
Let's Wrap!
</button>
</div>
"""
end
@impl true
def render(assigns) do
~H"""
<div class="mx-auto max-w-lg space-y-6">
<%= if @current_admin_user.id == @topic.owner_id do %>
<div>
<div class="bg-white p-6 space-y-6">
<p class="typography-subtitle-1">You are the owner of this session.</p>
<button
class="btn btn-primary-black"
phx-click={HadesWeb.LiveComponents.Wraps.TransferOwnershipModal.open_modal()}
type="button"
>
Transfer ownership
</button>
</div>
<.live_component
current_admin_user={@current_admin_user}
id="transfer-ownership-modal"
module={HadesWeb.LiveComponents.Wraps.TransferOwnershipModal}
topic={@topic}
/>
</div>
<% end %>
<.live_component
current_admin_user={@current_admin_user}
id="ask-question-form"
module={HadesWeb.LiveComponents.Wraps.AskQuestionForm}
topic_id={@topic.id}
/>
<ul class="space-y-6">
<%= for question <- @questions do %>
<.live_component
can_resolve={@current_admin_user.id == @topic.owner_id}
current_admin_user_id={@current_admin_user.id}
id={"question-#{question.id}"}
module={HadesWeb.LiveComponents.Wraps.QuestionCard}
question={question}
/>
<% end %>
</ul>
<%= if @current_admin_user.username == nil do %>
<.live_component
current_admin_user={@current_admin_user}
id="set-username-modal"
module={HadesWeb.LiveComponents.Wraps.SetUsernameModal}
/>
<% end %>
</div>
"""
end
end
Components:
defmodule HadesWeb.LiveComponents.Wraps.TransferOwnershipModal do
@moduledoc false
use HadesWeb, :live_component
alias Gaia.Admins
alias Gaia.FreshQA
alias Gaia.FreshQA.Topic
alias Phoenix.LiveView.JS
@impl true
def update(assigns, socket) do
socket =
socket
|> assign(assigns)
|> assign(:changeset, FreshQA.change_topic(%Topic{}))
|> assign(:users, Admins.get_all_admins_users())
{:ok, socket}
end
@impl true
def handle_event("submit", %{"topic" => topic}, socket) do
case FreshQA.update_topic(socket.assigns.topic, topic) do
{:ok, topic} ->
Phoenix.PubSub.broadcast(
Hades.PubSub,
inspect(FreshQA),
{:topic_updated, topic}
)
{:noreply, socket}
{:error, changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
@impl true
def handle_event("validate", %{"topic" => topic}, socket) do
changeset =
%Topic{}
|> FreshQA.change_topic(topic)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :changeset, changeset)}
end
@impl true
def render(assigns) do
~H"""
<div
aria-labelledby="modal-title"
aria-modal="true"
class="relative z-10"
id="transfer-ownership-modal"
phx-remove={hide_modal()}
role="dialog"
style="display: none;"
>
<div
class="fixed inset-0 bg-primary-off-black/50 backdrop-blur"
id="transfer-ownership-modal-overlay"
>
</div>
<div class="overflow-y-auto fixed inset-0 z-10">
<div class="flex justify-center items-center p-6 min-h-full">
<div
class="overflow-hidden relative p-6 w-full max-w-lg bg-white rounded-2xl"
id="transfer-ownership-modal-content"
phx-key="escape"
phx-window-keydown={JS.dispatch("click", to: "#transfer-ownership-modal-cancel-button")}
>
<.form
for={@changeset}
let={f}
phx-change="validate"
phx-submit="submit"
phx-target={@myself}
>
<div class="space-y-6">
<h4 class="typography-heading-4">Transfer ownership</h4>
<div>
<div>
<%= select(
f,
:owner_id,
@users
|> Enum.filter(&(&1.id != @current_admin_user.id))
|> Enum.map(&{&1.email, &1.id}),
class: "input"
) %>
</div>
<%= error_tag(f, :owner_id) %>
</div>
<div class="flex flex-col gap-4 sm:flex-row-reverse sm:items-center">
<%= submit("Save", class: "btn btn-primary-black") %>
<button
id="transfer-ownership-modal-cancel-button"
class="btn btn-secondary"
phx-click={hide_modal()}
type="button"
>
Cancel
</button>
</div>
</div>
</.form>
</div>
</div>
</div>
</div>
"""
end
defp hide_modal(js \\ %JS{}) do
js
|> JS.hide(to: "#transfer-ownership-modal", transition: "fade-out")
|> JS.hide(to: "#transfer-ownership-modal-content", transition: "fade-out-scale")
|> JS.hide(to: "#transfer-ownership-modal-overlay", transition: "fade-out")
end
def open_modal(js \\ %JS{}) do
js
|> JS.show(to: "#transfer-ownership-modal", transition: "fade-in")
|> JS.show(to: "#transfer-ownership-modal-content", transition: "fade-in-scale")
|> JS.show(to: "#transfer-ownership-modal-overlay", transition: "fade-in")
end
end
defmodule HadesWeb.LiveComponents.Wraps.AskQuestionForm do
@moduledoc false
use HadesWeb, :live_component
alias Gaia.FreshQA
alias Gaia.FreshQA.Question
@impl true
def update(assigns, socket) do
socket =
socket
|> assign(assigns)
|> assign(:changeset, FreshQA.change_question(%Question{}))
{:ok, socket}
end
@impl true
def handle_event("submit", %{"question" => question}, socket) do
question =
question
|> Map.put("creator_id", socket.assigns.current_admin_user.id)
|> Map.put("topic_id", socket.assigns.topic_id)
case FreshQA.create_question(question) do
{:ok, new_question} ->
Phoenix.PubSub.broadcast(
Hades.PubSub,
inspect(FreshQA),
{:question_created, new_question}
)
{:noreply, assign(socket, :changeset, FreshQA.change_question(%Question{}))}
_ ->
{:noreply, socket}
end
end
@impl true
def handle_event("validate", %{"question" => question}, socket) do
changeset =
%Question{}
|> FreshQA.change_question(question)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :changeset, changeset)}
end
@impl true
def render(assigns) do
~H"""
<div class="bg-white p-6">
<.form let={f} for={@changeset} phx-change="validate" phx-submit="submit" phx-target={@myself}>
<div class="space-y-6">
<div class="space-y-2">
<h2 class="typography-heading-4">Do you have any question?</h2>
<p class="text-text-grey typography-body-regular">Don't worry, it is anonymous!</p>
</div>
<%= textarea(f, :content,
class: "input",
phx_debounce: "blur",
placeholder: "What is today's gif?",
rows: 5
) %>
<div class="flex justify-end">
<%= submit("Submit", class: "btn btn-primary-black") %>
</div>
</div>
</.form>
</div>
"""
end
end
Question Card:
defmodule HadesWeb.LiveComponents.Wraps.QuestionCard do
@moduledoc false
use HadesWeb, :live_component
alias Gaia.FreshQA
@impl true
def update(assigns, socket), do: {:ok, assign(socket, assigns)}
@impl true
def handle_event("resolve-question", _params, socket) do
case FreshQA.update_question(socket.assigns.question, %{
resolved_at: NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
}) do
{:ok, question} ->
Phoenix.PubSub.broadcast(Hades.PubSub, inspect(FreshQA), {:question_updated, question})
{:noreply, socket}
_ ->
{:noreply, socket}
end
end
@impl true
def handle_event("toggle-vote", _params, socket) do
if v = my_vote(socket.assigns) do
v
|> FreshQA.delete_vote()
|> broadcast(:vote_deleted)
else
%{
creator_id: socket.assigns.current_admin_user_id,
question_id: socket.assigns.question.id
}
|> FreshQA.create_vote()
|> broadcast(:vote_created)
end
{:noreply, socket}
end
defp broadcast({:ok, result}, event) do
Phoenix.PubSub.broadcast(Hades.PubSub, inspect(FreshQA), {event, result})
end
defp broadcast(error, _event), do: error
defp my_vote(assigns) do
Enum.find(assigns.question.votes, &(&1.creator_id == assigns.current_admin_user_id))
end
@impl true
def render(assigns) do
~H"""
<li class="bg-white p-6 space-y-4">
<div class="flex justify-between gap-4">
<p class="typography-subtitle-1">
<%= @question.creator.username || "Anonymous" %>
</p>
<%= if @question.resolved_at do %>
<div class="inline-flex items-center py-0.5 px-3 rounded-full bg-status-green text-white typography-badge">
Answered
</div>
<% end %>
</div>
<p class="typography-body-regular">
<%= @question.content %>
</p>
<div class="flex items-center gap-4">
<button
class="flex items-center gap-2"
phx-click="toggle-vote"
phx-target={@myself}
type="button"
>
<%= if my_vote(assigns) do %>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M2 10.5a1.5 1.5 0 113 0v6a1.5 1.5 0 01-3 0v-6zM6 10.333v5.43a2 2 0 001.106 1.79l.05.025A4 4 0 008.943 18h5.416a2 2 0 001.962-1.608l1.2-6A2 2 0 0015.56 8H12V4a2 2 0 00-2-2 1 1 0 00-1 1v.667a4 4 0 01-.8 2.4L6.8 7.933a4 4 0 00-.8 2.4z" />
</svg>
<% else %>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5"
/>
</svg>
<% end %>
<span class="typography-button">
<%= length(@question.votes) %>
</span>
</button>
<%= if @can_resolve and @question.resolved_at == nil do %>
<button
class="flex items-center gap-2"
phx-click="resolve-question"
phx-target={@myself}
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
<span class="typography-button">
Resolve
</span>
</button>
<% end %>
</div>
</li>
"""
end
end
Set user name modal:
defmodule HadesWeb.LiveComponents.Wraps.SetUsernameModal do
@moduledoc false
use HadesWeb, :live_component
alias Gaia.Admins
alias Gaia.Admins.User
@impl true
def update(assigns, socket) do
socket =
socket
|> assign(assigns)
|> assign(:changeset, User.username_changeset(%User{}))
{:ok, socket}
end
@impl true
def handle_event("submit", %{"user" => %{"username" => username}}, socket) do
case Admins.update_user_username(socket.assigns.current_admin_user, username) do
{:ok, user} ->
Phoenix.PubSub.broadcast(
Hades.PubSub,
inspect(Admins),
{:user_updated, user}
)
{:noreply, socket}
{:error, changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
@impl true
def handle_event("validate", %{"user" => user}, socket) do
changeset =
%User{}
|> User.username_changeset(user)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :changeset, changeset)}
end
@impl true
def render(assigns) do
~H"""
<div aria-labelledby="modal-title" aria-modal="true" class="relative z-10" role="dialog">
<div class="fixed inset-0 bg-primary-off-black/50 backdrop-blur"></div>
<div class="overflow-y-auto fixed inset-0 z-10">
<div class="flex justify-center items-center p-6 min-h-full">
<div class="overflow-hidden relative p-6 w-full max-w-lg bg-white rounded-2xl">
<.form
for={@changeset}
let={f}
phx-change="validate"
phx-submit="submit"
phx-target={@myself}
>
<div class="space-y-6">
<h4 class="typography-heading-4">Set username</h4>
<div>
<div>
<%= text_input(f, :username, phx_debounce: "blur", class: "input") %>
</div>
<%= error_tag(f, :username) %>
</div>
<%= submit("Save", class: "btn btn-primary-black") %>
</div>
</.form>
</div>
</div>
</div>
</div>
"""
end
end
Database related function:
defmodule Gaia.FreshQA do
@moduledoc """
The FreshQA context.
"""
import Ecto.Query, warn: false
alias Gaia.FreshQA.{Question, Topic, Vote}
alias Gaia.Repo
@doc """
Returns the list of freshqa_topics.
## Examples
iex> list_freshqa_topics()
[%Topic{}, ...]
"""
def list_freshqa_topics do
Repo.all(Topic)
end
@doc """
Gets a single topic.
Raises `Ecto.NoResultsError` if the Topic does not exist.
## Examples
iex> get_topic!(123)
%Topic{}
iex> get_topic!(456)
** (Ecto.NoResultsError)
"""
def get_topic!(id), do: Repo.get!(Topic, id)
@doc """
Creates a topic.
## Examples
iex> create_topic(%{field: value})
{:ok, %Topic{}}
iex> create_topic(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_topic(attrs \\ %{}) do
%Topic{}
|> Topic.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a topic.
## Examples
iex> update_topic(topic, %{field: new_value})
{:ok, %Topic{}}
iex> update_topic(topic, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_topic(%Topic{} = topic, attrs) do
topic
|> Topic.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a topic.
## Examples
iex> delete_topic(topic)
{:ok, %Topic{}}
iex> delete_topic(topic)
{:error, %Ecto.Changeset{}}
"""
def delete_topic(%Topic{} = topic) do
Repo.delete(topic)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking topic changes.
## Examples
iex> change_topic(topic)
%Ecto.Changeset{data: %Topic{}}
"""
def change_topic(%Topic{} = topic, attrs \\ %{}) do
Topic.changeset(topic, attrs)
end
def get_todays_topic() do
date =
Timex.now()
|> Timex.beginning_of_day()
|> Timex.to_naive_datetime()
|> NaiveDateTime.truncate(:second)
Topic
|> where([t], fragment("date_trunc('day', ?)", t.inserted_at) == ^date)
|> Repo.one()
end
@doc """
Returns the list of freshqa_questions.
## Examples
iex> list_freshqa_questions()
[%Question{}, ...]
"""
def list_freshqa_questions do
Repo.all(Question)
end
@doc """
Gets a single question.
Raises `Ecto.NoResultsError` if the Question does not exist.
## Examples
iex> get_question!(123)
%Question{}
iex> get_question!(456)
** (Ecto.NoResultsError)
"""
def get_question!(id), do: Repo.get!(Question, id)
@doc """
Creates a question.
## Examples
iex> create_question(%{field: value})
{:ok, %Question{}}
iex> create_question(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_question(attrs \\ %{}) do
%Question{}
|> Question.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a question.
## Examples
iex> update_question(question, %{field: new_value})
{:ok, %Question{}}
iex> update_question(question, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_question(%Question{} = question, attrs) do
question
|> Question.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a question.
## Examples
iex> delete_question(question)
{:ok, %Question{}}
iex> delete_question(question)
{:error, %Ecto.Changeset{}}
"""
def delete_question(%Question{} = question) do
Repo.delete(question)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking question changes.
## Examples
iex> change_question(question)
%Ecto.Changeset{data: %Question{}}
"""
def change_question(%Question{} = question, attrs \\ %{}) do
Question.changeset(question, attrs)
end
def get_questions_by_topic_id(topic_id) do
Question
|> join(:left, [q], u in assoc(q, :creator))
|> join(:left, [q], v in assoc(q, :votes))
|> where([q], q.topic_id == ^topic_id)
|> group_by([q, u, v], [q.id, u.id, v.id])
|> order_by([q, u, v], desc_nulls_first: q.resolved_at, desc: count(v.id))
|> preload([_, u, v], creator: u, votes: v)
|> Repo.all()
end
@doc """
Returns the list of freshqa_votes.
## Examples
iex> list_freshqa_votes()
[%Vote{}, ...]
"""
def list_freshqa_votes do
Repo.all(Vote)
end
@doc """
Gets a single vote.
Raises `Ecto.NoResultsError` if the Vote does not exist.
## Examples
iex> get_vote!(123)
%Vote{}
iex> get_vote!(456)
** (Ecto.NoResultsError)
"""
def get_vote!(id), do: Repo.get!(Vote, id)
@doc """
Creates a vote.
## Examples
iex> create_vote(%{field: value})
{:ok, %Vote{}}
iex> create_vote(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_vote(attrs \\ %{}) do
%Vote{}
|> Vote.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a vote.
## Examples
iex> update_vote(vote, %{field: new_value})
{:ok, %Vote{}}
iex> update_vote(vote, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_vote(%Vote{} = vote, attrs) do
vote
|> Vote.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a vote.
## Examples
iex> delete_vote(vote)
{:ok, %Vote{}}
iex> delete_vote(vote)
{:error, %Ecto.Changeset{}}
"""
def delete_vote(%Vote{} = vote) do
Repo.delete(vote)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking vote changes.
## Examples
iex> change_vote(vote)
%Ecto.Changeset{data: %Vote{}}
"""
def change_vote(%Vote{} = vote, attrs \\ %{}) do
Vote.changeset(vote, attrs)
end
end
Another functionality I want to be able to achieve is to present who else join the room. Before you need to add a customise pubsub function to store the who join the room by yourself. But now with newly release Phoenix.Presence, everything is much easier.
First when socket connected, you need to track the user information into topic.
if connected?(socket) do
Phoenix.PubSub.subscribe(Hades.PubSub, inspect(Admins))
Phoenix.PubSub.subscribe(Hades.PubSub, inspect(FreshQA))
Presence.track(
self(),
inspect(FreshQA),
socket.assigns.current_admin_user.username,
%{}
)
end
Then when someone join the room, a message will be sent to everyone subscribe to Presence calling handle_info
function.
@impl true
def handle_info(
%{event: "presence_diff"},
socket
) do
{:noreply,
socket
|> assign(
:participants,
FreshQA
|> inspect
|> Presence.list()
|> Map.keys()
|> Enum.filter(& &1)
)}
end
To do something like this, when a user join the room, all people in the room will get a socket message to re-render the part which display all participants in this room.
This article is just a taste of how easy and powerful LiveView brings developers to create a live updated web application. As we build directly to our admin portal, we can not share the whole source code. But if you need some help to reproduce it, or if you want to learn more how to use LiveView, feel free to email us on devs@fresh.xyz or DM me through linkedin.