Merge branch 'f-content' into 'master'

feat: Add basic content management system

See merge request mythic-insight/legendary!5
This commit is contained in:
Robert Prehn 2020-07-22 19:22:44 +00:00
commit 5a629a3860
136 changed files with 2073 additions and 501 deletions

View file

@ -1,9 +1,15 @@
image: "elixir:1.10" image: "elixir:1.10"
cache:
paths:
- _build
- deps
variables: variables:
POSTGRES_PASSWORD: "postgres" POSTGRES_PASSWORD: "postgres"
POSTGRES_USER: "postgres" POSTGRES_USER: "postgres"
DATABASE_URL: "postgres" DATABASE_URL: "postgres"
MIX_ENV: "test"
services: services:
- name: postgres:12 - name: postgres:12

View file

@ -1,4 +1,4 @@
defmodule Auth.Users.User do defmodule Auth.User do
@moduledoc """ @moduledoc """
The baseline user schema module. The baseline user schema module.
""" """
@ -13,6 +13,8 @@ defmodule Auth.Users.User do
schema "users" do schema "users" do
field :roles, {:array, :string} field :roles, {:array, :string}
field :display_name, :string
field :homepage_url, :string
pow_user_fields() pow_user_fields()

View file

@ -4,7 +4,7 @@ defmodule Mix.Tasks.Legendary.CreateAdmin do
""" """
use Mix.Task use Mix.Task
alias Auth.Users.User alias Auth.User
alias Auth.Repo alias Auth.Repo
alias Ecto.Changeset alias Ecto.Changeset

View file

@ -0,0 +1,9 @@
defmodule Auth.Repo.Migrations.AddNicenameToUsers do
use Ecto.Migration
def change do
alter table(:users) do
add :display_name, :string, default: nil
end
end
end

View file

@ -0,0 +1,9 @@
defmodule Auth.Repo.Migrations.AddUrlToUsers do
use Ecto.Migration
def change do
alter table(:users) do
add :homepage_url, :string, default: nil
end
end
end

View file

@ -20,7 +20,7 @@ erl_crash.dump
*.ez *.ez
# Ignore package tarball (built via "mix hex.build"). # Ignore package tarball (built via "mix hex.build").
auth_web-*.tar content-*.tar
# If NPM crashes, it generates a log, let's ignore it too. # If NPM crashes, it generates a log, let's ignore it too.
npm-debug.log npm-debug.log

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -18,7 +18,7 @@ config :logger, :console,
config :content, Content.Scheduler, config :content, Content.Scheduler,
jobs: [ jobs: [
{"@hourly", {ContentWeb.Sitemaps, :generate, []}} {"@hourly", {Content.Sitemaps, :generate, []}}
] ]
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom

View file

