controller/view for tag changes, source changes

This commit is contained in:
byte[] 2019-11-24 21:16:22 -05:00
parent 0e5de7aaa2
commit dd8ecd531c
31 changed files with 431 additions and 34 deletions

View file

@ -14,4 +14,10 @@ defprotocol Philomena.Attribution do
"""
@spec best_user_identifier(struct()) :: String.t()
def best_user_identifier(object)
@doc """
Return whether this object is considered to be anonymous.
"""
@spec anonymous?(struct()) :: true | false
def anonymous?(object)
end

View file

@ -6,4 +6,8 @@ defimpl Philomena.Attribution, for: Philomena.Comments.Comment do
def best_user_identifier(comment) do
to_string(comment.user_id || comment.fingerprint || comment.ip)
end
def anonymous?(comment) do
!!comment.anonymous
end
end

View file

@ -12,6 +12,7 @@ defmodule Philomena.Images do
alias Philomena.SourceChanges.SourceChange
alias Philomena.TagChanges.TagChange
alias Philomena.Tags
alias Philomena.Tags.Tag
@doc """
Gets a single image.
@ -118,11 +119,27 @@ defmodule Philomena.Images do
{:ok, count}
end)
|> Multi.run(:added_tag_count, fn repo, %{image: {_image, added_tags, _removed}} ->
tag_ids = added_tags |> Enum.map(& &1.id)
tags = Tag |> where([t], t.id in ^tag_ids)
{count, nil} = repo.update_all(tags, inc: [images_count: 1])
{:ok, count}
end)
|> Multi.run(:removed_tag_count, fn repo, %{image: {_image, _added, removed_tags}} ->
tag_ids = removed_tags |> Enum.map(& &1.id)
tags = Tag |> where([t], t.id in ^tag_ids)
{count, nil} = repo.update_all(tags, inc: [images_count: -1])
{:ok, count}
end)
|> Repo.isolated_transaction(:serializable)
end
defp tag_change_attributes(attribution, image, tag, added, user) do
now = DateTime.utc_now() |> DateTime.truncate(:second)
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
user_id =
case user do
nil -> nil
@ -134,6 +151,7 @@ defmodule Philomena.Images do
tag_id: tag.id,
user_id: user_id,
created_at: now,
updated_at: now,
tag_name_cache: tag.name,
ip: attribution[:ip],
fingerprint: attribution[:fingerprint],

View file

@ -6,4 +6,8 @@ defimpl Philomena.Attribution, for: Philomena.Images.Image do
def best_user_identifier(image) do
to_string(image.user_id || image.fingerprint || image.ip)
end
def anonymous?(image) do
!!image.anonymous
end
end

View file

@ -16,9 +16,14 @@ defmodule Philomena.Images.TagDiffer do
{tags, actually_added, actually_removed} =
apply_changes(tags, added_tags, removed_tags)
{tag_list_cache, tag_list_plus_alias_cache} =
create_caches(tags)
changeset
|> put_change(:added_tags, actually_added)
|> put_change(:removed_tags, actually_removed)
|> put_change(:tag_list_cache, tag_list_cache)
|> put_change(:tag_list_plus_alias_cache, tag_list_plus_alias_cache)
|> put_assoc(:tags, tags)
end
@ -88,4 +93,26 @@ defmodule Philomena.Images.TagDiffer do
{tags, actually_added, actually_removed}
end
defp create_caches(tags) do
tag_list_cache =
tags
|> Tag.display_order()
|> Enum.map_join(", ", & &1.name)
tag_ids =
tags |> Enum.map(& &1.id)
aliases =
Tag
|> where([t], t.aliased_tag_id in ^tag_ids)
|> Repo.all()
tag_list_plus_alias_cache =
(tags ++ aliases)
|> Tag.display_order()
|> Enum.map_join(", ", & &1.name)
{tag_list_cache, tag_list_plus_alias_cache}
end
end

View file

@ -6,4 +6,8 @@ defimpl Philomena.Attribution, for: Philomena.Posts.Post do
def best_user_identifier(post) do
to_string(post.user_id || post.fingerprint || post.ip)
end
def anonymous?(post) do
!!post.anonymous
end
end

View file

@ -0,0 +1,13 @@
defimpl Philomena.Attribution, for: Philomena.SourceChanges.SourceChange do
def object_identifier(source_change) do
to_string(source_change.image_id || source_change.id)
end
def best_user_identifier(source_change) do
to_string(source_change.user_id || source_change.fingerprint || source_change.ip)
end
def anonymous?(source_change) do
source_change.user_id == source_change.image.user_id and !!source_change.image.anonymous
end
end

