mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-23 20:18:00 +01:00
commit
a569008267
27 changed files with 843 additions and 176 deletions
|
@ -322,3 +322,16 @@ span.spoiler div.image-container {
|
|||
.full-height {
|
||||
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;
|
||||
}
|
||||
|
|
83
assets/js/input-duplicator.js
Normal file
83
assets/js/input-duplicator.js
Normal 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 };
|
|
@ -1,41 +1,11 @@
|
|||
import { $, $$, removeEl} from './utils/dom';
|
||||
import { delegate, leftClick } from './utils/events';
|
||||
|
||||
function pollOptionRemover(_event, target) {
|
||||
removeEl(target.closest('.js-poll-option'));
|
||||
}
|
||||
import { inputDuplicatorCreator } from './input-duplicator';
|
||||
|
||||
function pollOptionCreator() {
|
||||
const addPollOptionButton = $('.js-poll-add-option');
|
||||
|
||||
delegate(document, 'click', {
|
||||
'.js-option-remove': leftClick(pollOptionRemover)
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
inputDuplicatorCreator({
|
||||
addButtonSelector: '.js-poll-add-option',
|
||||
fieldSelector: '.js-poll-option',
|
||||
maxInputCountSelector: '.js-max-option-count',
|
||||
removeButtonSelector: '.js-option-remove',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
12
assets/js/sources.js
Normal file
12
assets/js/sources.js
Normal 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 };
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
|
||||
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';
|
||||
|
||||
const MATROSKA_MAGIC = 0x1a45dfa3;
|
||||
|
@ -31,8 +31,10 @@ function setupImageUpload() {
|
|||
if (!imgPreviews) return;
|
||||
|
||||
const form = imgPreviews.closest('form');
|
||||
const [ fileField, remoteUrl, scraperError ] = $$('.js-scraper', form);
|
||||
const [ sourceEl, tagsEl, descrEl ] = $$('.js-image-input', form);
|
||||
const [fileField, remoteUrl, scraperError] = $$('.js-scraper', 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');
|
||||
if (!fetchButton) return;
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ import {
|
|||
toggleEl,
|
||||
whenReady,
|
||||
findFirstTextNode,
|
||||
disableEl,
|
||||
enableEl,
|
||||
} from '../dom';
|
||||
import { getRandomArrayItem, getRandomIntBetween } from '../../../test/randomness';
|
||||
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', () => {
|
||||
it(`should toggle the ${hiddenClass} class on the provided element`, () => {
|
||||
const mockVisibleElement = document.createElement('div');
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// DOM Utils
|
||||
|
||||
type PhilomenaInputElements = HTMLTextAreaElement | HTMLInputElement | HTMLButtonElement;
|
||||
|
||||
/**
|
||||
* 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>[]) {
|
||||
([] as E[]).concat(...elements).forEach(el => el.parentNode?.removeChild(el));
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import { setupToolbar } from './markdowntoolbar';
|
|||
import { hideStaffTools } from './staffhider';
|
||||
import { pollOptionCreator } from './poll';
|
||||
import { warnAboutPMs } from './pmwarning';
|
||||
import { imageSourcesCreator } from './sources';
|
||||
|
||||
whenReady(() => {
|
||||
|
||||
|
@ -68,5 +69,6 @@ whenReady(() => {
|
|||
hideStaffTools();
|
||||
pollOptionCreator();
|
||||
warnAboutPMs();
|
||||
imageSourcesCreator();
|
||||
|
||||
});
|
||||
|
|
|
@ -28,6 +28,7 @@ config :philomena,
|
|||
badge_url_root: System.fetch_env!("BADGE_URL_ROOT"),
|
||||
mailer_address: System.fetch_env!("MAILER_ADDRESS"),
|
||||
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"),
|
||||
redis_host: System.get_env("REDIS_HOST", "localhost"),
|
||||
proxy_host: System.get_env("PROXY_HOST"),
|
||||
|
|
|
@ -77,10 +77,12 @@ defmodule Philomena.Images do
|
|||
"""
|
||||
def create_image(attribution, attrs \\ %{}) do
|
||||
tags = Tags.get_or_create_tags(attrs["tag_input"])
|
||||
sources = attrs["sources"]
|
||||
|
||||
image =
|
||||
%Image{}
|
||||
|> Image.creation_changeset(attrs, attribution)
|
||||
|> Image.source_changeset(attrs, [], sources)
|
||||
|> Image.tag_changeset(attrs, [], tags)
|
||||
|> Image.dnp_changeset(attribution[:user])
|
||||
|> Uploader.analyze_upload(attrs)
|
||||
|
@ -92,11 +94,6 @@ defmodule Philomena.Images do
|
|||
|> Image.cache_changeset()
|
||||
|> repo.update()
|
||||
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} ->
|
||||
tag_ids = image.added_tags |> Enum.map(& &1.id)
|
||||
tags = Tag |> where([t], t.id in ^tag_ids)
|
||||
|
@ -333,29 +330,69 @@ defmodule Philomena.Images do
|
|||
|> Repo.update()
|
||||
end
|
||||
|
||||
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)
|
||||
def update_sources(%Image{} = image, attribution, attrs) do
|
||||
old_sources = attrs["old_sources"]
|
||||
new_sources = attrs["sources"]
|
||||
|
||||
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)
|
||||
|> Multi.run(:image, fn repo, _chg ->
|
||||
image = repo.preload(image, [:sources])
|
||||
|
||||
_ ->
|
||||
{:ok, nil}
|
||||
image
|
||||
|> 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)
|
||||
|> 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()
|
||||
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
|
||||
new_tags = Tags.get_or_create_tags(attrs["tag_input"])
|
||||
|
||||
|
@ -511,6 +548,13 @@ defmodule Philomena.Images do
|
|||
|> Multi.run(:copy_tags, fn _, %{} ->
|
||||
{:ok, Tags.copy_tags(image, duplicate_of_image)}
|
||||
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 _, %{} ->
|
||||
{:ok, Comments.migrate_comments(image, duplicate_of_image)}
|
||||
end)
|
||||
|
@ -787,6 +831,7 @@ defmodule Philomena.Images do
|
|||
:hiders,
|
||||
:deleter,
|
||||
:gallery_interactions,
|
||||
:sources,
|
||||
tags: [:aliases, :aliased_tag]
|
||||
]
|
||||
end
|
||||
|
@ -882,6 +927,17 @@ defmodule Philomena.Images do
|
|||
{:ok, count}
|
||||
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
|
||||
Exq.enqueue(Exq, "notifications", NotificationWorker, ["Images", [source.id, target.id]])
|
||||
end
|
||||
|
|
|
@ -119,7 +119,7 @@ defmodule Philomena.Images.ElasticsearchIndex do
|
|||
mime_type: image.image_mime_type,
|
||||
uploader: if(!!image.user and !image.anonymous, 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,
|
||||
original_format: image.image_format,
|
||||
processed: image.processed,
|
||||
|
|
|
@ -8,6 +8,7 @@ defmodule Philomena.Images.Image do
|
|||
alias Philomena.ImageVotes.ImageVote
|
||||
alias Philomena.ImageFaves.ImageFave
|
||||
alias Philomena.ImageHides.ImageHide
|
||||
alias Philomena.Images.Source
|
||||
alias Philomena.Images.Subscription
|
||||
alias Philomena.Users.User
|
||||
alias Philomena.Tags.Tag
|
||||
|
@ -18,6 +19,7 @@ defmodule Philomena.Images.Image do
|
|||
|
||||
alias Philomena.Images.Image
|
||||
alias Philomena.Images.TagDiffer
|
||||
alias Philomena.Images.SourceDiffer
|
||||
alias Philomena.Images.TagValidator
|
||||
alias Philomena.Images.DnpValidator
|
||||
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
|
||||
has_one :intensity, ImageIntensity
|
||||
has_many :galleries, through: [:gallery_interactions, :image]
|
||||
has_many :sources, Source, on_replace: :delete
|
||||
|
||||
field :image, :string
|
||||
field :image_name, :string
|
||||
|
@ -91,6 +94,8 @@ defmodule Philomena.Images.Image do
|
|||
|
||||
field :removed_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 :removed_image, :string, virtual: true
|
||||
|
@ -203,11 +208,15 @@ defmodule Philomena.Images.Image do
|
|||
|> change(image: nil)
|
||||
end
|
||||
|
||||
def source_changeset(image, attrs) do
|
||||
def source_changeset(image, attrs, old_sources, new_sources) do
|
||||
image
|
||||
|> cast(attrs, [:source_url])
|
||||
|> validate_required(:source_url)
|
||||
|> validate_format(:source_url, ~r/\Ahttps?:\/\//)
|
||||
|> cast(attrs, [])
|
||||
|> SourceDiffer.diff_input(old_sources, new_sources)
|
||||
end
|
||||
|
||||
def sources_changeset(image, new_sources) do
|
||||
change(image)
|
||||
|> put_assoc(:sources, new_sources)
|
||||
end
|
||||
|
||||
def tag_changeset(image, attrs, old_tags, new_tags, excluded_tags \\ []) do
|
||||
|
|
|
@ -4,9 +4,10 @@ defmodule Philomena.Images.Source do
|
|||
|
||||
alias Philomena.Images.Image
|
||||
|
||||
@primary_key false
|
||||
schema "image_sources" do
|
||||
belongs_to :image, Image
|
||||
field :source, :string
|
||||
belongs_to :image, Image, primary_key: true
|
||||
field :source, :string, primary_key: true
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
|
70
lib/philomena/images/source_differ.ex
Normal file
70
lib/philomena/images/source_differ.ex
Normal 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
|
|
@ -10,10 +10,10 @@ defmodule Philomena.SourceChanges.SourceChange do
|
|||
field :fingerprint, :string
|
||||
field :user_agent, :string, default: ""
|
||||
field :referrer, :string, default: ""
|
||||
field :new_value, :string
|
||||
field :initial, :boolean, default: false
|
||||
field :value, :string
|
||||
field :added, :boolean
|
||||
|
||||
field :source_url, :string, source: :new_value
|
||||
field :source_url, :string, source: :value
|
||||
|
||||
timestamps(inserted_at: :created_at, type: :utc_datetime)
|
||||
end
|
||||
|
|
|
@ -29,8 +29,8 @@ defmodule PhilomenaWeb.DuplicateReportController do
|
|||
|> preload([
|
||||
:user,
|
||||
:modifier,
|
||||
image: [:user, tags: :aliases],
|
||||
duplicate_of_image: [:user, tags: :aliases]
|
||||
image: [:user, :sources, tags: :aliases],
|
||||
duplicate_of_image: [:user, :sources, tags: :aliases]
|
||||
])
|
||||
|> order_by(desc: :created_at)
|
||||
|> Repo.paginate(conn.assigns.scrivener)
|
||||
|
|
|
@ -21,19 +21,18 @@ defmodule PhilomenaWeb.Image.SourceController do
|
|||
plug :load_and_authorize_resource,
|
||||
model: Image,
|
||||
id_name: "image_id",
|
||||
preload: [:user, tags: :aliases]
|
||||
preload: [:user, :sources, tags: :aliases]
|
||||
|
||||
def update(conn, %{"image" => image_params}) do
|
||||
attributes = conn.assigns.attributes
|
||||
image = conn.assigns.image
|
||||
old_source = image.source_url
|
||||
|
||||
case Images.update_source(image, attributes, image_params) do
|
||||
{:ok, %{image: image}} ->
|
||||
case Images.update_sources(image, attributes, image_params) do
|
||||
{:ok, %{image: {image, added_sources, removed_sources}}} ->
|
||||
PhilomenaWeb.Endpoint.broadcast!(
|
||||
"firehose",
|
||||
"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!(
|
||||
|
@ -49,7 +48,7 @@ defmodule PhilomenaWeb.Image.SourceController do
|
|||
|> where(image_id: ^image.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)
|
||||
end
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ defmodule PhilomenaWeb.ImageController do
|
|||
alias Philomena.{
|
||||
Images,
|
||||
Images.Image,
|
||||
Images.Source,
|
||||
Comments.Comment,
|
||||
Galleries.Gallery
|
||||
}
|
||||
|
@ -79,7 +80,7 @@ defmodule PhilomenaWeb.ImageController do
|
|||
|> Comments.change_comment()
|
||||
|
||||
image_changeset =
|
||||
image
|
||||
%{image | sources: sources_for_edit(image.sources)}
|
||||
|> Images.change_image()
|
||||
|
||||
watching = Images.subscribed?(image, conn.assigns.current_user)
|
||||
|
@ -108,7 +109,7 @@ defmodule PhilomenaWeb.ImageController do
|
|||
|
||||
def new(conn, _params) do
|
||||
changeset =
|
||||
%Image{}
|
||||
%Image{sources: sources_for_edit()}
|
||||
|> Images.change_image()
|
||||
|
||||
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),
|
||||
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})
|
||||
|> Repo.one()
|
||||
|> case do
|
||||
|
@ -217,4 +218,8 @@ defmodule PhilomenaWeb.ImageController do
|
|||
|> assign(:source_change_count, source_changes)
|
||||
end
|
||||
end
|
||||
|
||||
defp sources_for_edit(), do: [%Source{}]
|
||||
defp sources_for_edit([]), do: sources_for_edit()
|
||||
defp sources_for_edit(sources), do: sources
|
||||
end
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
.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 ->
|
||||
= if can?(@conn, :edit_metadata, @image) and !@conn.assigns.current_ban do
|
||||
|
||||
|
@ -6,9 +7,28 @@
|
|||
.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"
|
||||
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, [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"
|
||||
' Cancel
|
||||
|
||||
|
@ -18,22 +38,15 @@
|
|||
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"
|
||||
.flex.flex--wrap.flex--column#image-source
|
||||
.flex
|
||||
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>
|
||||
= if !has_sources || length(@image.sources) == 1 do
|
||||
' 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
|
||||
|
||||
= if @source_change_count > 1 do
|
||||
' Sources:
|
||||
= if @source_change_count > 0 do
|
||||
a.button.button--link.button--separate-left href=Routes.image_source_change_path(@conn, :index, @image) title="Source history"
|
||||
i.fa.fa-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"
|
||||
i.fas.fa-eraser>
|
||||
' 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
|
||||
|
|
|
@ -38,9 +38,25 @@
|
|||
.field-error-js.hidden.js-scraper
|
||||
|
||||
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
|
||||
= label f, :source_url, "The page you found this image on"
|
||||
= url_input f, :source_url, class: "input input--wide js-image-input", placeholder: "Source URL"
|
||||
button.button.js-image-add-source type="button"
|
||||
i.fa.fa-plus>
|
||||
' Add source
|
||||
|
||||
.field
|
||||
label for="image[tag_input]"
|
||||
|
@ -65,7 +81,7 @@
|
|||
.field
|
||||
.block
|
||||
.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"
|
||||
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
thead
|
||||
tr
|
||||
th colspan=2 Image
|
||||
th New Source
|
||||
th Source
|
||||
th Action
|
||||
th Timestamp
|
||||
th User
|
||||
th Initial?
|
||||
|
||||
tbody
|
||||
= 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
|
||||
|
||||
td
|
||||
= source_change.new_value
|
||||
= source_change.source_url
|
||||
|
||||
= if source_change.added do
|
||||
td.success Added
|
||||
- else
|
||||
td.danger Removed
|
||||
|
||||
td
|
||||
= pretty_time(source_change.created_at)
|
||||
|
@ -41,9 +46,5 @@
|
|||
br
|
||||
' Ask them before reverting their changes.
|
||||
|
||||
td
|
||||
= if source_change.initial do
|
||||
' ✓
|
||||
|
||||
.block__header
|
||||
= @pagination
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
elixir:
|
||||
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",
|
||||
autocomplete: "off"
|
||||
]
|
||||
|
|
|
@ -58,19 +58,24 @@ defmodule PhilomenaWeb.DuplicateReportView do
|
|||
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}),
|
||||
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}),
|
||||
do: to_string(duplicate_of_image.source_url) == to_string(image.source_url)
|
||||
def same_source?(%{image: image, duplicate_of_image: duplicate_of_image}) do
|
||||
MapSet.equal?(MapSet.new(image.sources), MapSet.new(duplicate_of_image.sources))
|
||||
end
|
||||
|
||||
def similar_source?(%{image: image, duplicate_of_image: duplicate_of_image}),
|
||||
do: uri_host(image.source_url) == uri_host(duplicate_of_image.source_url)
|
||||
def similar_source?(%{image: image, duplicate_of_image: duplicate_of_image}) do
|
||||
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}),
|
||||
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}),
|
||||
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}),
|
||||
do: MapSet.equal?(artist_tags(image), artist_tags(duplicate_of_image))
|
||||
|
|
|
@ -287,4 +287,96 @@ defmodule PhilomenaWeb.ImageView do
|
|||
|
||||
Philomena.Search.Evaluator.hits?(doc, query)
|
||||
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
|
||||
|
|
141
priv/repo/migrations/20211009011024_rewrite_source_changes.exs
Normal file
141
priv/repo/migrations/20211009011024_rewrite_source_changes.exs
Normal 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
|
|
@ -18,35 +18,48 @@
|
|||
"role": "user"
|
||||
}
|
||||
],
|
||||
"remote_images": [{
|
||||
"remote_images": [
|
||||
{
|
||||
"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.",
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -842,32 +842,12 @@ ALTER SEQUENCE public.image_intensities_id_seq OWNED BY public.image_intensities
|
|||
--
|
||||
|
||||
CREATE TABLE public.image_sources (
|
||||
id bigint NOT NULL,
|
||||
image_id bigint NOT NULL,
|
||||
source text NOT NULL,
|
||||
CONSTRAINT length_must_be_valid CHECK (((length(source) >= 8) AND (length(source) <= 1024)))
|
||||
source character varying(255) NOT NULL,
|
||||
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: -
|
||||
--
|
||||
|
@ -1136,6 +1116,44 @@ CREATE SEQUENCE public.notifications_id_seq
|
|||
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: -
|
||||
--
|
||||
|
@ -1414,17 +1432,17 @@ ALTER SEQUENCE public.site_notices_id_seq OWNED BY public.site_notices.id;
|
|||
--
|
||||
|
||||
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,
|
||||
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
|
||||
created_at timestamp(0) without time zone NOT NULL,
|
||||
updated_at timestamp(0) without time zone NOT NULL,
|
||||
added boolean NOT NULL,
|
||||
fingerprint character varying(255),
|
||||
user_agent character varying(255) DEFAULT ''::character varying,
|
||||
referrer character varying(255) DEFAULT ''::character varying,
|
||||
value character varying(255) 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);
|
||||
|
||||
|
||||
--
|
||||
-- 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: -
|
||||
--
|
||||
|
@ -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);
|
||||
|
||||
|
||||
--
|
||||
-- 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: -
|
||||
--
|
||||
|
@ -2627,14 +2645,6 @@ ALTER TABLE ONLY public.image_intensities
|
|||
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: -
|
||||
--
|
||||
|
@ -2675,6 +2685,14 @@ ALTER TABLE ONLY public.notifications
|
|||
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: -
|
||||
--
|
||||
|
@ -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);
|
||||
|
||||
|
||||
--
|
||||
-- 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: -
|
||||
--
|
||||
|
@ -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);
|
||||
|
||||
|
||||
--
|
||||
-- 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: -
|
||||
--
|
||||
|
@ -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;
|
||||
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
||||
|
@ -4950,6 +4996,19 @@ ALTER TABLE ONLY public.image_tag_locks
|
|||
|
||||
ALTER TABLE ONLY public.moderation_logs
|
||||
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 (20210921025336);
|
||||
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 (20211219194836);
|
||||
INSERT INTO public."schema_migrations" (version) VALUES (20220321173359);
|
||||
|
|
Loading…
Reference in a new issue