Read marks with Ecto and Phoenix
As always, Rails has an easy, pluggable way to do this with unread but it isn’t too hard to add the same to your Phoenix app. Let’s try!
First we’ll generate the table and schema that holds the read marks. This assumes you have two tables: users
and stories
.
mix phoenix.gen.model ReadMark read_marks \
user_id:references:users \
story_id:references:stories
Open up the newly generated web/models/read_mark.ex
and set up the associations:
defmodule MyApp.ReadMark do
use MyApp.Web, :model
schema "read_marks" do
belongs_to :user, MyApp.User
belongs_to :story, MyApp.Story
timestamps()
end
end
And in the existing models:
defmodule MyApp.User do
schema "users" do
has_many :read_marks, MyApp.ReadMark, on_delete: :delete_all
end
end
defmodule MyApp.Story do
schema "stories" do
has_many :read_marks, MyApp.ReadMark, on_delete: :delete_all
end
end
This is all the setup we need.
We can now get unread stories for a user with a query like:
defmodule MyApp.Story do
#...
def unread_by(query, user) do
from s in query,
left_join: rm in assoc(s, :read_marks),
on: rm.user_id == ^user.id,
where: is_nil(rm.id)
end
def unread_by(user), do: unread_by(DRBot.Story, user)
end
And use it like:
user = Repo.get(MyApp.User, 1)
unread_stories = MyApp.Story.unread_by(user) |> Repo.all
We can even chain it with other queries:
query = from s in MyApp.Story, where: s.published_on == ^today
unread_stories_published_today =
MyApp.Story.unread_by(query, user)
|> Repo.all
# or the other way around
query = MyApp.Story.unread_by(user)
unread_stories_published_today =
(from s in query, where: s.published_on == ^today)
|> Repo.all