View file

@ -0,0 +1,13 @@
defimpl Philomena.Attribution, for: Philomena.TagChanges.TagChange do
def object_identifier(tag_change) do
to_string(tag_change.image_id || tag_change.id)
end
def best_user_identifier(tag_change) do
to_string(tag_change.user_id || tag_change.fingerprint || tag_change.ip)
end
def anonymous?(tag_change) do
tag_change.user_id == tag_change.image.user_id and !!tag_change.image.anonymous
end
end

View file

@ -15,8 +15,10 @@ defmodule Philomena.Tags do
existent_tags =
Tag
|> where([t], t.name in ^tag_names)
|> preload(:implied_tags)
|> preload([:implied_tags, aliased_tag: :implied_tags])
|> Repo.all()
|> Enum.map(& &1.aliased_tag || &1)
|> Enum.uniq()
existent_tag_names =
existent_tags

View file

@ -75,6 +75,21 @@ defmodule Philomena.Tags.Tag do
|> Enum.reject(&"" == &1)
end
def display_order(tags) do
tags
|> Enum.sort_by(&{
&1.category != "rating",
&1.category != "origin",
&1.category != "character",
&1.category != "oc",
&1.category != "species",
&1.category != "content-fanmade",
&1.category != "content-official",
&1.category != "spoiler",
&1.name
})
end
def clean_tag_name(name) do
# Downcase, replace extra runs of spaces, replace unicode quotes
# with ascii quotes, trim space from end

View file

@ -6,4 +6,8 @@ defimpl Philomena.Attribution, for: Philomena.Topics.Topic do
def best_user_identifier(topic) do
to_string(topic.user_id)
end
def anonymous?(topic) do
!!topic.anonymous
end
end

View file

@ -64,6 +64,13 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do
when action in [:show, :index],
do: true
# Comment on images where that is allowed
def can?(_user, :create_comment, %Image{hidden_from_users: false, commenting_allowed: true}), do: true
# Edit metadata on images where that is allowed
def can?(_user, :edit_metadata, %Image{hidden_from_users: false, tag_editing_allowed: true}), do: true
def can?(%User{id: id}, :edit_description, %Image{user_id: id, hidden_from_users: false, description_editing_allowed: true}), do: true
# Vote on images they can see
def can?(user, :vote, image), do: can?(user, :show, image)

View file

@ -7,7 +7,7 @@ defmodule PhilomenaWeb.Image.CommentController do
alias Philomena.Repo
import Ecto.Query
plug PhilomenaWeb.CanaryMapPlug, create: :show, edit: :show, update: :show
plug PhilomenaWeb.CanaryMapPlug, create: :create_comment, edit: :create_comment, update: :create_comment
plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true
# Undo the previous private parameter screwery

View file

@ -0,0 +1,24 @@
defmodule PhilomenaWeb.Image.SourceChangeController do
use PhilomenaWeb, :controller
alias Philomena.Images.Image
alias Philomena.SourceChanges.SourceChange
alias Philomena.Repo
import Ecto.Query
plug PhilomenaWeb.CanaryMapPlug, index: :show
plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true
def index(conn, params) do
image = conn.assigns.image
source_changes =
SourceChange
|> where(image_id: ^image.id)
|> preload([:user, image: [:user, :tags]])
|> order_by(desc: :created_at)
|> Repo.paginate(conn.assigns.scrivener)
render(conn, "index.html", image: image, source_changes: source_changes)
end
end

View file

@ -7,7 +7,7 @@ defmodule PhilomenaWeb.Image.SourceController do
plug PhilomenaWeb.FilterBannedUsersPlug
plug PhilomenaWeb.CaptchaPlug
plug PhilomenaWeb.UserAttributionPlug
plug PhilomenaWeb.CanaryMapPlug, update: :show
plug PhilomenaWeb.CanaryMapPlug, update: :edit_metadata
plug :load_and_authorize_resource, model: Image, id_name: "image_id"
def update(conn, %{"image" => image_params}) do
@ -21,12 +21,12 @@ defmodule PhilomenaWeb.Image.SourceController do
conn
|> put_view(PhilomenaWeb.ImageView)
|> render("_source.html", image: image, changeset: changeset)
|> render("_source.html", layout: false, image: image, changeset: changeset)
{:error, :image, changeset, _} ->
conn
|> put_view(PhilomenaWeb.ImageView)
|> render("_source.html", image: image, changeset: changeset)
|> render("_source.html", layout: false, image: image, changeset: changeset)
end
end
end