@ -4,7 +4,7 @@ use Mix.Config
config :content, Content.Repo, config :content, Content.Repo,
username: "postgres", username: "postgres",
password: "postgres", password: "postgres",
database: "content_dev", database: "legendary_dev",
hostname: "localhost", hostname: "localhost",
show_sensitive_data_on_connection_error: true, show_sensitive_data_on_connection_error: true,
pool_size: 10 pool_size: 10
@ -15,7 +15,7 @@ config :content, Content.Repo,
# The watchers configuration can be used to run external # The watchers configuration can be used to run external
# watchers to your application. For example, we use it # watchers to your application. For example, we use it
# with webpack to recompile .js and .css sources. # with webpack to recompile .js and .css sources.
config :content, ContentWeb.Endpoint, config :content, Content.Endpoint,
http: [port: 4000], http: [port: 4000],
debug_errors: true, debug_errors: true,
code_reloader: true, code_reloader: true,
@ -55,7 +55,7 @@ config :content, ContentWeb.Endpoint,
# different ports. # different ports.
# Watch static and templates for browser reloading. # Watch static and templates for browser reloading.
config :content, ContentWeb.Endpoint, config :content, Content.Endpoint,
live_reload: [ live_reload: [
patterns: [ patterns: [
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",

View file

@ -2,11 +2,3 @@ use Mix.Config
# Print only warnings and errors during test # Print only warnings and errors during test
config :logger, level: :warn config :logger, level: :warn
config :content, Content.Repo,
username: "postgres",
password: "postgres",
database: "content_test#{System.get_env("MIX_TEST_PARTITION")}",
hostname: System.get_env("DATABASE_URL") || "localhost",
show_sensitive_data_on_connection_error: true,
pool: Ecto.Adapters.SQL.Sandbox

View file

@ -1,6 +1,6 @@
defmodule Content.Application do defmodule Content.Application do
@moduledoc """ @moduledoc """
The base module of the CMS application. The base module of the Content application.
""" """
use Application use Application
@ -17,6 +17,8 @@ defmodule Content.Application do
# Start your own worker by calling: Content.Worker.start_link(arg1, arg2, arg3) # Start your own worker by calling: Content.Worker.start_link(arg1, arg2, arg3)
# worker(Content.Worker, [arg1, arg2, arg3]), # worker(Content.Worker, [arg1, arg2, arg3]),
worker(Content.Scheduler, []), worker(Content.Scheduler, []),
Content.Telemetry,
Content.Endpoint,
] ]
# See https://hexdocs.pm/elixir/Supervisor.html # See https://hexdocs.pm/elixir/Supervisor.html
@ -28,7 +30,7 @@ defmodule Content.Application do
# Tell Phoenix to update the endpoint configuration # Tell Phoenix to update the endpoint configuration
# whenever the application is updated. # whenever the application is updated.
def config_change(changed, _new, removed) do def config_change(changed, _new, removed) do
Endpoint.config_change(changed, removed) Content.Endpoint.config_change(changed, removed)
:ok :ok
end end
end end

View file

@ -37,7 +37,7 @@ defmodule Content.Post do
has_many :categories, through: [:term_relationships, :category, :term] has_many :categories, through: [:term_relationships, :category, :term]
has_many :tags, through: [:term_relationships, :tag, :term] has_many :tags, through: [:term_relationships, :tag, :term]
has_one :post_format, through: [:term_relationships, :post_format, :term] has_one :post_format, through: [:term_relationships, :post_format, :term]
belongs_to :author, Content.User, foreign_key: :post_author, references: :ID belongs_to :author, Auth.User, foreign_key: :post_author
end end
def changeset(struct, params \\ %{}) do def changeset(struct, params \\ %{}) do
@ -123,14 +123,14 @@ defmodule Content.Post do
end end
def maybe_put_guid(changeset) do def maybe_put_guid(changeset) do
import ContentWeb.Router.Helpers, only: [posts_url: 3] import Content.Router.Helpers, only: [posts_url: 3]
slug = changeset |> get_field(:post_name) slug = changeset |> get_field(:post_name)
case slug do case slug do
nil -> changeset nil -> changeset
_ -> _ ->
changeset changeset
|> put_default(:guid, posts_url(ContentWeb.Endpoint, :show, slug)) |> put_default(:guid, posts_url(CoreWeb.Endpoint, :show, slug))
end end
end end
end end

View file

@ -159,6 +159,18 @@ defmodule Content.Posts do
""" """
def get_posts!(slug) do def get_posts!(slug) do
slug
|> get_post_scope()
|> Repo.one!()
end
def get_post(slug) do
slug
|> get_post_scope()
|> Repo.one()
end
defp get_post_scope(slug) do
id_filter = fn scope, id -> id_filter = fn scope, id ->
case Integer.parse(id, 10) do case Integer.parse(id, 10) do
@ -172,7 +184,6 @@ defmodule Content.Posts do
post_scope() post_scope()
|> where([p], p.post_type != "nav_menu_item") |> where([p], p.post_type != "nav_menu_item")
|> id_filter.(slug) |> id_filter.(slug)
|> Repo.one!()
end end
@doc """ @doc """

View file

@ -1,12 +1,12 @@
defmodule ContentWeb do defmodule Content do
@moduledoc """ @moduledoc """
The entrypoint for defining your web interface, such The entrypoint for defining your web interface, such
as controllers, views, channels and so on. as controllers, views, channels and so on.
This can be used in your application as: This can be used in your application as:
use ContentWeb, :controller use Content, :controller
use ContentWeb, :view use Content, :view
The definitions below will be executed for every view, The definitions below will be executed for every view,
controller, etc, so keep them short and clean, focused controller, etc, so keep them short and clean, focused
@ -19,11 +19,11 @@ defmodule ContentWeb do
def controller do def controller do
quote do quote do
use Phoenix.Controller, namespace: ContentWeb use Phoenix.Controller, namespace: Content
import Plug.Conn import Plug.Conn
import ContentWeb.Gettext import Content.Gettext
alias ContentWeb.Router.Helpers, as: Routes alias Content.Router.Helpers, as: Routes
end end
end end
@ -31,13 +31,21 @@ defmodule ContentWeb do
quote do quote do
use Phoenix.View, use Phoenix.View,
root: "lib/content_web/templates", root: "lib/content_web/templates",
namespace: ContentWeb, namespace: Content,
pattern: "**/*" pattern: "**/*"
use PhoenixHtmlSanitizer, :basic_html
# Import convenience functions from controllers # Import convenience functions from controllers
import Phoenix.Controller, import Phoenix.Controller,
only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
def process_content(text) do
text
|> Content.Shortcodes.expand_shortcodes()
|> Earmark.as_html!()
end
# Include shared imports and aliases for views # Include shared imports and aliases for views
unquote(view_helpers()) unquote(view_helpers())
end end
@ -55,7 +63,7 @@ defmodule ContentWeb do
def channel do def channel do
quote do quote do
use Phoenix.Channel use Phoenix.Channel
import ContentWeb.Gettext import Content.Gettext
end end
end end
@ -67,9 +75,9 @@ defmodule ContentWeb do
# Import basic rendering functionality (render, render_layout, etc) # Import basic rendering functionality (render, render_layout, etc)
import Phoenix.View import Phoenix.View
import ContentWeb.ErrorHelpers import Content.ErrorHelpers
import ContentWeb.Gettext import Content.Gettext
alias ContentWeb.Router.Helpers, as: Routes alias Content.Router.Helpers, as: Routes
end end
end end

View file

@ -1,8 +1,8 @@
defmodule ContentWeb.UserSocket do defmodule Content.UserSocket do
use Phoenix.Socket use Phoenix.Socket
## Channels ## Channels
# channel "room:*", ContentWeb.RoomChannel # channel "room:*", Content.RoomChannel
# Socket params are passed from the client and can # Socket params are passed from the client and can
# be used to verify and authenticate a user. After # be used to verify and authenticate a user. After
@ -27,7 +27,7 @@ defmodule ContentWeb.UserSocket do
# Would allow you to broadcast a "disconnect" event and terminate # Would allow you to broadcast a "disconnect" event and terminate
# all active sockets and channels for a given user: # all active sockets and channels for a given user:
# #
# ContentWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) # Content.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
# #
# Returning `nil` makes this socket anonymous. # Returning `nil` makes this socket anonymous.
@impl true @impl true

View file

@ -0,0 +1,7 @@
defmodule Content.AdminHomeController do
use Content, :controller
def index(conn, _params) do
render conn, "index.html"
end
end

View file

@ -0,0 +1,27 @@
defmodule Content.AdminPostsController do
use Content, :controller
alias Content.Posts
require Ecto.Query
def index(conn, %{"page" => page, "post_type" => post_type}) do
posts = Posts.list_admin_posts(page, post_type)
thumbs = posts |> Posts.thumbs_for_posts()
last_page = Posts.last_page
render(
conn,
"index.html",
[
posts: posts,
page: String.to_integer(page),
last_page: last_page,
thumbs: thumbs,
post_type: post_type,
]
)
end
def index(conn, %{"page" => page} = params), do: index(conn, params |> Map.merge(%{"page" => page, "post_type" => "post"}))
def index(conn, %{"post_type" => post_type} = params), do: index(conn, params |> Map.merge(%{"page" => "1", "post_type" => post_type}))
def index(conn, params), do: index(conn, params |> Map.merge(%{"page" => "1", "post_type" => "post"}))
end

View file

@ -0,0 +1,71 @@
defmodule Content.CommentController do
use Content, :controller
alias Content
alias Content.Comments
alias Content.Post
alias Content.Repo
import Ecto.Query
def create(conn, %{"comment" => comment_params}) do
comment_params = comment_params |> Map.merge(%{"comment_author_IP" => to_string(:inet_parse.ntoa(conn.remote_ip))})
case Comments.create_comment(comment_params) do
{:ok, comment} ->
post =
Post
|> where([p], p.'ID' == ^comment.comment_post_ID)
|> Repo.one()
conn
|> put_flash(:info, "Comment created successfully.")
|> redirect(to: Routes.posts_path(conn, :show, post))
{:error, _} ->
post =
Post
|> where([p], p.'ID' == ^comment_params["comment_post_ID"])
|> Repo.one()
conn
|> redirect(to: Routes.posts_path(conn, :show, post))
end
end
def update(conn, %{"id" => id, "comment" => comment_params}) do
comment = Comments.get_comment!(id)
case Comments.update_comment(comment, comment_params) do
{:ok, comment} ->
post =
Post
|> where([p], p.'ID' == ^comment.comment_post_ID)
|> Repo.one()
conn
|> put_flash(:info, "Comment updated successfully.")
|> redirect(to: Routes.posts_path(conn, :show, post))
{:error, _} ->
post =
Post
|> where([p], p.'ID' == ^comment_params["comment_post_ID"])
|> Repo.one()
conn
|> redirect(to: Routes.posts_path(conn, :show, post))
end
end
def delete(conn, %{"id" => id}) do
comment = Comments.get_comment!(id)
{:ok, comment} = Comments.delete_comment(comment)
post =
Post
|> where([p], p.'ID' == ^comment.comment_post_ID)
|> Repo.one()
conn
|> put_flash(:info, "Comment deleted successfully.")
|> redirect(to: Routes.posts_path(conn, :show, post))
end
end

View file

@ -0,0 +1,33 @@
defmodule Content.FeedsController do
use Content, :controller
alias Content.{Posts}
plug :put_layout, false when action in [:preview]
def index(conn, params) do
category = params |> Map.get("category")
posts = Posts.list_posts(params)
thumbs = posts |> Posts.thumbs_for_posts()
feed_url =
case category do
nil ->
Routes.index_feed_path(conn, :index)
_ ->
Routes.category_feed_path(conn, :index, category)
end
conn
|> put_resp_content_type("text/xml")
|> render(
"index.rss",
[
posts: posts,
thumbs: thumbs,
category: category,
feed_url: feed_url,
]
)
end
end

View file

@ -0,0 +1,39 @@
defmodule Content.MenusController do
use Content, :controller
alias Content.Repo
def edit(conn, %{"id" => id}) do
menu = id |> Content.Menu.get_menu_from_id()
posts =
Content.Posts.post_scope
|> Repo.all()
|> Enum.map(fn post ->
post |> Map.take([:ID, :post_title, :post_name])
end)
categories =
Content.Terms.categories
|> Repo.all()
|> Enum.map(fn cat ->
cat |> Map.take([:name, :slug, :term_group, :term_id])
end)
conn
|> render(
"edit.html",
[
id: id,
menu: menu,
posts: posts,
categories: categories,
]
)
end
def update(conn, %{"id" => id, "menu" => menu}) do
Content.UpdateMenu.run(id, menu |> Phoenix.json_library().decode!())
conn
|> redirect(to: Routes.menus_path(conn, :edit, id))
end
end

View file

@ -1,5 +1,5 @@
defmodule ContentWeb.PageController do defmodule Content.PageController do
use ContentWeb, :controller use Content, :controller
def index(conn, _params) do def index(conn, _params) do
render(conn, "index.html") render(conn, "index.html")

View file

@ -0,0 +1,20 @@
defmodule Content.PostPasswordController do
use Content, :controller
def create(conn, %{"post_password" => post_password}) do
conn = put_session(conn, "post_password", post_password)
redirect_path =
if Enum.count(get_req_header(conn, "referer")) > 0 do
conn
|> get_req_header("referer")
|> Enum.at(0)
|> URI.parse()
|> (&(&1.path)).()
else
"/"
end
redirect conn, to: redirect_path
end
end

View file

@ -0,0 +1,176 @@
defmodule Content.PostsController do
use Content, :controller
alias Auth.User
alias Content.{Options, Post, Posts, Repo}
plug :put_layout, false when action in [:preview]
def index(conn, params) do
show_on_front = Options.get_value("show_on_front") || "posts"
case show_on_front do
"posts" ->
conn |> index_posts(params)
"page" ->
conn |> index_page(params)
end
end
def index_posts(conn, params) do
page = params |> Map.get("page", "1")
params = params |> Map.merge(%{"page" => page})
category = params |> Map.get("category")
posts = Posts.list_posts(params)
thumbs = posts |> Posts.thumbs_for_posts()
last_page = Posts.last_page(params)
conn
|> render(
"index.html",
[
posts: posts,
page: String.to_integer(page),
last_page: last_page,
thumbs: thumbs,
category: category,
]
)
end
def index_page(conn, _params) do
page_id = Options.get("page_on_front") |> (&(&1.option_value)).()
post = Posts.get_posts!(page_id)
page = String.to_integer("1")
thumbs = [post] |> Posts.thumbs_for_posts()
render(conn, "front.html", post: post, page: page, thumbs: thumbs)
end
def new(conn, params) do
changeset = Posts.change_posts(%Post{})
render(
conn,
"new.html",
changeset: changeset,
post_type: params["post_type"] || "post",
author_options: User |> Repo.all()
)
end
def create(conn, %{"post" => post_params}) do
case Posts.create_posts(post_params) do
{:ok, post} ->
conn
|> put_flash(:info, "Posts created successfully.")
|> redirect(to: Routes.posts_path(conn, :show, post))
{:error, %Ecto.Changeset{} = changeset} ->
render(
conn,
"new.html",
changeset: changeset,
post_type: post_params["post_type"] || "post",
author_options: User |> Repo.all()
)
end
end
def preview(conn, %{"post" => post_params}) do
post = Posts.preview_post(post_params)
conn
|> render("show.html", post: post, page: 1, thumbs: [])
end
def show(conn, %{"id" => id, "page" => page_string}) do
{page_id_for_posts, _} = Options.get_value_as_int("page_for_posts")
post = Posts.get_post(id)
if is_nil(post) do
try_static_post(conn, id)
else
if post.'ID' == page_id_for_posts do
conn |> index_posts(%{"id" => id, "page" => page_string})
else
conn |> show_one(post, page_string)
end
end
end
def show(conn, %{"id" => id}), do: show(conn, %{"id" => id, "page" => "1"})
defp try_static_post(conn, id) do
try do
render(conn, "static_pages/#{id}.html")
rescue
Phoenix.Template.UndefinedError ->
raise Phoenix.Router.NoRouteError.exception(conn: conn, router: Content.Router)
end
end
def show_one(conn, post, page_string) do
{front_page_id, _} = Options.get_value_as_int("page_on_front")
template =
if post.'ID' == front_page_id do
"front.html"
else
"show.html"
end
page = String.to_integer(page_string)
thumbs = [post] |> Posts.thumbs_for_posts()
case post.post_type do
"attachment" ->
{:ok, decoded} = post.post_content |> Base.decode64
conn
|> put_resp_content_type(post.post_mime_type, "binary")
|> send_resp(conn.status || 200, decoded)
_ ->
render(conn, template, post: post, page: page, thumbs: thumbs)
end
end
def edit(conn, %{"id" => id}) do
posts = Posts.get_post_with_drafts!(id)
changeset = Posts.change_posts(posts)
render(
conn,
"edit.html",
posts: posts,
changeset: changeset,
post_type: posts.post_type || "post",
author_options: User |> Repo.all()
)
end
def update(conn, %{"id" => id, "post" => posts_params}) do
posts = Posts.get_post_with_drafts!(id)
case Posts.update_posts(posts, posts_params) do
{:ok, posts} ->
conn
|> put_flash(:info, "Posts updated successfully.")
|> redirect(to: Routes.posts_path(conn, :edit, posts))
{:error, %Ecto.Changeset{} = changeset} ->
render(
conn,
"edit.html",
posts: posts,
changeset: changeset,
post_type: posts.post_type || "post",
author_options: User |> Repo.all()
)
end
end
def delete(conn, %{"id" => id}) do
posts = Posts.get_post_with_drafts!(id)
{:ok, _posts} = Posts.delete_posts(posts)
conn
|> put_flash(:info, "Posts deleted successfully.")
|> redirect(to: Routes.admin_posts_path(conn, :index))
end
end

View file

@ -0,0 +1,20 @@
defmodule Content.SitemapController do
use Content, :controller
alias Content.{Posts, Repo, Terms}
import Ecto.Query
def index(conn, _params) do
posts =
Posts.post_scope
|> where([p], p.post_type not in ["nav_menu_item", "attachment"])
|> Repo.all()
categories =
Terms.categories
|> Repo.all()
render(conn, "index.html", conn: conn, posts: posts, categories: categories)
end
end

View file

@ -1,5 +1,5 @@
defmodule ContentWeb.Endpoint do defmodule Content.Endpoint do
use Phoenix.Endpoint, otp_app: :content_web use Phoenix.Endpoint, otp_app: :content
# The session will be stored in the cookie and signed, # The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with. # this means its contents can be read but not tampered with.
@ -10,7 +10,7 @@ defmodule ContentWeb.Endpoint do
signing_salt: "wfYQp84C" signing_salt: "wfYQp84C"
] ]
socket "/socket", ContentWeb.UserSocket, socket "/socket", Content.UserSocket,
websocket: true, websocket: true,
longpoll: false longpoll: false
@ -22,7 +22,7 @@ defmodule ContentWeb.Endpoint do
# when deploying your static files in production. # when deploying your static files in production.
plug Plug.Static, plug Plug.Static,
at: "/", at: "/",
from: :content_web, from: :content,
gzip: false, gzip: false,
only: ~w(css fonts images js favicon.ico robots.txt) only: ~w(css fonts images js favicon.ico robots.txt)
@ -32,7 +32,7 @@ defmodule ContentWeb.Endpoint do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader plug Phoenix.LiveReloader
plug Phoenix.CodeReloader plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :content_web plug Phoenix.Ecto.CheckRepoStatus, otp_app: :content
end end
plug Phoenix.LiveDashboard.RequestLogger, plug Phoenix.LiveDashboard.RequestLogger,
@ -50,6 +50,6 @@ defmodule ContentWeb.Endpoint do
plug Plug.MethodOverride plug Plug.MethodOverride
plug Plug.Head plug Plug.Head
plug Plug.Session, @session_options plug Plug.Session, @session_options
plug Pow.Plug.Session, otp_app: :content_web plug Pow.Plug.Session, otp_app: :content
plug ContentWeb.Router plug Content.Router
end end

