Как создать простой и расширяемый блог с Elixir и Phoenix

В этом посте мы обсудим, как создать шаблонное веб-приложение Phoenix с аутентификацией пользователя и панелью администратора, вместе с загрузкой изображения в Elixir.

Оригинал статьи https://medium.freecodecamp.org/simple-extensible-blog-built-with-elixir-and-phoenix-61d4dfafabb1

Итак, давайте начнем и построим новый проект в Фениксе. Мы будем следовать настройке по умолчанию, то есть Phoenix подключается к Ecto, работающему на PostgreSQL.

Вот финальные экраны, чтобы дать вам представление о том, как будет выглядеть приложение в конце.

На целевой странице будут показаны все опубликованные блоги в макете карты. Карту можно щелкнуть, чтобы просмотреть этот конкретный пост.

У нас будет панель инструментов, в которой краткая статистика. Доступ к этой странице требует входа в систему администратора.

Будет отдельный раздел с обзором всех сообщений. Здесь вы можете публиковать / изменять / удалять сообщения.

Это макет текстового редактора, включающий редактор меток и подборщик файлов для отображаемого изображения.

Примечание. Полный рабочий код размещен на GitHub . В проекте есть много файлов, которые нельзя использовать в одном блоге. Поэтому я объяснил конкретные, которые, как я предполагаю, являются критическими.

Давайте теперь держим имя проекта как CMS. Поэтому мы начнем с создания нового проекта mix phx.new cms. Запустите mix deps.get для установки зависимостей.

Создайте файл миграции для пользователей и сообщений, соответственно.

# User migration file

mix phx.gen.schema Auth.User users name:string email:string password_hash:string is_admin:boolean

# Posts migration file

mix phx.gen.schema Content.Post posts title:string body:text published:boolean cover:string user_id:integer slug:string

В базе данных должны быть созданы две таблицы, которые представляют собой пользователей и сообщения. Я сохранил это довольно просто, сохраняя только необходимые поля и расширяясь, когда возникает необходимость.

create table(:users) do add :name, :string
add :password_hash, :string
add :email, :string, unique: true
add :is_admin, :boolean, default: false, null: false
timestamps()
end
create table(:posts) do
add :slug, :string, unique: true
add :title, :string
add :body, :text
add :user_id, references(:users, on_delete: :delete_all)
add :published, :boolean, default: false, null: false
add :cover, :string
timestamps()
end

Впоследствии мы можем определить изменения и дополнительные методы в схеме пользователя и публикации.

user.ex

defmodule Cms.Auth.User do

# imports

# schema «users»
@create_fields ~w(name password email)a
@optional_fields ~w(is_admin)a
def create_changeset(user, attrs) do
user
|> cast(attrs, @create_fields ++ @optional_fields)
|> validate_required(@create_fields)
|> validate_format(:email, ~r/@/)
|> validate_length(:name, min: 3)
|> validate_length(:password, min: 3)
|> put_password_hash()
end
defp put_password_hash(changeset) do
case changeset do
%Ecto.Changeset{valid?: true, changes: %{password: pass}} ->
put_change(changeset, :password_hash, Comeonin.Bcrypt.hashpwsalt(pass))
_ ->
changeset
end
end
end

 

post.ex

defmodule Cms.Content.Post do

# imports

@derive {Phoenix.Param, key: :slug}
# schema «posts»
def create_changeset(post, attrs) do
post
|> common_changeset(attrs)
|> validate_required([:user_id, :cover])
end
def common_changeset(changeset, attrs) do
changeset
|> cast(attrs, [:title, :body, :published, :user_id])
|> cast_attachments(attrs, [:cover])
|> validate_required([:title, :body])
|> validate_length(:title, min: 3)
|> process_slug
end
# Private
defp process_slug(%Ecto.Changeset{valid?: validity, changes: %{title: title}} = changeset) do
case validity do
true -> put_change(changeset, :slug, Slugger.slugify_downcase(title))
false -> changeset
end
end
defp process_slug(changeset), do: changeset
end

@derive {Phoenix.Param, key: :slug}

Поскольку мы хотим, чтобы сообщения имели понятную и удобную для SEO структуру URL-адресов, мы сообщаем помощникам маршрута slug вместо ссылки id в пространстве имен URL-адресов.

Маршруты описаны здесь:

 

defmodule CmsWeb.Router do

use CmsWeb, :router

alias Cms.Plug
pipeline :browser do
# Default plugs
plug Plug.Authentication
end
pipeline :authenticated do
plug Plug.EnsureAuthentication
plug Plug.ShowSidebar
end
scope «/admin», CmsWeb, as: :admin do
pipe_through [:browser, :authenticated]
get «/», Admin.HomeController, :index
resources(«/post», Admin.PostController) do
get «/publish», Admin.PostController, :publish, as: :publish
end
end
scope «/», CmsWeb do
pipe_through :browser
resources(«/», PageController, only: [:index, :show])
resources(«/session», SessionController, only: [:create, :new, :delete])
end
end

Ресурсы, специфичные для раздела администратора, группируются вместе и назначаются конвейер, который принудительно выполняет аутентификацию.

Между тем, глобальные маршруты обрабатываются пассивной аутентификацией. Данные пользователя извлекаются, если сеанс присутствует, но страницы все еще доступны. Здесь находятся входные и домашние страницы.

Выполнение mix phx.routesдает мне этот результат:

 

Вид разделен на три логических раздела:

  1. Панель навигации
  2. Боковая панель
  3. Основное содержание