View file

@ -0,0 +1,32 @@
defmodule PhilomenaWeb.Image.TagChangeController do
use PhilomenaWeb, :controller
alias Philomena.Images.Image
alias Philomena.TagChanges.TagChange
alias Philomena.Repo
import Ecto.Query
plug PhilomenaWeb.CanaryMapPlug, index: :show
plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true
def index(conn, params) do
image = conn.assigns.image
tag_changes =
TagChange
|> where(image_id: ^image.id)
|> added_filter(params)
|> preload([:tag, :user, image: [:user, :tags]])
|> order_by(desc: :created_at)
|> Repo.paginate(conn.assigns.scrivener)
render(conn, "index.html", image: image, tag_changes: tag_changes)
end
defp added_filter(query, %{"added" => "1"}),
do: where(query, added: true)
defp added_filter(query, %{"added" => "0"}),
do: where(query, added: false)
defp added_filter(query, _params),
do: query
end

View file

@ -3,11 +3,13 @@ defmodule PhilomenaWeb.Image.TagController do
alias Philomena.Images
alias Philomena.Images.Image
alias Philomena.Tags
alias Philomena.Repo
plug PhilomenaWeb.FilterBannedUsersPlug
plug PhilomenaWeb.CaptchaPlug
plug PhilomenaWeb.UserAttributionPlug
plug PhilomenaWeb.CanaryMapPlug, update: :show
plug PhilomenaWeb.CanaryMapPlug, update: :edit_metadata
plug :load_and_authorize_resource, model: Image, id_name: "image_id"
def update(conn, %{"image" => image_params}) do
@ -15,18 +17,25 @@ defmodule PhilomenaWeb.Image.TagController do
image = conn.assigns.image
case Images.update_tags(image, attributes, image_params) do
{:ok, %{image: image}} ->
{:ok, %{image: {image, added_tags, removed_tags}}} ->
Images.reindex_image(image)
Tags.reindex_tags(added_tags ++ removed_tags)
image =
image
|> Repo.preload(:tags, force: true)
changeset =
Images.change_image(image)
conn
|> put_view(PhilomenaWeb.ImageView)
|> render("_tags.html", image: image, changeset: changeset)
|> render("_tags.html", layout: false, image: image, changeset: changeset)
{:error, :image, changeset, _} ->
conn
|> put_view(PhilomenaWeb.ImageView)
|> render("_tags.html", image: image, changeset: changeset)
|> render("_tags.html", layout: false, image: image, changeset: changeset)
end
end
end

View file

@ -57,6 +57,10 @@ defmodule PhilomenaWeb.ImageController do
%Comment{}
|> Comments.change_comment()
image_changeset =
image
|> Images.change_image()
watching =
Images.subscribed?(image, conn.assigns.current_user)
@ -65,6 +69,7 @@ defmodule PhilomenaWeb.ImageController do
"show.html",
image: image,
comments: comments,
image_changeset: image_changeset,
comment_changeset: comment_changeset,
description: description,
interactions: interactions,

View file

@ -85,6 +85,10 @@ defmodule PhilomenaWeb.Router do
resources "/activity", ActivityController, only: [:index]
resources "/images", ImageController, only: [:index, :show] do
resources "/comments", Image.CommentController, only: [:index, :show, :create]
resources "/tags", Image.TagController, only: [:update], singleton: true
resources "/sources", Image.SourceController, only: [:update], singleton: true
resources "/tag_changes", Image.TagChangeController, only: [:index]
resources "/source_changes", Image.SourceChangeController, only: [:index]
end
resources "/tags", TagController, only: [:index, :show]
resources "/search", SearchController, only: [:index]

View file

@ -0,0 +1,40 @@
.block
= form_for @changeset, Routes.image_source_path(@conn, :update, @image), [method: "put", class: "hidden", id: "source-form", data: [remote: true]], fn f ->
= if can?(@conn, :update_metadata, @image) and !@conn.assigns.current_ban do
= if @changeset.action do
.alert.alert-danger
p Oops, something went wrong! Please check the errors below.
.flex
= url_input f, :source_url, id: "source-field", class: "input input--wide", autocomplete: "off", placeholder: "Source URL"
= submit "Save source", class: "button button--separate-left"
button.button.button--separate-left type="reset" data-click-hide="#source-form" data-click-show="#image-source"
' Cancel
= if !@conn.assigns.current_user do
= checkbox f, :captcha, class: "js-captcha", value: 0
= label f, :captcha, "I am not a robot!"
- else
p
' You can't edit the source on this image.
.flex.flex--wrap#image-source
p
a.button.button--separate-right#edit-source data-click-focus="#source-field" data-click-hide="#image-source" data-click-show="#source-form" title="Edit source" accessKey="s"
i.fa.fa-pencil>
' Source:
p
= if @image.source_url not in [nil, ""] do
a.js-source-link href=@image.source_url
strong
= @image.source_url
- else
em> not provided yet
a.button.button--link.button--separate-left href=Routes.image_source_change_path(@conn, :index, @image) title="Source history"
i.fa.fa-history>
' History

