Added Hashtag entity and objects-hashtags association with auto-sync with `data.tag` on Object update.

This commit is contained in:
Ivan Tashkinov 2020-12-22 22:04:33 +03:00
parent ee221277b0
commit e369b1306b
6 changed files with 143 additions and 9 deletions

58
lib/pleroma/hashtag.ex Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]