Added Hashtag entity and objects-hashtags association with auto-sync with `data.tag` on Object update.
This commit is contained in:
parent
ee221277b0
commit
e369b1306b
|
@ -0,0 +1,58 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Hashtag do
|
||||||
|
use Ecto.Schema
|
||||||
|
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
alias Pleroma.Hashtag
|
||||||
|
alias Pleroma.Repo
|
||||||
|
|
||||||
|
@derive {Jason.Encoder, only: [:data]}
|
||||||
|
|
||||||
|
schema "hashtags" do
|
||||||
|
field(:name, :string)
|
||||||
|
field(:data, :map, default: %{})
|
||||||
|
|
||||||
|
many_to_many(:objects, Pleroma.Object, join_through: "hashtags_objects", on_replace: :delete)
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_by_name(name) do
|
||||||
|
Repo.get_by(Hashtag, name: name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_or_create_by_name(name) when is_bitstring(name) do
|
||||||
|
with %Hashtag{} = hashtag <- get_by_name(name) do
|
||||||
|
{:ok, hashtag}
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
%Hashtag{}
|
||||||
|
|> changeset(%{name: name})
|
||||||
|
|> Repo.insert()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_or_create_by_names(names) when is_list(names) do
|
||||||
|
Enum.reduce_while(names, {:ok, []}, fn name, {:ok, list} ->
|
||||||
|
case get_or_create_by_name(name) do
|
||||||
|
{:ok, %Hashtag{} = hashtag} ->
|
||||||
|
{:cont, {:ok, list ++ [hashtag]}}
|
||||||
|
|
||||||
|
error ->
|
||||||
|
{:halt, error}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(%Hashtag{} = struct, params) do
|
||||||
|
struct
|
||||||
|
|> cast(params, [:name, :data])
|
||||||
|
|> update_change(:name, &String.downcase/1)
|
||||||
|
|> validate_required([:name])
|
||||||
|
|> unique_constraint(:name)
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,6 +10,7 @@ defmodule Pleroma.Object do
|
||||||
|
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
alias Pleroma.Config
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.Hashtag
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Object.Fetcher
|
alias Pleroma.Object.Fetcher
|
||||||
alias Pleroma.ObjectTombstone
|
alias Pleroma.ObjectTombstone
|
||||||
|
@ -26,6 +27,8 @@ defmodule Pleroma.Object do
|
||||||
schema "objects" do
|
schema "objects" do
|
||||||
field(:data, :map)
|
field(:data, :map)
|
||||||
|
|
||||||
|
many_to_many(:hashtags, Hashtag, join_through: "hashtags_objects", on_replace: :delete)
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -53,17 +56,31 @@ def create(data) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def change(struct, params \\ %{}) do
|
def change(struct, params \\ %{}) do
|
||||||
changeset =
|
struct
|
||||||
struct
|
|> cast(params, [:data])
|
||||||
|> cast(params, [:data])
|
|> validate_required([:data])
|
||||||
|> validate_required([:data])
|
|> unique_constraint(:ap_id, name: :objects_unique_apid_index)
|
||||||
|> unique_constraint(:ap_id, name: :objects_unique_apid_index)
|
|> maybe_handle_hashtags_change(struct)
|
||||||
|
end
|
||||||
|
|
||||||
if hashtags_changed?(struct, get_change(changeset, :data)) do
|
defp maybe_handle_hashtags_change(changeset, struct) do
|
||||||
# TODO: modify assoc once it's introduced
|
with data_hashtags_change = get_change(changeset, :data),
|
||||||
changeset
|
true <- hashtags_changed?(struct, data_hashtags_change),
|
||||||
|
{:ok, hashtag_records} <-
|
||||||
|
data_hashtags_change
|
||||||
|
|> object_data_hashtags()
|
||||||
|
|> Hashtag.get_or_create_by_names() do
|
||||||
|
put_assoc(changeset, :hashtags, hashtag_records)
|
||||||
else
|
else
|
||||||
changeset
|
false ->
|
||||||
|
changeset
|
||||||
|
|
||||||
|
{:error, hashtag_changeset} ->
|
||||||
|
failed_hashtag = get_field(hashtag_changeset, :name)
|
||||||
|
|
||||||
|
validate_change(changeset, :data, fn _, _ ->
|
||||||
|
[data: "error referencing hashtag: #{failed_hashtag}"]
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
defmodule Pleroma.Repo.Migrations.CreateHashtags do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create_if_not_exists table(:hashtags) do
|
||||||
|
add(:name, :citext, null: false)
|
||||||
|
add(:data, :map, default: %{})
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
create_if_not_exists(unique_index(:hashtags, [:name]))
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,13 @@
|
||||||
|
defmodule Pleroma.Repo.Migrations.CreateHashtagsObjects do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create_if_not_exists table(:hashtags_objects) do
|
||||||
|
add(:hashtag_id, references(:hashtags), null: false)
|
||||||
|
add(:object_id, references(:objects), null: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
create_if_not_exists(unique_index(:hashtags_objects, [:hashtag_id, :object_id]))
|
||||||
|
create_if_not_exists(index(:hashtags_objects, [:object_id]))
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,10 +5,13 @@
|
||||||
defmodule Pleroma.ObjectTest do
|
defmodule Pleroma.ObjectTest do
|
||||||
use Pleroma.DataCase
|
use Pleroma.DataCase
|
||||||
use Oban.Testing, repo: Pleroma.Repo
|
use Oban.Testing, repo: Pleroma.Repo
|
||||||
|
|
||||||
import ExUnit.CaptureLog
|
import ExUnit.CaptureLog
|
||||||
import Pleroma.Factory
|
import Pleroma.Factory
|
||||||
import Tesla.Mock
|
import Tesla.Mock
|
||||||
|
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.Hashtag
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
alias Pleroma.Tests.ObanHelpers
|
alias Pleroma.Tests.ObanHelpers
|
||||||
|
@ -406,4 +409,28 @@ test "preserves internal fields on refetch", %{mock_modified: mock_modified} do
|
||||||
assert updated_object.data["like_count"] == 1
|
assert updated_object.data["like_count"] == 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe ":hashtags association" do
|
||||||
|
test "Hashtag records are created with Object record and updated on its change" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, %{object: object}} =
|
||||||
|
CommonAPI.post(user, %{status: "some text #hashtag1 #hashtag2 ..."})
|
||||||
|
|
||||||
|
assert [%Hashtag{name: "hashtag1"}, %Hashtag{name: "hashtag2"}] =
|
||||||
|
Enum.sort_by(object.hashtags, & &1.name)
|
||||||
|
|
||||||
|
{:ok, object} = Object.update_data(object, %{"tag" => []})
|
||||||
|
|
||||||
|
assert [] = object.hashtags
|
||||||
|
|
||||||
|
object = Object.get_by_id(object.id) |> Repo.preload(:hashtags)
|
||||||
|
assert [] = object.hashtags
|
||||||
|
|
||||||
|
{:ok, object} = Object.update_data(object, %{"tag" => ["abc", "def"]})
|
||||||
|
|
||||||
|
assert [%Hashtag{name: "abc"}, %Hashtag{name: "def"}] =
|
||||||
|
Enum.sort_by(object.hashtags, & &1.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -217,6 +217,11 @@ test "it fetches the appropriate tag-restricted posts" do
|
||||||
tag_all: ["test", "reject"]
|
tag_all: ["test", "reject"]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
[fetch_one, fetch_two, fetch_three, fetch_four] =
|
||||||
|
Enum.map([fetch_one, fetch_two, fetch_three, fetch_four], fn statuses ->
|
||||||
|
Enum.map(statuses, fn s -> Repo.preload(s, object: :hashtags) end)
|
||||||
|
end)
|
||||||
|
|
||||||
assert fetch_one == [status_one, status_three]
|
assert fetch_one == [status_one, status_three]
|
||||||
assert fetch_two == [status_one, status_two, status_three]
|
assert fetch_two == [status_one, status_two, status_three]
|
||||||
assert fetch_three == [status_one, status_two]
|
assert fetch_three == [status_one, status_two]
|
||||||
|
|
Loading…
Reference in New Issue