mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
avatar uploading
This commit is contained in:
parent
4627a82adb
commit
ae189bafce
16 changed files with 209 additions and 7 deletions
|
@ -75,6 +75,7 @@ defmodule Philomena.Images do
|
|||
end)
|
||||
|> Multi.run(:after, fn _repo, %{image: image} ->
|
||||
Uploader.persist_upload(image)
|
||||
Uploader.unpersist_old_upload(image)
|
||||
|
||||
{:ok, nil}
|
||||
end)
|
||||
|
|
|
@ -93,6 +93,7 @@ defmodule Philomena.Images.Image do
|
|||
field :added_tags, {:array, :any}, default: [], virtual: true
|
||||
|
||||
field :uploaded_image, :string, virtual: true
|
||||
field :removed_image, :string, virtual: true
|
||||
|
||||
timestamps(inserted_at: :created_at)
|
||||
end
|
||||
|
@ -129,7 +130,7 @@ defmodule Philomena.Images.Image do
|
|||
:image, :image_name, :image_width, :image_height, :image_size,
|
||||
:image_format, :image_mime_type, :image_aspect_ratio,
|
||||
:image_orig_sha512_hash, :image_sha512_hash, :uploaded_image,
|
||||
:image_is_animated
|
||||
:removed_image, :image_is_animated
|
||||
])
|
||||
|> validate_required([
|
||||
:image, :image_width, :image_height, :image_size,
|
||||
|
|
|
@ -14,6 +14,10 @@ defmodule Philomena.Images.Uploader do
|
|||
Uploader.persist_upload(image, image_file_root(), "image")
|
||||
end
|
||||
|
||||
def unpersist_old_upload(image) do
|
||||
Uploader.unpersist_old_upload(image, image_file_root(), "image")
|
||||
end
|
||||
|
||||
defp image_file_root do
|
||||
Application.get_env(:philomena, :image_file_root)
|
||||
end
|
||||
|
|
|
@ -6,6 +6,7 @@ defmodule Philomena.Uploader do
|
|||
alias Philomena.Filename
|
||||
alias Philomena.Analyzers
|
||||
alias Philomena.Sha512
|
||||
import Ecto.Changeset
|
||||
|
||||
@doc """
|
||||
Performs analysis of the passed Plug.Upload, and invokes a changeset
|
||||
|
@ -17,6 +18,11 @@ defmodule Philomena.Uploader do
|
|||
with {:ok, analysis} <- Analyzers.analyze(upload_parameter),
|
||||
analysis <- extra_attributes(analysis, upload_parameter)
|
||||
do
|
||||
removed =
|
||||
model_or_changeset
|
||||
|> change()
|
||||
|> get_field(field(field_name))
|
||||
|
||||
attributes =
|
||||
%{
|
||||
"name" => analysis.name,
|
||||
|
@ -33,6 +39,7 @@ defmodule Philomena.Uploader do
|
|||
|> prefix_attributes(field_name)
|
||||
|> Map.put(field_name, analysis.new_name)
|
||||
|> Map.put(upload_key(field_name), upload_parameter.path)
|
||||
|> Map.put(remove_key(field_name), removed)
|
||||
|
||||
changeset_fn.(model_or_changeset, attributes)
|
||||
else
|
||||
|
@ -42,13 +49,13 @@ defmodule Philomena.Uploader do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Writes the file to permanent storage. This should be the last step in the
|
||||
transaction.
|
||||
Writes the file to permanent storage. This should be the second-to-last step
|
||||
in the transaction.
|
||||
"""
|
||||
@spec persist_upload(any(), String.t(), String.t()) :: any()
|
||||
def persist_upload(model, file_root, field_name) do
|
||||
source = Map.get(model, String.to_existing_atom(upload_key(field_name)))
|
||||
dest = Map.get(model, String.to_existing_atom(field_name))
|
||||
source = Map.get(model, field(upload_key(field_name)))
|
||||
dest = Map.get(model, field(field_name))
|
||||
target = Path.join(file_root, dest)
|
||||
dir = Path.dirname(target)
|
||||
|
||||
|
@ -58,6 +65,17 @@ defmodule Philomena.Uploader do
|
|||
File.cp!(source, target)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes the old file from permanent storage. This should be the last step in
|
||||
the transaction.
|
||||
"""
|
||||
@spec unpersist_old_upload(any(), String.t(), String.t()) :: any()
|
||||
def unpersist_old_upload(model, file_root, field_name) do
|
||||
model
|
||||
|> Map.get(field(remove_key(field_name)))
|
||||
|> try_remove(file_root)
|
||||
end
|
||||
|
||||
defp extra_attributes(analysis, %Plug.Upload{path: path, filename: filename}) do
|
||||
{width, height} = analysis.dimensions
|
||||
aspect_ratio = aspect_ratio(width, height)
|
||||
|
@ -79,8 +97,16 @@ defmodule Philomena.Uploader do
|
|||
defp aspect_ratio(_, 0), do: 0.0
|
||||
defp aspect_ratio(w, h), do: w / h
|
||||
|
||||
defp try_remove("", _file_root), do: nil
|
||||
defp try_remove(nil, _file_root), do: nil
|
||||
defp try_remove(file, file_root), do: File.rm(Path.join(file_root, file))
|
||||
|
||||
defp prefix_attributes(map, prefix),
|
||||
do: Map.new(map, fn {key, value} -> {"#{prefix}_#{key}", value} end)
|
||||
|
||||
defp upload_key(field_name), do: "uploaded_#{field_name}"
|
||||
|
||||
defp remove_key(field_name), do: "removed_#{field_name}"
|
||||
|
||||
defp field(field_name), do: String.to_existing_atom(field_name)
|
||||
end
|
|
@ -4,8 +4,10 @@ defmodule Philomena.Users do
|
|||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Ecto.Multi
|
||||
alias Philomena.Repo
|
||||
|
||||
alias Philomena.Users.Uploader
|
||||
alias Philomena.Users.User
|
||||
|
||||
use Pow.Ecto.Context,
|
||||
|
@ -111,6 +113,33 @@ defmodule Philomena.Users do
|
|||
|> Repo.update()
|
||||
end
|
||||
|
||||
def update_avatar(%User{} = user, attrs) do
|
||||
changeset = Uploader.analyze_upload(user, attrs)
|
||||
|
||||
Multi.new
|
||||
|> Multi.update(:user, changeset)
|
||||
|> Multi.run(:update_file, fn _repo, %{user: user} ->
|
||||
Uploader.persist_upload(user)
|
||||
Uploader.unpersist_old_upload(user)
|
||||
|
||||
{:ok, nil}
|
||||
end)
|
||||
|> Repo.isolated_transaction(:serializable)
|
||||
end
|
||||
|
||||
def remove_avatar(%User{} = user) do
|
||||
changeset = User.remove_avatar_changeset(user)
|
||||
|
||||
Multi.new
|
||||
|> Multi.update(:user, changeset)
|
||||
|> Multi.run(:remove_file, fn _repo, %{user: user} ->
|
||||
Uploader.unpersist_old_upload(user)
|
||||
|
||||
{:ok, nil}
|
||||
end)
|
||||
|> Repo.isolated_transaction(:serializable)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking user changes.
|
||||
|
||||
|
|
24
lib/philomena/users/uploader.ex
Normal file
24
lib/philomena/users/uploader.ex
Normal file
|
@ -0,0 +1,24 @@
|
|||
defmodule Philomena.Users.Uploader do
|
||||
@moduledoc """
|
||||
Upload and processing callback logic for User avatars.
|
||||
"""
|
||||
|
||||
alias Philomena.Users.User
|
||||
alias Philomena.Uploader
|
||||
|
||||
def analyze_upload(user, params) do
|
||||
Uploader.analyze_upload(user, "avatar", params["avatar"], &User.avatar_changeset/2)
|
||||
end
|
||||
|
||||
def persist_upload(user) do
|
||||
Uploader.persist_upload(user, avatar_file_root(), "avatar")
|
||||
end
|
||||
|
||||
def unpersist_old_upload(user) do
|
||||
Uploader.unpersist_old_upload(user, avatar_file_root(), "avatar")
|
||||
end
|
||||
|
||||
defp avatar_file_root do
|
||||
Application.get_env(:philomena, :avatar_file_root)
|
||||
end
|
||||
end
|
|
@ -120,6 +120,14 @@ defmodule Philomena.Users.User do
|
|||
field :secondary_role, :string
|
||||
field :hide_default_role, :boolean, default: false
|
||||
|
||||
# For avatar validation/persistence
|
||||
field :avatar_width, :integer, virtual: true
|
||||
field :avatar_height, :integer, virtual: true
|
||||
field :avatar_size, :integer, virtual: true
|
||||
field :avatar_mime_type, :string, virtual: true
|
||||
field :uploaded_avatar, :string, virtual: true
|
||||
field :removed_avatar, :string, virtual: true
|
||||
|
||||
timestamps(inserted_at: :created_at)
|
||||
end
|
||||
|
||||
|
@ -193,6 +201,27 @@ defmodule Philomena.Users.User do
|
|||
|> validate_format(:personal_title, ~r/\A((?!site|admin|moderator|assistant|developer|\p{C}).)*\z/iu)
|
||||
end
|
||||
|
||||
def avatar_changeset(user, attrs) do
|
||||
user
|
||||
|> cast(attrs, [
|
||||
:avatar, :avatar_width, :avatar_height, :avatar_size, :uploaded_avatar,
|
||||
:removed_avatar
|
||||
])
|
||||
|> validate_required([
|
||||
:avatar, :avatar_width, :avatar_height, :avatar_size, :uploaded_avatar
|
||||
])
|
||||
|> validate_number(:avatar_size, greater_than: 0, less_than_or_equal_to: 300_000)
|
||||
|> validate_number(:avatar_width, greater_than: 0, less_than_or_equal_to: 1000)
|
||||
|> validate_number(:avatar_height, greater_than: 0, less_than_or_equal_to: 1000)
|
||||
|> validate_inclusion(:avatar_mime_type, ~W(image/gif image/jpeg image/png))
|
||||
end
|
||||
|
||||
def remove_avatar_changeset(user) do
|
||||
user
|
||||
|> change(removed_avatar: user.avatar)
|
||||
|> change(avatar: nil)
|
||||
end
|
||||
|
||||
def watched_tags_changeset(user, watched_tag_ids) do
|
||||
change(user, watched_tag_ids: watched_tag_ids)
|
||||
end
|
||||
|
|
33
lib/philomena_web/controllers/avatar_controller.ex
Normal file
33
lib/philomena_web/controllers/avatar_controller.ex
Normal file
|
@ -0,0 +1,33 @@
|
|||
defmodule PhilomenaWeb.AvatarController do
|
||||
use PhilomenaWeb, :controller
|
||||
|
||||
alias Philomena.Users
|
||||
|
||||
plug PhilomenaWeb.FilterBannedUsersPlug
|
||||
plug PhilomenaWeb.ScraperPlug, [params_name: "user", params_key: "avatar"] when action in [:update]
|
||||
|
||||
def edit(conn, _params) do
|
||||
changeset = Users.change_user(conn.assigns.current_user)
|
||||
render(conn, "edit.html", changeset: changeset)
|
||||
end
|
||||
|
||||
def update(conn, %{"user" => user_params}) do
|
||||
case Users.update_avatar(conn.assigns.current_user, user_params) do
|
||||
{:ok, _user} ->
|
||||
conn
|
||||
|> put_flash(:info, "Successfully updated avatar.")
|
||||
|> redirect(to: Routes.avatar_path(conn, :edit))
|
||||
|
||||
{:error, :user, changeset, _changes} ->
|
||||
render(conn, "edit.html", changeset: changeset)
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, _params) do
|
||||
{:ok, _user} = Users.remove_avatar(conn.assigns.current_user)
|
||||
|
||||
conn
|
||||
|> put_flash(:info, "Successfully removed avatar.")
|
||||
|> redirect(to: Routes.avatar_path(conn, :edit))
|
||||
end
|
||||
end
|
|
@ -5,6 +5,7 @@ defmodule PhilomenaWeb.Filter.HideController do
|
|||
alias Philomena.Tags.Tag
|
||||
alias Philomena.Repo
|
||||
|
||||
plug PhilomenaWeb.FilterBannedUsersPlug
|
||||
plug :authorize_filter
|
||||
plug :load_tag
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ defmodule PhilomenaWeb.Filter.SpoilerController do
|
|||
alias Philomena.Tags.Tag
|
||||
alias Philomena.Repo
|
||||
|
||||
plug PhilomenaWeb.FilterBannedUsersPlug
|
||||
plug :authorize_filter
|
||||
plug :load_tag
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ defmodule PhilomenaWeb.ReportController do
|
|||
|
||||
conn
|
||||
|> put_flash(:info, "Your report has been received and will be checked by staff shortly.")
|
||||
|> redirect(to: "/")
|
||||
|> redirect(to: redirect_path(conn, conn.assigns.current_user))
|
||||
|
||||
{:error, changeset} ->
|
||||
# Note that we are depending on the controller that called
|
||||
|
@ -86,4 +86,7 @@ defmodule PhilomenaWeb.ReportController do
|
|||
|
||||
reports_open >= 5
|
||||
end
|
||||
|
||||
defp redirect_path(conn, nil), do: "/"
|
||||
defp redirect_path(conn, _user), do: Routes.report_path(conn, :index)
|
||||
end
|
|
@ -134,6 +134,8 @@ defmodule PhilomenaWeb.Router do
|
|||
resources "/watch", Tag.WatchController, only: [:create, :delete], singleton: true
|
||||
end
|
||||
|
||||
resources "/avatar", AvatarController, only: [:edit, :update, :delete], singleton: true
|
||||
|
||||
resources "/reports", ReportController, only: [:index]
|
||||
resources "/user_links", UserLinkController, only: [:index, :new, :create, :show]
|
||||
resources "/galleries", GalleryController, only: [:new, :create, :edit, :update, :delete] do
|
||||
|
|
42
lib/philomena_web/templates/avatar/edit.html.slime
Normal file
42
lib/philomena_web/templates/avatar/edit.html.slime
Normal file
|
@ -0,0 +1,42 @@
|
|||
|
||||
|
||||
.profile-top
|
||||
.profile-top__avatar
|
||||
= render PhilomenaWeb.UserAttributionView, "_user_avatar.html", object: %{user: @current_user}, conn: @conn
|
||||
.profile-top__name-and-links
|
||||
div
|
||||
h1 Your avatar
|
||||
|
||||
p Add a new avatar or remove your existing one here.
|
||||
p Avatars must be less than 1000px tall and wide, and smaller than 300 kilobytes in size. PNG, JPEG, and GIF are acceptable.
|
||||
|
||||
= form_for @changeset, Routes.avatar_path(@conn, :update), [method: "put", multipart: true], fn f ->
|
||||
= if @changeset.action do
|
||||
.alert.alert-danger
|
||||
p Oops, something went wrong! Please check the errors below.
|
||||
|
||||
/ todo: extract this
|
||||
h4 Select an image
|
||||
.image-other
|
||||
#js-image-upload-previews
|
||||
p Upload a file from your computer, or provide a link to the page containing the image and click Fetch.
|
||||
.field
|
||||
= file_input f, :avatar, class: "input js-scraper"
|
||||
= error_tag f, :avatar_size
|
||||
= error_tag f, :avatar_width
|
||||
= error_tag f, :avatar_height
|
||||
= error_tag f, :avatar_mime_type
|
||||
|
||||
.field.field--inline
|
||||
= url_input f, :scraper_url, class: "input input--wide js-scraper", placeholder: "Link a deviantART page, a Tumblr post, or the image directly"
|
||||
button.button.button--separate-left#js-scraper-preview type="button" title="Fetch the image at the specified URL" data-disable-with="Fetch"
|
||||
' Fetch
|
||||
|
||||
.field-error-js.hidden.js-scraper
|
||||
|
||||
br
|
||||
|
||||
=> submit "Update my avatar", class: "button"
|
||||
|
||||
br
|
||||
= button_to "Remove my avatar", Routes.avatar_path(@conn, :delete), method: "delete", class: "button", data: [confirm: "Are you really, really sure?"]
|
|
@ -1,5 +1,4 @@
|
|||
= form_for @changeset, Routes.image_path(@conn, :create), [multipart: true], fn f ->
|
||||
|
||||
.dnp-warning
|
||||
h4
|
||||
' Read the
|
||||
|
|
|
@ -8,6 +8,10 @@ p
|
|||
' Looking for two-factor authentication?
|
||||
= link "Click here!", to: Routes.registration_totp_path(@conn, :edit)
|
||||
|
||||
p
|
||||
' Looking to change your avatar?
|
||||
= link "Click here!", to: Routes.avatar_path(@conn, :edit)
|
||||
|
||||
h3 API Key
|
||||
p
|
||||
' Your API key is
|
||||
|
|
3
lib/philomena_web/views/avatar_view.ex
Normal file
3
lib/philomena_web/views/avatar_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule PhilomenaWeb.AvatarView do
|
||||
use PhilomenaWeb, :view
|
||||
end
|
Loading…
Reference in a new issue