View file

@ -0,0 +1,60 @@
- form_class = if @changeset.action, do: "", else: "hidden"
.js-tagsauce#image_tags_and_source
= if can?(@conn, :update_metadata, @image) and !@conn.assigns.current_ban do
.js-imageform class=form_class
= form_for @changeset, Routes.image_tag_path(@conn, :update, @image), [id: "tags-form", method: "put", data: [remote: true]], fn f ->
= if @changeset.action do
.alert.alert-danger
p Oops, something went wrong! Please check the errors below.
= hidden_input f, :old_tag_input, value: @image.tag_list_cache
.field
= label f, :tag_input do
' Separate tags with commas. Use 'artist:name' tags to identify artists. Got questions? Check the
a> href="/pages/tags" tag guidelines
' or the
a href="/pages/spoilers" spoiler guidelines
' .
= render PhilomenaWeb.TagView, "_tag_editor.html", f: f, name: :tag_input, type: :edit, extra: [value: @image.tag_list_cache]
= if !@conn.assigns.current_user do
.block.block--fixed.block--warning
strong
em Hang on a sec…
br
' Make sure you have read and understood our
a> href="/pages/tags" tagging guidelines
' before editing tags.
.field
= checkbox f, :captcha, class: "js-captcha", value: 0
= label f, :captcha, "I am not a robot!"
ul.horizontal-list
li
.actions
= submit "Save tags", class: "button", id: "edit_save_button", data: [disable_with: raw("Saving…")]
li
button.button.js-tag-sauce-toggle data-click-toggle=".tagsauce, .js-imageform" data-click-focus=".js-taginput-plain:not(.hidden), .js-taginput-input"
' Cancel
- else
p
' You can't edit the tags on this image.
.tagsauce
.block
a.button.js-tag-sauce-toggle#edit-tags data-click-toggle=".tagsauce, .js-imageform" data-click-focus=".js-taginput-plain:not(.hidden), .js-taginput-input" title="Edit tags" accessKey="t"
i.fa.fa-pencil>
' Tags:
a.button.button--link.button--separate-left href=Routes.image_tag_change_path(@conn, :index, @image) title="Tag history"
i.fa.fa-history>
' History
= render PhilomenaWeb.TagView, "_tag_list.html", tags: display_order(@image.tags), conn: @conn

View file

@ -8,15 +8,9 @@
| Description:
.image-description__text
== @description
.js-tagsauce id="image_tags_and_source_#{@image.id}"
.tagsauce
= render PhilomenaWeb.TagView, "_tag_list.html", tags: display_order(@image.tags), conn: @conn
.block
.flex.flex--wrap#image-source
= if !!@image.source_url and @image.source_url != "" do
a href=@image.source_url = @image.source_url
- else
em> no source provided yet
= render PhilomenaWeb.ImageView, "_tags.html", image: @image, changeset: @image_changeset, conn: @conn
= render PhilomenaWeb.ImageView, "_source.html", image: @image, changeset: @image_changeset, conn: @conn
h4 Comments
= cond do

View file

@ -0,0 +1,46 @@
h1
' Source changes for
a href=Routes.image_path(@conn, :show, @image)
| image #
= @image.id
- route = fn p -> Routes.image_source_change_path(@conn, :index, @image, p) end
- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @source_changes, route: route, conn: @conn
.block
.block__header
= pagination
.block__content
table.table
thead
tr
th colspan=2 Image
th New Source
th Timestamp
th User
th Initial?
tbody
= for source_change <- @source_changes do
tr
td.center
= link source_change.image_id, to: Routes.image_path(@conn, :show, source_change.image)
td.center
= render PhilomenaWeb.ImageView, "_image_container.html", image: source_change.image, size: :thumb_tiny, conn: @conn
td
= source_change.new_value
td
= pretty_time(source_change.created_at)
td
= render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: source_change, conn: @conn
td
= if source_change.initial do
' &#x2713;
.block__header
= pagination

View file

