Merge pull request #143 from philomena-dev/sources

Multiple sources
This commit is contained in:
Meow 2023-05-29 11:16:32 +02:00 committed by GitHub
commit a569008267
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 843 additions and 176 deletions

View file

@ -322,3 +322,16 @@ span.spoiler div.image-container {
.full-height { .full-height {
height: 100%; height: 100%;
} }
.image_sources {
display: grid;
grid-template-columns: 2em auto;
}
.image_source__icon, .image_source__link {
padding: 0.5em;
}
.image_source__icon {
justify-self: center;
}

View file

@ -0,0 +1,83 @@
import { $, $$, disableEl, enableEl, removeEl } from './utils/dom';
import { delegate, leftClick } from './utils/events';
/**
* @typedef InputDuplicatorOptions
* @property {string} addButtonSelector
* @property {string} fieldSelector
* @property {string} maxInputCountSelector
* @property {string} removeButtonSelector
*/
/**
* @param {InputDuplicatorOptions} options
*/
function inputDuplicatorCreator({
addButtonSelector,
fieldSelector,
maxInputCountSelector,
removeButtonSelector
}) {
const addButton = $(addButtonSelector);
if (!addButton) {
return;
}
const form = addButton.closest('form');
const fieldRemover = (event, target) => {
event.preventDefault();
// Prevent removing the final field element to not "brick" the form
const existingFields = $$(fieldSelector, form);
if (existingFields.length <= 1) {
return;
}
removeEl(target.closest(fieldSelector));
enableEl(addButton);
};
delegate(document, 'click', {
[removeButtonSelector]: leftClick(fieldRemover)
});
const maxOptionCount = parseInt($(maxInputCountSelector, form).innerHTML, 10);
addButton.addEventListener('click', e => {
e.preventDefault();
const existingFields = $$(fieldSelector, form);
let existingFieldsLength = existingFields.length;
if (existingFieldsLength < maxOptionCount) {
// The last element matched by the `fieldSelector` will be the last field, make a copy
const prevField = existingFields[existingFieldsLength - 1];
const prevFieldCopy = prevField.cloneNode(true);
const prevFieldCopyInputs = $$('input', prevFieldCopy);
prevFieldCopyInputs.forEach(prevFieldCopyInput => {
// Reset new input's value
prevFieldCopyInput.value = '';
prevFieldCopyInput.removeAttribute('value');
// Increment sequential attributes of the input
['name', 'id'].forEach(attr => {
prevFieldCopyInput.setAttribute(attr, prevFieldCopyInput[attr].replace(/\d+/g, `${existingFieldsLength}`));
});
});
// Insert copy before the last field's next sibling, or if none, at the end of its parent
if (prevField.nextElementSibling) {
prevField.parentNode.insertBefore(prevFieldCopy, prevField.nextElementSibling);
}
else {
prevField.parentNode.appendChild(prevFieldCopy);
}
existingFieldsLength++;
}
// Remove the button if we reached the max number of options
if (existingFieldsLength >= maxOptionCount) {
disableEl(addButton);
}
});
}
export { inputDuplicatorCreator };

View file

@ -1,41 +1,11 @@
import { $, $$, removeEl} from './utils/dom'; import { inputDuplicatorCreator } from './input-duplicator';
import { delegate, leftClick } from './utils/events';
function pollOptionRemover(_event, target) {
removeEl(target.closest('.js-poll-option'));
}
function pollOptionCreator() { function pollOptionCreator() {
const addPollOptionButton = $('.js-poll-add-option'); inputDuplicatorCreator({
addButtonSelector: '.js-poll-add-option',
delegate(document, 'click', { fieldSelector: '.js-poll-option',
'.js-option-remove': leftClick(pollOptionRemover) maxInputCountSelector: '.js-max-option-count',
}); removeButtonSelector: '.js-option-remove',
if (!addPollOptionButton) {
return;
}
const form = addPollOptionButton.closest('form');
const maxOptionCount = parseInt($('.js-max-option-count', form).innerHTML, 10);
addPollOptionButton.addEventListener('click', e => {
e.preventDefault();
let existingOptionCount = $$('.js-poll-option', form).length;
if (existingOptionCount < maxOptionCount) {
// The element right before the add button will always be the last field, make a copy
const prevFieldCopy = addPollOptionButton.previousElementSibling.cloneNode(true);
const newHtml = prevFieldCopy.outerHTML.replace(/(\d+)/g, `${existingOptionCount}`);
// Insert copy before the button
addPollOptionButton.insertAdjacentHTML('beforebegin', newHtml);
existingOptionCount++;
}
// Remove the button if we reached the max number of options
if (existingOptionCount >= maxOptionCount) {
removeEl(addPollOptionButton);
}
}); });
} }

12
assets/js/sources.js Normal file
View file

@ -0,0 +1,12 @@
import { inputDuplicatorCreator } from './input-duplicator';
function imageSourcesCreator() {
inputDuplicatorCreator({
addButtonSelector: '.js-image-add-source',
fieldSelector: '.js-image-source',
maxInputCountSelector: '.js-max-source-count',
removeButtonSelector: '.js-source-remove',
});
}
export { imageSourcesCreator };

View file

@ -3,7 +3,7 @@
*/ */
import { fetchJson, handleError } from './utils/requests'; import { fetchJson, handleError } from './utils/requests';
import { $, $$, hideEl, showEl, makeEl, clearEl } from './utils/dom'; import { $, $$, clearEl, hideEl, makeEl, showEl } from './utils/dom';
import { addTag } from './tagsinput'; import { addTag } from './tagsinput';
const MATROSKA_MAGIC = 0x1a45dfa3; const MATROSKA_MAGIC = 0x1a45dfa3;
@ -31,8 +31,10 @@ function setupImageUpload() {
if (!imgPreviews) return; if (!imgPreviews) return;
const form = imgPreviews.closest('form'); const form = imgPreviews.closest('form');
const [ fileField, remoteUrl, scraperError ] = $$('.js-scraper', form); const [fileField, remoteUrl, scraperError] = $$('.js-scraper', form);
const [ sourceEl, tagsEl, descrEl ] = $$('.js-image-input', form); const descrEl = $('.js-image-descr-input', form);
const tagsEl = $('.js-image-tags-input', form);
const sourceEl = $$('.js-image-source', form).find(input => input.value === '');
const fetchButton = $('#js-scraper-preview'); const fetchButton = $('#js-scraper-preview');
if (!fetchButton) return; if (!fetchButton) return;

View file

@ -12,6 +12,8 @@ import {
toggleEl, toggleEl,
whenReady, whenReady,
findFirstTextNode, findFirstTextNode,
disableEl,
enableEl,
} from '../dom'; } from '../dom';
import { getRandomArrayItem, getRandomIntBetween } from '../../../test/randomness'; import { getRandomArrayItem, getRandomIntBetween } from '../../../test/randomness';
import { fireEvent } from '@testing-library/dom'; import { fireEvent } from '@testing-library/dom';
@ -149,6 +151,74 @@ describe('DOM Utilities', () => {
}); });
}); });
describe('disableEl', () => {
it('should set the disabled attribute to true', () => {
const mockElement = document.createElement('button');
disableEl(mockElement);
expect(mockElement).toBeDisabled();
});
it('should set the disabled attribute to true on all provided elements', () => {
const mockElements = [
document.createElement('input'),
document.createElement('button'),
];
disableEl(mockElements);
expect(mockElements[0]).toBeDisabled();
expect(mockElements[1]).toBeDisabled();
});
it('should set the disabled attribute to true on elements provided in multiple arrays', () => {
const mockElements1 = [
document.createElement('input'),
document.createElement('button'),
];
const mockElements2 = [
document.createElement('textarea'),
document.createElement('button'),
];
disableEl(mockElements1, mockElements2);
expect(mockElements1[0]).toBeDisabled();
expect(mockElements1[1]).toBeDisabled();
expect(mockElements2[0]).toBeDisabled();
expect(mockElements2[1]).toBeDisabled();
});
});
describe('enableEl', () => {
it('should set the disabled attribute to false', () => {
const mockElement = document.createElement('button');
enableEl(mockElement);
expect(mockElement).toBeEnabled();
});
it('should set the disabled attribute to false on all provided elements', () => {
const mockElements = [
document.createElement('input'),
document.createElement('button'),
];
enableEl(mockElements);
expect(mockElements[0]).toBeEnabled();
expect(mockElements[1]).toBeEnabled();
});
it('should set the disabled attribute to false on elements provided in multiple arrays', () => {
const mockElements1 = [
document.createElement('input'),
document.createElement('button'),
];
const mockElements2 = [
document.createElement('textarea'),
document.createElement('button'),
];
enableEl(mockElements1, mockElements2);
expect(mockElements1[0]).toBeEnabled();
expect(mockElements1[1]).toBeEnabled();
expect(mockElements2[0]).toBeEnabled();
expect(mockElements2[1]).toBeEnabled();
});
});
describe('toggleEl', () => { describe('toggleEl', () => {
it(`should toggle the ${hiddenClass} class on the provided element`, () => { it(`should toggle the ${hiddenClass} class on the provided element`, () => {
const mockVisibleElement = document.createElement('div'); const mockVisibleElement = document.createElement('div');

View file

@ -1,5 +1,7 @@
// DOM Utils // DOM Utils
type PhilomenaInputElements = HTMLTextAreaElement | HTMLInputElement | HTMLButtonElement;
/** /**
* Get the first matching element * Get the first matching element
*/ */
@ -34,6 +36,18 @@ export function clearEl<E extends HTMLElement>(...elements: E[] | ConcatArray<E>
}); });
} }
export function disableEl<E extends PhilomenaInputElements>(...elements: E[] | ConcatArray<E>[]) {
([] as E[]).concat(...elements).forEach(el => {
el.disabled = true;
});
}
export function enableEl<E extends PhilomenaInputElements>(...elements: E[] | ConcatArray<E>[]) {
([] as E[]).concat(...elements).forEach(el => {
el.disabled = false;
});
}
export function removeEl<E extends HTMLElement>(...elements: E[] | ConcatArray<E>[]) { export function removeEl<E extends HTMLElement>(...elements: E[] | ConcatArray<E>[]) {
([] as E[]).concat(...elements).forEach(el => el.parentNode?.removeChild(el)); ([] as E[]).concat(...elements).forEach(el => el.parentNode?.removeChild(el));
} }

View file

@ -35,6 +35,7 @@ import { setupToolbar } from './markdowntoolbar';
import { hideStaffTools } from './staffhider'; import { hideStaffTools } from './staffhider';
import { pollOptionCreator } from './poll'; import { pollOptionCreator } from './poll';
import { warnAboutPMs } from './pmwarning'; import { warnAboutPMs } from './pmwarning';
import { imageSourcesCreator } from './sources';
whenReady(() => { whenReady(() => {
@ -68,5 +69,6 @@ whenReady(() => {
hideStaffTools(); hideStaffTools();
pollOptionCreator(); pollOptionCreator();
warnAboutPMs(); warnAboutPMs();
imageSourcesCreator();
}); });

View file

@ -28,6 +28,7 @@ config :philomena,
badge_url_root: System.fetch_env!("BADGE_URL_ROOT"), badge_url_root: System.fetch_env!("BADGE_URL_ROOT"),
mailer_address: System.fetch_env!("MAILER_ADDRESS"), mailer_address: System.fetch_env!("MAILER_ADDRESS"),
tag_file_root: System.fetch_env!("TAG_FILE_ROOT"), tag_file_root: System.fetch_env!("TAG_FILE_ROOT"),
site_domains: System.fetch_env!("SITE_DOMAINS"),
tag_url_root: System.fetch_env!("TAG_URL_ROOT"), tag_url_root: System.fetch_env!("TAG_URL_ROOT"),
redis_host: System.get_env("REDIS_HOST", "localhost"), redis_host: System.get_env("REDIS_HOST", "localhost"),
proxy_host: System.get_env("PROXY_HOST"), proxy_host: System.get_env("PROXY_HOST"),

View file

@ -77,10 +77,12 @@ defmodule Philomena.Images do
""" """
def create_image(attribution, attrs \\ %{}) do def create_image(attribution, attrs \\ %{}) do
tags = Tags.get_or_create_tags(attrs["tag_input"]) tags = Tags.get_or_create_tags(attrs["tag_input"])
sources = attrs["sources"]
image = image =
%Image{} %Image{}
|> Image.creation_changeset(attrs, attribution) |> Image.creation_changeset(attrs, attribution)
|> Image.source_changeset(attrs, [], sources)
|> Image.tag_changeset(attrs, [], tags) |> Image.tag_changeset(attrs, [], tags)
|> Image.dnp_changeset(attribution[:user]) |> Image.dnp_changeset(attribution[:user])
|> Uploader.analyze_upload(attrs) |> Uploader.analyze_upload(attrs)
@ -92,11 +94,6 @@ defmodule Philomena.Images do
|> Image.cache_changeset() |> Image.cache_changeset()
|> repo.update() |> repo.update()
end) end)
|> Multi.run(:source_change, fn repo, %{image: image} ->
%SourceChange{image_id: image.id, initial: true}
|> SourceChange.creation_changeset(attrs, attribution)
|> repo.insert()
end)
|> Multi.run(:added_tag_count, fn repo, %{image: image} -> |> Multi.run(:added_tag_count, fn repo, %{image: image} ->
tag_ids = image.added_tags |> Enum.map(& &1.id) tag_ids = image.added_tags |> Enum.map(& &1.id)
tags = Tag |> where([t], t.id in ^tag_ids) tags = Tag |> where([t], t.id in ^tag_ids)
@ -333,29 +330,69 @@ defmodule Philomena.Images do
|> Repo.update() |> Repo.update()
end end
def update_source(%Image{} = image, attribution, attrs) do def update_sources(%Image{} = image, attribution, attrs) do
image_changes = old_sources = attrs["old_sources"]
image new_sources = attrs["sources"]
|> Image.source_changeset(attrs)
source_changes =
Ecto.build_assoc(image, :source_changes)
|> SourceChange.creation_changeset(attrs, attribution)
Multi.new() Multi.new()
|> Multi.update(:image, image_changes) |> Multi.run(:image, fn repo, _chg ->
|> Multi.run(:source_change, fn repo, _changes -> image = repo.preload(image, [:sources])
case image_changes.changes do
%{source_url: _new_source} ->
repo.insert(source_changes)
_ -> image
{:ok, nil} |> Image.source_changeset(%{}, old_sources, new_sources)
|> repo.update()
|> case do
{:ok, image} ->
{:ok, {image, image.added_sources, image.removed_sources}}
error ->
error
end end
end) end)
|> Multi.run(:added_source_changes, fn repo, %{image: {image, added_sources, _removed}} ->
source_changes =
added_sources
|> Enum.map(&source_change_attributes(attribution, image, &1, true, attribution[:user]))
{count, nil} = repo.insert_all(SourceChange, source_changes)
{:ok, count}
end)
|> Multi.run(:removed_source_changes, fn repo, %{image: {image, _added, removed_sources}} ->
source_changes =
removed_sources
|> Enum.map(&source_change_attributes(attribution, image, &1, false, attribution[:user]))
{count, nil} = repo.insert_all(SourceChange, source_changes)
{:ok, count}
end)
|> Repo.transaction() |> Repo.transaction()
end end
defp source_change_attributes(attribution, image, source, added, user) do
now = DateTime.utc_now() |> DateTime.truncate(:second)
user_id =
case user do
nil -> nil
user -> user.id
end
%{
image_id: image.id,
source_url: source,
user_id: user_id,
created_at: now,
updated_at: now,
ip: attribution[:ip],
fingerprint: attribution[:fingerprint],
user_agent: attribution[:user_agent],
referrer: attribution[:referrer],
added: added
}
end
def update_locked_tags(%Image{} = image, attrs) do def update_locked_tags(%Image{} = image, attrs) do
new_tags = Tags.get_or_create_tags(attrs["tag_input"]) new_tags = Tags.get_or_create_tags(attrs["tag_input"])
@ -511,6 +548,13 @@ defmodule Philomena.Images do
|> Multi.run(:copy_tags, fn _, %{} -> |> Multi.run(:copy_tags, fn _, %{} ->
{:ok, Tags.copy_tags(image, duplicate_of_image)} {:ok, Tags.copy_tags(image, duplicate_of_image)}
end) end)
|> Multi.run(:migrate_sources, fn repo, %{} ->
{:ok,
migrate_sources(
repo.preload(image, [:sources]),
repo.preload(duplicate_of_image, [:sources])
)}
end)
|> Multi.run(:migrate_comments, fn _, %{} -> |> Multi.run(:migrate_comments, fn _, %{} ->
{:ok, Comments.migrate_comments(image, duplicate_of_image)} {:ok, Comments.migrate_comments(image, duplicate_of_image)}
end) end)
@ -787,6 +831,7 @@ defmodule Philomena.Images do
:hiders, :hiders,
:deleter, :deleter,
:gallery_interactions, :gallery_interactions,
:sources,
tags: [:aliases, :aliased_tag] tags: [:aliases, :aliased_tag]
] ]
end end
@ -882,6 +927,17 @@ defmodule Philomena.Images do
{:ok, count} {:ok, count}
end end
def migrate_sources(source, target) do
sources =
(source.sources ++ target.sources)
|> Enum.uniq()
|> Enum.take(10)
target
|> Image.sources_changeset(sources)
|> Repo.update()
end
def notify_merge(source, target) do def notify_merge(source, target) do
Exq.enqueue(Exq, "notifications", NotificationWorker, ["Images", [source.id, target.id]]) Exq.enqueue(Exq, "notifications", NotificationWorker, ["Images", [source.id, target.id]])
end end

View file

@ -119,7 +119,7 @@ defmodule Philomena.Images.ElasticsearchIndex do
mime_type: image.image_mime_type, mime_type: image.image_mime_type,
uploader: if(!!image.user and !image.anonymous, do: String.downcase(image.user.name)), uploader: if(!!image.user and !image.anonymous, do: String.downcase(image.user.name)),
true_uploader: if(!!image.user, do: String.downcase(image.user.name)), true_uploader: if(!!image.user, do: String.downcase(image.user.name)),
source_url: image.source_url |> to_string |> String.downcase(), source_url: image.sources |> Enum.map(&String.downcase(&1.source)),
file_name: image.image_name, file_name: image.image_name,
original_format: image.image_format, original_format: image.image_format,
processed: image.processed, processed: image.processed,

View file

@ -8,6 +8,7 @@ defmodule Philomena.Images.Image do
alias Philomena.ImageVotes.ImageVote alias Philomena.ImageVotes.ImageVote
alias Philomena.ImageFaves.ImageFave alias Philomena.ImageFaves.ImageFave
alias Philomena.ImageHides.ImageHide alias Philomena.ImageHides.ImageHide
alias Philomena.Images.Source
alias Philomena.Images.Subscription alias Philomena.Images.Subscription
alias Philomena.Users.User alias Philomena.Users.User
alias Philomena.Tags.Tag alias Philomena.Tags.Tag
@ -18,6 +19,7 @@ defmodule Philomena.Images.Image do
alias Philomena.Images.Image alias Philomena.Images.Image
alias Philomena.Images.TagDiffer alias Philomena.Images.TagDiffer
alias Philomena.Images.SourceDiffer
alias Philomena.Images.TagValidator alias Philomena.Images.TagValidator
alias Philomena.Images.DnpValidator alias Philomena.Images.DnpValidator
alias Philomena.Repo alias Philomena.Repo
@ -42,6 +44,7 @@ defmodule Philomena.Images.Image do
many_to_many :locked_tags, Tag, join_through: "image_tag_locks", on_replace: :delete many_to_many :locked_tags, Tag, join_through: "image_tag_locks", on_replace: :delete
has_one :intensity, ImageIntensity has_one :intensity, ImageIntensity
has_many :galleries, through: [:gallery_interactions, :image] has_many :galleries, through: [:gallery_interactions, :image]
has_many :sources, Source, on_replace: :delete
field :image, :string field :image, :string
field :image_name, :string field :image_name, :string
@ -91,6 +94,8 @@ defmodule Philomena.Images.Image do
field :removed_tags, {:array, :any}, default: [], virtual: true field :removed_tags, {:array, :any}, default: [], virtual: true
field :added_tags, {:array, :any}, default: [], virtual: true field :added_tags, {:array, :any}, default: [], virtual: true
field :removed_sources, {:array, :any}, default: [], virtual: true
field :added_sources, {:array, :any}, default: [], virtual: true
field :uploaded_image, :string, virtual: true field :uploaded_image, :string, virtual: true
field :removed_image, :string, virtual: true field :removed_image, :string, virtual: true
@ -203,11 +208,15 @@ defmodule Philomena.Images.Image do
|> change(image: nil) |> change(image: nil)
end end
def source_changeset(image, attrs) do def source_changeset(image, attrs, old_sources, new_sources) do
image image
|> cast(attrs, [:source_url]) |> cast(attrs, [])
|> validate_required(:source_url) |> SourceDiffer.diff_input(old_sources, new_sources)
|> validate_format(:source_url, ~r/\Ahttps?:\/\//) end
def sources_changeset(image, new_sources) do
change(image)
|> put_assoc(:sources, new_sources)
end end
def tag_changeset(image, attrs, old_tags, new_tags, excluded_tags \\ []) do def tag_changeset(image, attrs, old_tags, new_tags, excluded_tags \\ []) do

View file

@ -4,9 +4,10 @@ defmodule Philomena.Images.Source do
alias Philomena.Images.Image alias Philomena.Images.Image
@primary_key false
schema "image_sources" do schema "image_sources" do
belongs_to :image, Image belongs_to :image, Image, primary_key: true
field :source, :string field :source, :string, primary_key: true
end end
@doc false @doc false

View file

@ -0,0 +1,70 @@
defmodule Philomena.Images.SourceDiffer do
import Ecto.Changeset
alias Philomena.Images.Source
def diff_input(changeset, old_sources, new_sources) do
old_set = MapSet.new(flatten_input(old_sources))
new_set = MapSet.new(flatten_input(new_sources))
source_set = MapSet.new(get_field(changeset, :sources), & &1.source)
added_sources = MapSet.difference(new_set, old_set)
removed_sources = MapSet.difference(old_set, new_set)
{sources, actually_added, actually_removed} =
apply_changes(source_set, added_sources, removed_sources)
image_id = fetch_field!(changeset, :id)
changeset
|> put_change(:added_sources, actually_added)
|> put_change(:removed_sources, actually_removed)
|> put_assoc(:sources, source_structs(image_id, sources))
end
defp apply_changes(source_set, added_set, removed_set) do
desired_sources =
source_set
|> MapSet.difference(removed_set)
|> MapSet.union(added_set)
actually_added =
desired_sources
|> MapSet.difference(source_set)
|> Enum.to_list()
actually_removed =
source_set
|> MapSet.difference(desired_sources)
|> Enum.to_list()
sources = Enum.to_list(desired_sources)
actually_added = Enum.to_list(actually_added)
actually_removed = Enum.to_list(actually_removed)
{sources, actually_added, actually_removed}
end
defp source_structs(image_id, sources) do
Enum.map(sources, &%Source{image_id: image_id, source: &1})
end
defp flatten_input(input) when is_map(input) do
Enum.flat_map(Map.values(input), fn
%{"source" => source} ->
source = String.trim(source)
if source != "" do
[source]
else
[]
end
_ ->
[]
end)
end
defp flatten_input(_input) do
[]
end
end

View file

@ -10,10 +10,10 @@ defmodule Philomena.SourceChanges.SourceChange do
field :fingerprint, :string field :fingerprint, :string
field :user_agent, :string, default: "" field :user_agent, :string, default: ""
field :referrer, :string, default: "" field :referrer, :string, default: ""
field :new_value, :string field :value, :string
field :initial, :boolean, default: false field :added, :boolean
field :source_url, :string, source: :new_value field :source_url, :string, source: :value
timestamps(inserted_at: :created_at, type: :utc_datetime) timestamps(inserted_at: :created_at, type: :utc_datetime)
end end

View file

@ -29,8 +29,8 @@ defmodule PhilomenaWeb.DuplicateReportController do
|> preload([ |> preload([
:user, :user,
:modifier, :modifier,
image: [:user, tags: :aliases], image: [:user, :sources, tags: :aliases],
duplicate_of_image: [:user, tags: :aliases] duplicate_of_image: [:user, :sources, tags: :aliases]
]) ])
|> order_by(desc: :created_at) |> order_by(desc: :created_at)
|> Repo.paginate(conn.assigns.scrivener) |> Repo.paginate(conn.assigns.scrivener)

View file

@ -21,19 +21,18 @@ defmodule PhilomenaWeb.Image.SourceController do
plug :load_and_authorize_resource, plug :load_and_authorize_resource,
model: Image, model: Image,
id_name: "image_id", id_name: "image_id",
preload: [:user, tags: :aliases] preload: [:user, :sources, tags: :aliases]
def update(conn, %{"image" => image_params}) do def update(conn, %{"image" => image_params}) do
attributes = conn.assigns.attributes attributes = conn.assigns.attributes
image = conn.assigns.image image = conn.assigns.image
old_source = image.source_url
case Images.update_source(image, attributes, image_params) do case Images.update_sources(image, attributes, image_params) do
{:ok, %{image: image}} -> {:ok, %{image: {image, added_sources, removed_sources}}} ->
PhilomenaWeb.Endpoint.broadcast!( PhilomenaWeb.Endpoint.broadcast!(
"firehose", "firehose",
"image:source_update", "image:source_update",
%{image_id: image.id, added: [image.source_url], removed: [old_source]} %{image_id: image.id, added: [added_sources], removed: [removed_sources]}
) )
PhilomenaWeb.Endpoint.broadcast!( PhilomenaWeb.Endpoint.broadcast!(
@ -49,7 +48,7 @@ defmodule PhilomenaWeb.Image.SourceController do
|> where(image_id: ^image.id) |> where(image_id: ^image.id)
|> Repo.aggregate(:count, :id) |> Repo.aggregate(:count, :id)
if old_source != image.source_url do if Enum.any?(added_sources) or Enum.any?(removed_sources) do
UserStatistics.inc_stat(conn.assigns.current_user, :metadata_updates) UserStatistics.inc_stat(conn.assigns.current_user, :metadata_updates)
end end

View file

@ -9,6 +9,7 @@ defmodule PhilomenaWeb.ImageController do
alias Philomena.{ alias Philomena.{
Images, Images,
Images.Image, Images.Image,
Images.Source,
Comments.Comment, Comments.Comment,
Galleries.Gallery Galleries.Gallery
} }
@ -79,7 +80,7 @@ defmodule PhilomenaWeb.ImageController do
|> Comments.change_comment() |> Comments.change_comment()
image_changeset = image_changeset =
image %{image | sources: sources_for_edit(image.sources)}
|> Images.change_image() |> Images.change_image()
watching = Images.subscribed?(image, conn.assigns.current_user) watching = Images.subscribed?(image, conn.assigns.current_user)
@ -108,7 +109,7 @@ defmodule PhilomenaWeb.ImageController do
def new(conn, _params) do def new(conn, _params) do
changeset = changeset =
%Image{} %Image{sources: sources_for_edit()}
|> Images.change_image() |> Images.change_image()
render(conn, "new.html", title: "New Image", changeset: changeset) render(conn, "new.html", title: "New Image", changeset: changeset)
@ -185,7 +186,7 @@ defmodule PhilomenaWeb.ImageController do
_ in fragment("SELECT COUNT(*) FROM source_changes s WHERE s.image_id = ?", i.id), _ in fragment("SELECT COUNT(*) FROM source_changes s WHERE s.image_id = ?", i.id),
on: true on: true
) )
|> preload([:deleter, :locked_tags, user: [awards: :badge], tags: :aliases]) |> preload([:deleter, :locked_tags, :sources, user: [awards: :badge], tags: :aliases])
|> select([i, t, s], {i, t.count, s.count}) |> select([i, t, s], {i, t.count, s.count})
|> Repo.one() |> Repo.one()
|> case do |> case do
@ -217,4 +218,8 @@ defmodule PhilomenaWeb.ImageController do
|> assign(:source_change_count, source_changes) |> assign(:source_change_count, source_changes)
end end
end end
defp sources_for_edit(), do: [%Source{}]
defp sources_for_edit([]), do: sources_for_edit()
defp sources_for_edit(sources), do: sources
end end

View file

@ -1,4 +1,5 @@
.block .block
- has_sources = Enum.any?(@image.sources)
= form_for @changeset, Routes.image_source_path(@conn, :update, @image), [method: "put", class: "hidden", id: "source-form", data: [remote: "true"]], fn f -> = 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, :edit_metadata, @image) and !@conn.assigns.current_ban do = if can?(@conn, :edit_metadata, @image) and !@conn.assigns.current_ban do
@ -6,9 +7,28 @@
.alert.alert-danger .alert.alert-danger
p Oops, something went wrong! Please check the errors below. p Oops, something went wrong! Please check the errors below.
.flex p
= url_input f, :source_url, id: "source-field", class: "input input--wide", autocomplete: "off", placeholder: "Source URL" 'The page(s) you found this image on. Images may have a maximum of
= submit "Save source", class: "button button--separate-left" span.js-max-source-count> 10
' source URLs. Leave any sources you don't want to use blank.
= inputs_for f, :sources, [as: "image[old_sources]", skip_hidden: true], fn fs ->
= hidden_input fs, :source
= inputs_for f, :sources, [skip_hidden: true], fn fs ->
.field.js-image-source.field--inline.flex--no-wrap.flex--centered
= text_input fs, :source, class: "input flex__grow js-source-url", placeholder: "Source URL"
= error_tag fs, :source
label.input--separate-left.flex__fixed.flex--centered
a.js-source-remove href="#"
i.fa.fa-trash>
' Delete
.field
button.button.js-image-add-source type="button"
i.fa.fa-plus>
' Add source
= submit "Save sources", class: "button button--separate-left"
button.button.button--separate-left type="reset" data-click-hide="#source-form" data-click-show="#image-source" button.button.button--separate-left type="reset" data-click-hide="#source-form" data-click-show="#image-source"
' Cancel ' Cancel
@ -18,22 +38,15 @@
p p
' You can't edit the source on this image. ' You can't edit the source on this image.
.flex.flex--wrap#image-source .flex.flex--wrap.flex--column#image-source
p .flex
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" a.button.button--separate-right#edit-source data-click-focus=".js-image-source" data-click-hide="#image-source" data-click-show="#source-form" title="Edit source" accessKey="s"
i.fas.fa-edit> i.fas.fa-edit>
' Source: = if !has_sources || length(@image.sources) == 1 do
' Source:
p - else
= if @image.source_url not in [nil, ""] do ' Sources:
a.js-source-link href=@image.source_url = if @source_change_count > 0 do
strong
= @image.source_url
- else
em> not provided yet
= if @source_change_count > 1 do
a.button.button--link.button--separate-left href=Routes.image_source_change_path(@conn, :index, @image) title="Source history" a.button.button--link.button--separate-left href=Routes.image_source_change_path(@conn, :index, @image) title="Source history"
i.fa.fa-history> i.fa.fa-history>
| History ( | History (
@ -45,3 +58,22 @@
button.button.button--state-danger.button--separate-left type="submit" data-confirm="Are you really, really sure?" title="Wipe sources" button.button.button--state-danger.button--separate-left type="submit" data-confirm="Are you really, really sure?" title="Wipe sources"
i.fas.fa-eraser> i.fas.fa-eraser>
' Wipe ' Wipe
.image_sources
= if has_sources do
- [first_source | sources] = @image.sources
.image_source__icon
i class=image_source_icon(first_source.source)
.image_source__link
a.js-source-link href=first_source.source
strong = first_source.source
= for source <- sources do
.image_source__icon
i class=image_source_icon(source.source)
.image_source__link
a href=source.source
strong = source.source
- else
.image_source__icon
i.fa.fa-unlink
.image_source__link
em> not provided yet

View file

@ -38,9 +38,25 @@
.field-error-js.hidden.js-scraper .field-error-js.hidden.js-scraper
h4 About this image h4 About this image
p
'The page(s) you found this image on. Images may have a maximum of
span.js-max-source-count> 10
' source URLs. Leave any sources you don't want to use blank.
= inputs_for f, :sources, fn fs ->
.field.js-image-source.field--inline.flex--no-wrap.flex--centered
= text_input fs, :source, class: "input flex__grow js-source-url", placeholder: "Source URL"
= error_tag fs, :source
label.input--separate-left.flex__fixed.flex--centered
a.js-source-remove href="#"
i.fa.fa-trash>
' Delete
.field .field
= label f, :source_url, "The page you found this image on" button.button.js-image-add-source type="button"
= url_input f, :source_url, class: "input input--wide js-image-input", placeholder: "Source URL" i.fa.fa-plus>
' Add source
.field .field
label for="image[tag_input]" label for="image[tag_input]"
@ -65,7 +81,7 @@
.field .field
.block .block
.communication-edit__wrap .communication-edit__wrap
= render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: f, action_icon: "pencil-alt", action_text: "Description", placeholder: "Describe this image in plain words - this should generally be info about the image that doesn't belong in the tags or source.", name: :description, class: "js-image-input", required: false = render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: f, action_icon: "pencil-alt", action_text: "Description", placeholder: "Describe this image in plain words - this should generally be info about the image that doesn't belong in the tags or source.", name: :description, class: "js-image-descr-input", required: false
= render PhilomenaWeb.MarkdownView, "_anon_checkbox.html", conn: @conn, f: f, label: "Post anonymously" = render PhilomenaWeb.MarkdownView, "_anon_checkbox.html", conn: @conn, f: f, label: "Post anonymously"

View file

@ -7,10 +7,10 @@
thead thead
tr tr
th colspan=2 Image th colspan=2 Image
th New Source th Source
th Action
th Timestamp th Timestamp
th User th User
th Initial?
tbody tbody
= for source_change <- @source_changes do = for source_change <- @source_changes do
@ -21,7 +21,12 @@
= render PhilomenaWeb.ImageView, "_image_container.html", image: source_change.image, size: :thumb_tiny, conn: @conn = render PhilomenaWeb.ImageView, "_image_container.html", image: source_change.image, size: :thumb_tiny, conn: @conn
td td
= source_change.new_value = source_change.source_url
= if source_change.added do
td.success Added
- else
td.danger Removed
td td
= pretty_time(source_change.created_at) = pretty_time(source_change.created_at)
@ -41,9 +46,5 @@
br br
' Ask them before reverting their changes. ' Ask them before reverting their changes.
td
= if source_change.initial do
' &#x2713;
.block__header .block__header
= @pagination = @pagination

View file

@ -1,6 +1,6 @@
elixir: elixir:
textarea_options = [ textarea_options = [
class: "input input--wide tagsinput js-image-input js-taginput js-taginput-plain js-taginput-#{@name}", class: "input input--wide tagsinput js-image-tags-input js-taginput js-taginput-plain js-taginput-#{@name}",
placeholder: "Add tags separated with commas", placeholder: "Add tags separated with commas",
autocomplete: "off" autocomplete: "off"
] ]

View file

@ -58,19 +58,24 @@ defmodule PhilomenaWeb.DuplicateReportView do
do: abs(duplicate_of_image.image_aspect_ratio - image.image_aspect_ratio) <= 0.009 do: abs(duplicate_of_image.image_aspect_ratio - image.image_aspect_ratio) <= 0.009
def neither_have_source?(%{image: image, duplicate_of_image: duplicate_of_image}), def neither_have_source?(%{image: image, duplicate_of_image: duplicate_of_image}),
do: blank?(duplicate_of_image.source_url) and blank?(image.source_url) do: Enum.empty?(duplicate_of_image.sources) and Enum.empty?(image.sources)
def same_source?(%{image: image, duplicate_of_image: duplicate_of_image}), def same_source?(%{image: image, duplicate_of_image: duplicate_of_image}) do
do: to_string(duplicate_of_image.source_url) == to_string(image.source_url) MapSet.equal?(MapSet.new(image.sources), MapSet.new(duplicate_of_image.sources))
end
def similar_source?(%{image: image, duplicate_of_image: duplicate_of_image}), def similar_source?(%{image: image, duplicate_of_image: duplicate_of_image}) do
do: uri_host(image.source_url) == uri_host(duplicate_of_image.source_url) MapSet.equal?(
MapSet.new(image.sources, &URI.parse(&1.source).host),
MapSet.new(duplicate_of_image.sources, &URI.parse(&1.source).host)
)
end
def source_on_target?(%{image: image, duplicate_of_image: duplicate_of_image}), def source_on_target?(%{image: image, duplicate_of_image: duplicate_of_image}),
do: present?(duplicate_of_image.source_url) and blank?(image.source_url) do: Enum.any?(duplicate_of_image.sources) and Enum.empty?(image.sources)
def source_on_source?(%{image: image, duplicate_of_image: duplicate_of_image}), def source_on_source?(%{image: image, duplicate_of_image: duplicate_of_image}),
do: blank?(duplicate_of_image.source_url) && present?(image.source_url) do: Enum.empty?(duplicate_of_image.sources) && Enum.any?(image.sources)
def same_artist_tags?(%{image: image, duplicate_of_image: duplicate_of_image}), def same_artist_tags?(%{image: image, duplicate_of_image: duplicate_of_image}),
do: MapSet.equal?(artist_tags(image), artist_tags(duplicate_of_image)) do: MapSet.equal?(artist_tags(image), artist_tags(duplicate_of_image))

View file

@ -287,4 +287,96 @@ defmodule PhilomenaWeb.ImageView do
Philomena.Search.Evaluator.hits?(doc, query) Philomena.Search.Evaluator.hits?(doc, query)
end end
def image_source_icon(nil), do: "fa fa-link"
def image_source_icon(""), do: "fa fa-link"
def image_source_icon(source) do
site_domains =
String.split(Application.get_env(:philomena, :site_domains), ",") ++
[Application.get_env(:philomena, :cdn_host)]
uri = URI.parse(source)
case uri.host do
u when u in ["twitter.com", "www.twitter.com", "pbs.twimg.com", "twimg.com"] ->
"fab fa-twitter"
u when u in ["deviantart.com", "www.deviantart.com", "sta.sh", "www.sta.sh"] ->
"fab fa-deviantart"
u when u in ["cdn.discordapp.com", "discordapp.com", "discord.com"] ->
"fab fa-discord"
u when u in ["youtube.com", "www.youtube.com"] ->
"fab fa-youtube"
u when u in ["pillowfort.social", "www.pillowfort.social"] ->
"fa fa-bed"
u when u in ["vk.com", "vk.ru"] ->
"fab fa-vk"
u when u in ["pixiv.net", "www.pixiv.net", "artfight.net", "www.artfight.net"] ->
"fa fa-paintbrush"
u when u in ["patreon.com", "www.patreon.com"] ->
"fab fa-patreon"
u when u in ["ych.art", "ych.commishes.com", "commishes.com"] ->
"fa fa-palette"
u when u in ["artstation.com", "www.artstation.com"] ->
"fab fa-artstation"
u when u in ["instagram.com", "www.instagram.com"] ->
"fab fa-instagram"
u when u in ["reddit.com", "www.reddit.com"] ->
"fab fa-reddit"
u when u in ["facebook.com", "www.facebook.com", "fb.me", "www.fb.me"] ->
"fab fa-facebook"
u when u in ["tiktok.com", "www.tiktok.com"] ->
"fab fa-tiktok"
u
when u in [
"furaffinity.net",
"www.furaffinity.net",
"furbooru.org",
"inkbunny.net",
"e621.net",
"e926.net"
] ->
"fa fa-paw"
u
when u in [
"awoo.space",
"bark.lgbt",
"equestria.social",
"foxy.social",
"mastodon.art",
"mastodon.social",
"meow.social",
"pawoo.net",
"pettingzoo.co",
"pony.social",
"vulpine.club",
"yiff.life"
] ->
"fab fa-mastodon"
link ->
cond do
Enum.member?(site_domains, link) -> "favicon-home"
String.contains?(link, "tumblr") -> "fab fa-tumblr"
String.contains?(link, "deviantart") -> "fab fa-deviantart"
String.contains?(link, "sofurry") -> "fa fa-paw"
true -> "fa fa-link"
end
end
end
end end

View file

@ -0,0 +1,141 @@
defmodule Philomena.Repo.Migrations.RewriteSourceChanges do
use Ecto.Migration
def up do
rename table(:source_changes), to: table(:old_source_changes)
execute(
"alter index index_source_changes_on_image_id rename to index_old_source_changes_on_image_id"
)
execute(
"alter index index_source_changes_on_user_id rename to index_old_source_changes_on_user_id"
)
execute("alter index index_source_changes_on_ip rename to index_old_source_changes_on_ip")
execute(
"alter table old_source_changes rename constraint source_changes_pkey to old_source_changes_pkey"
)
execute("alter sequence source_changes_id_seq rename to old_source_changes_id_seq")
create table(:source_changes) do
add :image_id, references(:images, on_update: :update_all, on_delete: :delete_all),
null: false
add :user_id, references(:users, on_update: :update_all, on_delete: :delete_all)
add :ip, :inet, null: false
timestamps(inserted_at: :created_at)
add :added, :boolean, null: false
add :fingerprint, :string
add :user_agent, :string, default: ""
add :referrer, :string, default: ""
add :value, :string, null: false
end
alter table(:image_sources) do
remove :id
modify :source, :string
end
create index(:image_sources, [:image_id, :source],
name: "index_image_source_on_image_id_and_source",
unique: true
)
drop constraint(:image_sources, :length_must_be_valid,
check: "length(source) >= 8 and length(source) <= 1024"
)
create constraint(:image_sources, :image_sources_source_check, check: "source ~* '^https?://'")
# These statements should not be ran by the migration in production.
# Run them manually in psql instead.
if System.get_env("MIX_ENV") != "prod" do
execute("""
insert into image_sources (image_id, source)
select id as image_id, substr(source_url, 1, 255) as source from images
where source_url is not null and source_url ~* '^https?://';
""")
# First insert the "added" changes...
execute("""
with ranked_added_source_changes as (
select
image_id, user_id, ip, created_at, updated_at, fingerprint, user_agent,
substr(referrer, 1, 255) as referrer,
substr(new_value, 1, 255) as value, true as added,
rank() over (partition by image_id order by created_at asc)
from old_source_changes
where new_value is not null
)
insert into source_changes
(image_id, user_id, ip, created_at, updated_at, fingerprint, user_agent, referrer, value, added)
select image_id, user_id, ip, created_at, updated_at, fingerprint, user_agent, referrer, value, added
from ranked_added_source_changes
where "rank" > 1;
""")
# ...then the "removed" changes
execute("""
with ranked_removed_source_changes as (
select
image_id, user_id, ip, created_at, updated_at, fingerprint, user_agent,
substr(referrer, 1, 255) as referrer,
substr(new_value, 1, 255) as value, false as added,
rank() over (partition by image_id order by created_at desc)
from old_source_changes
where new_value is not null
)
insert into source_changes
(image_id, user_id, ip, created_at, updated_at, fingerprint, user_agent, referrer, value, added)
select image_id, user_id, ip, created_at, updated_at, fingerprint, user_agent, referrer, value, added
from ranked_removed_source_changes
where "rank" > 1;
""")
end
create index(:source_changes, [:image_id], name: "index_source_changes_on_image_id")
create index(:source_changes, [:user_id], name: "index_source_changes_on_user_id")
create index(:source_changes, [:ip], name: "index_source_changes_on_ip")
end
def down do
drop table(:source_changes)
rename table(:old_source_changes), to: table(:source_changes)
execute(
"alter index index_old_source_changes_on_image_id rename to index_source_changes_on_image_id"
)
execute(
"alter index index_old_source_changes_on_user_id rename to index_source_changes_on_user_id"
)
execute("alter index index_old_source_changes_on_ip rename to index_source_changes_on_ip")
execute(
"alter table source_changes rename constraint old_source_changes_pkey to source_changes_pkey"
)
execute("alter sequence old_source_changes_id_seq rename to source_changes_id_seq")
execute("truncate image_sources")
drop constraint(:image_sources, :image_sources_source_check, check: "source ~* '^https?://'")
create constraint(:image_sources, :length_must_be_valid,
check: "length(source) >= 8 and length(source) <= 1024"
)
drop index(:image_sources, [:image_id, :source],
name: "index_image_source_on_image_id_and_source"
)
alter table(:image_sources) do
modify :source, :text
end
end
end

View file

@ -18,35 +18,48 @@
"role": "user" "role": "user"
} }
], ],
"remote_images": [{ "remote_images": [
{
"url": "https://derpicdn.net/img/2015/9/26/988000/thumb.gif", "url": "https://derpicdn.net/img/2015/9/26/988000/thumb.gif",
"source_url": "https://derpibooru.org/988000", "sources": [
"https://derpibooru.org/988000"
],
"description": "Fairly large GIF (~23MB), use to test WebM stuff.", "description": "Fairly large GIF (~23MB), use to test WebM stuff.",
"tag_input": "alicorn, angry, animated, art, artist:assasinmonkey, artist:equum_amici, badass, barrier, crying, dark, epic, female, fight, force field, glare, glow, good vs evil, lord tirek, low angle, magic, mare, messy mane, metal as fuck, perspective, plot, pony, raised hoof, safe, size difference, spread wings, stomping, twilight's kingdom, twilight sparkle, twilight sparkle (alicorn), twilight vs tirek, underhoof" "tag_input": "alicorn, angry, animated, art, artist:assasinmonkey, artist:equum_amici, badass, barrier, crying, dark, epic, female, fight, force field, glare, glow, good vs evil, lord tirek, low angle, magic, mare, messy mane, metal as fuck, perspective, plot, pony, raised hoof, safe, size difference, spread wings, stomping, twilight's kingdom, twilight sparkle, twilight sparkle (alicorn), twilight vs tirek, underhoof"
}, },
{ {
"url": "https://derpicdn.net/img/2012/1/2/25/large.png", "url": "https://derpicdn.net/img/2012/1/2/25/large.png",
"source_url": "https://derpibooru.org/25", "sources": [
"https://derpibooru.org/25"
],
"tag_input": "artist:moe, canterlot, castle, cliff, cloud, detailed background, fog, forest, grass, mountain, mountain range, nature, no pony, outdoors, path, river, safe, scenery, scenery porn, signature, source needed, sunset, technical advanced, town, tree, useless source url, water, waterfall, widescreen, wood" "tag_input": "artist:moe, canterlot, castle, cliff, cloud, detailed background, fog, forest, grass, mountain, mountain range, nature, no pony, outdoors, path, river, safe, scenery, scenery porn, signature, source needed, sunset, technical advanced, town, tree, useless source url, water, waterfall, widescreen, wood"
}, },
{ {
"url": "https://derpicdn.net/img/2018/6/28/1767886/full.webm", "url": "https://derpicdn.net/img/2018/6/28/1767886/full.webm",
"source_url": "http://hydrusbeta.deviantart.com/art/Gleaming-in-the-Sun-Our-Colors-Shine-in-Every-Hue-611497309", "sources": [
"http://hydrusbeta.deviantart.com/art/Gleaming-in-the-Sun-Our-Colors-Shine-in-Every-Hue-611497309"
],
"tag_input": "3d, animated, architecture, artist:hydrusbeta, castle, cloud, crystal empire, crystal palace, flag, flag waving, no pony, no sound, safe, scenery, webm" "tag_input": "3d, animated, architecture, artist:hydrusbeta, castle, cloud, crystal empire, crystal palace, flag, flag waving, no pony, no sound, safe, scenery, webm"
}, },
{ {
"url": "https://derpicdn.net/img/view/2015/2/19/832750.jpg", "url": "https://derpicdn.net/img/view/2015/2/19/832750.jpg",
"source_url": "http://sovietrussianbrony.tumblr.com/post/111504505079/this-image-actually-took-me-ages-to-edit-the", "sources": [
"http://sovietrussianbrony.tumblr.com/post/111504505079/this-image-actually-took-me-ages-to-edit-the"
],
"tag_input": "artist:rhads, artist:the sexy assistant, canterlot, cloud, cloudsdale, cloudy, edit, lens flare, no pony, ponyville, rainbow, river, safe, scenery, sweet apple acres" "tag_input": "artist:rhads, artist:the sexy assistant, canterlot, cloud, cloudsdale, cloudy, edit, lens flare, no pony, ponyville, rainbow, river, safe, scenery, sweet apple acres"
}, },
{ {
"url": "https://derpicdn.net/img/view/2016/3/17/1110529.jpg", "url": "https://derpicdn.net/img/view/2016/3/17/1110529.jpg",
"source_url": "https://www.deviantart.com/devinian/art/Commission-Crystals-of-thy-heart-511134926", "sources": [
"https://www.deviantart.com/devinian/art/Commission-Crystals-of-thy-heart-511134926"
],
"tag_input": "artist:devinian, aurora crystialis, bridge, cloud, crepuscular rays, crystal empire, crystal palace, edit, flower, forest, grass, log, mountain, no pony, river, road, safe, scenery, scenery porn, source needed, stars, sunset, swing, tree, wallpaper" "tag_input": "artist:devinian, aurora crystialis, bridge, cloud, crepuscular rays, crystal empire, crystal palace, edit, flower, forest, grass, log, mountain, no pony, river, road, safe, scenery, scenery porn, source needed, stars, sunset, swing, tree, wallpaper"
}, },
{ {
"url": "https://derpicdn.net/img/view/2019/6/16/2067468.svg", "url": "https://derpicdn.net/img/view/2019/6/16/2067468.svg",
"source_url": "https://derpibooru.org/2067468", "sources": [
"https://derpibooru.org/2067468"
],
"tag_input": "artist:cheezedoodle96, babs seed, bloom and gloom, cutie mark, cutie mark only, no pony, safe, scissors, simple background, svg, .svg available, transparent background, vector" "tag_input": "artist:cheezedoodle96, babs seed, bloom and gloom, cutie mark, cutie mark only, no pony, safe, scissors, simple background, svg, .svg available, transparent background, vector"
} }
], ],

View file

@ -842,32 +842,12 @@ ALTER SEQUENCE public.image_intensities_id_seq OWNED BY public.image_intensities
-- --
CREATE TABLE public.image_sources ( CREATE TABLE public.image_sources (
id bigint NOT NULL,
image_id bigint NOT NULL, image_id bigint NOT NULL,
source text NOT NULL, source character varying(255) NOT NULL,
CONSTRAINT length_must_be_valid CHECK (((length(source) >= 8) AND (length(source) <= 1024))) CONSTRAINT image_sources_source_check CHECK (((source)::text ~* '^https?://'::text))
); );
--
-- Name: image_sources_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.image_sources_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: image_sources_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.image_sources_id_seq OWNED BY public.image_sources.id;
-- --
-- Name: image_subscriptions; Type: TABLE; Schema: public; Owner: - -- Name: image_subscriptions; Type: TABLE; Schema: public; Owner: -
-- --
@ -1136,6 +1116,44 @@ CREATE SEQUENCE public.notifications_id_seq
ALTER SEQUENCE public.notifications_id_seq OWNED BY public.notifications.id; ALTER SEQUENCE public.notifications_id_seq OWNED BY public.notifications.id;
--
-- Name: old_source_changes; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.old_source_changes (
id integer NOT NULL,
ip inet NOT NULL,
fingerprint character varying,
user_agent character varying DEFAULT ''::character varying,
referrer character varying DEFAULT ''::character varying,
new_value character varying,
initial boolean DEFAULT false NOT NULL,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
user_id integer,
image_id integer NOT NULL
);
--
-- Name: old_source_changes_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.old_source_changes_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: old_source_changes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.old_source_changes_id_seq OWNED BY public.old_source_changes.id;
-- --
-- Name: poll_options; Type: TABLE; Schema: public; Owner: - -- Name: poll_options; Type: TABLE; Schema: public; Owner: -
-- --
@ -1414,17 +1432,17 @@ ALTER SEQUENCE public.site_notices_id_seq OWNED BY public.site_notices.id;
-- --
CREATE TABLE public.source_changes ( CREATE TABLE public.source_changes (
id integer NOT NULL, id bigint NOT NULL,
image_id bigint NOT NULL,
user_id bigint,
ip inet NOT NULL, ip inet NOT NULL,
fingerprint character varying, created_at timestamp(0) without time zone NOT NULL,
user_agent character varying DEFAULT ''::character varying, updated_at timestamp(0) without time zone NOT NULL,
referrer character varying DEFAULT ''::character varying, added boolean NOT NULL,
new_value character varying, fingerprint character varying(255),
initial boolean DEFAULT false NOT NULL, user_agent character varying(255) DEFAULT ''::character varying,
created_at timestamp without time zone NOT NULL, referrer character varying(255) DEFAULT ''::character varying,
updated_at timestamp without time zone NOT NULL, value character varying(255) NOT NULL
user_id integer,
image_id integer NOT NULL
); );
@ -2265,13 +2283,6 @@ ALTER TABLE ONLY public.image_features ALTER COLUMN id SET DEFAULT nextval('publ
ALTER TABLE ONLY public.image_intensities ALTER COLUMN id SET DEFAULT nextval('public.image_intensities_id_seq'::regclass); ALTER TABLE ONLY public.image_intensities ALTER COLUMN id SET DEFAULT nextval('public.image_intensities_id_seq'::regclass);
--
-- Name: image_sources id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.image_sources ALTER COLUMN id SET DEFAULT nextval('public.image_sources_id_seq'::regclass);
-- --
-- Name: images id; Type: DEFAULT; Schema: public; Owner: - -- Name: images id; Type: DEFAULT; Schema: public; Owner: -
-- --
@ -2307,6 +2318,13 @@ ALTER TABLE ONLY public.moderation_logs ALTER COLUMN id SET DEFAULT nextval('pub
ALTER TABLE ONLY public.notifications ALTER COLUMN id SET DEFAULT nextval('public.notifications_id_seq'::regclass); ALTER TABLE ONLY public.notifications ALTER COLUMN id SET DEFAULT nextval('public.notifications_id_seq'::regclass);
--
-- Name: old_source_changes id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.old_source_changes ALTER COLUMN id SET DEFAULT nextval('public.old_source_changes_id_seq'::regclass);
-- --
-- Name: poll_options id; Type: DEFAULT; Schema: public; Owner: - -- Name: poll_options id; Type: DEFAULT; Schema: public; Owner: -
-- --
@ -2627,14 +2645,6 @@ ALTER TABLE ONLY public.image_intensities
ADD CONSTRAINT image_intensities_pkey PRIMARY KEY (id); ADD CONSTRAINT image_intensities_pkey PRIMARY KEY (id);
--
-- Name: image_sources image_sources_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.image_sources
ADD CONSTRAINT image_sources_pkey PRIMARY KEY (id);
-- --
-- Name: images images_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- Name: images images_pkey; Type: CONSTRAINT; Schema: public; Owner: -
-- --
@ -2675,6 +2685,14 @@ ALTER TABLE ONLY public.notifications
ADD CONSTRAINT notifications_pkey PRIMARY KEY (id); ADD CONSTRAINT notifications_pkey PRIMARY KEY (id);
--
-- Name: old_source_changes old_source_changes_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.old_source_changes
ADD CONSTRAINT old_source_changes_pkey PRIMARY KEY (id);
-- --
-- Name: poll_options poll_options_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- Name: poll_options poll_options_pkey; Type: CONSTRAINT; Schema: public; Owner: -
-- --
@ -3407,6 +3425,13 @@ CREATE INDEX index_image_hides_on_user_id ON public.image_hides USING btree (use
CREATE UNIQUE INDEX index_image_intensities_on_image_id ON public.image_intensities USING btree (image_id); CREATE UNIQUE INDEX index_image_intensities_on_image_id ON public.image_intensities USING btree (image_id);
--
-- Name: index_image_source_on_image_id_and_source; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX index_image_source_on_image_id_and_source ON public.image_sources USING btree (image_id, source);
-- --
-- Name: index_image_subscriptions_on_image_id_and_user_id; Type: INDEX; Schema: public; Owner: - -- Name: index_image_subscriptions_on_image_id_and_user_id; Type: INDEX; Schema: public; Owner: -
-- --
@ -3540,6 +3565,27 @@ CREATE INDEX index_mod_notes_on_notable_type_and_notable_id ON public.mod_notes
CREATE UNIQUE INDEX index_notifications_on_actor_id_and_actor_type ON public.notifications USING btree (actor_id, actor_type); CREATE UNIQUE INDEX index_notifications_on_actor_id_and_actor_type ON public.notifications USING btree (actor_id, actor_type);
--
-- Name: index_old_source_changes_on_image_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_old_source_changes_on_image_id ON public.old_source_changes USING btree (image_id);
--
-- Name: index_old_source_changes_on_ip; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_old_source_changes_on_ip ON public.old_source_changes USING btree (ip);
--
-- Name: index_old_source_changes_on_user_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_old_source_changes_on_user_id ON public.old_source_changes USING btree (user_id);
-- --
-- Name: index_poll_options_on_poll_id_and_label; Type: INDEX; Schema: public; Owner: - -- Name: index_poll_options_on_poll_id_and_label; Type: INDEX; Schema: public; Owner: -
-- --
@ -4177,10 +4223,10 @@ ALTER TABLE ONLY public.image_taggings
-- --
-- Name: source_changes fk_rails_10271ec4d0; Type: FK CONSTRAINT; Schema: public; Owner: - -- Name: old_source_changes fk_rails_10271ec4d0; Type: FK CONSTRAINT; Schema: public; Owner: -
-- --
ALTER TABLE ONLY public.source_changes ALTER TABLE ONLY public.old_source_changes
ADD CONSTRAINT fk_rails_10271ec4d0 FOREIGN KEY (image_id) REFERENCES public.images(id) ON UPDATE CASCADE ON DELETE CASCADE; ADD CONSTRAINT fk_rails_10271ec4d0 FOREIGN KEY (image_id) REFERENCES public.images(id) ON UPDATE CASCADE ON DELETE CASCADE;
@ -4577,10 +4623,10 @@ ALTER TABLE ONLY public.polls
-- --
-- Name: source_changes fk_rails_8d8cb9cb3b; Type: FK CONSTRAINT; Schema: public; Owner: - -- Name: old_source_changes fk_rails_8d8cb9cb3b; Type: FK CONSTRAINT; Schema: public; Owner: -
-- --
ALTER TABLE ONLY public.source_changes ALTER TABLE ONLY public.old_source_changes
ADD CONSTRAINT fk_rails_8d8cb9cb3b FOREIGN KEY (user_id) REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; ADD CONSTRAINT fk_rails_8d8cb9cb3b FOREIGN KEY (user_id) REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL;
@ -4950,6 +4996,19 @@ ALTER TABLE ONLY public.image_tag_locks
ALTER TABLE ONLY public.moderation_logs ALTER TABLE ONLY public.moderation_logs
ADD CONSTRAINT moderation_logs_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; ADD CONSTRAINT moderation_logs_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
-- Name: source_changes source_changes_image_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.source_changes
ADD CONSTRAINT source_changes_image_id_fkey FOREIGN KEY (image_id) REFERENCES public.images(id) ON UPDATE CASCADE ON DELETE CASCADE;
--
-- Name: source_changes source_changes_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.source_changes
ADD CONSTRAINT source_changes_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE CASCADE;
-- --
@ -4988,6 +5047,7 @@ INSERT INTO public."schema_migrations" (version) VALUES (20210912171343);
INSERT INTO public."schema_migrations" (version) VALUES (20210917190346); INSERT INTO public."schema_migrations" (version) VALUES (20210917190346);
INSERT INTO public."schema_migrations" (version) VALUES (20210921025336); INSERT INTO public."schema_migrations" (version) VALUES (20210921025336);
INSERT INTO public."schema_migrations" (version) VALUES (20210929181319); INSERT INTO public."schema_migrations" (version) VALUES (20210929181319);
INSERT INTO public."schema_migrations" (version) VALUES (20211009011024);
INSERT INTO public."schema_migrations" (version) VALUES (20211107130226); INSERT INTO public."schema_migrations" (version) VALUES (20211107130226);
INSERT INTO public."schema_migrations" (version) VALUES (20211219194836); INSERT INTO public."schema_migrations" (version) VALUES (20211219194836);
INSERT INTO public."schema_migrations" (version) VALUES (20220321173359); INSERT INTO public."schema_migrations" (version) VALUES (20220321173359);