View file

@ -1,11 +1,11 @@
defmodule ContentWeb.Gettext do defmodule Content.Gettext do
@moduledoc """ @moduledoc """
A module providing Internationalization with a gettext-based API. A module providing Internationalization with a gettext-based API.
By using [Gettext](https://hexdocs.pm/gettext), By using [Gettext](https://hexdocs.pm/gettext),
your module gains a set of macros for translations, for example: your module gains a set of macros for translations, for example:
import ContentWeb.Gettext import Content.Gettext
# Simple translation # Simple translation
gettext("Here is the string to translate") gettext("Here is the string to translate")
@ -20,5 +20,5 @@ defmodule ContentWeb.Gettext do
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
""" """
use Gettext, otp_app: :content_web use Gettext, otp_app: :content
end end

View file

@ -0,0 +1,36 @@
defmodule Content.LoadUser do
@moduledoc """
Loads user into connection if user has session
"""
import Plug.Conn
alias Content.Users
def init(opts) do
opts
end
def call(conn, _) do
handle_auth(conn)
end
defp handle_auth(conn) do
user_id = get_session(conn, :user_id)
if user = (user_id && Users.get_user(user_id)) do
build_conn(conn, user)
else
build_conn(conn, nil)
end
end
defp build_conn(conn, nil) do
conn
|> assign(:current_user, nil)
end
defp build_conn(conn, user) do
conn
|> assign(:current_user, user)
end
end

View file

@ -0,0 +1,22 @@
defmodule Content.RequireAdmin do
@moduledoc """
A plug that returns 403 unauthorized if the user is not an admin. Used
to block out logged-in-only routes.
"""
import Plug.Conn
alias Auth.User
def init(opts) do
opts
end
def call(conn, _opts) do
if conn.assigns[:current_user] && User.is_admin?(conn.assigns[:current_user]) do
conn
else
conn
|> send_resp(403, "Unauthorized")
|> halt()
end
end
end

View file

@ -0,0 +1,22 @@
defmodule Content.RequireAuth do
@moduledoc """
A plug that returns 403 unauthorized if the user is not authenticated. Used
to block out logged-in-only routes.
"""
import Plug.Conn
def init(opts) do
opts
end
def call(conn, _) do
case conn.assigns[:current_user] do
nil ->
conn
|> send_resp(403, "Unauthorized")
|> halt()
_user ->
conn
end
end
end

View file

@ -0,0 +1,72 @@
defmodule Content.Router do
use Content, :router
alias Content.{RequireAdmin, RequireAuth}
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
pipeline :feed do
plug :accepts, ["rss"]
plug :fetch_session
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :require_auth do
plug(RequireAuth)
end
pipeline :require_admin do
plug(RequireAdmin)
end
pipeline :admin_layout do
plug :put_layout, {Content.LayoutView, :admin}
end
scope "/", Content do
pipe_through([:browser, :require_auth, :require_admin, :admin_layout])
get "/wp-admin/", AdminHomeController, :index
get "/posts", AdminPostsController, :index, as: :admin_posts
post "/posts", PostsController, :create
get "/posts/new", PostsController, :new
put "/posts/preview", PostsController, :preview
post "/posts/preview", PostsController, :preview
get "/posts/:id/edit", PostsController, :edit
put "/posts/:id", PostsController, :update
delete "/posts/:id", PostsController, :delete
get "/menus/:id/edit", MenusController, :edit
put "/menus/:id", MenusController, :update
end
scope "/", Content do
pipe_through :feed # Use the default browser stack
get "/category/:category/feed.rss", FeedsController, :index, as: :category_feed
get "/feed.rss", FeedsController, :index, as: :index_feed
end
scope "/", Content do
pipe_through :browser # Use the default browser stack
resources "/comments", CommentController, as: :comment, only: [:create, :delete, :update]
get "/page/:page", PostsController, :index_posts, as: :blog_page
get "/category/:category", PostsController, :index_posts, as: :category
get "/category/:category/page/:page", PostsController, :index, as: :category_page
post "/wp-login.php", PostPasswordController, :create
get "/", PostsController, :index
resources "/sitemap", SitemapController, only: [:index]
get "/:id", PostsController, :show
get "/:id/:page", PostsController, :show, as: :paged_post
end
end

View file

@ -1,4 +1,4 @@
defmodule ContentWeb.Telemetry do defmodule Content.Telemetry do
@moduledoc """ @moduledoc """
Collects metrics for the application and allows them to be transmitted using the Telemetry framework. Collects metrics for the application and allows them to be transmitted using the Telemetry framework.
""" """
@ -34,11 +34,11 @@ defmodule ContentWeb.Telemetry do
), ),
# Database Metrics # Database Metrics
summary("content_web.repo.query.total_time", unit: {:native, :millisecond}), summary("content.repo.query.total_time", unit: {:native, :millisecond}),
summary("content_web.repo.query.decode_time", unit: {:native, :millisecond}), summary("content.repo.query.decode_time", unit: {:native, :millisecond}),
summary("content_web.repo.query.query_time", unit: {:native, :millisecond}), summary("content.repo.query.query_time", unit: {:native, :millisecond}),
summary("content_web.repo.query.queue_time", unit: {:native, :millisecond}), summary("content.repo.query.queue_time", unit: {:native, :millisecond}),
summary("content_web.repo.query.idle_time", unit: {:native, :millisecond}), summary("content.repo.query.idle_time", unit: {:native, :millisecond}),
# VM Metrics # VM Metrics
summary("vm.memory.total", unit: {:byte, :kilobyte}), summary("vm.memory.total", unit: {:byte, :kilobyte}),
@ -52,7 +52,7 @@ defmodule ContentWeb.Telemetry do
[ [
# A module, function and arguments to be invoked periodically. # A module, function and arguments to be invoked periodically.
# This function must call :telemetry.execute/3 and a metric must be added above. # This function must call :telemetry.execute/3 and a metric must be added above.
# {ContentWeb, :count_users, []} # {Content, :count_users, []}
] ]
end end
end end

