Allow tag relations + bump ecto deps

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2019-02-14 14:19:55 +01:00
parent 4caa998ae0
commit 256d50e855
37 changed files with 515 additions and 132 deletions

View File

@@ -579,7 +579,7 @@ defmodule Mobilizon.Actors do
"""
def authenticate(%{user: user, password: password}) do
# Does password match the one stored in the database?
case Comeonin.Argon2.checkpw(password, user.password_hash) do
case Argon2.verify_pass(password, user.password_hash) do
true ->
# Yes, create and return the token
MobilizonWeb.Guardian.encode_and_sign(user)

View File

@@ -12,7 +12,7 @@ defmodule Mobilizon.Actors.Service.Activation do
with %User{} = user <- Actors.get_user_by_activation_token(token),
{:ok, %User{} = user} <-
Actors.update_user(user, %{
"confirmed_at" => DateTime.utc_now(),
"confirmed_at" => DateTime.utc_now() |> DateTime.truncate(:second),
"confirmation_sent_at" => nil,
"confirmation_token" => nil
}) do
@@ -26,7 +26,10 @@ defmodule Mobilizon.Actors.Service.Activation do
def resend_confirmation_email(%User{} = user, locale \\ "en") do
with :ok <- Tools.we_can_send_email(user, :confirmation_sent_at),
{:ok, user} <- Actors.update_user(user, %{"confirmation_sent_at" => DateTime.utc_now()}) do
{:ok, user} <-
Actors.update_user(user, %{
"confirmation_sent_at" => DateTime.utc_now() |> DateTime.truncate(:second)
}) do
send_confirmation_email(user, locale)
Logger.info("Sent confirmation email again to #{user.email}")
{:ok, user.email}

View File

@@ -43,7 +43,7 @@ defmodule Mobilizon.Actors.Service.ResetPassword do
Repo.update(
User.send_password_reset_changeset(user, %{
"reset_password_token" => Tools.random_string(30),
"reset_password_sent_at" => DateTime.utc_now()
"reset_password_sent_at" => DateTime.utc_now() |> DateTime.truncate(:second)
})
) do
mail =

View File

@@ -11,7 +11,10 @@ defmodule Mobilizon.Actors.Service.Tools do
:ok
_ ->
case Timex.before?(Timex.shift(Map.get(user, key), hours: 1), DateTime.utc_now()) do
case Timex.before?(
Timex.shift(Map.get(user, key), hours: 1),
DateTime.utc_now() |> DateTime.truncate(:second)
) do
true ->
:ok

View File

@@ -89,7 +89,12 @@ defmodule Mobilizon.Actors.User do
case changeset do
%Ecto.Changeset{valid?: true, changes: %{email: _email}} ->
changeset = put_change(changeset, :confirmation_token, random_string(30))
put_change(changeset, :confirmation_sent_at, DateTime.utc_now())
put_change(
changeset,
:confirmation_sent_at,
DateTime.utc_now() |> DateTime.truncate(:second)
)
_ ->
changeset
@@ -122,7 +127,7 @@ defmodule Mobilizon.Actors.User do
put_change(
changeset,
:password_hash,
Comeonin.Argon2.hashpwsalt(password)
Argon2.hash_pwd_salt(password)
)
_ ->

View File

@@ -32,16 +32,16 @@ defmodule Mobilizon.Events.Event do
schema "events" do
field(:url, :string)
field(:local, :boolean, default: true)
field(:begins_on, Timex.Ecto.DateTimeWithTimezone)
field(:begins_on, :utc_datetime)
field(:description, :string)
field(:ends_on, Timex.Ecto.DateTimeWithTimezone)
field(:ends_on, :utc_datetime)
field(:title, :string)
field(:status, Mobilizon.Events.EventStatusEnum, default: :confirmed)
field(:visibility, Mobilizon.Events.EventVisibilityEnum, default: :public)
field(:join_options, Mobilizon.Events.JoinOptionsEnum, default: :free)
field(:thumbnail, :string)
field(:large_image, :string)
field(:publish_at, Timex.Ecto.DateTimeWithTimezone)
field(:publish_at, :utc_datetime)
field(:uuid, Ecto.UUID, default: Ecto.UUID.generate())
field(:online_address, :string)
field(:phone_address, :string)

View File

@@ -458,8 +458,32 @@ defmodule Mobilizon.Events do
[%Tag{}, ...]
"""
def list_tags do
Repo.all(Tag)
def list_tags(page \\ nil, limit \\ nil) do
Repo.all(
Tag
|> paginate(page, limit)
)
end
@doc """
Returns the list of tags for an event.
## Examples
iex> list_tags_for_event(id)
[%Participant{}, ...]
"""
def list_tags_for_event(id, page \\ nil, limit \\ nil) do
Repo.all(
from(
t in Tag,
join: e in "events_tags",
on: t.id == e.tag_id,
where: e.event_id == ^id
)
|> paginate(page, limit)
)
end
@doc """
@@ -543,6 +567,85 @@ defmodule Mobilizon.Events do
Tag.changeset(tag, %{})
end
alias Mobilizon.Events.TagRelation
@doc """
Create a relation between two tags
"""
def create_tag_relation(attrs \\ {}) do
%TagRelation{}
|> TagRelation.changeset(attrs)
|> Repo.insert(conflict_target: [:tag_id, :link_id], on_conflict: [inc: [weight: 1]])
end
@doc """
Remove a tag relation
"""
def delete_tag_relation(%TagRelation{} = tag_relation) do
Repo.delete(tag_relation)
end
@doc """
Returns whether two tags are linked or not
"""
def are_tags_linked(%Tag{id: tag1_id}, %Tag{id: tag2_id}) do
case from(tr in TagRelation,
where: tr.tag_id == ^min(tag1_id, tag2_id) and tr.link_id == ^max(tag1_id, tag2_id)
)
|> Repo.one() do
%TagRelation{} -> true
_ -> false
end
end
@doc """
Returns the tags neighbors for a given tag
We can't rely on the single many_to_many relation since we also want tags that link to our tag, not just tags linked by this one
The SQL query looks like this:
```sql
SELECT * FROM tags t
RIGHT JOIN (
SELECT weight, link_id AS id
FROM tag_relations t2
WHERE tag_id = 1
UNION ALL
SELECT tag_id AS id, weight
FROM tag_relations t2
WHERE link_id = 1
) tr
ON t.id = tr.id
ORDER BY tr.weight
DESC;
```
"""
def tag_neighbors(%Tag{id: id}, relation_minimum \\ 1, limit \\ 10) do
query2 =
from(tr in TagRelation,
select: %{id: tr.tag_id, weight: tr.weight},
where: tr.link_id == ^id
)
query =
from(tr in TagRelation,
select: %{id: tr.link_id, weight: tr.weight},
union_all: ^query2,
where: tr.tag_id == ^id
)
final_query =
from(t in Tag,
right_join: q in subquery(query),
on: [id: t.id],
where: q.weight >= ^relation_minimum,
limit: ^limit,
order_by: [desc: q.weight]
)
Repo.all(final_query)
end
alias Mobilizon.Events.Participant
@doc """

