forked from e621ng/e621ng
Merge branch 'master' into style-changes
This commit is contained in:
commit
eb1f432fc5
@ -1 +1 @@
|
||||
2.7.0
|
||||
2.7.3
|
||||
|
116
Gemfile.lock
116
Gemfile.lock
@ -16,40 +16,40 @@ GIT
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (6.1.3.1)
|
||||
actionpack (= 6.1.3.1)
|
||||
activesupport (= 6.1.3.1)
|
||||
actioncable (6.1.3.2)
|
||||
actionpack (= 6.1.3.2)
|
||||
activesupport (= 6.1.3.2)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.1.3.1)
|
||||
actionpack (= 6.1.3.1)
|
||||
activejob (= 6.1.3.1)
|
||||
activerecord (= 6.1.3.1)
|
||||
activestorage (= 6.1.3.1)
|
||||
activesupport (= 6.1.3.1)
|
||||
actionmailbox (6.1.3.2)
|
||||
actionpack (= 6.1.3.2)
|
||||
activejob (= 6.1.3.2)
|
||||
activerecord (= 6.1.3.2)
|
||||
activestorage (= 6.1.3.2)
|
||||
activesupport (= 6.1.3.2)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.1.3.1)
|
||||
actionpack (= 6.1.3.1)
|
||||
actionview (= 6.1.3.1)
|
||||
activejob (= 6.1.3.1)
|
||||
activesupport (= 6.1.3.1)
|
||||
actionmailer (6.1.3.2)
|
||||
actionpack (= 6.1.3.2)
|
||||
actionview (= 6.1.3.2)
|
||||
activejob (= 6.1.3.2)
|
||||
activesupport (= 6.1.3.2)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.1.3.1)
|
||||
actionview (= 6.1.3.1)
|
||||
activesupport (= 6.1.3.1)
|
||||
actionpack (6.1.3.2)
|
||||
actionview (= 6.1.3.2)
|
||||
activesupport (= 6.1.3.2)
|
||||
rack (~> 2.0, >= 2.0.9)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.1.3.1)
|
||||
actionpack (= 6.1.3.1)
|
||||
activerecord (= 6.1.3.1)
|
||||
activestorage (= 6.1.3.1)
|
||||
activesupport (= 6.1.3.1)
|
||||
actiontext (6.1.3.2)
|
||||
actionpack (= 6.1.3.2)
|
||||
activerecord (= 6.1.3.2)
|
||||
activestorage (= 6.1.3.2)
|
||||
activesupport (= 6.1.3.2)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.1.3.1)
|
||||
activesupport (= 6.1.3.1)
|
||||
actionview (6.1.3.2)
|
||||
activesupport (= 6.1.3.2)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
@ -59,26 +59,26 @@ GEM
|
||||
activemodel (>= 4.1, < 6.2)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
activejob (6.1.3.1)
|
||||
activesupport (= 6.1.3.1)
|
||||
activejob (6.1.3.2)
|
||||
activesupport (= 6.1.3.2)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.1.3.1)
|
||||
activesupport (= 6.1.3.1)
|
||||
activemodel (6.1.3.2)
|
||||
activesupport (= 6.1.3.2)
|
||||
activemodel-serializers-xml (1.0.2)
|
||||
activemodel (> 5.x)
|
||||
activesupport (> 5.x)
|
||||
builder (~> 3.1)
|
||||
activerecord (6.1.3.1)
|
||||
activemodel (= 6.1.3.1)
|
||||
activesupport (= 6.1.3.1)
|
||||
activestorage (6.1.3.1)
|
||||
actionpack (= 6.1.3.1)
|
||||
activejob (= 6.1.3.1)
|
||||
activerecord (= 6.1.3.1)
|
||||
activesupport (= 6.1.3.1)
|
||||
activerecord (6.1.3.2)
|
||||
activemodel (= 6.1.3.2)
|
||||
activesupport (= 6.1.3.2)
|
||||
activestorage (6.1.3.2)
|
||||
actionpack (= 6.1.3.2)
|
||||
activejob (= 6.1.3.2)
|
||||
activerecord (= 6.1.3.2)
|
||||
activesupport (= 6.1.3.2)
|
||||
marcel (~> 1.0.0)
|
||||
mini_mime (~> 1.0.2)
|
||||
activesupport (6.1.3.1)
|
||||
activesupport (6.1.3.2)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
@ -184,7 +184,7 @@ GEM
|
||||
listen (3.5.1)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
loofah (2.9.0)
|
||||
loofah (2.9.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
@ -211,7 +211,7 @@ GEM
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2021.0225)
|
||||
mini_mime (1.0.3)
|
||||
mini_portile2 (2.5.0)
|
||||
mini_portile2 (2.5.1)
|
||||
minitest (5.14.4)
|
||||
minitest-ci (3.4.0)
|
||||
minitest (>= 5.0.6)
|
||||
@ -234,10 +234,10 @@ GEM
|
||||
netrc (0.11.0)
|
||||
newrelic_rpm (6.15.0)
|
||||
nio4r (2.5.7)
|
||||
nokogiri (1.11.2)
|
||||
nokogiri (1.11.3)
|
||||
mini_portile2 (~> 2.5.0)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.2-x64-mingw32)
|
||||
nokogiri (1.11.3-x64-mingw32)
|
||||
racc (~> 1.4)
|
||||
nokogumbo (2.0.5)
|
||||
nokogiri (~> 1.8, >= 1.8.4)
|
||||
@ -266,29 +266,29 @@ GEM
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
radix62 (1.0.1)
|
||||
rails (6.1.3.1)
|
||||
actioncable (= 6.1.3.1)
|
||||
actionmailbox (= 6.1.3.1)
|
||||
actionmailer (= 6.1.3.1)
|
||||
actionpack (= 6.1.3.1)
|
||||
actiontext (= 6.1.3.1)
|
||||
actionview (= 6.1.3.1)
|
||||
activejob (= 6.1.3.1)
|
||||
activemodel (= 6.1.3.1)
|
||||
activerecord (= 6.1.3.1)
|
||||
activestorage (= 6.1.3.1)
|
||||
activesupport (= 6.1.3.1)
|
||||
rails (6.1.3.2)
|
||||
actioncable (= 6.1.3.2)
|
||||
actionmailbox (= 6.1.3.2)
|
||||
actionmailer (= 6.1.3.2)
|
||||
actionpack (= 6.1.3.2)
|
||||
actiontext (= 6.1.3.2)
|
||||
actionview (= 6.1.3.2)
|
||||
activejob (= 6.1.3.2)
|
||||
activemodel (= 6.1.3.2)
|
||||
activerecord (= 6.1.3.2)
|
||||
activestorage (= 6.1.3.2)
|
||||
activesupport (= 6.1.3.2)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 6.1.3.1)
|
||||
railties (= 6.1.3.2)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.3.0)
|
||||
loofah (~> 2.3)
|
||||
railties (6.1.3.1)
|
||||
actionpack (= 6.1.3.1)
|
||||
activesupport (= 6.1.3.1)
|
||||
railties (6.1.3.2)
|
||||
actionpack (= 6.1.3.2)
|
||||
activesupport (= 6.1.3.2)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (~> 1.0)
|
||||
@ -477,4 +477,4 @@ DEPENDENCIES
|
||||
whenever
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.2
|
||||
2.1.4
|
||||
|
182
INSTALL.debian
182
INSTALL.debian
@ -1,182 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run: curl -L -s https://raw.githubusercontent.com/zwagoth/e621ng/master/INSTALL.debian -o install.sh ; chmod +x install.sh ; ./install.sh
|
||||
|
||||
export RUBY_VERSION=2.5.3
|
||||
export GITHUB_INSTALL_SCRIPTS=https://raw.githubusercontent.com/zwagoth/e621ng/master/script/install
|
||||
export VIPS_VERSION=8.7.0
|
||||
|
||||
if [[ "$(whoami)" != "root" ]] ; then
|
||||
echo "You must run this script as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
verlte() {
|
||||
[ "$1" = "`echo -e "$1\n$2" | sort -V | head -n1`" ]
|
||||
}
|
||||
|
||||
verlt() {
|
||||
[ "$1" = "$2" ] && return 1 || verlte $1 $2
|
||||
}
|
||||
|
||||
echo "* DANBOORU INSTALLATION SCRIPT"
|
||||
echo "*"
|
||||
echo "* This script will install all the necessary packages to run Danbooru on a "
|
||||
echo "* Debian server."
|
||||
echo
|
||||
echo -n "* Enter the hostname for this server (ex: danbooru.donmai.us): "
|
||||
read HOSTNAME
|
||||
|
||||
if [[ -z "$HOSTNAME" ]] ; then
|
||||
echo "* Must enter a hostname"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -n "* Enter the VLAN IP address for this server (ex: 172.16.0.1, enter nothing to skip): "
|
||||
read VLAN_IP_ADDR
|
||||
|
||||
# Install packages
|
||||
echo "* Installing packages..."
|
||||
|
||||
if [ -n "$(uname -a | grep Ubuntu)" ] ; then
|
||||
LIBSSL_DEV_PKG=libssl-dev
|
||||
else
|
||||
LIBSSL_DEV_PKG=$( verlt `lsb_release -sr` 9.0 && echo libssl-dev || echo libssl1.0-dev )
|
||||
fi
|
||||
apt-get update
|
||||
apt-get -y install $LIBSSL_DEV_PKG build-essential automake libxml2-dev libxslt-dev ncurses-dev sudo libreadline-dev flex bison ragel memcached libmemcached-dev git curl libcurl4-openssl-dev sendmail-bin sendmail nginx ssh coreutils ffmpeg mkvtoolnix
|
||||
apt-get -y install libpq-dev postgresql-client
|
||||
apt-get -y install liblcms2-dev libjpeg-turbo8-dev libexpat1-dev libgif-dev libpng-dev libexif-dev
|
||||
|
||||
# vrack specific stuff
|
||||
if [ -n "$VLAN_IP_ADDR" ] ; then
|
||||
apt-get -y install vlan
|
||||
modprobe 8021q
|
||||
echo "8021q" >> /etc/modules
|
||||
vconfig add eno2 99
|
||||
ip addr add $VLAN_IP_ADDR/24 dev eno2.99
|
||||
ip link set up eno2.99
|
||||
curl -L -s $GITHUB_INSTALL_SCRIPTS/vrack-cfg.yaml -o /etc/netplan/01-netcfg.yaml
|
||||
fi
|
||||
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||
curl -sSL https://deb.nodesource.com/setup_10.x | sudo -E bash -
|
||||
apt-get update
|
||||
apt-get -y install nodejs yarn
|
||||
apt-get remove cmdtest
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "* Error installing packages; aborting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# compile and install libvips (the version in apt is too old)
|
||||
cd /tmp
|
||||
wget -q https://github.com/libvips/libvips/releases/download/v$VIPS_VERSION/vips-$VIPS_VERSION.tar.gz
|
||||
tar xzf vips-$VIPS_VERSION.tar.gz
|
||||
cd vips-$VIPS_VERSION
|
||||
./configure --prefix=/usr
|
||||
make install
|
||||
ldconfig
|
||||
|
||||
# Create user account
|
||||
useradd -m danbooru
|
||||
chsh -s /bin/bash danbooru
|
||||
usermod -G danbooru,sudo danbooru
|
||||
|
||||
# Set up Postgres
|
||||
export PG_VERSION=`pg_config --version | egrep -o '[0-9]{1,}\.[0-9]{1,}'`
|
||||
if verlte 9.5 $PG_VERSION ; then
|
||||
# only do this on postgres 9.5 and above
|
||||
git clone https://github.com/r888888888/test_parser.git /tmp/test_parser
|
||||
cd /tmp/test_parser
|
||||
make install
|
||||
fi
|
||||
|
||||
# Install rbenv
|
||||
echo "* Installing rbenv..."
|
||||
cd /tmp
|
||||
sudo -i -u danbooru git clone git://github.com/sstephenson/rbenv.git ~danbooru/.rbenv
|
||||
sudo -i -u danbooru touch ~danbooru/.bash_profile
|
||||
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~danbooru/.bash_profile
|
||||
echo 'eval "$(rbenv init -)"' >> ~danbooru/.bash_profile
|
||||
sudo -i -u danbooru mkdir -p ~danbooru/.rbenv/plugins
|
||||
sudo -i -u danbooru git clone git://github.com/sstephenson/ruby-build.git ~danbooru/.rbenv/plugins/ruby-build
|
||||
sudo -i -u danbooru bash -l -c "rbenv install $RUBY_VERSION"
|
||||
sudo -i -u danbooru bash -l -c "rbenv global $RUBY_VERSION"
|
||||
|
||||
# Generate secret token and secret key
|
||||
echo "* Generating secret keys..."
|
||||
sudo -i -u danbooru mkdir ~danbooru/.danbooru/
|
||||
openssl rand -hex 32 > ~danbooru/.danbooru/secret_token
|
||||
openssl rand -hex 32 > ~danbooru/.danbooru/session_secret_key
|
||||
chown danbooru:danbooru ~danbooru/.danbooru/*
|
||||
chmod 600 ~danbooru/.danbooru/*
|
||||
|
||||
# Install gems
|
||||
echo "* Installing gems..."
|
||||
sudo -i -u danbooru bash -l -c 'gem install --no-ri --no-rdoc bundler'
|
||||
|
||||
echo "* Install configuration scripts..."
|
||||
|
||||
# Update PostgreSQL
|
||||
curl -L -s $GITHUB_INSTALL_SCRIPTS/postgresql_hba_conf -o /etc/postgresql/$PG_VERSION/main/pg_hba.conf
|
||||
/etc/init.d/postgresql restart
|
||||
sudo -u postgres createuser -s danbooru
|
||||
sudo -i -u danbooru createdb danbooru2
|
||||
|
||||
# Setup nginx
|
||||
curl -L -s $GITHUB_INSTALL_SCRIPTS/nginx.danbooru.conf -o /etc/nginx/sites-enabled/danbooru.conf
|
||||
sed -i -e "s/__hostname__/$HOSTNAME/g" /etc/nginx/sites-enabled/danbooru.conf
|
||||
/etc/init.d/nginx restart
|
||||
|
||||
# Setup logrotate
|
||||
curl -L -s $GITHUB_INSTALL_SCRIPTS/danbooru_logrotate_conf -o /etc/logrotate.d/danbooru.conf
|
||||
|
||||
# Setup danbooru account
|
||||
echo "* Enter a new password for the danbooru account"
|
||||
passwd danbooru
|
||||
|
||||
echo "* Setting up SSH keys for the danbooru account"
|
||||
sudo -i -u danbooru ssh-keygen
|
||||
|
||||
mkdir -p /var/www/danbooru2/shared/config
|
||||
mkdir -p /var/www/danbooru2/shared/data
|
||||
mkdir -p /var/www/danbooru2/shared/data/preview
|
||||
mkdir -p /var/www/danbooru2/shared/data/sample
|
||||
chown -R danbooru:danbooru /var/www/danbooru2
|
||||
curl -L -s $GITHUB_INSTALL_SCRIPTS/database.yml.templ -o /var/www/danbooru2/shared/config/database.yml
|
||||
curl -L -s $GITHUB_INSTALL_SCRIPTS/danbooru_local_config.rb.templ -o /var/www/danbooru2/shared/config/danbooru_local_config.rb
|
||||
|
||||
echo "* Almost done! You are now ready to deploy Danbooru onto this server."
|
||||
echo "* Log into Github and fork https://github.com/zwagoth/e621ng into"
|
||||
echo "* your own repository. Clone your fork onto your local development"
|
||||
echo "* machine and modify the following files:"
|
||||
echo "*"
|
||||
echo "* config/deploy.rb (github repo url)"
|
||||
echo "* config/deploy/production.rb (servers and users)"
|
||||
echo "* config/unicorn/production.rb (users)"
|
||||
echo "* config/application.rb (time zone)"
|
||||
echo "*"
|
||||
echo "* On the remote server you will want to modify this file:"
|
||||
echo "*"
|
||||
echo "* /var/www/danbooru2/shared/config/danbooru_local_config.rb"
|
||||
echo "*"
|
||||
read -p "Press [enter] to continue..."
|
||||
echo "* Commit your changes and push them to your fork. You are now ready to"
|
||||
echo "* deploy with the following command:"
|
||||
echo "*"
|
||||
echo "* bundle exec capistrano production deploy"
|
||||
echo "*"
|
||||
echo "* You can also run a server locally without having to deal with deploys"
|
||||
echo "* by running the following command:"
|
||||
echo "*"
|
||||
echo "* bundle install"
|
||||
echo "* bundle exec rake db:create db:migrate"
|
||||
echo "* bundle exec rails server"
|
||||
echo "*"
|
||||
echo "* This will start a web process running on port 3000 that you can"
|
||||
echo "* connect to. This is useful for development and testing purposes."
|
||||
echo "* If something breaks post about it on the Danbooru Github. Good luck!"
|
||||
|
@ -1,114 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run: curl -L -s https://raw.githubusercontent.com/zwagoth/e621ng/master/INSTALL.debian -o install.sh ; chmod +x install.sh ; ./install.sh
|
||||
|
||||
export RUBY_VERSION=2.5.3
|
||||
export GITHUB_INSTALL_SCRIPTS=https://raw.githubusercontent.com/zwagoth/e621ng/master/script/install
|
||||
export VIPS_VERSION=8.7.0
|
||||
|
||||
verlte() {
|
||||
[ "$1" = "`echo -e "$1\n$2" | sort -V | head -n1`" ]
|
||||
}
|
||||
|
||||
verlt() {
|
||||
[ "$1" = "$2" ] && return 1 || verlte $1 $2
|
||||
}
|
||||
|
||||
# Install packages
|
||||
echo "* Installing packages..."
|
||||
|
||||
if [ -n "$(uname -a | grep Ubuntu)" ] ; then
|
||||
LIBSSL_DEV_PKG=libssl-dev
|
||||
else
|
||||
LIBSSL_DEV_PKG=$( verlt `lsb_release -sr` 9.0 && echo libssl-dev || echo libssl1.0-dev )
|
||||
fi
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install $LIBSSL_DEV_PKG build-essential automake libxml2-dev libxslt-dev ncurses-dev sudo libreadline-dev flex bison ragel memcached libmemcached-dev git curl libcurl4-openssl-dev sendmail-bin sendmail nginx ssh coreutils ffmpeg mkvtoolnix
|
||||
sudo apt-get -y install libpq-dev postgresql-client postgresql postgresql-server-dev-10
|
||||
sudo apt-get -y install liblcms2-dev libjpeg-turbo8-dev libexpat1-dev libgif-dev libpng-dev libexif-dev
|
||||
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||
curl -sSL https://deb.nodesource.com/setup_10.x | sudo -E bash -
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install nodejs yarn
|
||||
sudo apt-get remove cmdtest
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "* Error installing packages; aborting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# compile and install libvips (the version in apt is too old)
|
||||
cd /tmp
|
||||
wget -q https://github.com/libvips/libvips/releases/download/v$VIPS_VERSION/vips-$VIPS_VERSION.tar.gz
|
||||
tar xzf vips-$VIPS_VERSION.tar.gz
|
||||
cd vips-$VIPS_VERSION
|
||||
./configure --prefix=/usr
|
||||
sudo make install
|
||||
sudo ldconfig
|
||||
|
||||
# Set up Postgres
|
||||
export PG_VERSION=`pg_config --version | egrep -o '[0-9]{1,}\.[0-9]{1,}'`
|
||||
if verlte 9.5 $PG_VERSION ; then
|
||||
# only do this on postgres 9.5 and above
|
||||
git clone https://github.com/r888888888/test_parser.git /tmp/test_parser
|
||||
cd /tmp/test_parser
|
||||
sudo make install
|
||||
fi
|
||||
|
||||
# Install rbenv
|
||||
echo "* Installing rbenv..."
|
||||
cd /tmp
|
||||
git clone git://github.com/sstephenson/rbenv.git ~/.rbenv
|
||||
touch ~/.bash_profile
|
||||
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
|
||||
echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
|
||||
mkdir -p ~/.rbenv/plugins
|
||||
git clone git://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
|
||||
rbenv install $RUBY_VERSION
|
||||
rbenv global $RUBY_VERSION
|
||||
|
||||
# Generate secret token and secret key
|
||||
echo "* Generating secret keys..."
|
||||
mkdir ~/.danbooru/
|
||||
openssl rand -hex 32 > ~/.danbooru/secret_token
|
||||
openssl rand -hex 32 > ~/.danbooru/session_secret_key
|
||||
chmod 600 ~/.danbooru/*
|
||||
|
||||
# Install gems
|
||||
echo "* Installing gems..."
|
||||
gem install --no-ri --no-rdoc bundler
|
||||
|
||||
echo "* Install configuration scripts..."
|
||||
|
||||
# Update PostgreSQL
|
||||
curl -L -s $GITHUB_INSTALL_SCRIPTS/postgresql_hba_conf -o /etc/postgresql/$PG_VERSION/main/pg_hba.conf
|
||||
/etc/init.d/postgresql restart
|
||||
sudo -u postgres createuser -s danbooru
|
||||
sudo -u postgres createdb danbooru2
|
||||
|
||||
# Setup nginx
|
||||
curl -L -s $GITHUB_INSTALL_SCRIPTS/nginx.danbooru.conf -o /etc/nginx/sites-enabled/danbooru.conf
|
||||
sed -i -e "s/__hostname__/$HOSTNAME/g" /etc/nginx/sites-enabled/danbooru.conf
|
||||
/etc/init.d/nginx restart
|
||||
|
||||
echo "* Clone the repo into your home folder."
|
||||
echo "* Copy the database.yml and danbooru_local_config.rb templates from the "
|
||||
echo "* scripts/install folder into the config folder and edit them to your needs."
|
||||
echo "* Remember to add 'user: danbooru' to the database yml file for development roles."
|
||||
echo "* You can then run a server locally without having to deal with deploys"
|
||||
echo "* by running the following command:"
|
||||
echo "*"
|
||||
echo "* bundle install"
|
||||
echo "* yarn install"
|
||||
echo "* RAILS_ENV=development bundle exec rake db:migrate"
|
||||
echo "* bundle exec rails server"
|
||||
echo "*"
|
||||
echo "* Run RAILS_ENV=development bundle exec rake db:seed after you have"
|
||||
echo "* created a new user!"
|
||||
echo "*"
|
||||
echo "* This will start a web process running on port 3000 that you can"
|
||||
echo "* connect to. This is useful for development and testing purposes."
|
||||
echo "* If something breaks post about it on the Github. Good luck!"
|
||||
|
79
README.md
79
README.md
@ -7,18 +7,17 @@
|
||||
1. Download and install the prerequisites
|
||||
2. Open Command Prompt/Terminal and run the following commands:
|
||||
```
|
||||
vagrant plugin install vagrant-hostmanager
|
||||
vagrant plugin install vagrant-bindfs
|
||||
vagrant plugin install vagrant-hostmanager
|
||||
vagrant plugin install vagrant-bindfs
|
||||
vagrant plugin install vagrant-vbguest
|
||||
```
|
||||
3. Download and extract the repo
|
||||
4. `cd` into the repo using Command Prompt/Terminal
|
||||
5. Run the following command:
|
||||
`vagrant up`
|
||||
6. Once vagrant exists with an error, execute `vagrant up` again (Workaround to install the linux headers required by the guest additions)
|
||||
7. This would be a good time to rewatch your favorite TV series installment, cook & have breakfast/lunch/dinner, walk the dog, clean your room, etc.<br>
|
||||
6. This would be a good time to rewatch your favorite TV series installment, cook & have breakfast/lunch/dinner, walk the dog, clean your room, etc.<br>
|
||||
By the time you get back the install will surely have completed.<sup>1</sup>
|
||||
8. To confirm the installation worked, open the web browser of your choice and enter `http://e621.lc` into the address bar and see if the website loads correctly.
|
||||
7. To confirm the installation worked, open the web browser of your choice and enter `http://e621.local` into the address bar and see if the website loads correctly.
|
||||
|
||||
<sub><sup>1</sup> If the install did not finish by the time an activity is complete please select another activity to avoid crippling boredom.</sub>
|
||||
|
||||
@ -26,28 +25,20 @@ By the time you get back the install will surely have completed.<sup>1</sup>
|
||||
|
||||
In case the VM doesn't start with the error `VT-x not available` and the error description `WHvCapabilityCodeHypervisorPresent is FALSE!` the the following solution should resolve the issue:
|
||||
|
||||
This error usually occours, when the `Hyper-V` or the `Windows Hypervisor Platform` features are activated. <br/>
|
||||
This error usually occurs, when the `Hyper-V` or the `Windows Hypervisor Platform` features are activated. <br/>
|
||||
To deactivate these features, press the windows key and enter `Turn Windows features on or off`, here you can deactivate both features by unchecking their respective checkboxes.
|
||||
Don't forget to restart the computer after deactivating the features.
|
||||
|
||||
## Installation
|
||||
|
||||
It is recommended that you install Danbooru on a Debian-based system
|
||||
since most of the required packages are available on APT. Danbooru
|
||||
has been successfully installed on Fedora, CentOS, FreeBSD, and OS X.
|
||||
The INSTALL.debian install script is straightforward and should be
|
||||
simple to adapt for other platforms.
|
||||
Installation follows the same steps as the vagrant setup script. Ubuntu 20.04 is the current installation target.
|
||||
There is no script that performs these steps for you, as you need to split them up to match your infrastructure.
|
||||
Running a single machine install in production is possible, but is likely to be somewhat sluggish due to contention in disk between postgresql and elasticsearch.
|
||||
Minimum RAM is 4GB. You will need to adjust values in config files to match how much RAM is available.
|
||||
If you are targeting more than a hundred thousand posts and reasonable user volumes, you probably want to procure yourself a database server. See tuning guides for postgresql and elasticsearch for help planning these requirements.
|
||||
|
||||
For best performance, you will need at least 256MB of RAM for
|
||||
PostgreSQL and Rails. The memory requirement will grow as your
|
||||
database gets bigger.
|
||||
|
||||
On production Danbooru uses PostgreSQL 9.4, but any 9.x release should
|
||||
work.
|
||||
|
||||
Use your operating system's package management system whenever
|
||||
possible. This will simplify the process of installing init scripts,
|
||||
which will not always happen when compiling from source.
|
||||
There are some forks that contain full docker setups. If you are looking for a Docker deployment and don't want to wait for this repo to slowly get there, look into those.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@ -69,55 +60,9 @@ debug your Nginx configuration file.
|
||||
|
||||
4) Check all log files.
|
||||
|
||||
## Services
|
||||
|
||||
Danbooru employs numerous external services to delegate some
|
||||
functionality.
|
||||
|
||||
For development purposes, you can just run mocked version of these
|
||||
services. They're available in `scrtip/mock_services` and can be started
|
||||
automatically using Foreman and the provided Procfile.
|
||||
|
||||
### Amazon Web Services
|
||||
|
||||
In order to enable the following features, you will need an AWS SQS
|
||||
account:
|
||||
|
||||
* Pool versions
|
||||
* Post versions
|
||||
* IQDB
|
||||
* Saved searches
|
||||
* Related tags
|
||||
|
||||
### Google APIs
|
||||
|
||||
The following features requires a Google API account:
|
||||
|
||||
* Bulk revert
|
||||
* Post versions report
|
||||
|
||||
### IQDB Service
|
||||
|
||||
IQDB integration is delegated to the [IQDBS service](https://github.com/r888888888/iqdbs).
|
||||
|
||||
### Archive Service
|
||||
|
||||
In order to access versioned data for pools and posts you will
|
||||
need to install and configure the [Archives service](https://github.com/r888888888/archives).
|
||||
|
||||
### Reportbooru Service
|
||||
|
||||
The following features are delegated to the [Reportbooru service](https://github.com/r888888888/reportbooru):
|
||||
|
||||
* Related tags
|
||||
* Missed searches report
|
||||
* Popular searches report
|
||||
* Favorite searches
|
||||
* Upload trend graphs
|
||||
|
||||
### Recommender Service
|
||||
|
||||
Post recommendations require the [Recommender service](https://github.com/r888888888/recommender).
|
||||
IQDB integration is delegated to the [IQDBS service](https://github.com/zwagoth/iqdbs).
|
||||
|
||||
### Cropped Thumbnails
|
||||
|
||||
|
3
Vagrantfile
vendored
3
Vagrantfile
vendored
@ -25,7 +25,7 @@ Vagrant.configure('2') do |config|
|
||||
# end
|
||||
|
||||
VAGRANT_COMMAND = ARGV[0]
|
||||
# config.ssh.username = 'danbooru' if VAGRANT_COMMAND == 'ssh'
|
||||
config.ssh.username = 'danbooru' if VAGRANT_COMMAND == 'ssh'
|
||||
|
||||
config.vm.define 'default' do |node|
|
||||
node.vm.hostname = 'e621.local'
|
||||
@ -34,4 +34,3 @@ Vagrant.configure('2') do |config|
|
||||
|
||||
config.vm.provision 'shell', path: 'vagrant/install.sh'
|
||||
end
|
||||
|
||||
|
44
app/concerns/user_warnable.rb
Normal file
44
app/concerns/user_warnable.rb
Normal file
@ -0,0 +1,44 @@
|
||||
module UserWarnable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
WARNING_TYPES = {
|
||||
'warning' => 1,
|
||||
'record' => 2,
|
||||
'ban' => 3,
|
||||
'unmark' => nil
|
||||
}
|
||||
|
||||
included do
|
||||
scope :user_warned, -> { where('warning_type IS NOT NULL') }
|
||||
validates :warning_type, inclusion: { :in => [1, 2, 3, nil] }
|
||||
end
|
||||
|
||||
def user_warned!(type, user=CurrentUser.id)
|
||||
unless WARNING_TYPES.has_key?(type)
|
||||
errors.add(:warning_type, 'invalid warning type')
|
||||
return
|
||||
end
|
||||
update({warning_type: WARNING_TYPES[type], warning_user_id: user})
|
||||
end
|
||||
|
||||
def remove_user_warning!
|
||||
update_columns({warning_type: nil, warning_user_id: nil})
|
||||
end
|
||||
|
||||
def was_warned?
|
||||
!warning_type.nil?
|
||||
end
|
||||
|
||||
def warning_type_string
|
||||
case warning_type
|
||||
when 1
|
||||
"User received a warning for the contents of this message."
|
||||
when 2
|
||||
"User received a record for the contents of this message."
|
||||
when 3
|
||||
"User was banned for the contents of this message."
|
||||
else
|
||||
"[This is a bug with the website. Woo!]"
|
||||
end
|
||||
end
|
||||
end
|
35
app/controllers/admin/staff_notes_controller.rb
Normal file
35
app/controllers/admin/staff_notes_controller.rb
Normal file
@ -0,0 +1,35 @@
|
||||
module Admin
|
||||
class StaffNotesController < ApplicationController
|
||||
before_action :moderator_only
|
||||
respond_to :html
|
||||
|
||||
def index
|
||||
@user = User.where('id = ?', params[:user_id]).first
|
||||
@notes = StaffNote.search(search_params.merge({user_id: params[:user_id]})).includes(:user, :creator).paginate(params[:page])
|
||||
respond_with(@notes)
|
||||
end
|
||||
|
||||
def new
|
||||
@user = User.find(params[:user_id])
|
||||
@staff_note = StaffNote.new(note_params)
|
||||
respond_with(@note)
|
||||
end
|
||||
|
||||
def create
|
||||
@user = User.find(params[:user_id])
|
||||
@staff_note = StaffNote.create(note_params.merge({creator: CurrentUser.user, user_id: @user.id}))
|
||||
flash[:notice] = @staff_note.valid? ? "Staff Note added" : @staff_note.errors.full_messages.join("; ")
|
||||
respond_with(@staff_note) do |format|
|
||||
format.html do
|
||||
redirect_back fallback_location: admin_staff_notes_path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def note_params
|
||||
params.fetch(:staff_note, {}).permit([:body])
|
||||
end
|
||||
end
|
||||
end
|
@ -60,6 +60,8 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
|
||||
case exception
|
||||
when ProcessingError
|
||||
render_expected_error(400, exception)
|
||||
when APIThrottled
|
||||
render_expected_error(429, "Throttled: Too many requests")
|
||||
when ActiveRecord::QueryCanceled
|
||||
@ -128,13 +130,24 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
|
||||
DanbooruLogger.log(@exception, expected: @expected)
|
||||
log = ExceptionLog.add(exception, CurrentUser.ip_addr, {
|
||||
log_params = {
|
||||
host: Socket.gethostname,
|
||||
params: params,
|
||||
params: request.filtered_parameters,
|
||||
user_id: CurrentUser.id,
|
||||
referrer: request.referrer,
|
||||
user_agent: request.user_agent
|
||||
}) if !@expected
|
||||
}
|
||||
# Required to unwrap exceptions that occur inside template rendering.
|
||||
new_exception = exception
|
||||
if exception.respond_to?(:cause) && exception.is_a?(ActionView::Template::Error)
|
||||
new_exception = exception.cause
|
||||
end
|
||||
if new_exception&.is_a?(ActiveRecord::QueryCanceled)
|
||||
log_params[:sql] = {}
|
||||
log_params[:sql][:query] = new_exception&.sql || "[NOT FOUND?]"
|
||||
log_params[:sql][:binds] = new_exception&.binds
|
||||
end
|
||||
log = ExceptionLog.add(exception, CurrentUser.ip_addr, log_params) if !@expected
|
||||
@log_code = log&.code
|
||||
render "static/error", layout: layout, status: status, formats: format
|
||||
end
|
||||
|
@ -2,7 +2,7 @@ class BlipsController < ApplicationController
|
||||
class BlipTooOld < Exception ; end
|
||||
respond_to :html, :json
|
||||
before_action :member_only, only: [:create, :new, :update, :edit, :hide]
|
||||
before_action :moderator_only, only: [:unhide, :destroy]
|
||||
before_action :moderator_only, only: [:unhide, :destroy, :warning]
|
||||
|
||||
rescue_from BlipTooOld, with: :blip_too_old
|
||||
|
||||
@ -84,6 +84,16 @@ class BlipsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def warning
|
||||
@blip = Blip.find(params[:id])
|
||||
if params[:record_type] == 'unmark'
|
||||
@blip.remove_user_warning!
|
||||
else
|
||||
@blip.user_warned!(params[:record_type])
|
||||
end
|
||||
respond_with(@blip)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def search_params
|
||||
|
@ -1,7 +1,7 @@
|
||||
class CommentsController < ApplicationController
|
||||
respond_to :html, :json
|
||||
before_action :member_only, :except => [:index, :search, :show]
|
||||
before_action :moderator_only, only: [:unhide, :destroy]
|
||||
before_action :moderator_only, only: [:unhide, :destroy, :warning]
|
||||
skip_before_action :api_check
|
||||
|
||||
def index
|
||||
@ -79,6 +79,16 @@ class CommentsController < ApplicationController
|
||||
respond_with(@comment)
|
||||
end
|
||||
|
||||
def warning
|
||||
@comment = Comment.find(params[:id])
|
||||
if params[:record_type] == 'unmark'
|
||||
@comment.remove_user_warning!
|
||||
else
|
||||
@comment.user_warned!(params[:record_type])
|
||||
end
|
||||
respond_with(@comment)
|
||||
end
|
||||
|
||||
private
|
||||
def index_by_post
|
||||
tags = params[:tags] || ""
|
||||
|
@ -11,10 +11,10 @@ class FavoritesController < ApplicationController
|
||||
@user = User.find(user_id)
|
||||
|
||||
if @user.hide_favorites?
|
||||
raise User::PrivilegeError.new
|
||||
raise User::PrivilegeError.new "User has hidden their favorites"
|
||||
end
|
||||
|
||||
@favorite_set = PostSets::Favorites.new(@user, params[:page])
|
||||
@favorite_set = PostSets::Favorites.new(@user, params[:page], params[:limit])
|
||||
respond_with(@favorite_set.posts) do |fmt|
|
||||
fmt.json do
|
||||
render json: @favorite_set.posts, root: 'posts'
|
||||
|
@ -1,8 +1,8 @@
|
||||
class ForumPostsController < ApplicationController
|
||||
respond_to :html, :json
|
||||
before_action :member_only, :except => [:index, :show, :search]
|
||||
before_action :moderator_only, only: [:destroy, :unhide]
|
||||
before_action :load_post, :only => [:edit, :show, :update, :destroy, :hide, :unhide]
|
||||
before_action :moderator_only, only: [:destroy, :unhide, :warning]
|
||||
before_action :load_post, :only => [:edit, :show, :update, :destroy, :hide, :unhide, :warning]
|
||||
before_action :check_min_level, :only => [:edit, :show, :update, :destroy, :hide, :unhide]
|
||||
skip_before_action :api_check
|
||||
|
||||
@ -70,6 +70,15 @@ class ForumPostsController < ApplicationController
|
||||
respond_with(@forum_post)
|
||||
end
|
||||
|
||||
def warning
|
||||
if params[:record_type] == 'unmark'
|
||||
@forum_post.remove_user_warning!
|
||||
else
|
||||
@forum_post.user_warned!(params[:record_type])
|
||||
end
|
||||
respond_with(@forum_post)
|
||||
end
|
||||
|
||||
private
|
||||
def load_post
|
||||
@forum_post = ForumPost.includes(topic: [:category]).find(params[:id])
|
||||
|
@ -1,10 +1,10 @@
|
||||
module Maintenance
|
||||
module User
|
||||
class ApiKeysController < ApplicationController
|
||||
before_action :check_privilege
|
||||
before_action :member_only
|
||||
before_action :authenticate!, :except => [:show]
|
||||
rescue_from ::SessionLoader::AuthenticationFailure, :with => :authentication_failed
|
||||
respond_to :html, :json
|
||||
respond_to :html
|
||||
|
||||
def view
|
||||
respond_with(CurrentUser.user, @api_key)
|
||||
@ -22,10 +22,6 @@ module Maintenance
|
||||
|
||||
protected
|
||||
|
||||
def check_privilege
|
||||
raise ::User::PrivilegeError unless params[:user_id].to_i == CurrentUser.id
|
||||
end
|
||||
|
||||
def authenticate!
|
||||
if ::User.authenticate(CurrentUser.user.name, params[:user][:password]) == CurrentUser.user
|
||||
@api_key = CurrentUser.user.api_key || ApiKey.generate!(CurrentUser.user)
|
||||
|
@ -2,6 +2,7 @@ module Moderator
|
||||
module Post
|
||||
class PostsController < ApplicationController
|
||||
before_action :approver_only
|
||||
before_action :janitor_only, only: [:regenerate_thumbnails, :regenerate_videos]
|
||||
before_action :admin_only, :only => [:expunge]
|
||||
skip_before_action :api_check
|
||||
|
||||
@ -47,6 +48,20 @@ module Moderator
|
||||
@post.expunge!
|
||||
respond_with(@post)
|
||||
end
|
||||
|
||||
def regenerate_thumbnails
|
||||
@post = ::Post.find(params[:id])
|
||||
raise ::User::PrivilegeError.new "Cannot regenerate thumbnails on deleted images" if @post.is_deleted?
|
||||
@post.regenerate_image_samples!
|
||||
respond_with(@post)
|
||||
end
|
||||
|
||||
def regenerate_videos
|
||||
@post = ::Post.find(params[:id])
|
||||
raise ::User::PrivilegeError.new "Cannot regenerate thumbnails on deleted images" if @post.is_deleted?
|
||||
@post.regenerate_video_samples!
|
||||
respond_with(@post)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,44 +1,68 @@
|
||||
class PostReplacementsController < ApplicationController
|
||||
respond_to :html, :json, :js
|
||||
before_action :moderator_only, except: [:index]
|
||||
respond_to :html
|
||||
before_action :moderator_only, only: [:destroy]
|
||||
before_action :janitor_only, only: [:create, :new, :approve, :reject, :promote]
|
||||
content_security_policy only: [:new] do |p|
|
||||
p.img_src :self, :data, "*"
|
||||
end
|
||||
|
||||
def new
|
||||
@post_replacement = Post.find(params[:post_id]).replacements.new
|
||||
@post = Post.find(params[:post_id])
|
||||
@post_replacement = @post.replacements.new
|
||||
respond_with(@post_replacement)
|
||||
end
|
||||
|
||||
def create
|
||||
@post = Post.find(params[:post_id])
|
||||
@post_replacement = @post.replace!(create_params)
|
||||
|
||||
flash[:notice] = "Post replaced"
|
||||
@post_replacement = @post.replacements.create(create_params.merge(creator_id: CurrentUser.id, creator_ip_addr: CurrentUser.ip_addr))
|
||||
if @post_replacement.errors.size == 0
|
||||
flash[:notice] = "Post replacement submitted"
|
||||
else
|
||||
flash[:notice] = @post_replacement.errors.full_messages.join('; ')
|
||||
end
|
||||
respond_with(@post_replacement, location: @post)
|
||||
end
|
||||
|
||||
def update
|
||||
def approve
|
||||
@post_replacement = PostReplacement.find(params[:id])
|
||||
@post_replacement.update(update_params)
|
||||
@post_replacement.approve!
|
||||
|
||||
respond_with(@post_replacement, location: post_path(@post_replacement.post))
|
||||
end
|
||||
|
||||
def reject
|
||||
@post_replacement = PostReplacement.find(params[:id])
|
||||
@post_replacement.reject!
|
||||
|
||||
respond_with(@post_replacement)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@post_replacement = PostReplacement.find(params[:id])
|
||||
@post_replacement.destroy
|
||||
|
||||
respond_with(@post_replacement)
|
||||
end
|
||||
|
||||
def promote
|
||||
@post_replacement = PostReplacement.find(params[:id])
|
||||
@post = @post_replacement.promote!
|
||||
if @post.errors.any?
|
||||
respond_with(@post)
|
||||
else
|
||||
respond_with(@post.post)
|
||||
end
|
||||
end
|
||||
|
||||
def index
|
||||
params[:search][:post_id] = params.delete(:post_id) if params.has_key?(:post_id)
|
||||
@post_replacements = PostReplacement.search(search_params).paginate(params[:page], limit: params[:limit])
|
||||
@post_replacements = PostReplacement.visible(CurrentUser.user).search(search_params).paginate(params[:page], limit: params[:limit])
|
||||
|
||||
respond_with(@post_replacements)
|
||||
end
|
||||
|
||||
private
|
||||
def create_params
|
||||
params.require(:post_replacement).permit(:replacement_url, :replacement_file, :final_source, :tags)
|
||||
end
|
||||
|
||||
def update_params
|
||||
params.require(:post_replacement).permit(
|
||||
:old_file_ext, :old_file_size, :old_image_width, :old_image_height, :old_md5,
|
||||
:file_ext, :file_size, :image_width, :image_height, :md5,
|
||||
:original_url, :replacement_url
|
||||
)
|
||||
params.require(:post_replacement).permit(:replacement_url, :replacement_file, :reason, :source)
|
||||
end
|
||||
end
|
||||
|
@ -19,7 +19,11 @@ class TagsController < ApplicationController
|
||||
|
||||
expires_in params[:expiry].to_i.days if params[:expiry]
|
||||
|
||||
respond_with(@tags)
|
||||
respond_with(@tags) do |fmt|
|
||||
fmt.json do
|
||||
render json: @tags.to_json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def preview
|
||||
|
@ -2,7 +2,7 @@ class UploadsController < ApplicationController
|
||||
before_action :member_only
|
||||
before_action :janitor_only, only: [:index, :show]
|
||||
respond_to :html, :json
|
||||
content_security_policy do |p|
|
||||
content_security_policy only: [:new] do |p|
|
||||
p.img_src :self, :data, "*"
|
||||
end
|
||||
|
||||
@ -11,7 +11,6 @@ class UploadsController < ApplicationController
|
||||
return access_denied("You can not upload during your first week.")
|
||||
end
|
||||
@source = Sources::Strategies.find(params[:url], params[:ref]) if params[:url].present?
|
||||
@upload_notice_wiki = WikiPage.titled(Danbooru.config.upload_notice_wiki_page).first
|
||||
@upload = Upload.new
|
||||
respond_with(@upload)
|
||||
end
|
||||
@ -76,6 +75,6 @@ class UploadsController < ApplicationController
|
||||
permitted_params << :locked_tags if CurrentUser.is_admin?
|
||||
permitted_params << :locked_rating if CurrentUser.is_privileged?
|
||||
|
||||
params.require(:upload).permit(permitted_params)
|
||||
params.require(:upload).permit(permitted_params).merge(uploader_id: CurrentUser.id, uploader_ip_addr: CurrentUser.ip_addr)
|
||||
end
|
||||
end
|
||||
|
@ -19,6 +19,7 @@ class WikiPagesController < ApplicationController
|
||||
return
|
||||
end
|
||||
end
|
||||
ensure_can_edit(@wiki_page, CurrentUser.user)
|
||||
respond_with(@wiki_page)
|
||||
end
|
||||
|
||||
@ -67,6 +68,7 @@ class WikiPagesController < ApplicationController
|
||||
|
||||
def update
|
||||
@wiki_page = WikiPage.find(params[:id])
|
||||
ensure_can_edit(@wiki_page, CurrentUser.user)
|
||||
@wiki_page.update(wiki_page_params(:update))
|
||||
respond_with(@wiki_page)
|
||||
end
|
||||
@ -79,6 +81,7 @@ class WikiPagesController < ApplicationController
|
||||
|
||||
def revert
|
||||
@wiki_page = WikiPage.find(params[:id])
|
||||
ensure_can_edit(@wiki_page, CurrentUser.user)
|
||||
@version = @wiki_page.versions.find(params[:version_id])
|
||||
@wiki_page.revert_to!(@version)
|
||||
flash[:notice] = "Page was reverted"
|
||||
@ -97,6 +100,11 @@ class WikiPagesController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def ensure_can_edit(page, user)
|
||||
return if user.is_janitor?
|
||||
raise User::PrivilegeError.new("Wiki page is locked.") if page.is_locked
|
||||
end
|
||||
|
||||
def normalize_search_params
|
||||
if params[:title]
|
||||
params[:search] ||= {}
|
||||
|
@ -88,7 +88,7 @@ class PostsDecorator < ApplicationDecorator
|
||||
return "<em>none</em>".html_safe
|
||||
end
|
||||
|
||||
if !options[:show_deleted] && post.is_deleted? && options[:tags] !~ /(?:status:(?:all|any|deleted))|(?:deletedby:)|(?:delreason:)/ && !options[:raw]
|
||||
if !options[:show_deleted] && post.is_deleted? && options[:tags] !~ /(?:status:(?:all|any|deleted))|(?:deletedby:)|(?:delreason:)/i && !options[:raw]
|
||||
return ""
|
||||
end
|
||||
|
||||
|
6
app/helpers/post_replacement_helper.rb
Normal file
6
app/helpers/post_replacement_helper.rb
Normal file
@ -0,0 +1,6 @@
|
||||
module PostReplacementHelper
|
||||
def replacement_thumbnail(replacement)
|
||||
return tag.a(image_tag(replacement.replacement_thumb_url), href: replacement.replacement_file_url) if replacement.file_visible_to?(CurrentUser.user)
|
||||
image_tag(replacement.replacement_thumb_url)
|
||||
end
|
||||
end
|
@ -7,32 +7,25 @@ module TagsHelper
|
||||
if tag.antecedent_alias
|
||||
html << "<p class='hint'>This tag has been aliased to "
|
||||
html << link_to(tag.antecedent_alias.consequent_name, show_or_new_wiki_pages_path(:title => tag.antecedent_alias.consequent_name))
|
||||
html << " (#{link_to "learn more", wiki_pages_path(title: "help:tag_aliases")}).</p>"
|
||||
html << " (#{link_to "learn more", wiki_pages_path(title: "e621:tag_aliases")}).</p>"
|
||||
end
|
||||
|
||||
if tag.consequent_aliases.present?
|
||||
html << "<p class='hint'>The following tags are aliased to this tag: "
|
||||
html << raw(tag.consequent_aliases.map {|x| link_to(x.antecedent_name, show_or_new_wiki_pages_path(:title => x.antecedent_name))}.join(", "))
|
||||
html << " (#{link_to "learn more", wiki_pages_path(title: "help:tag_aliases")}).</p>"
|
||||
end
|
||||
|
||||
automatic_tags = TagImplication.automatic_tags_for([tag.name])
|
||||
if automatic_tags.present?
|
||||
html << "<p class='hint'>This tag automatically adds "
|
||||
html << raw(automatic_tags.map {|x| link_to(x, show_or_new_wiki_pages_path(:title => x))}.join(", "))
|
||||
html << " (#{link_to "learn more", wiki_pages_path(title: "help:autotags")}).</p>"
|
||||
html << " (#{link_to "learn more", wiki_pages_path(title: "e621:tag_aliases")}).</p>"
|
||||
end
|
||||
|
||||
if tag.antecedent_implications.present?
|
||||
html << "<p class='hint'>This tag implicates "
|
||||
html << raw(tag.antecedent_implications.map {|x| link_to(x.consequent_name, show_or_new_wiki_pages_path(:title => x.consequent_name))}.join(", "))
|
||||
html << " (#{link_to "learn more", wiki_pages_path(title: "help:tag_implications")}).</p>"
|
||||
html << " (#{link_to "learn more", wiki_pages_path(title: "e621:tag_implications")}).</p>"
|
||||
end
|
||||
|
||||
if tag.consequent_implications.present?
|
||||
html << "<p class='hint'>The following tags implicate this tag: "
|
||||
html << raw(tag.consequent_implications.map {|x| link_to(x.antecedent_name, show_or_new_wiki_pages_path(:title => x.antecedent_name))}.join(", "))
|
||||
html << " (#{link_to "learn more", wiki_pages_path(title: "help:tag_implications")}).</p>"
|
||||
html << " (#{link_to "learn more", wiki_pages_path(title: "e621:tag_implications")}).</p>"
|
||||
end
|
||||
|
||||
html.html_safe
|
||||
|
@ -138,27 +138,7 @@ Autocomplete.initialize_artist_autocomplete = function($fields) {
|
||||
$(this).data("ui-autocomplete").menu.bindings = $();
|
||||
},
|
||||
source: function(req, resp) {
|
||||
$.ajax({
|
||||
url: "/artists.json",
|
||||
data: {
|
||||
"search[name_like]": req.term.trim().replace(/\s+/g, "_") + "*",
|
||||
"search[is_active]": true,
|
||||
"search[order]": "post_count",
|
||||
"limit": 10,
|
||||
"expiry": 7
|
||||
},
|
||||
method: "get",
|
||||
success: function(data) {
|
||||
resp($.map(data, function(artist) {
|
||||
return {
|
||||
type: "tag",
|
||||
label: artist.name.replace(/_/g, " "),
|
||||
value: artist.name,
|
||||
category: Autocomplete.TAG_CATEGORIES.artist,
|
||||
};
|
||||
}));
|
||||
}
|
||||
});
|
||||
Autocomplete.artist_source(req.term, resp);
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -191,61 +171,11 @@ Autocomplete.initialize_wiki_autocomplete = function($fields) {
|
||||
$(this).data("ui-autocomplete").menu.bindings = $();
|
||||
},
|
||||
source: function(req, resp) {
|
||||
$.ajax({
|
||||
url: "/wiki_pages.json",
|
||||
data: {
|
||||
"search[title]": req.term + "*",
|
||||
"search[hide_deleted]": "Yes",
|
||||
"search[order]": "post_count",
|
||||
"limit": 10,
|
||||
"expiry": 7
|
||||
},
|
||||
method: "get",
|
||||
success: function(data) {
|
||||
resp($.map(data, function(wiki_page) {
|
||||
return {
|
||||
type: "tag",
|
||||
label: wiki_page.title.replace(/_/g, " "),
|
||||
value: wiki_page.title,
|
||||
category: wiki_page.category_name
|
||||
};
|
||||
}));
|
||||
}
|
||||
});
|
||||
Autocomplete.wiki_source(req.term, resp);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Autocomplete.normal_source = function(term, resp) {
|
||||
if (term.length < 3) {
|
||||
return;
|
||||
}
|
||||
return $.ajax({
|
||||
url: "/tags/autocomplete.json",
|
||||
data: {
|
||||
"search[name_matches]": term,
|
||||
"expiry": 7
|
||||
},
|
||||
method: "get",
|
||||
success: function(data) {
|
||||
var d = $.map(data, function(tag) {
|
||||
return {
|
||||
type: "tag",
|
||||
label: tag.name.replace(/_/g, " "),
|
||||
antecedent: tag.antecedent_name,
|
||||
value: tag.name,
|
||||
category: tag.category,
|
||||
source: tag.source,
|
||||
weight: tag.weight,
|
||||
post_count: tag.post_count
|
||||
};
|
||||
});
|
||||
|
||||
resp(d);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Autocomplete.parse_query = function(text, caret) {
|
||||
var metatag = "";
|
||||
var term = "";
|
||||
@ -316,7 +246,7 @@ Autocomplete.on_tab = function(event) {
|
||||
Autocomplete.render_item = function(list, item) {
|
||||
var $link = $("<a/>");
|
||||
$link.text(item.label);
|
||||
$link.attr("href", "/posts?tags=" + encodeURIComponent(item.value));
|
||||
$link.attr("href", Autocomplete.get_href(item));
|
||||
$link.on("click.danbooru", function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
@ -357,7 +287,7 @@ Autocomplete.render_item = function(list, item) {
|
||||
var $menu_item = $("<div/>").append($link);
|
||||
var $list_item = $("<li/>").data("item.autocomplete", item).append($menu_item);
|
||||
|
||||
var data_attributes = ["type", "source", "antecedent", "value", "category", "post_count", "weight"];
|
||||
var data_attributes = ["id", "type", "source", "antecedent", "value", "category", "post_count", "weight"];
|
||||
data_attributes.forEach(attr => {
|
||||
$list_item.attr(`data-autocomplete-${attr.replace(/_/g, "-")}`, item[attr]);
|
||||
});
|
||||
@ -365,6 +295,53 @@ Autocomplete.render_item = function(list, item) {
|
||||
return $list_item.appendTo(list);
|
||||
};
|
||||
|
||||
Autocomplete.get_href = function(item) {
|
||||
switch(item.type) {
|
||||
case "user":
|
||||
return "/users/" + item.id;
|
||||
case "pool":
|
||||
return "/pools/" + item.id;
|
||||
case "artist":
|
||||
return "/artists/" + item.id;
|
||||
case "wiki_page":
|
||||
return "/wiki_pages/" + item.id;
|
||||
case "tag":
|
||||
default:
|
||||
return "/posts?tags=" + encodeURIComponent(item.value);
|
||||
}
|
||||
}
|
||||
|
||||
Autocomplete.normal_source = function(term, resp) {
|
||||
if (term.length < 3) {
|
||||
return;
|
||||
}
|
||||
return $.ajax({
|
||||
url: "/tags/autocomplete.json",
|
||||
data: {
|
||||
"search[name_matches]": term,
|
||||
"expiry": 7
|
||||
},
|
||||
method: "get",
|
||||
success: function(data) {
|
||||
var d = $.map(data, function(tag) {
|
||||
return {
|
||||
id: tag.id,
|
||||
type: "tag",
|
||||
label: tag.name.replace(/_/g, " "),
|
||||
antecedent: tag.antecedent_name,
|
||||
value: tag.name,
|
||||
category: tag.category,
|
||||
source: tag.source,
|
||||
weight: tag.weight,
|
||||
post_count: tag.post_count
|
||||
};
|
||||
});
|
||||
|
||||
resp(d);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Autocomplete.static_metatags = {
|
||||
order: Autocomplete.ORDER_METATAGS,
|
||||
status: [
|
||||
@ -414,6 +391,7 @@ Autocomplete.user_source = function(term, resp, prefix) {
|
||||
success: function(data) {
|
||||
resp($.map(data, function(user) {
|
||||
return {
|
||||
id: user.id,
|
||||
type: "user",
|
||||
label: user.name.replace(/_/, " "),
|
||||
value: prefix + user.name,
|
||||
@ -436,6 +414,7 @@ Autocomplete.pool_source = function(term, resp, metatag) {
|
||||
success: function(data) {
|
||||
resp($.map(data, function(pool) {
|
||||
return {
|
||||
id: pool.id,
|
||||
type: "pool",
|
||||
label: pool.name.replace(/_/g, " "),
|
||||
value: (metatag ? (metatag + ":" + pool.name) : pool.name),
|
||||
@ -447,9 +426,58 @@ Autocomplete.pool_source = function(term, resp, metatag) {
|
||||
});
|
||||
}
|
||||
|
||||
Autocomplete.artist_source = function(term, resp) {
|
||||
return $.ajax({
|
||||
url: "/artists.json",
|
||||
data: {
|
||||
"search[name_like]": term.trim().replace(/\s+/g, "_") + "*",
|
||||
"search[is_active]": true,
|
||||
"search[order]": "post_count",
|
||||
"limit": 10,
|
||||
"expiry": 7
|
||||
},
|
||||
method: "get",
|
||||
success: function(data) {
|
||||
resp($.map(data, function(artist) {
|
||||
return {
|
||||
id: artist.id,
|
||||
type: "artist",
|
||||
label: artist.name.replace(/_/g, " "),
|
||||
value: artist.name,
|
||||
category: Autocomplete.TAG_CATEGORIES.artist,
|
||||
};
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Autocomplete.wiki_source = function(term, resp) {
|
||||
return $.ajax({
|
||||
url: "/wiki_pages.json",
|
||||
data: {
|
||||
"search[title]": term + "*",
|
||||
"search[hide_deleted]": "Yes",
|
||||
"search[order]": "post_count",
|
||||
"limit": 10,
|
||||
"expiry": 7
|
||||
},
|
||||
method: "get",
|
||||
success: function(data) {
|
||||
resp($.map(data, function(wiki_page) {
|
||||
return {
|
||||
id: wiki_page.id,
|
||||
type: "wiki_page",
|
||||
label: wiki_page.title.replace(/_/g, " "),
|
||||
value: wiki_page.title,
|
||||
category: wiki_page.category_name
|
||||
};
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
Autocomplete.initialize_all();
|
||||
});
|
||||
|
||||
export default Autocomplete;
|
||||
|
||||
|
@ -419,7 +419,7 @@ let Note = {
|
||||
$dialog.data("uiDialog")._title = function(title) {
|
||||
title.html(this.options.title); // Allow unescaped html in dialog title
|
||||
}
|
||||
$dialog.dialog("option", "title", 'Edit note #' + id + ' (<a href="/wiki_pages/help:notes">view help</a>)');
|
||||
$dialog.dialog("option", "title", 'Edit note #' + id + ' (<a href="/wiki_pages/e621:notes">view help</a>)');
|
||||
|
||||
$dialog.on("dialogclose.danbooru", function() {
|
||||
Note.editing = false;
|
||||
@ -828,4 +828,3 @@ $(function() {
|
||||
});
|
||||
|
||||
export default Note
|
||||
|
||||
|
@ -57,6 +57,8 @@ PostVersion.tag_script_selected = function() {
|
||||
PostVersion.updated = 0;
|
||||
let selected_rows = $(".post-version-select:checked").parents(".post-version");
|
||||
const script = $("#update-tag-script").val();
|
||||
if(!script)
|
||||
return;
|
||||
|
||||
for (let row of selected_rows) {
|
||||
let id = $(row).data("post-id");
|
||||
|
@ -361,6 +361,14 @@ Post.initialize_links = function() {
|
||||
return;
|
||||
Post.destroy($(e.target).data('pid'));
|
||||
});
|
||||
$("#regenerate-image-samples-link").on('click', e => {
|
||||
e.preventDefault();
|
||||
Post.regenerate_image_samples($(e.target).data('pid'));
|
||||
});
|
||||
$("#regenerate-video-samples-link").on('click', e => {
|
||||
e.preventDefault();
|
||||
Post.regenerate_video_samples($(e.target).data('pid'));
|
||||
});
|
||||
$(".disapprove-post-link").on('click', e => {
|
||||
e.preventDefault();
|
||||
const target = $(e.target);
|
||||
@ -907,6 +915,24 @@ Post.destroy = function(post_id) {
|
||||
});
|
||||
};
|
||||
|
||||
Post.regenerate_image_samples = function(post_id) {
|
||||
$.post(`/moderator/post/posts/${post_id}/regenerate_thumbnails.json`, {}
|
||||
).fail(data => {
|
||||
Utility.error("Error: " + data.responseJSON.reason);
|
||||
}).done(data => {
|
||||
Utility.notice("Image samples regenerated.");
|
||||
});
|
||||
};
|
||||
|
||||
Post.regenerate_video_samples = function(post_id) {
|
||||
$.post(`/moderator/post/posts/${post_id}/regenerate_videos.json`, {}
|
||||
).fail(data => {
|
||||
Utility.error("Error: " + data.responseJSON.reason);
|
||||
}).done(data => {
|
||||
Utility.notice("Video samples will be regenerated in a few minutes.");
|
||||
});
|
||||
};
|
||||
|
||||
Post.approve = function(post_id, should_reload) {
|
||||
Post.notice_update("inc");
|
||||
SendQueue.add(function() {
|
||||
|
145
app/javascript/src/javascripts/replacement_uploader.js
Normal file
145
app/javascript/src/javascripts/replacement_uploader.js
Normal file
@ -0,0 +1,145 @@
|
||||
const Replacer = {};
|
||||
|
||||
const thumbURLs = [
|
||||
"/images/notfound-preview.png",
|
||||
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
|
||||
];
|
||||
const thumbs = {
|
||||
notfound: "/images/notfound-preview.png",
|
||||
none: 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
|
||||
};
|
||||
|
||||
Replacer.old_domain = "";
|
||||
|
||||
Replacer.update_preview_file = function (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
Replacer.set_preview_url(e.target.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
Replacer.update_preview_url = function () {
|
||||
const url = $("#post_replacement_replacement_url").val();
|
||||
if (!url) {
|
||||
Replacer.upload_allow_clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const sampleURL = Replacer.isSampleURL(url);
|
||||
if (sampleURL !== false) {
|
||||
$('#bad_upload_url_reason').text(sampleURL);
|
||||
$('#bad_upload_url').show();
|
||||
return;
|
||||
} else {
|
||||
$('#bad_upload_url').hide();
|
||||
}
|
||||
|
||||
const domain = $("<a>").prop("href", url).prop("hostname");
|
||||
if (domain && domain !== Replacer.old_domain) {
|
||||
$.getJSON("/upload_whitelists/is_allowed.json", {url: url}, function(data) {
|
||||
if(data.domain) {
|
||||
Replacer.upload_allow_set(data.is_allowed, data.domain, data.reason);
|
||||
if(!data.is_allowed)
|
||||
Replacer.set_preview_url(thumbs.none);
|
||||
}
|
||||
});
|
||||
} else if (!domain) {
|
||||
Replacer.upload_allow_clear();
|
||||
}
|
||||
Replacer.old_domain = domain;
|
||||
Replacer.set_preview_url(url);
|
||||
};
|
||||
|
||||
Replacer.update_preview_dims = function () {
|
||||
const img = $('#replacement_preview_img')[0];
|
||||
if (thumbURLs.filter(function (x) {
|
||||
return img.src.indexOf(x) !== -1;
|
||||
}).length !== 0)
|
||||
return;
|
||||
Replacer.set_preview_dims(img.naturalHeight, img.naturalWidth);
|
||||
};
|
||||
|
||||
Replacer.preview_error = function (e) {
|
||||
const img = e.target;
|
||||
Replacer.set_preview_dims(-1, -1);
|
||||
if (thumbURLs.filter(function (x) {
|
||||
return img.src.indexOf(x) !== -1;
|
||||
}).length !== 0)
|
||||
return;
|
||||
Replacer.set_preview_url(thumbs.notfound);
|
||||
};
|
||||
|
||||
Replacer.set_preview_dims = function (height, width) {
|
||||
if (height <= 0 && width <= 0) {
|
||||
$('#replacement_preview_dims').text('');
|
||||
} else {
|
||||
$('#replacement_preview_dims').text(`${width}x${height}`);
|
||||
}
|
||||
};
|
||||
|
||||
Replacer.set_preview_url = function (url) {
|
||||
$('#replacement_preview_img').attr('src', url);
|
||||
};
|
||||
|
||||
|
||||
Replacer.update_preview = function () {
|
||||
const $file = $("#post_replacement_replacement_file")[0];
|
||||
if ($file && $file.files[0])
|
||||
Replacer.update_preview_file($file.files[0]);
|
||||
else
|
||||
Replacer.update_preview_url();
|
||||
}
|
||||
|
||||
Replacer.update_preview_paste = function () {
|
||||
setTimeout(Replacer.update_preview, 150);
|
||||
}
|
||||
|
||||
Replacer.isSampleURL = function (url) {
|
||||
const patterns = [
|
||||
{reason: 'Thumbnail URL', test: /[at]\.facdn\.net/gi},
|
||||
{reason: 'Sample URL', test: /pximg\.net\/img-master/gi},
|
||||
{reason: 'Sample URL', test: /d3gz42uwgl1r1y\.cloudfront\.net\/.*\/\d+x\d+\./gi},
|
||||
{reason: 'Sample URL', test: /pbs\.twimg\.com\/media\/[\w\-_]+\.(jpg|png)(:large)?$/gi},
|
||||
{reason: 'Sample URL', test: /pbs\.twimg\.com\/media\/[\w\-_]+\?format=(jpg|png)(?!&name=orig)/gi},
|
||||
{reason: 'Sample URL', test: /derpicdn\.net\/.*\/large\./gi},
|
||||
{reason: 'Sample URL', test: /metapix\.net\/files\/(preview|screen)\//gi},
|
||||
{reason: 'Sample URL', test: /sofurryfiles\.com\/std\/preview/gi}];
|
||||
for (let i = 0; i < patterns.length; ++i) {
|
||||
const pattern = patterns[i];
|
||||
if (pattern.test.test(url))
|
||||
return pattern.reason;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Replacer.upload_allow_clear = function() {
|
||||
$('#whitelist-warning').hide();
|
||||
}
|
||||
|
||||
Replacer.upload_allow_set = function(allowed, domain, reason) {
|
||||
const classes = ['whitelist-warning-disallowed', 'whitelist-warning-allowed'];
|
||||
$('#whitelist-warning').removeClass().addClass(classes[allowed ? 1 : 0]);
|
||||
$('#whitelist-warning-domain').text(domain);
|
||||
if(allowed)
|
||||
$('#whitelist-warning-not').hide();
|
||||
else
|
||||
$('#whitelist-warning-not').show();
|
||||
$('#whitelist-warning').show();
|
||||
}
|
||||
|
||||
Replacer.init_uploader = function () {
|
||||
$('#replacement_preview_img').on('load', Replacer.update_preview_dims);
|
||||
$('#replacement_preview_img').on('error', Replacer.preview_error);
|
||||
$('#post_replacement_replacement_url').on('keyup', Replacer.update_preview);
|
||||
$('#post_replacement_replacement_file').on('change', Replacer.update_preview);
|
||||
$('#post_replacement_replacement_url').on('paste', Replacer.update_preview_paste);
|
||||
};
|
||||
|
||||
$(function () {
|
||||
if ($("#c-post-replacements > #a-new").length) {
|
||||
Replacer.init_uploader();
|
||||
}
|
||||
});
|
||||
|
||||
export default Replacer;
|
22
app/javascript/src/javascripts/user_warning.js
Normal file
22
app/javascript/src/javascripts/user_warning.js
Normal file
@ -0,0 +1,22 @@
|
||||
import Utility from './utility.js';
|
||||
|
||||
$(() => {
|
||||
$('.item-mark-user-warned').on('click', function(evt) {
|
||||
const target = $(evt.target);
|
||||
const type = target.data('item-type');
|
||||
const id = target.data('item-id');
|
||||
const record_type = target.data('record-type');
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: `/${type}s/${id}/warning.json`,
|
||||
data: {
|
||||
'record_type': record_type
|
||||
},
|
||||
}).done(function(data) {
|
||||
location.reload();
|
||||
}).fail(function(data) {
|
||||
Utility.error("Failed to mark as warned.");
|
||||
});
|
||||
});
|
||||
});
|
@ -58,10 +58,12 @@
|
||||
@import "specific/post_flags.scss";
|
||||
@import "specific/post_mode_menu.scss";
|
||||
@import "specific/posts.scss";
|
||||
@import "specific/post_replacements.scss";
|
||||
@import "specific/post_versions.scss";
|
||||
@import "specific/related_tags.scss";
|
||||
@import "specific/sessions.scss";
|
||||
@import "specific/site_map.scss";
|
||||
@import "specific/staff_notes.scss";
|
||||
@import "specific/tags.scss";
|
||||
@import "specific/takedowns.scss";
|
||||
@import "specific/terms_of_service.scss";
|
||||
@ -69,6 +71,7 @@
|
||||
@import "specific/user_deletions.scss";
|
||||
@import "specific/user_feedback.scss";
|
||||
@import "specific/user_name_change_requests.scss";
|
||||
@import "specific/user_warned.scss";
|
||||
@import "specific/users.scss";
|
||||
@import "specific/wiki_pages.scss";
|
||||
@import "specific/wiki_page_versions.scss";
|
||||
|
@ -0,0 +1,7 @@
|
||||
div#c-post-replacements {
|
||||
.replacement-pending-row {
|
||||
@include themable {
|
||||
background-color: darken( themed("color-danger"), 10%);
|
||||
}
|
||||
}
|
||||
}
|
5
app/javascript/src/styles/specific/staff_notes.scss
Normal file
5
app/javascript/src/styles/specific/staff_notes.scss
Normal file
@ -0,0 +1,5 @@
|
||||
.staff-note-list {
|
||||
.staff-note {
|
||||
margin-bottom: $padding-050;
|
||||
}
|
||||
}
|
3
app/javascript/src/styles/specific/user_warned.scss
Normal file
3
app/javascript/src/styles/specific/user_warned.scss
Normal file
@ -0,0 +1,3 @@
|
||||
.user-warning em {
|
||||
@include themable { color: themed("color-rating-explicit"); }
|
||||
}
|
@ -88,8 +88,8 @@ module Danbooru
|
||||
case key
|
||||
when :limit
|
||||
limit = @paginator_options.try(:[], :limit) || Danbooru.config.posts_per_page
|
||||
if limit.to_i > 1_000
|
||||
limit = 1_000
|
||||
if limit.to_i > 320
|
||||
limit = 320
|
||||
end
|
||||
limit
|
||||
|
||||
|
@ -148,8 +148,8 @@ module Danbooru
|
||||
case key
|
||||
when :limit
|
||||
limit = @paginator_options.try(:[], :limit) || Danbooru.config.posts_per_page
|
||||
if limit.to_i > 1_000
|
||||
limit = 1_000
|
||||
if limit.to_i > 320
|
||||
limit = 320
|
||||
end
|
||||
limit
|
||||
|
||||
|
@ -5,9 +5,11 @@ module DanbooruImageResizer
|
||||
SRGB_PROFILE = "#{Rails.root}/config/sRGB.icm"
|
||||
# http://jcupitt.github.io/libvips/API/current/libvips-resample.html#vips-thumbnail
|
||||
THUMBNAIL_OPTIONS = { size: :down, linear: false, no_rotate: true, export_profile: SRGB_PROFILE, import_profile: SRGB_PROFILE }
|
||||
THUMBNAIL_OPTIONS_NO_ICC = { size: :down, linear: false, no_rotate: true, export_profile: SRGB_PROFILE }
|
||||
# http://jcupitt.github.io/libvips/API/current/VipsForeignSave.html#vips-jpegsave
|
||||
JPEG_OPTIONS = { background: 0, strip: true, interlace: true, optimize_coding: true }
|
||||
CROP_OPTIONS = { linear: false, no_rotate: true, export_profile: SRGB_PROFILE, import_profile: SRGB_PROFILE, crop: :attention }
|
||||
CROP_OPTIONS_NO_ICC = { linear: false, no_rotate: true, export_profile: SRGB_PROFILE, crop: :attention }
|
||||
|
||||
# XXX libvips-8.4 on Debian doesn't support the `Vips::Image.thumbnail` method.
|
||||
# On 8.4 we have to shell out to vipsthumbnail instead. Remove when Debian supports 8.5.
|
||||
@ -31,7 +33,12 @@ module DanbooruImageResizer
|
||||
# http://jcupitt.github.io/libvips/API/current/Using-vipsthumbnail.md.html
|
||||
def resize_ruby(file, width, height, resize_quality)
|
||||
output_file = Tempfile.new
|
||||
resized_image = Vips::Image.thumbnail(file.path, width, height: height, **THUMBNAIL_OPTIONS)
|
||||
begin
|
||||
resized_image = Vips::Image.thumbnail(file.path, width, height: height, **THUMBNAIL_OPTIONS)
|
||||
rescue Vips::Error => e
|
||||
raise e unless e.message =~ /icc_transform/i
|
||||
resized_image = Vips::Image.thumbnail(file.path, width, height: height, **THUMBNAIL_OPTIONS_NO_ICC)
|
||||
end
|
||||
resized_image.jpegsave(output_file.path, Q: resize_quality, **JPEG_OPTIONS)
|
||||
|
||||
output_file
|
||||
@ -41,7 +48,12 @@ module DanbooruImageResizer
|
||||
return nil unless Danbooru.config.enable_image_cropping
|
||||
|
||||
output_file = Tempfile.new
|
||||
resized_image = Vips::Image.thumbnail(file.path, width, height: height, **CROP_OPTIONS)
|
||||
begin
|
||||
resized_image = Vips::Image.thumbnail(file.path, width, height: height, **CROP_OPTIONS)
|
||||
rescue Vips::Error => e
|
||||
raise e unless e.message =~ /icc_transform/i
|
||||
resized_image = Vips::Image.thumbnail(file.path, width, height: height, **CROP_OPTIONS_NO_ICC)
|
||||
end
|
||||
resized_image.jpegsave(output_file.path, Q: resize_quality, **JPEG_OPTIONS)
|
||||
|
||||
output_file
|
||||
@ -94,19 +106,11 @@ module DanbooruImageResizer
|
||||
output_file
|
||||
end
|
||||
|
||||
def validate_shell(file)
|
||||
temp = Tempfile.new("validate")
|
||||
output, status = Open3.capture2e("vips", "stats", file.path, "#{temp.path}.v")
|
||||
|
||||
# png | jpeg | gif
|
||||
if output =~ /Read Error|Premature end of JPEG file|Failed to read from given file/m
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
|
||||
ensure
|
||||
temp.close
|
||||
temp.unlink
|
||||
def is_corrupt?(filename)
|
||||
image = Vips::Image.new_from_file(filename, fail: true)
|
||||
image.stats
|
||||
false
|
||||
rescue
|
||||
true
|
||||
end
|
||||
end
|
||||
|
@ -324,7 +324,7 @@ class ElasticPostQueryBuilder
|
||||
end
|
||||
|
||||
if q[:note_neg]
|
||||
must_not.push({match: {notes: q[:note]}})
|
||||
must_not.push({match: {notes: q[:note_neg]}})
|
||||
end
|
||||
|
||||
if q[:delreason]
|
||||
|
@ -3,10 +3,10 @@ module PostSets
|
||||
class Favorites < PostSets::Base
|
||||
attr_reader :page, :limit
|
||||
|
||||
def initialize(user, page)
|
||||
def initialize(user, page = 1, limit = CurrentUser.per_page)
|
||||
@user = user
|
||||
@page = page
|
||||
@limit = CurrentUser.per_page
|
||||
@limit = limit || CurrentUser.per_page
|
||||
end
|
||||
|
||||
def public_tag_string
|
||||
|
61
app/logical/post_thumbnailer.rb
Normal file
61
app/logical/post_thumbnailer.rb
Normal file
@ -0,0 +1,61 @@
|
||||
module PostThumbnailer
|
||||
extend self
|
||||
def generate_resizes(file, height, width, type)
|
||||
if type == :video
|
||||
video = FFMPEG::Movie.new(file.path)
|
||||
crop_file = generate_video_crop_for(video, Danbooru.config.small_image_width)
|
||||
preview_file = generate_video_preview_for(file.path, Danbooru.config.small_image_width)
|
||||
sample_file = generate_video_sample_for(file.path)
|
||||
elsif type == :image
|
||||
preview_file = DanbooruImageResizer.resize(file, Danbooru.config.small_image_width, Danbooru.config.small_image_width, 87)
|
||||
crop_file = DanbooruImageResizer.crop(file, Danbooru.config.small_image_width, Danbooru.config.small_image_width, 87)
|
||||
if width > Danbooru.config.large_image_width
|
||||
sample_file = DanbooruImageResizer.resize(file, Danbooru.config.large_image_width, height, 87)
|
||||
end
|
||||
end
|
||||
|
||||
[preview_file, crop_file, sample_file]
|
||||
end
|
||||
|
||||
def generate_thumbnail(file, type)
|
||||
if type == :video
|
||||
preview_file = generate_video_preview_for(file.path, Danbooru.config.small_image_width)
|
||||
elsif type == :image
|
||||
preview_file = DanbooruImageResizer.resize(file, Danbooru.config.small_image_width, Danbooru.config.small_image_width, 87)
|
||||
end
|
||||
|
||||
preview_file
|
||||
end
|
||||
|
||||
def generate_video_crop_for(video, width)
|
||||
vp = Tempfile.new(["video-preview", ".jpg"], binmode: true)
|
||||
video.screenshot(vp.path, {:seek_time => 0, :resolution => "#{video.width}x#{video.height}"})
|
||||
crop = DanbooruImageResizer.crop(vp, width, width, 87)
|
||||
vp.close
|
||||
return crop
|
||||
end
|
||||
|
||||
def generate_video_preview_for(video, width)
|
||||
output_file = Tempfile.new(["video-preview", ".jpg"], binmode: true)
|
||||
stdout, stderr, status = Open3.capture3(Danbooru.config.ffmpeg_path, '-y', '-i', video, '-vf', "thumbnail,scale=#{width}:-1", '-frames:v', '1', output_file.path)
|
||||
|
||||
unless status == 0
|
||||
Rails.logger.warn("[FFMPEG PREVIEW STDOUT] #{stdout.chomp!}")
|
||||
Rails.logger.warn("[FFMPEG PREVIEW STDERR] #{stderr.chomp!}")
|
||||
raise CorruptFileError.new("could not generate thumbnail")
|
||||
end
|
||||
output_file
|
||||
end
|
||||
|
||||
def generate_video_sample_for(video)
|
||||
output_file = Tempfile.new(["video-sample", ".jpg"], binmode: true)
|
||||
stdout, stderr, status = Open3.capture3(Danbooru.config.ffmpeg_path, '-y', '-i', video, '-vf', 'thumbnail', '-frames:v', '1', output_file.path)
|
||||
|
||||
unless status == 0
|
||||
Rails.logger.warn("[FFMPEG SAMPLE STDOUT] #{stdout.chomp!}")
|
||||
Rails.logger.warn("[FFMPEG SAMPLE STDERR] #{stderr.chomp!}")
|
||||
raise CorruptFileError.new("could not generate sample")
|
||||
end
|
||||
output_file
|
||||
end
|
||||
end
|
3
app/logical/processing_error.rb
Normal file
3
app/logical/processing_error.rb
Normal file
@ -0,0 +1,3 @@
|
||||
class ProcessingError < Exception
|
||||
|
||||
end
|
@ -4,15 +4,17 @@ class StorageManager
|
||||
DEFAULT_BASE_DIR = "#{Rails.root}/public/data"
|
||||
IMAGE_TYPES = %i[preview large crop original]
|
||||
|
||||
attr_reader :base_url, :base_dir, :hierarchical, :large_image_prefix, :protected_prefix, :base_path
|
||||
attr_reader :base_url, :base_dir, :hierarchical, :large_image_prefix, :protected_prefix, :base_path, :replacement_prefix
|
||||
|
||||
def initialize(base_url: default_base_url, base_path: default_base_path, base_dir: DEFAULT_BASE_DIR, hierarchical: false,
|
||||
large_image_prefix: Danbooru.config.large_image_prefix,
|
||||
protected_prefix: Danbooru.config.protected_path_prefix)
|
||||
protected_prefix: Danbooru.config.protected_path_prefix,
|
||||
replacement_prefix: Danbooru.config.replacement_path_prefix)
|
||||
@base_url = base_url.chomp("/")
|
||||
@base_dir = base_dir
|
||||
@base_path = base_path
|
||||
@protected_prefix = protected_prefix
|
||||
@replacement_prefix = replacement_prefix
|
||||
@hierarchical = hierarchical
|
||||
@large_image_prefix = large_image_prefix
|
||||
end
|
||||
@ -48,9 +50,13 @@ class StorageManager
|
||||
store(io, file_path(post.md5, post.file_ext, type))
|
||||
end
|
||||
|
||||
def delete_file(post_id, md5, file_ext, type)
|
||||
delete(file_path(md5, file_ext, type))
|
||||
delete(file_path(md5, file_ext, type, true))
|
||||
def store_replacement(io, replacement, image_size)
|
||||
store(io, replacement_path(replacement.storage_id, replacement.file_ext, image_size))
|
||||
end
|
||||
|
||||
def delete_file(post_id, md5, file_ext, type, scale_factor: nil)
|
||||
delete(file_path(md5, file_ext, type, scale_factor: scale_factor))
|
||||
delete(file_path(md5, file_ext, type, true, scale_factor: scale_factor))
|
||||
end
|
||||
|
||||
def delete_post_files(post_or_md5, file_ext)
|
||||
@ -69,6 +75,11 @@ class StorageManager
|
||||
delete(file_path(md5, 'mp4', :original, true))
|
||||
end
|
||||
|
||||
def delete_replacement(replacement)
|
||||
delete(replacement_path(replacement.storage_id, replacement.file_ext, :original))
|
||||
delete(replacement_path(replacement.storage_id, replacement.file_ext, :preview))
|
||||
end
|
||||
|
||||
def open_file(post, type)
|
||||
open(file_path(post.md5, post.file_ext, type))
|
||||
end
|
||||
@ -81,11 +92,15 @@ class StorageManager
|
||||
raise NotImplementedError, "move_file_undelete not implemented"
|
||||
end
|
||||
|
||||
def protected_params(url, post)
|
||||
def move_file_replacement(post, replacement, direction)
|
||||
raise NotImplementedError, "move_file_replacement not implemented"
|
||||
end
|
||||
|
||||
def protected_params(url, post, secret: Danbooru.config.protected_file_secret)
|
||||
user_id = CurrentUser.id
|
||||
ip = CurrentUser.ip_addr
|
||||
time = (Time.now + 15.minute).to_i
|
||||
secret = Danbooru.config.protected_file_secret
|
||||
secret = secret
|
||||
hmac = Digest::MD5.base64digest("#{time} #{url} #{user_id} #{secret}").tr("+/","-_").gsub("==",'')
|
||||
"?auth=#{hmac}&expires=#{time}&uid=#{user_id}"
|
||||
end
|
||||
@ -118,6 +133,14 @@ class StorageManager
|
||||
file_url_ext(post, type, post.file_ext)
|
||||
end
|
||||
|
||||
def replacement_url(replacement, image_size = :original)
|
||||
subdir = subdir_for(replacement.storage_id)
|
||||
file = "#{replacement.storage_id}#{'_thumb' if image_size == :preview}.#{replacement.file_ext}"
|
||||
base = "#{base_path}/#{replacement_prefix}"
|
||||
path = "#{base}/#{subdir}#{file}"
|
||||
"#{base_url}#{path}#{protected_params(path, nil, secret: Danbooru.config.replacement_file_secret)}"
|
||||
end
|
||||
|
||||
def root_url
|
||||
origin = Addressable::URI.parse(base_url).origin
|
||||
origin = "" if origin == "null" # base_url was relative
|
||||
@ -161,6 +184,13 @@ class StorageManager
|
||||
end
|
||||
end
|
||||
|
||||
def replacement_path(replacement_or_storage_id, file_ext, image_size)
|
||||
storage_id = replacement_or_storage_id.is_a?(String) ? replacement_or_storage_id : replacement_or_storage_id.storage_id
|
||||
subdir = subdir_for(storage_id)
|
||||
file = "#{storage_id}#{'_thumb' if image_size == :preview}.#{file_ext}"
|
||||
"#{base_dir}/#{replacement_prefix}/#{subdir}#{file}"
|
||||
end
|
||||
|
||||
def subdir_for(md5)
|
||||
hierarchical ? "#{md5[0..1]}/#{md5[2..3]}/" : ""
|
||||
end
|
||||
|
@ -64,6 +64,14 @@ class StorageManager::Local < StorageManager
|
||||
move_file(path, new_path)
|
||||
end
|
||||
|
||||
def move_file_replacement(post, replacement, direction)
|
||||
if direction == :to_replacement
|
||||
move_file(file_path(post, post.file_ext, :original), replacement_path(replacement, replacement.file_ext))
|
||||
else
|
||||
move_file(replacement_path(replacement, replacement.file_ext), file_path(post, post.file_ext, :original))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def move_file(old_path, new_path)
|
||||
|
@ -1,115 +0,0 @@
|
||||
module TagAutocomplete
|
||||
extend self
|
||||
|
||||
PREFIX_BOUNDARIES = "(_/:;-"
|
||||
LIMIT = 10
|
||||
|
||||
class Result < Struct.new(:name, :post_count, :category, :antecedent_name, :source)
|
||||
include ActiveModel::Serializers::JSON
|
||||
include ActiveModel::Serializers::Xml
|
||||
|
||||
def attributes
|
||||
(members + [:weight]).map { |x| [x.to_s, send(x)] }.to_h
|
||||
end
|
||||
|
||||
def weight
|
||||
case source
|
||||
when :exact then 1.0
|
||||
when :prefix then 0.8
|
||||
when :alias then 0.2
|
||||
when :correct then 0.1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def search(query)
|
||||
query = Tag.normalize_name(query)
|
||||
|
||||
candidates = count_sort(
|
||||
query,
|
||||
search_exact(query, 8) +
|
||||
search_prefix(query, 4) +
|
||||
search_correct(query, 2) +
|
||||
search_aliases(query, 3)
|
||||
)
|
||||
end
|
||||
|
||||
def count_sort(query, words)
|
||||
words.uniq(&:name).sort_by do |x|
|
||||
x.post_count * x.weight
|
||||
end.reverse.slice(0, LIMIT)
|
||||
end
|
||||
|
||||
def search_exact(query, n=4)
|
||||
Tag
|
||||
.where("name like ? escape e'\\\\'", query.to_escaped_for_sql_like + "%")
|
||||
.where("post_count > 0")
|
||||
.order("post_count desc")
|
||||
.limit(n)
|
||||
.pluck(:name, :post_count, :category)
|
||||
.map {|row| Result.new(*row, nil, :exact)}
|
||||
end
|
||||
|
||||
def search_correct(query, n=2)
|
||||
if query.size <= 3
|
||||
return []
|
||||
end
|
||||
|
||||
Tag
|
||||
.where("name % ?", query)
|
||||
.where("abs(length(name) - ?) <= 3", query.size)
|
||||
.where("name like ? escape E'\\\\'", query[0].to_escaped_for_sql_like + '%')
|
||||
.where("post_count > 0")
|
||||
.order(Arel.sql("similarity(name, #{Tag.connection.quote(query)}) DESC"))
|
||||
.limit(n)
|
||||
.pluck(:name, :post_count, :category)
|
||||
.map {|row| Result.new(*row, nil, :correct)}
|
||||
end
|
||||
|
||||
def search_prefix(query, n=3)
|
||||
if query.size >= 5
|
||||
return []
|
||||
end
|
||||
|
||||
if query.size <= 1
|
||||
return []
|
||||
end
|
||||
|
||||
if query =~ /[-_()]/
|
||||
return []
|
||||
end
|
||||
|
||||
if query.size >= 3
|
||||
min_post_count = 0
|
||||
else
|
||||
min_post_count = 5_000
|
||||
n += 2
|
||||
end
|
||||
|
||||
regexp = "([a-z0-9])[a-z0-9']*($|[^a-z0-9']+)"
|
||||
Tag
|
||||
.where('regexp_replace(name, ?, ?, ?) like ?', regexp, '\1', 'g', query.to_escaped_for_sql_like + '%')
|
||||
.where("post_count > ?", min_post_count)
|
||||
.where("post_count > 0")
|
||||
.order("post_count desc")
|
||||
.limit(n)
|
||||
.pluck(:name, :post_count, :category)
|
||||
.map {|row| Result.new(*row, nil, :prefix)}
|
||||
end
|
||||
|
||||
def search_aliases(query, n=10)
|
||||
wildcard_name = query + "*"
|
||||
TagAlias
|
||||
.select("tags.name, tags.post_count, tags.category, tag_aliases.antecedent_name")
|
||||
.joins("INNER JOIN tags ON tags.name = tag_aliases.consequent_name")
|
||||
.where("tag_aliases.antecedent_name LIKE ? ESCAPE E'\\\\'", wildcard_name.to_escaped_for_sql_like)
|
||||
.active
|
||||
.where("tags.name NOT LIKE ? ESCAPE E'\\\\'", wildcard_name.to_escaped_for_sql_like)
|
||||
.where("tag_aliases.post_count > 0")
|
||||
.order("tag_aliases.post_count desc")
|
||||
.limit(n)
|
||||
.pluck(:name, :post_count, :category, :antecedent_name)
|
||||
.map {|row| Result.new(*row, :alias)}
|
||||
end
|
||||
end
|
||||
|
@ -1,8 +1,6 @@
|
||||
class UploadService
|
||||
class Replacer
|
||||
extend Memoist
|
||||
class Error < Exception;
|
||||
end
|
||||
|
||||
attr_reader :post, :replacement
|
||||
|
||||
@ -11,90 +9,54 @@ class UploadService
|
||||
@replacement = replacement
|
||||
end
|
||||
|
||||
def comment_replacement_message(post, replacement)
|
||||
%("#{replacement.creator.name}":[/users/#{replacement.creator.id}] replaced this post with a new image:\n\n#{replacement_message(post, replacement)})
|
||||
end
|
||||
|
||||
def replacement_message(post, replacement)
|
||||
linked_source = linked_source(replacement.replacement_url)
|
||||
linked_source_was = linked_source(post.source_was)
|
||||
|
||||
<<-EOS.strip_heredoc
|
||||
[table]
|
||||
[tbody]
|
||||
[tr]
|
||||
[th]Old[/th]
|
||||
[td]#{linked_source_was}[/td]
|
||||
[td]#{post.md5_was}[/td]
|
||||
[td]#{post.file_ext_was}[/td]
|
||||
[td]#{post.image_width_was} x #{post.image_height_was}[/td]
|
||||
[td]#{post.file_size_was.to_s(:human_size, precision: 4)}[/td]
|
||||
[/tr]
|
||||
[tr]
|
||||
[th]New[/th]
|
||||
[td]#{linked_source}[/td]
|
||||
[td]#{post.md5}[/td]
|
||||
[td]#{post.file_ext}[/td]
|
||||
[td]#{post.image_width} x #{post.image_height}[/td]
|
||||
[td]#{post.file_size.to_s(:human_size, precision: 4)}[/td]
|
||||
[/tr]
|
||||
[/tbody]
|
||||
[/table]
|
||||
EOS
|
||||
end
|
||||
|
||||
def linked_source(source)
|
||||
return nil if source.nil?
|
||||
|
||||
# truncate long sources in the middle: "www.pixiv.net...lust_id=23264933"
|
||||
truncated_source = source.gsub(%r{\Ahttps?://}, "").truncate(64, omission: "...#{source.last(32)}")
|
||||
|
||||
if source =~ %r{\Ahttps?://}i
|
||||
%("#{truncated_source}":[#{source}])
|
||||
else
|
||||
truncated_source
|
||||
end
|
||||
end
|
||||
|
||||
def undo!
|
||||
undo_replacement = post.replacements.create(replacement_url: replacement.original_url)
|
||||
undoer = Replacer.new(post: post, replacement: undo_replacement)
|
||||
undoer.process!
|
||||
end
|
||||
|
||||
def source_strategy(upload)
|
||||
return Sources::Strategies.find(upload.source, upload.referer_url)
|
||||
end
|
||||
|
||||
def find_replacement_url(repl, upload)
|
||||
if repl.replacement_file.present?
|
||||
return "file://#{repl.replacement_file.original_filename}"
|
||||
return "file://#{repl.file_name}"
|
||||
end
|
||||
|
||||
if !upload.source.present?
|
||||
raise "No source found in upload for replacement"
|
||||
end
|
||||
|
||||
if source_strategy(upload).canonical_url.present?
|
||||
return source_strategy(upload).canonical_url
|
||||
raise ProcessingError "No source found in upload for replacement"
|
||||
end
|
||||
|
||||
return upload.source
|
||||
end
|
||||
|
||||
def create_backup_replacement
|
||||
begin
|
||||
repl = post.replacements.new(creator_id: post.uploader_id, creator_ip_addr: post.uploader_ip_addr, status: 'original',
|
||||
image_width: post.image_width, image_height: post.image_height, file_ext: post.file_ext,
|
||||
file_size: post.file_size, md5: post.md5, file_name: "#{post.md5}.#{post.file_ext}",
|
||||
source: post.source, reason: 'Backup of original file', is_backup: true)
|
||||
repl.replacement_file = Danbooru.config.storage_manager.open(Danbooru.config.storage_manager.file_path(post, post.file_ext, :original))
|
||||
repl.save
|
||||
rescue Exception => e
|
||||
raise ProcessingError, "Failed to create post file backup: #{e.message}"
|
||||
end
|
||||
raise ProcessingError, "Could not create post file backup?" if !repl.valid?
|
||||
end
|
||||
|
||||
def process!
|
||||
# Prevent trying to replace deleted posts
|
||||
raise ProcessingError, "Cannot replace post: post is deleted." if post.is_deleted?
|
||||
|
||||
create_backup_replacement
|
||||
replacement.replacement_file = Danbooru.config.storage_manager.open(Danbooru.config.storage_manager.replacement_path(replacement, replacement.file_ext, :original))
|
||||
|
||||
upload = Upload.create(
|
||||
uploader_id: CurrentUser.id,
|
||||
uploader_ip_addr: CurrentUser.ip_addr,
|
||||
rating: post.rating,
|
||||
tag_string: replacement.tags,
|
||||
source: replacement.replacement_url,
|
||||
tag_string: post.tag_string,
|
||||
source: replacement.source,
|
||||
file: replacement.replacement_file,
|
||||
replaced_post: post,
|
||||
original_post_id: post.id
|
||||
original_post_id: post.id,
|
||||
replacement_id: replacement.id
|
||||
)
|
||||
|
||||
begin
|
||||
if upload.invalid? || upload.is_errored?
|
||||
raise Error, upload.status
|
||||
raise ProcessingError, upload.status
|
||||
end
|
||||
|
||||
upload.update(status: "processing")
|
||||
@ -105,41 +67,30 @@ class UploadService
|
||||
upload.save!
|
||||
rescue Exception => x
|
||||
upload.update(status: "error: #{x.class} - #{x.message}", backtrace: x.backtrace.join("\n"))
|
||||
raise Error, upload.status
|
||||
raise ProcessingError, upload.status
|
||||
end
|
||||
md5_changed = upload.md5 != post.md5
|
||||
|
||||
replacement.replacement_url = find_replacement_url(replacement, upload)
|
||||
|
||||
if md5_changed
|
||||
post.queue_delete_files(PostReplacement::DELETION_GRACE_PERIOD)
|
||||
post.delete_files
|
||||
post.generated_samples = nil
|
||||
end
|
||||
|
||||
replacement.file_ext = upload.file_ext
|
||||
replacement.file_size = upload.file_size
|
||||
replacement.image_height = upload.image_height
|
||||
replacement.image_width = upload.image_width
|
||||
replacement.md5 = upload.md5
|
||||
|
||||
post.md5 = upload.md5
|
||||
post.file_ext = upload.file_ext
|
||||
post.image_width = upload.image_width
|
||||
post.image_height = upload.image_height
|
||||
post.file_size = upload.file_size
|
||||
post.source = replacement.final_source.presence || replacement.replacement_url
|
||||
post.source = "#{replacement.source}\n" + post.source
|
||||
post.tag_string = upload.tag_string
|
||||
# Reset ownership information on post.
|
||||
post.uploader_id = replacement.creator_id
|
||||
post.uploader_ip_addr = replacement.creator_ip_addr
|
||||
|
||||
rescale_notes(post)
|
||||
update_ugoira_frame_data(post, upload)
|
||||
|
||||
if md5_changed
|
||||
CurrentUser.as(User.system) do
|
||||
post.comments.create!(body: comment_replacement_message(post, replacement), do_not_bump_post: true)
|
||||
end
|
||||
end
|
||||
|
||||
replacement.save!
|
||||
replacement.update({status: 'approved', approver_id: CurrentUser.id})
|
||||
post.save!
|
||||
|
||||
if post.is_video?
|
||||
|
@ -3,6 +3,8 @@ class UploadService
|
||||
extend self
|
||||
class CorruptFileError < RuntimeError; end
|
||||
|
||||
IMAGE_TYPES = %i[original large preview crop]
|
||||
|
||||
def file_header_to_file_ext(file)
|
||||
case File.read(file.path, 16)
|
||||
when /^\xff\xd8/n
|
||||
@ -36,7 +38,7 @@ class UploadService
|
||||
Upload.find(upload_id).update(status: "preprocessed + deleted")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Danbooru.config.storage_manager.delete_post_files(md5, file_ext)
|
||||
end
|
||||
|
||||
@ -86,58 +88,14 @@ class UploadService
|
||||
end
|
||||
|
||||
def generate_resizes(file, upload)
|
||||
if upload.is_video?
|
||||
video = FFMPEG::Movie.new(file.path)
|
||||
crop_file = generate_video_crop_for(video, Danbooru.config.small_image_width)
|
||||
preview_file = generate_video_preview_for(file.path, Danbooru.config.small_image_width)
|
||||
sample_file = generate_video_sample_for(file.path)
|
||||
|
||||
elsif upload.is_ugoira?
|
||||
if upload.is_ugoira?
|
||||
preview_file = PixivUgoiraConverter.generate_preview(file)
|
||||
crop_file = PixivUgoiraConverter.generate_crop(file)
|
||||
sample_file = PixivUgoiraConverter.generate_webm(file, upload.context["ugoira"]["frame_data"])
|
||||
|
||||
elsif upload.is_image?
|
||||
preview_file = DanbooruImageResizer.resize(file, Danbooru.config.small_image_width, Danbooru.config.small_image_width, 85)
|
||||
crop_file = DanbooruImageResizer.crop(file, Danbooru.config.small_image_width, Danbooru.config.small_image_width, 85)
|
||||
if upload.image_width > Danbooru.config.large_image_width
|
||||
sample_file = DanbooruImageResizer.resize(file, Danbooru.config.large_image_width, upload.image_height, 90)
|
||||
end
|
||||
[preview_file, crop_file, sample_file]
|
||||
else
|
||||
PostThumbnailer.generate_resizes(file, upload.image_height, upload.image_width, upload.is_video? ? :video : :image)
|
||||
end
|
||||
|
||||
[preview_file, crop_file, sample_file]
|
||||
end
|
||||
|
||||
def generate_video_crop_for(video, width)
|
||||
vp = Tempfile.new(["video-preview", ".jpg"], binmode: true)
|
||||
video.screenshot(vp.path, {:seek_time => 0, :resolution => "#{video.width}x#{video.height}"})
|
||||
crop = DanbooruImageResizer.crop(vp, width, width, 85)
|
||||
vp.close
|
||||
return crop
|
||||
end
|
||||
|
||||
def generate_video_preview_for(video, width)
|
||||
output_file = Tempfile.new(["video-preview", ".jpg"], binmode: true)
|
||||
stdout, stderr, status = Open3.capture3(Danbooru.config.ffmpeg_path, '-y', '-i', video, '-vf', "thumbnail,scale=#{width}:-1", '-frames:v', '1', output_file.path)
|
||||
|
||||
unless status == 0
|
||||
Rails.logger.warn("[FFMPEG PREVIEW STDOUT] #{stdout.chomp!}")
|
||||
Rails.logger.warn("[FFMPEG PREVIEW STDERR] #{stderr.chomp!}")
|
||||
raise CorruptFileError.new("could not generate thumbnail")
|
||||
end
|
||||
output_file
|
||||
end
|
||||
|
||||
def generate_video_sample_for(video)
|
||||
output_file = Tempfile.new(["video-sample", ".jpg"], binmode: true)
|
||||
stdout, stderr, status = Open3.capture3(Danbooru.config.ffmpeg_path, '-y', '-i', video, '-vf', 'thumbnail', '-frames:v', '1', output_file.path)
|
||||
|
||||
unless status == 0
|
||||
Rails.logger.warn("[FFMPEG SAMPLE STDOUT] #{stdout.chomp!}")
|
||||
Rails.logger.warn("[FFMPEG SAMPLE STDERR] #{stderr.chomp!}")
|
||||
raise CorruptFileError.new("could not generate sample")
|
||||
end
|
||||
output_file
|
||||
end
|
||||
|
||||
def process_file(upload, file, original_post_id: nil)
|
||||
@ -214,24 +172,8 @@ class UploadService
|
||||
return file if file.present?
|
||||
raise RuntimeError, "No file or source URL provided" if upload.direct_url_parsed.blank?
|
||||
|
||||
attempts = 0
|
||||
|
||||
begin
|
||||
download = Downloads::File.new(upload.direct_url_parsed, upload.referer_url)
|
||||
file, strategy = download.download!
|
||||
|
||||
if !DanbooruImageResizer.validate_shell(file)
|
||||
raise CorruptFileError.new("File is corrupted")
|
||||
end
|
||||
|
||||
rescue
|
||||
if attempts == 3
|
||||
raise
|
||||
end
|
||||
|
||||
attempts += 1
|
||||
retry
|
||||
end
|
||||
download = Downloads::File.new(upload.direct_url_parsed, upload.referer_url)
|
||||
file, strategy = download.download!
|
||||
|
||||
if download.data[:ugoira_frame_data].present?
|
||||
upload.context = {
|
||||
|
@ -1,4 +1,5 @@
|
||||
class Blip < ApplicationRecord
|
||||
include UserWarnable
|
||||
simple_versioning
|
||||
belongs_to_creator
|
||||
user_status_counter :blip_count
|
||||
@ -48,6 +49,7 @@ class Blip < ApplicationRecord
|
||||
end
|
||||
|
||||
def can_edit?(user)
|
||||
return false if was_warned? && !user.is_moderator?
|
||||
(creator_id == user.id && created_at > 5.minutes.ago) || user.is_moderator?
|
||||
end
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
class Comment < ApplicationRecord
|
||||
|
||||
include UserWarnable
|
||||
simple_versioning
|
||||
belongs_to_creator
|
||||
belongs_to_updater
|
||||
@ -168,6 +168,7 @@ class Comment < ApplicationRecord
|
||||
end
|
||||
|
||||
def editable_by?(user)
|
||||
return false if was_warned? && !user.is_moderator?
|
||||
creator_id == user.id || user.is_moderator?
|
||||
end
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
class ForumPost < ApplicationRecord
|
||||
|
||||
include UserWarnable
|
||||
simple_versioning
|
||||
attr_readonly :topic_id
|
||||
belongs_to_creator
|
||||
@ -183,6 +183,7 @@ class ForumPost < ApplicationRecord
|
||||
end
|
||||
|
||||
def editable_by?(user)
|
||||
return false if was_warned? && !user.is_moderator?
|
||||
(creator_id == user.id || user.is_moderator?) && visible?(user)
|
||||
end
|
||||
|
||||
|
@ -95,6 +95,9 @@ class ModAction < ApplicationRecord
|
||||
:post_undelete,
|
||||
:post_unapprove,
|
||||
:post_rating_lock,
|
||||
:post_replacement_accept,
|
||||
:post_replacement_reject,
|
||||
:post_replacement_delete,
|
||||
:report_reason_create,
|
||||
:report_reason_delete,
|
||||
:report_reason_update,
|
||||
|
@ -65,6 +65,8 @@ class Post < ApplicationRecord
|
||||
|
||||
has_many :versions, -> {order("post_versions.id ASC")}, :class_name => "PostArchive", :dependent => :destroy
|
||||
|
||||
IMAGE_TYPES = %i[original large preview crop]
|
||||
|
||||
module FileMethods
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
@ -279,6 +281,23 @@ class Post < ApplicationRecord
|
||||
PostVideoConversionJob.perform_async(self.id)
|
||||
end
|
||||
end
|
||||
|
||||
def regenerate_video_samples!
|
||||
# force code to assume no samples exist
|
||||
update_column(:generated_samples, nil)
|
||||
generate_video_samples(later: true)
|
||||
end
|
||||
|
||||
def regenerate_image_samples!
|
||||
file = self.file()
|
||||
preview_file, crop_file, sample_file = ::PostThumbnailer.generate_resizes(file, image_height, image_width, is_video? ? :video : :image)
|
||||
storage_manager.store_file(sample_file, self, :large) if sample_file.present?
|
||||
storage_manager.store_file(preview_file, self, :preview) if preview_file.present?
|
||||
storage_manager.store_file(crop_file, self, :crop) if crop_file.present?
|
||||
update({has_cropped: crop_file.present?})
|
||||
ensure
|
||||
file.close
|
||||
end
|
||||
end
|
||||
|
||||
module ImageMethods
|
||||
@ -841,7 +860,6 @@ class Post < ApplicationRecord
|
||||
normalized_tags = apply_locked_tags(normalized_tags, @locked_to_add, @locked_to_remove)
|
||||
normalized_tags = %w(tagme) if normalized_tags.empty?
|
||||
normalized_tags = add_automatic_tags(normalized_tags)
|
||||
# normalized_tags = normalized_tags + Tag.create_for_list(TagImplication.automatic_tags_for(normalized_tags))
|
||||
normalized_tags = TagImplication.with_descendants(normalized_tags)
|
||||
enforce_dnp_tags(normalized_tags)
|
||||
normalized_tags -= @locked_to_remove if @locked_to_remove # Prevent adding locked tags through implications or aliases.
|
||||
@ -1580,6 +1598,11 @@ class Post < ApplicationRecord
|
||||
UserStatus.for_user(uploader_id).update_all("post_deleted_count = post_deleted_count + 1")
|
||||
give_favorites_to_parent(options) if options[:move_favorites]
|
||||
give_post_sets_to_parent if options[:move_favorites]
|
||||
reject_pending_replacements
|
||||
end
|
||||
|
||||
def reject_pending_replacements
|
||||
replacements.where(status: 'pending').update_all(status: 'rejected')
|
||||
end
|
||||
|
||||
def undelete!(options = {})
|
||||
@ -1607,15 +1630,6 @@ class Post < ApplicationRecord
|
||||
move_files_on_undelete
|
||||
UserStatus.for_user(uploader_id).update_all("post_deleted_count = post_deleted_count - 1")
|
||||
end
|
||||
|
||||
def replace!(params)
|
||||
transaction do
|
||||
replacement = replacements.create(params)
|
||||
processor = UploadService::Replacer.new(post: self, replacement: replacement)
|
||||
processor.process!
|
||||
replacement
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module VersionMethods
|
||||
|
@ -1,38 +1,224 @@
|
||||
class PostReplacement < ApplicationRecord
|
||||
DELETION_GRACE_PERIOD = 24.hours
|
||||
|
||||
self.table_name = 'post_replacements2'
|
||||
belongs_to :post
|
||||
belongs_to :creator, class_name: "User"
|
||||
before_validation :initialize_fields, on: :create
|
||||
attr_accessor :replacement_file, :final_source, :tags
|
||||
belongs_to :approver, class_name: "User", optional: true
|
||||
attr_accessor :replacement_file, :replacement_url, :final_source, :tags, :is_backup
|
||||
|
||||
def initialize_fields
|
||||
self.creator = CurrentUser.user
|
||||
self.original_url = post.source
|
||||
self.tags = post.tag_string + " " + self.tags.to_s
|
||||
validate :user_is_not_limited, on: :create
|
||||
validate :post_is_valid, on: :create
|
||||
validate :set_file_name, on: :create
|
||||
validate :fetch_source_file, on: :create
|
||||
validate :update_file_attributes, on: :create
|
||||
validate :no_pending_duplicates, on: :create
|
||||
validate :write_storage_file, on: :create
|
||||
|
||||
self.old_file_ext = post.file_ext
|
||||
self.old_file_size = post.file_size
|
||||
self.old_image_width = post.image_width
|
||||
self.old_image_height = post.image_height
|
||||
self.old_md5 = post.md5
|
||||
before_destroy :remove_files
|
||||
|
||||
def replacement_url_parsed
|
||||
return nil unless replacement_url =~ %r!\Ahttps?://!i
|
||||
Addressable::URI.heuristic_parse(replacement_url) rescue nil
|
||||
end
|
||||
|
||||
module PostMethods
|
||||
def post_is_valid
|
||||
if post.is_deleted?
|
||||
self.errors.add(:post, "is deleted")
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module FileMethods
|
||||
def is_image?
|
||||
%w(jpg jpeg gif png).include?(file_ext)
|
||||
end
|
||||
|
||||
def is_flash?
|
||||
%w(swf).include?(file_ext)
|
||||
end
|
||||
|
||||
def is_video?
|
||||
%w(webm).include?(file_ext)
|
||||
end
|
||||
|
||||
def is_ugoira?
|
||||
%w(zip).include?(file_ext)
|
||||
end
|
||||
end
|
||||
|
||||
def no_pending_duplicates
|
||||
return true if is_backup
|
||||
post = Post.where(md5: md5).first
|
||||
if post
|
||||
self.errors.add(:md5, "duplicate of existing post ##{post.id}")
|
||||
return false
|
||||
end
|
||||
replacements = PostReplacement.where(status: 'pending', md5: md5)
|
||||
replacements.each do |replacement|
|
||||
self.errors.add(:md5, "duplicate of pending replacement on post ##{replacement.post_id}")
|
||||
end
|
||||
replacements.size == 0
|
||||
end
|
||||
|
||||
def user_is_not_limited
|
||||
return true if status == 'original'
|
||||
replaceable = creator.can_replace_post_with_reason
|
||||
if replaceable != true
|
||||
self.errors.add(:creator, User.throttle_reason(replaceable))
|
||||
return false
|
||||
end
|
||||
uploadable = creator.can_upload_with_reason
|
||||
if uploadable != true
|
||||
self.errors.add(:creator, User.upload_reason_string(uploadable))
|
||||
return false
|
||||
end
|
||||
|
||||
# Janitor bypass replacement limits
|
||||
return true if creator.is_janitor?
|
||||
|
||||
if post.replacements.where(creator_id: creator.id).where('created_at > ?', 1.day.ago).count >= Danbooru.config.post_replacement_per_day_limit
|
||||
self.errors.add(:creator, 'has already suggested too many replacements for this post today')
|
||||
return false
|
||||
end
|
||||
if post.replacements.where(creator_id: creator.id).count >= Danbooru.config.post_replacement_per_post_limit
|
||||
self.errors.add(:creator, 'has already suggested too many total replacements for this post')
|
||||
return false
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def source_list
|
||||
source.split("\n").uniq.reject(&:blank?)
|
||||
end
|
||||
|
||||
module StorageMethods
|
||||
def remove_files
|
||||
ModAction.log(:post_replacement_delete, {id: id, post_id: post_id, md5: md5, storage_id: storage_id})
|
||||
Danbooru.config.storage_manager.delete_replacement(self)
|
||||
end
|
||||
|
||||
def fetch_source_file
|
||||
return if replacement_file.present?
|
||||
|
||||
download = Downloads::File.new(replacement_url_parsed, "")
|
||||
file, strategy = download.download!
|
||||
|
||||
self.replacement_file = file
|
||||
self.source = replacement_url + "\n#{self.source}"
|
||||
rescue Downloads::File::Error
|
||||
self.errors.add(:replacement_url, "failed to fetch file")
|
||||
throw :abort
|
||||
end
|
||||
|
||||
def update_file_attributes
|
||||
self.file_ext = UploadService::Utils.file_header_to_file_ext(replacement_file)
|
||||
if file_ext == "bin"
|
||||
self.errors.add(:base, "Unknown or invalid file format")
|
||||
throw :abort
|
||||
end
|
||||
self.file_size = replacement_file.size
|
||||
self.md5 = Digest::MD5.file(replacement_file.path).hexdigest
|
||||
|
||||
UploadService::Utils.calculate_dimensions(self, replacement_file) do |width, height|
|
||||
self.image_width = width
|
||||
self.image_height = height
|
||||
end
|
||||
end
|
||||
|
||||
def set_file_name
|
||||
if replacement_file.present?
|
||||
self.file_name = replacement_file.try(:original_filename) || File.basename(replacement_file.path)
|
||||
else
|
||||
if replacement_url_parsed.blank?
|
||||
self.errors.add(:base, "No file or source URL provided")
|
||||
throw :abort
|
||||
end
|
||||
self.file_name = replacement_url_parsed.basename
|
||||
end
|
||||
end
|
||||
|
||||
def write_storage_file
|
||||
self.storage_id = SecureRandom.hex(16)
|
||||
Danbooru.config.storage_manager.store_replacement(replacement_file, self, :original)
|
||||
thumbnail_file = PostThumbnailer.generate_thumbnail(replacement_file, is_video? ? :video : :image)
|
||||
Danbooru.config.storage_manager.store_replacement(thumbnail_file, self, :preview)
|
||||
ensure
|
||||
thumbnail_file.try(:close!)
|
||||
end
|
||||
|
||||
def replacement_file_path
|
||||
Danbooru.config.storage_manager.replacement_path(self, file_ext, :original)
|
||||
end
|
||||
|
||||
def replacement_thumb_path
|
||||
Danbooru.config.storage_manager.replacement_path(self, file_ext, :preview)
|
||||
end
|
||||
|
||||
def replacement_file_url
|
||||
Danbooru.config.storage_manager.replacement_url(self)
|
||||
end
|
||||
|
||||
def replacement_thumb_url
|
||||
Danbooru.config.storage_manager.replacement_url(self, :preview)
|
||||
end
|
||||
end
|
||||
|
||||
module ApiMethods
|
||||
def hidden_attributes
|
||||
super + [:storage_id]
|
||||
end
|
||||
end
|
||||
|
||||
module ProcessingMethods
|
||||
def approve!
|
||||
transaction do
|
||||
ModAction.log(:post_replacement_accept, {post_id: post.id, replacement_id: self.id, old_md5: post.md5, new_md5: self.md5})
|
||||
processor = UploadService::Replacer.new(post: post, replacement: self)
|
||||
processor.process!
|
||||
end
|
||||
end
|
||||
|
||||
def promote!
|
||||
transaction do
|
||||
processor = UploadService.new(new_upload_params)
|
||||
new_post = processor.start!
|
||||
update_attribute(:status, 'promoted')
|
||||
new_post
|
||||
end
|
||||
end
|
||||
|
||||
def reject!
|
||||
ModAction.log(:post_replacement_reject, {post_id: post.id, replacement_id: self.id})
|
||||
update_attribute(:status, 'rejected')
|
||||
end
|
||||
end
|
||||
|
||||
module PromotionMethods
|
||||
def new_upload_params
|
||||
{
|
||||
uploader_id: creator_id,
|
||||
uploader_ip_addr: creator_ip_addr,
|
||||
file: Danbooru.config.storage_manager.open(Danbooru.config.storage_manager.replacement_path(self, file_ext, :original)),
|
||||
tag_string: post.tag_string,
|
||||
rating: post.rating,
|
||||
source: "#{self.source}\n" + post.source,
|
||||
parent_id: post.id,
|
||||
description: post.description,
|
||||
locked_tags: post.locked_tags,
|
||||
replacement_id: self.id
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
concerning :Search do
|
||||
class_methods do
|
||||
def post_tags_match(query)
|
||||
where(post_id: PostQueryBuilder.new(query).build.reorder(""))
|
||||
end
|
||||
|
||||
def search(params = {})
|
||||
q = super
|
||||
|
||||
q = q.attribute_matches(:replacement_url, params[:replacement_url])
|
||||
q = q.attribute_matches(:original_url, params[:original_url])
|
||||
q = q.attribute_matches(:old_file_ext, params[:old_file_ext])
|
||||
q = q.attribute_matches(:file_ext, params[:file_ext])
|
||||
q = q.attribute_matches(:old_md5, params[:old_md5])
|
||||
q = q.attribute_matches(:md5, params[:md5])
|
||||
q = q.attribute_exact_matches(:file_ext, params[:file_ext])
|
||||
q = q.attribute_exact_matches(:md5, params[:md5])
|
||||
q = q.attribute_exact_matches(:status, params[:status])
|
||||
|
||||
if params[:creator_id].present?
|
||||
q = q.where(creator_id: params[:creator_id].split(",").map(&:to_i))
|
||||
@ -46,19 +232,44 @@ class PostReplacement < ApplicationRecord
|
||||
q = q.where(post_id: params[:post_id].split(",").map(&:to_i))
|
||||
end
|
||||
|
||||
if params[:post_tags_match].present?
|
||||
q = q.post_tags_match(params[:post_tags_match])
|
||||
end
|
||||
|
||||
q.apply_default_order(params)
|
||||
q.order(Arel.sql("CASE status WHEN 'pending' THEN 0 ELSE 1 END ASC, id DESC"))
|
||||
end
|
||||
|
||||
def pending
|
||||
where(status: 'pending')
|
||||
end
|
||||
|
||||
def rejected
|
||||
where(status: 'rejected')
|
||||
end
|
||||
|
||||
def approved
|
||||
where(status: 'approved')
|
||||
end
|
||||
|
||||
def for_user(id)
|
||||
where(creator_id: id.to_i)
|
||||
end
|
||||
|
||||
def visible(user)
|
||||
return where('status != ?', 'rejected') if user.is_anonymous?
|
||||
return all if user.is_janitor?
|
||||
where('creator_id = ? or status != ?', user.id, 'rejected')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def suggested_tags_for_removal
|
||||
tags = post.tag_array.select { |tag| Danbooru.config.remove_tag_after_replacement?(tag) }
|
||||
tags = tags.map { |tag| "-#{tag}" }
|
||||
tags.join(" ")
|
||||
def file_visible_to?(user)
|
||||
return true if user.is_janitor?
|
||||
false
|
||||
end
|
||||
|
||||
include ApiMethods
|
||||
include StorageMethods
|
||||
include FileMethods
|
||||
include ProcessingMethods
|
||||
include PromotionMethods
|
||||
include PostMethods
|
||||
|
||||
end
|
||||
|
7
app/models/staff_audit_log.rb
Normal file
7
app/models/staff_audit_log.rb
Normal file
@ -0,0 +1,7 @@
|
||||
class StaffAuditLog < ApplicationRecord
|
||||
belongs_to :user, :class_name => "User"
|
||||
|
||||
def self.log(category, user, details)
|
||||
create(user: user, action: category.to_s, values: details)
|
||||
end
|
||||
end
|
66
app/models/staff_note.rb
Normal file
66
app/models/staff_note.rb
Normal file
@ -0,0 +1,66 @@
|
||||
class StaffNote < ApplicationRecord
|
||||
belongs_to :creator, :class_name => "User"
|
||||
belongs_to :user
|
||||
|
||||
after_create :add_audit_entry
|
||||
|
||||
module SearchMethods
|
||||
def for_creator(user_id)
|
||||
user_id.present? ? where("creator_id = ?", user_id) : none
|
||||
end
|
||||
|
||||
def for_creator_name(user_name)
|
||||
for_creator(User.name_to_id(user_name))
|
||||
end
|
||||
|
||||
def for_user(user_id)
|
||||
user_id.present? ? where('creator_id = ?', user_id) : none
|
||||
end
|
||||
|
||||
def for_user_name(user_name)
|
||||
for_user(User.name_to_id(user_name))
|
||||
end
|
||||
|
||||
def search(params)
|
||||
q = super
|
||||
|
||||
if params[:resolved]
|
||||
q = q.attribute_matches(:resolved, params[:resolved])
|
||||
end
|
||||
|
||||
if params[:user_id].present?
|
||||
q = q.where('user_id = ?', params[:user_id])
|
||||
end
|
||||
|
||||
if params[:user_name].present?
|
||||
q = q.for_user_name(params[:user_name])
|
||||
end
|
||||
|
||||
if params[:creator_name].present?
|
||||
q = q.for_creator_name(params[:creator_name])
|
||||
end
|
||||
q.apply_default_order(params)
|
||||
end
|
||||
|
||||
def default_order
|
||||
order("resolved asc, id desc")
|
||||
end
|
||||
end
|
||||
|
||||
extend SearchMethods
|
||||
|
||||
|
||||
def add_audit_entry
|
||||
StaffAuditLog.log(:staff_note_add, creator, {user_id: user_id})
|
||||
end
|
||||
|
||||
def resolve!
|
||||
self.resolved = true
|
||||
save
|
||||
end
|
||||
|
||||
def unresolve!
|
||||
self.resolved = false
|
||||
save
|
||||
end
|
||||
end
|
@ -1139,10 +1139,10 @@ class Tag < ApplicationRecord
|
||||
name = normalize_name(name)
|
||||
wildcard_name = name + '*'
|
||||
|
||||
query1 = Tag.select("tags.name, tags.post_count, tags.category, null AS antecedent_name")
|
||||
query1 = Tag.select("tags.id, tags.name, tags.post_count, tags.category, null AS antecedent_name")
|
||||
.search(:name_matches => wildcard_name, :order => "count").limit(10)
|
||||
|
||||
query2 = TagAlias.select("tags.name, tags.post_count, tags.category, tag_aliases.antecedent_name")
|
||||
query2 = TagAlias.select("tags.id, tags.name, tags.post_count, tags.category, tag_aliases.antecedent_name")
|
||||
.joins("INNER JOIN tags ON tags.name = tag_aliases.consequent_name")
|
||||
.where("tag_aliases.antecedent_name LIKE ? ESCAPE E'\\\\'", wildcard_name.to_escaped_for_sql_like)
|
||||
.active
|
||||
@ -1155,7 +1155,7 @@ class Tag < ApplicationRecord
|
||||
tags = Tag.select("DISTINCT ON (name, post_count) *").from(sql_query).order("post_count desc").limit(10).to_a
|
||||
|
||||
if tags.size == 0
|
||||
tags = Tag.select("tags.name, tags.post_count, tags.category, null AS antecedent_name").fuzzy_name_matches(name).order_similarity(name).nonempty.limit(10)
|
||||
tags = Tag.select("tags.id, tags.name, tags.post_count, tags.category, null AS antecedent_name").fuzzy_name_matches(name).order_similarity(name).nonempty.limit(10)
|
||||
end
|
||||
|
||||
tags
|
||||
|
@ -28,12 +28,6 @@ class TagImplication < TagRelationship
|
||||
def descendants_with_originals(names)
|
||||
active.where(antecedent_name: names).map { |x| [x.antecedent_name, x.descendant_names] }.uniq
|
||||
end
|
||||
|
||||
def automatic_tags_for(names)
|
||||
# TODO: Remove this?
|
||||
tags = []
|
||||
tags.uniq
|
||||
end
|
||||
end
|
||||
|
||||
def descendants
|
||||
|
@ -150,9 +150,9 @@ class TagRelationship < ApplicationRecord
|
||||
params[:order] ||= "status"
|
||||
case params[:order].downcase
|
||||
when "created_at"
|
||||
q = q.order("created_at desc")
|
||||
q = q.order("created_at desc nulls last, id desc")
|
||||
when "updated_at"
|
||||
q = q.order("updated_at desc")
|
||||
q = q.order("updated_at desc nulls last, id desc")
|
||||
when "name"
|
||||
q = q.order("antecedent_name asc, consequent_name asc")
|
||||
when "tag_count"
|
||||
|
@ -88,6 +88,7 @@ class Takedown < ApplicationRecord
|
||||
def add_posts_by_ids!(ids)
|
||||
added_ids = []
|
||||
with_lock do
|
||||
ids = ids.gsub(/(https?:\/\/)?(e621|e926)\.net\/posts\/(\d+)/i, '\3')
|
||||
self.post_ids = (post_array + ids.scan(/\d+/).map(&:to_i)).uniq.join(' ')
|
||||
added_ids = self.post_array - self.post_array_was
|
||||
save!
|
||||
|
@ -8,11 +8,18 @@ class Upload < ApplicationRecord
|
||||
validate_file_ext(record)
|
||||
validate_md5_uniqueness(record)
|
||||
validate_file_size(record)
|
||||
validate_file_integrity(record)
|
||||
validate_video_container_format(record)
|
||||
validate_video_duration(record)
|
||||
validate_resolution(record)
|
||||
end
|
||||
|
||||
def validate_file_integrity(record)
|
||||
if record.file_ext.in?(["jpg", "jpeg", "gif", "png"]) && DanbooruImageResizer.is_corrupt?(record.file.path)
|
||||
record.errors[:file] << "File is corrupt"
|
||||
end
|
||||
end
|
||||
|
||||
def validate_file_ext(record)
|
||||
if record.file_ext == "bin"
|
||||
record.errors.add(:file_ext, "is invalid (only JPEG, PNG, GIF, and WebM files are allowed")
|
||||
@ -20,6 +27,9 @@ class Upload < ApplicationRecord
|
||||
end
|
||||
|
||||
def validate_file_size(record)
|
||||
if record.file_size <= 16
|
||||
record.errors[:file_size] << "is too small"
|
||||
end
|
||||
max_size = Danbooru.config.max_file_sizes.fetch(record.file_ext, 0)
|
||||
if record.file_size > max_size
|
||||
record.errors.add(:file_size, "is too large. Maximum allowed for this file type is #{max_size / (1024*1024)} MiB")
|
||||
@ -34,6 +44,16 @@ class Upload < ApplicationRecord
|
||||
return
|
||||
end
|
||||
|
||||
replacements = PostReplacement.pending.where(md5: record.md5)
|
||||
replacements = replacements.where('id != ?', record.replacement_id) if record.replacement_id
|
||||
|
||||
if !record.replaced_post && replacements.size > 0
|
||||
replacements.each do |rep|
|
||||
record.errors.add(:md5) << "duplicate of pending replacement on post ##{rep.post_id}"
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
md5_post = Post.find_by_md5(record.md5)
|
||||
|
||||
if md5_post.nil?
|
||||
@ -81,7 +101,7 @@ class Upload < ApplicationRecord
|
||||
end
|
||||
|
||||
|
||||
attr_accessor :as_pending, :replaced_post, :file, :direct_url, :is_apng, :original_post_id, :locked_tags, :locked_rating
|
||||
attr_accessor :as_pending, :replaced_post, :file, :direct_url, :is_apng, :original_post_id, :locked_tags, :locked_rating, :replacement_id
|
||||
belongs_to :uploader, :class_name => "User"
|
||||
belongs_to :post, optional: true
|
||||
|
||||
@ -97,8 +117,6 @@ class Upload < ApplicationRecord
|
||||
serialize :context, JSON
|
||||
|
||||
def initialize_attributes
|
||||
self.uploader_id = CurrentUser.id
|
||||
self.uploader_ip_addr = CurrentUser.ip_addr
|
||||
self.server = Danbooru.config.server_host
|
||||
end
|
||||
|
||||
|
@ -100,7 +100,6 @@ class User < ApplicationRecord
|
||||
before_create :encrypt_password_on_create
|
||||
before_update :encrypt_password_on_update
|
||||
after_save :update_cache
|
||||
before_create :promote_to_admin_if_first_user
|
||||
#after_create :notify_sock_puppets
|
||||
after_create :create_user_status
|
||||
has_many :feedback, :class_name => "UserFeedback", :dependent => :destroy
|
||||
@ -304,18 +303,6 @@ class User < ApplicationRecord
|
||||
UserPromotion.new(self, CurrentUser.user, new_level, options).promote!
|
||||
end
|
||||
|
||||
def promote_to_admin_if_first_user
|
||||
return if Rails.env.test?
|
||||
|
||||
if User.admins.count == 0
|
||||
self.level = Levels::ADMIN
|
||||
self.can_approve_posts = true
|
||||
self.can_upload_free = true
|
||||
else
|
||||
self.level = Levels::MEMBER
|
||||
end
|
||||
end
|
||||
|
||||
def role
|
||||
level_string.downcase.to_sym
|
||||
end
|
||||
@ -535,6 +522,8 @@ class User < ApplicationRecord
|
||||
:is_janitor?, 7.days)
|
||||
create_user_throttle(:forum_vote, -> { Danbooru.config.forum_vote_limit - ForumPostVote.by(id).where("created_at > ?", 1.hour.ago).count },
|
||||
:is_janitor?, 3.days)
|
||||
create_user_throttle(:replace_post, ->{ Danbooru.config.replace_post_limit - PostReplacement.for_user(id).where("created_at > ?", 1.hour.ago).count },
|
||||
:can_approve_posts?, 7.days)
|
||||
|
||||
def can_remove_from_pools?
|
||||
is_member? && older_than(7.days)
|
||||
@ -557,15 +546,15 @@ class User < ApplicationRecord
|
||||
end
|
||||
|
||||
def can_upload_with_reason
|
||||
if hourly_upload_limit <= 0
|
||||
if hourly_upload_limit <= 0 && !Danbooru.config.disable_throttles
|
||||
:REJ_UPLOAD_HOURLY
|
||||
elsif can_upload_free? || is_admin?
|
||||
true
|
||||
elsif younger_than(7.days) && !Danbooru.config.disable_throttles
|
||||
elsif younger_than(7.days)
|
||||
:REJ_UPLOAD_NEWBIE
|
||||
elsif !is_privileged? && post_edit_limit <= 0
|
||||
elsif !is_privileged? && post_edit_limit <= 0 && !Danbooru.config.disable_throttles
|
||||
:REJ_UPLOAD_EDIT
|
||||
elsif upload_limit <= 0
|
||||
elsif upload_limit <= 0 && !Danbooru.config.disable_throttles
|
||||
:REJ_UPLOAD_LIMIT
|
||||
else
|
||||
true
|
||||
@ -590,10 +579,12 @@ class User < ApplicationRecord
|
||||
|
||||
def upload_limit_pieces
|
||||
deleted_count = Post.deleted.for_user(id).count
|
||||
rejected_replacement_count = PostReplacement.rejected.for_user(id).count
|
||||
unapproved_count = Post.pending_or_flagged.for_user(id).count
|
||||
unapproved_replacements_count = PostReplacement.pending.for_user(id).count
|
||||
approved_count = Post.for_user(id).where('is_flagged = false AND is_deleted = false AND is_pending = false').count
|
||||
|
||||
return {deleted: deleted_count, approved: approved_count, pending: unapproved_count}
|
||||
return {deleted: deleted_count, approved: approved_count, pending: unapproved_count + unapproved_replacements_count}
|
||||
end
|
||||
memoize :upload_limit_pieces
|
||||
|
||||
|
@ -165,7 +165,6 @@ class WikiPage < ApplicationRecord
|
||||
|
||||
self.title = version.title
|
||||
self.body = version.body
|
||||
self.is_locked = version.is_locked
|
||||
self.other_names = version.other_names
|
||||
end
|
||||
|
||||
|
@ -7,7 +7,7 @@ class PostPresenter < Presenter
|
||||
return "<em>none</em>".html_safe
|
||||
end
|
||||
|
||||
if !options[:show_deleted] && post.is_deleted? && options[:tags] !~ /(?:status:(?:all|any|deleted))|(?:deletedby:)|(?:delreason:)/ && !options[:raw]
|
||||
if !options[:show_deleted] && post.is_deleted? && options[:tags] !~ /(?:status:(?:all|any|deleted))|(?:deletedby:)|(?:delreason:)/i && !options[:raw]
|
||||
return ""
|
||||
end
|
||||
|
||||
|
@ -183,4 +183,16 @@ class UserPresenter
|
||||
return false if user.enable_privacy_mode? && !CurrentUser.is_admin?
|
||||
true
|
||||
end
|
||||
|
||||
def show_staff_notes?
|
||||
CurrentUser.is_moderator?
|
||||
end
|
||||
|
||||
def staff_notes
|
||||
StaffNote.where(user_id: user.id).order(id: :desc).limit(15)
|
||||
end
|
||||
|
||||
def new_staff_note
|
||||
StaffNote.new(user_id: user.id)
|
||||
end
|
||||
end
|
||||
|
@ -12,5 +12,7 @@
|
||||
<strong>Extra Params:</strong>
|
||||
<pre class="box-section"><%= JSON.pretty_generate(@exception_log.extra_params) %></pre>
|
||||
<strong>Stacktrace:</strong>
|
||||
<pre class="box-section"><%= Rails.backtrace_cleaner.clean(@exception_log.trace.split("\n")).join("\n") %></pre>
|
||||
<strong>Raw Stacktrace:</strong>
|
||||
<pre class="box-section"><%= @exception_log.trace %></pre>
|
||||
</div>
|
||||
|
13
app/views/admin/staff_notes/_search.html.erb
Normal file
13
app/views/admin/staff_notes/_search.html.erb
Normal file
@ -0,0 +1,13 @@
|
||||
<div id='searchform_hide'>
|
||||
<%= search_show_link %>
|
||||
</div>
|
||||
<div class='section' id='searchform' style='width:400px;<% unless params[:show] %>display:none;
|
||||
<% end %>'>
|
||||
<%= simple_form_for(:search, :method => :get, url: admin_staff_notes_path, defaults: {required: false}, html: {class: "inline-form"}) do |f| %>
|
||||
<%= f.input :creator_name, label: "Creator Name", input_html: {data: {autocomplete: "user"}} %>
|
||||
<%= f.input :user_name, label: "User Name", input_html: {data: {autocomplete: "user"}} %>
|
||||
<%= f.input :body_matches, label: "Body" %>
|
||||
<%= f.submit "Search" %>
|
||||
<% end %>
|
||||
<%= search_hide_link %>
|
||||
</div>
|
19
app/views/admin/staff_notes/index.html.erb
Normal file
19
app/views/admin/staff_notes/index.html.erb
Normal file
@ -0,0 +1,19 @@
|
||||
<div id="c-staff-notes">
|
||||
<div id="a-index">
|
||||
<% if @user %>
|
||||
<h1>Staff Notes for <%= link_to_user @user %></h1>
|
||||
<% else %>
|
||||
<h1>Staff Notes</h1>
|
||||
<% end %>
|
||||
<%= render "search" %>
|
||||
|
||||
<%= render "admin/staff_notes/partials/list_of_notes", staff_notes: @notes %>
|
||||
<%= numbered_paginator(@notes) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= render "users/secondary_links" %>
|
||||
|
||||
<% content_for(:page_title) do %>
|
||||
<%= @user ? "#{@user.name} - Staff Notes" : "Staff Notes" %>
|
||||
<% end %>
|
13
app/views/admin/staff_notes/new.html.erb
Normal file
13
app/views/admin/staff_notes/new.html.erb
Normal file
@ -0,0 +1,13 @@
|
||||
<div id="c-staff-notes">
|
||||
<div id="a-new">
|
||||
<h1>New Staff Note for <%= link_to_user(@user) %></h1>
|
||||
|
||||
<%= render "admin/staff_notes/partials/new", staff_note: @staff_note, user: @user %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= render "users/secondary_links" %>
|
||||
|
||||
<% content_for(:page_title) do %>
|
||||
New Staff Note
|
||||
<% end %>
|
@ -0,0 +1,3 @@
|
||||
<div id="p-staff-notes-list" class="staff-note-list">
|
||||
<%= render :partial => "admin/staff_notes/partials/staff_note", :collection => staff_notes %>
|
||||
</div>
|
7
app/views/admin/staff_notes/partials/_new.html.erb
Normal file
7
app/views/admin/staff_notes/partials/_new.html.erb
Normal file
@ -0,0 +1,7 @@
|
||||
<%= error_messages_for :staff_note %>
|
||||
|
||||
<%= simple_form_for(staff_note, url: user_staff_notes_path(user_id: user.id), method: :post) do |f| %>
|
||||
<%= dtext_field "staff_note", "body", name: "", :value => staff_note.body, :input_id => "staff_note_body_for_#{staff_note.id}", :preview_id => "dtext-preview-for-#{staff_note.id}" %>
|
||||
<%= f.button :submit, "Submit" %>
|
||||
<%= dtext_preview_button "staff_note", "body", :input_id => "staff_note_body_for_#{staff_note.id}", :preview_id => "dtext-preview-for-#{staff_note.id}" %>
|
||||
<% end %>
|
19
app/views/admin/staff_notes/partials/_staff_note.html.erb
Normal file
19
app/views/admin/staff_notes/partials/_staff_note.html.erb
Normal file
@ -0,0 +1,19 @@
|
||||
<article class="staff-note comment-post-grid" id="staff-note-<%= staff_note.id %>">
|
||||
<div class="author-info">
|
||||
<div class="name-rank">
|
||||
<h4 class="author-name"><%= link_to_user staff_note.creator %></h4>
|
||||
<%= staff_note.creator.level_string %>
|
||||
</div>
|
||||
<div class="post-time">
|
||||
<%= link_to time_ago_in_words_tagged(staff_note.created_at), user_staff_notes_path(user_id: staff_note.user_id, anchor: "staff-note-#{staff_note.id}") %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<% unless @user %>
|
||||
<h4>On <%= link_to_user staff_note.user %></h4>
|
||||
<% end %>
|
||||
<div class="body styled-dtext">
|
||||
<%= format_text(staff_note.body, allow_color: true) %>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
@ -23,13 +23,21 @@
|
||||
<div class="body styled-dtext">
|
||||
<%= format_text(blip.body, allow_color: blip.creator.is_privileged?) %>
|
||||
</div>
|
||||
<% if blip.was_warned? %>
|
||||
<div class="user-warning">
|
||||
<hr>
|
||||
<em><%= blip.warning_type_string %></em>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="content-menu">
|
||||
<menu>
|
||||
<li><%= tag.a "Reply", href: '#', class: 'blip-reply-link', 'data-bid': blip.id %></li>
|
||||
<% if blip.can_edit?(CurrentUser.user) %>
|
||||
<li><%= link_to "Edit", edit_blip_path(blip) %></li>
|
||||
<% if CurrentUser.is_member? %>
|
||||
<li><%= tag.a "Reply", href: '#', class: 'blip-reply-link', 'data-bid': blip.id %></li>
|
||||
<% if blip.can_edit?(CurrentUser.user) %>
|
||||
<li><%= link_to "Edit", edit_blip_path(blip) %></li>
|
||||
<% end %>
|
||||
<li><%= tag.a "@", href: '#', class: 'blip-atme-link', 'data-bid': blip.id %></li>
|
||||
<% end %>
|
||||
<li><%= tag.a "@", href: '#', class: 'blip-atme-link', 'data-bid': blip.id %></li>
|
||||
|
||||
<% if !blip.is_hidden && blip.can_hide?(CurrentUser.user) %>
|
||||
<li><%= link_to "Hide", hide_blip_path(blip), data: {confirm: "Are you sure you want to hide this blip?"}, method: :post %></li>
|
||||
@ -56,6 +64,15 @@
|
||||
<strong>IP</strong>
|
||||
<span><%= link_to_ip blip.creator_ip_addr %></span>
|
||||
</li>
|
||||
<li>|</li>
|
||||
<% if !blip.was_warned? %>
|
||||
<li>Mark for:</li>
|
||||
<li><%= tag.a "Warning", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'blip', 'item-id': blip.id, 'record-type': 'warning' } %></li>
|
||||
<li><%= tag.a "Record", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'blip', 'item-id': blip.id, 'record-type': 'record' } %></li>
|
||||
<li><%= tag.a "Ban", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'blip', 'item-id': blip.id, 'record-type': 'ban' } %></li>
|
||||
<% else %>
|
||||
<li><%= tag.a "Unmark", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'blip', 'item-id': blip.id, 'record-type': 'unmark' } %></li>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</menu>
|
||||
</div>
|
||||
|
@ -24,6 +24,12 @@
|
||||
|
||||
<%= render "application/update_notice", record: comment %>
|
||||
</div>
|
||||
<% if comment.was_warned? %>
|
||||
<div class="user-warning">
|
||||
<hr>
|
||||
<em><%= comment.warning_type_string %></em>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="content-menu">
|
||||
<menu>
|
||||
<% if @post || @posts || comment.post.present? %>
|
||||
@ -58,6 +64,15 @@
|
||||
<strong>IP</strong>
|
||||
<span><%= link_to_ip comment.creator_ip_addr %></span>
|
||||
</li>
|
||||
<li>|</li>
|
||||
<% if !comment.was_warned? %>
|
||||
<li>Mark for:</li>
|
||||
<li><%= tag.a "Warning", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'comment', 'item-id': comment.id, 'record-type': 'warning' } %></li>
|
||||
<li><%= tag.a "Record", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'comment', 'item-id': comment.id, 'record-type': 'record' } %></li>
|
||||
<li><%= tag.a "Ban", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'comment', 'item-id': comment.id, 'record-type': 'ban' } %></li>
|
||||
<% else %>
|
||||
<li><%= tag.a "Unmark", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'comment', 'item-id': comment.id, 'record-type': 'unmark' } %></li>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</menu>
|
||||
|
@ -23,6 +23,12 @@
|
||||
<%= format_text(parse_embedded_tag_request_text(forum_post.body), allow_color: forum_post.creator.is_privileged?) %>
|
||||
</div>
|
||||
<%= render "application/update_notice", record: forum_post %>
|
||||
<% if forum_post.was_warned? %>
|
||||
<div class="user-warning">
|
||||
<hr>
|
||||
<em><%= forum_post.warning_type_string %></em>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="content-menu">
|
||||
<menu>
|
||||
<% if CurrentUser.is_member? && @forum_topic && params[:controller] != "forum_posts" %>
|
||||
@ -65,6 +71,15 @@
|
||||
<span><%= link_to_ip forum_post.creator_ip_addr %></span>
|
||||
</li>
|
||||
<li>|</li>
|
||||
<% if !forum_post.was_warned? %>
|
||||
<li>Mark for:</li>
|
||||
<li><%= tag.a "Warning", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'forum_post', 'item-id': forum_post.id, 'record-type': 'warning' } %></li>
|
||||
<li><%= tag.a "Record", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'forum_post', 'item-id': forum_post.id, 'record-type': 'record' } %></li>
|
||||
<li><%= tag.a "Ban", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'forum_post', 'item-id': forum_post.id, 'record-type': 'ban' } %></li>
|
||||
<% else %>
|
||||
<li><%= tag.a "Unmark", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'forum_post', 'item-id': forum_post.id, 'record-type': 'unmark' } %></li>
|
||||
<% end %>
|
||||
<li>|</li>
|
||||
<% end %>
|
||||
<% if params[:controller] == "forum_posts" %>
|
||||
<li><%= link_to "Parent", forum_topic_path(forum_post.topic, :page => forum_post.forum_topic_page, :anchor => "forum_post_#{forum_post.id}") %></li>
|
||||
|
@ -6,15 +6,19 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Creator</th>
|
||||
<th>Date</th>
|
||||
<th>Message</th>
|
||||
<th></th>
|
||||
<% if CurrentUser.is_moderator? %>
|
||||
<th></th>
|
||||
<% end %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @news_updates.each do |news_update| %>
|
||||
<tr id="news-update-<%= news_update.id %>">
|
||||
<td><%= link_to_user news_update.creator %></td>
|
||||
<td><%= news_update.message %></td>
|
||||
<td><%= compact_time news_update.updated_at %></td>
|
||||
<td><div class="styled-dtext"><%= format_text news_update.message %></div></td>
|
||||
<% if CurrentUser.is_moderator? %>
|
||||
<td><%= link_to "Edit", edit_news_update_path(news_update) %> | <%= link_to "Delete", news_update_path(news_update), :method => :delete %></td>
|
||||
<% end %>
|
||||
|
@ -4,10 +4,30 @@
|
||||
</div>
|
||||
|
||||
<%= simple_form_for(post_replacement, url: post_replacements_path(post_id: post_replacement.post_id), method: :post) do |f| %>
|
||||
<div>
|
||||
<% if post.visible? %>
|
||||
<%= PostPresenter.preview(post, tags: "status:any", no_blacklist: true) %>
|
||||
<% end %>
|
||||
<div><%= "#{post.image_width}x#{post.image_height} (#{post.file_size.to_s(:human_size, precision: 5)})" %></div>
|
||||
</div>
|
||||
|
||||
<%= f.input :replacement_file, label: "File", as: :file %>
|
||||
<%= f.input :replacement_url, label: "Replacement URL", hint: "The source URL to download the replacement from.", as: :string, input_html: { value: post_replacement.post.normalized_source } %>
|
||||
<%= f.input :final_source, label: "Final Source", hint: "If present, the source field will be changed to this after replacement.", as: :string, input_html: { value: post_replacement.post.source } %>
|
||||
<%= f.input :tags, label: "Tags", as: :string, input_html: { value: post_replacement.suggested_tags_for_removal, data: { autocomplete: "tag-edit" } } %>
|
||||
<%= f.input :replacement_url, label: "Replacement URL", hint: "The source URL to download the replacement from.", as: :string, input_html: {size: 40} %>
|
||||
<div class="box-section sect_red" id="bad_upload_url" style="display: none;">
|
||||
The direct URL entered has the following problem: <span id="bad_upload_url_reason"></span><br>
|
||||
You should review <a href="/wiki/show/howto:sites_and_sources">the sourcing guide</a>.
|
||||
</div>
|
||||
<div id="whitelist-warning" style="display: none;"
|
||||
:class="{'whitelist-warning-allowed': whitelist.allowed, 'whitelist-warning-disallowed': !whitelist.allowed}">
|
||||
<span>Uploads from <b id="whitelist-warning-domain"></b> are <span id="whitelist-warning-not">not </span>permitted.</span>
|
||||
</div>
|
||||
<%= f.input :source, label: "Additional Source", hint: "(Optional) The submission page the replacement file came from.", input_html: {size: 40} %>
|
||||
<%= f.input :reason, label: "Reason", hint: "Tell us why this file should replace the original.", as: :string, input_html: {size: 40} %>
|
||||
<%= f.submit "Submit" %>
|
||||
<% end %>
|
||||
|
||||
<div id="replacement-upload-preview">
|
||||
<div id="replacement_preview_dims"></div>
|
||||
<img id="replacement_preview_img" src="" style="max-width: 100%;"/>
|
||||
</div>
|
||||
</div>
|
||||
|
7
app/views/post_replacements/_search.html.erb
Normal file
7
app/views/post_replacements/_search.html.erb
Normal file
@ -0,0 +1,7 @@
|
||||
<%= simple_form_for(:search, url: post_replacements_path, method: :get, defaults: { required: false }, html: { class: "inline-form" }) do |f| %>
|
||||
<%= f.input :md5, label: "MD5", input_html: { value: params.dig(:search, :md5) } %>
|
||||
<%= f.input :creator_name, label: "Creator", input_html: { value: params.dig(:search, :creator_name) } %>
|
||||
<%= f.input :post_id, label: "Post ID", input_html: { value: params.dig(:search, :post_id) } %>
|
||||
<%= f.input :status, label: "status", collection: ["pending", "rejected", "approved", "promoted"], include_blank: true, selected: params.dig(:search, :status) %>
|
||||
<%= f.submit "Search" %>
|
||||
<% end %>
|
@ -2,78 +2,96 @@
|
||||
<div id="a-index">
|
||||
<h1>Post Replacements</h1>
|
||||
|
||||
<%= render "search" %>
|
||||
<%= render "posts/partials/common/inline_blacklist" %>
|
||||
|
||||
<%= simple_form_for(:search, url: post_replacements_path, method: :get, defaults: { required: false }, html: { class: "inline-form" }) do |f| %>
|
||||
<%= f.input :creator_name, label: "Replacer", input_html: { value: params.dig(:search, :creator_name), data: { autocomplete: "user" } } %>
|
||||
<%= f.input :post_tags_match, label: "Tags", input_html: { value: params.dig(:search, :post_tags_match), data: { autocomplete: "tag-query" } } %>
|
||||
<%= f.submit "Search" %>
|
||||
<% end %>
|
||||
|
||||
<table width="100%" class="striped autofit">
|
||||
<table width="100%" class="striped autofit" style="max-width:100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="1%">Post</th>
|
||||
<th>Source</th>
|
||||
<th>MD5</th>
|
||||
<th>Size</th>
|
||||
<th>Replacer</th>
|
||||
<th width="1%">Replacement</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @post_replacements.each do |post_replacement| %>
|
||||
<tr>
|
||||
<tr class="<%= 'replacement-pending-row' if post_replacement.status == 'pending' %>">
|
||||
<td><%= PostPresenter.preview(post_replacement.post, show_deleted: true) %></td>
|
||||
<td><%= replacement_thumbnail(post_replacement) %></td>
|
||||
<td>
|
||||
<dl>
|
||||
<dt>Original Source</dt>
|
||||
<dd><%= external_link_to post_replacement.original_url, truncate: 64 %></dd>
|
||||
<dt>Replacement Source</dt>
|
||||
<dd>
|
||||
<% if post_replacement.replacement_url.present? %>
|
||||
<%= external_link_to post_replacement.replacement_url, truncate: 64 %>
|
||||
<% if post_replacement.source.present? %>
|
||||
<ul>
|
||||
<% post_replacement.source_list.each do |source| %>
|
||||
<li><%= external_link_to source, truncate: 64 %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<em>file</em>
|
||||
<% end %>
|
||||
</dd>
|
||||
<dt>Filename</dt>
|
||||
<dd><%= post_replacement.file_name %></dd>
|
||||
<% if post_replacement.md5.present? %>
|
||||
<dt>Replacement MD5</dt>
|
||||
<dd><%= post_replacement.md5 %></dd>
|
||||
<% end %>
|
||||
</dl>
|
||||
<div style="display:grid;grid-template-columns: 1fr 1fr;grid-auto-rows: auto;">
|
||||
<dl>
|
||||
<% if post_replacement.post %>
|
||||
<dt>Current Size</dt>
|
||||
<dd>
|
||||
<%= post_replacement.post.image_width %>x<%= post_replacement.post.image_height %>
|
||||
(<%= post_replacement.post.file_size.to_s(:human_size, precision: 5) %>, <%= post_replacement.post.file_ext %>)
|
||||
</dd>
|
||||
<% end %>
|
||||
</dl>
|
||||
<dl>
|
||||
<% if %i[image_width image_height file_size file_ext].all? { |k| post_replacement[k].present? } %>
|
||||
<dt>Replacement Size</dt>
|
||||
<dd>
|
||||
<%= post_replacement.image_width %>x<%= post_replacement.image_height %>
|
||||
(<%= post_replacement.file_size.to_s(:human_size, precision: 5) %>, <%= post_replacement.file_ext %>)
|
||||
</dd>
|
||||
<% end %>
|
||||
</dl>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<dl>
|
||||
<dt>Status</dt>
|
||||
<dd><%= post_replacement.status %></dd>
|
||||
<dt>Reason</dt>
|
||||
<dd><%= post_replacement.reason %></dd>
|
||||
<% if post_replacement.status == 'approved' %>
|
||||
<dt>Approver</dt>
|
||||
<dd><%= link_to_user post_replacement.approver %></dd>
|
||||
<% end %>
|
||||
<dt>Replacer</dt>
|
||||
<dd>
|
||||
<%= compact_time post_replacement.created_at %>
|
||||
<br> by <%= link_to_user post_replacement.creator %>
|
||||
<%= link_to "»", post_replacements_path(search: params[:search].merge(creator_name: post_replacement.creator.name)) %>
|
||||
</dd>
|
||||
</dl>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<% if post_replacement.old_md5.present? && post_replacement.md5.present? %>
|
||||
<dl>
|
||||
<dt>Original MD5</dt>
|
||||
<dd><%= post_replacement.old_md5 %></dd>
|
||||
|
||||
<dt>Replacement MD5</dt>
|
||||
<dd><%= post_replacement.md5 %></dd>
|
||||
</dl>
|
||||
<% if CurrentUser.is_janitor? %>
|
||||
<%= link_to "Approve", approve_post_replacement_path(post_replacement), method: :PUT %><br>
|
||||
<%= link_to "Reject", reject_post_replacement_path(post_replacement), method: :PUT %><br>
|
||||
<%= link_to "As New Post", promote_post_replacement_path(post_replacement), method: :PUT %><br>
|
||||
<% end %>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<% if %i[old_image_width old_image_height old_file_size old_file_ext image_width image_height file_size file_ext].all? { |k| post_replacement[k].present? } %>
|
||||
<dl>
|
||||
<dt>Original Size</dt>
|
||||
<dd>
|
||||
<%= post_replacement.old_image_width %>x<%= post_replacement.old_image_height %>
|
||||
(<%= post_replacement.old_file_size.to_s(:human_size, precision: 4) %>, <%= post_replacement.old_file_ext %>)
|
||||
</dd>
|
||||
|
||||
<dt>Replacement Size</dt>
|
||||
<dd>
|
||||
<%= post_replacement.image_width %>x<%= post_replacement.image_height %>
|
||||
(<%= post_replacement.file_size.to_s(:human_size, precision: 4) %>, <%= post_replacement.file_ext %>)
|
||||
</dd>
|
||||
</dl>
|
||||
<% if CurrentUser.is_moderator? %>
|
||||
<%= link_to "Destroy", post_replacement_path(post_replacement), method: :DELETE, 'data-confirm': 'Are you sure you want to destroy this replacement?' %>
|
||||
<% end %>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<%= compact_time post_replacement.created_at %>
|
||||
<br> by <%= link_to_user post_replacement.creator %>
|
||||
<%= link_to "»", post_replacements_path(search: params[:search].merge(creator_name: post_replacement.creator.name)) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div id="c-post-replacements">
|
||||
<div id="a-new">
|
||||
<%= render "new", post_replacement: @post_replacement %>
|
||||
<%= render "new", post_replacement: @post_replacement, post: @post %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -7,7 +7,9 @@
|
||||
<% if params[:action] == "index" %>
|
||||
| <%= subnav_link_to "Undo selected", "" %>
|
||||
<%= subnav_link_to "Select All", "" %>
|
||||
<% if CurrentUser.is_admin? %>
|
||||
| <%= subnav_link_to "Apply Tag Script To Selected", "" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</menu>
|
||||
<% end %>
|
||||
|
@ -47,6 +47,12 @@
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if CurrentUser.is_janitor? && post.replacements.pending.any? %>
|
||||
<div class="notice notice-flagged">
|
||||
<p>This post has <%= fast_link_to "pending replacements.", post_replacements_path(:search => {:post_id => @post.id}) %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if CurrentUser.is_janitor? && (post.is_flagged? || post.is_deleted?) && post.appeals.any? %>
|
||||
<div class="notice notice-appealed">
|
||||
<p>This post was appealed:</p>
|
||||
|
@ -63,6 +63,12 @@
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if !CurrentUser.is_janitor? && post.replacements.pending.any? %>
|
||||
<div class="notice notice-flagged">
|
||||
<p>This post has <%= fast_link_to "pending replacements.", post_replacements_path(:search => {:post_id => @post.id}) %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if !CurrentUser.is_janitor? && (post.is_flagged? || post.is_deleted?) && post.appeals.any? %>
|
||||
<div class="notice notice-appealed">
|
||||
<p>This post was appealed:</p>
|
||||
|
@ -51,9 +51,12 @@
|
||||
<li><%= link_to "Update IQDB", update_iqdb_post_path(@post) %></li>
|
||||
<li><%= tag.a "Destroy", href: '#', id: 'destroy-post-link', 'data-pid': post.id %></li>
|
||||
<% end %>
|
||||
|
||||
<% if CurrentUser.is_moderator? %>
|
||||
<li><%= link_to "Replace image", new_post_replacement_path(post_id: post.id), id: "replace-image" %></li>
|
||||
<% end %>
|
||||
<% if CurrentUser.is_janitor? %>
|
||||
<li><%= link_to "Replace image", new_post_replacement_path(post_id: post.id), id: "replace-image" %></li>
|
||||
<li><%= tag.a "Regenerate Thumbnails", href: '#', id: 'regenerate-image-samples-link', 'data-pid': post.id %></li>
|
||||
<% if post.is_video? %>
|
||||
<li><%= tag.a "Regenerate Video Samples", href: '#', id: 'regenerate-video-samples-link', 'data-pid': post.id %></li>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
@ -86,7 +86,7 @@
|
||||
<ul>
|
||||
<li><h1>Blips</h1></li>
|
||||
<li><%= link_to("Listing", blips_path) %></li>
|
||||
<li><%= link_to("Help", wiki_pages_path(title: "help:blips")) %></li>
|
||||
<li><%= link_to("Help", wiki_pages_path(title: "e621:blips")) %></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
|
@ -3,14 +3,31 @@
|
||||
<div class="profile-container">
|
||||
<%= render "statistics", :presenter => @presenter, :user => @user %>
|
||||
|
||||
<% if @presenter.show_staff_notes? %>
|
||||
<div class="styled-dtext">
|
||||
<div class="expandable">
|
||||
<div class="expandable-header">
|
||||
<span class="section-arrow"></span>
|
||||
<span>Staff Notes (<%= @presenter.staff_notes.count %>)</span>
|
||||
</div>
|
||||
<div class="expandable-content">
|
||||
<h4><%= link_to "Staff Notes", user_staff_notes_path(user_id: @user.id) %></h4>
|
||||
<%= render "admin/staff_notes/partials/list_of_notes", staff_notes: @presenter.staff_notes %>
|
||||
<h4>New Staff Note</h4>
|
||||
<%= render "admin/staff_notes/partials/new", staff_note: @presenter.new_staff_note, user: @user %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="bottom-section">
|
||||
<div class="posts-section">
|
||||
<% if @presenter.can_view_favorites? %>
|
||||
<div class="blacklist">
|
||||
<%= render "posts/partials/common/inline_blacklist" %>
|
||||
<%= render "posts/partials/common/inline_blacklist" %>
|
||||
</div>
|
||||
<div class="posts">
|
||||
<%= render "post_summary", :presenter => @presenter, :user => @user %>
|
||||
<%= render "post_summary", :presenter => @presenter, :user => @user %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
<div id="wiki-page-body" class="styled-dtext">
|
||||
<% if @wiki_page.visible? %>
|
||||
<%= format_text(@wiki_page.body, allow_color: true) %>
|
||||
<%= format_text(@wiki_page.body, allow_color: true, max_thumbs: 75) %>
|
||||
|
||||
<% if @wiki_page.artist %>
|
||||
<p><%= link_to "View artist", @wiki_page.artist %></p>
|
||||
|
@ -58,17 +58,13 @@ module Danbooru
|
||||
#
|
||||
# Run `rake db:seed` to create this account if it doesn't already exist in your install.
|
||||
def system_user
|
||||
"E621_Bot"
|
||||
"auto_moderator"
|
||||
end
|
||||
|
||||
def upload_feedback_topic
|
||||
ForumTopic.where(title: "Upload Feedback Thread").first
|
||||
end
|
||||
|
||||
def upgrade_account_email
|
||||
contact_email
|
||||
end
|
||||
|
||||
def source_code_url
|
||||
"https://github.com/zwagoth/e621ng"
|
||||
end
|
||||
@ -95,11 +91,6 @@ module Danbooru
|
||||
"Anonymous"
|
||||
end
|
||||
|
||||
# This is a salt used to make dictionary attacks on account passwords harder.
|
||||
def password_salt
|
||||
"choujin-steiner"
|
||||
end
|
||||
|
||||
def levels
|
||||
{
|
||||
"Anonymous" => 0,
|
||||
@ -162,6 +153,14 @@ fart'
|
||||
"abc123"
|
||||
end
|
||||
|
||||
def replacement_path_prefix
|
||||
"replacements"
|
||||
end
|
||||
|
||||
def replacement_file_secret
|
||||
"abc123"
|
||||
end
|
||||
|
||||
def deleted_preview_url
|
||||
"/images/deleted-preview.png"
|
||||
end
|
||||
@ -271,6 +270,10 @@ fart'
|
||||
30
|
||||
end
|
||||
|
||||
def replace_post_limit
|
||||
10
|
||||
end
|
||||
|
||||
def ticket_limit
|
||||
30
|
||||
end
|
||||
@ -280,6 +283,14 @@ fart'
|
||||
30
|
||||
end
|
||||
|
||||
def post_replacement_per_day_limit
|
||||
2
|
||||
end
|
||||
|
||||
def post_replacement_per_post_limit
|
||||
5
|
||||
end
|
||||
|
||||
def remember_key
|
||||
"abc123"
|
||||
end
|
||||
@ -649,10 +660,6 @@ fart'
|
||||
nil
|
||||
end
|
||||
|
||||
def upload_notice_wiki_page
|
||||
"help:upload_notice"
|
||||
end
|
||||
|
||||
def flag_notice_wiki_page
|
||||
"help:flag_notice"
|
||||
end
|
||||
@ -715,14 +722,6 @@ fart'
|
||||
nil
|
||||
end
|
||||
|
||||
def tinami_login
|
||||
nil
|
||||
end
|
||||
|
||||
def tinami_password
|
||||
nil
|
||||
end
|
||||
|
||||
def nico_seiga_login
|
||||
nil
|
||||
end
|
||||
@ -731,14 +730,6 @@ fart'
|
||||
nil
|
||||
end
|
||||
|
||||
def pixa_login
|
||||
nil
|
||||
end
|
||||
|
||||
def pixa_password
|
||||
nil
|
||||
end
|
||||
|
||||
def nijie_login
|
||||
nil
|
||||
end
|
||||
@ -794,12 +785,6 @@ fart'
|
||||
"/var/www/danbooru2/shared"
|
||||
end
|
||||
|
||||
def stripe_secret_key
|
||||
end
|
||||
|
||||
def stripe_publishable_key
|
||||
end
|
||||
|
||||
def twitter_api_key
|
||||
end
|
||||
|
||||
@ -841,11 +826,6 @@ fart'
|
||||
'noreply@localhost'
|
||||
end
|
||||
|
||||
# impose additional requirements to create tag aliases and implications
|
||||
def strict_tag_requirements
|
||||
true
|
||||
end
|
||||
|
||||
# For downloads, if the host matches any of these IPs, block it
|
||||
def banned_ip_for_download?(ip_addr)
|
||||
raise ArgumentError unless ip_addr.is_a?(IPAddr)
|
||||
@ -865,19 +845,6 @@ fart'
|
||||
def twitter_site
|
||||
end
|
||||
|
||||
def addthis_key
|
||||
end
|
||||
|
||||
# enable s3-nginx proxy caching
|
||||
def use_s3_proxy?(post)
|
||||
false
|
||||
end
|
||||
|
||||
# include essential tags in image urls (requires nginx/apache rewrites)
|
||||
def enable_seo_post_urls
|
||||
false
|
||||
end
|
||||
|
||||
# enable some (donmai-specific) optimizations for post counts
|
||||
def estimate_post_counts
|
||||
false
|
||||
@ -888,19 +855,10 @@ fart'
|
||||
true
|
||||
end
|
||||
|
||||
# Enables recording of popular searches, missed searches, and post view
|
||||
# counts. Requires Reportbooru to be configured and running - see below.
|
||||
def enable_post_search_counts
|
||||
false
|
||||
end
|
||||
|
||||
# reportbooru options - see https://github.com/r888888888/reportbooru
|
||||
def reportbooru_server
|
||||
end
|
||||
|
||||
def reportbooru_key
|
||||
end
|
||||
|
||||
def iqdb_enabled?
|
||||
false
|
||||
end
|
||||
|
@ -57,4 +57,5 @@ Rails.application.configure do
|
||||
|
||||
# config.logger = Logger.new(STDOUT)
|
||||
# config.log_level = :info
|
||||
|
||||
end
|
||||
|
@ -5,4 +5,6 @@
|
||||
|
||||
# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code
|
||||
# by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'".
|
||||
Rails.backtrace_cleaner.add_filter { |line| line.gsub(Rails.root.to_s, '') }
|
||||
|
||||
Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"]
|
||||
|
@ -3,7 +3,6 @@ Rails.application.routes.draw do
|
||||
require 'sidekiq/web'
|
||||
require 'sidekiq_unique_jobs/web'
|
||||
|
||||
Sidekiq::Web.set :session_secret, Rails.application.credentials[:secret_key_base]
|
||||
mount Sidekiq::Web => '/sidekiq', constraints: AdminRouteConstraint.new
|
||||
|
||||
namespace :admin do
|
||||
@ -22,6 +21,7 @@ Rails.application.routes.draw do
|
||||
resource :dashboard, :only => [:show]
|
||||
resources :exceptions, only: [:index, :show]
|
||||
resource :reowner, controller: 'reowner', only: [:new, :create]
|
||||
resources :staff_notes, only: [:index]
|
||||
end
|
||||
resources :edit_histories
|
||||
namespace :moderator do
|
||||
@ -52,6 +52,8 @@ Rails.application.routes.draw do
|
||||
get :confirm_ban
|
||||
post :ban
|
||||
post :unban
|
||||
post :regenerate_thumbnails
|
||||
post :regenerate_videos
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -136,6 +138,7 @@ Rails.application.routes.draw do
|
||||
member do
|
||||
post :hide
|
||||
post :unhide
|
||||
post :warning
|
||||
end
|
||||
end
|
||||
resources :comment_votes, only: [:index, :delete, :lock] do
|
||||
@ -167,6 +170,7 @@ Rails.application.routes.draw do
|
||||
member do
|
||||
post :hide
|
||||
post :unhide
|
||||
post :warning
|
||||
end
|
||||
collection do
|
||||
get :search
|
||||
@ -246,7 +250,13 @@ Rails.application.routes.draw do
|
||||
get :diff
|
||||
end
|
||||
end
|
||||
resources :post_replacements, :only => [:index, :new, :create, :update]
|
||||
resources :post_replacements, :only => [:index, :new, :create, :destroy] do
|
||||
member do
|
||||
put :approve
|
||||
put :reject
|
||||
put :promote
|
||||
end
|
||||
end
|
||||
resources :deleted_posts, only: [:index]
|
||||
resources :posts, :only => [:index, :show, :update] do
|
||||
resources :events, :only => [:index], :controller => "post_events"
|
||||
@ -327,6 +337,7 @@ Rails.application.routes.draw do
|
||||
resource :api_key, :only => [:show, :view, :update, :destroy], :controller => "maintenance/user/api_keys" do
|
||||
post :view
|
||||
end
|
||||
resources :staff_notes, only: [:index, :new, :create], controller: "admin/staff_notes"
|
||||
|
||||
collection do
|
||||
get :home
|
||||
@ -365,6 +376,7 @@ Rails.application.routes.draw do
|
||||
member do
|
||||
post :hide
|
||||
post :unhide
|
||||
post :warning
|
||||
end
|
||||
end
|
||||
resources :post_report_reasons
|
||||
|
25
db/migrate/20200806101238_create_new_replacements_table.rb
Normal file
25
db/migrate/20200806101238_create_new_replacements_table.rb
Normal file
@ -0,0 +1,25 @@
|
||||
class CreateNewReplacementsTable < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
create_table :post_replacements2 do |t|
|
||||
t.timestamps
|
||||
t.integer :post_id, null: false
|
||||
t.integer :creator_id, null: false
|
||||
t.inet :creator_ip_addr, null: false
|
||||
t.integer :approver_id
|
||||
t.string :file_ext, length: 8, null: false
|
||||
t.integer :file_size, null: false
|
||||
t.integer :image_height, null: false
|
||||
t.integer :image_width, null: false
|
||||
t.string :md5, null: false
|
||||
t.string :source
|
||||
t.string :file_name, length: 512
|
||||
t.string :storage_id, null: false
|
||||
t.string :status, null: false, default: 'pending'
|
||||
t.string :reason, null: false, length: 500
|
||||
t.boolean :protected, null: false, default: false
|
||||
end
|
||||
|
||||
add_index :post_replacements2, :creator_id
|
||||
add_index :post_replacements2, :post_id
|
||||
end
|
||||
end
|
10
db/migrate/20210405040522_add_user_warn_fields.rb
Normal file
10
db/migrate/20210405040522_add_user_warn_fields.rb
Normal file
@ -0,0 +1,10 @@
|
||||
class AddUserWarnFields < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :blips, :warning_type, :integer
|
||||
add_column :blips, :warning_user_id, :integer
|
||||
add_column :forum_posts, :warning_type, :integer
|
||||
add_column :forum_posts, :warning_user_id, :integer
|
||||
add_column :comments, :warning_type, :integer
|
||||
add_column :comments, :warning_user_id, :integer
|
||||
end
|
||||
end
|
@ -0,0 +1,9 @@
|
||||
class CreateMissingUserIdIndexes < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_index :favorites, :user_id
|
||||
add_index :favorites, :post_id
|
||||
add_index :post_votes, :user_id
|
||||
add_index :post_votes, :post_id
|
||||
add_index :comments, :creator_id
|
||||
end
|
||||
end
|
11
db/migrate/20210426025625_add_staff_notes_table.rb
Normal file
11
db/migrate/20210426025625_add_staff_notes_table.rb
Normal file
@ -0,0 +1,11 @@
|
||||
class AddStaffNotesTable < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
create_table :staff_notes do |t|
|
||||
t.timestamps
|
||||
t.references :user, null: false, foreign_key: true, index: true
|
||||
t.integer :creator_id, null: false, index: true
|
||||
t.string :body
|
||||
t.boolean :resolved, null: false, default: false
|
||||
end
|
||||
end
|
||||
end
|
9
db/migrate/20210430201028_set_user_level_default.rb
Normal file
9
db/migrate/20210430201028_set_user_level_default.rb
Normal file
@ -0,0 +1,9 @@
|
||||
class SetUserLevelDefault < ActiveRecord::Migration[6.1]
|
||||
def up
|
||||
change_column :users, :level, :integer, default: 20
|
||||
end
|
||||
|
||||
def down
|
||||
change_column :users, :level, :integer, default: 0
|
||||
end
|
||||
end
|
10
db/migrate/20210506235640_add_staff_audit_logs_table.rb
Normal file
10
db/migrate/20210506235640_add_staff_audit_logs_table.rb
Normal file
@ -0,0 +1,10 @@
|
||||
class AddStaffAuditLogsTable < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
create_table :staff_audit_logs do |t|
|
||||
t.timestamps
|
||||
t.references :user, null: false, foreign_key: true, index: true
|
||||
t.string :action, null: false, default: 'unknown_action'
|
||||
t.json :values
|
||||
end
|
||||
end
|
||||
end
|
28
db/seeds.rb
28
db/seeds.rb
@ -22,6 +22,7 @@ admin = User.find_or_create_by!(name: "admin") do |user|
|
||||
user.password_hash = ""
|
||||
user.email = "admin@e621.net"
|
||||
user.can_upload_free = true
|
||||
user.can_approve_posts = true
|
||||
user.level = User::Levels::ADMIN
|
||||
end
|
||||
|
||||
@ -31,7 +32,8 @@ User.find_or_create_by!(name: Danbooru.config.system_user) do |user|
|
||||
user.password_hash = ""
|
||||
user.email = "system@e621.net"
|
||||
user.can_upload_free = true
|
||||
user.level = User::Levels::ADMIN
|
||||
user.can_approve_posts = true
|
||||
user.level = User::Levels::JANITOR
|
||||
end
|
||||
|
||||
ForumCategory.find_or_create_by!(id: Danbooru.config.alias_implication_forum_category) do |category|
|
||||
@ -44,18 +46,32 @@ unless Rails.env.test?
|
||||
CurrentUser.ip_addr = "127.0.0.1"
|
||||
|
||||
resources = YAML.load_file Rails.root.join("db", "seeds.yml")
|
||||
resources["images"].each do |image|
|
||||
puts image["url"]
|
||||
url = "https://e621.net/posts.json?limit=100&tags=id:" + resources["post_ids"].join(",")
|
||||
response = HTTParty.get(url, {
|
||||
headers: {"User-Agent" => "e621ng/seeding"}
|
||||
})
|
||||
json = JSON.parse(response.body)
|
||||
|
||||
data = Net::HTTP.get(URI(image["url"]))
|
||||
json["posts"].each do |post|
|
||||
puts post["file"]["url"]
|
||||
|
||||
data = Net::HTTP.get(URI(post["file"]["url"]))
|
||||
file = Tempfile.new.binmode
|
||||
file.write data
|
||||
|
||||
post["tags"].each do |category, tags|
|
||||
Tag.find_or_create_by_name_list(tags.map {|tag| category + ":" + tag})
|
||||
end
|
||||
|
||||
md5 = Digest::MD5.hexdigest(data)
|
||||
service = UploadService.new({
|
||||
uploader_id: CurrentUser.id,
|
||||
uploader_ip_addr: CurrentUser.ip_addr,
|
||||
file: file,
|
||||
tag_string: image["tags"],
|
||||
rating: "s",
|
||||
tag_string: post["tags"].values.flatten.join(" "),
|
||||
source: post["sources"].join("\n"),
|
||||
description: post["description"],
|
||||
rating: post["rating"],
|
||||
md5: md5,
|
||||
md5_confirmation: md5
|
||||
})
|
||||
|
527
db/seeds.yml
527
db/seeds.yml
@ -1,446 +1,81 @@
|
||||
images:
|
||||
- tags: zoodystopia 2016 anthro bandage black_nose brown_fur canine clothed clothing
|
||||
collar disney fox fur gloves_(marking) green_eyes looking_at_viewer male mammal
|
||||
markings multicolored_fur nick_wilde open_shirt orange_fur pantssad shirt shock_collar
|
||||
simple_background solo standing turningtides_(artist) white_background zootopia
|
||||
url: https://static1.e621.net/data/87/8f/878f64f8df553ce09218bc9b1083b4e3.png
|
||||
- tags: 5:4 2016 3_toes anthro barefoot canine claws clothed clothing cuddling disney
|
||||
duo fabienne_growley female flat_chested fox green_eyes ipoke judy_hopps lagomorph
|
||||
male mammal nick_wilde peter_moosebridge phone purple_eyes rabbit sitting smile
|
||||
sofa soles television toes zootopia
|
||||
url: https://static1.e621.net/data/62/f1/62f197461e755a7d4bd5884dc8ff20d6.jpg
|
||||
- tags: begging begging_pose blue_fur blue_tail canine chest_tuft dipstick_tail facial_markings
|
||||
fan_character feral feralized fluffy fur looking_away male mammal markings mask_(marking)
|
||||
multicolored_fur multicolored_tail nintendo pokémon pokémon_(species) portrait red_eyes
|
||||
riolu simple_background smile solo tuft two_tone_fur two_tone_tail umpherio_(umpherio)
|
||||
video_games white_background white_fur white_tail xnirox
|
||||
url: https://static1.e621.net/data/97/38/973887346d669d97c7a75b7324855773.png
|
||||
- tags: felid pantherine 2017 armor big_breasts black_lips blue_eyes blue_hair bodysuit
|
||||
breasts clothed clothing f-r95 female fingerless_gloves fluffy fluffy_tail gloves
|
||||
hair inner_ear_fluff inside knee_pads long_tail looking_away looking_outside mammal
|
||||
pink_noseshoulder_pads sitting skinsuit snow_leopard solo space spots tight_clothing
|
||||
toeless_footwear yuna_snowleopard
|
||||
url: https://static1.e621.net/data/34/7f/347fec14942b176b074c1cad64c31ad2.png
|
||||
- tags: back_muscles 2015 anthro ass_up bed bedroom black_fur black_stripes book bookshelf
|
||||
butt crawling feline fireplace fur green_eyes hax_(artist) inside kenket lofi
|
||||
lying male mammal morning multicolored_fur nude on_floor on_front one_eye_closed
|
||||
orange_fur painting pawpads platerug shaded solo striped_body stripes table tiger
|
||||
vase white_fur window
|
||||
url: https://static1.e621.net/data/sample/6f/f4/6ff406e64fe870f43512d863a8fd119b.jpg
|
||||
- tags: pink_eyes 2018 anthro canine clothed clothing cookie digital_media_(artwork)
|
||||
dog female food fur hair looking_at_viewer mammal nana_(fadey) open_mouthsmile
|
||||
solo standing tofu93
|
||||
url: https://static1.e621.net/data/sample/0f/01/0f01bc5401835aa1a082b19e83a0e956.jpg
|
||||
- tags: 2013 anthro beverage blue_fur blue_hair borny_(character) canine clothing
|
||||
coffee digital_media_(artwork) food fox fur green_eyes hair hoodie male mammal
|
||||
open_mouth phonesolo wolfy-nail
|
||||
url: https://static1.e621.net/data/sample/ac/9f/ac9f231b988d4b6f43b66e89e1d5578e.jpg
|
||||
- tags: clothed detailed_background fully_clothed holding_object menacing outside
|
||||
standing yellow_sclera 2015 anthro cheetah clothing feline female flashlight gun
|
||||
handgun mammal muscular muscular_female pistol police ranged_weaponsolo street_lamp
|
||||
sunrise tirrel uniform weapon
|
||||
url: https://static1.e621.net/data/sample/d3/ae/d3aee9d4de485f2be935e84ebbd971b7.jpg
|
||||
- tags: 2015 ambiguous_gender anthro blue_eyes clothing cosplay costume day detailed_background
|
||||
dragon dreamworks falvie fudge_the_otter how_to_train_your_dragon kigurumi mammal
|
||||
membranous_wings mustelid night_fury open_mouth otter outsidesitting sky smile
|
||||
solo star starry_sky sunset toothless wings
|
||||
url: https://static1.e621.net/data/sample/0f/d1/0fd1d92190633265fb32a800d65c82d7.jpg
|
||||
- tags: holding_object food ambiguous_gender beverage black_fur blue_fur blush canine
|
||||
chigiri clothing coffee cup first_person_view fur happy long_ears lucario mammal
|
||||
nintendo one_eye_closed pokémon red_eyes sitting smile solo steam video_games wink
|
||||
url: https://static1.e621.net/data/5a/c6/5ac6787ce0216184bb1737ffc97972e7.png
|
||||
- tags: android animatronic anthro canine clothed clothing exposed_endoskeleton eye_patch
|
||||
eyewear five_nights_at_freddy's fox foxy_(fnaf) fur hook hook_hand machine male
|
||||
mammal naoren piratered_fur robot simple_background solo topless video_games white_background
|
||||
yellow_eyes
|
||||
url: https://static1.e621.net/data/17/0a/170ad0093d771d4b99c59f9626712578.png
|
||||
- tags: 2015 finger_claws 5_fingers <3 anthro blush boss_monster breasts caprine claws
|
||||
clothed clothing eyebrows eyelashes female fur gastropod goat hi-biki hi_res horn
|
||||
long_ears looking_at_viewer mammal poserobe simple_background smile snail solo
|
||||
toriel undertale video_games white_fur wide_hips
|
||||
url: https://static1.e621.net/data/sample/31/16/31162bd0709f5c79726eb55a05997237.jpg
|
||||
- tags: 2017 4_toes anthro arnou barefoot black_fur black_nose blue_eyes canine clothed
|
||||
clothing cuddling day detailed_background digitigrade duo fur hoodie koul male
|
||||
male/male mammal outside red_eyes smile toes tree wolf wulfcole
|
||||
url: https://static1.e621.net/data/sample/e2/4e/e24e60d0e2604a1e18de6ec698462e75.jpg
|
||||
- tags: 2014 anthro black_nose blue_eyes blush brown_fur clothed clothing collar countershade_face
|
||||
countershading female flower flower_in_hair fur headshot_portrait kiki looking_away
|
||||
mammal markings multicolored_fur mustelid open_mouth otter plant portrait rarakiesimple_background
|
||||
solo spots teeth tongue two_tone_fur whiskers white_fur
|
||||
url: https://static1.e621.net/data/8d/8e/8d8ec834c0b6bd02181510e3bc3f7e4d.png
|
||||
- tags: pretending anthro antlers brown_fur cellphone cervine cloud deer duo feline
|
||||
fur green_eyes hi_res horn humor imminent_death lion male mammal nbowa outside
|
||||
phone predator/preyselfie sky tirrel tirrel_(character) tongue tongue_out tree
|
||||
url: https://static1.e621.net/data/sample/d9/0e/d90ea5d58bbb37f6c08d054946a478f9.jpg
|
||||
- tags: "... ambiguous_gender anthro black_fur blue_fur blush canine close-up crossed_arms
|
||||
fur icon jia looking_at_viewer lucario mammal nintendo pokémon pokémon_(species) red_eyes
|
||||
simple_background solo tan_fur video_games white_background"
|
||||
url: https://static1.e621.net/data/sample/db/b6/dbb6db28e0514bb981f417edf6479c9a.jpg
|
||||
- tags: winged_arms wings ... absurd_res anthro avian beak bird blush breath_of_the_wild
|
||||
clothed clothing dialogue english_text feathers hi_res kass_(zelda) male muscular
|
||||
muscular_male nintendo outsiderito sketch solo tacklebox text the_legend_of_zelda
|
||||
video_games
|
||||
url: https://static1.e621.net/data/sample/c6/a3/c6a32022972d9807cfaae5000f45b2c4.jpg
|
||||
- tags: too_early_for_this 2014 5_fingers annoyed anthro beverage big_eyebrows black_eyebrows
|
||||
black_eyes black_fur black_hair black_nose black_shirt blue_fur bust_portrait
|
||||
canine clothed clothing coffee coffee_mug countertop crossed_arms cup dialogue
|
||||
digital_media_(artwork) ear_piercing english_text eyebrows facial_hair facial_piercing
|
||||
food fur gauged_ear goatee grumpy hair holding_cup holding_object hot_drink lip_piercing
|
||||
lol_comments looking_at_viewer male mammal multicolored_fur multicolored_hair
|
||||
nose_piercing nose_ring open_mouth piercing portrait profanityreaction_image restricted_palette
|
||||
shirt signature simple_background sitting sky3 skye_blue solo speech_bubble steam
|
||||
striped_ears table teeth text thick_eyebrows two_tone_fur two_tone_hair white_background
|
||||
white_fur white_hair wolf
|
||||
url: https://static1.e621.net/data/sample/16/61/1661025f6995846607e709c6da99e123.jpg
|
||||
- tags: 2013 ambiguous_gender black_eyes cloud cloudscape colorful detailed_background
|
||||
feral fish happy lol_comments marine open_mouth outside rainbow rainbow_archreaction_image
|
||||
shark sky solo sparkles spunky_(artist) super_gay teeth
|
||||
url: https://static1.e621.net/data/e5/03/e503e7d3465337448b715cfd6457edd0.jpg
|
||||
- tags: 2017 anthro belly big_breasts breasts brown_hair canine chinese_clothing chinese_dress
|
||||
cleavage cleavage_cutout clothed clothing dress female food fox gradient_background
|
||||
hair hairclip hi_res kemono long_hair looking_at_viewer mammal purple_eyessetouchi_kurage
|
||||
simple_background slightly_chubby solo voluptuous waiter wide_hips
|
||||
url: https://static1.e621.net/data/sample/b1/0d/b10d8740126b4fb20d4b21920d131c8f.jpg
|
||||
- tags: brown_hair orange_hair sitting striped_body striped_fur striped_tail 2018
|
||||
<3 anthro black_topwear blue_eyes blue_topwear breasts clothed clothing collaboration
|
||||
countershade_legs countershade_torso countershading day deymos digital_media_(artwork)
|
||||
dipstick_tail domestic_cat felid feline felis female fingerless_(marking) flower
|
||||
fully_clothed fur hair inside iskra legwear looking_aside mammal multicolored_body
|
||||
multicolored_fur multicolored_tail orange_body orange_fur panties pink_nose plantshaded
|
||||
shirt solo stripes sunflower tan_body tan_fur thigh_highs underwear url vera_(iskra)
|
||||
white_legwear window
|
||||
url: https://static1.e621.net/data/sample/80/8d/808d424fd5c3ccbaddac9fc5b92f028a.jpg
|
||||
- tags: 2016 anthro blush buckteeth canine disney duo embarrassed english_text female
|
||||
fireworks fox freedomthai fur green_eyes grey_fur half-closed_eyes hand_on_head
|
||||
hi_res holding_object judy_hopps lagomorph larger_male long_ears male male/female
|
||||
mammal nick_wilde night open_mouth orange_fur outside pen predator/prey purple_eyes
|
||||
rabbitromantic_ambiance size_difference smaller_female smile smirk tears teeth
|
||||
text zootopia
|
||||
url: https://static1.e621.net/data/sample/c6/32/c6321f137f68f99f2bee51dba3bf1de5.jpg
|
||||
- tags: ambiguous_gender capcom cub dragon horn kneeling kosian looking_at_viewer
|
||||
membranous_wings monster_hunter red_eyes rukodiora scalie simple_background solo
|
||||
video_games white_background wings young
|
||||
url: https://static1.e621.net/data/sample/da/92/da922cd07342972262e98de824595cd1.jpg
|
||||
- tags: 2014 anthro beach bikini bikini_top blue_eyes bottomless clothed clothing
|
||||
cloud detailed_background digital_media_(artwork) ear_piercing falvie fangs female
|
||||
grin hair looking_at_viewer mammal markings mustelid neck_tuft otter outside piercingsand
|
||||
seaside short_hair sky smile solo swimsuit tuft water white_hair
|
||||
url: https://static1.e621.net/data/sample/aa/3f/aa3f6074e6c14ff78b40ff51f109869b.jpg
|
||||
- tags: casual_nudity day eyebrows eyelashes grey_hair half-closed_eyes looking_aside
|
||||
multicolored_fur multicolored_hair partially_submerged skinny_dipping slim snout
|
||||
sunlight swimming tan_fur thick_tail 2015 anthro back_muscles blonde_hair brown_eyes
|
||||
brown_fur brown_hair brown_nose butt digital_media_(artwork) eyewear fangs female
|
||||
feral fish fur goggles hair hi_res high-angle_view mammal marine mikhaila mouth_hold
|
||||
mustelid nude otter patreonshort_hair solo teeth tsampikos water wet wide_hips
|
||||
url: https://static1.e621.net/data/sample/82/ab/82abacf96b5e3bde9d92ef6e5e0197eb.jpg
|
||||
- tags: 5_fingers anthro_on_anthro black_nose blush brown_fur brown_nose brown_stripes
|
||||
brown_tail canid cheek_tuft chest_tuft embrace eyebrows felid fluffy fluffy_tail
|
||||
gloves_(marking) glowing_ears glowing_fur glowing_spots green_fur green_spots
|
||||
holding_object holding_plush humanoid_hands interspecies markings orange_tail
|
||||
pantherine romantic_couple snout spots spotted_fur striped_fur striped_tail tan_belly
|
||||
tan_fur tuft white_belly white_tail anthro bed bed_sheet bedding bioluminescence
|
||||
brown_hair canine cuddling digital_media_(artwork) duo eyes_closed fox fur gabe_(mytigertail)
|
||||
glowing hair hug inside lan lying male male/male mammal multicolored_fur mytigertail
|
||||
nude on_bed on_side open_mouth orange_fur orange_stripes pillow plushie rating:q
|
||||
sleeping smile spooning stripes tiger two_tone_fur white_fur zeta-haru
|
||||
url: https://static1.e621.net/data/sample/b8/89/b889bbc7002cc1152ed7f1c8e3a14670.jpg
|
||||
- tags: 2014 3:2 afternoon alien big_ears black_scales blue_eyes blue_fur blue_nose
|
||||
claws clothed clothing cosplay costume crossover daww disney dragon dreamworks
|
||||
duo experiment_(species) feral fish food fur grasp green_sclera hat head_tuft
|
||||
headgear holding_food holding_object how_to_train_your_dragon inside lilo_and_stitch
|
||||
long_ears looking_down looking_up lying male marine membranous_wings night_fury
|
||||
no_sclera offering_(disambiguation) offering_food offering_to_another on_front
|
||||
on_ground outstretched_arms raised_arm scales scalie scarf sharp_teeth size_difference
|
||||
smile standing stitch sunlight teeth toe_claws toothless tsaoshin wings
|
||||
url: https://static1.e621.net/data/50/cc/50cc1f4f731dca8cb66fdb9fd42bc57d.png
|
||||
- tags: 2017 ambiguous_form ambiguous_gender big_ears brown_eyes canid canine derp_eyes
|
||||
dipstick_ears ears_down english_text fangs felisrandomis fennec fox frown fur
|
||||
headshot_portrait humor inner_ear_fluff lol_comments mammal multiple_poses open_mouth
|
||||
open_smile portrait pose screaming simple_background smile solo tan_fur teeth
|
||||
text the_truth tongue weasyl
|
||||
url: https://static1.e621.net/data/79/d7/79d74e8154dcea5339ff51f77fd3c98b.png
|
||||
- tags: 2018 anthro black_hair blue_eyes blush canid canine canis clothing domestic_dog
|
||||
faceless_male female hair human male mammal on_lap sally_(povinne10) talilly tongue
|
||||
tongue_out
|
||||
url: https://static1.e621.net/data/08/1e/081e279f730cccb5948853e8d1ea6a9f.jpg
|
||||
- tags: 2018 <3 ambiguous_gender blush coal_(rakkuguy) eyes_closed happy human kobold
|
||||
mammal motion_lines one_eye_closed open_mouth petting rakkuguy scalie simple_background
|
||||
smile tailwag tongue tongue_out white_background
|
||||
url: https://static1.e621.net/data/48/5d/485d385d6bb518ee12c78747054e827c.jpg
|
||||
- tags: 2018 <3 3_toes ambiguous/ambiguous ambiguous_gender anthro anthro_on_anthro
|
||||
blue_fur butt canid canine cewljoke chest_tuft cute_fangs duo evolutionary_family
|
||||
eyes_closed fur hindpaw hi_res hug kneeling level_difference lucario mammal nintendo
|
||||
nude pawpads paws pink_pawpads pokémon pokémon_(species) red_eyes riolu signature
|
||||
simple_background smile spikes toes tuft video_games white_background wholesome
|
||||
url: https://static1.e621.net/data/69/14/6914390405ab241003e48b4cfeeea5f3.png
|
||||
- tags: 2017 absurd_res ambiguous_gender anatomy black_background blep canid canine
|
||||
cross-eyed diagram english_text feral fox fur hi_res humor mammal meme red_fox
|
||||
riot_the_red_fox simple_background sitting solo text tongue tongue_out whiskers
|
||||
zillion_ross
|
||||
url: https://static1.e621.net/data/8b/a7/8ba7610b0a2b91d91aebf4a55e131aa7.png
|
||||
- tags: 2018 4_toes anatomy black_background black_fur black_hair black_nose blue_fur
|
||||
blue_hair blue_tongue canid canine claws diagram ear_piercing english_text feral
|
||||
fox fur fur_markings gloves_(marking) hair hi_res humor industrial_piercing male
|
||||
mammal markings meme mostly_nude multicolored_fur multicolored_hair open_mouth
|
||||
original_character_do_not_steal owo owo_whats_this paws piercing quadruped restricted_palette
|
||||
scarf simple_background socks_(marking) solo teeth text the_truth toe_claws toes
|
||||
tongue two_tone_hair white_fur zillion_ross
|
||||
url: https://static1.e621.net/data/63/3b/633bbadd77a49e662768428ffc026491.png
|
||||
- tags: 2017 <3 alcohol avian beak beer beverage bird black_feathers bottle corvid
|
||||
crow deep_throat digital_media_(artwork) english_text feathers feral hi_res humor
|
||||
lol_comments oral oral_penetration penetration solo swish talons text wings
|
||||
url: https://static1.e621.net/data/7c/d9/7cd90352fa57ddaeb3f422de355e727c.jpg
|
||||
- tags: 2017 5_fingers alternate_form anthro canid canine clothed clothing cup cups_on_ears
|
||||
cute_fangs dagger detailed_background dialogue digital_media_(artwork) dutch_angle
|
||||
english_text fox fur gregg_(nitw) happy inside jacket knife koul looking_at_viewer
|
||||
male mammal melee_weapon night_in_the_woods open_mouth orange_fur smile solo speech_bubble
|
||||
text video_games weapon
|
||||
url: https://static1.e621.net/data/4f/be/4fbe36ed49bf8d501edb8509a554837d.jpg
|
||||
- tags: ambiguous_gender canid canine conditional_dnp daww derp_eyes digital_media_(artwork)
|
||||
digital_painting_(artwork) ears_back eyes_closed feral flexible fluffy fox full-length_portrait
|
||||
fur humor lol_comments mammal markings oops orange_fur paws playing portrait red_fox
|
||||
side_view simple_background snow socks_(marking) solo trunchbull upside_down whiskers
|
||||
white_background white_fur
|
||||
url: https://static1.e621.net/data/94/d1/94d108d2e4ed476a113f93e7768accce.jpg
|
||||
- tags: anthro canid canine clothed clothing english_text fangs female fur hair image_macro
|
||||
impact_(font) jurassic_park mammal meme nasusbot open_mouth parody profanity reaction_image
|
||||
skrillex smile solo teeth text
|
||||
url: https://static1.e621.net/data/cb/a4/cba428d3227dd7358dbf63787c759e8d.png
|
||||
- tags: "<3 3_toes ambiguous_gender anthro barefoot biped black_fur black_markings
|
||||
blue_fur blue_tail blush canid canine cel_shading cheek_tuft collar digital_media_(artwork)
|
||||
digitigrade facial_markings feet fur fur_tuft gloves_(marking) gradient_background
|
||||
kneeling leash leash_in_mouth lucario mammal markings mouth_hold multicolored_fur
|
||||
neck_tuft nintendo nude object_in_mouth paws pink_background pokémon pokémon_(species)
|
||||
pseudo_clothing rakkuguy red_eyes shaded side_view simple_background smile snout
|
||||
solo spikes submissive toes tuft video_games white_background yellow_fur"
|
||||
url: https://static1.e621.net/data/a1/1e/a11e49bd7c556049770b3e905e9528ef.png
|
||||
- tags: 2013 4_fingers anthro barefoot beverage black_fur black_nose canid canine
|
||||
clothed clothing coffee coffee_mug cup dipstick_ears dipstick_tail english_text
|
||||
eyes_closed fennec fingerless_(marking) fluffy fluffy_tail food fox fully_clothed
|
||||
fur gloves_(marking) hair hi_res inner_ear_fluff lying male mammal markings multicolored_tail
|
||||
orange_fur orange_hair pants reaching shirt side_view simple_background sleeping
|
||||
socks_(marking) solo sound_effects text thanshuhai tired toeless_(marking) tongue
|
||||
tongue_out white_background white_fur zzz
|
||||
url: https://static1.e621.net/data/8d/eb/8deb2ae9fe81f9f6383a24c591bdd3be.png
|
||||
- tags: 2016 alolan_vulpix altered_reflection ambiguous_gender black_nose brown_eyes
|
||||
brown_hair canid canine contrast digital_media_(artwork) duality feral fur grey_nose
|
||||
hair hindpaw hi_res inner_ear_fluff looking_at_viewer mammal multi_tail nintendo
|
||||
paws pokémon pokémon_(species) realistic reflection regional_variant solo split_screen
|
||||
standing symmetry tamberella tan_fur video_games vulpix white_fur
|
||||
url: https://static1.e621.net/data/7e/11/7e11e929a5c5b58078d600d3aa23cf21.jpg
|
||||
- tags: anthro canid canine clothed clothing disney edit english_text fox fully_clothed
|
||||
gradient_background lyrics male mammal michael_jackson music nick_wilde simple_background
|
||||
smile smooth_criminal solo song sound text zootopia 羽默空_(artist)
|
||||
url: https://static1.e621.net/data/fc/09/fc090f0332c2ae9e91bb6953179bceaf.jpg
|
||||
- tags: anthro antlers brown_fur cellphone cervid cloud duo felid fur green_eyes hi_res
|
||||
horn humor imminent_death lion male mammal nbowa outside pantherine phone predator/prey
|
||||
pretending selfie sky tirrel tirrel_(character) tongue tongue_out tree
|
||||
url: https://static1.e621.net/data/d9/0e/d90ea5d58bbb37f6c08d054946a478f9.jpg
|
||||
- tags: 2016 anthro black_fur black_hair black_nose blizzard_entertainment bound breasts
|
||||
canid canine cheek_tuft conditional_dnp crying digital_media_(artwork) domestic_cat
|
||||
dream_eater duo e621 english_text eyelashes fan_character felid feline felis female
|
||||
feral floof flora_fauna fluffy food food_creature fruit fur green_eyes hair hi_res
|
||||
horn humor impact_(font) kingdom_hearts lol_comments looking_up mammal melon meloncat
|
||||
melonyan meme meow_wow meta nude paralee_(character) plant poof profanity rainbow
|
||||
ratte sad square_enix tardar_sauce tears technicolor_yawn text tuft video_games
|
||||
vomit wall_of_text warcraft watermelon were werecanid werecanine werewolf worgen
|
||||
url: https://static1.e621.net/data/42/2a/422aaf2c210d6e0e48480ce419b06896.png
|
||||
- tags: 2016 <3 4_fingers abstract_background anthro asriel_dreemurr big_eyes black_background
|
||||
blush boss_monster bovid braeburned brown_background caprine cheek_tuft clothing
|
||||
cute_fangs digital_media_(artwork) fangs feels fur gesture goat gradient_background
|
||||
green_clothing green_topwear hair half-length_portrait hand_heart head_tuft hi_res
|
||||
long_ears looking_at_viewer love male mammal multicolored_clothing open_mouth
|
||||
pink_tongue portrait sharp_teeth signature simple_background smile solo sweater
|
||||
teeth tongue tuft undertale video_games white_fur yellow_bottomwear yellow_clothing
|
||||
young
|
||||
url: https://static1.e621.net/data/14/e7/14e7eff45c61ba29e02f4f59846dc6bc.png
|
||||
- tags: "... <3 absurd_res ambiguous_gender beady_eyes blep dialogue english_text
|
||||
feral forked_tongue hi_res iron_and_whine python reptile scalie simple_background
|
||||
snake solo text tongue tongue_out traditional_media_(artwork) white_background"
|
||||
url: https://static1.e621.net/data/7f/6a/7f6af5deef179238510cfa1d606ef81f.jpg
|
||||
- tags: 2016 alantka anthro canid canine cuddling detailed_background disney duo embrace
|
||||
eyes_closed female forest fox fur gloves_(marking) green_eyes grey_fur judy_hopps
|
||||
lagomorph long_ears looking_down male mammal markings nick_wilde orange_fur outside
|
||||
predator/prey rabbit romantic_couple size_difference smile style_parody tree zootopia
|
||||
url: https://static1.e621.net/data/e1/28/e128617350d9e02bf816f0d4d529c210.jpg
|
||||
- tags: 2016 absurd_res anthro barefoot bed bedroom bracelet buckteeth canid canine
|
||||
choker dancing dennyvixen dipstick_tail disney duo earbuds eyes_closed female
|
||||
fox fur gloves_(marking) good_morning grey_fur happy headphones hi_res inner_ear_fluff
|
||||
inside ipod ipod_nano jewelry judy_hopps lagomorph long_ears male mammal markings
|
||||
multicolored_tail nick_wilde on_bed open_mouth orange_fur pawpads portable_music_player
|
||||
predator/prey rabbit smile teeth two_tone_tail upscale zootopia
|
||||
url: https://static1.e621.net/data/2e/d4/2ed4a63c7105f9e6ca099033403b242a.png
|
||||
- tags: ">.< :< :> :| •≠• :3 ambiguous_gender black_eyes :d d: expressions food_creature
|
||||
frown group humor looking_at_viewer low_res mint not_furry :o o3o open_mouth plastic
|
||||
real smile tic_tac tongue tongue_out w4nw4n x_x"
|
||||
url: https://static1.e621.net/data/e5/1e/e51edfb8ec608817cca55d9bf7579c47.jpg
|
||||
- tags: absurd_res anthro bed bouquet canid canine canis clothing duo eyes_closed
|
||||
female flower_bouquet hair hi_res human larger_anthro larger_female male mammal
|
||||
size_difference sleeping smaller_human smaller_male spooning vanchamarl were werecanid
|
||||
werecanine werewolf wholesome wolf zephra
|
||||
url: https://static1.e621.net/data/14/b5/14b5352ee09a0dee5a565f5c0e6bfb3c.png
|
||||
- tags: 2016 anthro blue_eyes boss_monster bovid caprine cellphone crossover disney
|
||||
english_text female fur goat holding_object horn image_macro long_ears mammal
|
||||
meme monsters_inc open_mouth parody phone pixar reaction_image solo sulley taken_(movie)
|
||||
text toriel undertale video_games white_fur wolfjedisamuel
|
||||
url: https://static1.e621.net/data/fb/52/fb52b8b36ecb85eaf155855a23e70f24.png
|
||||
- tags: animated anthro asriel_dreemurr blush boss_monster bovid caprine clothing
|
||||
computer edit empty_eyes english_text floppy_ears goat happy hyperactive lol_comments
|
||||
male mammal o_o simple_background smile solo text undertale undertale-tales video_games
|
||||
url: https://static1.e621.net/data/bf/3c/bf3ce93ea4f3e7e3719c59b6557343b0.gif
|
||||
- tags: 2016 angry anthro antlers bell cervid collar computer furry_problems green_eyes
|
||||
horn laptop lol_comments male mammal reaction_image solo tirrel tirrel_(character)
|
||||
url: https://static1.e621.net/data/9c/39/9c39de16aa06ea3afbc0ca4cd3137960.jpg
|
||||
- tags: anthro big_breasts black_fur black_nose blue_eyes breasts claws clothed clothing
|
||||
english_text female fur giant_panda gillpanda gillpanda_(character) looking_at_viewer
|
||||
mammal morbidly_obese motivational_poster multicolored_fur obese overweight overweight_female
|
||||
pawpads pen sharp_teeth simple_background smile solo teeth text thumbs_up two_tone_fur
|
||||
ursid white_background white_fur
|
||||
url: https://static1.e621.net/data/fa/67/fa67485134b0ee19fdaa49a68161f4f5.jpg
|
||||
- tags: 2015 anthro asriel_dreemurr big_eyes boss_monster bovid caprine clothed clothing
|
||||
fangs fatz_geronimo goat hoodie horn long_ears male mammal purple_background reaction_image
|
||||
simple_background solo undertale video_games wide_eyed
|
||||
url: https://static1.e621.net/data/f1/8d/f18db1184cf4c62d7afbdf8fd28f39f7.png
|
||||
- tags: 2015 anatomy anthro asriel_dreemurr boss_monster bovid bukoya-star caprine
|
||||
chart child clothed clothing cub english_text flower fur goat green_eyes grey_background
|
||||
hi_res long_ears looking_at_viewer male mammal open_mouth plant simple_background
|
||||
solo text undertale video_games white_fur young
|
||||
url: https://static1.e621.net/data/b6/33/b633e381a700c619f7ea22666cf6b93d.jpg
|
||||
- tags: 2015 <3 anthro asriel_dreemurr_(god_form) blue_background boss_monster bovid
|
||||
caprine clothed clothing digital_media_(artwork) eating fluffy_hair fur gem goat
|
||||
hair horn looking_at_viewer male mammal pocky red_eyes robe saturnspace simple_background
|
||||
solo undertale video_games white_fur white_hair
|
||||
url: https://static1.e621.net/data/8b/80/8b8000f1845841e50515eb0aa224abfb.png
|
||||
- tags: 2015 ambiguous_gender baseball_cap beady_eyes black_eyes digital_media_(artwork)
|
||||
feral forked_tongue grey_tongue half-length_portrait hat lol_comments meme mostly_nude
|
||||
portrait purple_headwear python reptile scalie simple_background smile snake solo
|
||||
tongue tongue_out unknown_artist white_background
|
||||
url: https://static1.e621.net/data/bf/f6/bff630817262325b514eee7ff197802e.png
|
||||
- tags: 2015 ambiguous_gender avian beak bird bread cere_(feature) columbid common_pigeon
|
||||
crumbs cryptid-creations derp_eyes doing_it_wrong feathers feral flour folded_wings
|
||||
food humor in_bread lol_comments meme pigeon red_eyes simple_background solo watermark
|
||||
what white_background
|
||||
url: https://static1.e621.net/data/f9/1f/f91f92ac563c3bbe4e569d68a46dfafe.png
|
||||
- tags: 2015 ambiguous_gender anthro blue_eyes clothing cosplay costume day detailed_background
|
||||
dragon dreamworks falvie fudge_the_otter how_to_train_your_dragon kigurumi mammal
|
||||
membranous_wings mustelid night_fury open_mouth otter outside sitting sky smile
|
||||
solo star starry_sky sunset toothless wings
|
||||
url: https://static1.e621.net/data/0f/d1/0fd1d92190633265fb32a800d65c82d7.png
|
||||
- tags: 2015 5_fingers ambiguous_gender anthro black_lips black_nose canid canine
|
||||
canis controller domestic_cat duo eye_contact felid feline felis fluffy fluffy_tail
|
||||
fur grey_fur handpaw hax_(artist) inside jackal kenket lamp lofi long_mouth looking_at_another
|
||||
looking_down looking_up lying mammal no_sclera nude on_back orange_eyes pawpads
|
||||
paws pink_nose reaching remote_control romantic_couple shaded side_view slice_of_life
|
||||
smile snout sofa table tv_remote whiskers white_fur yellow_eyes
|
||||
url: https://static1.e621.net/data/0c/09/0c09080148c52ed89f16fe8fbc731dcf.jpg
|
||||
- tags: 2012 anthro beverage black_fur building cafe camera_view canid canine car
|
||||
chair cheek_tuft clothed clothing coffee coffee_mug coffee_shop cup detailed_background
|
||||
digital_media_(artwork) eyebrows fog food fox fur green_eyes hair half-length_portrait
|
||||
high-angle_view inside looking_at_viewer male mammal multicolored_fur orange_fur
|
||||
orange_hair portrait red_fox selfie sitting slice_of_life smile solo table thanshuhai
|
||||
tuft vehicle white_fur
|
||||
url: https://static1.e621.net/data/a0/47/a0476a240b0b780b926d831a3e6426ea.png
|
||||
- tags: angry blue_body blue_eyes blue_hair blush dialogue english_text equine eyelashes
|
||||
eyes_closed female friendship_is_magic hair horn horse humor long_hair mammal
|
||||
my_little_pony open_mouth orange_background pink_tongue princess_luna_(mlp) profanity
|
||||
reaction_image simple_background solo sweat teeth text the_truth tongue unknown_artist
|
||||
winged_unicorn wings
|
||||
url: https://static1.e621.net/data/ef/e5/efe507d244c5841d8496ce52f4f706a6.png
|
||||
- tags: "... ambiguous_gender bed bedding black_fur blanket checkered_background domestic_cat
|
||||
english_text felid feline felis female fur garfield_(series) lying mae_(nitw)
|
||||
mammal night_in_the_woods on_bed on_front pattern_background reaction_image simple_background
|
||||
solo speech_bubble text under_covers unknown_artist video_games"
|
||||
url: https://static1.e621.net/data/19/43/194344f13372ea3e80ef007dd56463c2.png
|
||||
- tags: 2015 anthro cheetah clothed clothing detailed_background felid feline female
|
||||
flashlight fully_clothed gun handgun holding_object mammal menacing muscular muscular_female
|
||||
outside pistol police ranged_weapon solo standing street_lamp sunrise tirrel uniform
|
||||
weapon yellow_sclera
|
||||
url: https://static1.e621.net/data/d3/ae/d3aee9d4de485f2be935e84ebbd971b7.jpg
|
||||
- tags: 2015 anthro antlers bell blue_eyes brown_hair canid canine car cervid cheetah
|
||||
clothed clothing collar comic dialogue driver driving duo english_text felid feline
|
||||
female green_eyes hair hat headlight hi_res horn humor inside_car male mammal
|
||||
outside police profanity running text tirrel tirrel_(character) uniform vehicle
|
||||
url: https://static1.e621.net/data/ed/e7/ede74f19c7adc6115fb751d729eceaf8.jpg
|
||||
- tags: 2013 ambiguous_gender angry brown_fur canid canine chibi dipstick_tail english_text
|
||||
feral fox fur humor mammal markings multicolored_fur multicolored_tail orange_fur
|
||||
profanity running serious simple_background socks_(marking) solo technicolorpie
|
||||
technicolor_pie text two_tone_tail white_background white_fur yellow_fur
|
||||
url: https://static1.e621.net/data/44/53/4453fcdd6d4ce1cf6d6da88c9a35bafc.png
|
||||
- tags: 2013 abstract_background ambiguous_gender anthro arm_support blonde_hair bow_tie
|
||||
bust_portrait clothed clothing coat cosplay digital_media_(artwork) felid fur
|
||||
gene_wilder hair half-closed_eyes hat humor inner_ear_fluff leaning_on_elbow lol_comments
|
||||
long_hair low_res mammal meme pantherine parody portrait reaction_image smile
|
||||
solo tassy tiger top_hat willy_wonka willy_wonka_and_the_chocolate_factory yellow_eyes
|
||||
yellow_fur
|
||||
url: https://static1.e621.net/data/d1/bb/d1bbf569513c5acf13b1be36d1d64bff.png
|
||||
- tags: begging begging_pose blue_fur blue_tail canid canine chest_tuft conditional_dnp
|
||||
dipstick_tail facial_markings fan_character feral feralized fluffy fur looking_away
|
||||
male mammal markings mask_(marking) multicolored_fur multicolored_tail nintendo
|
||||
pokémon pokémon_(species) portrait red_eyes riolu simple_background smile solo
|
||||
tuft two_tone_fur two_tone_tail umpherio_(umpherio) video_games white_background
|
||||
white_fur white_tail xnirox
|
||||
url: https://static1.e621.net/data/97/38/973887346d669d97c7a75b7324855773.png
|
||||
- tags: 2014 <3 anthro avian beak bird black_eyes black_nose blue_body blue_feathers
|
||||
blue_jay blush brown_fur cartoon_network chest_tuft comic corvid dialogue duo
|
||||
english_text eyes_closed fangs feathers fur grey_beak hi_res kissing kissing_cheek
|
||||
love male male/male mammal markings mordecai_(regular_show) open_beak open_mouth
|
||||
procyonid raccoon regular_show rigby_(regular_show) romantic_couple simple_background
|
||||
stripes surprise tan_fur teeth text tongue tuft white_background white_body white_feathers
|
||||
wholesome xiamtheferret
|
||||
url: https://static1.e621.net/data/ca/c9/cac98850f5b5b48440c100ad800caac1.png
|
||||
- tags: anthro blonde_hair blue_eyes bone_necklace breasts carnivore clothed clothing
|
||||
digital_media_(artwork) domestic_cat eating eyebrows felid feline felis female
|
||||
food front_view hair half-length_portrait holding_food holding_object inuki inuki_(character)
|
||||
jewelry mammal meat melloque necklace pink_nose ponytail portrait shirt shirt_logo
|
||||
signature simple_background smile solo steak teeth
|
||||
url: https://static1.e621.net/data/63/43/63433706609c428f2027986940547834.png
|
||||
- tags: 2015 applejack_(mlp) blonde_hair cutie_mark earth_pony equine face_paint feathers
|
||||
female feral friendship_is_magic green_eyes hair headdress hi_res horse jewelry
|
||||
looking_at_viewer mammal my_little_pony native_american necklace pony portrait
|
||||
santagiera solo teeth
|
||||
url: https://static1.e621.net/data/ac/85/ac85e5ea07ae712946bb897bff734d59.jpg
|
||||
- tags: 2015 animatronic anthro avian beak beverage bird blue_eyes blush bonnie_(fnaf)
|
||||
canid canine cellphone chica_(fnaf) chicken eye_patch eyewear five_nights_at_freddy's
|
||||
food fox foxy_(fnaf) freddy_(fnaf) group happy hat humor kissing lagomorph laugh
|
||||
looking_away machine male male/male mammal phone purple_eyes rabbit robot smile
|
||||
spin_the_bottle ursid video_games xiamtheferret yellow_eyes
|
||||
url: https://static1.e621.net/data/be/55/be5581393bcf2451ca90c16ba48c694c.png
|
||||
- tags: 2014 anthro antiander blue_eyes canid canine clothed clothing dress feathered_wings
|
||||
feathers female fox gloves hair heterochromia hi_res holly_(plant) long_hair mammal
|
||||
plant solo source_request white_hair wings
|
||||
url: https://static1.e621.net/data/7c/8a/7c8a07df68f6b8c93d425158396df811.png
|
||||
- tags: 2012 ambiguous_gender black_fur black_nose eeveelution fadingsky feral fire
|
||||
fur markings nintendo pink_background pokémon pokémon_(species) red_eyes simple_background
|
||||
smoke solo umbreon video_games yellow_markings
|
||||
url: https://static1.e621.net/data/d8/2f/d82fc9c3bfa78c17db4d117950063124.png
|
||||
- tags: 2014 ass_up avian beak blue_eyes blue_fur braeburned digital_media_(artwork)
|
||||
earth_pony equine feathered_wings feathers female feral friendship_is_magic fur
|
||||
gilda_(mlp) group gryphon hair horse laser laser_pointer mammal multicolored_hair
|
||||
my_little_pony pegasus pink_fur pink_hair pinkie_pie_(mlp) pony purple_eyes rainbow_dash_(mlp)
|
||||
rainbow_hair tongue tongue_out wings yellow_eyes
|
||||
url: https://static1.e621.net/data/8c/08/8c08e77a944dd6df29eb3e908f5bd93f.png
|
||||
- tags: ambiguous_gender anthro bust_portrait canid canine canis close-up darkwingo
|
||||
dingo eye_reflection fluffy green_background green_eyes head_tilt hi_res hybrid
|
||||
jewelry mammal markings necklace portrait scar simple_background smile solo thanshuhai
|
||||
wolf
|
||||
url: https://static1.e621.net/data/d7/10/d710ff227b96e59a655d152ac2fcc9f7.jpg
|
||||
- tags: 2014 <3 anthro beverage beverage_can black_hair blue_hair blue_highlights
|
||||
clothing collar digital_media_(artwork) domestic_cat fangs felid feline felis
|
||||
female food gradient_hair green_eyes hair highlights jacket looking_at_viewer
|
||||
mammal smile solo tentacletongue wolfy-nail young
|
||||
url: https://static1.e621.net/data/86/94/86942ce7f5be8ba952fef914c5593d2e.jpg
|
||||
- tags: 2012 ambiguous_gender bed bedding blanket blue_eyes eeveelution feral hi_res
|
||||
looking_at_viewer lying nintendo on_bed on_front open_mouth pawpads pokémon pokémon_(species)
|
||||
shadow solo tartii under_covers vaporeon video_games watermark
|
||||
url: https://static1.e621.net/data/c0/d4/c0d4964ebc0fb0eeddb61bc03cbc6a2b.jpg
|
||||
- tags: 2014 5:4 anthro atryl avian beak black_feathers brown_feathers cloud digital_media_(artwork)
|
||||
duo eye_contact feathered_wings feathers female friendship_is_magic genevieve_(mlp)
|
||||
grass greta_(mlp) grey_eyes gryphon hug my_little_pony one_eye_closed outside
|
||||
sky smile white_feathers wings wink
|
||||
url: https://static1.e621.net/data/67/c9/67c952de260ede67d0fe507bb99d3a6e.png
|
||||
- tags: 2014 3_toes ambiguous_gender animated anthro ass_up black_scales butt digitigrade
|
||||
dragon felid feral group hindpaw humor inside_train lamp loop low_res male mammal
|
||||
membranous_wings metro multicolored_scales paws pushing rubble scales scalie size_difference
|
||||
solo_focus subway tirrel toes train tunnel two_tone_scales vehicle white_scales
|
||||
wings
|
||||
url: https://static1.e621.net/data/7d/0d/7d0de8443bad63b6bd56a24ca0d0fb98.gif
|
||||
post_ids:
|
||||
- 864982
|
||||
- 831434
|
||||
- 604569
|
||||
- 1384351
|
||||
- 713232
|
||||
- 2058923
|
||||
- 348854
|
||||
- 677272
|
||||
- 721020
|
||||
- 783617
|
||||
- 524011
|
||||
- 738084
|
||||
- 1324650
|
||||
- 447317
|
||||
- 902353
|
||||
- 608312
|
||||
- 1196171
|
||||
- 539888
|
||||
- 426556
|
||||
- 1158932
|
||||
- 1787376
|
||||
- 844336
|
||||
- 133831
|
||||
- 543720
|
||||
- 675477
|
||||
- 468397
|
||||
- 480698
|
||||
- 1201721
|
||||
- 2104542
|
||||
- 1910123
|
||||
- 1162821
|
||||
- 1559520
|
||||
- 1559522
|
||||
- 1337331
|
||||
- 1289163
|
||||
- 1124466
|
||||
- 922038
|
||||
- 1051272
|
||||
- 1014268
|
||||
- 972377
|
||||
- 891419
|
||||
- 902353
|
||||
- 896454
|
||||
- 882158
|
||||
- 864033
|
||||
- 854142
|
||||
- 853653
|
||||
- 818700
|
||||
- 830459
|
||||
- 818006
|
||||
- 818294
|
||||
- 802009
|
||||
- 810790
|
||||
- 799347
|
||||
- 1401572
|
||||
- 766674
|
||||
- 735523
|
||||
- 722531
|
||||
- 721020
|
||||
- 2038195
|
||||
- 701921
|
||||
- 692610
|
||||
- 687969
|
||||
- 677272
|
||||
- 672692
|
||||
- 668603
|
||||
- 664515
|
||||
- 604569
|
||||
- 605113
|
||||
- 589740
|
||||
- 586160
|
||||
- 584859
|
||||
- 577089
|
||||
- 559155
|
||||
- 494915
|
||||
- 492433
|
||||
- 464910
|
||||
- 444992
|
||||
- 429880
|
||||
- 437103
|
||||
|
190
db/structure.sql
190
db/structure.sql
@ -412,7 +412,9 @@ CREATE TABLE public.blips (
|
||||
is_hidden boolean DEFAULT false,
|
||||
body_index tsvector NOT NULL,
|
||||
created_at timestamp without time zone NOT NULL,
|
||||
updated_at timestamp without time zone NOT NULL
|
||||
updated_at timestamp without time zone NOT NULL,
|
||||
warning_type integer,
|
||||
warning_user_id integer
|
||||
);
|
||||
|
||||
|
||||
@ -527,7 +529,9 @@ CREATE TABLE public.comments (
|
||||
updater_ip_addr inet,
|
||||
do_not_bump_post boolean DEFAULT false NOT NULL,
|
||||
is_hidden boolean DEFAULT false NOT NULL,
|
||||
is_sticky boolean DEFAULT false NOT NULL
|
||||
is_sticky boolean DEFAULT false NOT NULL,
|
||||
warning_type integer,
|
||||
warning_user_id integer
|
||||
);
|
||||
|
||||
|
||||
@ -919,7 +923,9 @@ CREATE TABLE public.forum_posts (
|
||||
is_hidden boolean DEFAULT false NOT NULL,
|
||||
created_at timestamp without time zone,
|
||||
updated_at timestamp without time zone,
|
||||
creator_ip_addr inet
|
||||
creator_ip_addr inet,
|
||||
warning_type integer,
|
||||
warning_user_id integer
|
||||
);
|
||||
|
||||
|
||||
@ -2016,6 +2022,73 @@ CREATE TABLE public.schema_migrations (
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: staff_audit_logs; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public.staff_audit_logs (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp(6) without time zone NOT NULL,
|
||||
updated_at timestamp(6) without time zone NOT NULL,
|
||||
user_id bigint NOT NULL,
|
||||
action character varying DEFAULT 'unknown_action'::character varying NOT NULL,
|
||||
"values" json
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: staff_audit_logs_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.staff_audit_logs_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
--
|
||||
-- Name: staff_audit_logs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.staff_audit_logs_id_seq OWNED BY public.staff_audit_logs.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: staff_notes; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public.staff_notes (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp(6) without time zone NOT NULL,
|
||||
updated_at timestamp(6) without time zone NOT NULL,
|
||||
user_id bigint NOT NULL,
|
||||
creator_id integer NOT NULL,
|
||||
body character varying,
|
||||
resolved boolean DEFAULT false NOT NULL
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: staff_notes_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.staff_notes_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
--
|
||||
-- Name: staff_notes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.staff_notes_id_seq OWNED BY public.staff_notes.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: tag_aliases; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
@ -2552,7 +2625,7 @@ CREATE TABLE public.users (
|
||||
password_hash character varying NOT NULL,
|
||||
email character varying,
|
||||
email_verification_key character varying,
|
||||
level integer DEFAULT 0 NOT NULL,
|
||||
level integer DEFAULT 20 NOT NULL,
|
||||
base_upload_limit integer DEFAULT 10 NOT NULL,
|
||||
last_logged_in_at timestamp without time zone,
|
||||
last_forum_read_at timestamp without time zone,
|
||||
@ -3019,6 +3092,20 @@ ALTER TABLE ONLY public.posts ALTER COLUMN change_seq SET DEFAULT nextval('publi
|
||||
ALTER TABLE ONLY public.saved_searches ALTER COLUMN id SET DEFAULT nextval('public.saved_searches_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: staff_audit_logs id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.staff_audit_logs ALTER COLUMN id SET DEFAULT nextval('public.staff_audit_logs_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: staff_notes id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.staff_notes ALTER COLUMN id SET DEFAULT nextval('public.staff_notes_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: tag_aliases id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
@ -3541,6 +3628,22 @@ ALTER TABLE ONLY public.schema_migrations
|
||||
ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version);
|
||||
|
||||
|
||||
--
|
||||
-- Name: staff_audit_logs staff_audit_logs_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.staff_audit_logs
|
||||
ADD CONSTRAINT staff_audit_logs_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: staff_notes staff_notes_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.staff_notes
|
||||
ADD CONSTRAINT staff_notes_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: tag_aliases tag_aliases_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@ -3879,6 +3982,13 @@ CREATE INDEX index_comment_votes_on_user_id ON public.comment_votes USING btree
|
||||
CREATE INDEX index_comments_on_body_index ON public.comments USING gin (body_index);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_comments_on_creator_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_comments_on_creator_id ON public.comments USING btree (creator_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_comments_on_creator_id_and_post_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@ -3970,6 +4080,20 @@ CREATE INDEX index_favorite_groups_on_creator_id ON public.favorite_groups USING
|
||||
CREATE INDEX index_favorite_groups_on_lower_name ON public.favorite_groups USING btree (lower(name));
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_favorites_on_post_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_favorites_on_post_id ON public.favorites USING btree (post_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_favorites_on_user_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_favorites_on_user_id ON public.favorites USING btree (user_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_favorites_on_user_id_and_post_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@ -4383,6 +4507,20 @@ CREATE INDEX index_post_versions_on_updater_id ON public.post_versions USING btr
|
||||
CREATE INDEX index_post_versions_on_updater_ip_addr ON public.post_versions USING btree (updater_ip_addr);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_post_votes_on_post_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_post_votes_on_post_id ON public.post_votes USING btree (post_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_post_votes_on_user_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_post_votes_on_user_id ON public.post_votes USING btree (user_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_post_votes_on_user_id_and_post_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@ -4481,6 +4619,27 @@ CREATE INDEX index_saved_searches_on_query ON public.saved_searches USING btree
|
||||
CREATE INDEX index_saved_searches_on_user_id ON public.saved_searches USING btree (user_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_staff_audit_logs_on_user_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_staff_audit_logs_on_user_id ON public.staff_audit_logs USING btree (user_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_staff_notes_on_creator_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_staff_notes_on_creator_id ON public.staff_notes USING btree (creator_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_staff_notes_on_user_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_staff_notes_on_user_id ON public.staff_notes USING btree (user_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_tag_aliases_on_antecedent_name; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@ -4824,6 +4983,14 @@ CREATE TRIGGER trigger_posts_on_tag_index_update BEFORE INSERT OR UPDATE ON publ
|
||||
CREATE TRIGGER trigger_wiki_pages_on_update BEFORE INSERT OR UPDATE ON public.wiki_pages FOR EACH ROW EXECUTE PROCEDURE tsvector_update_trigger('body_index', 'public.danbooru', 'body', 'title');
|
||||
|
||||
|
||||
--
|
||||
-- Name: staff_audit_logs fk_rails_02329e5ef9; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.staff_audit_logs
|
||||
ADD CONSTRAINT fk_rails_02329e5ef9 FOREIGN KEY (user_id) REFERENCES public.users(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: post_image_hashes fk_rails_2b7afcc2f0; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@ -4840,6 +5007,14 @@ ALTER TABLE ONLY public.favorites
|
||||
ADD CONSTRAINT fk_rails_a7668ef613 FOREIGN KEY (user_id) REFERENCES public.users(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: staff_notes fk_rails_bab7e2d92a; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.staff_notes
|
||||
ADD CONSTRAINT fk_rails_bab7e2d92a FOREIGN KEY (user_id) REFERENCES public.users(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: favorites fk_rails_d20e53bb68; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@ -5084,6 +5259,11 @@ INSERT INTO "schema_migrations" (version) VALUES
|
||||
('20201113073842'),
|
||||
('20201220172926'),
|
||||
('20201220190335'),
|
||||
('20210117173030');
|
||||
('20210117173030'),
|
||||
('20210405040522'),
|
||||
('20210425020131'),
|
||||
('20210426025625'),
|
||||
('20210430201028'),
|
||||
('20210506235640');
|
||||
|
||||
|
||||
|
@ -1,6 +1,63 @@
|
||||
FactoryBot.define do
|
||||
factory(:post_replacement) do
|
||||
original_url { FFaker::Internet.http_url }
|
||||
creator_ip_addr { "127.0.0.1" }
|
||||
replacement_url { FFaker::Internet.http_url }
|
||||
reason { FFaker::Lorem.words.join(" ") }
|
||||
|
||||
factory(:webm_replacement) do
|
||||
replacement_file do
|
||||
f = Tempfile.new
|
||||
IO.copy_stream("#{Rails.root}/test/files/test-512x512.webm", f.path)
|
||||
ActionDispatch::Http::UploadedFile.new(tempfile: f, filename: "video.webm")
|
||||
end
|
||||
end
|
||||
|
||||
factory(:mp4_replacement) do
|
||||
replacement_file do
|
||||
f = Tempfile.new
|
||||
IO.copy_stream("#{Rails.root}/test/files/test-300x300.mp4", f.path)
|
||||
ActionDispatch::Http::UploadedFile.new(tempfile: f, filename: "video.mp4")
|
||||
end
|
||||
end
|
||||
|
||||
factory(:jpg_replacement) do
|
||||
replacement_file do
|
||||
f = Tempfile.new
|
||||
IO.copy_stream("#{Rails.root}/test/files/test.jpg", f.path)
|
||||
ActionDispatch::Http::UploadedFile.new(tempfile: f, filename: "test.jpg")
|
||||
end
|
||||
end
|
||||
|
||||
factory(:jpg_invalid_replacement) do
|
||||
replacement_file do
|
||||
f = Tempfile.new
|
||||
IO.copy_stream("#{Rails.root}/test/files/test-corrupt.jpg", f.path)
|
||||
ActionDispatch::Http::UploadedFile.new(tempfile: f, filename: "test.jpg")
|
||||
end
|
||||
end
|
||||
|
||||
factory(:gif_replacement) do
|
||||
replacement_file do
|
||||
f = Tempfile.new
|
||||
IO.copy_stream("#{Rails.root}/test/files/test.gif", f.path)
|
||||
ActionDispatch::Http::UploadedFile.new(tempfile: f, filename: "test.gif")
|
||||
end
|
||||
end
|
||||
|
||||
factory(:empty_replacement) do
|
||||
replacement_file do
|
||||
f = Tempfile.new
|
||||
IO.copy_stream("#{Rails.root}/test/files/empty.jpg", f.path)
|
||||
ActionDispatch::Http::UploadedFile.new(tempfile: f, filename: "test.jpg")
|
||||
end
|
||||
end
|
||||
|
||||
factory(:png_replacement) do
|
||||
replacement_file do
|
||||
f = Tempfile.new
|
||||
IO.copy_stream("#{Rails.root}/test/files/test.png", f.path)
|
||||
ActionDispatch::Http::UploadedFile.new(tempfile: f, filename: "test.png")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
0
test/files/empty.jpg
Normal file
0
test/files/empty.jpg
Normal file
@ -14,19 +14,20 @@ class PostReplacementsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
@user = create(:moderator_user, can_approve_posts: true, created_at: 1.month.ago)
|
||||
@user.as_current do
|
||||
UploadWhitelist.create!(pattern: "*raikou1.donmai.us/*", reason: "test")
|
||||
@post = create(:post, source: "https://google.com")
|
||||
@post_replacement = create(:post_replacement, post: @post)
|
||||
@upload = UploadService.new(FactoryBot.attributes_for(:jpg_upload).merge({uploader: @user})).start!
|
||||
@post = @upload.post
|
||||
@replacement = create(:png_replacement, creator: @user, post: @post)
|
||||
end
|
||||
end
|
||||
|
||||
context "create action" do
|
||||
should "render" do
|
||||
should "accept new non duplicate replacement" do
|
||||
file = Rack::Test::UploadedFile.new("#{Rails.root}/test/files/alpha.png", "image/png")
|
||||
params = {
|
||||
format: :json,
|
||||
post_id: @post.id,
|
||||
post_replacement: {
|
||||
replacement_url: "https://raikou1.donmai.us/d3/4e/d34e4cf0a437a5d65f8e82b7bcd02606.jpg",
|
||||
replacement_file: file,
|
||||
reason: 'test replacement'
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,38 +36,54 @@ class PostReplacementsControllerTest < ActionDispatch::IntegrationTest
|
||||
@post.reload
|
||||
end
|
||||
|
||||
# travel_to(Time.now + PostReplacement::DELETION_GRACE_PERIOD + 1.day) do
|
||||
# Delayed::Worker.new.work_off
|
||||
# end
|
||||
|
||||
assert_response :success
|
||||
assert_equal("https://raikou1.donmai.us/d3/4e/d34e4cf0a437a5d65f8e82b7bcd02606.jpg", @post.source)
|
||||
assert_equal("d34e4cf0a437a5d65f8e82b7bcd02606", @post.md5)
|
||||
assert_equal("d34e4cf0a437a5d65f8e82b7bcd02606", Digest::MD5.file(@post.file(:original)).hexdigest)
|
||||
assert_redirected_to post_path(@post)
|
||||
end
|
||||
end
|
||||
|
||||
context "update action" do
|
||||
should "update the replacement" do
|
||||
params = {
|
||||
format: :json,
|
||||
id: @post_replacement.id,
|
||||
post_replacement: {
|
||||
file_size_was: 23,
|
||||
file_size: 42,
|
||||
}
|
||||
}
|
||||
context "reject action" do
|
||||
should "reject replacement" do
|
||||
put_auth reject_post_replacement_path(@replacement), @user
|
||||
assert_redirected_to post_replacement_path(@replacement)
|
||||
@replacement.reload
|
||||
@post.reload
|
||||
assert_equal @replacement.status, "rejected"
|
||||
assert_not_equal @post.md5, @replacement.md5
|
||||
end
|
||||
end
|
||||
|
||||
put_auth post_replacement_path(@post_replacement), @user, params: params
|
||||
@post_replacement.reload
|
||||
assert_equal(23, @post_replacement.file_size_was)
|
||||
assert_equal(42, @post_replacement.file_size)
|
||||
context "approve action" do
|
||||
should "replace post" do
|
||||
put_auth approve_post_replacement_path(@replacement), @user
|
||||
assert_redirected_to post_path(@post)
|
||||
@replacement.reload
|
||||
@post.reload
|
||||
assert_equal @replacement.md5, @post.md5
|
||||
assert_equal @replacement.status, "approved"
|
||||
end
|
||||
end
|
||||
|
||||
context "promote action" do
|
||||
should "create post" do
|
||||
put_auth promote_post_replacement_path(@replacement), @user
|
||||
last_post = Post.last
|
||||
assert_redirected_to post_path(last_post)
|
||||
@replacement.reload
|
||||
@post.reload
|
||||
assert_equal @replacement.md5, last_post.md5
|
||||
assert_equal @replacement.status, "promoted"
|
||||
end
|
||||
end
|
||||
|
||||
context "index action" do
|
||||
should "render" do
|
||||
get post_replacements_path, params: {format: "json"}
|
||||
get post_replacements_path
|
||||
assert_response :success
|
||||
end
|
||||
end
|
||||
|
||||
context "new action" do
|
||||
should "render" do
|
||||
get_auth new_post_replacement_path, @user, params: {post_id: @post.id}
|
||||
assert_response :success
|
||||
end
|
||||
end
|
||||
|
@ -1,115 +0,0 @@
|
||||
require 'test_helper'
|
||||
|
||||
class TagAutocompleteTest < ActiveSupport::TestCase
|
||||
subject { TagAutocomplete }
|
||||
|
||||
context "#search" do
|
||||
should "be case insensitive" do
|
||||
create(:tag, name: "abcdef", post_count: 1)
|
||||
assert_equal(["abcdef"], subject.search("A").map(&:name))
|
||||
end
|
||||
|
||||
should "not return duplicates" do
|
||||
create(:tag, name: "red_eyes", post_count: 5001)
|
||||
assert_equal(%w[red_eyes], subject.search("re").map(&:name))
|
||||
end
|
||||
end
|
||||
|
||||
context "#search_exact" do
|
||||
setup do
|
||||
@tags = [
|
||||
create(:tag, name: "abcdef", post_count: 1),
|
||||
create(:tag, name: "abczzz", post_count: 2),
|
||||
create(:tag, name: "abcyyy", post_count: 0),
|
||||
create(:tag, name: "bbbbbb")
|
||||
]
|
||||
end
|
||||
|
||||
should "find the tags" do
|
||||
expected = [
|
||||
@tags[1],
|
||||
@tags[0]
|
||||
].map(&:name)
|
||||
assert_equal(expected, subject.search_exact("abc", 3).map(&:name))
|
||||
end
|
||||
end
|
||||
|
||||
context "#search_correct" do
|
||||
setup do
|
||||
CurrentUser.stubs(:id).returns(1)
|
||||
|
||||
@tags = [
|
||||
create(:tag, name: "abcde", post_count: 1),
|
||||
create(:tag, name: "abcdz", post_count: 2),
|
||||
|
||||
# one char mismatch
|
||||
create(:tag, name: "abcez", post_count: 2),
|
||||
|
||||
# too long
|
||||
create(:tag, name: "abcdefghijk", post_count: 2),
|
||||
|
||||
# wrong prefix
|
||||
create(:tag, name: "bbcdef", post_count: 2),
|
||||
|
||||
# zero post count
|
||||
create(:tag, name: "abcdy", post_count: 0),
|
||||
|
||||
# completely different
|
||||
create(:tag, name: "bbbbb")
|
||||
]
|
||||
end
|
||||
|
||||
should "find the tags" do
|
||||
expected = [
|
||||
@tags[0],
|
||||
@tags[1],
|
||||
@tags[2]
|
||||
].map(&:name)
|
||||
assert_equal(expected, subject.search_correct("abcd", 3).map(&:name))
|
||||
end
|
||||
end
|
||||
|
||||
context "#search_prefix" do
|
||||
setup do
|
||||
@tags = [
|
||||
create(:tag, name: "abcdef", post_count: 1),
|
||||
create(:tag, name: "alpha_beta_cat", post_count: 2),
|
||||
create(:tag, name: "alpha_beta_dat", post_count: 0),
|
||||
create(:tag, name: "alpha_beta_(cane)", post_count: 2),
|
||||
create(:tag, name: "alpha_beta/cane", post_count: 2)
|
||||
]
|
||||
end
|
||||
|
||||
should "find the tags" do
|
||||
expected = [
|
||||
@tags[1],
|
||||
@tags[3],
|
||||
@tags[4]
|
||||
].map(&:name)
|
||||
assert_equal(expected, subject.search_prefix("abc", 3).map(&:name))
|
||||
end
|
||||
end
|
||||
|
||||
context "#search_aliases" do
|
||||
setup do
|
||||
@user = create(:user)
|
||||
@tags = [
|
||||
create(:tag, name: "/abc", post_count: 0),
|
||||
create(:tag, name: "abcdef", post_count: 1),
|
||||
create(:tag, name: "zzzzzz", post_count: 1),
|
||||
]
|
||||
as_user do
|
||||
@aliases = [
|
||||
create(:tag_alias, antecedent_name: "/abc", consequent_name: "abcdef", status: "active", post_count: 1)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
should "find the tags" do
|
||||
results = subject.search_aliases("/abc", 3)
|
||||
assert_equal(1, results.size)
|
||||
assert_equal("abcdef", results[0].name)
|
||||
assert_equal("/abc", results[0].antecedent_name)
|
||||
end
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user