@ -0,0 +1,58 @@
h1
' Tag changes for
a href=Routes.image_path(@conn, :show, @image)
| image #
= @image.id
- route = fn p -> Routes.image_tag_change_path(@conn, :index, @image, p) end
- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @tag_changes, route: route, conn: @conn
.block
.block__header
span.block__header_title
| Display only:
= link "Removed", to: Routes.image_tag_change_path(@conn, :index, @image, added: 0)
= link "Added", to: Routes.image_tag_change_path(@conn, :index, @image, added: 1)
= link "All", to: Routes.image_tag_change_path(@conn, :index, @image)
.block__header
= pagination
.block__content
table.table
thead
tr
th colspan=2 Image
th Tag
th Action
th Timestamp
th User
tbody
= for tag_change <- @tag_changes do
tr
td.center
= link tag_change.image_id, to: Routes.image_path(@conn, :show, tag_change.image)
td.center
= render PhilomenaWeb.ImageView, "_image_container.html", image: tag_change.image, size: :thumb_tiny, conn: @conn
td
= if tag_change.tag do
= render PhilomenaWeb.TagView, "_tag.html", tag: tag_change.tag, conn: @conn
- else
= tag_change.tag_name_cache || "Unknown tag"
= if tag_change.added do
td.success Added
- else
td.danger Removed
td
= pretty_time(tag_change.created_at)
td
= render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: tag_change, conn: @conn
.block__header
= pagination

View file

@ -1,5 +1,12 @@
elixir:
textarea_options = [
class: "input input--wide tagsinput js-image-input js-taginput js-taginput-plain js-taginput-#{@name}",
placeholder: "Add tags separated with commas"
]
html_options = Keyword.merge(textarea_options, assigns[:extra] || [])
.js-tag-block class="fancy-tag-#{@type}"
= textarea @f, @name, class: "input input--wide tagsinput js-image-input js-taginput js-taginput-plain js-taginput-#{@name}", placeholder: "Add tags separated with commas"
= textarea @f, @name, html_options
.js-taginput.input.input--wide.tagsinput.hidden class="js-taginput-fancy" data-click-focus=".js-taginput-input.js-taginput-#{@name}"
input.input class="js-taginput-input js-taginput-#{@name}" id="taginput-fancy-#{@name}" type="text" placeholder="add a tag" autocapitalize="none" data-ac="true" data-ac-min-length="3" data-ac-source='/tags/autocomplete.json?term='
button.button.button--state-primary.button--bold class="js-taginput-show" data-click-show=".js-taginput-fancy,.js-taginput-hide" data-click-hide=".js-taginput-plain,.js-taginput-show" data-click-focus=".js-taginput-input.js-taginput-#{@name}"

View file

@ -1,4 +1,4 @@
= if !!@object.user and !@object.anonymous do
= if !!@object.user and !anonymous?(@object) do
strong<>
= link(@object.user.name, to: Routes.profile_path(@conn, :show, @object.user))
= if assigns[:awards] do

View file

@ -1,4 +1,4 @@
= if !!@object.user and !@object.anonymous do
= if !!@object.user and !anonymous?(@object) do
= user_avatar(@object, assigns[:class] || "avatar--100px")
- else
= anonymous_avatar(@object, assigns[:class] || "avatar--100px")

View file

@ -0,0 +1,3 @@
defmodule PhilomenaWeb.Image.SourceChangeView do
use PhilomenaWeb, :view
end

View file

@ -0,0 +1,3 @@
defmodule PhilomenaWeb.Image.TagChangeView do
use PhilomenaWeb, :view
end

View file

@ -1,6 +1,8 @@
defmodule PhilomenaWeb.ImageView do
use PhilomenaWeb, :view
alias Philomena.Tags.Tag
def thumb_urls(image, show_hidden) do
%{
thumb_tiny: thumb_url(image, show_hidden, :thumb_tiny),
@ -76,18 +78,7 @@ defmodule PhilomenaWeb.ImageView do
end
def display_order(tags) do
tags
|> Enum.sort_by(&{
&1.category != "rating",
&1.category != "origin",
&1.category != "character",
&1.category != "oc",
&1.category != "species",
&1.category != "content-fanmade",
&1.category != "content-official",
&1.category != "spoiler",
&1.name
})
Tag.display_order(tags)
end
defp thumb_format("svg"), do: "png"

View file

@ -3,6 +3,10 @@ defmodule PhilomenaWeb.UserAttributionView do
use Bitwise
use PhilomenaWeb, :view
def anonymous?(object) do
Attribution.anonymous?(object)
end
def anonymous_name(object) do
salt = anonymous_name_salt()
id = Attribution.object_identifier(object)