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

Commit 0fb98e9

Browse files
authored
feat(post-qa): post and comment workflow (#383)
* feat(post-qa): basic setup * feat(post-qa): basic logic done * feat(post-qa): only allows one best solution * feat(post-qa): pin solution logic * feat(post-qa): improve pinned logic readabily * feat(post-qa): improve nameing func matching * feat(post-qa): solution digest edge case * feat(post-qa): gq workflow
1 parent aa9fcff commit 0fb98e9

File tree

24 files changed

+533
-62
lines changed

24 files changed

+533
-62
lines changed

lib/groupher_server/accounts/helper/loader.ex

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ defmodule GroupherServer.Accounts.Helper.Loader do
55
import Ecto.Query, warn: false
66

77
alias Helper.QueryBuilder
8-
alias GroupherServer.{Accounts, CMS, Repo}
8+
alias GroupherServer.{CMS, Repo}
99

1010
def data, do: Dataloader.Ecto.new(Repo, query: &query/2)
1111

@@ -17,10 +17,4 @@ defmodule GroupherServer.Accounts.Helper.Loader do
1717
end
1818

1919
def query(queryable, _args), do: queryable
20-
21-
defp count_contents(queryable) do
22-
queryable
23-
|> group_by([f], f.user_id)
24-
|> select([f], count(f.id))
25-
end
2620
end

lib/groupher_server/cms/article_comment.ex

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ defmodule GroupherServer.CMS.ArticleComment do
1717
@article_threads get_config(:article, :threads)
1818

1919
@required_fields ~w(body_html author_id)a
20-
@optional_fields ~w(reply_to_id replies_count is_folded is_deleted floor is_article_author thread)a
21-
@updatable_fields ~w(is_folded is_deleted floor upvotes_count is_pinned)a
20+
@optional_fields ~w(reply_to_id replies_count is_folded is_deleted floor is_article_author thread is_for_question is_solution)a
21+
@updatable_fields ~w(is_folded is_deleted floor upvotes_count is_pinned is_for_question is_solution)a
2222

2323
@article_fields @article_threads |> Enum.map(&:"#{&1}_id")
2424

@@ -59,6 +59,9 @@ defmodule GroupherServer.CMS.ArticleComment do
5959
# 楼层
6060
field(:floor, :integer, default: 0)
6161

62+
field(:is_for_question, :boolean, default: false)
63+
field(:is_solution, :boolean, default: false)
64+
6265
# 是否是评论文章的作者
6366
field(:is_article_author, :boolean, default: false)
6467
field(:upvotes_count, :integer, default: 0)

lib/groupher_server/cms/cms.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ defmodule GroupherServer.CMS do
141141
defdelegate create_article_comment(thread, article_id, args, user), to: ArticleComment
142142
defdelegate update_article_comment(comment, content), to: ArticleComment
143143
defdelegate delete_article_comment(comment), to: ArticleComment
144+
defdelegate mark_comment_solution(comment, user), to: ArticleComment
145+
defdelegate undo_mark_comment_solution(comment, user), to: ArticleComment
144146

145147
defdelegate upvote_article_comment(comment_id, user), to: ArticleCommentAction
146148
defdelegate undo_upvote_article_comment(comment_id, user), to: ArticleCommentAction

lib/groupher_server/cms/delegates/article_comment.ex

Lines changed: 132 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ defmodule GroupherServer.CMS.Delegate.ArticleComment do
33
CURD and operations for article comments
44
"""
55
import Ecto.Query, warn: false
6-
import Helper.Utils, only: [done: 1, ensure: 2]
6+
import Helper.Utils, only: [done: 1, ensure: 2, get_config: 2]
77
import Helper.ErrorCode
88

99
import GroupherServer.CMS.Delegate.Helper, only: [mark_viewer_emotion_states: 3]
@@ -13,11 +13,14 @@ defmodule GroupherServer.CMS.Delegate.ArticleComment do
1313
alias Helper.Types, as: T
1414
alias Helper.{ORM, QueryBuilder}
1515
alias GroupherServer.{Accounts, CMS, Repo}
16+
alias CMS.Post
1617

1718
alias Accounts.User
1819
alias CMS.{ArticleComment, ArticlePinnedComment, Embeds}
1920
alias Ecto.Multi
2021

22+
@article_threads get_config(:article, :threads)
23+
2124
@max_participator_count ArticleComment.max_participator_count()
2225
@default_emotions Embeds.ArticleCommentEmotion.default_emotions()
2326
@delete_hint ArticleComment.delete_hint()
@@ -102,9 +105,10 @@ defmodule GroupherServer.CMS.Delegate.ArticleComment do
102105
|> Multi.run(:update_article_comments_count, fn _, %{create_article_comment: comment} ->
103106
update_article_comments_count(comment, :inc)
104107
end)
105-
|> Multi.run(:add_participator, fn _, _ ->
106-
add_participator_to_article(article, user)
108+
|> Multi.run(:set_question_flag_ifneed, fn _, %{create_article_comment: comment} ->
109+
set_question_flag_ifneed(article, comment)
107110
end)
111+
|> Multi.run(:add_participator, fn _, _ -> add_participator_to_article(article, user) end)
108112
|> Multi.run(:update_article_active_timestamp, fn _, %{create_article_comment: comment} ->
109113
case comment.author_id == article.author.user.id do
110114
true -> {:ok, :pass}
@@ -130,10 +134,74 @@ defmodule GroupherServer.CMS.Delegate.ArticleComment do
130134
@doc """
131135
update a comment for article like psot, job ...
132136
"""
137+
# 如果是 solution, 那么要更新对应的 post 的 solution_digest
138+
def update_article_comment(%ArticleComment{is_solution: true} = article_comment, content) do
139+
with {:ok, post} <- ORM.find(Post, article_comment.post_id) do
140+
post |> ORM.update(%{solution_digest: content})
141+
article_comment |> ORM.update(%{body_html: content})
142+
end
143+
end
144+
133145
def update_article_comment(%ArticleComment{} = article_comment, content) do
134146
article_comment |> ORM.update(%{body_html: content})
135147
end
136148

149+
@doc """
150+
mark a comment as question post's best solution
151+
"""
152+
def mark_comment_solution(comment_id, user) do
153+
with {:ok, article_comment} <- ORM.find(ArticleComment, comment_id),
154+
{:ok, post} <- ORM.find(Post, article_comment.post_id, preload: [author: :user]) do
155+
# 确保只有一个最佳答案
156+
batch_update_solution_flag(post, false)
157+
CMS.pin_article_comment(article_comment.id)
158+
do_mark_comment_solution(post, article_comment, user, true)
159+
end
160+
end
161+
162+
@doc """
163+
undo mark a comment as question post's best solution
164+
"""
165+
def undo_mark_comment_solution(comment_id, user) do
166+
with {:ok, article_comment} <- ORM.find(ArticleComment, comment_id),
167+
{:ok, post} <- ORM.find(Post, article_comment.post_id, preload: [author: :user]) do
168+
do_mark_comment_solution(post, article_comment, user, false)
169+
end
170+
end
171+
172+
defp do_mark_comment_solution(post, %ArticleComment{} = article_comment, user, is_solution) do
173+
# check if user is questioner
174+
with true <- user.id == post.author.user.id do
175+
Multi.new()
176+
|> Multi.run(:mark_solution, fn _, _ ->
177+
ORM.update(article_comment, %{is_solution: is_solution, is_for_question: true})
178+
end)
179+
|> Multi.run(:update_post_state, fn _, _ ->
180+
ORM.update(post, %{is_solved: is_solution, solution_digest: article_comment.body_html})
181+
end)
182+
|> Repo.transaction()
183+
|> result()
184+
else
185+
false -> raise_error(:require_questioner, "oops, questioner only")
186+
{:error, error} -> {:error, error}
187+
end
188+
end
189+
190+
@doc """
191+
batch update is_question flag for post-only article
192+
"""
193+
def batch_update_question_flag(%Post{is_question: is_question} = post) do
194+
from(c in ArticleComment,
195+
where: c.post_id == ^post.id,
196+
update: [set: [is_for_question: ^is_question]]
197+
)
198+
|> Repo.update_all([])
199+
200+
{:ok, :pass}
201+
end
202+
203+
def batch_update_question_flag(_), do: {:ok, :pass}
204+
137205
@doc "delete article comment"
138206
def delete_article_comment(%ArticleComment{} = comment) do
139207
Multi.new()
@@ -155,7 +223,9 @@ defmodule GroupherServer.CMS.Delegate.ArticleComment do
155223
%{article_comments_participators: article_comments_participators} = article,
156224
%User{} = user
157225
) do
158-
total_participators = article_comments_participators |> List.insert_at(0, user) |> Enum.uniq()
226+
total_participators =
227+
article_comments_participators |> List.insert_at(0, user) |> Enum.uniq_by(& &1.id)
228+
159229
new_comment_participators = total_participators |> Enum.slice(0, @max_participator_count)
160230
total_participators_count = length(total_participators)
161231

@@ -225,10 +295,9 @@ defmodule GroupherServer.CMS.Delegate.ArticleComment do
225295
query
226296
|> where(^thread_query)
227297
|> where(^where_query)
228-
# |> QueryBuilder.filter_pack(Map.merge(filters, %{sort: :asc_inserted}))
229298
|> QueryBuilder.filter_pack(Map.merge(filters, %{sort: sort}))
230299
|> ORM.paginater(~m(page size)a)
231-
|> add_pined_comments_ifneed(thread, article_id, filters)
300+
|> add_pinned_comments_ifneed(thread, article_id, filters)
232301
|> mark_viewer_emotion_states(user, :comment)
233302
|> mark_viewer_has_upvoted(user)
234303
|> done()
@@ -250,37 +319,57 @@ defmodule GroupherServer.CMS.Delegate.ArticleComment do
250319
|> done()
251320
end
252321

253-
defp add_pined_comments_ifneed(%{entries: entries} = paged_comments, thread, article_id, %{
254-
page: 1
255-
}) do
322+
defp add_pinned_comments_ifneed(paged_comments, thread, article_id, %{page: 1}) do
256323
with {:ok, info} <- match(thread),
257-
query <-
258-
from(p in ArticlePinnedComment,
259-
join: c in ArticleComment,
260-
on: p.article_comment_id == c.id,
261-
where: field(p, ^info.foreign_key) == ^article_id,
262-
select: c
263-
),
264-
{:ok, pined_comments} <- Repo.all(query) |> done() do
265-
case pined_comments do
324+
{:ok, pinned_comments} <- list_pinned_comments(info, article_id) do
325+
case pinned_comments do
266326
[] ->
267327
paged_comments
268328

269329
_ ->
270-
preloaded_pined_comments =
271-
Enum.slice(pined_comments, 0, @pinned_comment_limit)
330+
pinned_comments =
331+
sort_solution_to_front(thread, pinned_comments)
332+
|> Enum.slice(0, @pinned_comment_limit)
272333
|> Repo.preload(reply_to: :author)
273334

274-
entries = Enum.concat(preloaded_pined_comments, entries)
275-
pined_comment_count = length(pined_comments)
335+
entries = pinned_comments ++ paged_comments.entries
336+
pinned_comment_count = length(pinned_comments)
276337

277-
total_count = paged_comments.total_count + pined_comment_count
338+
total_count = paged_comments.total_count + pinned_comment_count
278339
paged_comments |> Map.merge(%{entries: entries, total_count: total_count})
279340
end
280341
end
281342
end
282343

283-
defp add_pined_comments_ifneed(paged_comments, _thread, _article_id, _), do: paged_comments
344+
defp add_pinned_comments_ifneed(paged_comments, _thread, _article_id, _), do: paged_comments
345+
346+
defp list_pinned_comments(%{foreign_key: foreign_key}, article_id) do
347+
from(p in ArticlePinnedComment,
348+
join: c in ArticleComment,
349+
on: p.article_comment_id == c.id,
350+
where: field(p, ^foreign_key) == ^article_id,
351+
order_by: [desc: p.inserted_at],
352+
select: c
353+
)
354+
|> Repo.all()
355+
|> done
356+
end
357+
358+
# only support post
359+
defp sort_solution_to_front(:post, pinned_comments) do
360+
solution_index = Enum.find_index(pinned_comments, & &1.is_solution)
361+
362+
case is_nil(solution_index) do
363+
true ->
364+
pinned_comments
365+
366+
false ->
367+
{solution_comment, rest_comments} = List.pop_at(pinned_comments, solution_index)
368+
[solution_comment] ++ rest_comments
369+
end
370+
end
371+
372+
defp sort_solution_to_front(_, pinned_comments), do: pinned_comments
284373

285374
defp mark_viewer_has_upvoted(paged_comments, nil), do: paged_comments
286375

@@ -294,8 +383,26 @@ defmodule GroupherServer.CMS.Delegate.ArticleComment do
294383
Map.merge(paged_comments, %{entries: entries})
295384
end
296385

297-
defp result({:ok, %{create_article_comment: result}}), do: {:ok, result}
386+
defp set_question_flag_ifneed(%{is_question: true} = article, %ArticleComment{} = comment) do
387+
ORM.update(comment, %{is_for_question: true})
388+
end
389+
390+
defp set_question_flag_ifneed(_, comment), do: ORM.update(comment, %{is_for_question: false})
391+
392+
# batch update is_solution flag for artilce comment
393+
defp batch_update_solution_flag(%Post{} = post, is_question) do
394+
from(c in ArticleComment,
395+
where: c.post_id == ^post.id,
396+
update: [set: [is_solution: ^is_question]]
397+
)
398+
|> Repo.update_all([])
399+
400+
{:ok, :pass}
401+
end
402+
403+
defp result({:ok, %{set_question_flag_ifneed: result}}), do: {:ok, result}
298404
defp result({:ok, %{delete_article_comment: result}}), do: {:ok, result}
405+
defp result({:ok, %{mark_solution: result}}), do: {:ok, result}
299406

300407
defp result({:error, :create_article_comment, result, _steps}) do
301408
raise_error(:create_comment, result)

lib/groupher_server/cms/delegates/article_community.ex

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ defmodule GroupherServer.CMS.Delegate.ArticleCommunity do
1717

1818
alias Ecto.Multi
1919

20-
@default_article_meta Embeds.ArticleMeta.default_meta()
2120
@max_pinned_article_count_per_thread Community.max_pinned_article_count_per_thread()
2221

2322
@spec pin_article(T.article_thread(), Integer.t(), Integer.t()) :: {:ok, PinnedArticle.t()}

lib/groupher_server/cms/delegates/article_curd.ex

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ defmodule GroupherServer.CMS.Delegate.ArticleCURD do
2727
alias Accounts.User
2828
alias CMS.{Author, Community, PinnedArticle, Embeds, Delegate}
2929

30-
alias Delegate.{ArticleCommunity, ArticleTag, CommunityCURD}
30+
alias Delegate.{ArticleCommunity, ArticleComment, ArticleTag, CommunityCURD}
3131

3232
alias Ecto.Multi
3333

@@ -214,8 +214,13 @@ defmodule GroupherServer.CMS.Delegate.ArticleCURD do
214214
"""
215215
def update_article(article, args) do
216216
Multi.new()
217-
|> Multi.run(:update_article, fn _, _ ->
218-
ORM.update(article, args)
217+
|> Multi.run(:update_article, fn _, _ -> ORM.update(article, args) end)
218+
|> Multi.run(:update_comment_question_flag_if_need, fn _, %{update_article: update_article} ->
219+
# 如果帖子的类型变了,那么 update 所有的 flag
220+
case Map.has_key?(args, :is_question) do
221+
true -> ArticleComment.batch_update_question_flag(update_article)
222+
false -> {:ok, :pass}
223+
end
219224
end)
220225
|> Multi.run(:update_edit_status, fn _, %{update_article: update_article} ->
221226
ArticleCommunity.update_edit_status(update_article)

lib/groupher_server/cms/embeds/article_comment_meta.ex

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,22 @@ defmodule GroupherServer.CMS.Embeds.ArticleCommentMeta do
77

88
import Ecto.Changeset
99

10-
@optional_fields ~w(is_article_author_upvoted is_solution report_count is_reply_to_others reported_count reported_user_ids)a
11-
12-
@default_meta %{
13-
is_article_author_upvoted: false,
14-
is_solution: false,
15-
is_reply_to_others: false,
16-
report_count: 0,
17-
upvoted_user_ids: [],
18-
reported_user_ids: [],
19-
reported_count: 0
20-
}
10+
@optional_fields ~w(is_article_author_upvoted report_count is_reply_to_others reported_count reported_user_ids)a
2111

2212
@doc "for test usage"
23-
def default_meta(), do: @default_meta
13+
def default_meta() do
14+
%{
15+
is_article_author_upvoted: false,
16+
is_reply_to_others: false,
17+
report_count: 0,
18+
upvoted_user_ids: [],
19+
reported_user_ids: [],
20+
reported_count: 0
21+
}
22+
end
2423

2524
embedded_schema do
2625
field(:is_article_author_upvoted, :boolean, default: false)
27-
field(:is_solution, :boolean, default: false)
2826
# used in replies mode, for those reply to other user in replies box (for frontend)
2927
# 用于回复模式,指代这条回复是回复“回复列表其他人的” (方便前端展示)
3028
field(:is_reply_to_others, :boolean, default: false)

lib/groupher_server/cms/post.ex

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ defmodule GroupherServer.CMS.Post do
1717

1818
@required_fields ~w(title body digest length)a
1919
@article_cast_fields general_article_fields(:cast)
20-
@optional_fields ~w(link_addr copy_right link_addr link_icon)a ++ @article_cast_fields
20+
@optional_fields ~w(link_addr copy_right link_addr link_icon is_question is_solved solution_digest)a ++
21+
@article_cast_fields
2122

2223
@type t :: %Post{}
2324
schema "cms_posts" do
@@ -29,6 +30,10 @@ defmodule GroupherServer.CMS.Post do
2930
field(:copy_right, :string)
3031
field(:length, :integer)
3132

33+
field(:is_question, :boolean, default: false)
34+
field(:is_solved, :boolean, default: false)
35+
field(:solution_digest, :string)
36+
3237
# TODO: remove after legacy data migrated
3338
has_many(:comments, {"posts_comments", PostComment})
3439

lib/groupher_server_web/resolvers/cms_resolver.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,14 @@ defmodule GroupherServerWeb.Resolvers.CMS do
320320
CMS.undo_emotion_to_comment(id, emotion, user)
321321
end
322322

323+
def mark_comment_solution(_root, ~m(id)a, %{context: %{cur_user: user}}) do
324+
CMS.mark_comment_solution(id, user)
325+
end
326+
327+
def undo_mark_comment_solution(_root, ~m(id)a, %{context: %{cur_user: user}}) do
328+
CMS.undo_mark_comment_solution(id, user)
329+
end
330+
323331
############
324332
############
325333
############

0 commit comments

Comments
 (0)