View file

@ -0,0 +1 @@
<h1>Dashboard</h1>

View file

@ -0,0 +1,93 @@
<div class="grid">
<div class="column">
<h1>
<%= humanize(@post_type) %>s
</h1>
</div>
<div class="column admin-actions">
<%= link "New " <> humanize(@post_type), to: Routes.posts_path(@conn, :new, post_type: @post_type), class: "button" %>
</div>
</div>
<table class="admin-table small">
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th>Categories</th>
<th>Tags</th>
<th>Comments</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<%= for post <- @posts do %>
<tr>
<td>
<%= link post.post_title, to: Routes.posts_path(@conn, :edit, post) %>
</td>
<td>
<%= if !is_nil(post.author) do %>
<%= post.author.display_name %>
<% end %>
</td>
<td>
<%= post.categories |> Enum.map(&(&1.name)) |> Enum.join(", ") %>
</td>
<td>
<%= post.tags |> Enum.map(&(&1.name)) |> Enum.join(", ") %>
</td>
<td>
<%= post.comments |> Enum.count() %>
</td>
<td>
<%= case post.post_status do %>
<% "publish" -> %>
<%= "Published" %>
<% "future" -> %>
<%= "Scheduled" %>
<% "draft" -> %>
<%= "Last Modified" %>
<% "pending" -> %>
<%= "Scheduled" %>
<% "private" -> %>
<%= "Published Privately" %>
<% "inherit" -> %>
<%= "Inherit" %>
<% end %>
<div>
<%= post.post_date |> Timex.format!("%F", :strftime) %>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
<hr />
<%= if @page > 1 do %>
<%= link 1, to: Routes.admin_posts_path(@conn, :index, page: 1) %>
<% end %>
<%= if @page > 3 do %>
...
<% end %>
<%= if @page > 2 do %>
<%= link @page - 1, to: Routes.admin_posts_path(@conn, :index, page: @page - 1) %>
<% end %>
<%= @page %>
<%= if @page + 1 < @last_page do %>
<%= link @page + 1, to: Routes.admin_posts_path(@conn, :index, page: @page + 1) %>
<% end %>
<%= if @page + 2 < @last_page do %>
...
<% end %>
<%= if @page < @last_page do %>
<%= link @last_page, to: Routes.admin_posts_path(@conn, :index, page: @last_page) %>
<% end %>

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title><%= LayoutView.title(@view_module, @view_template, assigns) %></title>
<description><%= LayoutView.excerpt(@view_module, @view_template, assigns) %></description>
<link>https://pre.hn</link>
<atom:link href="https://pre.hn<%= @feed_url %>" rel="self" type="application/rss+xml" />
<%= for post <- @posts do %>
<item>
<title><%= post.post_title |> HtmlSanitizeEx.strip_tags() %></title>
<description>
<%= post.post_content |> process_content |> html_escape |> safe_to_string %>
</description>
<pubDate><%=
post.post_date
|> DateTime.from_naive!("Etc/UTC")
|> Timex.format!("{WDshort}, {D} {Mshort} {YYYY} {h24}:{m}:{s} {Z}")
%></pubDate>
<guid isPermaLink="true"><%= post.guid %></guid>
</item>
<% end %>
</channel>
</rss>

