diff --git a/lib/philomena/badges.ex b/lib/philomena/badges.ex index 6ee95705..5c5f3361 100644 --- a/lib/philomena/badges.ex +++ b/lib/philomena/badges.ex @@ -4,9 +4,11 @@ defmodule Philomena.Badges do """ import Ecto.Query, warn: false + alias Ecto.Multi alias Philomena.Repo alias Philomena.Badges.Badge + alias Philomena.Badges.Uploader @doc """ Returns the list of badges. @@ -50,9 +52,20 @@ defmodule Philomena.Badges do """ def create_badge(attrs \\ %{}) do - %Badge{} - |> Badge.changeset(attrs) - |> Repo.insert() + badge = + %Badge{} + |> Badge.changeset(attrs) + |> Uploader.analyze_upload(attrs) + + Multi.new() + |> Multi.insert(:badge, badge) + |> Multi.run(:after, fn _repo, %{badge: badge} -> + Uploader.persist_upload(badge) + Uploader.unpersist_old_upload(badge) + + {:ok, nil} + end) + |> Repo.isolated_transaction(:serializable) end @doc """ @@ -68,9 +81,20 @@ defmodule Philomena.Badges do """ def update_badge(%Badge{} = badge, attrs) do - badge - |> Badge.changeset(attrs) - |> Repo.update() + badge = + badge + |> Badge.changeset(attrs) + |> Uploader.analyze_upload(attrs) + + Multi.new() + |> Multi.update(:badge, badge) + |> Multi.run(:after, fn _repo, %{badge: badge} -> + Uploader.persist_upload(badge) + Uploader.unpersist_old_upload(badge) + + {:ok, nil} + end) + |> Repo.isolated_transaction(:serializable) end @doc """ @@ -145,8 +169,8 @@ defmodule Philomena.Badges do {:error, %Ecto.Changeset{}} """ - def create_badge_award(attrs \\ %{}) do - %Award{} + def create_badge_award(creator, user, attrs \\ %{}) do + %Award{awarded_by_id: creator.id, user_id: user.id} |> Award.changeset(attrs) |> Repo.insert() end diff --git a/lib/philomena/badges/award.ex b/lib/philomena/badges/award.ex index 9b0a8a36..f39403ff 100644 --- a/lib/philomena/badges/award.ex +++ b/lib/philomena/badges/award.ex @@ -21,7 +21,14 @@ defmodule Philomena.Badges.Award do @doc false def changeset(badge_award, attrs) do badge_award - |> cast(attrs, []) - |> validate_required([]) + |> cast(attrs, [:badge_id, :label, :reason, :badge_name]) + |> put_awarded_on() end + + defp put_awarded_on(%{data: %{awarded_on: nil}} = changeset) do + now = DateTime.utc_now() |> DateTime.truncate(:second) + + put_change(changeset, :awarded_on, now) + end + defp put_awarded_on(changeset), do: changeset end diff --git a/lib/philomena/badges/badge.ex b/lib/philomena/badges/badge.ex index 379cf7a8..9682a638 100644 --- a/lib/philomena/badges/badge.ex +++ b/lib/philomena/badges/badge.ex @@ -4,18 +4,29 @@ defmodule Philomena.Badges.Badge do schema "badges" do field :title, :string - field :description, :string + field :description, :string, default: "" field :image, :string field :disable_award, :boolean, default: false field :priority, :boolean, default: false + field :uploaded_image, :string, virtual: true + field :removed_image, :string, virtual: true + field :image_mime_type, :string, virtual: true + timestamps(inserted_at: :created_at) end @doc false def changeset(badge, attrs) do badge - |> cast(attrs, []) - |> validate_required([]) + |> cast(attrs, [:title, :description, :disable_award, :priority]) + |> validate_required([:title]) + end + + def image_changeset(badge, attrs) do + badge + |> cast(attrs, [:image, :image_mime_type, :uploaded_image, :removed_image]) + |> validate_required([:image, :image_mime_type]) + |> validate_inclusion(:image_mime_type, ["image/svg+xml"]) end end diff --git a/lib/philomena/badges/uploader.ex b/lib/philomena/badges/uploader.ex new file mode 100644 index 00000000..8ab44f9f --- /dev/null +++ b/lib/philomena/badges/uploader.ex @@ -0,0 +1,24 @@ +defmodule Philomena.Badges.Uploader do + @moduledoc """ + Upload and processing callback logic for Badge images. + """ + + alias Philomena.Badges.Badge + alias Philomena.Uploader + + def analyze_upload(badge, params) do + Uploader.analyze_upload(badge, "image", params["image"], &Badge.image_changeset/2) + end + + def persist_upload(badge) do + Uploader.persist_upload(badge, badge_file_root(), "image") + end + + def unpersist_old_upload(badge) do + Uploader.unpersist_old_upload(badge, badge_file_root(), "image") + end + + defp badge_file_root do + Application.get_env(:philomena, :badge_file_root) + end +end diff --git a/lib/philomena/users/ability.ex b/lib/philomena/users/ability.ex index fd815603..2d9199a5 100644 --- a/lib/philomena/users/ability.ex +++ b/lib/philomena/users/ability.ex @@ -1,5 +1,6 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do alias Philomena.Users.User + alias Philomena.Badges.Award alias Philomena.Comments.Comment alias Philomena.Commissions.Commission alias Philomena.Conversations.Conversation @@ -93,6 +94,9 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do def can?(%User{role: "moderator"}, :edit, %Tag{}), do: true def can?(%User{role: "moderator"}, :alias, %Tag{}), do: true + # Award badges + def can?(%User{role: "moderator"}, :create, %Award{}), do: true + # # Assistants can... # diff --git a/lib/philomena_web/controllers/admin/badge_controller.ex b/lib/philomena_web/controllers/admin/badge_controller.ex new file mode 100644 index 00000000..16d0f893 --- /dev/null +++ b/lib/philomena_web/controllers/admin/badge_controller.ex @@ -0,0 +1,61 @@ +defmodule PhilomenaWeb.Admin.BadgeController do + use PhilomenaWeb, :controller + + alias Philomena.Badges.Badge + alias Philomena.Badges + alias Philomena.Repo + import Ecto.Query + + plug :verify_authorized + plug :load_resource, model: Badge, only: [:edit, :update] + + def index(conn, _params) do + badges = + Badge + |> order_by(asc: :title) + |> Repo.paginate(conn.assigns.scrivener) + + render(conn, "index.html", badges: badges) + end + + def new(conn, _params) do + changeset = Badges.change_badge(%Badge{}) + render(conn, "new.html", changeset: changeset) + end + + def create(conn, %{"badge" => badge_params}) do + case Badges.create_badge(badge_params) do + {:ok, %{badge: _badge}} -> + conn + |> put_flash(:info, "Badge created successfully.") + |> redirect(to: Routes.admin_badge_path(conn, :index)) + + {:error, :badge, changeset, _changes} -> + render(conn, "new.html", changeset: changeset) + end + end + + def edit(conn, _params) do + changeset = Badges.change_badge(conn.assigns.badge) + render(conn, "edit.html", changeset: changeset) + end + + def update(conn, %{"badge" => badge_params}) do + case Badges.update_badge(conn.assigns.badge, badge_params) do + {:ok, %{badge: _badge}} -> + conn + |> put_flash(:info, "Badge updated successfully.") + |> redirect(to: Routes.admin_badge_path(conn, :index)) + + {:error, :badge, changeset, _changes} -> + render(conn, "edit.html", changeset: changeset) + end + end + + defp verify_authorized(conn, _opts) do + case Canada.Can.can?(conn.assigns.current_user, :index, Badge) do + true -> conn + _false -> PhilomenaWeb.NotAuthorizedPlug.call(conn) + end + end +end diff --git a/lib/philomena_web/controllers/admin/forum_controller.ex b/lib/philomena_web/controllers/admin/forum_controller.ex index 4bb8413e..34a091c1 100644 --- a/lib/philomena_web/controllers/admin/forum_controller.ex +++ b/lib/philomena_web/controllers/admin/forum_controller.ex @@ -18,7 +18,7 @@ defmodule PhilomenaWeb.Admin.ForumController do def create(conn, %{"forum" => forum_params}) do case Forums.create_forum(forum_params) do - {:ok, forum} -> + {:ok, _forum} -> conn |> put_flash(:info, "Forum created successfully.") |> redirect(to: Routes.admin_forum_path(conn, :index)) @@ -35,7 +35,7 @@ defmodule PhilomenaWeb.Admin.ForumController do def update(conn, %{"forum" => forum_params}) do case Forums.update_forum(conn.assigns.forum, forum_params) do - {:ok, forum} -> + {:ok, _forum} -> conn |> put_flash(:info, "Forum updated successfully.") |> redirect(to: Routes.admin_forum_path(conn, :index)) diff --git a/lib/philomena_web/controllers/profile/award_controller.ex b/lib/philomena_web/controllers/profile/award_controller.ex new file mode 100644 index 00000000..da7ebc43 --- /dev/null +++ b/lib/philomena_web/controllers/profile/award_controller.ex @@ -0,0 +1,74 @@ +defmodule PhilomenaWeb.Profile.AwardController do + use PhilomenaWeb, :controller + + alias Philomena.Badges.Award + alias Philomena.Badges.Badge + alias Philomena.Users.User + alias Philomena.Badges + alias Philomena.Repo + import Ecto.Query + + plug :verify_authorized + plug :load_resource, model: User, id_name: "profile_id", id_field: "slug", persisted: true + plug :load_resource, model: Award, only: [:edit, :update, :delete] + plug :load_badges when action in [:new, :create, :edit, :update] + + def new(conn, _params) do + changeset = Badges.change_badge_award(%Award{}) + render(conn, "new.html", changeset: changeset) + end + + def create(conn, %{"award" => award_params}) do + case Badges.create_badge_award(conn.assigns.current_user, conn.assigns.user, award_params) do + {:ok, _award} -> + conn + |> put_flash(:info, "Award successfully created.") + |> redirect(to: Routes.profile_path(conn, :show, conn.assigns.user)) + + {:error, changeset} -> + render(conn, "new.html", changeset: changeset) + end + end + + def edit(conn, _params) do + changeset = Badges.change_badge_award(conn.assigns.award) + render(conn, "edit.html", changeset: changeset) + end + + def update(conn, %{"award" => award_params}) do + case Badges.update_badge_award(conn.assigns.award, award_params) do + {:ok, _award} -> + conn + |> put_flash(:info, "Award successfully updated.") + |> redirect(to: Routes.profile_path(conn, :show, conn.assigns.user)) + + {:error, changeset} -> + render(conn, "edit.html", changeset: changeset) + end + end + + def delete(conn, _params) do + {:ok, _award} = Badges.delete_badge_award(conn.assigns.award) + + conn + |> put_flash(:info, "Award successfully destroyed. By cruel and unusual means.") + |> redirect(to: Routes.profile_path(conn, :show, conn.assigns.user)) + end + + defp verify_authorized(conn, _opts) do + case Canada.Can.can?(conn.assigns.current_user, :create, Award) do + true -> conn + _false -> PhilomenaWeb.NotAuthorizedPlug.call(conn) + end + end + + defp load_badges(conn, _opts) do + badges = + Badge + |> where(disable_award: false) + |> order_by(asc: :title) + |> Repo.all() + + assign(conn, :badges, badges) + end +end diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index f9ba1738..08c81493 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -147,6 +147,7 @@ defmodule PhilomenaWeb.Router do end resources "/description", Profile.DescriptionController, only: [:edit, :update], singleton: true resources "/user_links", Profile.UserLinkController + resources "/awards", Profile.AwardController, except: [:index, :show] end scope "/filters", Filter, as: :filter do @@ -203,6 +204,7 @@ defmodule PhilomenaWeb.Router do resources "/adverts", AdvertController, except: [:show] resources "/forums", ForumController, except: [:show, :delete] + resources "/badges", BadgeController, except: [:show, :delete] end resources "/duplicate_reports", DuplicateReportController, only: [] do diff --git a/lib/philomena_web/templates/admin/badge/_form.html.slime b/lib/philomena_web/templates/admin/badge/_form.html.slime new file mode 100644 index 00000000..ef26c5d6 --- /dev/null +++ b/lib/philomena_web/templates/admin/badge/_form.html.slime @@ -0,0 +1,31 @@ += form_for @changeset, @action, [multipart: true], fn f -> + = if @changeset.action do + .alert.alert-danger + p Oops, something went wrong! Please check the errors below. + + .field + => label f, :title, "Badge name:" + = text_input f, :title, class: "input input--wide", placeholder: "Name", required: true + = error_tag f, :title + + .field + => label f, :description, "An optional short description:" + = text_input f, :description, class: "input input--wide", placeholder: "Description" + = error_tag f, :description + + .field + => checkbox f, :disable_award, class: "checkbox" + = label f, :disable_award, "Prevent image from appearing in \"Badge to award\" list" + + .field + => checkbox f, :priority, class: "checkbox" + = label f, :priority, "Displays before badges that don't have this checkbox checked" + + h4 Image + .field + => label f, :image, "Upload SVG image:" + = file_input f, :image, class: "input input--wide" + = error_tag f, :image + = error_tag f, :image_mime_type + + = submit "Save Badge", class: "button", data: [disable_with: raw("Saving…")] diff --git a/lib/philomena_web/templates/admin/badge/edit.html.slime b/lib/philomena_web/templates/admin/badge/edit.html.slime new file mode 100644 index 00000000..01df2fba --- /dev/null +++ b/lib/philomena_web/templates/admin/badge/edit.html.slime @@ -0,0 +1,5 @@ +h2 Edit Badge + += render "_form.html", Map.put(assigns, :action, Routes.admin_badge_path(@conn, :update, @badge)) + += link "Back", to: Routes.admin_badge_path(@conn, :index) diff --git a/lib/philomena_web/templates/admin/badge/index.html.slime b/lib/philomena_web/templates/admin/badge/index.html.slime new file mode 100644 index 00000000..d7bce004 --- /dev/null +++ b/lib/philomena_web/templates/admin/badge/index.html.slime @@ -0,0 +1,34 @@ +h2 Badges + +- route = fn p -> Routes.admin_badge_path(@conn, :index, p) end +- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @badges, route: route, conn: @conn + +.block + .block__header + a href=Routes.admin_badge_path(@conn, :new) + i.fa.fa-plus> + ' New Badge + + = pagination + + .block__content + table.table + thead + tr + th Badge + th Image + th Options + tbody + = for badge <- @badges do + tr + td + = badge.title + + td + = badge_image(badge, width: 32, height: 32) + + td + = link "Edit", to: Routes.admin_badge_path(@conn, :edit, badge) + + .block__header.block__header--light + = pagination diff --git a/lib/philomena_web/templates/admin/badge/new.html.slime b/lib/philomena_web/templates/admin/badge/new.html.slime new file mode 100644 index 00000000..72962247 --- /dev/null +++ b/lib/philomena_web/templates/admin/badge/new.html.slime @@ -0,0 +1,5 @@ +h2 New Badge + += render "_form.html", Map.put(assigns, :action, Routes.admin_badge_path(@conn, :create)) + += link "Back", to: Routes.admin_badge_path(@conn, :index) diff --git a/lib/philomena_web/templates/layout/_header_staff_links.html.slime b/lib/philomena_web/templates/layout/_header_staff_links.html.slime index 2c46d9e5..8d7e697a 100644 --- a/lib/philomena_web/templates/layout/_header_staff_links.html.slime +++ b/lib/philomena_web/templates/layout/_header_staff_links.html.slime @@ -17,7 +17,7 @@ ' Users = if manages_forums?(@conn) do - = link to: "#", class: "header__link" do + = link to: Routes.admin_forum_path(@conn, :index), class: "header__link" do i.fa.fa-fw.fa-paragraph> ' Forums @@ -27,7 +27,7 @@ ' Adverts = if manages_badges?(@conn) do - = link to: "#", class: "header__link" do + = link to: Routes.admin_badge_path(@conn, :index), class: "header__link" do i.fa.fa-fw.fa-trophy> ' Badges diff --git a/lib/philomena_web/templates/profile/award/_form.html.slime b/lib/philomena_web/templates/profile/award/_form.html.slime new file mode 100644 index 00000000..d1bf4e85 --- /dev/null +++ b/lib/philomena_web/templates/profile/award/_form.html.slime @@ -0,0 +1,27 @@ += form_for @changeset, @action, fn f -> + = if @changeset.action do + .alert.alert-danger + p Oops, something went wrong! Please check the errors below. + + .field + => label f, :badge_id, "Badge type" + = select f, :badge_id, badge_options(@badges), class: "input", data: [change_selectvalue: "#award_label"] + = error_tag f, :badge_id + + .field + => label f, :label, "Label (Public)" + = text_input f, :label, class: "input input--wide" + = error_tag f, :label + + .field + => label f, :reason, "Reason (Private)" + = text_input f, :reason, class: "input input--wide" + = error_tag f, :reason + + .field + => label f, :badge_name, "Badge name (overrides default name if set, leave blank for default badge name)" + = text_input f, :badge_name, class: "input input--wide" + = error_tag f, :badge_name + + .field + = submit "Save", class: "button", data: [disable_with: raw("Saving…")] diff --git a/lib/philomena_web/templates/profile/award/edit.html.slime b/lib/philomena_web/templates/profile/award/edit.html.slime new file mode 100644 index 00000000..0216955e --- /dev/null +++ b/lib/philomena_web/templates/profile/award/edit.html.slime @@ -0,0 +1,3 @@ +h1 Editing award + += render PhilomenaWeb.Profile.AwardView, "_form.html", changeset: @changeset, badges: @badges, action: Routes.profile_award_path(@conn, :update, @user, @award), conn: @conn diff --git a/lib/philomena_web/templates/profile/award/new.html.slime b/lib/philomena_web/templates/profile/award/new.html.slime new file mode 100644 index 00000000..489bc93e --- /dev/null +++ b/lib/philomena_web/templates/profile/award/new.html.slime @@ -0,0 +1,3 @@ +h1 New award + += render PhilomenaWeb.Profile.AwardView, "_form.html", changeset: @changeset, badges: @badges, action: Routes.profile_award_path(@conn, :create, @user), conn: @conn diff --git a/lib/philomena_web/templates/profile/show.html.slime b/lib/philomena_web/templates/profile/show.html.slime index 018f7981..516e4593 100644 --- a/lib/philomena_web/templates/profile/show.html.slime +++ b/lib/philomena_web/templates/profile/show.html.slime @@ -81,6 +81,14 @@ .flex__grow.center = pretty_time(award.awarded_on) + = if manages_awards?(@conn) do + .flex__grow.center + a href=Routes.profile_award_path(@conn, :delete, @user, award) data-method="delete" data-confirm="Are you really, really sure?" + ' Remove + br + a href=Routes.profile_award_path(@conn, :edit, @user, award) + ' Edit + .block .block__header = if can?(@conn, :edit_description, @user) do diff --git a/lib/philomena_web/views/admin/badge_view.ex b/lib/philomena_web/views/admin/badge_view.ex new file mode 100644 index 00000000..bb0157f8 --- /dev/null +++ b/lib/philomena_web/views/admin/badge_view.ex @@ -0,0 +1,5 @@ +defmodule PhilomenaWeb.Admin.BadgeView do + use PhilomenaWeb, :view + + import PhilomenaWeb.ProfileView, only: [badge_image: 2] +end diff --git a/lib/philomena_web/views/profile/award_view.ex b/lib/philomena_web/views/profile/award_view.ex new file mode 100644 index 00000000..61215dec --- /dev/null +++ b/lib/philomena_web/views/profile/award_view.ex @@ -0,0 +1,13 @@ +defmodule PhilomenaWeb.Profile.AwardView do + use PhilomenaWeb, :view + + def badge_options(badges) do + for badge <- badges do + [ + key: badge.title, + value: badge.id, + data: [set_value: badge.description] + ] + end + end +end diff --git a/lib/philomena_web/views/profile_view.ex b/lib/philomena_web/views/profile_view.ex index a7efde75..48dd36ad 100644 --- a/lib/philomena_web/views/profile_view.ex +++ b/lib/philomena_web/views/profile_view.ex @@ -12,6 +12,9 @@ defmodule PhilomenaWeb.ProfileView do def current?(%{id: id}, %{id: id}), do: true def current?(_user1, _user2), do: false + def manages_awards?(conn), + do: can?(conn, :create, Philomena.Badges.Award) + def award_title(%{badge_name: nil} = award), do: award.badge.title def award_title(%{badge_name: ""} = award),