В то время как панель навигации всегда видна, боковая панель появляется только в том случае, если вошел пользователь admin. Просмотр содержимого будет внутри контекста администратора. Ссылки на боковой панели будут расти, когда и когда приложение будет развиваться.

Контроллер Admin.Post следует типичной архитектуре CRUD и включает действие для переключения опубликованного состояния данного сообщения.

 

defmodule CmsWeb.Admin.PostController do

use CmsWeb, :controller

def index(conn, _) do
posts = Blog.get_posts_list()
render(conn, «index.html», posts: posts)
end
def new(conn, _) do
changeset = Post.create_changeset(%Post{}, %{})
render(conn, «new.html», changeset: changeset)
end
def create(conn, %{«post» => params}) do
user = Accounts.get_current_user(conn)
with {:ok, _post} <- Blog.create(params, user) do
redirect(conn, to: admin_post_path(conn, :index))
else
{:error, changeset} ->
render(conn, «new.html», changeset: %{changeset | action: :new})
end
end
def edit(conn, %{«id» => id} = params) do
with %Post{} = post <- Blog.get(id) do
changeset = Post.create_changeset(post, params)
render(conn, «edit.html», changeset: changeset, id: id)
end
end
def update(conn, %{«id» => id, «post» => params}) do
with %Post{} = post <- Blog.get(id),
{:ok, _post} <- Blog.update(post, params) do
redirect(conn, to: admin_post_path(conn, :index))
end
end
def publish(conn, %{«post_id» => id}) do
with %Post{} = post <- Blog.get(id),
{:ok, _post} <- Blog.publish(post) do
redirect(conn, to: admin_post_path(conn, :index))
end
end
end

Множество элементов управления находится на индексной странице раздела администратора. Здесь сообщения могут быть удалены, опубликованы и изменены.

 

templates/admin/post/index.html.eex

<%= button(«New Post», method: «get», to: admin_post_path(@conn, :new), class: «btn btn-sm btn-info») %>

<table class=»table table-bordered»>

<thead>
<th>Post Title</th>
<th>Author</th>
<th>Created</th>
<th>Published</th>
<th>Actions</th>
</thead>
<tbody>
<%= for post <- @posts do %>
<tr>
<td><%= post.title %></td>
<td><%= post.user.name %></td>
<td><%= format_date(post) %></td>
<td><%= published_status(post) %></td>
<td>
<%= link(«View», to: page_path(@conn, :show, post), class: «btn btn-sm btn-secondary») %>
<%= link(«Modify», to: admin_post_path(@conn, :edit, post), class: «btn btn-sm btn-info») %>
<%= if post.published do %>
<%= link(«Unpublish», to: admin_post_publish_path(@conn, :publish, post.slug), data: [confirm: «Are you sure?»], class: «btn btn-sm btn-warning») %>
<% else %>
<%= link(«Publish», to: admin_post_publish_path(@conn, :publish, post.slug), class: «btn btn-sm btn-success») %>
<%= link(«Delete», method: :delete, to: admin_post_path(@conn, :delete, post), data: [confirm: «Are you sure?»], class: «btn btn-sm btn-danger») %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>

 

Чтобы сохранить шаблон не загромождённым, мы можем определить помощники вида удобства, такие как время форматирования и т. д. Отдельно.

views/admin/post_view.ex

defmodule CmsWeb.Admin.PostView do

use CmsWeb, :view

def published_status(post) do
case post.published do
true -> «Yes»
false -> «No»
end
end
def format_date(post) do
{:ok, relative_str} = Timex.format(post.inserted_at, «{relative}», :relative)
relative_str
end
end

 

Arc вместе с arc_ecto предоставляет возможности загрузки файлов в ящик. Поскольку сообщение имеет обложку, мы должны определить конфигурацию дуги в нашем приложении.

Каждый пост, в нашем блоге, требует двух версий обложки — оригинала, который видим внутри определенного вида сообщения и большой версии с меньшим размером для заполнения карт. Пока же, давайте перейдем с разрешением 250×250 для большой версии.

 

defmodule CmsWeb.Uploaders.Cover do

use Arc.Definition

use Arc.Ecto.Definition
@versions [:original, :thumb]
def validate({file, _}) do
~w(.jpg .jpeg .png) |> Enum.member?(Path.extname(file.file_name))
end
def transform(:thumb, _) do
{:convert, «-strip -thumbnail 250×250^ -gravity center -extent 250×250 -format jpg», :jpg}
end
def storage_dir(version, {file, scope}) do
«uploads/posts/cover/#{version}»
end
end

 

Возвращаясь к целевой странице приложения, он будет размещать карты для всех опубликованных сообщений. И каждое сообщение будет доступно через, сформированный слизень.

controllers/page_controller.ex

 

defmodule CmsWeb.PageController do

use CmsWeb, :controller

alias Cms.Content.Blog
alias Cms.Content.Post
def index(conn, _) do
posts = Blog.get_published_posts()
render(conn, «index.html», posts: posts)
end
def show(conn, %{«id» => slug}) do
with %Post{} = post <- Blog.get(slug, true) do
render(conn, «show.html», post: post)
end
end
end

Этот проект исследует Phoenix — как структурированное приложение Phoenix и как смонтировать проект на его базе. Надеюсь, ты чему-то научился и наслаждался этим!

Форк проекта, с небольшими исправлениями, в том числе с использованием новых версий Phoenix и Cowboy. https://github.com/PavelZX/cms

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *