philomena/lib/philomena/images.ex

548 lines
14 KiB
Elixir
Raw Normal View History

2019-08-18 18:17:05 +02:00
defmodule Philomena.Images do
@moduledoc """
The Images context.
"""
import Ecto.Query, warn: false
2019-11-24 19:36:21 +01:00
alias Ecto.Multi
2019-08-18 18:17:05 +02:00
alias Philomena.Repo
alias Philomena.Images.Image
2019-12-08 05:53:18 +01:00
alias Philomena.Images.Hider
2019-12-07 06:49:20 +01:00
alias Philomena.Images.Uploader
2019-12-16 23:11:16 +01:00
alias Philomena.Images.Tagging
2019-12-16 06:25:06 +01:00
alias Philomena.ImageFeatures.ImageFeature
2019-11-24 19:36:21 +01:00
alias Philomena.SourceChanges.SourceChange
alias Philomena.TagChanges.TagChange
alias Philomena.Tags
alias Philomena.Tags.Tag
alias Philomena.Notifications
2019-12-08 05:53:18 +01:00
alias Philomena.Interactions
alias Philomena.Reports.Report
2019-12-21 22:37:06 +01:00
alias Philomena.Comments
2019-08-18 18:17:05 +02:00
@doc """
Gets a single image.
Raises `Ecto.NoResultsError` if the Image does not exist.
## Examples
iex> get_image!(123)
%Image{}
iex> get_image!(456)
** (Ecto.NoResultsError)
"""
2019-08-18 20:14:36 +02:00
def get_image!(id) do
Repo.one!(Image |> where(id: ^id) |> preload(:tags))
end
2019-08-18 18:17:05 +02:00
@doc """
Creates a image.
## Examples
iex> create_image(%{field: value})
{:ok, %Image{}}
iex> create_image(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
2019-11-26 05:51:17 +01:00
def create_image(attribution, attrs \\ %{}) do
tags = Tags.get_or_create_tags(attrs["tag_input"])
image =
%Image{}
|> Image.creation_changeset(attrs, attribution)
|> Image.tag_changeset(attrs, [], tags)
2019-12-07 06:49:20 +01:00
|> Uploader.analyze_upload(attrs)
2019-11-26 05:51:17 +01:00
Multi.new
|> Multi.insert(:image, image)
2019-11-27 06:51:20 +01:00
|> Multi.run(:name_caches, fn repo, %{image: image} ->
image
|> Image.cache_changeset()
|> repo.update()
end)
2019-12-17 20:06:32 +01:00
|> Multi.run(:source_change, fn repo, %{image: image} ->
%SourceChange{image_id: image.id, initial: true}
|> SourceChange.creation_changeset(attrs, attribution)
|> repo.insert()
end)
2019-11-27 02:45:57 +01:00
|> Multi.run(:added_tag_count, fn repo, %{image: image} ->
tag_ids = image.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(:subscribe, fn _repo, %{image: image} ->
create_subscription(image, attribution[:user])
end)
2019-11-26 05:51:17 +01:00
|> Multi.run(:after, fn _repo, %{image: image} ->
2019-12-07 06:49:20 +01:00
Uploader.persist_upload(image)
2019-12-07 17:26:45 +01:00
Uploader.unpersist_old_upload(image)
2019-11-26 05:51:17 +01:00
{:ok, nil}
end)
2019-11-27 02:45:57 +01:00
|> Repo.isolated_transaction(:serializable)
2019-08-18 18:17:05 +02:00
end
2019-12-16 06:25:06 +01:00
def feature_image(featurer, %Image{} = image) do
image = Repo.preload(image, :tags)
[featured] = Tags.get_or_create_tags("featured image")
feature =
%ImageFeature{user_id: featurer.id, image_id: image.id}
|> ImageFeature.changeset(%{})
image =
image
|> Image.tag_changeset(%{}, image.tags, [featured | image.tags])
|> Image.cache_changeset()
Multi.new()
|> Multi.insert(:feature, feature)
|> Multi.update(:image, image)
|> Multi.run(:added_tag_count, fn repo, %{image: image} ->
tag_ids = image.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)
|> Repo.isolated_transaction(:serializable)
end
def lock_comments(%Image{} = image, locked) do
image
|> Image.lock_comments_changeset(locked)
|> Repo.update()
end
def lock_description(%Image{} = image, locked) do
image
|> Image.lock_description_changeset(locked)
|> Repo.update()
end
def lock_tags(%Image{} = image, locked) do
image
|> Image.lock_tags_changeset(locked)
|> Repo.update()
end
def remove_hash(%Image{} = image) do
image
|> Image.remove_hash_changeset()
|> Repo.update()
end
def update_scratchpad(%Image{} = image, attrs) do
image
|> Image.scratchpad_changeset(attrs)
|> Repo.update()
end
def remove_source_history(%Image{} = image) do
image
|> Repo.preload(:source_changes)
|> Image.remove_source_history_changeset()
|> Repo.update()
end
def repair_image(%Image{} = image) do
Philomena.Images.Thumbnailer.generate_thumbnails(image.id)
end
def update_file(%Image{} = image, attrs) do
image =
image
|> Image.changeset(attrs)
|> Uploader.analyze_upload(attrs)
Multi.new()
|> Multi.update(:image, image)
|> Multi.run(:after, fn _repo, %{image: image} ->
Uploader.persist_upload(image)
Uploader.unpersist_old_upload(image)
{:ok, nil}
end)
|> Repo.isolated_transaction(:serializable)
end
2019-08-18 18:17:05 +02:00
@doc """
Updates a image.
## Examples
iex> update_image(image, %{field: new_value})
{:ok, %Image{}}
iex> update_image(image, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_image(%Image{} = image, attrs) do
image
|> Image.changeset(attrs)
|> Repo.update()
end
2019-11-29 06:39:15 +01:00
def update_description(%Image{} = image, attrs) do
image
|> Image.description_changeset(attrs)
|> Repo.update()
end
2019-11-24 19:36:21 +01:00
def update_source(%Image{} = image, attribution, attrs) do
image_changes =
image
|> Image.source_changeset(attrs)
source_changes =
Ecto.build_assoc(image, :source_changes)
|> SourceChange.creation_changeset(attrs, attribution)
Multi.new
|> Multi.update(:image, image_changes)
|> Multi.run(:source_change, fn repo, _changes ->
case image_changes.changes do
%{source_url: _new_source} ->
repo.insert(source_changes)
_ ->
{:ok, nil}
end
end)
2019-11-24 19:36:21 +01:00
|> Repo.isolated_transaction(:serializable)
end
def update_tags(%Image{} = image, attribution, attrs) do
old_tags = Tags.get_or_create_tags(attrs["old_tag_input"])
new_tags = Tags.get_or_create_tags(attrs["tag_input"])
Multi.new
|> Multi.run(:image, fn repo, _chg ->
image
2019-12-16 23:11:16 +01:00
|> repo.preload(:tags)
2019-11-24 19:36:21 +01:00
|> Image.tag_changeset(%{}, old_tags, new_tags)
|> repo.update()
|> case do
{:ok, image} ->
{:ok, {image, image.added_tags, image.removed_tags}}
error ->
error
end
end)
|> Multi.run(:added_tag_changes, fn repo, %{image: {image, added_tags, _removed}} ->
tag_changes =
added_tags
|> Enum.map(&tag_change_attributes(attribution, image, &1, true, attribution[:user]))
{count, nil} = repo.insert_all(TagChange, tag_changes)
{:ok, count}
end)
|> Multi.run(:removed_tag_changes, fn repo, %{image: {image, _added, removed_tags}} ->
tag_changes =
removed_tags
|> Enum.map(&tag_change_attributes(attribution, image, &1, false, attribution[:user]))
{count, nil} = repo.insert_all(TagChange, tag_changes)
{:ok, count}
end)
|> Multi.run(:added_tag_count, fn
_repo, %{image: {%{hidden_from_users: true}, _added, _removed}} ->
{:ok, 0}
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: {%{hidden_from_users: true}, _added, _removed}} ->
{:ok, 0}
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)
2019-11-24 19:36:21 +01:00
|> Repo.isolated_transaction(:serializable)
end
defp tag_change_attributes(attribution, image, tag, added, user) do
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
2019-11-24 19:36:21 +01:00
user_id =
case user do
nil -> nil
user -> user.id
end
%{
image_id: image.id,
tag_id: tag.id,
user_id: user_id,
created_at: now,
updated_at: now,
2019-11-24 19:36:21 +01:00
tag_name_cache: tag.name,
ip: attribution[:ip],
fingerprint: attribution[:fingerprint],
user_agent: attribution[:user_agent],
referrer: attribution[:referrer],
added: added
}
end
2019-12-17 19:53:41 +01:00
def update_uploader(%Image{} = image, attrs) do
image
|> Image.uploader_changeset(attrs)
|> Repo.update()
end
2019-12-08 05:53:18 +01:00
def hide_image(%Image{} = image, user, attrs) do
Image.hide_changeset(image, attrs, user)
|> internal_hide_image(image)
2019-12-08 05:53:18 +01:00
end
def merge_image(%Image{} = image, duplicate_of_image) do
result =
Image.merge_changeset(image, duplicate_of_image)
|> internal_hide_image(image)
2019-12-08 05:53:18 +01:00
case result do
2019-12-17 18:13:05 +01:00
{:ok, changes} ->
tags = Tags.copy_tags(image, duplicate_of_image)
2019-12-21 22:37:06 +01:00
Comments.migrate_comments(image, duplicate_of_image)
2019-12-08 05:53:18 +01:00
Interactions.migrate_interactions(image, duplicate_of_image)
2019-12-17 18:13:05 +01:00
{:ok, %{changes | tags: changes.tags ++ tags}}
2019-12-08 05:53:18 +01:00
_error ->
result
end
end
defp internal_hide_image(changeset, image) do
reports =
Report
|> where(reportable_type: "Image", reportable_id: ^image.id)
|> select([r], r.id)
|> update(set: [open: false, state: "closed"])
Multi.new()
2019-12-08 05:53:18 +01:00
|> Multi.update(:image, changeset)
|> Multi.update_all(:reports, reports, [])
2019-12-08 05:53:18 +01:00
|> Multi.run(:tags, fn repo, %{image: image} ->
image = Repo.preload(image, :tags, force: true)
# I'm not convinced this is a good idea. It leads
# to way too much drift, and the index has to be
# maintained.
tag_ids = Enum.map(image.tags, & &1.id)
query = where(Tag, [t], t.id in ^tag_ids)
repo.update_all(query, inc: [images_count: -1])
{:ok, image.tags}
end)
|> Multi.run(:file, fn _repo, %{image: image} ->
Hider.hide_thumbnails(image, image.hidden_image_key)
{:ok, nil}
end)
|> Repo.isolated_transaction(:serializable)
end
2019-12-09 05:41:35 +01:00
def unhide_image(%Image{hidden_from_users: true} = image) do
2019-12-08 05:53:18 +01:00
key = image.hidden_image_key
Multi.new
|> Multi.update(:image, Image.unhide_changeset(image))
|> Multi.run(:tags, fn repo, %{image: image} ->
image = Repo.preload(image, :tags, force: true)
tag_ids = Enum.map(image.tags, & &1.id)
query = where(Tag, [t], t.id in ^tag_ids)
repo.update_all(query, inc: [images_count: 1])
{:ok, image.tags}
end)
|> Multi.run(:file, fn _repo, %{image: image} ->
Hider.unhide_thumbnails(image, key)
{:ok, nil}
end)
|> Repo.isolated_transaction(:serializable)
end
2019-12-09 05:41:35 +01:00
def unhide_image(image), do: {:ok, image}
2019-12-08 05:53:18 +01:00
2019-12-16 23:11:16 +01:00
def batch_update(image_ids, added_tags, removed_tags, tag_change_attributes) do
image_ids =
Image
|> where([i], i.id in ^image_ids and i.hidden_from_users == false)
|> select([i], i.id)
|> Repo.all()
2019-12-16 23:11:16 +01:00
added_tags = Enum.map(added_tags, & &1.id)
removed_tags = Enum.map(removed_tags, & &1.id)
# Change everything in one go, ignoring any validation errors
# Note: computing the Cartesian product
insertions =
for tag_id <- added_tags, image_id <- image_ids do
%{tag_id: tag_id, image_id: image_id}
end
deletions =
Tagging
|> where([t], t.image_id in ^image_ids and t.tag_id in ^removed_tags)
|> select([t], [t.image_id, t.tag_id])
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
tag_change_attributes = Map.merge(tag_change_attributes, %{created_at: now, updated_at: now})
Repo.transaction fn ->
{added_count, inserted} = Repo.insert_all(Tagging, insertions, on_conflict: :nothing, returning: [:image_id, :tag_id])
{removed_count, deleted} = Repo.delete_all(deletions)
inserted = Enum.map(inserted, &[&1.image_id, &1.tag_id])
added_changes = Enum.map(inserted, fn [image_id, tag_id] ->
Map.merge(tag_change_attributes, %{image_id: image_id, tag_id: tag_id, added: true})
end)
removed_changes = Enum.map(deleted, fn [image_id, tag_id] ->
Map.merge(tag_change_attributes, %{image_id: image_id, tag_id: tag_id, added: false})
end)
changes = added_changes ++ removed_changes
Repo.insert_all(TagChange, changes)
Repo.update_all(where(Tag, [t], t.id in ^added_tags), inc: [images_count: added_count])
Repo.update_all(where(Tag, [t], t.id in ^removed_tags), inc: [images_count: -removed_count])
end
end
2019-08-18 18:17:05 +02:00
@doc """
Deletes a Image.
## Examples
iex> delete_image(image)
{:ok, %Image{}}
iex> delete_image(image)
{:error, %Ecto.Changeset{}}
"""
def delete_image(%Image{} = image) do
Repo.delete(image)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking image changes.
## Examples
iex> change_image(image)
%Ecto.Changeset{source: %Image{}}
"""
def change_image(%Image{} = image) do
Image.changeset(image, %{})
end
2019-11-17 03:20:33 +01:00
def reindex_image(%Image{} = image) do
2019-12-05 05:12:49 +01:00
reindex_images([image.id])
image
end
def reindex_images(image_ids) do
2019-11-17 03:20:33 +01:00
spawn fn ->
Image
|> preload(^indexing_preloads())
2019-12-05 05:12:49 +01:00
|> where([i], i.id in ^image_ids)
|> Image.reindex()
2019-11-17 03:20:33 +01:00
end
2019-12-05 05:12:49 +01:00
image_ids
2019-11-17 03:20:33 +01:00
end
def indexing_preloads do
[:user, :favers, :downvoters, :upvoters, :hiders, :deleter, :gallery_interactions, tags: [:aliases, :aliased_tag]]
end
alias Philomena.Images.Subscription
2019-11-17 19:18:21 +01:00
def subscribed?(_image, nil), do: false
2019-11-17 18:50:42 +01:00
def subscribed?(image, user) do
Subscription
|> where(image_id: ^image.id, user_id: ^user.id)
|> Repo.exists?()
end
@doc """
Creates a subscription.
## Examples
iex> create_subscription(%{field: value})
{:ok, %Subscription{}}
iex> create_subscription(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
2019-11-18 05:47:09 +01:00
def create_subscription(_image, nil), do: {:ok, nil}
2019-11-17 18:50:42 +01:00
def create_subscription(image, user) do
%Subscription{image_id: image.id, user_id: user.id}
|> Subscription.changeset(%{})
|> Repo.insert(on_conflict: :nothing)
end
@doc """
Deletes a Subscription.
## Examples
iex> delete_subscription(subscription)
{:ok, %Subscription{}}
iex> delete_subscription(subscription)
{:error, %Ecto.Changeset{}}
"""
2019-11-17 18:50:42 +01:00
def delete_subscription(image, user) do
clear_notification(image, user)
2019-11-17 18:50:42 +01:00
%Subscription{image_id: image.id, user_id: user.id}
|> Repo.delete()
end
2019-12-01 06:03:45 +01:00
def clear_notification(_image, nil), do: nil
def clear_notification(image, user) do
Notifications.delete_unread_notification("Image", image.id, user)
end
2019-08-18 18:17:05 +02:00
end