diff --git a/lib/sanbase/project/list/selector/project_list_selector.ex b/lib/sanbase/project/list/selector/project_list_selector.ex index 35000f5d58..0fca0bfb18 100644 --- a/lib/sanbase/project/list/selector/project_list_selector.ex +++ b/lib/sanbase/project/list/selector/project_list_selector.ex @@ -91,6 +91,7 @@ defmodule Sanbase.Project.ListSelector do order_by = Transform.args_to_order_by(args) pagination = Transform.args_to_pagination(args) filters_combinator = Transform.args_to_filters_combinator(args) + include_hidden = Map.get(args, :include_hidden, false) base_slugs = base_slugs(base_projects_selector) @@ -105,7 +106,8 @@ defmodule Sanbase.Project.ListSelector do pagination: pagination, min_volume: Map.get(args, :min_volume), included_slugs: included_slugs, - ordered_slugs: ordered_slugs + ordered_slugs: ordered_slugs, + include_hidden: include_hidden ] {:ok, opts} diff --git a/lib/sanbase/project/project.ex b/lib/sanbase/project/project.ex index dbfc878151..aabaed3b59 100644 --- a/lib/sanbase/project/project.ex +++ b/lib/sanbase/project/project.ex @@ -33,6 +33,8 @@ defmodule Sanbase.Project do field(:slug, :string) field(:coinmarketcap_id, :string) field(:is_hidden, :boolean, default: false) + field(:hidden_since, :utc_datetime) + field(:hidden_reason, :string) field(:description, :string) field(:long_description, :string) @@ -123,6 +125,8 @@ defmodule Sanbase.Project do :github_link, :infrastructure_id, :is_hidden, + :hidden_since, + :hidden_reason, :linkedin_link, :logo_url, :long_description, @@ -149,6 +153,22 @@ defmodule Sanbase.Project do |> cast_assoc(:market_segments) |> cast_assoc(:ecosystems) |> unique_constraint(:slug) + |> maybe_add_hidden_since() + end + + defp maybe_add_hidden_since(changeset) do + case changeset.changes do + %{is_hidden: true} -> + changeset + |> put_change(:hidden_since, DateTime.utc_now() |> DateTime.truncate(:second)) + + %{is_hiden: false} -> + changeset + |> put_change(:hidden_since, nil) + + _ -> + changeset + end end defdelegate roi_usd(project), to: Project.Roi diff --git a/lib/sanbase_web/generic_admin/project.ex b/lib/sanbase_web/generic_admin/project.ex index 4d60334478..c715335c8d 100644 --- a/lib/sanbase_web/generic_admin/project.ex +++ b/lib/sanbase_web/generic_admin/project.ex @@ -27,6 +27,7 @@ defmodule SanbaseWeb.GenericAdmin.Project do :infrastructure, :token_decimals, :is_hidden, + :hidden_reason, :telegram_chat_id, :logo_url, :dark_logo_url, @@ -61,6 +62,7 @@ defmodule SanbaseWeb.GenericAdmin.Project do :infrastructure, :token_decimals, :is_hidden, + :hidden_reason, :telegram_chat_id, :logo_url, :dark_logo_url, diff --git a/lib/sanbase_web/graphql/schema/queries/project_queries.ex b/lib/sanbase_web/graphql/schema/queries/project_queries.ex index c40e3d2b7e..545bfad3ab 100644 --- a/lib/sanbase_web/graphql/schema/queries/project_queries.ex +++ b/lib/sanbase_web/graphql/schema/queries/project_queries.ex @@ -25,6 +25,8 @@ defmodule SanbaseWeb.Graphql.Schema.ProjectQueries do arg(:page_size, :integer) arg(:min_volume, :integer) + arg(:include_hidden, :boolean, default_value: false) + middleware(ProjectPermissions) cache_resolve(&ProjectListResolver.all_projects/3) end diff --git a/lib/sanbase_web/graphql/schema/types/project_types.ex b/lib/sanbase_web/graphql/schema/types/project_types.ex index 8e884aa947..2ef9f84ee5 100644 --- a/lib/sanbase_web/graphql/schema/types/project_types.ex +++ b/lib/sanbase_web/graphql/schema/types/project_types.ex @@ -359,6 +359,32 @@ defmodule SanbaseWeb.Graphql.ProjectTypes do field(:long_description, :string) field(:token_decimals, :integer) + @desc ~s""" + Shows if a project is marked as hidden. + + Hidden projects are excluded from lists of projects + like allProjects and screeners that are filtered by a condition. + + Hidden projects can be accessed when directly queried via project or projectBySlug, + or by passing the `includeHiddenProjects: true` flag to `allProjects`. + """ + field(:is_hidden, non_null(:boolean)) + + @desc ~s""" + If the project is hidden, this field shows the datetime since which the project + is hidden. + + Not all hidden projects have a hidden_since field. The projects that are hidden + before August 2024 don't have their hidden_since stored. + """ + field(:hidden_since, :datetime) + + @desc ~s""" + If the project is hidden, this field can contain the reason why the project is + hidden. + """ + field(:hidden_reason, :string) + @desc ~s""" A full list of all the ecosystem this project contributes to in one or more ways: diff --git a/priv/repo/migrations/20240809122904_extend_projects_table_hidden_info.exs b/priv/repo/migrations/20240809122904_extend_projects_table_hidden_info.exs new file mode 100644 index 0000000000..5e2618396b --- /dev/null +++ b/priv/repo/migrations/20240809122904_extend_projects_table_hidden_info.exs @@ -0,0 +1,10 @@ +defmodule Sanbase.Repo.Migrations.ExtendProjectsTableHiddenInfo do + use Ecto.Migration + + def change do + alter table(:project) do + add(:hidden_since, :utc_datetime) + add(:hidden_reason, :text) + end + end +end diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql index 2791645e4c..b7f72f80c1 100644 --- a/priv/repo/structure.sql +++ b/priv/repo/structure.sql @@ -2902,7 +2902,9 @@ CREATE TABLE public.project ( ecosystem character varying(255), ecosystem_full_path character varying(255), multichain_project_group_key character varying(255) DEFAULT NULL::character varying, - deployed_on_ecosystem_id bigint + deployed_on_ecosystem_id bigint, + hidden_since timestamp(0) without time zone, + hidden_reason text ); @@ -9559,3 +9561,4 @@ INSERT INTO public."schema_migrations" (version) VALUES (20240531121027); INSERT INTO public."schema_migrations" (version) VALUES (20240723122118); INSERT INTO public."schema_migrations" (version) VALUES (20240725122924); INSERT INTO public."schema_migrations" (version) VALUES (20240805115620); +INSERT INTO public."schema_migrations" (version) VALUES (20240809122904); diff --git a/test/sanbase_web/graphql/projects/project_hidden_api_test.exs b/test/sanbase_web/graphql/projects/project_hidden_api_test.exs new file mode 100644 index 0000000000..5fb4f0aeb5 --- /dev/null +++ b/test/sanbase_web/graphql/projects/project_hidden_api_test.exs @@ -0,0 +1,119 @@ +defmodule SanbaseWeb.Graphql.ProjectHiddenApiTest do + use SanbaseWeb.ConnCase, async: false + + import Sanbase.Factory + import SanbaseWeb.Graphql.TestHelpers + + test "all projects", %{conn: conn} do + p1 = insert(:random_erc20_project) + p2 = insert(:random_erc20_project) + p3 = insert(:random_erc20_project, is_hidden: true) + + # Without includeHidden flag. + projects = all_projects(conn) + assert length(projects) == 2 + + slugs = projects |> Enum.map(& &1["slug"]) + + assert p1.slug in slugs + assert p2.slug in slugs + refute p3.slug in slugs + + # With includeHidden: true flag + projects = all_projects_including_hidden(conn) + assert length(projects) == 3 + + slugs = projects |> Enum.map(& &1["slug"]) + assert p1.slug in slugs + assert p2.slug in slugs + assert p3.slug in slugs + end + + test "project by slug", %{conn: conn} do + p1 = insert(:random_erc20_project) + p2 = insert(:random_erc20_project) + + assert {:ok, _} = + Sanbase.Project.changeset(p2, %{is_hidden: true, hidden_reason: "duplicate"}) + |> Sanbase.Repo.update() + + # not a hidden project + project = project_by_slug(conn, %{slug: p1.slug}) + + assert project == %{ + "hiddenReason" => nil, + "hiddenSince" => nil, + "isHidden" => false, + "slug" => p1.slug + } + + # hidden project + project = project_by_slug(conn, %{slug: p2.slug}) + + assert %{ + "hiddenReason" => "duplicate", + "hiddenSince" => dt, + "isHidden" => true, + "slug" => slug + } = project + + assert slug == p2.slug + + assert Sanbase.TestUtils.datetime_close_to( + DateTime.utc_now(), + Sanbase.DateTimeUtils.from_iso8601!(dt), + 1, + :seconds + ) + end + + defp project_by_slug(conn, args) do + query = """ + { + projectBySlug(#{map_to_args(args)}){ + slug + isHidden + hiddenSince + hiddenReason + } + } + """ + + conn + |> post("/graphql", query_skeleton(query)) + |> json_response(200) + |> get_in(["data", "projectBySlug"]) + end + + defp all_projects(conn) do + query = """ + { + allProjects{ + slug + isHidden + } + } + """ + + conn + |> post("/graphql", query_skeleton(query)) + |> json_response(200) + |> get_in(["data", "allProjects"]) + end + + defp all_projects_including_hidden(conn) do + query = """ + { + allProjects(includeHidden: true){ + slug + isHidden + } + } + """ + + conn + |> post("/graphql", query_skeleton(query)) + |> json_response(200) + |> get_in(["data", "allProjects"]) + end +end