At present, anyone involved in a thread gets an email. That's bound to become annoying. Let's add the concept of "watching" a Thread, so that a user can specify whether or not they wish to receive notifications for new posts.
Project
We're starting with the dailydrip/firestorm repo tagged before this episode .
We'll start by writing a test:
vim test/forums_test.exs
defmodule FirestormWeb.ForumsTest do
# ...
describe "watching a thread" do
setup [:create_user, :create_category, :create_thread]
test "watching a thread", %{thread: thread, user: user} do
{:ok, watch} = user |> Forums.watch(thread)
assert thread |> Forums.watched_by?(user)
end
end
# ...
end
This test will of course fail. Let's begin by adding a schema for our
watches
.
These will be polymorphic, and each thing that can be watched will have its own
table to store the corresponding
Watch
es:
vim lib/firestorm_web/forums/watch.ex
defmodule FirestormWeb.Forums.Watch do
@moduledoc """
A `Watch` is a polymorphic representation that a user is watching a thing in our system.
"""
use Ecto.Schema
import Ecto.Changeset
schema "abstract table: watches" do
# This will be used by associations on each "concrete" table
field :assoc_id, :integer
field :user_id, :integer
timestamps()
end
end
We'll add an association to the
Thread
schema to represent these watches:
vim lib/firestorm_web/forums/thread.ex
defmodule FirestormWeb.Forums.Thread do
# ...
alias FirestormWeb.Forums.{Category, Post, Watch, User}
# ...
schema "forums_threads" do
# ...
# We're specifying the table to find associated watches through, rather than
# just providing another schema.
has_many :watches, {"forums_threads_watches", Watch}, foreign_key: :assoc_id
# We'll also use `many_to_many` to find all the users watching this thread
# through the same association.
many_to_many :watchers, User, join_through: "forums_threads_watches", join_keys: [assoc_id: :id, user_id: :id]
# ...
end
end
Next, we'll open up the
Forums
context and add the functions we identified in
our test, closing the loop:
vim lib/firestorm_web/forums/forums.ex
defmodule FirestormWeb.Forums do
# ...
alias FirestormWeb.Forums.{User, Category, Thread, Post, Watch}
# ...
@doc """
Have a user watch a thread:
iex> %User{} |> watch(%Thread{})
{:ok, %Watch{}}
"""
def watch(%User{} = user, %Thread{} = thread) do
thread
|> Ecto.build_assoc(:watches, %{user_id: user.id})
|> watch_changeset(%{})
|> Repo.insert()
end
defp watch_changeset(%Watch{} = watch, attrs) do
watch
|> cast(attrs, [:assoc_id, :user_id])
|> validate_required([:assoc_id, :user_id])
end
end
If we run the test, we'll find that we don't have the table to handle this polymorphic association on threads yet. Let's add it:
mix ecto.gen.migration add_forums_threads_watches
defmodule FirestormWeb.Repo.Migrations.AddForumsThreadsWatches do
use Ecto.Migration
def change do
create table(:forums_threads_watches) do
add :assoc_id, references(:forums_threads)
add :user_id, references(:forums_users)
timestamps()
end
end
end
mix ecto.migrate
If we run the one test now, we see that it gets a bit further - now we need a
way to know if a thread is watched by a given user. Let's write that function.
In the process, we'll support checking anything that's
watchable
, by which I
mean it has the same general associations around watches as
Thread
has. This
will allow us to reuse this in the future:
defmodule FirestormWeb.Forums do
# ...
@doc """
Determine if a user is watching a given watchable (Thread, etc):
iex> %Thread{} |> watched_by?(%User{})
false
"""
def watched_by?(watchable, user = %User{}) do
watch_count(watchable, user) > 0
end
def watcher_ids(watchable) do
watchable
|> watches()
|> select([f], f.user_id)
|> Repo.all
end
def watch_count(watchable) do
watchable
|> watches()
|> Repo.aggregate(:count, :id)
end
defp watch_count(watchable, user = %User{}) do
watchable
|> watches()
|> where([f], f.user_id == ^user.id)
|> Repo.aggregate(:count, :id)
end
defp watches(watchable) do
watchable
|> Ecto.assoc(:watches)
end
defp watch_changeset(%Watch{} = watch, attrs) do
watch
|> cast(attrs, [:assoc_id, :user_id])
|> validate_required([:assoc_id, :user_id])
end
end
With this, the tests pass.
Summary
Today we added support for watching a
Thread
in the
Forums
context. In the
process, we saw one way to manage polymorphic associations in
Ecto
, using an
abstract table in the
Watch
schema. In the next episode, we'll add support for
watching and unwatching
Threads
in the controllers. We'll also only send
notifications about new posts to users watching the
Thread
. See you soon!
Comments for Watching Threads via Polymorphic Associations