View file

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<title><%= title(@view_module, @view_template, assigns) %></title>
<link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>">
<link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/admin.css") %>">
</head>
<body>
<div class="container">
<main role="main" class="grid">
<nav class="column admin-nav">
<menu type="list">
<li>
<%= link "🏠 Home", to: Routes.admin_home_path(@conn, :index) %>
</li>
<li>
<%= link "📝 Posts", to: Routes.admin_posts_path(@conn, :index) %>
</li>
<li>
<%= link "📄 Pages", to: Routes.admin_posts_path(@conn, :index, post_type: "page") %>
</li>
<li>
<%= link "👋 Logout", to: AuthWeb.Router.Helpers.pow_session_path(@conn, :delete), method: :delete %>
</li>
</menu>
</nav>
<section class="column four with-gutters ">
<%= if get_flash(@conn, :info) do %>
<input type="checkbox" id="dismiss-alert-info" class="alert-dismisser" />
<div class="alert alert-info" role="alert">
<p class="alert-text">
<%= get_flash(@conn, :info) %>
<label for="dismiss-alert-info">Dismiss</label>
</p>
</div>
<% end %>
<%= if get_flash(@conn, :error) do %>
<input type="checkbox" id="dismiss-alert-error" class="alert-dismisser" />
<div class="alert alert-error" role="alert">
<p class="alert-text">
<%= get_flash(@conn, :error) %>
<label for="dismiss-alert-error">Dismiss</label>
</p>
</div>
<% end %>
<%= @inner_content %>
</section>
</main>
</div> <!-- /container -->
<script src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
<script src="<%= Routes.static_path(@conn, "/js/admin.js") %>"></script>
</body>
</html>

