~afontaine/home

f97bde2c4720254cdb9c357e85c152161a103898 — Andrew Fontaine 8 months ago afee4f3
Add Tags to Posts

Tags can be created by starting the subject line with [, ] and are
comma-separated. White space between tags is elminited, while whitespace
within tags is untouched.

Examples:

[elixir,phoenix,live view] => "elixir", "phoenix", "live view"
[elxiir, pheonix, live view] => "elixir", "phoenix", "live view"

Also displays the last 3 most recent posts for each tag, and will
definitely show duplicates.
M apps/blog/lib/blog/content.ex => apps/blog/lib/blog/content.ex +45 -3
@@ 7,6 7,7 @@ defmodule Blog.Content do
  alias Blog.Repo

  alias Blog.Content.Post
  alias Blog.Content.Tag

  @doc """
  Returns the list of posts.


@@ 18,7 19,10 @@ defmodule Blog.Content do

  """
  def list_posts do
    Repo.all(Post)
    Post
    |> join(:left, [post], tags in assoc(post, :tags))
    |> preload([_, tags], tags: tags)
    |> Repo.all()
  end

  @doc """


@@ 35,7 39,17 @@ defmodule Blog.Content do
      ** (Ecto.NoResultsError)

  """
  def get_post!(slug), do: Repo.one!(from p in Post, where: p.slug == ^slug)
  def get_post!(slug),
    do:
      Post
      |> where([post], post.slug == ^slug)
      |> join(:left, [post], _ in assoc(post, :tags))
      |> join(:left, [post, tag], posts in assoc(tag, :posts), on: post.id != posts.id)
      |> order_by([_, _, posts], desc: posts.inserted_at)
      |> preload([_, tags, posts], tags: {tags, posts: posts})
      |> Repo.one!()

  def last_post!(), do: Repo.one!(from p in Post, order_by: [desc: p.id], limit: 1)

  @doc """
  Creates a post.


@@ 51,7 65,8 @@ defmodule Blog.Content do
  """
  def create_post(%{} = post) do
    %Post{}
    |> Post.changeset(post)
    |> Repo.preload(:tags)
    |> Post.changeset(%{post | tags: fetch_tags(post.tags)})
    |> Repo.insert()
  end



@@ 101,4 116,31 @@ defmodule Blog.Content do
  def change_post(%Post{} = post, attrs \\ %{}) do
    Post.changeset(post, attrs)
  end

  def fetch_tags(tags) do
    tags
    |> Enum.reject(&(&1 == ""))
    |> insert_and_get_all_tags()
  end

  defp insert_and_get_all_tags([]), do: []

  defp insert_and_get_all_tags(tags) do
    timestamp =
      NaiveDateTime.utc_now()
      |> NaiveDateTime.truncate(:second)

    maps =
      Enum.map(
        tags,
        &%{
          tag: &1,
          inserted_at: timestamp,
          updated_at: timestamp
        }
      )

    Repo.insert_all(Tag, maps, on_conflict: :nothing)
    Repo.all(from t in Tag, where: t.tag in ^tags)
  end
end

M apps/blog/lib/blog/content/post.ex => apps/blog/lib/blog/content/post.ex +5 -0
@@ 10,6 10,10 @@ defmodule Blog.Content.Post do
    field :slug, :string
    field :author, :string

    many_to_many :tags, Blog.Content.Tag,
      join_through: "posts_tags",
      on_replace: :delete

    timestamps()
  end



@@ 20,5 24,6 @@ defmodule Blog.Content.Post do
    |> validate_required([:subject, :text, :message_id])
    |> unique_constraint(:message_id)
    |> slugify(:subject)
    |> put_assoc(:tags, attrs.tags)
  end
end

A apps/blog/lib/blog/content/tag.ex => apps/blog/lib/blog/content/tag.ex +19 -0
@@ 0,0 1,19 @@
defmodule Blog.Content.Tag do
  use Ecto.Schema
  import Ecto.Changeset

  schema "tags" do
    field :tag, :string
    many_to_many(:posts, Blog.Content.Post, join_through: "posts_tags", on_replace: :delete)

    timestamps()
  end

  @doc false
  def changeset(tag, attrs) do
    tag
    |> cast(attrs, [:tag])
    |> validate_required([:tag])
    |> unique_constraint(:tag)
  end
end

M apps/blog/lib/blog/mail/post.ex => apps/blog/lib/blog/mail/post.ex +36 -0
@@ 2,12 2,15 @@ defmodule Blog.Mail.Post do
  use Ecto.Schema
  import Ecto.Changeset

  @tag_regex ~r/\[(.+)\]/

  embedded_schema do
    field :message_id, :string
    field :subject, :string
    field :envelope, Blog.Types.Message
    field :text, :string
    field :author, :string
    field :tags, {:array, :string}
  end

  @doc false