View File

@@ -15,8 +15,8 @@ defmodule Mobilizon.Events.Session do
field(:subtitle, :string)
field(:title, :string)
field(:videos_urls, :string)
field(:begins_on, Timex.Ecto.DateTimeWithTimezone)
field(:ends_on, Timex.Ecto.DateTimeWithTimezone)
field(:begins_on, :utc_datetime)
field(:ends_on, :utc_datetime)
belongs_to(:event, Event)
belongs_to(:track, Track)

View File

@@ -39,10 +39,12 @@ defmodule Mobilizon.Events.Tag do
import Ecto.Changeset
alias Mobilizon.Events.Tag
alias Mobilizon.Events.Tag.TitleSlug
alias Mobilizon.Events.TagRelation
schema "tags" do
field(:title, :string)
field(:slug, TitleSlug.Type)
many_to_many(:related_tags, Tag, join_through: TagRelation)
timestamps()
end

View File

@@ -0,0 +1,41 @@
defmodule Mobilizon.Events.TagRelation do
@moduledoc """
Represents a tag for events
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Events.Tag
alias Mobilizon.Events.TagRelation
@primary_key false
schema "tag_relations" do
belongs_to(:tag, Tag, primary_key: true)
belongs_to(:link, Tag, primary_key: true)
field(:weight, :integer, default: 1)
end
@doc false
def changeset(%TagRelation{} = tag, attrs) do
changeset =
tag
|> cast(attrs, [:tag_id, :link_id, :weight])
|> validate_required([:tag_id, :link_id])
# Return if tag_id or link_id are not set because it will fail later otherwise
with %Ecto.Changeset{errors: []} <- changeset do
changes = changeset.changes
changeset =
changeset
|> put_change(:tag_id, min(changes.tag_id, changes.link_id))
|> put_change(:link_id, max(changes.tag_id, changes.link_id))
changeset
|> unique_constraint(:tag_id, name: :tag_relations_pkey)
|> check_constraint(:tag_id,
name: :no_self_loops_check,
message: "Can't add a relation on self"
)
end
end
end

View File

@@ -1,5 +1,5 @@
Postgrex.Types.define(
Mobilizon.PostgresTypes,
[Geo.PostGIS.Extension] ++ Ecto.Adapters.Postgres.extensions(),
json: Poison
json: Jason
)

View File

@@ -2,7 +2,9 @@ defmodule Mobilizon.Repo do
@moduledoc """
Mobilizon Repo
"""
use Ecto.Repo, otp_app: :mobilizon
use Ecto.Repo,
otp_app: :mobilizon,
adapter: Ecto.Adapters.Postgres
@doc """
Dynamically loads the repository url from the