View file

@ -0,0 +1,5 @@
<div data-react-component="MenuEditor" data-react-props="<%= %{menu: @menu, posts: @posts, categories: @categories, id: @id} |> Phoenix.json_library().encode!() |> Base.encode64() %>">
</div>
<%= form_for @conn, Routes.menus_path(@conn, :update, @id), [method: :put], fn _f -> %>
<% end %>

View file

@ -0,0 +1,35 @@
<%= Enum.map(Content.Comments.children(@parent_id, @post.comments), fn comment -> %>
<li class="Comment">
<div class="Comment-topmatter">
<span class="Gravatar" style="background-image: url(<%= comment.comment_author_email |> gravatar_url_for_email %>)">
</span>
<h4>
<%= comment.comment_author || "Anonymous" %>
</h4>
<h5>
<%= comment.comment_date |> Timex.format!("%F", :strftime) %>
</h5>
</div>
<div class="Comment-content">
<%= sanitize comment.comment_content |> auto_paragraph_tags |> elem(1) |> IO.iodata_to_binary() %>
<p class="Comment-actions">
<a href="#reply-to-<%= comment.comment_ID %>" class="Comment--replyLink">
&#x27A6; Reply
</a>
</p>
</div>
<ul class="CommentList">
<%= render "comments.html", post: @post, parent_id: comment.comment_ID, conn: @conn %>
<li class="Comment Comment--replyForm" id="reply-to-<%= comment.comment_ID %>">
<h4>
Reply to <%= comment.comment_author || "Anonymous" %>
</h4>
<%=
render "reply_form.html",
comment_changeset: comment_changeset_for_parent(comment),
conn: @conn
%>
</li>
</ul>
</li>
<% end) %>

View file

@ -0,0 +1 @@
<%= render "form.html", Map.put(assigns, :action, Routes.posts_path(@conn, :update, @posts)) %>

View file

@ -0,0 +1,82 @@
<%= form_for @changeset, @action, fn f -> %>
<div class="grid">
<div class="column three">
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<ul>
<%= for {error_key, error} <- @changeset.errors |> Keyword.drop([:post_title, :post_content]) do %>
<%= if error do %>
<li><%= error_key %>: <%= error_tag f, error_key %></li>
<% end %>
<% end %>
</ul>
<% end %>
<%= label f, :post_title do %>
Title
<%= text_input f, :post_title %>
<% end %>
<%= label f, :post_content, "data-react-class": "Editor" do %>
Content
<%= textarea f, :post_content, "data-simplemde": true %>
<% end %>
<div class="grid">
<div class="form-group input-group column uLeft">
<%= submit "Delete", class: "btn btn-warning", form: "deleteForm" %>
</div>
<div class="form-group input-group column">
<%= submit "Save", class: "btn btn-primary" %>
</div>
</div>
</div>
<div class="column post-admin-sidebar">
<%= label f, :post_status, class: "column input-group" do %>
<div>Status</div>
<%= select f, :post_status, [{"Publish", :publish}, {"Draft", :draft}] %>
<% end %>
<%= label f, :post_author, class: "input-group" do %>
<div>Author</div>
<%= select f, :post_author, @author_options |> Enum.map(&({&1.display_name, &1."ID"})) %>
<% end %>
<%= label f, :post_excerpt, class: "input-group" do %>
<div>Excerpt</div>
<%= textarea f, :post_excerpt %>
<% end %>
<%= label f, :sticky, class: "input-group" do %>
<div>Sticky?</div>
<%= checkbox f, :sticky %>
<% end %>
<%= label f, :comment_status, class: "input-group" do %>
<div>Comment Status</div>
<%= select f, :comment_status, ["open", "closed"] %>
<% end %>
<%= label f, :ping_status, class: "input-group" do %>
<div>Ping Status</div>
<%= select f, :ping_status, ["open", "closed"] %>
<% end %>
<%= label f, :post_password, class: "input-group" do %>
<div>Post Password</div>
<%= text_input f, :post_password %>
<% end %>
<%= label f, :post_name, class: "input-group" do %>
<div>Slug</div>
<%= text_input f, :post_name %>
<% end %>
<%= label f, :post_order, class: "input-group" do %>
<div>Post Order</div>
<%= number_input f, :post_order %>
<% end %>
</div>
</div>
<%= hidden_input f, :post_type, value: @post_type %>
<% end %>
<%= if assigns[:posts] do %>
<%= form_for @changeset, Routes.posts_path(@conn, :delete, @posts), [method: :delete, id: "deleteForm"], fn _f -> %>
<% end %>
<% end %>

View file

@ -0,0 +1,15 @@
<article class="<%= post_class(@post) %> h-entry">
<div class="uHidden">
<h1 class="p-name">
<%= link to: Routes.posts_path(@conn, :show, @post), class: "u-url" do %>
<%= raw @post.post_title %>
<% end %>
</h1>
<%= post_topmatter(@conn, @post) %>
</div>
<div class="Article-content <%= if @post.post_format, do: @post.post_format.slug %> e-content">
<%= render "thumb.html", post: @post, thumbs: @thumbs %>
<%= @post |> Content.Post.content_page(@page) |> process_content |> raw %>
</div>
<%= render "pagination.html", conn: @conn, post: @post, current_page: @page %>
</article>

View file

