diff --git a/lib/philomena/posts.ex b/lib/philomena/posts.ex index 61e201f3..ec759d5f 100644 --- a/lib/philomena/posts.ex +++ b/lib/philomena/posts.ex @@ -4,22 +4,12 @@ defmodule Philomena.Posts do """ import Ecto.Query, warn: false + alias Ecto.Multi alias Philomena.Repo + alias Philomena.Topics.Topic alias Philomena.Posts.Post - - @doc """ - Returns the list of posts. - - ## Examples - - iex> list_posts() - [%Post{}, ...] - - """ - def list_posts do - Repo.all(Post) - end + alias Philomena.Notifications @doc """ Gets a single post. @@ -49,10 +39,60 @@ defmodule Philomena.Posts do {:error, %Ecto.Changeset{}} """ - def create_post(attrs \\ %{}) do - %Post{} - |> Post.changeset(attrs) - |> Repo.insert() + def create_post(topic, user, attributes, params \\ %{}) do + topic_query = + Topic + |> where(id: ^topic.id) + + Multi.new + |> Multi.run(:post, fn repo, _ -> + last_position = + Post + |> where(topic_id: ^topic.id) + |> order_by(desc: :topic_position) + |> select([p], p.topic_position) + |> limit(1) + |> repo.one() + + Ecto.build_assoc(topic, :posts, [topic_position: (last_position || -1) + 1] ++ attributes) + |> Post.creation_changeset(user, params) + |> repo.insert() + end) + |> Multi.run(:update_topic, fn repo, %{post: %{id: post_id}} -> + {count, nil} = + repo.update_all(topic_query, inc: [post_count: 1], set: [last_post_id: post_id]) + + {:ok, count} + end) + |> Repo.isolated_transaction(:serializable) + end + + def notify_post(post) do + spawn fn -> + topic = + post + |> Repo.preload(:topic) + |> Map.fetch!(:topic) + + subscriptions = + topic + |> Repo.preload(:subscriptions) + |> Map.fetch!(:subscriptions) + + Notifications.notify( + post, + subscriptions, + %{ + actor_id: topic.id, + actor_type: "Topic", + actor_child_id: post.id, + actor_child_type: "Post", + action: "posted a new reply in" + } + ) + end + + post end @doc """ @@ -101,4 +141,20 @@ defmodule Philomena.Posts do def change_post(%Post{} = post) do Post.changeset(post, %{}) end + + def reindex_post(%Post{} = post) do + spawn fn -> + Post + |> preload(^indexing_preloads()) + |> where(id: ^post.id) + |> Repo.one() + |> Post.index_document() + end + + post + end + + def indexing_preloads do + [:user, topic: :forum] + end end diff --git a/lib/philomena/posts/post.ex b/lib/philomena/posts/post.ex index 9364ef6d..7413ac9c 100644 --- a/lib/philomena/posts/post.ex +++ b/lib/philomena/posts/post.ex @@ -38,4 +38,18 @@ defmodule Philomena.Posts.Post do |> cast(attrs, []) |> validate_required([]) end + + @doc false + def creation_changeset(post, user, attrs) do + post + |> cast(attrs, [:body, :anonymous]) + |> set_name_at_post_time(user) + |> validate_required([:body]) + |> validate_length(:body, min: 1, max: 300_000, count: :bytes) + end + + def set_name_at_post_time(changeset, nil), do: changeset + def set_name_at_post_time(changeset, %{name: name}) do + change(changeset, name_at_post_time: name) + end end diff --git a/lib/philomena/repo.ex b/lib/philomena/repo.ex index cd5da3a5..dd6cf89f 100644 --- a/lib/philomena/repo.ex +++ b/lib/philomena/repo.ex @@ -4,4 +4,24 @@ defmodule Philomena.Repo do adapter: Ecto.Adapters.Postgres use Scrivener, page_size: 250 + + @levels %{ + read_committed: "READ COMMITTED", + repeatable_read: "REPEATABLE READ", + serializable: "SERIALIZABLE" + } + + def isolated_transaction(f, level) do + Philomena.Repo.transaction(fn -> + Philomena.Repo.query!("SET TRANSACTION ISOLATION LEVEL #{@levels[level]}") + Philomena.Repo.transaction(f) + end) + |> case do + {:ok, value} -> + value + + error -> + error + end + end end diff --git a/lib/philomena/topics/topic.ex b/lib/philomena/topics/topic.ex index e6146821..47059906 100644 --- a/lib/philomena/topics/topic.ex +++ b/lib/philomena/topics/topic.ex @@ -6,6 +6,7 @@ defmodule Philomena.Topics.Topic do alias Philomena.Users.User alias Philomena.Polls.Poll alias Philomena.Posts.Post + alias Philomena.Topics.Subscription @derive {Phoenix.Param, key: :slug} schema "topics" do @@ -15,6 +16,8 @@ defmodule Philomena.Topics.Topic do belongs_to :last_post, Post belongs_to :forum, Forum has_one :poll, Poll + has_many :posts, Post + has_many :subscriptions, Subscription field :title, :string field :post_count, :integer, default: 0 diff --git a/lib/philomena_web/controllers/topic/post_controller.ex b/lib/philomena_web/controllers/topic/post_controller.ex index 0d602294..eff9f93d 100644 --- a/lib/philomena_web/controllers/topic/post_controller.ex +++ b/lib/philomena_web/controllers/topic/post_controller.ex @@ -1,10 +1,51 @@ defmodule PhilomenaWeb.Topic.PostController do use PhilomenaWeb, :controller - #alias Philomena.{Forums.Forum} - #alias Philomena.Posts + alias Philomena.{Forums.Forum, Topics.Topic, Posts} + alias Philomena.Repo plug PhilomenaWeb.FilterBannedUsersPlug + plug PhilomenaWeb.UserAttributionPlug plug PhilomenaWeb.CanaryMapPlug, create: :show, edit: :show, update: :show plug :load_and_authorize_resource, model: Forum, id_field: "short_name", id_name: "forum_id", persisted: true + plug :load_topic + + def create(conn, %{"post" => post_params}) do + attributes = conn.assigns.attributes + forum = conn.assigns.forum + topic = conn.assigns.topic + user = conn.assigns.current_user + + case Posts.create_post(topic, user, attributes, post_params) do + {:ok, %{post: post}} -> + Posts.notify_post(post) + Posts.reindex_post(post) + + conn + |> put_flash(:info, "Post created successfully.") + |> redirect(to: Routes.forum_topic_path(conn, :show, forum, topic, post_id: post.id) <> "#post_#{post.id}") + + _error -> + conn + |> put_flash(:error, "There was an error creating the post") + |> redirect(external: conn.assigns.referrer) + end + end + + defp load_topic(%{params: %{"topic_id" => slug}} = conn, _args) do + forum = conn.assigns.forum + user = conn.assigns.current_user + + with topic when not is_nil(topic) <- Repo.get_by(Topic, slug: slug, forum_id: forum.id), + true <- Canada.Can.can?(user, :show, topic) + do + conn + |> assign(:topic, topic) + else + _ -> + conn + |> put_flash(:error, "Couldn't access that topic") + |> redirect(external: conn.assigns.referrer) + end + end end \ No newline at end of file diff --git a/lib/philomena_web/controllers/topic_controller.ex b/lib/philomena_web/controllers/topic_controller.ex index c2f227bb..caf39418 100644 --- a/lib/philomena_web/controllers/topic_controller.ex +++ b/lib/philomena_web/controllers/topic_controller.ex @@ -1,7 +1,7 @@ defmodule PhilomenaWeb.TopicController do use PhilomenaWeb, :controller - alias Philomena.{Forums.Forum, Topics, Topics.Topic, Posts.Post, Textile.Renderer} + alias Philomena.{Forums.Forum, Topics, Topics.Topic, Posts, Posts.Post, Textile.Renderer} alias Philomena.Repo import Ecto.Query @@ -54,6 +54,10 @@ defmodule PhilomenaWeb.TopicController do watching = Topics.subscribed?(topic, conn.assigns.current_user) - render(conn, "show.html", posts: posts, watching: watching) + changeset = + %Post{} + |> Posts.change_post() + + render(conn, "show.html", posts: posts, changeset: changeset, watching: watching) end end diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index 1a1b434c..2ff8fc78 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -86,7 +86,9 @@ defmodule PhilomenaWeb.Router do resources "/tags", TagController, only: [:index, :show] resources "/search", SearchController, only: [:index] resources "/forums", ForumController, only: [:index, :show] do - resources "/topics", TopicController, only: [:show] + resources "/topics", TopicController, only: [:show] do + resources "/posts", Topic.PostController, only: [:create], singleton: true + end end resources "/comments", CommentController, only: [:index] diff --git a/lib/philomena_web/templates/topic/post/_form.html.slime b/lib/philomena_web/templates/topic/post/_form.html.slime new file mode 100644 index 00000000..ae39f0ea --- /dev/null +++ b/lib/philomena_web/templates/topic/post/_form.html.slime @@ -0,0 +1,25 @@ += form_for @changeset, Routes.forum_topic_post_path(@conn, :create, @forum, @topic), fn f -> + = if @changeset.action do + .alert.alert-danger + p Oops, something went wrong! Please check the errors below. + + .block + .block__header.block__header--js-tabbed + a.selected href="#" data-click-tab="write" + i.fa.fa-pencil> + ' Edit + + a href="#" data-click-tab="preview" + i.fa.fa-eye> + ' Preview + + .block__tab.communication-edit__tab.selected data-tab="write" + .field + = textarea f, :body, class: "input input--wide input--text js-preview-input js-toolbar-input", placeholder: "Please read the site rules before posting and use [spoiler][/spoiler] for NSFW stuff in SFW forums.", required: true + = error_tag f, :body + + .block__tab.communication-edit__tab.hidden data-tab="preview" + ' [Loading preview...] + + .block__content.communication-edit__actions + = submit "Post", class: "button" \ No newline at end of file diff --git a/lib/philomena_web/templates/topic/show.html.slime b/lib/philomena_web/templates/topic/show.html.slime index 33ef648d..1faaa139 100644 --- a/lib/philomena_web/templates/topic/show.html.slime +++ b/lib/philomena_web/templates/topic/show.html.slime @@ -47,7 +47,7 @@ h1 = @topic.title / Post form = if @topic.post_count < 200_000 do - /= render partial: 'topics/posts/form' + = render PhilomenaWeb.Topic.PostView, "_form.html", conn: @conn, forum: @forum, topic: @topic, changeset: @changeset - else /h3 Okay, we're impressed /p You're looking at a thread with over 200,000 posts in it! diff --git a/lib/philomena_web/views/topic/post_view.ex b/lib/philomena_web/views/topic/post_view.ex new file mode 100644 index 00000000..e24f727d --- /dev/null +++ b/lib/philomena_web/views/topic/post_view.ex @@ -0,0 +1,3 @@ +defmodule PhilomenaWeb.Topic.PostView do + use PhilomenaWeb, :view +end