@@ 17,6 20,7 @@ defmodule Blog.Mail.Post do
    |> validate_required([:message_id, :subject, :envelope])
    |> put_body()
    |> put_author()
    |> put_tags()
  end

  defp put_body(%Ecto.Changeset{valid?: true, changes: %{envelope: envelope}} = changeset) do


@@ 43,4 47,36 @@ defmodule Blog.Mail.Post do
  end

  defp put_author(changeset), do: changeset

  defp put_tags(%Ecto.Changeset{valid?: true, changes: %{subject: subject}} = changeset) do
    [head, tags] =
      @tag_regex
      |> Regex.run(subject)
      |> case do
        [head, tags] ->
          [head, parse_tags(tags)]

        _ ->
          ["", ""]
      end

    changeset
    |> put_change(:tags, tags)
    |> put_change(:subject, remove_tags_from_subject(subject, head))
  end

  defp put_tags(changeset), do: changeset

  defp parse_tags(tags) do
    tags
    |> String.split(",")
    |> Enum.map(&String.trim/1)
    |> Enum.map(&String.downcase/1)
  end

  defp remove_tags_from_subject(subject, tags) do
    subject
    |> String.replace(tags, "")
    |> String.trim()
  end
end

A apps/blog/priv/repo/migrations/20210328180009_create_tags.exs => apps/blog/priv/repo/migrations/20210328180009_create_tags.exs +13 -0
@@ 0,0 1,13 @@
defmodule Blog.Repo.Migrations.CreateTags do
  use Ecto.Migration

  def change do
    create table(:tags) do
      add :tag, :string

      timestamps()
    end

    create unique_index(:tags, [:tag])
  end
end

A apps/blog/priv/repo/migrations/20210328180220_add_tags_to_posts.exs => apps/blog/priv/repo/migrations/20210328180220_add_tags_to_posts.exs +14 -0
@@ 0,0 1,14 @@
defmodule Blog.Repo.Migrations.AddTagsToPosts do
  use Ecto.Migration

  def change do
    create table(:posts_tags, primary_key: false) do
      add(:post_id, references(:posts, on_delete: :delete_all), primary_key: true)
      add(:tag_id, references(:tags, on_delete: :delete_all), primary_key: true)
      timestamps()
    end

    create(index(:posts_tags, [:post_id]))
    create(index(:posts_tags, [:tag_id]))
  end
end

A apps/blog/priv/repo/migrations/20210328181838_remove_timestamps_from_assoc.exs => apps/blog/priv/repo/migrations/20210328181838_remove_timestamps_from_assoc.exs +10 -0
@@ 0,0 1,10 @@
defmodule Blog.Repo.Migrations.RemoveTimestampsFromAssoc do
  use Ecto.Migration

  def change do
    alter table(:posts_tags) do
      remove :inserted_at
      remove :updated_at
    end
  end
end

M apps/home_web/lib/home_web/templates/layout/app.html.eex => apps/home_web/lib/home_web/templates/layout/app.html.eex +1 -1
@@ 31,7 31,7 @@
        </nav>
      </section>
    </header>
    <main role="main" class="container mx-auto">
    <main role="main" class="container mx-auto flex flex-col md:flex-row">
      <%= @inner_content %>
    </main>
  </body>

M apps/home_web/lib/home_web/templates/post/show.html.eex => apps/home_web/lib/home_web/templates/post/show.html.eex +23 -1
@@ 1,6 1,28 @@
<article class="prose prose-indigo dark:prose-light mt-10">
<article class="prose prose-indigo dark:prose-light mt-10 mb-8">
  <%= render_shared "post_header.html", post: @post %>

  <h1><%= @post.subject %></h1>
  <%= markdown(@post.text) %>
</article>

<nav class="md:mx-auto">
  <% tags = @post.tags |> Enum.filter(fn tag -> tag.posts != [] end)  %>
  <%= if tags != [] do %>
    <div class="shadow-lg rounded px-4 pt-4 pb-2 dark:bg-gray-800 md:mt-10">
      <h3 class="text-xl mb-2">Related Posts</h3>
      <%= tags |> Enum.map(fn tag ->  %>
        <div class="mt-2">
          <h4 class="text-lg">For more posts about <span class="text-indigo-500 dark:text-indigo-300"><%= tag.tag %></span>:</h4>
          <ul>
            <%= tag.posts |> Enum.take(3) |> Enum.map(fn post -> %>
            <li class="mb-4">
              <%= link post.subject, to: Routes.post_path(@conn, :show, post.slug), class: "underline text-indigo-500 dark:text-indigo-300" %>
              <span class="text-sm ml-2"><%= NaiveDateTime.to_date(post.inserted_at) %></span>
            </li>
            <% end) %>
          </ul>
        </div>
      <%  end) |> Enum.intersperse(raw("<hr/>")) %>
    </div>
  <% end %>
</nav>