Skip to content
This repository was archived by the owner on Nov 8, 2022. It is now read-only.

feat: article document concept #409

Merged
merged 13 commits into from
Jun 24, 2021
2 changes: 2 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/groupher_server/cms/cms.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 63 additions & 23 deletions lib/groupher_server/cms/delegates/article_curd.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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
Expand All @@ -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)})

Expand All @@ -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 = %{
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -409,26 +439,35 @@ 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

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)
Expand Down Expand Up @@ -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}
Expand Down
124 changes: 124 additions & 0 deletions lib/groupher_server/cms/delegates/document.ex
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions lib/groupher_server/cms/delegates/hooks/cite.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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:
[
Expand Down
6 changes: 6 additions & 0 deletions lib/groupher_server/cms/delegates/hooks/mention.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading