Merge branch 'f-content' into 'master'
feat: Add basic content management system See merge request mythic-insight/legendary!5
This commit is contained in:
commit
5a629a3860
136 changed files with 2073 additions and 501 deletions
|
@ -1,9 +1,15 @@
|
|||
image: "elixir:1.10"
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- _build
|
||||
- deps
|
||||
|
||||
variables:
|
||||
POSTGRES_PASSWORD: "postgres"
|
||||
POSTGRES_USER: "postgres"
|
||||
DATABASE_URL: "postgres"
|
||||
MIX_ENV: "test"
|
||||
|
||||
services:
|
||||
- name: postgres:12
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Auth.Users.User do
|
||||
defmodule Auth.User do
|
||||
@moduledoc """
|
||||
The baseline user schema module.
|
||||
"""
|
||||
|
@ -13,6 +13,8 @@ defmodule Auth.Users.User do
|
|||
|
||||
schema "users" do
|
||||
field :roles, {:array, :string}
|
||||
field :display_name, :string
|
||||
field :homepage_url, :string
|
||||
|
||||
pow_user_fields()
|
||||
|
|
@ -4,7 +4,7 @@ defmodule Mix.Tasks.Legendary.CreateAdmin do
|
|||
"""
|
||||
use Mix.Task
|
||||
|
||||
alias Auth.Users.User
|
||||
alias Auth.User
|
||||
alias Auth.Repo
|
||||
alias Ecto.Changeset
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
2
apps/content/.gitignore
vendored
2
apps/content/.gitignore
vendored
|
@ -20,7 +20,7 @@ erl_crash.dump
|
|||
*.ez
|
||||
|
||||
# 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.
|
||||
npm-debug.log
|
||||
|
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
@ -18,7 +18,7 @@ config :logger, :console,
|
|||
|
||||
config :content, Content.Scheduler,
|
||||
jobs: [
|
||||
{"@hourly", {ContentWeb.Sitemaps, :generate, []}}
|
||||
{"@hourly", {Content.Sitemaps, :generate, []}}
|
||||
]
|
||||
|
||||
# Import environment specific config. This must remain at the bottom
|
||||
|
|
|
@ -4,7 +4,7 @@ use Mix.Config
|
|||
config :content, Content.Repo,
|
||||
username: "postgres",
|
||||
password: "postgres",
|
||||
database: "content_dev",
|
||||
database: "legendary_dev",
|
||||
hostname: "localhost",
|
||||
show_sensitive_data_on_connection_error: true,
|
||||
pool_size: 10
|
||||
|
@ -15,7 +15,7 @@ config :content, Content.Repo,
|
|||
# The watchers configuration can be used to run external
|
||||
# watchers to your application. For example, we use it
|
||||
# with webpack to recompile .js and .css sources.
|
||||
config :content, ContentWeb.Endpoint,
|
||||
config :content, Content.Endpoint,
|
||||
http: [port: 4000],
|
||||
debug_errors: true,
|
||||
code_reloader: true,
|
||||
|
@ -55,7 +55,7 @@ config :content, ContentWeb.Endpoint,
|
|||
# different ports.
|
||||
|
||||
# Watch static and templates for browser reloading.
|
||||
config :content, ContentWeb.Endpoint,
|
||||
config :content, Content.Endpoint,
|
||||
live_reload: [
|
||||
patterns: [
|
||||
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
|
||||
|
|
|
@ -2,11 +2,3 @@ use Mix.Config
|
|||
|
||||
# Print only warnings and errors during test
|
||||
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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Content.Application do
|
||||
@moduledoc """
|
||||
The base module of the CMS application.
|
||||
The base module of the Content application.
|
||||
"""
|
||||
use Application
|
||||
|
||||
|
@ -17,6 +17,8 @@ defmodule Content.Application do
|
|||
# Start your own worker by calling: Content.Worker.start_link(arg1, arg2, arg3)
|
||||
# worker(Content.Worker, [arg1, arg2, arg3]),
|
||||
worker(Content.Scheduler, []),
|
||||
Content.Telemetry,
|
||||
Content.Endpoint,
|
||||
]
|
||||
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||
|
@ -28,7 +30,7 @@ defmodule Content.Application do
|
|||
# Tell Phoenix to update the endpoint configuration
|
||||
# whenever the application is updated.
|
||||
def config_change(changed, _new, removed) do
|
||||
Endpoint.config_change(changed, removed)
|
||||
Content.Endpoint.config_change(changed, removed)
|
||||
:ok
|
||||
end
|
||||
end
|
|
@ -37,7 +37,7 @@ defmodule Content.Post do
|
|||
has_many :categories, through: [:term_relationships, :category, :term]
|
||||
has_many :tags, through: [:term_relationships, :tag, :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
|
||||
|
||||
def changeset(struct, params \\ %{}) do
|
||||
|
@ -123,14 +123,14 @@ defmodule Content.Post do
|
|||
end
|
||||
|
||||
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)
|
||||
|
||||
case slug do
|
||||
nil -> changeset
|
||||
_ ->
|
||||
changeset
|
||||
|> put_default(:guid, posts_url(ContentWeb.Endpoint, :show, slug))
|
||||
|> put_default(:guid, posts_url(CoreWeb.Endpoint, :show, slug))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -159,6 +159,18 @@ defmodule Content.Posts 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 ->
|
||||
|
||||
case Integer.parse(id, 10) do
|
||||
|
@ -172,7 +184,6 @@ defmodule Content.Posts do
|
|||
post_scope()
|
||||
|> where([p], p.post_type != "nav_menu_item")
|
||||
|> id_filter.(slug)
|
||||
|> Repo.one!()
|
||||
end
|
||||
|
||||
@doc """
|
|
@ -1,12 +1,12 @@
|
|||
defmodule ContentWeb do
|
||||
defmodule Content do
|
||||
@moduledoc """
|
||||
The entrypoint for defining your web interface, such
|
||||
as controllers, views, channels and so on.
|
||||
|
||||
This can be used in your application as:
|
||||
|
||||
use ContentWeb, :controller
|
||||
use ContentWeb, :view
|
||||
use Content, :controller
|
||||
use Content, :view
|
||||
|
||||
The definitions below will be executed for every view,
|
||||
controller, etc, so keep them short and clean, focused
|
||||
|
@ -19,11 +19,11 @@ defmodule ContentWeb do
|
|||
|
||||
def controller do
|
||||
quote do
|
||||
use Phoenix.Controller, namespace: ContentWeb
|
||||
use Phoenix.Controller, namespace: Content
|
||||
|
||||
import Plug.Conn
|
||||
import ContentWeb.Gettext
|
||||
alias ContentWeb.Router.Helpers, as: Routes
|
||||
import Content.Gettext
|
||||
alias Content.Router.Helpers, as: Routes
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -31,13 +31,21 @@ defmodule ContentWeb do
|
|||
quote do
|
||||
use Phoenix.View,
|
||||
root: "lib/content_web/templates",
|
||||
namespace: ContentWeb,
|
||||
namespace: Content,
|
||||
pattern: "**/*"
|
||||
|
||||
use PhoenixHtmlSanitizer, :basic_html
|
||||
|
||||
# Import convenience functions from controllers
|
||||
import Phoenix.Controller,
|
||||
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
|
||||
unquote(view_helpers())
|
||||
end
|
||||
|
@ -55,7 +63,7 @@ defmodule ContentWeb do
|
|||
def channel do
|
||||
quote do
|
||||
use Phoenix.Channel
|
||||
import ContentWeb.Gettext
|
||||
import Content.Gettext
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -67,9 +75,9 @@ defmodule ContentWeb do
|
|||
# Import basic rendering functionality (render, render_layout, etc)
|
||||
import Phoenix.View
|
||||
|
||||
import ContentWeb.ErrorHelpers
|
||||
import ContentWeb.Gettext
|
||||
alias ContentWeb.Router.Helpers, as: Routes
|
||||
import Content.ErrorHelpers
|
||||
import Content.Gettext
|
||||
alias Content.Router.Helpers, as: Routes
|
||||
end
|
||||
end
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
defmodule ContentWeb.UserSocket do
|
||||
defmodule Content.UserSocket do
|
||||
use Phoenix.Socket
|
||||
|
||||
## Channels
|
||||
# channel "room:*", ContentWeb.RoomChannel
|
||||
# channel "room:*", Content.RoomChannel
|
||||
|
||||
# Socket params are passed from the client and can
|
||||
# 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
|
||||
# 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.
|
||||
@impl true
|
|
@ -0,0 +1,7 @@
|
|||
defmodule Content.AdminHomeController do
|
||||
use Content, :controller
|
||||
|
||||
def index(conn, _params) do
|
||||
render conn, "index.html"
|
||||
end
|
||||
end
|
|
@ -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
|
|
@ -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
|
33
apps/content/lib/content_web/controllers/feeds_controller.ex
Normal file
33
apps/content/lib/content_web/controllers/feeds_controller.ex
Normal 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
|
39
apps/content/lib/content_web/controllers/menus_controller.ex
Normal file
39
apps/content/lib/content_web/controllers/menus_controller.ex
Normal 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
|
|
@ -1,5 +1,5 @@
|
|||
defmodule ContentWeb.PageController do
|
||||
use ContentWeb, :controller
|
||||
defmodule Content.PageController do
|
||||
use Content, :controller
|
||||
|
||||
def index(conn, _params) do
|
||||
render(conn, "index.html")
|
|
@ -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
|
176
apps/content/lib/content_web/controllers/posts_controller.ex
Normal file
176
apps/content/lib/content_web/controllers/posts_controller.ex
Normal 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
|
|
@ -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
|
|
@ -1,5 +1,5 @@
|
|||
defmodule ContentWeb.Endpoint do
|
||||
use Phoenix.Endpoint, otp_app: :content_web
|
||||
defmodule Content.Endpoint do
|
||||
use Phoenix.Endpoint, otp_app: :content
|
||||
|
||||
# The session will be stored in the cookie and signed,
|
||||
# this means its contents can be read but not tampered with.
|
||||
|
@ -10,7 +10,7 @@ defmodule ContentWeb.Endpoint do
|
|||
signing_salt: "wfYQp84C"
|
||||
]
|
||||
|
||||
socket "/socket", ContentWeb.UserSocket,
|
||||
socket "/socket", Content.UserSocket,
|
||||
websocket: true,
|
||||
longpoll: false
|
||||
|
||||
|
@ -22,7 +22,7 @@ defmodule ContentWeb.Endpoint do
|
|||
# when deploying your static files in production.
|
||||
plug Plug.Static,
|
||||
at: "/",
|
||||
from: :content_web,
|
||||
from: :content,
|
||||
gzip: false,
|
||||
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
|
||||
plug Phoenix.LiveReloader
|
||||
plug Phoenix.CodeReloader
|
||||
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :content_web
|
||||
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :content
|
||||
end
|
||||
|
||||
plug Phoenix.LiveDashboard.RequestLogger,
|
||||
|
@ -50,6 +50,6 @@ defmodule ContentWeb.Endpoint do
|
|||
plug Plug.MethodOverride
|
||||
plug Plug.Head
|
||||
plug Plug.Session, @session_options
|
||||
plug Pow.Plug.Session, otp_app: :content_web
|
||||
plug ContentWeb.Router
|
||||
plug Pow.Plug.Session, otp_app: :content
|
||||
plug Content.Router
|
||||
end
|
|
@ -1,11 +1,11 @@
|
|||
defmodule ContentWeb.Gettext do
|
||||
defmodule Content.Gettext do
|
||||
@moduledoc """
|
||||
A module providing Internationalization with a gettext-based API.
|
||||
|
||||
By using [Gettext](https://hexdocs.pm/gettext),
|
||||
your module gains a set of macros for translations, for example:
|
||||
|
||||
import ContentWeb.Gettext
|
||||
import Content.Gettext
|
||||
|
||||
# Simple translation
|
||||
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.
|
||||
"""
|
||||
use Gettext, otp_app: :content_web
|
||||
use Gettext, otp_app: :content
|
||||
end
|
36
apps/content/lib/content_web/plugs/load_user.ex
Normal file
36
apps/content/lib/content_web/plugs/load_user.ex
Normal 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
|
22
apps/content/lib/content_web/plugs/require_admin.ex
Normal file
22
apps/content/lib/content_web/plugs/require_admin.ex
Normal 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
|
22
apps/content/lib/content_web/plugs/require_auth.ex
Normal file
22
apps/content/lib/content_web/plugs/require_auth.ex
Normal 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
|
72
apps/content/lib/content_web/router.ex
Normal file
72
apps/content/lib/content_web/router.ex
Normal 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
|
|
@ -1,4 +1,4 @@
|
|||
defmodule ContentWeb.Telemetry do
|
||||
defmodule Content.Telemetry do
|
||||
@moduledoc """
|
||||
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
|
||||
summary("content_web.repo.query.total_time", unit: {:native, :millisecond}),
|
||||
summary("content_web.repo.query.decode_time", unit: {:native, :millisecond}),
|
||||
summary("content_web.repo.query.query_time", unit: {:native, :millisecond}),
|
||||
summary("content_web.repo.query.queue_time", unit: {:native, :millisecond}),
|
||||
summary("content_web.repo.query.idle_time", unit: {:native, :millisecond}),
|
||||
summary("content.repo.query.total_time", unit: {:native, :millisecond}),
|
||||
summary("content.repo.query.decode_time", unit: {:native, :millisecond}),
|
||||
summary("content.repo.query.query_time", unit: {:native, :millisecond}),
|
||||
summary("content.repo.query.queue_time", unit: {:native, :millisecond}),
|
||||
summary("content.repo.query.idle_time", unit: {:native, :millisecond}),
|
||||
|
||||
# VM Metrics
|
||||
summary("vm.memory.total", unit: {:byte, :kilobyte}),
|
||||
|
@ -52,7 +52,7 @@ defmodule ContentWeb.Telemetry do
|
|||
[
|
||||
# A module, function and arguments to be invoked periodically.
|
||||
# This function must call :telemetry.execute/3 and a metric must be added above.
|
||||
# {ContentWeb, :count_users, []}
|
||||
# {Content, :count_users, []}
|
||||
]
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
<h1>Dashboard</h1>
|
|
@ -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 %>
|
24
apps/content/lib/content_web/templates/feeds/index.rss.eex
Normal file
24
apps/content/lib/content_web/templates/feeds/index.rss.eex
Normal 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>
|
61
apps/content/lib/content_web/templates/layout/admin.html.eex
Normal file
61
apps/content/lib/content_web/templates/layout/admin.html.eex
Normal 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>
|
|
@ -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 %>
|
|
@ -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">
|
||||
➦ 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) %>
|
|
@ -0,0 +1 @@
|
|||
<%= render "form.html", Map.put(assigns, :action, Routes.posts_path(@conn, :update, @posts)) %>
|
82
apps/content/lib/content_web/templates/posts/form.html.eex
Normal file
82
apps/content/lib/content_web/templates/posts/form.html.eex
Normal 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 %>
|
15
apps/content/lib/content_web/templates/posts/front.html.eex
Normal file
15
apps/content/lib/content_web/templates/posts/front.html.eex
Normal 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>
|
62
apps/content/lib/content_web/templates/posts/index.html.eex
Normal file
62
apps/content/lib/content_web/templates/posts/index.html.eex
Normal 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>
|
|
@ -0,0 +1 @@
|
|||
<%= render "form.html", Map.put(assigns, :action, Routes.posts_path(@conn, :create)) %>
|
|
@ -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 %>
|
|
@ -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 %>
|
|
@ -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 %>
|
39
apps/content/lib/content_web/templates/posts/show.html.eex
Normal file
39
apps/content/lib/content_web/templates/posts/show.html.eex
Normal 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>
|
13
apps/content/lib/content_web/templates/posts/thumb.html.eex
Normal file
13
apps/content/lib/content_web/templates/posts/thumb.html.eex
Normal 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 %>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 %>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
3
apps/content/lib/content_web/views/admin_home_view.ex
Normal file
3
apps/content/lib/content_web/views/admin_home_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule Content.AdminHomeView do
|
||||
use Content, :view
|
||||
end
|
3
apps/content/lib/content_web/views/admin_posts_view.ex
Normal file
3
apps/content/lib/content_web/views/admin_posts_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule Content.AdminPostsView do
|
||||
use Content, :view
|
||||
end
|
|
@ -1,4 +1,4 @@
|
|||
defmodule ContentWeb.ErrorHelpers do
|
||||
defmodule Content.ErrorHelpers do
|
||||
@moduledoc """
|
||||
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
|
||||
# set by Ecto and indicates we should also apply plural rules.
|
||||
if count = opts[:count] do
|
||||
Gettext.dngettext(ContentWeb.Gettext, "errors", msg, msg, count, opts)
|
||||
Gettext.dngettext(Content.Gettext, "errors", msg, msg, count, opts)
|
||||
else
|
||||
Gettext.dgettext(ContentWeb.Gettext, "errors", msg, opts)
|
||||
Gettext.dgettext(Content.Gettext, "errors", msg, opts)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +1,5 @@
|
|||
defmodule ContentWeb.ErrorView do
|
||||
use ContentWeb, :view
|
||||
defmodule Content.ErrorView do
|
||||
use Content, :view
|
||||
|
||||
# If you want to customize a particular status code
|
||||
# for a certain format, you may uncomment below.
|
70
apps/content/lib/content_web/views/feeds_view.ex
Normal file
70
apps/content/lib/content_web/views/feeds_view.ex
Normal 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
|
92
apps/content/lib/content_web/views/layout_view.ex
Normal file
92
apps/content/lib/content_web/views/layout_view.ex
Normal 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
|
3
apps/content/lib/content_web/views/menus_view.ex
Normal file
3
apps/content/lib/content_web/views/menus_view.ex
Normal 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
Loading…
Reference in a new issue