@ -0,0 +1,62 @@
<%= for post <- @posts do %>
<article class="<%= post_class(post) %> h-entry">
<h2 class="entry-title p-name">
<%= link to: Routes.posts_path(@conn, :show, post), class: "u-url" do %>
<%= raw post.post_title %>
<% end %>
</h2>
<%= post_topmatter(@conn, post) %>
<div class="Article-content <%= if post.post_format, do: post.post_format.slug %> e-content">
<%= if authenticated_for_post?(@conn, post) do %>
<%= render "thumb.html", post: post, thumbs: @thumbs %>
<div class="Article-content-words">
<%= raw post |> Content.Post.content_page(1) |> Content.Post.before_more |> process_content |> raw %>
<%= if post.post_content =~ "<!--more-->" do %>
<p>
<%= link "Keep Reading", to: Routes.posts_path(@conn, :show, post) %>
</p>
<% end %>
<%= render "pagination.html", conn: @conn, post: post %>
</div>
<% else %>
<%= render "password_form.html", post: post, conn: @conn %>
<% end %>
</div>
<p class="CategoryBlock">
Categories:
<%= for term <- post.categories do %>
<%= link term.name, to: Routes.category_path(@conn, :index_posts, term.slug), class: "p-category" %>
<% end %>
</p>
<hr />
</article>
<% end %>
<nav class="paginator">
Page:
<%= if @page > 1 do %>
<%= link 1, to: paginated_posts_path(@conn, @category, 1) %>
<% end %>
<%= if @page > 3 do %>
<span class="paginator-page">...</span>
<% end %>
<%= if @page > 2 do %>
<%= link @page - 1, to: paginated_posts_path(@conn, @category, @page - 1) %>
<% end %>
<span class="paginator-page"><%= @page %></span>
<%= if @page + 1 < @last_page do %>
<%= link @page + 1, to: paginated_posts_path(@conn, @category, @page + 1) %>
<% end %>
<%= if @page + 2 < @last_page do %>
<span class="paginator-page">...</span>
<% end %>
<%= if @page < @last_page do %>
<%= link @last_page, to: paginated_posts_path(@conn, @category, @last_page) %>
<% end %>
</nav>

View file

@ -0,0 +1 @@
<%= render "form.html", Map.put(assigns, :action, Routes.posts_path(@conn, :create)) %>

View file

@ -0,0 +1,14 @@
<%= if Content.Post.paginated_post?(@post) do %>
<nav class="paginator">
Page:
<%= Enum.map(
1..Content.Post.content_page_count(@post),
fn page ->
if assigns[:current_page] == nil || assigns[:current_page] != page do
link page, to: Routes.paged_post_path(@conn, :show, @post, page)
else
content_tag :span, page, class: "paginator-page"
end
end) %>
</nav>
<% end %>

View file

@ -0,0 +1,11 @@
<%= form_for @conn, "/wp-login.php?action=postpass", [class: "post-password-form"], fn f -> %>
<p>This content is password protected. To view it please enter your password below:</p>
<p>
<%= label f, :post_password do %>
Password: <input name="post_password" id="pwbox-131" type="password" size="20">
<% end %>
</p>
<p class="form-group">
<input type="submit" name="Submit" value="Enter">
</p>
<% end %>

View file

@ -0,0 +1,24 @@
<%= form_for @comment_changeset, Routes.comment_path(@conn, :create), fn f -> %>
<%= hidden_input f, :comment_parent %>
<%= hidden_input f, :comment_post_ID %>
<%= label f, :comment_author do %>
Your Name
<%= text_input f, :comment_author %>
<% end %>
<%= label f, :comment_author_email do %>
Your Email
<%= email_input f, :comment_author_email %>
<% end %>
<%= label f, :comment_author_url do %>
Your Website
<%= text_input f, :comment_author_url %>
<% end %>
<%= label f, :comment_content do %>
Comment
<%= textarea f, :comment_content %>
<% end %>
<div class="form-group">
<%= submit "\u27A6 Reply", class: "btn btn-primary" %>
</div>
<% end %>

View file

@ -0,0 +1,39 @@
<article class="<%= post_class(@post) %> h-entry">
<div>
<h1 class="p-name">
<%= link to: Routes.posts_path(@conn, :show, @post), class: "u-url" do %>
<%= raw @post.post_title %>
<% end %>
</h1>
<%= post_topmatter(@conn, @post) %>
</div>
<div class="Article-content <%= if @post.post_format, do: @post.post_format.slug %> e-content">
<%= render "thumb.html", post: @post, thumbs: @thumbs %>
<%= @post |> Content.Post.content_page(@page) |> process_content |> raw %>
<p class="CategoryBlock">
Categories:
<%= for term <- @post.categories do %>
<%= link term.name, to: Routes.category_path(@conn, :index_posts, term.slug), class: "p-category" %>
<% end %>
</p>
</div>
<%= render "pagination.html", conn: @conn, post: @post, current_page: @page %>
<%= if @post.comment_status == "open" do %>
<h3>Comments</h3>
<a href="#reply-to-post">
Reply to "<%= @post.post_title %>"
</a>
<ul class="CommentList">
<%= render "comments.html", post: @post, parent_id: 0, conn: @conn %>
<li class="Comment" id="reply-to-post">
<%=
render "reply_form.html",
comment_changeset: comment_changeset_for_post(@post),
post: @post,
conn: @conn
%>
</li>
</ul>
<% end %>
<hr />
</article>

View file

@ -0,0 +1,13 @@
<%= case @thumbs[@post.'ID'] do %>
<% thumb = %Content.Post{} -> %>
<%= if thumb |> Content.Attachment.vertical?() do %>
<div class="post-thumbnail post-thumbnail--vertical">
<%= img_tag thumb.guid %>
</div>
<% else %>
<div class="post-thumbnail">
<%= img_tag thumb.guid %>
</div>
<% end %>
<% nil -> %>
<% end %>

View file

@ -0,0 +1,21 @@
<h1>Site Index</h1>
<h2>Posts and Pages</h2>
<ul>
<%= for post <- @posts do %>
<li>
<%= link to: Routes.posts_path(@conn, :show, post) do %>
<%= raw post.post_title %>
<% end %>
</li>
<% end %>
</ul>
<h2>Categories</h2>
<ul>
<%= for category <- @categories do %>
<li>
<%= link to: Routes.category_path(@conn, :index_posts, category.slug)do %>
<%= raw category.name %>
<% end %>
</li>
<% end %>
</ul>

View file

@ -0,0 +1,5 @@
<h2>Edit comment</h2>
<%= render "form.html", Map.put(assigns, :action, Routes.comment_path(@conn, :update, @comment)) %>
<span><%= link "Back", to: Routes.comment_path(@conn, :index) %></span>

View file

@ -0,0 +1,11 @@
<%= form_for @changeset, @action, fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<div class="form-group">
<%= submit "Submit", class: "btn btn-primary" %>
</div>
<% end %>

View file

@ -0,0 +1,24 @@
<h2>Listing comments</h2>
<table class="table">
<thead>
<tr>
<th></th>
</tr>
</thead>
<tbody>
<%= for comment <- @comments do %>
<tr>
<td class="text-right">
<span><%= link "Show", to: Routes.comment_path(@conn, :show, comment), class: "btn btn-default btn-xs" %></span>
<span><%= link "Edit", to: Routes.comment_path(@conn, :edit, comment), class: "btn btn-default btn-xs" %></span>
<span><%= link "Delete", to: Routes.comment_path(@conn, :delete, comment), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %></span>
</td>
</tr>
<% end %>
</tbody>
</table>
<span><%= link "New comment", to: Routes.comment_path(@conn, :new) %></span>

