diff --git a/config/config.exs b/config/config.exs index 80feaece3..ef90323d5 100644 --- a/config/config.exs +++ b/config/config.exs @@ -66,6 +66,8 @@ config :groupher_server, :customization, sidebar_communities_index: %{} config :groupher_server, :article, + min_length: 10, + max_length: 20_000, # NOTE: do not change unless you know what you are doing threads: [:post, :job, :repo, :blog], # in this period, paged articles will sort front if non-article-author commented diff --git a/lib/groupher_server/cms/cms.ex b/lib/groupher_server/cms/cms.ex index f4a7a98b5..32d6ac2c6 100644 --- a/lib/groupher_server/cms/cms.ex +++ b/lib/groupher_server/cms/cms.ex @@ -92,6 +92,8 @@ defmodule GroupherServer.CMS do defdelegate mark_delete_article(thread, id), to: ArticleCURD defdelegate undo_mark_delete_article(thread, id), to: ArticleCURD + defdelegate remove_article(thread, id), to: ArticleCURD + defdelegate remove_article(thread, id, reason), to: ArticleCURD defdelegate update_active_timestamp(thread, article), to: ArticleCURD defdelegate sink_article(thread, id), to: ArticleCURD diff --git a/lib/groupher_server/cms/delegates/article_curd.ex b/lib/groupher_server/cms/delegates/article_curd.ex index 86fc2cb93..8e99317f7 100644 --- a/lib/groupher_server/cms/delegates/article_curd.ex +++ b/lib/groupher_server/cms/delegates/article_curd.ex @@ -18,12 +18,14 @@ defmodule GroupherServer.CMS.Delegate.ArticleCURD do alias Accounts.Model.User alias CMS.Model.{Author, Community, PinnedArticle, Embeds} + alias CMS.Model.Repo, as: CMSRepo alias CMS.Delegate.{ ArticleCommunity, CommentCurd, ArticleTag, CommunityCURD, + Document, Hooks } @@ -32,6 +34,7 @@ defmodule GroupherServer.CMS.Delegate.ArticleCURD do @active_period get_config(:article, :active_period_days) @default_emotions Embeds.ArticleEmotion.default_emotions() @default_article_meta Embeds.ArticleMeta.default_meta() + @remove_article_hint "The content does not comply with the community norms" @doc """ read articles for un-logined user @@ -40,7 +43,10 @@ defmodule GroupherServer.CMS.Delegate.ArticleCURD do with {:ok, info} <- match(thread) do Multi.new() |> Multi.run(:inc_views, fn _, _ -> ORM.read(info.model, id, inc: :views) end) - |> Multi.run(:update_article_meta, fn _, %{inc_views: article} -> + |> Multi.run(:load_html, fn _, %{inc_views: article} -> + article |> Repo.preload(:document) |> done + end) + |> Multi.run(:update_article_meta, fn _, %{load_html: article} -> article_meta = ensure(article.meta, @default_article_meta) meta = Map.merge(article_meta, %{can_undo_sink: in_active_period?(thread, article)}) @@ -57,17 +63,11 @@ defmodule GroupherServer.CMS.Delegate.ArticleCURD do def read_article(thread, id, %User{id: user_id}) do with {:ok, info} <- match(thread) do Multi.new() - |> Multi.run(:inc_views, fn _, _ -> ORM.read(info.model, id, inc: :views) end) - |> Multi.run(:update_article_meta, fn _, %{inc_views: article} -> - article_meta = ensure(article.meta, @default_article_meta) - meta = Map.merge(article_meta, %{can_undo_sink: in_active_period?(thread, article)}) - - ORM.update_meta(article, meta) - end) - |> Multi.run(:add_viewed_user, fn _, %{inc_views: article} -> + |> Multi.run(:normal_read, fn _, _ -> read_article(thread, id) end) + |> Multi.run(:add_viewed_user, fn _, %{normal_read: article} -> update_viewed_user_list(article, user_id) end) - |> Multi.run(:set_viewer_has_states, fn _, %{inc_views: article} -> + |> Multi.run(:set_viewer_has_states, fn _, %{normal_read: article} -> article_meta = if is_nil(article.meta), do: @default_article_meta, else: article.meta viewer_has_states = %{ @@ -76,7 +76,7 @@ defmodule GroupherServer.CMS.Delegate.ArticleCURD do viewer_has_reported: user_id in article_meta.reported_user_ids } - {:ok, Map.merge(article, viewer_has_states)} + article |> Map.merge(viewer_has_states) |> done end) |> Repo.transaction() |> result() @@ -156,6 +156,9 @@ defmodule GroupherServer.CMS.Delegate.ArticleCURD do |> Multi.run(:create_article, fn _, _ -> do_create_article(info.model, attrs, author, community) end) + |> Multi.run(:create_document, fn _, %{create_article: article} -> + Document.create(article, attrs) + end) |> Multi.run(:mirror_article, fn _, %{create_article: article} -> ArticleCommunity.mirror_article(thread, article.id, community.id) end) @@ -211,14 +214,17 @@ defmodule GroupherServer.CMS.Delegate.ArticleCURD do @doc """ update a article(post/job ...) """ - def update_article(article, args) do + def update_article(article, attrs) do Multi.new() |> Multi.run(:update_article, fn _, _ -> - do_update_article(article, args) + do_update_article(article, attrs) + end) + |> Multi.run(:update_document, fn _, %{update_article: update_article} -> + Document.update(update_article, attrs) end) |> Multi.run(:update_comment_question_flag_if_need, fn _, %{update_article: update_article} -> # 如果帖子的类型变了,那么 update 所有的 flag - case Map.has_key?(args, :is_question) do + case Map.has_key?(attrs, :is_question) do true -> CommentCurd.batch_update_question_flag(update_article) false -> {:ok, :pass} end @@ -319,6 +325,31 @@ defmodule GroupherServer.CMS.Delegate.ArticleCURD do end end + @doc """ + remove article forever + """ + def remove_article(thread, id, reason \\ @remove_article_hint) do + with {:ok, info} <- match(thread), + {:ok, article} <- ORM.find(info.model, id, preload: [:communities, [author: :user]]) do + Multi.new() + |> Multi.run(:remove_article, fn _, _ -> + article |> ORM.delete() + end) + |> Multi.run(:update_community_article_count, fn _, _ -> + CommunityCURD.update_community_count_field(article.communities, thread) + end) + |> Multi.run(:update_user_published_meta, fn _, _ -> + Accounts.update_published_states(article.author.user.id, thread) + end) + |> Multi.run(:delete_document, fn _, _ -> + Document.remove(thread, id) + end) + # TODO: notify author + |> Repo.transaction() + |> result() + end + end + @spec ensure_author_exists(User.t()) :: {:ok, User.t()} def ensure_author_exists(%User{} = user) do # unique_constraint: avoid race conditions, make sure user_id unique @@ -392,13 +423,12 @@ defmodule GroupherServer.CMS.Delegate.ArticleCURD do end # for create artilce step in Multi.new - defp do_create_article(model, attrs, %Author{id: author_id}, %Community{id: community_id}) do - # special article like Repo do not have :body, assign it with default-empty rich text - body = Map.get(attrs, :body, Converter.Article.default_rich_text()) + defp do_create_article(model, %{body: _body} = attrs, %Author{id: author_id}, %Community{ + id: community_id + }) do meta = @default_article_meta |> Map.merge(%{thread: module_to_upcase(model)}) - attrs = attrs |> Map.merge(%{body: body}) - with {:ok, attrs} <- add_rich_text_attrs(attrs) do + with {:ok, attrs} <- add_digest_attrs(attrs) do model.__struct__ |> model.changeset(attrs) |> Ecto.Changeset.put_change(:emotions, @default_emotions) @@ -409,8 +439,18 @@ defmodule GroupherServer.CMS.Delegate.ArticleCURD do end end + # Github Repo 没有传统的 body, 需要特殊处理 + # 赋值一个空的 body, 后续在 document 中处理 + # 注意:digest 那里也要特殊处理 + defp do_create_article(CMSRepo, attrs, author, community) do + body = Map.get(attrs, :body, Converter.Article.default_rich_text()) + attrs = Map.put(attrs, :body, body) + + do_create_article(CMSRepo, attrs, author, community) + end + defp do_update_article(article, %{body: _} = attrs) do - with {:ok, attrs} <- add_rich_text_attrs(attrs) do + with {:ok, attrs} <- add_digest_attrs(attrs) do ORM.update(article, attrs) end end @@ -418,17 +458,16 @@ defmodule GroupherServer.CMS.Delegate.ArticleCURD do defp do_update_article(article, attrs), do: ORM.update(article, attrs) # is update or create article with body field, parsed and extand it into attrs - defp add_rich_text_attrs(%{body: body} = attrs) when not is_nil(body) do + defp add_digest_attrs(%{body: body} = attrs) when not is_nil(body) do with {:ok, parsed} <- Converter.Article.parse_body(body), {:ok, digest} <- Converter.Article.parse_digest(parsed.body_map) do attrs - |> Map.merge(Map.take(parsed, [:body, :body_html])) |> Map.merge(%{digest: digest}) |> done end end - defp add_rich_text_attrs(attrs), do: attrs + defp add_digest_attrs(attrs), do: attrs defp update_viewed_user_list(%{meta: nil} = article, user_id) do new_ids = Enum.uniq([user_id] ++ @default_article_meta.viewed_user_ids) @@ -458,6 +497,7 @@ defmodule GroupherServer.CMS.Delegate.ArticleCURD do defp result({:ok, %{update_edit_status: result}}), do: {:ok, result} defp result({:ok, %{update_article: result}}), do: {:ok, result} + defp result({:ok, %{remove_article: result}}), do: {:ok, result} # NOTE: for read article, order is import defp result({:ok, %{set_viewer_has_states: result}}), do: result |> done() defp result({:ok, %{update_article_meta: result}}), do: {:ok, result} diff --git a/lib/groupher_server/cms/delegates/document.ex b/lib/groupher_server/cms/delegates/document.ex new file mode 100644 index 000000000..feada3a07 --- /dev/null +++ b/lib/groupher_server/cms/delegates/document.ex @@ -0,0 +1,124 @@ +defmodule GroupherServer.CMS.Delegate.Document do + @moduledoc """ + CURD operation on post/job ... + """ + import Ecto.Query, warn: false + import Helper.Utils, only: [done: 1, thread_of_article: 2, get_config: 2] + + import Helper.ErrorCode + import ShortMaps + + alias Helper.{ORM, Converter} + alias GroupherServer.{CMS, Repo} + + alias CMS.Model.ArticleDocument + alias Ecto.Multi + + # alias Helper.Converter.MdToEditor + alias GroupherServer.Support.Factory + + # TODO: spec repo logic + def create(article, %{readme: readme} = attrs) do + # .parse(markdown) + # body = MdToEditor.mock_rich_text(readme) + body = Factory.mock_rich_text(readme) + attrs = attrs |> Map.drop([:readme]) |> Map.put(:body, body) + create(article, attrs) + end + + # for create artilce step in Multi.new + def create(article, %{body: body} = attrs) do + with {:ok, article_thread} <- thread_of_article(article, :upcase), + {:ok, parsed} <- Converter.Article.parse_body(body) do + attrs = Map.take(parsed, [:body, :body_html]) + + Multi.new() + |> Multi.run(:create_article_document, fn _, _ -> + document_attrs = + Map.merge(attrs, %{ + thread: article_thread, + article_id: article.id, + title: article.title + }) + + ArticleDocument |> ORM.create(document_attrs) + end) + |> Multi.run(:create_thread_document, fn _, _ -> + attrs = attrs |> Map.put(foreign_key(article_thread), article.id) + + CMS.Model + |> Module.concat("#{Recase.to_title(article_thread)}Document") + |> ORM.create(attrs) + end) + |> Repo.transaction() + |> result() + end + end + + @doc """ + update both article and thread document + """ + def update(article, %{body: body} = attrs) when not is_nil(body) do + with {:ok, article_thread} <- thread_of_article(article, :upcase), + {:ok, article_doc} <- find_article_document(article_thread, article), + {:ok, thread_doc} <- find_thread_document(article_thread, article), + {:ok, parsed} <- Converter.Article.parse_body(body) do + attrs = Map.take(parsed, [:body, :body_html]) + + Multi.new() + |> Multi.run(:update_article_document, fn _, _ -> + case Map.has_key?(attrs, :title) do + true -> article_doc |> ORM.update(Map.merge(attrs, %{title: attrs.title})) + false -> article_doc |> ORM.update(attrs) + end + end) + |> Multi.run(:update_thread_document, fn _, _ -> + thread_doc |> ORM.update(attrs) + end) + |> Repo.transaction() + |> result() + end + end + + # 只更新 title 的情况 + def update(article, %{title: title} = attrs) do + with {:ok, article_thread} <- thread_of_article(article, :upcase), + {:ok, article_doc} <- find_article_document(article_thread, article) do + article_doc |> ORM.update(%{title: attrs.title}) + end + end + + def update(article, _), do: {:ok, article} + + defp find_article_document(article_thread, article) do + ORM.find_by(ArticleDocument, %{article_id: article.id, thread: article_thread}) + end + + defp find_thread_document(article_thread, article) do + CMS.Model + |> Module.concat("#{Recase.to_title(article_thread)}Document") + |> ORM.find_by(Map.put(%{}, foreign_key(article_thread), article.id)) + end + + @doc """ + remove article document foever + """ + def remove(thread, id) do + thread = thread |> to_string |> String.upcase() + + ArticleDocument |> ORM.findby_delete!(%{thread: thread, article_id: id}) + end + + defp foreign_key(article_thread) do + thread_atom = article_thread |> String.downcase() |> String.to_atom() + + :"#{thread_atom}_id" + end + + defp result({:ok, %{create_thread_document: result}}), do: {:ok, result} + defp result({:ok, %{update_article_document: result}}), do: {:ok, result} + + defp result({:error, _, _result, _steps}) do + {:error, [message: "create document", code: ecode(:create_fails)]} + end +end diff --git a/lib/groupher_server/cms/delegates/hooks/cite.ex b/lib/groupher_server/cms/delegates/hooks/cite.ex index 7f97b8375..d836c45bd 100644 --- a/lib/groupher_server/cms/delegates/hooks/cite.ex +++ b/lib/groupher_server/cms/delegates/hooks/cite.ex @@ -65,6 +65,12 @@ defmodule GroupherServer.CMS.Delegate.Hooks.Cite do end end + def handle(%{document: document} = article) do + body = Repo.preload(article, :document) |> get_in([:document, :body]) + article = article |> Map.put(:body, body) + handle(article) + end + @doc """ return fmt like: [ diff --git a/lib/groupher_server/cms/delegates/hooks/mention.ex b/lib/groupher_server/cms/delegates/hooks/mention.ex index f48d66fa5..5b49dea64 100644 --- a/lib/groupher_server/cms/delegates/hooks/mention.ex +++ b/lib/groupher_server/cms/delegates/hooks/mention.ex @@ -27,6 +27,12 @@ defmodule GroupherServer.CMS.Delegate.Hooks.Mention do end end + def handle(%{document: document} = article) do + body = Repo.preload(article, :document) |> get_in([:document, :body]) + article = article |> Map.put(:body, body) + handle(article) + end + defp handle_mentions(mentions, artiment) do with {:ok, author} <- author_of(artiment) do Delivery.send(:mention, artiment, mentions, author) diff --git a/lib/groupher_server/cms/helper/macros.ex b/lib/groupher_server/cms/helper/macros.ex index 61a8e95d0..03788d089 100644 --- a/lib/groupher_server/cms/helper/macros.ex +++ b/lib/groupher_server/cms/helper/macros.ex @@ -114,10 +114,8 @@ defmodule GroupherServer.CMS.Helper.Macros do common casting fields for general_article_fields """ - def general_article_fields(:cast) do + def general_article_cast_fields() do [ - :body, - :body_html, :digest, :original_community_id, :comments_count, @@ -176,19 +174,22 @@ defmodule GroupherServer.CMS.Helper.Macros do add(:[article]_id, references(:cms_[article]s, on_delete: :delete_all)) """ - defmacro general_article_fields do + defmacro general_article_fields(thread) do quote do field(:title, :string) - field(:body, :string) - field(:body_html, :string) field(:digest, :string) - belongs_to(:author, Author) - field(:views, :integer, default: 0) field(:is_pinned, :boolean, default: false, virtual: true) field(:mark_delete, :boolean, default: false) + belongs_to(:author, Author) + + has_one( + :document, + unquote(Module.concat(CMS.Model, "#{Recase.to_title(to_string(thread))}Document")) + ) + embeds_one(:meta, Embeds.ArticleMeta, on_replace: :update) embeds_one(:emotions, Embeds.ArticleEmotion, on_replace: :update) diff --git a/lib/groupher_server/cms/models/article_document.ex b/lib/groupher_server/cms/models/article_document.ex new file mode 100644 index 000000000..167c6bd15 --- /dev/null +++ b/lib/groupher_server/cms/models/article_document.ex @@ -0,0 +1,53 @@ +defmodule GroupherServer.CMS.Model.ArticleDocument do + @moduledoc """ + mainly for full-text search + """ + alias __MODULE__ + + use Ecto.Schema + use Accessible + + import Ecto.Changeset + import GroupherServer.CMS.Helper.Macros + import Helper.Utils, only: [get_config: 2] + + alias GroupherServer.CMS + alias CMS.Model.Embeds + + alias Helper.HTML + + @timestamps_opts [type: :utc_datetime_usec] + + @max_body_length get_config(:article, :max_length) + @min_body_length get_config(:article, :min_length) + + @required_fields ~w(thread title article_id body body_html)a + @optional_fields [] + + @type t :: %ArticleDocument{} + schema "article_documents" do + field(:thread, :string) + field(:title, :string) + field(:article_id, :id) + field(:body, :string) + field(:body_html, :string) + # TODO: 分词数据 + + timestamps() + end + + @doc false + def changeset(%ArticleDocument{} = doc, attrs) do + doc + |> cast(attrs, @optional_fields ++ @required_fields) + |> validate_required(@required_fields) + |> validate_length(:body, min: @min_body_length, max: @max_body_length) + end + + @doc false + def update_changeset(%ArticleDocument{} = doc, attrs) do + doc + |> cast(attrs, @optional_fields ++ @required_fields) + |> validate_length(:body, min: @min_body_length, max: @max_body_length) + end +end diff --git a/lib/groupher_server/cms/models/blog.ex b/lib/groupher_server/cms/models/blog.ex index 442fc5ac2..e7520eb26 100644 --- a/lib/groupher_server/cms/models/blog.ex +++ b/lib/groupher_server/cms/models/blog.ex @@ -14,7 +14,7 @@ defmodule GroupherServer.CMS.Model.Blog do @timestamps_opts [type: :utc_datetime_usec] @required_fields ~w(title digest)a - @article_cast_fields general_article_fields(:cast) + @article_cast_fields general_article_cast_fields() @optional_fields ~w(link_addr digest length)a ++ @article_cast_fields @type t :: %Blog{} @@ -24,7 +24,7 @@ defmodule GroupherServer.CMS.Model.Blog do article_tags_field(:blog) article_communities_field(:blog) - general_article_fields() + general_article_fields(:blog) end @doc false diff --git a/lib/groupher_server/cms/models/blog_document.ex b/lib/groupher_server/cms/models/blog_document.ex new file mode 100644 index 000000000..6e3bdb356 --- /dev/null +++ b/lib/groupher_server/cms/models/blog_document.ex @@ -0,0 +1,47 @@ +defmodule GroupherServer.CMS.Model.BlogDocument do + @moduledoc """ + mainly for full-text search + """ + alias __MODULE__ + + use Ecto.Schema + use Accessible + + import Ecto.Changeset + import Helper.Utils, only: [get_config: 2] + + alias GroupherServer.CMS + alias CMS.Model.{Embeds, Blog} + + @timestamps_opts [type: :utc_datetime_usec] + + @max_body_length get_config(:article, :max_length) + @min_body_length get_config(:article, :min_length) + + @required_fields ~w(body body_html blog_id)a + @optional_fields [] + + @type t :: %BlogDocument{} + schema "blog_documents" do + belongs_to(:blog, Blog, foreign_key: :blog_id) + + field(:body, :string) + field(:body_html, :string) + field(:toc, :map) + end + + @doc false + def changeset(%BlogDocument{} = blog, attrs) do + blog + |> cast(attrs, @optional_fields ++ @required_fields) + |> validate_required(@required_fields) + |> validate_length(:body, min: @min_body_length, max: @max_body_length) + end + + @doc false + def update_changeset(%BlogDocument{} = blog, attrs) do + blog + |> cast(attrs, @optional_fields ++ @required_fields) + |> validate_length(:body, min: @min_body_length, max: @max_body_length) + end +end diff --git a/lib/groupher_server/cms/models/cited_artiment.ex b/lib/groupher_server/cms/models/cited_artiment.ex index 9d11232bb..85422a665 100644 --- a/lib/groupher_server/cms/models/cited_artiment.ex +++ b/lib/groupher_server/cms/models/cited_artiment.ex @@ -16,7 +16,7 @@ defmodule GroupherServer.CMS.Model.CitedArtiment do @timestamps_opts [type: :utc_datetime] @required_fields ~w(cited_by_type cited_by_id user_id)a - @article_cast_fields general_article_fields(:cast) + @article_cast_fields general_article_cast_fields() @optional_fields ~w(comment_id block_linker)a ++ @article_cast_fields @type t :: %CitedArtiment{} diff --git a/lib/groupher_server/cms/models/job.ex b/lib/groupher_server/cms/models/job.ex index 20ab33218..142a3ffb9 100644 --- a/lib/groupher_server/cms/models/job.ex +++ b/lib/groupher_server/cms/models/job.ex @@ -10,11 +10,10 @@ defmodule GroupherServer.CMS.Model.Job do alias GroupherServer.CMS alias CMS.Model.Embeds - alias Helper.HTML @timestamps_opts [type: :utc_datetime_usec] - @required_fields ~w(title company body digest length)a - @article_cast_fields general_article_fields(:cast) + @required_fields ~w(title company digest length)a + @article_cast_fields general_article_cast_fields() @optional_fields @article_cast_fields ++ ~w(desc company_link link_addr copy_right)a @type t :: %Job{} @@ -29,7 +28,7 @@ defmodule GroupherServer.CMS.Model.Job do article_tags_field(:job) article_communities_field(:job) - general_article_fields() + general_article_fields(:job) end @doc false @@ -52,8 +51,5 @@ defmodule GroupherServer.CMS.Model.Job do defp generl_changeset(content) do content |> validate_length(:title, min: 3, max: 50) - |> validate_length(:body, min: 3, max: 10_000) - # |> cast_embed(:emotions, with: &Embeds.ArticleEmotion.changeset/2) - |> HTML.safe_string(:body) end end diff --git a/lib/groupher_server/cms/models/job_document.ex b/lib/groupher_server/cms/models/job_document.ex new file mode 100644 index 000000000..7f1fab644 --- /dev/null +++ b/lib/groupher_server/cms/models/job_document.ex @@ -0,0 +1,47 @@ +defmodule GroupherServer.CMS.Model.JobDocument do + @moduledoc """ + mainly for full-text search + """ + alias __MODULE__ + + use Ecto.Schema + use Accessible + + import Ecto.Changeset + import Helper.Utils, only: [get_config: 2] + + alias GroupherServer.CMS + alias CMS.Model.{Embeds, Job} + + @timestamps_opts [type: :utc_datetime_usec] + + @max_body_length get_config(:article, :max_length) + @min_body_length get_config(:article, :min_length) + + @required_fields ~w(body body_html job_id)a + @optional_fields [] + + @type t :: %JobDocument{} + schema "job_documents" do + belongs_to(:job, Job, foreign_key: :job_id) + + field(:body, :string) + field(:body_html, :string) + field(:toc, :map) + end + + @doc false + def changeset(%JobDocument{} = job, attrs) do + job + |> cast(attrs, @optional_fields ++ @required_fields) + |> validate_required(@required_fields) + |> validate_length(:body, min: @min_body_length, max: @max_body_length) + end + + @doc false + def update_changeset(%JobDocument{} = job, attrs) do + job + |> cast(attrs, @optional_fields ++ @required_fields) + |> validate_length(:body, min: @min_body_length, max: @max_body_length) + end +end diff --git a/lib/groupher_server/cms/models/post.ex b/lib/groupher_server/cms/models/post.ex index ff92b9c96..6bdd15a07 100644 --- a/lib/groupher_server/cms/models/post.ex +++ b/lib/groupher_server/cms/models/post.ex @@ -15,8 +15,8 @@ defmodule GroupherServer.CMS.Model.Post do @timestamps_opts [type: :utc_datetime_usec] - @required_fields ~w(title body digest length)a - @article_cast_fields general_article_fields(:cast) + @required_fields ~w(title digest length)a + @article_cast_fields general_article_cast_fields() @optional_fields ~w(link_addr copy_right is_question is_solved solution_digest)a ++ @article_cast_fields @@ -30,13 +30,9 @@ defmodule GroupherServer.CMS.Model.Post do field(:is_solved, :boolean, default: false) field(:solution_digest, :string) - # TODO: move to general_article_fields - # embeds_one(:block_task_runner, Embeds.BlockTaskRunner, on_replace: :update) - # embeds_many(:citing_contents, CMS.CitedArtiment, on_replace: :delete) - article_tags_field(:post) article_communities_field(:post) - general_article_fields() + general_article_fields(:post) end @doc false @@ -59,7 +55,6 @@ defmodule GroupherServer.CMS.Model.Post do changeset |> validate_length(:title, min: 3, max: 50) |> cast_embed(:emotions, with: &Embeds.ArticleEmotion.changeset/2) - |> validate_length(:body, min: 3, max: 10_000) |> validate_length(:link_addr, min: 5, max: 400) |> HTML.safe_string(:body) end diff --git a/lib/groupher_server/cms/models/post_document.ex b/lib/groupher_server/cms/models/post_document.ex new file mode 100644 index 000000000..b7e7bec9b --- /dev/null +++ b/lib/groupher_server/cms/models/post_document.ex @@ -0,0 +1,47 @@ +defmodule GroupherServer.CMS.Model.PostDocument do + @moduledoc """ + mainly for full-text search + """ + alias __MODULE__ + + use Ecto.Schema + use Accessible + + import Ecto.Changeset + import Helper.Utils, only: [get_config: 2] + + alias GroupherServer.CMS + alias CMS.Model.{Embeds, Post} + + @timestamps_opts [type: :utc_datetime_usec] + + @max_body_length get_config(:article, :max_length) + @min_body_length get_config(:article, :min_length) + + @required_fields ~w(body body_html post_id)a + @optional_fields [] + + @type t :: %PostDocument{} + schema "post_documents" do + belongs_to(:post, Post, foreign_key: :post_id) + + field(:body, :string) + field(:body_html, :string) + field(:toc, :map) + end + + @doc false + def changeset(%PostDocument{} = post, attrs) do + post + |> cast(attrs, @optional_fields ++ @required_fields) + |> validate_required(@required_fields) + |> validate_length(:body, min: @min_body_length, max: @max_body_length) + end + + @doc false + def update_changeset(%PostDocument{} = post, attrs) do + post + |> cast(attrs, @optional_fields ++ @required_fields) + |> validate_length(:body, min: @min_body_length, max: @max_body_length) + end +end diff --git a/lib/groupher_server/cms/models/repo.ex b/lib/groupher_server/cms/models/repo.ex index 8d992ebd8..8333d2562 100644 --- a/lib/groupher_server/cms/models/repo.ex +++ b/lib/groupher_server/cms/models/repo.ex @@ -15,7 +15,7 @@ defmodule GroupherServer.CMS.Model.Repo do @timestamps_opts [type: :utc_datetime_usec] @required_fields ~w(title owner_name owner_url repo_url desc readme star_count issues_count prs_count fork_count watch_count)a - @article_cast_fields general_article_fields(:cast) + @article_cast_fields general_article_cast_fields() @optional_fields @article_cast_fields ++ ~w(last_sync homepage_url release_tag license)a @type t :: %Repo{} @@ -42,7 +42,7 @@ defmodule GroupherServer.CMS.Model.Repo do article_tags_field(:repo) article_communities_field(:repo) - general_article_fields() + general_article_fields(:repo) end @doc false diff --git a/lib/groupher_server/cms/models/repo_document.ex b/lib/groupher_server/cms/models/repo_document.ex new file mode 100644 index 000000000..5d08129e9 --- /dev/null +++ b/lib/groupher_server/cms/models/repo_document.ex @@ -0,0 +1,41 @@ +defmodule GroupherServer.CMS.Model.RepoDocument do + @moduledoc """ + mainly for full-text search + """ + alias __MODULE__ + + use Ecto.Schema + use Accessible + + import Ecto.Changeset + + alias GroupherServer.CMS + alias CMS.Model.{Embeds, Repo} + + @timestamps_opts [type: :utc_datetime_usec] + + @required_fields ~w(body body_html repo_id)a + @optional_fields [] + + @type t :: %RepoDocument{} + schema "repo_documents" do + belongs_to(:repo, Repo, foreign_key: :repo_id) + + field(:body, :string) + field(:body_html, :string) + field(:toc, :map) + end + + @doc false + def changeset(%RepoDocument{} = repo, attrs) do + repo + |> cast(attrs, @optional_fields ++ @required_fields) + |> validate_required(@required_fields) + end + + @doc false + def update_changeset(%RepoDocument{} = repo, attrs) do + repo + |> cast(attrs, @optional_fields ++ @required_fields) + end +end diff --git a/lib/groupher_server_web/schema/Helper/fields.ex b/lib/groupher_server_web/schema/Helper/fields.ex index e36af5264..1ba8b9da8 100644 --- a/lib/groupher_server_web/schema/Helper/fields.ex +++ b/lib/groupher_server_web/schema/Helper/fields.ex @@ -1,6 +1,6 @@ defmodule GroupherServerWeb.Schema.Helper.Fields do @moduledoc """ - general fields used in schema definition + general fields used in GraphQL schema definition """ import Helper.Utils, only: [get_config: 2] import Absinthe.Resolution.Helpers, only: [dataloader: 2] @@ -18,8 +18,7 @@ defmodule GroupherServerWeb.Schema.Helper.Fields do quote do field(:id, :id) field(:title, :string) - field(:body, :string) - field(:body_html, :string) + field(:document, :thread_document, resolve: dataloader(CMS, :document)) field(:digest, :string) field(:views, :integer) field(:is_pinned, :boolean) @@ -112,7 +111,7 @@ defmodule GroupherServerWeb.Schema.Helper.Fields do "e do field(unquote(:"#{&1}_count"), :integer) field(unquote(:"viewer_has_#{&1}ed"), :boolean) - field(unquote(:"latest_#{&1}_users"), list_of(:simple_user)) + field(unquote(:"latest_#{&1}_users"), list_of(:common_user)) end ) end @@ -123,7 +122,7 @@ defmodule GroupherServerWeb.Schema.Helper.Fields do "e do field(unquote(:"#{&1}_count"), :integer) field(unquote(:"viewer_has_#{&1}ed"), :boolean) - field(unquote(:"latest_#{&1}_users"), list_of(:simple_user)) + field(unquote(:"latest_#{&1}_users"), list_of(:common_user)) end ) end diff --git a/lib/groupher_server_web/schema/cms/cms_metrics.ex b/lib/groupher_server_web/schema/cms/cms_metrics.ex index 5257fc809..1d072749e 100644 --- a/lib/groupher_server_web/schema/cms/cms_metrics.ex +++ b/lib/groupher_server_web/schema/cms/cms_metrics.ex @@ -191,6 +191,12 @@ defmodule GroupherServerWeb.Schema.CMS.Metrics do field(:sort, :sort_enum) end + input_object :paged_blogs_filter do + pagination_args() + article_filter_fields() + field(:sort, :sort_enum) + end + @desc "article_filter doc" input_object :paged_repos_filter do @desc "limit of records (default 20), if first > 30, only return 30 at most" diff --git a/lib/groupher_server_web/schema/cms/cms_queries.ex b/lib/groupher_server_web/schema/cms/cms_queries.ex index c45be857a..394c440bd 100644 --- a/lib/groupher_server_web/schema/cms/cms_queries.ex +++ b/lib/groupher_server_web/schema/cms/cms_queries.ex @@ -147,6 +147,7 @@ defmodule GroupherServerWeb.Schema.CMS.Queries do article_queries(:post) article_queries(:job) + article_queries(:blog) article_queries(:repo) end end diff --git a/lib/groupher_server_web/schema/cms/cms_types.ex b/lib/groupher_server_web/schema/cms/cms_types.ex index 4e774fdfd..350c91b47 100644 --- a/lib/groupher_server_web/schema/cms/cms_types.ex +++ b/lib/groupher_server_web/schema/cms/cms_types.ex @@ -46,9 +46,9 @@ defmodule GroupherServerWeb.Schema.CMS.Types do field(:id, :id) end - object :simple_user do - field(:login, :string) - field(:nickname, :string) + object :thread_document do + field(:body, :string) + field(:body_html, :string) end object :post do diff --git a/priv/repo/migrations/20210623093412_creaet_article_documents.exs b/priv/repo/migrations/20210623093412_creaet_article_documents.exs new file mode 100644 index 000000000..a53b23047 --- /dev/null +++ b/priv/repo/migrations/20210623093412_creaet_article_documents.exs @@ -0,0 +1,15 @@ +defmodule GroupherServer.Repo.Migrations.CreaetArticleDocuments do + use Ecto.Migration + + def change do + create table(:article_documents) do + add(:thread, :string) + add(:article_id, :id) + add(:title, :string) + add(:body, :text) + add(:body_html, :text) + + timestamps() + end + end +end diff --git a/priv/repo/migrations/20210623094741_creaet_domain_article_documents.exs b/priv/repo/migrations/20210623094741_creaet_domain_article_documents.exs new file mode 100644 index 000000000..ec9c8df14 --- /dev/null +++ b/priv/repo/migrations/20210623094741_creaet_domain_article_documents.exs @@ -0,0 +1,37 @@ +defmodule GroupherServer.Repo.Migrations.CreaetDomainArticleDocuments do + use Ecto.Migration + + def change do + create table(:post_documents) do + add(:post_id, references(:cms_posts, on_delete: :delete_all), null: false) + add(:body, :text) + add(:body_html, :text) + add(:markdown, :text) + add(:toc, :map) + end + + create table(:job_documents) do + add(:job_id, references(:cms_jobs, on_delete: :delete_all), null: false) + add(:body, :text) + add(:body_html, :text) + add(:markdown, :text) + add(:toc, :map) + end + + create table(:repo_documents) do + add(:repo_id, references(:cms_repos, on_delete: :delete_all), null: false) + add(:body, :text) + add(:body_html, :text) + add(:markdown, :text) + add(:toc, :map) + end + + create table(:blog_documents) do + add(:blog_id, references(:cms_blogs, on_delete: :delete_all), null: false) + add(:body, :text) + add(:body_html, :text) + add(:markdown, :text) + add(:toc, :map) + end + end +end diff --git a/priv/repo/migrations/20210623123527_remove_body_html_in_articles.exs b/priv/repo/migrations/20210623123527_remove_body_html_in_articles.exs new file mode 100644 index 000000000..48c37ec2c --- /dev/null +++ b/priv/repo/migrations/20210623123527_remove_body_html_in_articles.exs @@ -0,0 +1,10 @@ +defmodule GroupherServer.Repo.Migrations.RemoveBodyHtmlInArticles do + use Ecto.Migration + + def change do + alter(table(:cms_posts), do: remove(:body_html)) + alter(table(:cms_jobs), do: remove(:body_html)) + alter(table(:cms_blogs), do: remove(:body_html)) + alter(table(:cms_repos), do: remove(:body_html)) + end +end diff --git a/test/groupher_server/cms/articles/blog_test.exs b/test/groupher_server/cms/articles/blog_test.exs index 2e1e00deb..83f97a162 100644 --- a/test/groupher_server/cms/articles/blog_test.exs +++ b/test/groupher_server/cms/articles/blog_test.exs @@ -1,11 +1,11 @@ defmodule GroupherServer.Test.Articles.Blog do use GroupherServer.TestTools - alias GroupherServer.CMS + alias GroupherServer.{CMS, Repo} alias Helper.Converter.{EditorToHTML, HtmlSanitizer} alias EditorToHTML.{Class, Validator} - alias CMS.Model.{Author, Blog, Community} + alias CMS.Model.{Author, Blog, Community, ArticleDocument, BlogDocument} alias Helper.ORM @root_class Class.article() @@ -25,15 +25,19 @@ defmodule GroupherServer.Test.Articles.Blog do test "can create blog with valid attrs", ~m(user community blog_attrs)a do assert {:error, _} = ORM.find_by(Author, user_id: user.id) {:ok, blog} = CMS.create_article(community, :blog, blog_attrs, user) + blog = Repo.preload(blog, :document) - body_map = Jason.decode!(blog.body) + body_map = Jason.decode!(blog.document.body) assert blog.meta.thread == "BLOG" assert blog.title == blog_attrs.title assert body_map |> Validator.is_valid() - assert blog.body_html |> String.contains?(~s(
)) - assert blog.body_html |> String.contains?(~s(

)) + + assert blog.document.body_html |> String.contains?(~s(

List.first() |> get_in(["data", "text"]) assert blog.digest == paragraph_text |> HtmlSanitizer.strip_all_tags() @@ -148,4 +152,45 @@ defmodule GroupherServer.Test.Articles.Blog do is_error?(reason, :undo_sink_old_article) end end + + describe "[cms blog document]" do + test "will create related document after create", ~m(user community blog_attrs)a do + {:ok, blog} = CMS.create_article(community, :blog, blog_attrs, user) + {:ok, blog} = CMS.read_article(:blog, blog.id) + assert not is_nil(blog.document.body_html) + {:ok, blog} = CMS.read_article(:blog, blog.id, user) + assert not is_nil(blog.document.body_html) + + {:ok, article_doc} = ORM.find_by(ArticleDocument, %{article_id: blog.id, thread: "BLOG"}) + {:ok, blog_doc} = ORM.find_by(BlogDocument, %{blog_id: blog.id}) + + assert blog.document.body == blog_doc.body + assert article_doc.body == blog_doc.body + end + + test "delete blog should also delete related document", ~m(user community blog_attrs)a do + {:ok, blog} = CMS.create_article(community, :blog, blog_attrs, user) + {:ok, _article_doc} = ORM.find_by(ArticleDocument, %{article_id: blog.id, thread: "BLOG"}) + {:ok, _blog_doc} = ORM.find_by(BlogDocument, %{blog_id: blog.id}) + + CMS.remove_article(:blog, blog.id) + + {:error, _} = ORM.find(Blog, blog.id) + {:error, _} = ORM.find_by(ArticleDocument, %{article_id: blog.id, thread: "BLOG"}) + {:error, _} = ORM.find_by(BlogDocument, %{blog_id: blog.id}) + end + + test "update blog should also update related document", ~m(user community blog_attrs)a do + {:ok, blog} = CMS.create_article(community, :blog, blog_attrs, user) + + body = mock_rich_text(~s(new content)) + {:ok, blog} = CMS.update_article(blog, %{body: body}) + + {:ok, article_doc} = ORM.find_by(ArticleDocument, %{article_id: blog.id, thread: "BLOG"}) + {:ok, blog_doc} = ORM.find_by(BlogDocument, %{blog_id: blog.id}) + + assert String.contains?(blog_doc.body, "new content") + assert String.contains?(article_doc.body, "new content") + end + end end diff --git a/test/groupher_server/cms/articles/job_test.exs b/test/groupher_server/cms/articles/job_test.exs index d653a0644..8769c6cd9 100644 --- a/test/groupher_server/cms/articles/job_test.exs +++ b/test/groupher_server/cms/articles/job_test.exs @@ -1,11 +1,11 @@ defmodule GroupherServer.Test.Articles.Job do use GroupherServer.TestTools - alias GroupherServer.CMS + alias GroupherServer.{CMS, Repo} alias Helper.Converter.{EditorToHTML, HtmlSanitizer} alias EditorToHTML.{Class, Validator} - alias CMS.Model.{Author, Job, Community} + alias CMS.Model.{Author, Job, Community, ArticleDocument, JobDocument} alias Helper.ORM @@ -26,15 +26,19 @@ defmodule GroupherServer.Test.Articles.Job do test "can create job with valid attrs", ~m(user community job_attrs)a do assert {:error, _} = ORM.find_by(Author, user_id: user.id) {:ok, job} = CMS.create_article(community, :job, job_attrs, user) + job = Repo.preload(job, :document) - body_map = Jason.decode!(job.body) + body_map = Jason.decode!(job.document.body) assert job.meta.thread == "JOB" assert job.title == job_attrs.title assert body_map |> Validator.is_valid() - assert job.body_html |> String.contains?(~s(

)) - assert job.body_html |> String.contains?(~s(

)) + + assert job.document.body_html |> String.contains?(~s(

List.first() |> get_in(["data", "text"]) assert job.digest == paragraph_text |> HtmlSanitizer.strip_all_tags() @@ -149,4 +153,45 @@ defmodule GroupherServer.Test.Articles.Job do is_error?(reason, :undo_sink_old_article) end end + + describe "[cms job document]" do + test "will create related document after create", ~m(user community job_attrs)a do + {:ok, job} = CMS.create_article(community, :job, job_attrs, user) + {:ok, job} = CMS.read_article(:job, job.id) + assert not is_nil(job.document.body_html) + {:ok, job} = CMS.read_article(:job, job.id, user) + assert not is_nil(job.document.body_html) + + {:ok, article_doc} = ORM.find_by(ArticleDocument, %{article_id: job.id, thread: "JOB"}) + {:ok, job_doc} = ORM.find_by(JobDocument, %{job_id: job.id}) + + assert job.document.body == job_doc.body + assert article_doc.body == job_doc.body + end + + test "delete job should also delete related document", ~m(user community job_attrs)a do + {:ok, job} = CMS.create_article(community, :job, job_attrs, user) + {:ok, _article_doc} = ORM.find_by(ArticleDocument, %{article_id: job.id, thread: "JOB"}) + {:ok, _job_doc} = ORM.find_by(JobDocument, %{job_id: job.id}) + + CMS.remove_article(:job, job.id) + + {:error, _} = ORM.find(Job, job.id) + {:error, _} = ORM.find_by(ArticleDocument, %{article_id: job.id, thread: "JOB"}) + {:error, _} = ORM.find_by(JobDocument, %{job_id: job.id}) + end + + test "update job should also update related document", ~m(user community job_attrs)a do + {:ok, job} = CMS.create_article(community, :job, job_attrs, user) + + body = mock_rich_text(~s(new content)) + {:ok, job} = CMS.update_article(job, %{body: body}) + + {:ok, article_doc} = ORM.find_by(ArticleDocument, %{article_id: job.id, thread: "JOB"}) + {:ok, job_doc} = ORM.find_by(JobDocument, %{job_id: job.id}) + + assert String.contains?(job_doc.body, "new content") + assert String.contains?(article_doc.body, "new content") + end + end end diff --git a/test/groupher_server/cms/articles/post_test.exs b/test/groupher_server/cms/articles/post_test.exs index 2e71f92c5..9d98efc3a 100644 --- a/test/groupher_server/cms/articles/post_test.exs +++ b/test/groupher_server/cms/articles/post_test.exs @@ -2,11 +2,11 @@ defmodule GroupherServer.Test.CMS.Articles.Post do use GroupherServer.TestTools alias Helper.ORM - alias GroupherServer.CMS + alias GroupherServer.{CMS, Repo} alias Helper.Converter.{EditorToHTML, HtmlSanitizer} alias EditorToHTML.{Class, Validator} - alias CMS.Model.{Author, Community, Post} + alias CMS.Model.{Author, ArticleDocument, Community, Post, PostDocument} @root_class Class.article() @last_year Timex.shift(Timex.beginning_of_year(Timex.now()), days: -3, seconds: -1) @@ -26,15 +26,19 @@ defmodule GroupherServer.Test.CMS.Articles.Post do test "can create post with valid attrs", ~m(user community post_attrs)a do assert {:error, _} = ORM.find_by(Author, user_id: user.id) {:ok, post} = CMS.create_article(community, :post, post_attrs, user) + post = Repo.preload(post, :document) - body_map = Jason.decode!(post.body) + body_map = Jason.decode!(post.document.body) assert post.meta.thread == "POST" assert post.title == post_attrs.title assert body_map |> Validator.is_valid() - assert post.body_html |> String.contains?(~s(

)) - assert post.body_html |> String.contains?(~s(

)) + + assert post.document.body_html |> String.contains?(~s(

List.first() |> get_in(["data", "text"]) assert post.digest == paragraph_text |> HtmlSanitizer.strip_all_tags() @@ -183,4 +187,45 @@ defmodule GroupherServer.Test.CMS.Articles.Post do assert not post.is_question end end + + describe "[cms post document]" do + test "will create related document after create", ~m(user community post_attrs)a do + {:ok, post} = CMS.create_article(community, :post, post_attrs, user) + {:ok, post} = CMS.read_article(:post, post.id) + assert not is_nil(post.document.body_html) + {:ok, post} = CMS.read_article(:post, post.id, user) + assert not is_nil(post.document.body_html) + + {:ok, article_doc} = ORM.find_by(ArticleDocument, %{article_id: post.id, thread: "POST"}) + {:ok, post_doc} = ORM.find_by(PostDocument, %{post_id: post.id}) + + assert post.document.body == post_doc.body + assert article_doc.body == post_doc.body + end + + test "delete post should also delete related document", ~m(user community post_attrs)a do + {:ok, post} = CMS.create_article(community, :post, post_attrs, user) + {:ok, _article_doc} = ORM.find_by(ArticleDocument, %{article_id: post.id, thread: "POST"}) + {:ok, _post_doc} = ORM.find_by(PostDocument, %{post_id: post.id}) + + CMS.remove_article(:post, post.id) + + {:error, _} = ORM.find(Post, post.id) + {:error, _} = ORM.find_by(ArticleDocument, %{article_id: post.id, thread: "POST"}) + {:error, _} = ORM.find_by(PostDocument, %{post_id: post.id}) + end + + test "update post should also update related document", ~m(user community post_attrs)a do + {:ok, post} = CMS.create_article(community, :post, post_attrs, user) + + body = mock_rich_text(~s(new content)) + {:ok, post} = CMS.update_article(post, %{body: body}) + + {:ok, article_doc} = ORM.find_by(ArticleDocument, %{article_id: post.id, thread: "POST"}) + {:ok, post_doc} = ORM.find_by(PostDocument, %{post_id: post.id}) + + assert String.contains?(post_doc.body, "new content") + assert String.contains?(article_doc.body, "new content") + end + end end diff --git a/test/groupher_server/cms/articles/repo_test.exs b/test/groupher_server/cms/articles/repo_test.exs index fb5f006ff..e2439c285 100644 --- a/test/groupher_server/cms/articles/repo_test.exs +++ b/test/groupher_server/cms/articles/repo_test.exs @@ -2,6 +2,7 @@ defmodule GroupherServer.Test.Articles.Repo do use GroupherServer.TestTools alias GroupherServer.CMS + alias GroupherServer.Repo, as: GlabelRepo alias Helper.Converter.{EditorToHTML} alias EditorToHTML.{Class, Validator} @@ -26,14 +27,18 @@ defmodule GroupherServer.Test.Articles.Repo do test "can create repo with valid attrs", ~m(user community repo_attrs)a do assert {:error, _} = ORM.find_by(Author, user_id: user.id) {:ok, repo} = CMS.create_article(community, :repo, repo_attrs, user) + repo = GlabelRepo.preload(repo, :document) - body_map = Jason.decode!(repo.body) + body_map = Jason.decode!(repo.document.body) assert repo.meta.thread == "REPO" assert repo.title == repo_attrs.title assert body_map |> Validator.is_valid() - assert repo.body_html |> String.contains?(~s(

)) + + assert repo.document.body_html + |> String.contains?(~s(
)) + assert repo.contributors |> length !== 0 end diff --git a/test/groupher_server_web/mutation/cms/articles/blog_test.exs b/test/groupher_server_web/mutation/cms/articles/blog_test.exs index 94992996b..0e39c4438 100644 --- a/test/groupher_server_web/mutation/cms/articles/blog_test.exs +++ b/test/groupher_server_web/mutation/cms/articles/blog_test.exs @@ -2,15 +2,17 @@ defmodule GroupherServer.Test.Mutation.Articles.Blog do use GroupherServer.TestTools alias Helper.ORM - alias GroupherServer.CMS + alias GroupherServer.{CMS, Repo} alias CMS.Model.Blog setup do - {:ok, blog} = db_insert(:blog) {:ok, user} = db_insert(:user) {:ok, community} = db_insert(:community) + blog_attrs = mock_attrs(:blog, %{community_id: community.id}) + {:ok, blog} = CMS.create_article(community, :blog, blog_attrs, user) + guest_conn = simu_conn(:guest) user_conn = simu_conn(:user) owner_conn = simu_conn(:owner, blog) @@ -38,7 +40,9 @@ defmodule GroupherServer.Test.Mutation.Articles.Blog do ) { id title - body + document { + bodyHtml + } originalCommunity { id } @@ -91,10 +95,12 @@ defmodule GroupherServer.Test.Mutation.Articles.Blog do blog_attr = mock_attrs(:blog, %{body: mock_xss_string()}) variables = blog_attr |> Map.merge(%{communityId: community.id}) |> camelize_map_key - created = user_conn |> mutation_result(@create_blog_query, variables, "createBlog") - {:ok, blog} = ORM.find(Blog, created["id"]) + result = user_conn |> mutation_result(@create_blog_query, variables, "createBlog") - assert not String.contains?(blog.body_html, "script") + {:ok, blog} = ORM.find(Blog, result["id"], preload: :document) + body_html = blog |> get_in([:document, :body_html]) + + assert not String.contains?(body_html, "script") end test "create blog should excape xss attracts 2" do @@ -105,10 +111,11 @@ defmodule GroupherServer.Test.Mutation.Articles.Blog do blog_attr = mock_attrs(:blog, %{body: mock_xss_string(:safe)}) variables = blog_attr |> Map.merge(%{communityId: community.id}) |> camelize_map_key - created = user_conn |> mutation_result(@create_blog_query, variables, "createBlog") - {:ok, blog} = ORM.find(Blog, created["id"]) + result = user_conn |> mutation_result(@create_blog_query, variables, "createBlog") + {:ok, blog} = ORM.find(Blog, result["id"], preload: :document) + body_html = blog |> get_in([:document, :body_html]) - assert String.contains?(blog.body_html, "<script>blackmail</script>") + assert String.contains?(body_html, "<script>blackmail</script>") end @query """ @@ -116,15 +123,15 @@ defmodule GroupherServer.Test.Mutation.Articles.Blog do updateBlog(id: $id, title: $title, body: $body, articleTags: $articleTags) { id title - body - bodyHtml + document { + bodyHtml + } articleTags { id } } } """ - test "update a blog without login user fails", ~m(guest_conn blog)a do unique_num = System.unique_integer([:positive, :monotonic]) @@ -146,13 +153,18 @@ defmodule GroupherServer.Test.Mutation.Articles.Blog do body: mock_rich_text("updated body #{unique_num}") } - updated = owner_conn |> mutation_result(@query, variables, "updateBlog") + result = owner_conn |> mutation_result(@query, variables, "updateBlog") - assert updated["title"] == variables.title - assert updated["bodyHtml"] |> String.contains?(~s(updated body #{unique_num})) + assert result["title"] == variables.title + + assert result + |> get_in(["document", "bodyHtml"]) + |> String.contains?(~s(updated body #{unique_num})) end test "login user with auth passport update a blog", ~m(blog)a do + blog = blog |> Repo.preload(:communities) + blog_communities_0 = blog.communities |> List.first() |> Map.get(:title) passport_rules = %{blog_communities_0 => %{"blog.edit" => true}} rule_conn = simu_conn(:user, cms: passport_rules) @@ -202,6 +214,7 @@ defmodule GroupherServer.Test.Mutation.Articles.Blog do end test "can delete a blog by auth user", ~m(blog)a do + blog = blog |> Repo.preload(:communities) belongs_community_title = blog.communities |> List.first() |> Map.get(:title) rule_conn = simu_conn(:user, cms: %{belongs_community_title => %{"blog.delete" => true}}) diff --git a/test/groupher_server_web/mutation/cms/articles/job_test.exs b/test/groupher_server_web/mutation/cms/articles/job_test.exs index 7a7d7bb6a..c6b8ae025 100644 --- a/test/groupher_server_web/mutation/cms/articles/job_test.exs +++ b/test/groupher_server_web/mutation/cms/articles/job_test.exs @@ -2,15 +2,17 @@ defmodule GroupherServer.Test.Mutation.Articles.Job do use GroupherServer.TestTools alias Helper.ORM - alias GroupherServer.CMS + alias GroupherServer.{CMS, Repo} alias CMS.Model.Job setup do - {:ok, job} = db_insert(:job) {:ok, user} = db_insert(:user) {:ok, community} = db_insert(:community) + job_attrs = mock_attrs(:job, %{community_id: community.id}) + {:ok, job} = CMS.create_article(community, :job, job_attrs, user) + guest_conn = simu_conn(:guest) user_conn = simu_conn(:user) owner_conn = simu_conn(:owner, job) @@ -40,7 +42,9 @@ defmodule GroupherServer.Test.Mutation.Articles.Job do ) { id title - body + document { + bodyHtml + } originalCommunity { id } @@ -93,10 +97,12 @@ defmodule GroupherServer.Test.Mutation.Articles.Job do job_attr = mock_attrs(:job, %{body: mock_xss_string()}) variables = job_attr |> Map.merge(%{communityId: community.id}) |> camelize_map_key - created = user_conn |> mutation_result(@create_job_query, variables, "createJob") - {:ok, job} = ORM.find(Job, created["id"]) + result = user_conn |> mutation_result(@create_job_query, variables, "createJob") + {:ok, job} = ORM.find(Job, result["id"], preload: :document) - assert not String.contains?(job.body_html, "script") + body_html = job |> get_in([:document, :body_html]) + + assert not String.contains?(body_html, "script") end test "create job should excape xss attracts 2" do @@ -107,10 +113,12 @@ defmodule GroupherServer.Test.Mutation.Articles.Job do job_attr = mock_attrs(:job, %{body: mock_xss_string(:safe)}) variables = job_attr |> Map.merge(%{communityId: community.id}) |> camelize_map_key - created = user_conn |> mutation_result(@create_job_query, variables, "createJob") - {:ok, job} = ORM.find(Job, created["id"]) + result = user_conn |> mutation_result(@create_job_query, variables, "createJob") - assert String.contains?(job.body_html, "<script>blackmail</script>") + {:ok, job} = ORM.find(Job, result["id"], preload: :document) + body_html = job |> get_in([:document, :body_html]) + + assert String.contains?(body_html, "<script>blackmail</script>") end @query """ @@ -118,8 +126,9 @@ defmodule GroupherServer.Test.Mutation.Articles.Job do updateJob(id: $id, title: $title, body: $body, articleTags: $articleTags) { id title - body - bodyHtml + document { + bodyHtml + } articleTags { id } @@ -147,13 +156,18 @@ defmodule GroupherServer.Test.Mutation.Articles.Job do body: mock_rich_text("updated body #{unique_num}") } - updated = owner_conn |> mutation_result(@query, variables, "updateJob") + result = owner_conn |> mutation_result(@query, variables, "updateJob") - assert updated["title"] == variables.title - assert updated["bodyHtml"] |> String.contains?(~s(updated body #{unique_num})) + assert result["title"] == variables.title + + assert result + |> get_in(["document", "bodyHtml"]) + |> String.contains?(~s(updated body #{unique_num})) end test "login user with auth passport update a job", ~m(job)a do + job = job |> Repo.preload(:communities) + job_communities_0 = job.communities |> List.first() |> Map.get(:title) passport_rules = %{job_communities_0 => %{"job.edit" => true}} rule_conn = simu_conn(:user, cms: passport_rules) @@ -203,6 +217,7 @@ defmodule GroupherServer.Test.Mutation.Articles.Job do end test "can delete a job by auth user", ~m(job)a do + job = job |> Repo.preload(:communities) belongs_community_title = job.communities |> List.first() |> Map.get(:title) rule_conn = simu_conn(:user, cms: %{belongs_community_title => %{"job.delete" => true}}) diff --git a/test/groupher_server_web/mutation/cms/articles/post_test.exs b/test/groupher_server_web/mutation/cms/articles/post_test.exs index 588c52b98..c25b724cc 100644 --- a/test/groupher_server_web/mutation/cms/articles/post_test.exs +++ b/test/groupher_server_web/mutation/cms/articles/post_test.exs @@ -2,15 +2,17 @@ defmodule GroupherServer.Test.Mutation.Articles.Post do use GroupherServer.TestTools alias Helper.ORM - alias GroupherServer.CMS + alias GroupherServer.{CMS, Repo} alias CMS.Model.{Post, Author} setup do - {:ok, post} = db_insert(:post) {:ok, user} = db_insert(:user) {:ok, community} = db_insert(:community) + post_attrs = mock_attrs(:post, %{community_id: community.id}) + {:ok, post} = CMS.create_article(community, :post, post_attrs, user) + guest_conn = simu_conn(:guest) user_conn = simu_conn(:user) owner_conn = simu_conn(:owner, post) @@ -36,9 +38,11 @@ defmodule GroupherServer.Test.Mutation.Articles.Post do communityId: $communityId articleTags: $articleTags ) { - title - body id + title + document { + bodyHtml + } originalCommunity { id } @@ -85,10 +89,11 @@ defmodule GroupherServer.Test.Mutation.Articles.Post do post_attr = mock_attrs(:post, %{body: mock_xss_string()}) variables = post_attr |> Map.merge(%{communityId: community.id}) |> camelize_map_key - created = user_conn |> mutation_result(@create_post_query, variables, "createPost") - {:ok, post} = ORM.find(Post, created["id"]) + result = user_conn |> mutation_result(@create_post_query, variables, "createPost") + {:ok, post} = ORM.find(Post, result["id"], preload: :document) + body_html = post |> get_in([:document, :body_html]) - assert not String.contains?(post.body_html, "script") + assert not String.contains?(body_html, "script") end test "create post should excape xss attracts 2" do @@ -99,10 +104,11 @@ defmodule GroupherServer.Test.Mutation.Articles.Post do post_attr = mock_attrs(:post, %{body: mock_xss_string(:safe)}) variables = post_attr |> Map.merge(%{communityId: community.id}) |> camelize_map_key - created = user_conn |> mutation_result(@create_post_query, variables, "createPost") - {:ok, post} = ORM.find(Post, created["id"]) + result = user_conn |> mutation_result(@create_post_query, variables, "createPost") + {:ok, post} = ORM.find(Post, result["id"], preload: :document) + body_html = post |> get_in([:document, :body_html]) - assert String.contains?(post.body_html, "<script>blackmail</script>") + assert String.contains?(body_html, "<script>blackmail</script>") end # NOTE: this test is IMPORTANT, cause json_codec: Jason in router will cause @@ -134,6 +140,7 @@ defmodule GroupherServer.Test.Mutation.Articles.Post do end test "can delete a post by auth user", ~m(post)a do + post = post |> Repo.preload(:communities) belongs_community_title = post.communities |> List.first() |> Map.get(:title) rule_conn = simu_conn(:user, cms: %{belongs_community_title => %{"post.delete" => true}}) @@ -148,6 +155,7 @@ defmodule GroupherServer.Test.Mutation.Articles.Post do end test "login user with auth passport delete a post", ~m(post)a do + post = post |> Repo.preload(:communities) post_communities_0 = post.communities |> List.first() |> Map.get(:title) passport_rules = %{post_communities_0 => %{"post.delete" => true}} rule_conn = simu_conn(:user, cms: passport_rules) @@ -173,8 +181,9 @@ defmodule GroupherServer.Test.Mutation.Articles.Post do updatePost(id: $id, title: $title, body: $body, copyRight: $copyRight, articleTags: $articleTags) { id title - body - bodyHtml + document { + bodyHtml + } copyRight meta { isEdited @@ -213,11 +222,14 @@ defmodule GroupherServer.Test.Mutation.Articles.Post do copyRight: "translate" } - updated_post = owner_conn |> mutation_result(@query, variables, "updatePost") + result = owner_conn |> mutation_result(@query, variables, "updatePost") + assert result["title"] == variables.title + + assert result + |> get_in(["document", "bodyHtml"]) + |> String.contains?(~s(updated body #{unique_num})) - assert updated_post["title"] == variables.title - assert updated_post["bodyHtml"] |> String.contains?(~s(updated body #{unique_num})) - assert updated_post["copyRight"] == variables.copyRight + assert result["copyRight"] == variables.copyRight end test "update post with valid attrs should have is_edited meta info update", @@ -236,6 +248,7 @@ defmodule GroupherServer.Test.Mutation.Articles.Post do end test "login user with auth passport update a post", ~m(post)a do + post = post |> Repo.preload(:communities) belongs_community_title = post.communities |> List.first() |> Map.get(:title) passport_rules = %{belongs_community_title => %{"post.edit" => true}} diff --git a/test/groupher_server_web/mutation/cms/articles/repo_test.exs b/test/groupher_server_web/mutation/cms/articles/repo_test.exs index 658c2440a..604368efe 100644 --- a/test/groupher_server_web/mutation/cms/articles/repo_test.exs +++ b/test/groupher_server_web/mutation/cms/articles/repo_test.exs @@ -172,20 +172,6 @@ defmodule GroupherServer.Test.Mutation.Articles.Repo do # assert updated["readme"] == "new readme" end - test "create repo should excape xss attracts" do - {:ok, user} = db_insert(:user) - user_conn = simu_conn(:user, user) - - {:ok, community} = db_insert(:community) - - repo_attr = mock_attrs(:repo, %{body: mock_xss_string()}) - variables = repo_attr |> Map.merge(%{communityId: community.id}) |> camelize_map_key - created = user_conn |> mutation_result(@create_repo_query, variables, "createRepo") - {:ok, repo} = ORM.find(Repo, created["id"]) - - assert not String.contains?(repo.body_html, "script") - end - test "unauth user update git-repo fails", ~m(user_conn guest_conn repo)a do unique_num = System.unique_integer([:positive, :monotonic]) diff --git a/test/groupher_server_web/mutation/cms/publish_throttle_test.exs b/test/groupher_server_web/mutation/cms/publish_throttle_test.exs index cef019192..99c3e9a93 100644 --- a/test/groupher_server_web/mutation/cms/publish_throttle_test.exs +++ b/test/groupher_server_web/mutation/cms/publish_throttle_test.exs @@ -35,7 +35,6 @@ defmodule GroupherServer.Test.Mutation.PublishThrottle do communityId: $communityId ) { title - body id } } diff --git a/test/groupher_server_web/mutation/statistics/statistics_test.exs b/test/groupher_server_web/mutation/statistics/statistics_test.exs index 79a1da489..d4ec757d5 100644 --- a/test/groupher_server_web/mutation/statistics/statistics_test.exs +++ b/test/groupher_server_web/mutation/statistics/statistics_test.exs @@ -35,7 +35,6 @@ defmodule GroupherServer.Test.Mutation.Statistics do articleTags: $articleTags ) { title - body id } } @@ -82,7 +81,6 @@ defmodule GroupherServer.Test.Mutation.Statistics do ) { id title - body communities { id title @@ -117,7 +115,6 @@ defmodule GroupherServer.Test.Mutation.Statistics do ) { id title - body } } """ diff --git a/test/groupher_server_web/query/cms/articles/job_test.exs b/test/groupher_server_web/query/cms/articles/job_test.exs index 8dd09fbf9..0009964bf 100644 --- a/test/groupher_server_web/query/cms/articles/job_test.exs +++ b/test/groupher_server_web/query/cms/articles/job_test.exs @@ -15,7 +15,6 @@ defmodule GroupherServer.Test.Query.Articles.Job do job(id: $id) { id title - body } } """ @@ -25,8 +24,7 @@ defmodule GroupherServer.Test.Query.Articles.Job do assert results["id"] == to_string(job.id) assert is_valid_kv?(results, "title", :string) - assert is_valid_kv?(results, "body", :string) - assert length(Map.keys(results)) == 3 + assert length(Map.keys(results)) == 2 end test "basic graphql query on job with stranger(unloged user)", ~m(guest_conn job)a do @@ -35,6 +33,5 @@ defmodule GroupherServer.Test.Query.Articles.Job do assert results["id"] == to_string(job.id) assert is_valid_kv?(results, "title", :string) - assert is_valid_kv?(results, "body", :string) end end diff --git a/test/groupher_server_web/query/cms/articles/post_test.exs b/test/groupher_server_web/query/cms/articles/post_test.exs index c676f3cc5..e9753f28e 100644 --- a/test/groupher_server_web/query/cms/articles/post_test.exs +++ b/test/groupher_server_web/query/cms/articles/post_test.exs @@ -21,7 +21,6 @@ defmodule GroupherServer.Test.Query.Articles.Post do post(id: $id) { id title - body meta { isEdited } @@ -38,9 +37,8 @@ defmodule GroupherServer.Test.Query.Articles.Post do assert results["id"] == to_string(post.id) assert is_valid_kv?(results, "title", :string) - assert is_valid_kv?(results, "body", :string) assert %{"isEdited" => false} == results["meta"] - assert length(Map.keys(results)) == 4 + assert length(Map.keys(results)) == 3 end test "basic graphql query on post with stranger(unloged user)", ~m(guest_conn post)a do @@ -49,6 +47,5 @@ defmodule GroupherServer.Test.Query.Articles.Post do assert results["id"] == to_string(post.id) assert is_valid_kv?(results, "title", :string) - assert is_valid_kv?(results, "body", :string) end end diff --git a/test/groupher_server_web/query/cms/comments/job_comment_test.exs b/test/groupher_server_web/query/cms/comments/job_comment_test.exs index 380c6bcf8..bf1cc2686 100644 --- a/test/groupher_server_web/query/cms/comments/job_comment_test.exs +++ b/test/groupher_server_web/query/cms/comments/job_comment_test.exs @@ -22,7 +22,6 @@ defmodule GroupherServer.Test.Query.Comments.JobComment do job(id: $id) { id title - body commentsParticipants { id nickname diff --git a/test/groupher_server_web/query/cms/comments/post_comment_test.exs b/test/groupher_server_web/query/cms/comments/post_comment_test.exs index ce7bf5207..518bc5bc4 100644 --- a/test/groupher_server_web/query/cms/comments/post_comment_test.exs +++ b/test/groupher_server_web/query/cms/comments/post_comment_test.exs @@ -25,7 +25,6 @@ defmodule GroupherServer.Test.Query.Comments.PostComment do post(id: $id) { id title - body commentsParticipants { id nickname @@ -34,7 +33,6 @@ defmodule GroupherServer.Test.Query.Comments.PostComment do } } """ - test "guest user can get comment participants after comment created", ~m(guest_conn post user user2)a do total_count = 5 diff --git a/test/groupher_server_web/query/cms/paged_articles/paged_blogs_test.exs b/test/groupher_server_web/query/cms/paged_articles/paged_blogs_test.exs new file mode 100644 index 000000000..d64b332b2 --- /dev/null +++ b/test/groupher_server_web/query/cms/paged_articles/paged_blogs_test.exs @@ -0,0 +1,474 @@ +defmodule GroupherServer.Test.Query.PagedArticles.PagedBlogs do + @moduledoc false + use GroupherServer.TestTools + + import Helper.Utils, only: [get_config: 2] + import Ecto.Query, warn: false + + alias GroupherServer.CMS + alias GroupherServer.Repo + + alias CMS.Model.Blog + + @page_size get_config(:general, :page_size) + + @cur_date Timex.now() + @last_week Timex.shift(Timex.beginning_of_week(@cur_date), days: -1, seconds: -1) + @last_month Timex.shift(Timex.beginning_of_month(@cur_date), days: -7, seconds: -1) + @last_year Timex.shift(Timex.beginning_of_year(@cur_date), days: -2, seconds: -1) + + @today_count 15 + + @last_week_count 1 + @last_month_count 1 + @last_year_count 1 + + @total_count @today_count + @last_week_count + @last_month_count + @last_year_count + + setup do + {:ok, user} = db_insert(:user) + + {:ok, blog_last_week} = + db_insert(:blog, %{title: "last week", inserted_at: @last_week, active_at: @last_week}) + + db_insert(:blog, %{title: "last month", inserted_at: @last_month}) + + {:ok, blog_last_year} = + db_insert(:blog, %{title: "last year", inserted_at: @last_year, active_at: @last_year}) + + db_insert_multi(:blog, @today_count) + guest_conn = simu_conn(:guest) + + {:ok, ~m(guest_conn user blog_last_week blog_last_year)a} + end + + describe "[query paged_blogs filter pagination]" do + @query """ + query($filter: PagedBlogsFilter!) { + pagedBlogs(filter: $filter) { + entries { + id + document { + bodyHtml + } + communities { + id + raw + } + articleTags { + id + } + } + totalPages + totalCount + pageSize + pageNumber + } + } + """ + test "should get pagination info", ~m(guest_conn)a do + variables = %{filter: %{page: 1, size: 10}} + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + + assert results |> is_valid_pagination? + assert results["pageSize"] == 10 + assert results["totalCount"] == @total_count + assert results["entries"] |> List.first() |> Map.get("articleTags") |> is_list + end + + test "should get valid thread document", ~m(guest_conn)a do + {:ok, user} = db_insert(:user) + {:ok, community} = db_insert(:community) + blog_attrs = mock_attrs(:blog, %{community_id: community.id}) + Process.sleep(2000) + {:ok, _blog} = CMS.create_article(community, :blog, blog_attrs, user) + + variables = %{filter: %{page: 1, size: 30}} + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + + blog = results["entries"] |> List.first() + assert not is_nil(get_in(blog, ["document", "bodyHtml"])) + end + + test "support article_tag filter", ~m(guest_conn user)a do + {:ok, community} = db_insert(:community) + blog_attrs = mock_attrs(:blog, %{community_id: community.id}) + {:ok, blog} = CMS.create_article(community, :blog, blog_attrs, user) + + article_tag_attrs = mock_attrs(:article_tag) + {:ok, article_tag} = CMS.create_article_tag(community, :blog, article_tag_attrs, user) + {:ok, _} = CMS.set_article_tag(:blog, blog.id, article_tag.id) + + variables = %{filter: %{page: 1, size: 10, article_tag: article_tag.title}} + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + + blog = results["entries"] |> List.first() + assert results["totalCount"] == 1 + assert exist_in?(article_tag, blog["articleTags"], :string_key) + end + + test "support multi-tag (article_tags) filter", ~m(guest_conn user)a do + {:ok, community} = db_insert(:community) + blog_attrs = mock_attrs(:blog, %{community_id: community.id}) + {:ok, blog} = CMS.create_article(community, :blog, blog_attrs, user) + + article_tag_attrs = mock_attrs(:article_tag) + + {:ok, article_tag} = CMS.create_article_tag(community, :blog, article_tag_attrs, user) + {:ok, article_tag2} = CMS.create_article_tag(community, :blog, article_tag_attrs, user) + {:ok, article_tag3} = CMS.create_article_tag(community, :blog, article_tag_attrs, user) + + {:ok, _} = CMS.set_article_tag(:blog, blog.id, article_tag.id) + {:ok, _} = CMS.set_article_tag(:blog, blog.id, article_tag2.id) + + variables = %{ + filter: %{page: 1, size: 10, article_tags: [article_tag.title, article_tag2.title]} + } + + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + + blog = results["entries"] |> List.first() + assert results["totalCount"] == 1 + assert exist_in?(article_tag, blog["articleTags"], :string_key) + assert exist_in?(article_tag2, blog["articleTags"], :string_key) + assert not exist_in?(article_tag3, blog["articleTags"], :string_key) + end + + test "should not have pined blogs when filter have article_tag or article_tags", + ~m(guest_conn user)a do + {:ok, community} = db_insert(:community) + blog_attrs = mock_attrs(:blog, %{community_id: community.id}) + {:ok, pinned_blog} = CMS.create_article(community, :blog, blog_attrs, user) + {:ok, blog} = CMS.create_article(community, :blog, blog_attrs, user) + + {:ok, _} = CMS.pin_article(:blog, pinned_blog.id, community.id) + + article_tag_attrs = mock_attrs(:article_tag) + {:ok, article_tag} = CMS.create_article_tag(community, :blog, article_tag_attrs, user) + {:ok, _} = CMS.set_article_tag(:blog, blog.id, article_tag.id) + + variables = %{ + filter: %{page: 1, size: 10, community: community.raw, article_tag: article_tag.title} + } + + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + + assert not exist_in?(pinned_blog, results["entries"], :string_key) + assert exist_in?(blog, results["entries"], :string_key) + + variables = %{ + filter: %{page: 1, size: 10, community: community.raw, article_tags: [article_tag.title]} + } + + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + + assert not exist_in?(pinned_blog, results["entries"], :string_key) + assert exist_in?(blog, results["entries"], :string_key) + end + + test "support community filter", ~m(guest_conn user)a do + {:ok, community} = db_insert(:community) + + blog_attrs = mock_attrs(:blog, %{community_id: community.id}) + {:ok, _blog} = CMS.create_article(community, :blog, blog_attrs, user) + blog_attrs2 = mock_attrs(:blog, %{community_id: community.id}) + {:ok, _blog} = CMS.create_article(community, :blog, blog_attrs2, user) + + variables = %{filter: %{page: 1, size: 10, community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + + blog = results["entries"] |> List.first() + assert results["totalCount"] == 2 + assert exist_in?(%{id: to_string(community.id)}, blog["communities"], :string_key) + end + + test "request large size fails", ~m(guest_conn)a do + variables = %{filter: %{page: 1, size: 200}} + assert guest_conn |> query_get_error?(@query, variables, ecode(:pagination)) + end + + test "request 0 or neg-size fails", ~m(guest_conn)a do + variables_0 = %{filter: %{page: 1, size: 0}} + variables_neg_1 = %{filter: %{page: 1, size: -1}} + + assert guest_conn |> query_get_error?(@query, variables_0) + assert guest_conn |> query_get_error?(@query, variables_neg_1) + end + + test "pagination should have default page and size arg", ~m(guest_conn)a do + variables = %{filter: %{}} + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + assert results |> is_valid_pagination? + assert results["pageSize"] == @page_size + assert results["totalCount"] == @total_count + end + end + + describe "[query paged_blogs filter has_xxx]" do + @query """ + query($filter: PagedBlogsFilter!) { + pagedBlogs(filter: $filter) { + entries { + id + viewerHasCollected + viewerHasUpvoted + viewerHasViewed + viewerHasReported + } + totalCount + } + } + """ + + test "has_xxx state should work", ~m(user)a do + user_conn = simu_conn(:user, user) + {:ok, community} = db_insert(:community) + + {:ok, blog} = CMS.create_article(community, :blog, mock_attrs(:blog), user) + {:ok, _blog} = CMS.create_article(community, :blog, mock_attrs(:blog), user) + {:ok, _blog3} = CMS.create_article(community, :blog, mock_attrs(:blog), user) + + variables = %{filter: %{community: community.raw}} + results = user_conn |> query_result(@query, variables, "pagedBlogs") + assert results["totalCount"] == 3 + + the_blog = Enum.find(results["entries"], &(&1["id"] == to_string(blog.id))) + assert not the_blog["viewerHasViewed"] + assert not the_blog["viewerHasUpvoted"] + assert not the_blog["viewerHasCollected"] + assert not the_blog["viewerHasReported"] + + {:ok, _} = CMS.read_article(:blog, blog.id, user) + {:ok, _} = CMS.upvote_article(:blog, blog.id, user) + {:ok, _} = CMS.collect_article(:blog, blog.id, user) + {:ok, _} = CMS.report_article(:blog, blog.id, "reason", "attr_info", user) + + results = user_conn |> query_result(@query, variables, "pagedBlogs") + the_blog = Enum.find(results["entries"], &(&1["id"] == to_string(blog.id))) + assert the_blog["viewerHasViewed"] + assert the_blog["viewerHasUpvoted"] + assert the_blog["viewerHasCollected"] + assert the_blog["viewerHasReported"] + end + end + + describe "[query paged_blogs filter sort]" do + @query """ + query($filter: PagedBlogsFilter!) { + pagedBlogs(filter: $filter) { + entries { + id + inserted_at + active_at + author { + id + nickname + avatar + } + } + } + } + """ + + test "filter community should get blogs which belongs to that community", + ~m(guest_conn user)a do + {:ok, community} = db_insert(:community) + {:ok, blog} = CMS.create_article(community, :blog, mock_attrs(:blog), user) + + variables = %{filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + + assert length(results["entries"]) == 1 + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(blog.id))) + end + + test "should have a active_at same with inserted_at", ~m(guest_conn user)a do + {:ok, community} = db_insert(:community) + {:ok, _blog} = CMS.create_article(community, :blog, mock_attrs(:blog), user) + + variables = %{filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + blog = results["entries"] |> List.first() + + assert blog["inserted_at"] == blog["active_at"] + end + + test "filter sort should have default :desc_active", ~m(guest_conn)a do + variables = %{filter: %{}} + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + active_timestamps = results["entries"] |> Enum.map(& &1["active_at"]) + + {:ok, first_active_time, 0} = active_timestamps |> List.first() |> DateTime.from_iso8601() + {:ok, last_active_time, 0} = active_timestamps |> List.last() |> DateTime.from_iso8601() + + assert :gt = DateTime.compare(first_active_time, last_active_time) + end + + @query """ + query($filter: PagedBlogsFilter!) { + pagedBlogs(filter: $filter) { + entries { + id + views + } + } + } + """ + + test "filter sort MOST_VIEWS should work", ~m(guest_conn)a do + most_views_blog = Blog |> order_by(desc: :views) |> limit(1) |> Repo.one() + variables = %{filter: %{sort: "MOST_VIEWS"}} + + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + find_blog = results |> Map.get("entries") |> hd + + assert find_blog["views"] == most_views_blog |> Map.get(:views) + end + end + + # TODO test sort, tag, community, when ... + @doc """ + test: FILTER when [TODAY] [THIS_WEEK] [THIS_MONTH] [THIS_YEAR] + """ + describe "[query paged_blogs filter when]" do + @query """ + query($filter: PagedBlogsFilter!) { + pagedBlogs(filter: $filter) { + entries { + id + views + inserted_at + } + totalCount + } + } + """ + test "THIS_YEAR option should work", ~m(guest_conn blog_last_year)a do + variables = %{filter: %{when: "THIS_YEAR"}} + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + + assert results["entries"] |> Enum.any?(&(&1["id"] != blog_last_year.id)) + end + + test "TODAY option should work", ~m(guest_conn)a do + variables = %{filter: %{when: "TODAY"}} + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + + expect_count = @total_count - @last_year_count - @last_month_count - @last_week_count + + assert results |> Map.get("totalCount") == expect_count + end + + test "THIS_WEEK option should work", ~m(guest_conn)a do + variables = %{filter: %{when: "THIS_WEEK"}} + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + + assert results |> Map.get("totalCount") == @today_count + end + + test "THIS_MONTH option should work", ~m(guest_conn)a do + variables = %{filter: %{when: "THIS_MONTH"}} + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + + {_, cur_week_month, _} = @cur_date |> Date.to_erl() + {_, last_week_month, _} = @last_week |> Date.to_erl() + + expect_count = + case cur_week_month == last_week_month do + true -> + @total_count - @last_year_count - @last_month_count + + false -> + @total_count - @last_year_count - @last_month_count - @last_week_count + end + + assert results |> Map.get("totalCount") == expect_count + end + end + + describe "[query paged_blogs filter extra]" do + @query """ + query($filter: PagedBlogsFilter!) { + pagedBlogs(filter: $filter) { + entries { + id + title + } + totalCount + } + } + """ + test "basic filter should work", ~m(guest_conn)a do + {:ok, blog} = db_insert(:blog) + {:ok, blog2} = db_insert(:blog) + + variables = %{filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + + assert results["totalCount"] >= 1 + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(blog.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] != to_string(blog2.id))) + end + end + + describe "[paged blogs active_at]" do + @query """ + query($filter: PagedBlogsFilter!) { + pagedBlogs(filter: $filter) { + entries { + id + insertedAt + activeAt + } + } + } + """ + + test "latest commented blog should appear on top", ~m(guest_conn blog_last_week user)a do + variables = %{filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + entries = results["entries"] + first_blog = entries |> List.first() + assert first_blog["id"] !== to_string(blog_last_week.id) + + Process.sleep(1500) + {:ok, _comment} = CMS.create_comment(:blog, blog_last_week.id, mock_comment(), user) + + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + entries = results["entries"] + first_blog = entries |> List.first() + + assert first_blog["id"] == to_string(blog_last_week.id) + end + + test "comment on very old blog have no effect", ~m(guest_conn blog_last_year user)a do + variables = %{filter: %{page: 1, size: 20}} + + {:ok, _comment} = CMS.create_comment(:blog, blog_last_year.id, mock_comment(), user) + + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + entries = results["entries"] + first_blog = entries |> List.first() + + assert first_blog["id"] !== to_string(blog_last_year.id) + end + + test "latest blog author commented blog have no effect", ~m(guest_conn blog_last_week)a do + variables = %{filter: %{page: 1, size: 20}} + + {:ok, _comment} = + CMS.create_comment( + :blog, + blog_last_week.id, + mock_comment(), + blog_last_week.author.user + ) + + results = guest_conn |> query_result(@query, variables, "pagedBlogs") + entries = results["entries"] + first_blog = entries |> List.first() + + assert first_blog["id"] !== to_string(blog_last_week.id) + end + end +end diff --git a/test/groupher_server_web/query/cms/paged_articles/paged_jobs_test.exs b/test/groupher_server_web/query/cms/paged_articles/paged_jobs_test.exs index 85594507c..c7d9df692 100644 --- a/test/groupher_server_web/query/cms/paged_articles/paged_jobs_test.exs +++ b/test/groupher_server_web/query/cms/paged_articles/paged_jobs_test.exs @@ -48,6 +48,9 @@ defmodule GroupherServer.Test.Query.PagedArticles.PagedJobs do pagedJobs(filter: $filter) { entries { id + document { + bodyHtml + } communities { id raw @@ -73,6 +76,20 @@ defmodule GroupherServer.Test.Query.PagedArticles.PagedJobs do assert results["entries"] |> List.first() |> Map.get("articleTags") |> is_list end + test "should get valid thread document", ~m(guest_conn)a do + {:ok, user} = db_insert(:user) + {:ok, community} = db_insert(:community) + job_attrs = mock_attrs(:job, %{community_id: community.id}) + Process.sleep(2000) + {:ok, _job} = CMS.create_article(community, :job, job_attrs, user) + + variables = %{filter: %{page: 1, size: 30}} + results = guest_conn |> query_result(@query, variables, "pagedJobs") + + job = results["entries"] |> List.first() + assert not is_nil(get_in(job, ["document", "bodyHtml"])) + end + test "support article_tag filter", ~m(guest_conn user)a do {:ok, community} = db_insert(:community) job_attrs = mock_attrs(:job, %{community_id: community.id}) @@ -318,7 +335,6 @@ defmodule GroupherServer.Test.Query.PagedArticles.PagedJobs do pagedJobs(filter: $filter) { entries { id - body company views inserted_at diff --git a/test/groupher_server_web/query/cms/paged_articles/paged_posts_test.exs b/test/groupher_server_web/query/cms/paged_articles/paged_posts_test.exs index f216a3eaf..fb414d09c 100644 --- a/test/groupher_server_web/query/cms/paged_articles/paged_posts_test.exs +++ b/test/groupher_server_web/query/cms/paged_articles/paged_posts_test.exs @@ -49,6 +49,9 @@ defmodule GroupherServer.Test.Query.PagedArticles.PagedPosts do pagedPosts(filter: $filter) { entries { id + document { + bodyHtml + } communities { id raw @@ -74,6 +77,21 @@ defmodule GroupherServer.Test.Query.PagedArticles.PagedPosts do assert results["entries"] |> List.first() |> Map.get("articleTags") |> is_list end + test "should get valid thread document", ~m(guest_conn)a do + {:ok, user} = db_insert(:user) + {:ok, community} = db_insert(:community) + post_attrs = mock_attrs(:post, %{community_id: community.id}) + Process.sleep(2000) + {:ok, _post} = CMS.create_article(community, :post, post_attrs, user) + + variables = %{filter: %{page: 1, size: 10}} + results = guest_conn |> query_result(@query, variables, "pagedPosts") + + post = results["entries"] |> List.first() + + assert not is_nil(get_in(post, ["document", "bodyHtml"])) + end + test "support article_tag filter", ~m(guest_conn user)a do {:ok, community} = db_insert(:community) post_attrs = mock_attrs(:post, %{community_id: community.id}) diff --git a/test/groupher_server_web/query/cms/paged_articles/paged_repos_test.exs b/test/groupher_server_web/query/cms/paged_articles/paged_repos_test.exs index 3b028b3cb..309a81f48 100644 --- a/test/groupher_server_web/query/cms/paged_articles/paged_repos_test.exs +++ b/test/groupher_server_web/query/cms/paged_articles/paged_repos_test.exs @@ -46,6 +46,9 @@ defmodule GroupherServer.Test.Query.PagedArticles.PagedRepos do pagedRepos(filter: $filter) { entries { id + document { + bodyHtml + } communities { id raw @@ -61,7 +64,6 @@ defmodule GroupherServer.Test.Query.PagedArticles.PagedRepos do } } """ - test "should get pagination info", ~m(guest_conn)a do variables = %{filter: %{page: 1, size: 10}} results = guest_conn |> query_result(@query, variables, "pagedRepos") @@ -72,6 +74,21 @@ defmodule GroupherServer.Test.Query.PagedArticles.PagedRepos do assert results["entries"] |> List.first() |> Map.get("articleTags") |> is_list end + # + # test "should get valid thread document", ~m(guest_conn)a do + # {:ok, user} = db_insert(:user) + # {:ok, community} = db_insert(:community) + # repo_attrs = mock_attrs(:repo, %{community_id: community.id}) + # {:ok, _repo} = CMS.create_article(community, :repo, repo_attrs, user) + + # variables = %{filter: %{page: 1, size: 10}} + # results = guest_conn |> query_result(@query, variables, "pagedRepos") + + # repo = results["entries"] |> List.first() + + # assert not is_nil(get_in(repo, ["document", "bodyHtml"])) + # end + test "support article_tag filter", ~m(guest_conn user)a do {:ok, community} = db_insert(:community) repo_attrs = mock_attrs(:repo, %{community_id: community.id}) diff --git a/test/support/conn_simulator.ex b/test/support/conn_simulator.ex index d242ee05b..6da017299 100644 --- a/test/support/conn_simulator.ex +++ b/test/support/conn_simulator.ex @@ -6,6 +6,8 @@ defmodule GroupherServer.Test.ConnSimulator do import Phoenix.ConnTest, only: [build_conn: 0] import Plug.Conn, only: [put_req_header: 3] + import GroupherServer.CMS.Delegate.Helper, only: [author_of: 1] + alias GroupherServer.{Accounts, CMS} alias Accounts.Model.User alias Helper.{Guardian, ORM} @@ -30,9 +32,11 @@ defmodule GroupherServer.Test.ConnSimulator do end def simu_conn(:owner, content) do - token = gen_jwt_token(id: content.author.user.id) + with {:ok, author} <- author_of(content) do + token = gen_jwt_token(id: author.id) - build_conn() |> put_req_header("authorization", token) + build_conn() |> put_req_header("authorization", token) + end end def simu_conn(:user, %User{} = user) do