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

Commit d4f67f0

Browse files
committed
chore(works): basic setup & test
1 parent 2e3b9ce commit d4f67f0

File tree

5 files changed

+319
-2
lines changed

5 files changed

+319
-2
lines changed

config/config.exs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,15 @@ config :groupher_server, :article,
6969
min_length: 10,
7070
max_length: 20_000,
7171
# NOTE: do not change unless you know what you are doing
72-
threads: [:post, :job, :repo, :blog],
72+
threads: [:post, :job, :repo, :blog, :works],
7373
# in this period, paged articles will sort front if non-article-author commented
7474
# 在此时间段内,一旦有非文章作者的用户评论,该文章就会排到前面
7575
active_period_days: %{
7676
post: 10,
7777
job: 10,
7878
repo: 10,
79-
blog: 10
79+
blog: 10,
80+
works: 10
8081
},
8182

8283
# NOTE: if you want to add/remove emotion, just edit the list below
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
defmodule GroupherServer.CMS.Model.Works do
2+
@moduledoc false
3+
alias __MODULE__
4+
5+
use Ecto.Schema
6+
use Accessible
7+
8+
import Ecto.Changeset
9+
import GroupherServer.CMS.Helper.Macros
10+
11+
alias GroupherServer.CMS
12+
alias CMS.Model.Embeds
13+
14+
alias Helper.HTML
15+
16+
@timestamps_opts [type: :utc_datetime_usec]
17+
18+
@required_fields ~w(title digest)a
19+
@article_cast_fields general_article_cast_fields()
20+
@optional_fields @article_cast_fields
21+
22+
@type t :: %Works{}
23+
schema "cms_works" do
24+
article_tags_field(:works)
25+
article_communities_field(:works)
26+
general_article_fields(:works)
27+
end
28+
29+
@doc false
30+
def changeset(%Works{} = works, attrs) do
31+
works
32+
|> cast(attrs, @optional_fields ++ @required_fields)
33+
|> validate_required(@required_fields)
34+
|> cast_embed(:meta, required: false, with: &Embeds.ArticleMeta.changeset/2)
35+
|> generl_changeset
36+
end
37+
38+
@doc false
39+
def update_changeset(%Works{} = works, attrs) do
40+
works
41+
|> cast(attrs, @optional_fields ++ @required_fields)
42+
|> generl_changeset
43+
end
44+
45+
defp generl_changeset(changeset) do
46+
changeset
47+
|> validate_length(:title, min: 3, max: 50)
48+
|> cast_embed(:emotions, with: &Embeds.ArticleEmotion.changeset/2)
49+
|> validate_length(:link_addr, min: 5, max: 400)
50+
|> HTML.safe_string(:body)
51+
end
52+
end
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
defmodule GroupherServer.CMS.Model.WorksDocument do
2+
@moduledoc """
3+
mainly for full-text search
4+
"""
5+
alias __MODULE__
6+
7+
use Ecto.Schema
8+
use Accessible
9+
10+
import Ecto.Changeset
11+
import Helper.Utils, only: [get_config: 2]
12+
13+
alias GroupherServer.CMS
14+
alias CMS.Model.Works
15+
16+
@timestamps_opts [type: :utc_datetime_usec]
17+
18+
@max_body_length get_config(:article, :max_length)
19+
@min_body_length get_config(:article, :min_length)
20+
21+
@required_fields ~w(body body_html works_id)a
22+
@optional_fields []
23+
24+
@type t :: %WorksDocument{}
25+
schema "works_documents" do
26+
belongs_to(:works, Works, foreign_key: :works_id)
27+
28+
field(:body, :string)
29+
field(:body_html, :string)
30+
field(:toc, :map)
31+
end
32+
33+
@doc false
34+
def changeset(%WorksDocument{} = works, attrs) do
35+
works
36+
|> cast(attrs, @optional_fields ++ @required_fields)
37+
|> validate_required(@required_fields)
38+
|> validate_length(:body, min: @min_body_length, max: @max_body_length)
39+
end
40+
41+
@doc false
42+
def update_changeset(%WorksDocument{} = works, attrs) do
43+
works
44+
|> cast(attrs, @optional_fields ++ @required_fields)
45+
|> validate_length(:body, min: @min_body_length, max: @max_body_length)
46+
end
47+
end
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
defmodule GroupherServer.Test.Articles.Works do
2+
use GroupherServer.TestTools
3+
4+
alias GroupherServer.{CMS, Repo}
5+
alias Helper.Converter.{EditorToHTML, HtmlSanitizer}
6+
7+
alias EditorToHTML.{Class, Validator}
8+
alias CMS.Model.{Author, Works, Community, ArticleDocument, WorksDocument}
9+
alias Helper.ORM
10+
11+
@root_class Class.article()
12+
@last_year Timex.shift(Timex.beginning_of_year(Timex.now()), days: -3, seconds: -1)
13+
14+
setup do
15+
{:ok, user} = db_insert(:user)
16+
{:ok, user2} = db_insert(:user)
17+
{:ok, community} = db_insert(:community)
18+
19+
works_attrs = mock_attrs(:works, %{community_id: community.id})
20+
21+
{:ok, ~m(user user2 community works_attrs)a}
22+
end
23+
24+
describe "[cms workss curd]" do
25+
test "can create works with valid attrs", ~m(user community works_attrs)a do
26+
assert {:error, _} = ORM.find_by(Author, user_id: user.id)
27+
{:ok, works} = CMS.create_article(community, :works, works_attrs, user)
28+
works = Repo.preload(works, :document)
29+
30+
body_map = Jason.decode!(works.document.body)
31+
32+
assert works.meta.thread == "WORKS"
33+
34+
assert works.title == works_attrs.title
35+
assert body_map |> Validator.is_valid()
36+
37+
assert works.document.body_html
38+
|> String.contains?(~s(<div class="#{@root_class["viewer"]}">))
39+
40+
assert works.document.body_html |> String.contains?(~s(<p id="block-))
41+
42+
paragraph_text = body_map["blocks"] |> List.first() |> get_in(["data", "text"])
43+
assert works.digest == paragraph_text |> HtmlSanitizer.strip_all_tags()
44+
end
45+
46+
test "created works should have a acitve_at field, same with inserted_at",
47+
~m(user community works_attrs)a do
48+
{:ok, works} = CMS.create_article(community, :works, works_attrs, user)
49+
50+
assert works.active_at == works.inserted_at
51+
end
52+
53+
test "read works should update views and meta viewed_user_list",
54+
~m(works_attrs community user user2)a do
55+
{:ok, works} = CMS.create_article(community, :works, works_attrs, user)
56+
{:ok, _} = CMS.read_article(:works, works.id, user)
57+
{:ok, _created} = ORM.find(Works, works.id)
58+
59+
# same user duplicate case
60+
{:ok, _} = CMS.read_article(:works, works.id, user)
61+
{:ok, created} = ORM.find(Works, works.id)
62+
63+
assert created.meta.viewed_user_ids |> length == 1
64+
assert user.id in created.meta.viewed_user_ids
65+
66+
{:ok, _} = CMS.read_article(:works, works.id, user2)
67+
{:ok, created} = ORM.find(Works, works.id)
68+
69+
assert created.meta.viewed_user_ids |> length == 2
70+
assert user.id in created.meta.viewed_user_ids
71+
assert user2.id in created.meta.viewed_user_ids
72+
end
73+
74+
test "read works should contains viewer_has_xxx state",
75+
~m(works_attrs community user user2)a do
76+
{:ok, works} = CMS.create_article(community, :works, works_attrs, user)
77+
{:ok, works} = CMS.read_article(:works, works.id, user)
78+
79+
assert not works.viewer_has_collected
80+
assert not works.viewer_has_upvoted
81+
assert not works.viewer_has_reported
82+
83+
{:ok, works} = CMS.read_article(:works, works.id)
84+
85+
assert not works.viewer_has_collected
86+
assert not works.viewer_has_upvoted
87+
assert not works.viewer_has_reported
88+
89+
{:ok, works} = CMS.read_article(:works, works.id, user2)
90+
91+
assert not works.viewer_has_collected
92+
assert not works.viewer_has_upvoted
93+
assert not works.viewer_has_reported
94+
95+
{:ok, _} = CMS.upvote_article(:works, works.id, user)
96+
{:ok, _} = CMS.collect_article(:works, works.id, user)
97+
{:ok, _} = CMS.report_article(:works, works.id, "reason", "attr_info", user)
98+
99+
{:ok, works} = CMS.read_article(:works, works.id, user)
100+
101+
assert works.viewer_has_collected
102+
assert works.viewer_has_upvoted
103+
assert works.viewer_has_reported
104+
end
105+
106+
test "create works with an exsit community fails", ~m(user)a do
107+
invalid_attrs = mock_attrs(:works, %{community_id: non_exsit_id()})
108+
ivalid_community = %Community{id: non_exsit_id()}
109+
110+
assert {:error, _} = CMS.create_article(ivalid_community, :works, invalid_attrs, user)
111+
end
112+
end
113+
114+
describe "[cms works sink/undo_sink]" do
115+
test "if a works is too old, read works should update can_undo_sink flag",
116+
~m(user community works_attrs)a do
117+
{:ok, works} = CMS.create_article(community, :works, works_attrs, user)
118+
119+
assert works.meta.can_undo_sink
120+
121+
{:ok, works_last_year} = db_insert(:works, %{title: "last year", inserted_at: @last_year})
122+
{:ok, works_last_year} = CMS.read_article(:works, works_last_year.id)
123+
assert not works_last_year.meta.can_undo_sink
124+
125+
{:ok, works_last_year} = CMS.read_article(:works, works_last_year.id, user)
126+
assert not works_last_year.meta.can_undo_sink
127+
end
128+
129+
test "can sink a works", ~m(user community works_attrs)a do
130+
{:ok, works} = CMS.create_article(community, :works, works_attrs, user)
131+
assert not works.meta.is_sinked
132+
133+
{:ok, works} = CMS.sink_article(:works, works.id)
134+
assert works.meta.is_sinked
135+
assert works.active_at == works.inserted_at
136+
end
137+
138+
test "can undo sink works", ~m(user community works_attrs)a do
139+
{:ok, works} = CMS.create_article(community, :works, works_attrs, user)
140+
{:ok, works} = CMS.sink_article(:works, works.id)
141+
assert works.meta.is_sinked
142+
assert works.meta.last_active_at == works.active_at
143+
144+
{:ok, works} = CMS.undo_sink_article(:works, works.id)
145+
assert not works.meta.is_sinked
146+
assert works.active_at == works.meta.last_active_at
147+
end
148+
149+
test "can not undo sink to old works", ~m()a do
150+
{:ok, works_last_year} = db_insert(:works, %{title: "last year", inserted_at: @last_year})
151+
152+
{:error, reason} = CMS.undo_sink_article(:works, works_last_year.id)
153+
is_error?(reason, :undo_sink_old_article)
154+
end
155+
end
156+
157+
describe "[cms works document]" do
158+
test "will create related document after create", ~m(user community works_attrs)a do
159+
{:ok, works} = CMS.create_article(community, :works, works_attrs, user)
160+
{:ok, works} = CMS.read_article(:works, works.id)
161+
assert not is_nil(works.document.body_html)
162+
{:ok, works} = CMS.read_article(:works, works.id, user)
163+
assert not is_nil(works.document.body_html)
164+
165+
{:ok, article_doc} = ORM.find_by(ArticleDocument, %{article_id: works.id, thread: "WORKS"})
166+
{:ok, works_doc} = ORM.find_by(WorksDocument, %{works_id: works.id})
167+
168+
assert works.document.body == works_doc.body
169+
assert article_doc.body == works_doc.body
170+
end
171+
172+
test "delete works should also delete related document", ~m(user community works_attrs)a do
173+
{:ok, works} = CMS.create_article(community, :works, works_attrs, user)
174+
{:ok, _article_doc} = ORM.find_by(ArticleDocument, %{article_id: works.id, thread: "WORKS"})
175+
{:ok, _works_doc} = ORM.find_by(WorksDocument, %{works_id: works.id})
176+
177+
{:ok, _} = CMS.delete_article(works)
178+
179+
{:error, _} = ORM.find(Works, works.id)
180+
{:error, _} = ORM.find_by(ArticleDocument, %{article_id: works.id, thread: "WORKS"})
181+
{:error, _} = ORM.find_by(WorksDocument, %{works_id: works.id})
182+
end
183+
184+
test "update works should also update related document", ~m(user community works_attrs)a do
185+
{:ok, works} = CMS.create_article(community, :works, works_attrs, user)
186+
187+
body = mock_rich_text(~s(new content))
188+
{:ok, works} = CMS.update_article(works, %{body: body})
189+
190+
{:ok, article_doc} = ORM.find_by(ArticleDocument, %{article_id: works.id, thread: "WORKS"})
191+
{:ok, works_doc} = ORM.find_by(WorksDocument, %{works_id: works.id})
192+
193+
assert String.contains?(works_doc.body, "new content")
194+
assert String.contains?(article_doc.body, "new content")
195+
end
196+
end
197+
end

test/support/factory.ex

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,26 @@ defmodule GroupherServer.Support.Factory do
219219
}
220220
end
221221

222+
defp mock_meta(:works) do
223+
text = Faker.Lorem.sentence(%Range{first: 80, last: 120})
224+
225+
%{
226+
meta: @default_article_meta |> Map.merge(%{thread: "BLOG"}),
227+
title: String.slice(text, 1, 49),
228+
body: mock_rich_text(),
229+
digest: String.slice(text, 1, 150),
230+
# length: String.length(text),
231+
author: mock(:author),
232+
views: Enum.random(0..2000),
233+
original_community: mock(:community),
234+
communities: [
235+
mock(:community)
236+
],
237+
emotions: @default_emotions,
238+
active_at: Timex.shift(Timex.now(), seconds: +1)
239+
}
240+
end
241+
222242
defp mock_meta(:comment) do
223243
# text = Faker.Lorem.sentence(%Range{first: 30, last: 80})
224244

0 commit comments

Comments
 (0)