Skip to content

Commit

Permalink
Merge pull request #60 from CaptainFact/feature/notifications
Browse files Browse the repository at this point in the history
Notifications
  • Loading branch information
Betree authored Mar 17, 2019
2 parents cbca9cb + 525537d commit 7fd50f4
Show file tree
Hide file tree
Showing 68 changed files with 1,759 additions and 180 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ apps/*/priv/secrets/*

# Elixir LS
.elixir_ls

# IDE
.history
20 changes: 10 additions & 10 deletions apps/cf/lib/actions/action_creator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ defmodule CF.Actions.ActionCreator do
video_id: statement.video_id,
statement_id: statement.id,
changes: %{
text: statement.text,
time: statement.time,
speaker_id: statement.speaker_id
"text" => statement.text,
"time" => statement.time,
"speaker_id" => statement.speaker_id
}
)
end
Expand All @@ -35,8 +35,8 @@ defmodule CF.Actions.ActionCreator do
:create,
speaker_id: speaker.id,
changes: %{
full_name: speaker.full_name,
title: speaker.title
"full_name" => speaker.full_name,
"title" => speaker.title
}
)
end
Expand All @@ -50,10 +50,10 @@ defmodule CF.Actions.ActionCreator do
statement_id: comment.statement_id,
comment_id: comment.id,
changes: %{
text: comment.text,
source: source_url,
statement_id: comment.statement_id,
reply_to_id: comment.reply_to_id
"text" => comment.text,
"source" => source_url,
"statement_id" => comment.statement_id,
"reply_to_id" => comment.reply_to_id
}
)
end
Expand All @@ -77,7 +77,7 @@ defmodule CF.Actions.ActionCreator do
:add,
video_id: video.id,
changes: %{
url: Video.build_url(video)
"url" => Video.build_url(video)
}
)
end
Expand Down
56 changes: 55 additions & 1 deletion apps/cf/lib/actions/actions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule CF.Actions do

alias DB.Schema.{User, UserAction}
alias DB.Repo
alias DB.Query.Actions, as: ActionsQuery
alias CF.Actions, as: ActionsQuery

@doc """
Count all actions with `action_type` type for this entity
Expand Down Expand Up @@ -36,6 +36,60 @@ defmodule CF.Actions do
|> Repo.aggregate(:count, :id)
end

@doc """
Return all action concerning user, which is actions he made + actions he was
targeted by.
"""
@spec about_user(Ecto.Queryable.t(), %User{}) :: Ecto.Queryable.t()
def about_user(query, %User{id: id}) do
query
|> where([a], a.user_id == ^id)
|> or_where([a], a.target_user_id == ^id)
end

@doc """
Return all action made by user.
"""
@spec by_user(Ecto.Queryable.t(), %User{}) :: Ecto.Queryable.t()
def by_user(query, %User{id: id}) do
where(query, [a], a.user_id == ^id)
end

@doc """
Return all action targeting user.
"""
@spec targeting_user(Ecto.Queryable.t(), %User{}) :: Ecto.Queryable.t()
def targeting_user(query, %User{id: id}) do
where(query, [a], a.target_user_id == ^id)
end

@doc """
Filter given query on matching `types` only
"""
@spec matching_types(Ecto.Queryable.t(), nonempty_list(integer)) :: Ecto.Queryable.t()
def matching_types(query, types) do
where(query, [a], a.type in ^types)
end

@doc """
Filter given query on matching entity types
"""
@spec matching_entities(Ecto.Queryable.t(), nonempty_list(atom)) :: Ecto.Queryable.t()
def matching_entities(query, types) do
where(query, [a], a.entity in ^types)
end

@doc """
Filter given query to return only actions that occured between `date_start`
and `date_end`.
"""
@spec for_period(Ecto.Queryable.t(), NaiveDateTime.t(), NaiveDateTime.t()) :: Ecto.Queryable.t()
def for_period(query, datetime_start, datetime_end) do
query
|> where([a], a.inserted_at >= ^datetime_start)
|> where([a], a.inserted_at <= ^datetime_end)
end

# ---- Private methods ----

defp age_filter(query, -1),
Expand Down
2 changes: 1 addition & 1 deletion apps/cf/lib/actions/reputation_change.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule CF.Actions.ReputationChange do
"""

alias DB.Schema.{User, UserAction}
alias DB.Query.Actions
alias CF.Actions
alias CF.Actions.ReputationChangeConfigLoader

# Reputation changes definition
Expand Down
44 changes: 35 additions & 9 deletions apps/cf/lib/comments/comments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule CF.Comments do
import CF.Actions.ActionCreator

alias Ecto.Multi
alias Kaur.Result

alias DB.Repo
alias DB.Schema.Source
Expand All @@ -14,6 +15,7 @@ defmodule CF.Comments do
alias DB.Schema.UserAction

alias CF.Accounts.UserPermissions
alias CF.Notifications.Subscriptions
alias CF.Sources

# ---- Public API ----
Expand All @@ -28,6 +30,12 @@ defmodule CF.Comments do
|> Repo.all()
end

@doc """
Add a new comment.
[!] This function is very bad and should be refactored, especially the async
source fetcher should be moved to a job.
"""
def add_comment(user, video_id, params, source_url \\ nil, source_fetch_callback \\ nil) do
# TODO [Security] What if reply_to_id refer to a comment that is on a different statement ?
UserPermissions.check!(user, :create, :comment)
Expand All @@ -38,26 +46,44 @@ defmodule CF.Comments do
source_url &&
(Sources.get_by_url(source_url) || Source.changeset(%Source{}, %{url: source_url}))

# Insert comment in DB
full_comment =
comment_changeset =
user
|> Ecto.build_assoc(:comments)
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_assoc(:source, source)
|> Comment.changeset(params)

Multi.new()
|> Multi.run(:comment, fn _ ->
comment_changeset
|> Repo.insert!()
|> Map.put(:user, user)
|> Repo.preload(:source)
|> Repo.preload([:source, :statement])
|> Map.put(:score, 0)
|> Result.ok()
end)
|> Multi.run(:action, fn %{comment: comment} ->
Repo.insert(action_create(user.id, video_id, comment, source_url))
end)
|> Multi.run(:suscription, fn %{comment: comment} ->
Subscriptions.subscribe(user, comment, :is_author)
end)
|> Repo.transaction()
|> case do
{:error, _operation, reason, _changes} ->
{:error, reason}

# Record action
Repo.insert(action_create(user.id, video_id, full_comment, source_url))
{:ok, %{comment: comment}} ->
# Set default on comment
full_comment = comment

# If new source, fetch metadata
unless is_nil(source) || !is_nil(Map.get(source, :id)),
do: fetch_source_metadata_and_update_comment(full_comment, source_fetch_callback)
# If new source, fetch metadata
if source && is_nil(Map.get(source, :id)),
do: fetch_source_metadata_and_update_comment(comment, source_fetch_callback)

full_comment
# Return comment
full_comment
end
end

# Delete
Expand Down
69 changes: 69 additions & 0 deletions apps/cf/lib/notifications/notification_builder.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
defmodule CF.Notifications.NotificationBuilder do
@moduledoc """
A module to build Notification from various inputs.
"""

require Logger

alias DB.Schema.Subscription
alias DB.Schema.UserAction
alias DB.Schema.Notification
alias DB.Type.NotificationType
alias DB.Type.SubscriptionReason

@doc """
Return `Notification` params to put in a changeset for given action and
subscription.
## Examples
iex> action = %UserAction{id: 42, type: :create, entity: :statement}
iex> subscription = %Subscription{user_id: 100}
iex> NotificationBuilder.for_subscribed_action(action, subscription)
%{action_id: 42, type: :new_statement, user_id: 100}
"""
@spec for_subscribed_action(UserAction.t(), Subscription.t()) :: Notification.t()
def for_subscribed_action(action, subscription) when not is_nil(subscription) do
%{
user_id: subscription.user_id,
action_id: action.id,
type: notification_type(action, subscription)
}
end

@spec notification_type(SubscriptionReason.t(), Subscription.t()) :: NotificationType.t()
defp notification_type(
%{type: :create, entity: :comment, changes: %{"reply_to_id" => comment_id}},
%{reason: :is_author, comment_id: comment_id}
),
do: :reply_to_comment

defp notification_type(%{type: :create, entity: :comment}, _),
do: :new_comment

defp notification_type(%{type: :create, entity: :statement}, _),
do: :new_statement

defp notification_type(%{type: type, entity: :speaker}, _) when type in [:create, :add],
do: :new_speaker

defp notification_type(%{type: :update, entity: :statement}, _),
do: :updated_statement

defp notification_type(%{type: :update, entity: :video}, _),
do: :updated_video

defp notification_type(%{type: :update, entity: :speaker}, _),
do: :updated_speaker

defp notification_type(%{type: :remove, entity: :speaker}, _),
do: :removed_speaker

defp notification_type(%{type: :remove, entity: :statement}, _),
do: :removed_statement

defp notification_type(%{type: type, entity: entity}, _) do
Logger.warn("Don't know how to generate a notification for #{type} #{entity}")
:default
end
end
61 changes: 61 additions & 0 deletions apps/cf/lib/notifications/notifications.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
defmodule CF.Notifications do
@moduledoc """
Functions to create, fetch, and manipulate notifications.
"""

import Ecto.Query

alias DB.Repo
alias DB.Schema.User
alias DB.Schema.UserAction
alias DB.Schema.Notification

@doc """
Get all notifications for user, last inserted first.
Paginated with `page` + `limit`.
"""
@spec all(User.t(), integer(), integer(), :all | :seen | :unseen) :: Scrivener.Page.t()
def all(%User{id: user_id}, page \\ 1, page_size \\ 10, filter \\ :all) do
Notification
|> where([n], n.user_id == ^user_id)
|> add_filter(filter)
|> order_by(desc: :inserted_at)
|> Repo.paginate(page: page, page_size: page_size)
end

@doc """
Insert a new notification in DB.
"""
@spec create!(User.t(), UserAction.t(), atom()) ::
{:ok, Notification.t()} | {:error, Ecto.Changeset.t()}
def create!(%User{id: user_id}, %UserAction{id: action_id}, type) do
%Notification{}
|> Notification.changeset(%{user_id: user_id, action_id: action_id, type: type})
|> Repo.insert()
end

@doc """
Mark the given notification as seen or unseed.
seen.
"""
@spec mark_as_seen(Notification.t(), boolean()) :: Notification.t()
def mark_as_seen(notification = %Notification{seen_at: nil}, true) do
notification
|> Notification.changeset(%{seen_at: DateTime.utc_now()})
|> Repo.update()
end

def mark_as_seen(notification = %Notification{seen_at: seen_at}, false)
when not is_nil(seen_at) do
notification
|> Notification.changeset(%{seen_at: nil})
|> Repo.update()
end

def mark_as_seen(notification, _),
do: {:ok, notification}

defp add_filter(query, :seen), do: where(query, [n], not is_nil(n.seen_at))
defp add_filter(query, :unseen), do: where(query, [n], is_nil(n.seen_at))
defp add_filter(query, :all), do: query
end
Loading

0 comments on commit 7fd50f4

Please sign in to comment.