В этом посте мы обсудим, как создать шаблонное веб-приложение 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 douse 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дает мне этот результат:
Вид разделен на три логических раздела:
- Панель навигации
- Боковая панель
- Основное содержание
В то время как панель навигации всегда видна, боковая панель появляется только в том случае, если вошел пользователь admin. Просмотр содержимого будет внутри контекста администратора. Ссылки на боковой панели будут расти, когда и когда приложение будет развиваться.
Контроллер Admin.Post следует типичной архитектуре CRUD и включает действие для переключения опубликованного состояния данного сообщения.
defmodule CmsWeb.Admin.PostController douse 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 douse 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 douse 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 douse 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