View File

@@ -5,7 +5,7 @@ defmodule MobilizonWeb.AuthErrorHandler do
import Plug.Conn
def auth_error(conn, {type, _reason}, _opts) do
body = Poison.encode!(%{message: to_string(type)})
body = Jason.encode!(%{message: to_string(type)})
send_resp(conn, 401, body)
end
end

View File

@@ -40,7 +40,7 @@ defmodule MobilizonWeb.Endpoint do
Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Poison
json_decoder: Jason
)
plug(Plug.MethodOverride)

View File

@@ -0,0 +1,40 @@
defmodule MobilizonWeb.Resolvers.Tag do
@moduledoc """
Handles the tag-related GraphQL calls
"""
require Logger
alias Mobilizon.Events.Event
alias Mobilizon.Events.Tag
def list_tags(_parent, %{page: page, limit: limit}, _resolution) do
tags = Mobilizon.Events.list_tags(page, limit)
{:ok, tags}
end
@doc """
Retrieve the list of tags for an event
"""
def list_tags_for_event(%Event{id: id}, _args, _resolution) do
{:ok, Mobilizon.Events.list_tags_for_event(id)}
end
@doc """
Retrieve the list of related tags for a given tag ID
"""
def get_related_tags(_parent, %{tag_id: tag_id}, _resolution) do
with %Tag{} = tag <- Mobilizon.Events.get_tag!(tag_id),
tags <- Mobilizon.Events.tag_neighbors(tag) do
{:ok, tags}
end
end
@doc """
Retrieve the list of related tags for a parent tag
"""
def get_related_tags(%Tag{} = tag, _args, _resolution) do
with tags <- Mobilizon.Events.tag_neighbors(tag) do
{:ok, tags}
end
end
end

View File

@@ -8,6 +8,7 @@ defmodule MobilizonWeb.Schema.EventType do
import_types(MobilizonWeb.Schema.AddressType)
import_types(MobilizonWeb.Schema.Events.ParticipantType)
import_types(MobilizonWeb.Schema.Events.CategoryType)
import_types(MobilizonWeb.Schema.TagType)
alias MobilizonWeb.Resolvers
@desc "An event"
@@ -37,7 +38,12 @@ defmodule MobilizonWeb.Schema.EventType do
)
field(:attributed_to, :actor, description: "Who the event is attributed to (often a group)")
# field(:tags, list_of(:tag))
field(:tags, list_of(:tag),
resolve: &MobilizonWeb.Resolvers.Tag.list_tags_for_event/3,
description: "The event's tags"
)
field(:category, :category, description: "The event's category")
field(:participants, list_of(:participant),

View File

@@ -0,0 +1,30 @@
defmodule MobilizonWeb.Schema.TagType do
@moduledoc """
Schema representation for Tags
"""
use Absinthe.Schema.Notation
alias MobilizonWeb.Resolvers
@desc "A tag"
object :tag do
field(:id, :id, description: "The tag's ID")
field(:slug, :string, description: "The tags's slug")
field(:title, :string, description: "The tag's title")
field(
:related,
list_of(:tag),
resolve: &Resolvers.Tag.get_related_tags/3,
description: "Related tags to this tag"
)
end
object :tag_queries do
@desc "Get the list of tags"
field :tags, non_null(list_of(:tag)) do
arg(:page, :integer, default_value: 1)
arg(:limit, :integer, default_value: 10)
resolve(&Resolvers.Tag.list_tags/3)
end
end
end

View File

@@ -495,7 +495,10 @@ defmodule Mobilizon.Service.ActivityPub do
ical_events =
body
|> ExIcal.parse()
|> ExIcal.by_range(DateTime.utc_now(), DateTime.utc_now() |> Timex.shift(years: 1))
|> ExIcal.by_range(
DateTime.utc_now(),
DateTime.utc_now() |> DateTime.truncate(:second) |> Timex.shift(years: 1)
)
activities =
ical_events

View File

@@ -46,7 +46,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
end
def make_date do
DateTime.utc_now() |> DateTime.to_iso8601()
DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
end
@doc """

View File

@@ -59,7 +59,7 @@ defmodule Mobilizon.Service.Federator do
_e ->
# Just drop those for now
Logger.error("Unhandled activity")
Logger.error(Poison.encode!(params, pretty: 2))
Logger.error(Jason.encode!(params))
end
end