View file

@ -0,0 +1,5 @@
<h2>New comment</h2>
<%= render "form.html", Map.put(assigns, :action, Routes.comment_path(@conn, :create)) %>
<span><%= link "Back", to: Routes.comment_path(@conn, :index) %></span>

View file

@ -0,0 +1,8 @@
<h2>Show comment</h2>
<ul>
</ul>
<span><%= link "Edit", to: Routes.comment_path(@conn, :edit, @comment) %></span>
<span><%= link "Back", to: Routes.comment_path(@conn, :index) %></span>

View file

@ -0,0 +1,3 @@
defmodule Content.AdminHomeView do
use Content, :view
end

View file

@ -0,0 +1,3 @@
defmodule Content.AdminPostsView do
use Content, :view
end

View file

@ -1,4 +1,4 @@
defmodule ContentWeb.ErrorHelpers do defmodule Content.ErrorHelpers do
@moduledoc """ @moduledoc """
Conveniences for translating and building error messages. Conveniences for translating and building error messages.
""" """
@ -39,9 +39,9 @@ defmodule ContentWeb.ErrorHelpers do
# should be written to the errors.po file. The :count option is # should be written to the errors.po file. The :count option is
# set by Ecto and indicates we should also apply plural rules. # set by Ecto and indicates we should also apply plural rules.
if count = opts[:count] do if count = opts[:count] do
Gettext.dngettext(ContentWeb.Gettext, "errors", msg, msg, count, opts) Gettext.dngettext(Content.Gettext, "errors", msg, msg, count, opts)
else else
Gettext.dgettext(ContentWeb.Gettext, "errors", msg, opts) Gettext.dgettext(Content.Gettext, "errors", msg, opts)
end end
end end
end end

View file

@ -1,5 +1,5 @@
defmodule ContentWeb.ErrorView do defmodule Content.ErrorView do
use ContentWeb, :view use Content, :view
# If you want to customize a particular status code # If you want to customize a particular status code
# for a certain format, you may uncomment below. # for a certain format, you may uncomment below.

View file

@ -0,0 +1,70 @@
defmodule Content.FeedsView do
use Content, :view
use Phoenix.HTML
alias Phoenix.HTML
alias Phoenix.HTML.Tag
alias Content.LayoutView
def gravatar_url_for_email(email) do
email
|> Kernel.||("noreply@example.com")
|> String.trim()
|> String.downcase()
|> (&(:crypto.hash(:md5, &1))).()
|> Base.encode16()
|> String.downcase()
|> (&("https://www.gravatar.com/avatar/#{&1}")).()
end
def auto_paragraph_tags(string) do
string
|> Kernel.||("")
|> String.split(["\n\n", "\r\n\r\n"], trim: true)
|> Enum.map(fn text ->
[Tag.content_tag(:p, text |> HTML.raw(), []), ?\n]
end)
|> HTML.html_escape()
end
def post_class(post) do
sticky =
if post.sticky do
"sticky"
end
"post post-#{post.'ID'} #{sticky}"
end
def post_topmatter(conn, post) do
author =
post.author ||
%Auth.User{
email: "example@example.org",
display_name: "Anonymous",
homepage_url: "#"
}
assigns = %{post: post, author: author, conn: conn}
~E"""
<% _ = assigns # suppress unused assigns warning %>
<div class="Comment-topmatter">
<h4>
<%= link to: author.homepage_url, rel: "author", class: "p-author h-card" do %>
<%= author.display_name %>
<%= img_tag gravatar_url_for_email(author.email), alt: "Photo of #{author.display_name}", class: "Gravatar u-photo" %>
<% end %>
</h4>
<h5>
<%= link to: Routes.posts_path(conn, :show, post) do %>
<time class="dt-published" datetime="<%= post.post_date %>">
<%= post.post_date |> Timex.format!("%F", :strftime) %>
</time>
<% end %>
</h5>
</div>
"""
end
def unauthenticated_post?(_conn, post) do
post.post_password == nil || String.length(post.post_password) == 0
end
end

View file

@ -0,0 +1,92 @@
defmodule Content.LayoutView do
use Content, :view
alias Content.{Option, Options}
def title(Content.PostsView, "index.html", assigns) do
"Page #{assigns.page} | #{title(nil, nil, nil)}"
end
def title(Content.FeedsView, "index.rss", %{category: category}) do
"#{category} | #{title(nil, nil, nil)}"
end
def title(Content.PostsView, "show.html", assigns) do
(assigns.post.post_title |> HtmlSanitizeEx.strip_tags()) <> " | " <> title(nil, nil, nil)
end
def title(_, _, _) do
case Options.get("blogname") do
opt = %Option{} ->
opt.option_value
_ ->
"Hello"
end
end
def excerpt(Content.PostsView, "show.html", assigns) do
assigns.post.post_excerpt
|> HtmlSanitizeEx.strip_tags()
end
def excerpt(Content.FeedsView, "index.rss", %{category: category}) do
"#{category} | #{excerpt(nil, nil, nil)}"
end
def excerpt(_, _, _) do
case Options.get("blogdescription") do
opt = %Option{} ->
opt.option_value
_ ->
"Yet another website"
end
end
def author(Content.PostsView, "show.html", assigns) do
case assigns do
%{author: %{display_name: name}} ->
name
_ ->
"Anonymous"
end
end
def author(_, _, _) do
"Anonymous"
end
def corresponding_feed_url(conn, _, _, %{category: nil}) do
Routes.index_feed_url(conn, :index)
end
def corresponding_feed_url(conn, Content.PostsView, "index.html", %{category: category}) do
Routes.category_feed_url(conn, :index, category)
end
def corresponding_feed_url(conn, _, _, _) do
Routes.index_feed_url(conn, :index)
end
def menu_markup(menu_items, conn), do: menu_markup(menu_items, conn, 0)
def menu_markup(nil, _, _), do: ""
def menu_markup([], _, _), do: ""
def menu_markup(menu_items, conn, level) do
~E"""
<ul style="--menu-level: <%= level %>;">
<%= for item <- menu_items do %>
<li>
<label>
<%= case item[:type] do %>
<% "category" -> %>
<%= link item[:related_item].title, to: Routes.category_path(conn, :index_posts, item[:related_item].slug) %>
<% _ -> %>
<%= link item[:related_item].title, to: Routes.posts_path(conn, :show, item[:related_item].slug) %>
<% end %>
<%= menu_markup(item[:children], conn, level + 1) %>
</label>
</li>
<% end %>
</ul>
"""
end
end

View file

@ -0,0 +1,3 @@
defmodule Content.MenusView do
use Content, :view
end

Some files were not shown because too many files have changed in this diff Show more