create posts table
This commit is contained in:
parent
d7ac169607
commit
b051e06454
21 changed files with 577 additions and 41 deletions
.credo.exs.formatter.exs
config
lib
mix.exsmix.lockpriv/repo/migrations
test
56
.credo.exs
56
.credo.exs
|
@ -100,34 +100,34 @@
|
||||||
# Some of the rules have `priority: :high`, meaning Credo runs them unless we explicitly disable them
|
# Some of the rules have `priority: :high`, meaning Credo runs them unless we explicitly disable them
|
||||||
# (removing them from this file wouldn't be enough, the `false` is required)
|
# (removing them from this file wouldn't be enough, the `false` is required)
|
||||||
#
|
#
|
||||||
{Credo.Check.Consistency.MultiAliasImportRequireUse, false},
|
# {Credo.Check.Consistency.MultiAliasImportRequireUse, false},
|
||||||
{Credo.Check.Consistency.ParameterPatternMatching, false},
|
# {Credo.Check.Consistency.ParameterPatternMatching, false},
|
||||||
{Credo.Check.Design.AliasUsage, false},
|
# {Credo.Check.Design.AliasUsage, false},
|
||||||
{Credo.Check.Readability.AliasOrder, false},
|
# {Credo.Check.Readability.AliasOrder, false},
|
||||||
{Credo.Check.Readability.BlockPipe, false},
|
# {Credo.Check.Readability.BlockPipe, false},
|
||||||
{Credo.Check.Readability.LargeNumbers, false},
|
# {Credo.Check.Readability.LargeNumbers, false},
|
||||||
{Credo.Check.Readability.ModuleDoc, false},
|
# {Credo.Check.Readability.ModuleDoc, false},
|
||||||
{Credo.Check.Readability.MultiAlias, false},
|
# {Credo.Check.Readability.MultiAlias, false},
|
||||||
{Credo.Check.Readability.OneArityFunctionInPipe, false},
|
# {Credo.Check.Readability.OneArityFunctionInPipe, false},
|
||||||
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, false},
|
# {Credo.Check.Readability.ParenthesesOnZeroArityDefs, false},
|
||||||
{Credo.Check.Readability.PipeIntoAnonymousFunctions, false},
|
# {Credo.Check.Readability.PipeIntoAnonymousFunctions, false},
|
||||||
{Credo.Check.Readability.PreferImplicitTry, false},
|
# {Credo.Check.Readability.PreferImplicitTry, false},
|
||||||
{Credo.Check.Readability.SinglePipe, false},
|
# {Credo.Check.Readability.SinglePipe, false},
|
||||||
{Credo.Check.Readability.StrictModuleLayout, false},
|
# {Credo.Check.Readability.StrictModuleLayout, false},
|
||||||
{Credo.Check.Readability.StringSigils, false},
|
# {Credo.Check.Readability.StringSigils, false},
|
||||||
{Credo.Check.Readability.UnnecessaryAliasExpansion, false},
|
# {Credo.Check.Readability.UnnecessaryAliasExpansion, false},
|
||||||
{Credo.Check.Readability.WithSingleClause, false},
|
# {Credo.Check.Readability.WithSingleClause, false},
|
||||||
{Credo.Check.Refactor.CaseTrivialMatches, false},
|
# {Credo.Check.Refactor.CaseTrivialMatches, false},
|
||||||
{Credo.Check.Refactor.CondStatements, false},
|
# {Credo.Check.Refactor.CondStatements, false},
|
||||||
{Credo.Check.Refactor.FilterCount, false},
|
# {Credo.Check.Refactor.FilterCount, false},
|
||||||
{Credo.Check.Refactor.MapInto, false},
|
# {Credo.Check.Refactor.MapInto, false},
|
||||||
{Credo.Check.Refactor.MapJoin, false},
|
# {Credo.Check.Refactor.MapJoin, false},
|
||||||
{Credo.Check.Refactor.NegatedConditionsInUnless, false},
|
# {Credo.Check.Refactor.NegatedConditionsInUnless, false},
|
||||||
{Credo.Check.Refactor.NegatedConditionsWithElse, false},
|
# {Credo.Check.Refactor.NegatedConditionsWithElse, false},
|
||||||
{Credo.Check.Refactor.PipeChainStart, false},
|
# {Credo.Check.Refactor.PipeChainStart, false},
|
||||||
{Credo.Check.Refactor.RedundantWithClauseResult, false},
|
# {Credo.Check.Refactor.RedundantWithClauseResult, false},
|
||||||
{Credo.Check.Refactor.UnlessWithElse, false},
|
# {Credo.Check.Refactor.UnlessWithElse, false},
|
||||||
{Credo.Check.Refactor.WithClauses, false},
|
# {Credo.Check.Refactor.WithClauses, false},
|
||||||
|
|
||||||
#
|
#
|
||||||
# Checks scheduled for next check update (opt-in for now)
|
# Checks scheduled for next check update (opt-in for now)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[
|
[
|
||||||
import_deps: [:ecto, :ecto_sql, :phoenix],
|
import_deps: [:ecto, :ecto_sql, :phoenix],
|
||||||
subdirectories: ["priv/*/migrations"],
|
subdirectories: ["priv/*/migrations"],
|
||||||
plugins: [Styler, Phoenix.LiveView.HTMLFormatter],
|
plugins: [Phoenix.LiveView.HTMLFormatter],
|
||||||
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
|
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
|
||||||
]
|
]
|
||||||
|
|
|
@ -3,7 +3,8 @@ import Config
|
||||||
config :esbuild,
|
config :esbuild,
|
||||||
version: "0.17.11",
|
version: "0.17.11",
|
||||||
sloanely_but_surely: [
|
sloanely_but_surely: [
|
||||||
args: ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
|
args:
|
||||||
|
~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
|
||||||
cd: Path.expand("../assets", __DIR__),
|
cd: Path.expand("../assets", __DIR__),
|
||||||
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
|
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
|
||||||
]
|
]
|
||||||
|
@ -27,7 +28,9 @@ config :sloanely_but_surely, Web.Endpoint,
|
||||||
config :sloanely_but_surely,
|
config :sloanely_but_surely,
|
||||||
namespace: Core,
|
namespace: Core,
|
||||||
ecto_repos: [Core.Repo],
|
ecto_repos: [Core.Repo],
|
||||||
generators: [timestamp_type: :utc_datetime, binary_id: true]
|
generators: [timestamp_type: :utc_datetime_usec, binary_id: true]
|
||||||
|
|
||||||
|
config :sloanely_but_surely, Core.Repo, migration_timestamps: [type: :utc_datetime_usec]
|
||||||
|
|
||||||
config :tailwind,
|
config :tailwind,
|
||||||
version: "3.4.3",
|
version: "3.4.3",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Core do
|
defmodule Core do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Boundary, deps: [Schema], exports: [Accounts]
|
use Boundary, deps: [Schema], exports: [Accounts, Posts]
|
||||||
end
|
end
|
||||||
|
|
55
lib/core/posts.ex
Normal file
55
lib/core/posts.ex
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
defmodule Core.Posts do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
alias Core.Posts.Post
|
||||||
|
|
||||||
|
def get!(id) do
|
||||||
|
Core.Repo.get!(Schema.Post, id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_by_publish_date_and_slug!(%Date{} = publish_date, slug) when is_binary(slug) do
|
||||||
|
publish_date
|
||||||
|
|> Post.Query.where_publish_date_and_slug(slug)
|
||||||
|
|> Core.Repo.one!()
|
||||||
|
end
|
||||||
|
|
||||||
|
def change_post(%Schema.Post{} = post \\ %Schema.Post{}, attrs) do
|
||||||
|
Post.content_changeset(post, attrs)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_post(attrs) do
|
||||||
|
attrs
|
||||||
|
|> change_post()
|
||||||
|
|> Core.Repo.insert()
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_post(%Schema.Post{} = post, attrs) do
|
||||||
|
post
|
||||||
|
|> change_post(attrs)
|
||||||
|
|> Core.Repo.update()
|
||||||
|
end
|
||||||
|
|
||||||
|
def publish_post(%Schema.Post{} = post, published_at \\ DateTime.utc_now()) do
|
||||||
|
post
|
||||||
|
|> Post.publish_changeset(published_at)
|
||||||
|
|> Core.Repo.update()
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_post(%Schema.Post{} = post, deleted_at \\ DateTime.utc_now()) do
|
||||||
|
post
|
||||||
|
|> Post.delete_changeset(deleted_at)
|
||||||
|
|> Core.Repo.update()
|
||||||
|
end
|
||||||
|
|
||||||
|
def unpublish_post(%Schema.Post{} = post) do
|
||||||
|
post
|
||||||
|
|> Post.unpublish_changeset()
|
||||||
|
|> Core.Repo.update()
|
||||||
|
end
|
||||||
|
|
||||||
|
def undelete_post(%Schema.Post{} = post) do
|
||||||
|
post
|
||||||
|
|> Post.undelete_changeset()
|
||||||
|
|> Core.Repo.update()
|
||||||
|
end
|
||||||
|
end
|
100
lib/core/posts/post.ex
Normal file
100
lib/core/posts/post.ex
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
defmodule Core.Posts.Post do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
def content_changeset(%Schema.Post{} = post, attrs) do
|
||||||
|
changeset =
|
||||||
|
post
|
||||||
|
|> cast(attrs, [:tid, :kind, :slug, :title, :body])
|
||||||
|
|> validate_required([:kind], message: "must have a kind")
|
||||||
|
|> validate_required([:body], message: "must have a body")
|
||||||
|
|
||||||
|
changeset =
|
||||||
|
case get_field(changeset, :kind) do
|
||||||
|
:blog ->
|
||||||
|
validate_required(changeset, [:title, :slug])
|
||||||
|
|
||||||
|
:status ->
|
||||||
|
changeset
|
||||||
|
|> put_change(:slug, nil)
|
||||||
|
|> put_change(:title, nil)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
changeset
|
||||||
|
end
|
||||||
|
|
||||||
|
changeset
|
||||||
|
|> validate_format(:slug, ~r/^[a-z](?:[a-z-]*[a-z])?$/)
|
||||||
|
|> unique_constraint([:slug])
|
||||||
|
end
|
||||||
|
|
||||||
|
def publish_changeset(%Schema.Post{} = post, published_at) do
|
||||||
|
changeset = change(post)
|
||||||
|
|
||||||
|
if is_nil(get_field(changeset, :published_at)) do
|
||||||
|
put_change(changeset, :published_at, published_at)
|
||||||
|
else
|
||||||
|
changeset
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_changeset(%Schema.Post{} = post, deleted_at) do
|
||||||
|
changeset = change(post)
|
||||||
|
|
||||||
|
if is_nil(get_field(changeset, :deleted_at)) do
|
||||||
|
put_change(changeset, :deleted_at, deleted_at)
|
||||||
|
else
|
||||||
|
changeset
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def unpublish_changeset(%Schema.Post{} = post) do
|
||||||
|
post
|
||||||
|
|> change()
|
||||||
|
|> put_change(:published_at, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def undelete_changeset(%Schema.Post{} = post) do
|
||||||
|
post
|
||||||
|
|> change()
|
||||||
|
|> put_change(:deleted_at, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule Query do
|
||||||
|
@moduledoc false
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
def base do
|
||||||
|
from _ in Schema.Post, as: :posts
|
||||||
|
end
|
||||||
|
|
||||||
|
def current(query \\ base()) do
|
||||||
|
where(query, [posts: p], is_nil(p.deleted_at))
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_order(query \\ base()) do
|
||||||
|
order_by(query, [posts: p], desc: :inserted_at, desc: :updated_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
def where_publish_date_and_slug(query \\ current(), publish_date, slug) do
|
||||||
|
where(
|
||||||
|
query,
|
||||||
|
[posts: p],
|
||||||
|
p.slug == ^slug and
|
||||||
|
fragment("(? at time zone 'UTC' at time zone 'America/New_York')::date", p.published_at) ==
|
||||||
|
^publish_date
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def published(query \\ base()) do
|
||||||
|
query
|
||||||
|
|> current()
|
||||||
|
|> where([posts: p], not is_nil(p.published_at))
|
||||||
|
end
|
||||||
|
|
||||||
|
def deleted(query \\ base()) do
|
||||||
|
where(query, [posts: p], not is_nil(p.deleted_at))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,4 +1,15 @@
|
||||||
defmodule Schema do
|
defmodule Schema do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Boundary, deps: [], exports: [User, UserToken]
|
use Boundary, deps: [], exports: [Post, User, UserToken]
|
||||||
|
|
||||||
|
defmacro __using__(_) do
|
||||||
|
quote do
|
||||||
|
use Ecto.Schema
|
||||||
|
|
||||||
|
@primary_key {:id, :binary_id, autogenerate: true}
|
||||||
|
@foreign_key_type :binary_id
|
||||||
|
|
||||||
|
@type t() :: %__MODULE__{}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
21
lib/schema/post.ex
Normal file
21
lib/schema/post.ex
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
defmodule Schema.Post do
|
||||||
|
@moduledoc false
|
||||||
|
use Schema
|
||||||
|
|
||||||
|
@post_kinds ~w[status blog]a
|
||||||
|
|
||||||
|
schema "posts" do
|
||||||
|
field :tid, :string
|
||||||
|
field :kind, Ecto.Enum, values: @post_kinds
|
||||||
|
field :slug, :string
|
||||||
|
field :title, :string
|
||||||
|
field :body, :string
|
||||||
|
|
||||||
|
field :published_at, :utc_datetime_usec
|
||||||
|
field :deleted_at, :utc_datetime_usec
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
def kinds, do: @post_kinds
|
||||||
|
end
|
|
@ -1,9 +1,7 @@
|
||||||
defmodule Schema.User do
|
defmodule Schema.User do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ecto.Schema
|
use Schema
|
||||||
|
|
||||||
@primary_key {:id, :binary_id, autogenerate: true}
|
|
||||||
@foreign_key_type :binary_id
|
|
||||||
schema "users" do
|
schema "users" do
|
||||||
field :username, :string
|
field :username, :string
|
||||||
field :password, :string, virtual: true, redact: true
|
field :password, :string, virtual: true, redact: true
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
defmodule Schema.UserToken do
|
defmodule Schema.UserToken do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ecto.Schema
|
use Schema
|
||||||
|
|
||||||
@primary_key {:id, :binary_id, autogenerate: true}
|
|
||||||
@foreign_key_type :binary_id
|
|
||||||
schema "users_tokens" do
|
schema "users_tokens" do
|
||||||
field :token, :binary
|
field :token, :binary
|
||||||
field :context, :string
|
field :context, :string
|
||||||
|
|
21
lib/web/controllers/post_controller.ex
Normal file
21
lib/web/controllers/post_controller.ex
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
defmodule Web.PostController do
|
||||||
|
use Web, :controller
|
||||||
|
|
||||||
|
def show(conn, %{"year" => year, "month" => month, "day" => day, "slug" => slug}) do
|
||||||
|
with {year, ""} <- Integer.parse(year),
|
||||||
|
{month, ""} <- Integer.parse(month),
|
||||||
|
{day, ""} <- Integer.parse(day),
|
||||||
|
{:ok, publish_date} <- Date.new(year, month, day) do
|
||||||
|
post = Core.Posts.get_by_publish_date_and_slug!(publish_date, slug)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> render_post(post)
|
||||||
|
else
|
||||||
|
_ -> raise Ecto.NoResultsError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_post(conn, %Schema.Post{kind: :blog} = blog) do
|
||||||
|
render(conn, :show_blog, blog: blog)
|
||||||
|
end
|
||||||
|
end
|
5
lib/web/controllers/post_html.ex
Normal file
5
lib/web/controllers/post_html.ex
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
defmodule Web.PostHTML do
|
||||||
|
use Web, :html
|
||||||
|
|
||||||
|
embed_templates "post_html/*"
|
||||||
|
end
|
9
lib/web/controllers/post_html/show_blog.html.heex
Normal file
9
lib/web/controllers/post_html/show_blog.html.heex
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<article>
|
||||||
|
<header class="mb-2">
|
||||||
|
<h1 class="text-2xl font-bold">{@blog.title}</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<p>{@blog.body}</p>
|
||||||
|
|
||||||
|
<%!-- <footer class="mt-4 border-t border-gray-200"></footer> --%>
|
||||||
|
</article>
|
|
@ -40,6 +40,8 @@ defmodule Web.Router do
|
||||||
|
|
||||||
delete "/admin/users/log_out", UserSessionController, :delete
|
delete "/admin/users/log_out", UserSessionController, :delete
|
||||||
|
|
||||||
|
get "/:year/:month/:day/:slug", PostController, :show
|
||||||
|
|
||||||
# live_session :current_user, on_mount: [{Web.UserAuth, :mount_current_user}] do
|
# live_session :current_user, on_mount: [{Web.UserAuth, :mount_current_user}] do
|
||||||
# end
|
# end
|
||||||
end
|
end
|
||||||
|
|
4
mix.exs
4
mix.exs
|
@ -55,8 +55,8 @@ defmodule SlaonelyButSurely.MixProject do
|
||||||
{:boundary, "~> 0.10.4"},
|
{:boundary, "~> 0.10.4"},
|
||||||
|
|
||||||
# Added dev and/or test dependencies
|
# Added dev and/or test dependencies
|
||||||
{:styler, "~> 1.4", only: [:dev, :test], runtime: false},
|
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
|
{:faker, "~> 0.18", only: :test}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
1
mix.lock
1
mix.lock
|
@ -16,6 +16,7 @@
|
||||||
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
||||||
"esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"},
|
"esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"},
|
||||||
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
|
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
|
||||||
|
"faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"},
|
||||||
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
|
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
|
||||||
"floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"},
|
"floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"},
|
||||||
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
|
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
|
||||||
|
|
35
priv/repo/migrations/20250325174616_create_posts_table.exs
Normal file
35
priv/repo/migrations/20250325174616_create_posts_table.exs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
defmodule Core.Repo.Migrations.CreatePostsTable do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:posts, primary_key: false) do
|
||||||
|
add :id, :uuid, primary_key: true
|
||||||
|
add :tid, :text
|
||||||
|
|
||||||
|
add :kind, :text, null: false
|
||||||
|
add :body, :text, null: false
|
||||||
|
|
||||||
|
# blog only fields
|
||||||
|
add :slug, :text
|
||||||
|
add :title, :text
|
||||||
|
|
||||||
|
add :published_at, :utc_datetime_usec
|
||||||
|
add :deleted_at, :utc_datetime_usec
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
create index(:posts, [:kind])
|
||||||
|
create unique_index(:posts, [:slug])
|
||||||
|
create index(:posts, [:published_at])
|
||||||
|
|
||||||
|
create index(
|
||||||
|
:posts,
|
||||||
|
["((published_at at time zone 'UTC' at time zone 'America/New_York')::date)"],
|
||||||
|
name: :posts_published_at_date_index
|
||||||
|
)
|
||||||
|
|
||||||
|
create index(:posts, [:deleted_at])
|
||||||
|
create index(:posts, [:inserted_at])
|
||||||
|
create index(:posts, [:updated_at])
|
||||||
|
end
|
||||||
|
end
|
194
test/core/posts_test.exs
Normal file
194
test/core/posts_test.exs
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
defmodule Test.Core.PostsTest do
|
||||||
|
use Test.DataCase, async: true
|
||||||
|
|
||||||
|
describe "get!/1" do
|
||||||
|
@tag post: :post
|
||||||
|
test "returns the post with the given id", %{posts: %{post: post}} do
|
||||||
|
assert result = Core.Posts.get!(post.id)
|
||||||
|
assert %Schema.Post{} = result
|
||||||
|
assert result.id == post.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "raises an Ecto.NoResultsError if there is no matching post" do
|
||||||
|
assert_raise Ecto.NoResultsError, fn ->
|
||||||
|
Core.Posts.get!(Faker.UUID.v4())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "get_by_publish_date_and_slug!/2" do
|
||||||
|
@tag blog: :blog
|
||||||
|
test "returns the post with the given publish date and slug", %{blogs: %{blog: blog}} do
|
||||||
|
assert {:ok, blog} = Core.Posts.publish_post(blog)
|
||||||
|
publish_date = DateTime.to_date(blog.published_at)
|
||||||
|
assert result = Core.Posts.get_by_publish_date_and_slug!(publish_date, blog.slug)
|
||||||
|
assert %Schema.Post{} = result
|
||||||
|
assert result.id == blog.id
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag blog: :blog
|
||||||
|
test "raises an Ecto.NoResultsError if there is no matching post", %{blogs: %{blog: blog}} do
|
||||||
|
assert_raise Ecto.NoResultsError, fn ->
|
||||||
|
Core.Posts.get_by_publish_date_and_slug!(~D[1993-11-28], "first-day-on-earth")
|
||||||
|
end
|
||||||
|
|
||||||
|
assert {:ok, blog} = Core.Posts.publish_post(blog)
|
||||||
|
|
||||||
|
assert_raise Ecto.NoResultsError, fn ->
|
||||||
|
Core.Posts.get_by_publish_date_and_slug!(~D[1993-11-28], blog.slug)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "change_post/1/2" do
|
||||||
|
test "returns a changeset" do
|
||||||
|
assert %Ecto.Changeset{} = Core.Posts.change_post(%{})
|
||||||
|
assert %Ecto.Changeset{} = Core.Posts.change_post(%Schema.Post{}, %{})
|
||||||
|
|
||||||
|
assert %Ecto.Changeset{} =
|
||||||
|
Test.Fixtures.Posts.blog_post(:blog)
|
||||||
|
|> Core.Posts.change_post()
|
||||||
|
|
||||||
|
assert %Ecto.Changeset{} =
|
||||||
|
%Schema.Post{}
|
||||||
|
|> Core.Posts.change_post(Test.Fixtures.Posts.blog_post(:blog))
|
||||||
|
|
||||||
|
assert %Ecto.Changeset{} =
|
||||||
|
Test.Fixtures.Posts.status_post(:status)
|
||||||
|
|> Core.Posts.change_post()
|
||||||
|
|
||||||
|
assert %Ecto.Changeset{} =
|
||||||
|
%Schema.Post{}
|
||||||
|
|> Core.Posts.change_post(Test.Fixtures.Posts.status_post(:status))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "create_post/1" do
|
||||||
|
test "creates a status post" do
|
||||||
|
assert {:ok, %Schema.Post{}} =
|
||||||
|
Test.Fixtures.Posts.status_post(:status)
|
||||||
|
|> Core.Posts.create_post()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "creates a blug post" do
|
||||||
|
assert {:ok, %Schema.Post{}} =
|
||||||
|
Test.Fixtures.Posts.blog_post(:blog)
|
||||||
|
|> Core.Posts.create_post()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns an error changeset for invalid attrs" do
|
||||||
|
assert {:error, %Ecto.Changeset{} = changeset} = Core.Posts.create_post(%{})
|
||||||
|
refute changeset.valid?
|
||||||
|
assert "must have a kind" in errors_on(changeset).kind
|
||||||
|
assert "must have a body" in errors_on(changeset).body
|
||||||
|
|
||||||
|
assert {:error, %Ecto.Changeset{} = changeset} = Core.Posts.create_post(%{kind: :blog})
|
||||||
|
refute changeset.valid?
|
||||||
|
assert "must have a body" in errors_on(changeset).body
|
||||||
|
assert "can't be blank" in errors_on(changeset).title
|
||||||
|
assert "can't be blank" in errors_on(changeset).slug
|
||||||
|
|
||||||
|
assert {:error, %Ecto.Changeset{} = changeset} =
|
||||||
|
Core.Posts.create_post(%{
|
||||||
|
kind: :blog,
|
||||||
|
title: "Hello, World!",
|
||||||
|
slug: "hello-world!",
|
||||||
|
body: "This is a sample blog posts."
|
||||||
|
})
|
||||||
|
|
||||||
|
refute changeset.valid?
|
||||||
|
assert "has invalid format" in errors_on(changeset).slug
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update_post/2" do
|
||||||
|
@tag blog: :to_update
|
||||||
|
test "updates a blog post", %{blogs: %{to_update: post}} do
|
||||||
|
assert {:ok, %Schema.Post{} = post} = Core.Posts.update_post(post, %{title: "A new title"})
|
||||||
|
assert post.tid == "to_update"
|
||||||
|
assert post.title == "A new title"
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag status: :to_update
|
||||||
|
test "updates a status post", %{statuses: %{to_update: post}} do
|
||||||
|
assert {:ok, %Schema.Post{} = post} = Core.Posts.update_post(post, %{body: "Hello, World!"})
|
||||||
|
assert post.tid == "to_update"
|
||||||
|
assert post.body == "Hello, World!"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "publish_post/1/2" do
|
||||||
|
@describetag post: :to_publish
|
||||||
|
|
||||||
|
test "publishes a post", %{posts: %{to_publish: to_publish}} do
|
||||||
|
refute to_publish.published_at
|
||||||
|
assert {:ok, published} = Core.Posts.publish_post(to_publish)
|
||||||
|
assert published.tid == "to_publish"
|
||||||
|
assert published.published_at
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not update an already published post", %{posts: %{to_publish: to_publish}} do
|
||||||
|
assert {:ok, published} = Core.Posts.publish_post(to_publish)
|
||||||
|
assert {:ok, published_again} = Core.Posts.publish_post(published)
|
||||||
|
assert published.updated_at == published_again.updated_at
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "delete_post/1/2" do
|
||||||
|
@describetag post: :to_delete
|
||||||
|
|
||||||
|
test "soft deletes a post", %{posts: %{to_delete: post}} do
|
||||||
|
refute post.deleted_at
|
||||||
|
assert {:ok, post} = Core.Posts.delete_post(post)
|
||||||
|
assert post.deleted_at
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not update an already deleted post", %{posts: %{to_delete: to_delete}} do
|
||||||
|
assert {:ok, deleted} = Core.Posts.delete_post(to_delete)
|
||||||
|
assert {:ok, deleted_again} = Core.Posts.delete_post(deleted)
|
||||||
|
assert deleted.updated_at == deleted_again.updated_at
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "unpublish_post/1" do
|
||||||
|
@describetag posts: [:published, :not_published]
|
||||||
|
setup %{posts: %{published: published, not_published: not_published}} do
|
||||||
|
{:ok, published} = Core.Posts.publish_post(published)
|
||||||
|
|
||||||
|
[posts: %{published: published, not_published: not_published}]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "unpublishes a post", %{posts: %{published: published}} do
|
||||||
|
assert published.published_at
|
||||||
|
assert {:ok, unpublished} = Core.Posts.unpublish_post(published)
|
||||||
|
refute unpublished.published_at
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not update a post that isn't published", %{posts: %{not_published: not_published}} do
|
||||||
|
refute not_published.published_at
|
||||||
|
assert {:ok, not_updated} = Core.Posts.unpublish_post(not_published)
|
||||||
|
assert not_published.updated_at == not_updated.updated_at
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "undelete_post/1" do
|
||||||
|
@describetag posts: [:deleted, :not_deleted]
|
||||||
|
setup %{posts: %{deleted: deleted, not_deleted: not_deleted}} do
|
||||||
|
{:ok, deleted} = Core.Posts.delete_post(deleted)
|
||||||
|
|
||||||
|
[posts: %{deleted: deleted, not_deleted: not_deleted}]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "undeletees a post", %{posts: %{deleted: deleted}} do
|
||||||
|
assert deleted.deleted_at
|
||||||
|
assert {:ok, undeleted} = Core.Posts.undelete_post(deleted)
|
||||||
|
refute undeleted.deleted_at
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not update a post that isn't deleted", %{posts: %{not_deleted: not_deleted}} do
|
||||||
|
refute not_deleted.deleted_at
|
||||||
|
assert {:ok, not_updated} = Core.Posts.undelete_post(not_deleted)
|
||||||
|
assert not_deleted.updated_at == not_updated.updated_at
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -16,6 +16,8 @@ defmodule Test.DataCase do
|
||||||
|
|
||||||
use ExUnit.CaseTemplate
|
use ExUnit.CaseTemplate
|
||||||
|
|
||||||
|
import Test.SharedSetup
|
||||||
|
|
||||||
alias Ecto.Adapters.SQL.Sandbox
|
alias Ecto.Adapters.SQL.Sandbox
|
||||||
|
|
||||||
using do
|
using do
|
||||||
|
@ -34,6 +36,8 @@ defmodule Test.DataCase do
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
setup [:handle_post_tags, :handle_blog_tags, :handle_status_tags]
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Sets up the sandbox based on the test tags.
|
Sets up the sandbox based on the test tags.
|
||||||
"""
|
"""
|
||||||
|
|
31
test/support/test/fixtures/posts.ex
vendored
Normal file
31
test/support/test/fixtures/posts.ex
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
defmodule Test.Fixtures.Posts do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
def status_post(tid, attrs \\ %{}) do
|
||||||
|
Enum.into(attrs, %{
|
||||||
|
tid: "#{tid}",
|
||||||
|
kind: :status,
|
||||||
|
body: Faker.Lorem.paragraph(),
|
||||||
|
title: nil,
|
||||||
|
status: nil
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
def blog_post(tid, attrs \\ %{}) do
|
||||||
|
title = Faker.Lorem.sentence()
|
||||||
|
|
||||||
|
slug =
|
||||||
|
title
|
||||||
|
|> String.downcase()
|
||||||
|
|> String.replace(~r/[^\w]/, "-")
|
||||||
|
|> String.trim("-")
|
||||||
|
|
||||||
|
Enum.into(attrs, %{
|
||||||
|
tid: "#{tid}",
|
||||||
|
kind: :blog,
|
||||||
|
title: title,
|
||||||
|
slug: slug,
|
||||||
|
body: Enum.join(Faker.Lorem.paragraphs(), "\n\n")
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
48
test/support/test/shared_setup.ex
Normal file
48
test/support/test/shared_setup.ex
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
defmodule Test.SharedSetup do
|
||||||
|
def handle_post_tags(tags) do
|
||||||
|
posts =
|
||||||
|
for tid <- get_test_ids(tags, [:post, :posts]), into: %{} do
|
||||||
|
fun = Enum.random([&Test.Fixtures.Posts.blog_post/1, &Test.Fixtures.Posts.status_post/1])
|
||||||
|
{:ok, post} = Core.Posts.create_post(fun.(tid))
|
||||||
|
{tid, post}
|
||||||
|
end
|
||||||
|
|
||||||
|
[posts: posts]
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_blog_tags(tags) do
|
||||||
|
blogs =
|
||||||
|
for tid <- get_test_ids(tags, [:blog, :blogs]), into: %{} do
|
||||||
|
{:ok, blog} =
|
||||||
|
Test.Fixtures.Posts.blog_post(tid)
|
||||||
|
|> Core.Posts.create_post()
|
||||||
|
|
||||||
|
{tid, blog}
|
||||||
|
end
|
||||||
|
|
||||||
|
[blogs: blogs]
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_status_tags(tags) do
|
||||||
|
statuses =
|
||||||
|
for tid <- get_test_ids(tags, [:status, :statuses]), into: %{} do
|
||||||
|
{:ok, status} =
|
||||||
|
Test.Fixtures.Posts.status_post(tid)
|
||||||
|
|> Core.Posts.create_post()
|
||||||
|
|
||||||
|
{tid, status}
|
||||||
|
end
|
||||||
|
|
||||||
|
[statuses: statuses]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_test_ids(tags, keys) do
|
||||||
|
keys
|
||||||
|
|> Enum.flat_map(fn key ->
|
||||||
|
tags
|
||||||
|
|> Map.get(key)
|
||||||
|
|> List.wrap()
|
||||||
|
end)
|
||||||
|
|> Enum.uniq()
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue