Merge branch 'master' into minor_fix
This commit is contained in:
6
Gemfile
6
Gemfile
@@ -40,7 +40,8 @@ gem 'puma'
|
|||||||
gem 'scenic'
|
gem 'scenic'
|
||||||
gem 'ipaddress_2'
|
gem 'ipaddress_2'
|
||||||
gem 'http'
|
gem 'http'
|
||||||
gem 'activerecord-hierarchical_query'
|
gem 'activerecord-hierarchical_query', git: "https://github.com/walski/activerecord-hierarchical_query", branch: "rails-6-1"
|
||||||
|
gem 'http-cookie', git: "https://github.com/danbooru/http-cookie"
|
||||||
gem 'pundit'
|
gem 'pundit'
|
||||||
gem 'mail'
|
gem 'mail'
|
||||||
gem 'nokogiri'
|
gem 'nokogiri'
|
||||||
@@ -59,7 +60,7 @@ end
|
|||||||
group :development do
|
group :development do
|
||||||
gem 'rubocop'
|
gem 'rubocop'
|
||||||
gem 'rubocop-rails'
|
gem 'rubocop-rails'
|
||||||
gem 'meta_request'
|
# gem 'meta_request' # hangs on Rails 6.1
|
||||||
gem 'rack-mini-profiler'
|
gem 'rack-mini-profiler'
|
||||||
gem 'stackprof'
|
gem 'stackprof'
|
||||||
gem 'flamegraph'
|
gem 'flamegraph'
|
||||||
@@ -85,4 +86,5 @@ group :test do
|
|||||||
gem "capybara"
|
gem "capybara"
|
||||||
gem "selenium-webdriver"
|
gem "selenium-webdriver"
|
||||||
gem "codecov", require: false
|
gem "codecov", require: false
|
||||||
|
gem 'stripe-ruby-mock', require: "stripe_mock"
|
||||||
end
|
end
|
||||||
|
|||||||
218
Gemfile.lock
218
Gemfile.lock
@@ -1,3 +1,10 @@
|
|||||||
|
GIT
|
||||||
|
remote: https://github.com/danbooru/http-cookie
|
||||||
|
revision: 382d8a641e4df226e0e7b0d2bfaeadb2fe71dd84
|
||||||
|
specs:
|
||||||
|
http-cookie (1.0.4)
|
||||||
|
domain_name (~> 0.5)
|
||||||
|
|
||||||
GIT
|
GIT
|
||||||
remote: https://github.com/evazion/dtext_rb.git
|
remote: https://github.com/evazion/dtext_rb.git
|
||||||
revision: a95bf1d537cbdba4585adb8e123f03f001f56fd7
|
revision: a95bf1d537cbdba4585adb8e123f03f001f56fd7
|
||||||
@@ -5,71 +12,81 @@ GIT
|
|||||||
dtext_rb (1.10.6)
|
dtext_rb (1.10.6)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
|
|
||||||
|
GIT
|
||||||
|
remote: https://github.com/walski/activerecord-hierarchical_query
|
||||||
|
revision: 3d6663307ed2f6a23347084c04700a26c7e7bb55
|
||||||
|
branch: rails-6-1
|
||||||
|
specs:
|
||||||
|
activerecord-hierarchical_query (1.2.3)
|
||||||
|
activerecord (>= 5.0, < 6.2)
|
||||||
|
pg (>= 0.21, < 1.3)
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.0.3.4)
|
actioncable (6.1.0)
|
||||||
actionpack (= 6.0.3.4)
|
actionpack (= 6.1.0)
|
||||||
|
activesupport (= 6.1.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.0.3.4)
|
actionmailbox (6.1.0)
|
||||||
actionpack (= 6.0.3.4)
|
actionpack (= 6.1.0)
|
||||||
activejob (= 6.0.3.4)
|
activejob (= 6.1.0)
|
||||||
activerecord (= 6.0.3.4)
|
activerecord (= 6.1.0)
|
||||||
activestorage (= 6.0.3.4)
|
activestorage (= 6.1.0)
|
||||||
activesupport (= 6.0.3.4)
|
activesupport (= 6.1.0)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.0.3.4)
|
actionmailer (6.1.0)
|
||||||
actionpack (= 6.0.3.4)
|
actionpack (= 6.1.0)
|
||||||
actionview (= 6.0.3.4)
|
actionview (= 6.1.0)
|
||||||
activejob (= 6.0.3.4)
|
activejob (= 6.1.0)
|
||||||
|
activesupport (= 6.1.0)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (6.0.3.4)
|
actionpack (6.1.0)
|
||||||
actionview (= 6.0.3.4)
|
actionview (= 6.1.0)
|
||||||
activesupport (= 6.0.3.4)
|
activesupport (= 6.1.0)
|
||||||
rack (~> 2.0, >= 2.0.8)
|
rack (~> 2.0, >= 2.0.9)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (6.0.3.4)
|
actiontext (6.1.0)
|
||||||
actionpack (= 6.0.3.4)
|
actionpack (= 6.1.0)
|
||||||
activerecord (= 6.0.3.4)
|
activerecord (= 6.1.0)
|
||||||
activestorage (= 6.0.3.4)
|
activestorage (= 6.1.0)
|
||||||
activesupport (= 6.0.3.4)
|
activesupport (= 6.1.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.0.3.4)
|
actionview (6.1.0)
|
||||||
activesupport (= 6.0.3.4)
|
activesupport (= 6.1.0)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||||
activejob (6.0.3.4)
|
activejob (6.1.0)
|
||||||
activesupport (= 6.0.3.4)
|
activesupport (= 6.1.0)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.0.3.4)
|
activemodel (6.1.0)
|
||||||
activesupport (= 6.0.3.4)
|
activesupport (= 6.1.0)
|
||||||
activemodel-serializers-xml (1.0.2)
|
activemodel-serializers-xml (1.0.2)
|
||||||
activemodel (> 5.x)
|
activemodel (> 5.x)
|
||||||
activesupport (> 5.x)
|
activesupport (> 5.x)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
activerecord (6.0.3.4)
|
activerecord (6.1.0)
|
||||||
activemodel (= 6.0.3.4)
|
activemodel (= 6.1.0)
|
||||||
activesupport (= 6.0.3.4)
|
activesupport (= 6.1.0)
|
||||||
activerecord-hierarchical_query (1.2.3)
|
activestorage (6.1.0)
|
||||||
activerecord (>= 5.0, < 6.1)
|
actionpack (= 6.1.0)
|
||||||
pg (>= 0.21, < 1.3)
|
activejob (= 6.1.0)
|
||||||
activestorage (6.0.3.4)
|
activerecord (= 6.1.0)
|
||||||
actionpack (= 6.0.3.4)
|
activesupport (= 6.1.0)
|
||||||
activejob (= 6.0.3.4)
|
|
||||||
activerecord (= 6.0.3.4)
|
|
||||||
marcel (~> 0.3.1)
|
marcel (~> 0.3.1)
|
||||||
activesupport (6.0.3.4)
|
mimemagic (~> 0.3.2)
|
||||||
|
activesupport (6.1.0)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (~> 5.1)
|
minitest (>= 5.1)
|
||||||
tzinfo (~> 1.1)
|
tzinfo (~> 2.0)
|
||||||
zeitwerk (~> 2.2, >= 2.2.2)
|
zeitwerk (~> 2.3)
|
||||||
addressable (2.7.0)
|
addressable (2.7.0)
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
airbrussh (1.4.0)
|
airbrussh (1.4.0)
|
||||||
@@ -77,13 +94,13 @@ GEM
|
|||||||
ansi (1.5.0)
|
ansi (1.5.0)
|
||||||
ast (2.4.1)
|
ast (2.4.1)
|
||||||
aws-eventstream (1.1.0)
|
aws-eventstream (1.1.0)
|
||||||
aws-partitions (1.402.0)
|
aws-partitions (1.414.0)
|
||||||
aws-sdk-core (3.109.3)
|
aws-sdk-core (3.110.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.239.0)
|
aws-partitions (~> 1, >= 1.239.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-sqs (1.34.0)
|
aws-sdk-sqs (1.35.0)
|
||||||
aws-sdk-core (~> 3, >= 3.109.0)
|
aws-sdk-core (~> 3, >= 3.109.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sigv4 (1.2.2)
|
aws-sigv4 (1.2.2)
|
||||||
@@ -120,20 +137,20 @@ GEM
|
|||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
childprocess (3.0.0)
|
childprocess (3.0.0)
|
||||||
chronic (0.10.2)
|
chronic (0.10.2)
|
||||||
codecov (0.2.12)
|
codecov (0.2.15)
|
||||||
json
|
simplecov (>= 0.15, < 0.21)
|
||||||
simplecov
|
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
concurrent-ruby (1.1.7)
|
concurrent-ruby (1.1.7)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
daemons (1.3.1)
|
daemons (1.3.1)
|
||||||
delayed_job (4.1.8)
|
dante (0.2.0)
|
||||||
activesupport (>= 3.0, < 6.1)
|
delayed_job (4.1.9)
|
||||||
delayed_job_active_record (4.1.4)
|
activesupport (>= 3.0, < 6.2)
|
||||||
activerecord (>= 3.0, < 6.1)
|
delayed_job_active_record (4.1.5)
|
||||||
|
activerecord (>= 3.0, < 6.2)
|
||||||
delayed_job (>= 3.0, < 5)
|
delayed_job (>= 3.0, < 5)
|
||||||
diff-lcs (1.4.4)
|
diff-lcs (1.4.4)
|
||||||
docile (1.3.2)
|
docile (1.3.4)
|
||||||
domain_name (0.5.20190701)
|
domain_name (0.5.20190701)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
dotenv (2.7.6)
|
dotenv (2.7.6)
|
||||||
@@ -143,11 +160,13 @@ GEM
|
|||||||
erubi (1.10.0)
|
erubi (1.10.0)
|
||||||
factory_bot (6.1.0)
|
factory_bot (6.1.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
faraday (1.1.0)
|
faraday (1.3.0)
|
||||||
|
faraday-net_http (~> 1.0)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
ruby2_keywords
|
ruby2_keywords
|
||||||
|
faraday-net_http (1.0.0)
|
||||||
ffaker (2.17.0)
|
ffaker (2.17.0)
|
||||||
ffi (1.13.1)
|
ffi (1.14.2)
|
||||||
ffi-compiler (1.0.1)
|
ffi-compiler (1.0.1)
|
||||||
ffi (>= 1.0.0)
|
ffi (>= 1.0.0)
|
||||||
rake
|
rake
|
||||||
@@ -161,19 +180,17 @@ GEM
|
|||||||
http-cookie (~> 1.0)
|
http-cookie (~> 1.0)
|
||||||
http-form_data (~> 2.2)
|
http-form_data (~> 2.2)
|
||||||
http-parser (~> 1.2.0)
|
http-parser (~> 1.2.0)
|
||||||
http-cookie (1.0.3)
|
|
||||||
domain_name (~> 0.5)
|
|
||||||
http-form_data (2.3.0)
|
http-form_data (2.3.0)
|
||||||
http-parser (1.2.2)
|
http-parser (1.2.2)
|
||||||
ffi-compiler
|
ffi-compiler
|
||||||
i18n (1.8.5)
|
i18n (1.8.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
ipaddress_2 (0.13.0)
|
ipaddress_2 (0.13.0)
|
||||||
jmespath (1.4.0)
|
jmespath (1.4.0)
|
||||||
json (2.3.1)
|
json (2.5.1)
|
||||||
jwt (2.2.2)
|
jwt (2.2.2)
|
||||||
kgio (2.11.3)
|
kgio (2.11.3)
|
||||||
listen (3.3.3)
|
listen (3.4.0)
|
||||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
rb-inotify (~> 0.9, >= 0.9.10)
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
loofah (2.8.0)
|
loofah (2.8.0)
|
||||||
@@ -185,9 +202,6 @@ GEM
|
|||||||
mimemagic (~> 0.3.2)
|
mimemagic (~> 0.3.2)
|
||||||
memoist (0.16.2)
|
memoist (0.16.2)
|
||||||
memory_profiler (1.0.0)
|
memory_profiler (1.0.0)
|
||||||
meta_request (0.7.2)
|
|
||||||
rack-contrib (>= 1.1, < 3)
|
|
||||||
railties (>= 3.0.0, < 7)
|
|
||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
mimemagic (0.3.5)
|
mimemagic (0.3.5)
|
||||||
mini_mime (1.0.2)
|
mini_mime (1.0.2)
|
||||||
@@ -200,7 +214,7 @@ GEM
|
|||||||
builder
|
builder
|
||||||
minitest (>= 5.0)
|
minitest (>= 5.0)
|
||||||
ruby-progressbar
|
ruby-progressbar
|
||||||
mocha (1.11.2)
|
mocha (1.12.0)
|
||||||
mock_redis (0.26.0)
|
mock_redis (0.26.0)
|
||||||
msgpack (1.3.3)
|
msgpack (1.3.3)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
@@ -224,7 +238,7 @@ GEM
|
|||||||
multi_xml (~> 0.5)
|
multi_xml (~> 0.5)
|
||||||
rack (>= 1.2, < 3)
|
rack (>= 1.2, < 3)
|
||||||
parallel (1.20.1)
|
parallel (1.20.1)
|
||||||
parser (2.7.2.0)
|
parser (3.0.0.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
pg (1.2.3)
|
pg (1.2.3)
|
||||||
pry (0.13.1)
|
pry (0.13.1)
|
||||||
@@ -236,48 +250,46 @@ GEM
|
|||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (4.0.6)
|
public_suffix (4.0.6)
|
||||||
puma (5.1.0)
|
puma (5.1.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.1.0)
|
pundit (2.1.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
rack (2.2.3)
|
rack (2.2.3)
|
||||||
rack-contrib (2.3.0)
|
rack-mini-profiler (2.3.0)
|
||||||
rack (~> 2.0)
|
|
||||||
rack-mini-profiler (2.2.0)
|
|
||||||
rack (>= 1.2.0)
|
rack (>= 1.2.0)
|
||||||
rack-proxy (0.6.5)
|
rack-proxy (0.6.5)
|
||||||
rack
|
rack
|
||||||
rack-test (1.1.0)
|
rack-test (1.1.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rails (6.0.3.4)
|
rails (6.1.0)
|
||||||
actioncable (= 6.0.3.4)
|
actioncable (= 6.1.0)
|
||||||
actionmailbox (= 6.0.3.4)
|
actionmailbox (= 6.1.0)
|
||||||
actionmailer (= 6.0.3.4)
|
actionmailer (= 6.1.0)
|
||||||
actionpack (= 6.0.3.4)
|
actionpack (= 6.1.0)
|
||||||
actiontext (= 6.0.3.4)
|
actiontext (= 6.1.0)
|
||||||
actionview (= 6.0.3.4)
|
actionview (= 6.1.0)
|
||||||
activejob (= 6.0.3.4)
|
activejob (= 6.1.0)
|
||||||
activemodel (= 6.0.3.4)
|
activemodel (= 6.1.0)
|
||||||
activerecord (= 6.0.3.4)
|
activerecord (= 6.1.0)
|
||||||
activestorage (= 6.0.3.4)
|
activestorage (= 6.1.0)
|
||||||
activesupport (= 6.0.3.4)
|
activesupport (= 6.1.0)
|
||||||
bundler (>= 1.3.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.0.3.4)
|
railties (= 6.1.0)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.0.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.3.0)
|
rails-html-sanitizer (1.3.0)
|
||||||
loofah (~> 2.3)
|
loofah (~> 2.3)
|
||||||
railties (6.0.3.4)
|
railties (6.1.0)
|
||||||
actionpack (= 6.0.3.4)
|
actionpack (= 6.1.0)
|
||||||
activesupport (= 6.0.3.4)
|
activesupport (= 6.1.0)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 0.8.7)
|
rake (>= 0.8.7)
|
||||||
thor (>= 0.20.3, < 2.0)
|
thor (~> 1.0)
|
||||||
rainbow (3.0.0)
|
rainbow (3.0.0)
|
||||||
raindrops (0.19.1)
|
raindrops (0.19.1)
|
||||||
rake (13.0.1)
|
rake (13.0.3)
|
||||||
rakismet (1.5.4)
|
rakismet (1.5.4)
|
||||||
rb-fsevent (0.10.4)
|
rb-fsevent (0.10.4)
|
||||||
rb-inotify (0.10.1)
|
rb-inotify (0.10.1)
|
||||||
@@ -292,22 +304,22 @@ GEM
|
|||||||
actionpack (>= 5.0)
|
actionpack (>= 5.0)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
rexml (3.2.4)
|
rexml (3.2.4)
|
||||||
rubocop (1.4.2)
|
rubocop (1.7.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 2.7.1.5)
|
parser (>= 2.7.1.5)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8)
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
rexml
|
rexml
|
||||||
rubocop-ast (>= 1.1.1)
|
rubocop-ast (>= 1.2.0, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 2.0)
|
unicode-display_width (>= 1.4.0, < 2.0)
|
||||||
rubocop-ast (1.3.0)
|
rubocop-ast (1.4.0)
|
||||||
parser (>= 2.7.1.5)
|
parser (>= 2.7.1.5)
|
||||||
rubocop-rails (2.8.1)
|
rubocop-rails (2.9.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 0.87.0)
|
rubocop (>= 0.90.0, < 2.0)
|
||||||
ruby-progressbar (1.10.1)
|
ruby-progressbar (1.11.0)
|
||||||
ruby-vips (2.0.17)
|
ruby-vips (2.0.17)
|
||||||
ffi (~> 1.9)
|
ffi (~> 1.9)
|
||||||
ruby2_keywords (0.0.2)
|
ruby2_keywords (0.0.2)
|
||||||
@@ -349,15 +361,18 @@ GEM
|
|||||||
streamio-ffmpeg (3.0.2)
|
streamio-ffmpeg (3.0.2)
|
||||||
multi_json (~> 1.8)
|
multi_json (~> 1.8)
|
||||||
stripe (5.28.0)
|
stripe (5.28.0)
|
||||||
|
stripe-ruby-mock (3.0.1)
|
||||||
|
dante (>= 0.2.0)
|
||||||
|
multi_json (~> 1.0)
|
||||||
|
stripe (> 5, < 6)
|
||||||
thor (1.0.1)
|
thor (1.0.1)
|
||||||
thread_safe (0.3.6)
|
tzinfo (2.0.4)
|
||||||
tzinfo (1.2.8)
|
concurrent-ruby (~> 1.0)
|
||||||
thread_safe (~> 0.1)
|
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.7.7)
|
unf_ext (0.0.7.7)
|
||||||
unicode-display_width (1.7.0)
|
unicode-display_width (1.7.0)
|
||||||
unicorn (5.7.0)
|
unicorn (5.8.0)
|
||||||
kgio (~> 2.6)
|
kgio (~> 2.6)
|
||||||
raindrops (~> 0.7)
|
raindrops (~> 0.7)
|
||||||
unicorn-worker-killer (0.4.4)
|
unicorn-worker-killer (0.4.4)
|
||||||
@@ -382,7 +397,7 @@ PLATFORMS
|
|||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
activemodel-serializers-xml
|
activemodel-serializers-xml
|
||||||
activerecord-hierarchical_query
|
activerecord-hierarchical_query!
|
||||||
addressable
|
addressable
|
||||||
aws-sdk-sqs (~> 1)
|
aws-sdk-sqs (~> 1)
|
||||||
bcrypt
|
bcrypt
|
||||||
@@ -405,12 +420,12 @@ DEPENDENCIES
|
|||||||
ffaker
|
ffaker
|
||||||
flamegraph
|
flamegraph
|
||||||
http
|
http
|
||||||
|
http-cookie!
|
||||||
ipaddress_2
|
ipaddress_2
|
||||||
listen
|
listen
|
||||||
mail
|
mail
|
||||||
memoist
|
memoist
|
||||||
memory_profiler
|
memory_profiler
|
||||||
meta_request
|
|
||||||
minitest-ci
|
minitest-ci
|
||||||
minitest-reporters
|
minitest-reporters
|
||||||
mocha
|
mocha
|
||||||
@@ -446,6 +461,7 @@ DEPENDENCIES
|
|||||||
stackprof
|
stackprof
|
||||||
streamio-ffmpeg
|
streamio-ffmpeg
|
||||||
stripe
|
stripe
|
||||||
|
stripe-ruby-mock
|
||||||
unicorn
|
unicorn
|
||||||
unicorn-worker-killer
|
unicorn-worker-killer
|
||||||
webpacker (>= 4.0.x)
|
webpacker (>= 4.0.x)
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ apt-get -y install zlib1g-dev libglib2.0-dev
|
|||||||
apt-get -y install $LIBSSL_DEV_PKG build-essential automake libxml2-dev libxslt-dev ncurses-dev sudo libreadline-dev flex bison ragel redis git curl libcurl4-openssl-dev sendmail-bin sendmail nginx ssh coreutils ffmpeg mkvtoolnix
|
apt-get -y install $LIBSSL_DEV_PKG build-essential automake libxml2-dev libxslt-dev ncurses-dev sudo libreadline-dev flex bison ragel redis 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 libpq-dev postgresql-client
|
||||||
apt-get -y install liblcms2-dev $LIBJPEG_TURBO_DEV_PKG libexpat1-dev libgif-dev libpng-dev libexif-dev
|
apt-get -y install liblcms2-dev $LIBJPEG_TURBO_DEV_PKG libexpat1-dev libgif-dev libpng-dev libexif-dev
|
||||||
|
apt-get -y install gcc g++
|
||||||
|
|
||||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
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
|
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||||
@@ -76,7 +77,7 @@ chsh -s /bin/bash danbooru
|
|||||||
usermod -G danbooru,sudo danbooru
|
usermod -G danbooru,sudo danbooru
|
||||||
|
|
||||||
# Set up Postgres
|
# Set up Postgres
|
||||||
export PG_VERSION=`pg_config --version | egrep -o '[0-9]{1,}\.[0-9]{1,}'`
|
export PG_VERSION=`pg_config --version | egrep -o '[0-9]{1,}\.[0-9]{1,}[^-]'`
|
||||||
if verlte 9.5 $PG_VERSION ; then
|
if verlte 9.5 $PG_VERSION ; then
|
||||||
# only do this on postgres 9.5 and above
|
# only do this on postgres 9.5 and above
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ module Admin
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
@user = authorize User.find(params[:id]), :promote?
|
@user = authorize User.find(params[:id]), :promote?
|
||||||
@user.promote_to!(params[:user][:level], params[:user])
|
|
||||||
|
@level = params.dig(:user, :level)
|
||||||
|
@can_upload_free = params.dig(:user, :can_upload_free)
|
||||||
|
@can_approve_posts = params.dig(:user, :can_approve_posts)
|
||||||
|
|
||||||
|
@user.promote_to!(@level, CurrentUser.user, can_upload_free: @can_upload_free, can_approve_posts: @can_approve_posts)
|
||||||
|
|
||||||
redirect_to edit_admin_user_path(@user), :notice => "User updated"
|
redirect_to edit_admin_user_path(@user), :notice => "User updated"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class ApplicationController < ActionController::Base
|
|||||||
before_action :set_variant
|
before_action :set_variant
|
||||||
before_action :add_headers
|
before_action :add_headers
|
||||||
before_action :cause_error
|
before_action :cause_error
|
||||||
|
after_action :skip_session_if_publicly_cached
|
||||||
after_action :reset_current_user
|
after_action :reset_current_user
|
||||||
layout "default"
|
layout "default"
|
||||||
|
|
||||||
@@ -87,6 +88,8 @@ class ApplicationController < ActionController::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def rescue_exception(exception)
|
def rescue_exception(exception)
|
||||||
|
raise exception if Danbooru.config.debug_mode
|
||||||
|
|
||||||
case exception
|
case exception
|
||||||
when ActionView::Template::Error
|
when ActionView::Template::Error
|
||||||
rescue_exception(exception.cause)
|
rescue_exception(exception.cause)
|
||||||
@@ -121,17 +124,17 @@ class ApplicationController < ActionController::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_error_page(status, exception, message: exception.message, template: "static/error", format: request.format.symbol)
|
def render_error_page(status, exception = nil, message: exception.message, template: "static/error", format: request.format.symbol)
|
||||||
@exception = exception
|
@exception = exception
|
||||||
@expected = status < 500
|
@expected = status < 500
|
||||||
@message = message.encode("utf-8", invalid: :replace, undef: :replace)
|
@message = message.encode("utf-8", invalid: :replace, undef: :replace)
|
||||||
@backtrace = Rails.backtrace_cleaner.clean(@exception.backtrace)
|
@backtrace = Rails.backtrace_cleaner.clean(@exception.backtrace) if @exception
|
||||||
format = :html unless format.in?(%i[html json xml js atom])
|
format = :html unless format.in?(%i[html json xml js atom])
|
||||||
|
|
||||||
# if InvalidAuthenticityToken was raised, CurrentUser isn't set so we have to use the blank layout.
|
# if InvalidAuthenticityToken was raised, CurrentUser isn't set so we have to use the blank layout.
|
||||||
layout = CurrentUser.user.present? ? "default" : "blank"
|
layout = CurrentUser.user.present? ? "default" : "blank"
|
||||||
|
|
||||||
DanbooruLogger.log(@exception, expected: @expected)
|
DanbooruLogger.log(@exception, expected: @expected) if @exception
|
||||||
render template, layout: layout, status: status, formats: format
|
render template, layout: layout, status: status, formats: format
|
||||||
rescue ActionView::MissingTemplate
|
rescue ActionView::MissingTemplate
|
||||||
render "static/error", layout: layout, status: status, formats: format
|
render "static/error", layout: layout, status: status, formats: format
|
||||||
@@ -148,6 +151,14 @@ class ApplicationController < ActionController::Base
|
|||||||
CurrentUser.root_url = root_url.chomp("/")
|
CurrentUser.root_url = root_url.chomp("/")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Skip setting the session cookie if the response is being publicly cached to
|
||||||
|
# prevent the user's session cookie from being leaked to other users.
|
||||||
|
def skip_session_if_publicly_cached
|
||||||
|
if response.cache_control[:public] == true
|
||||||
|
request.session_options[:skip] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def set_variant
|
def set_variant
|
||||||
request.variant = params[:variant].try(:to_sym)
|
request.variant = params[:variant].try(:to_sym)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ class AutocompleteController < ApplicationController
|
|||||||
respond_to :xml, :json
|
respond_to :xml, :json
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@tags = Tag.names_matches_with_aliases(params[:query], params.fetch(:limit, 10).to_i)
|
@query = params.dig(:search, :query)
|
||||||
|
@type = params.dig(:search, :type)
|
||||||
|
@limit = params.fetch(:limit, 10).to_i
|
||||||
|
@autocomplete = AutocompleteService.new(@query, @type, current_user: CurrentUser.user, limit: @limit)
|
||||||
|
|
||||||
if request.variant.opensearch?
|
@results = @autocomplete.autocomplete_results
|
||||||
expires_in 1.hour
|
@expires_in = @autocomplete.cache_duration
|
||||||
results = [params[:query], @tags.map(&:pretty_name)]
|
@public = @autocomplete.cache_publicly?
|
||||||
respond_with(results)
|
|
||||||
else
|
expires_in @expires_in, public: @public unless response.cache_control.present?
|
||||||
# XXX
|
respond_with(@results)
|
||||||
respond_with(@tags.map(&:attributes))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
class EmailsController < ApplicationController
|
class EmailsController < ApplicationController
|
||||||
respond_to :html, :xml, :json
|
respond_to :html, :xml, :json
|
||||||
|
|
||||||
|
def index
|
||||||
|
@email_addresses = authorize EmailAddress.visible(CurrentUser.user).paginated_search(params, count_pages: true)
|
||||||
|
@email_addresses = @email_addresses.includes(:user)
|
||||||
|
respond_with(@email_addresses)
|
||||||
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@email_address = authorize EmailAddress.find_by_user_id!(params[:user_id])
|
if params[:user_id]
|
||||||
|
@email_address = authorize EmailAddress.find_by_user_id!(params[:user_id])
|
||||||
|
else
|
||||||
|
@email_address = authorize EmailAddress.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
respond_with(@email_address)
|
respond_with(@email_address)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -17,7 +28,7 @@ class EmailsController < ApplicationController
|
|||||||
if @user.authenticate_password(params[:user][:password])
|
if @user.authenticate_password(params[:user][:password])
|
||||||
@user.update(email_address_attributes: { address: params[:user][:email] })
|
@user.update(email_address_attributes: { address: params[:user][:email] })
|
||||||
else
|
else
|
||||||
@user.errors[:base] << "Password was incorrect"
|
@user.errors.add(:base, "Password was incorrect")
|
||||||
end
|
end
|
||||||
|
|
||||||
if @user.errors.none?
|
if @user.errors.none?
|
||||||
|
|||||||
@@ -17,18 +17,10 @@ class LegacyController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def users
|
|
||||||
@users = User.limit(100).search(params).paginate(params[:page])
|
|
||||||
end
|
|
||||||
|
|
||||||
def tags
|
def tags
|
||||||
@tags = Tag.limit(100).search(params).paginate(params[:page], :limit => params[:limit])
|
@tags = Tag.limit(100).search(params).paginate(params[:page], :limit => params[:limit])
|
||||||
end
|
end
|
||||||
|
|
||||||
def artists
|
|
||||||
@artists = Artist.limit(100).search(search_params).paginate(params[:page])
|
|
||||||
end
|
|
||||||
|
|
||||||
def unavailable
|
def unavailable
|
||||||
render :plain => "this resource is no longer available", :status => 410
|
render :plain => "this resource is no longer available", :status => 410
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
class NotesController < ApplicationController
|
class NotesController < ApplicationController
|
||||||
respond_to :html, :xml, :json, :js
|
respond_to :html, :xml, :json, :js
|
||||||
|
|
||||||
def search
|
|
||||||
end
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@notes = authorize Note.paginated_search(params)
|
@notes = authorize Note.paginated_search(params)
|
||||||
@notes = @notes.includes(:post) if request.format.html?
|
@notes = @notes.includes(:post) if request.format.html?
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ class PasswordsController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
@user = authorize User.find(params[:user_id]), policy_class: PasswordPolicy
|
@user = authorize User.find(params[:user_id]), policy_class: PasswordPolicy
|
||||||
|
|
||||||
if @user.authenticate_password(params[:user][:old_password]) || @user.authenticate_login_key(params[:user][:signed_user_id])
|
if @user.authenticate_password(params[:user][:old_password]) || @user.authenticate_login_key(params[:user][:signed_user_id]) || CurrentUser.user.is_owner?
|
||||||
@user.update(password: params[:user][:password], password_confirmation: params[:user][:password_confirmation])
|
@user.update(password: params[:user][:password], password_confirmation: params[:user][:password_confirmation])
|
||||||
else
|
else
|
||||||
@user.errors[:base] << "Incorrect password"
|
@user.errors.add(:base, "Incorrect password")
|
||||||
end
|
end
|
||||||
|
|
||||||
flash[:notice] = @user.errors.none? ? "Password updated" : @user.errors.full_messages.join("; ")
|
flash[:notice] = @user.errors.none? ? "Password updated" : @user.errors.full_messages.join("; ")
|
||||||
|
|||||||
@@ -6,12 +6,6 @@ class SavedSearchesController < ApplicationController
|
|||||||
respond_with(@saved_searches)
|
respond_with(@saved_searches)
|
||||||
end
|
end
|
||||||
|
|
||||||
def labels
|
|
||||||
authorize SavedSearch
|
|
||||||
@labels = SavedSearch.search_labels(CurrentUser.id, params[:search]).take(params[:limit].to_i || 10)
|
|
||||||
respond_with(@labels)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@saved_search = authorize SavedSearch.new(user: CurrentUser.user, **permitted_attributes(SavedSearch))
|
@saved_search = authorize SavedSearch.new(user: CurrentUser.user, **permitted_attributes(SavedSearch))
|
||||||
@saved_search.save
|
@saved_search.save
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
class StaticController < ApplicationController
|
class StaticController < ApplicationController
|
||||||
|
respond_to :html, :json, :xml
|
||||||
|
|
||||||
def privacy_policy
|
def privacy_policy
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -6,7 +8,11 @@ class StaticController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def not_found
|
def not_found
|
||||||
render plain: "not found", status: :not_found
|
@pool = Pool.find(Danbooru.config.page_not_found_pool_id) if Danbooru.config.page_not_found_pool_id.present?
|
||||||
|
@post = @pool.posts.sample if @pool.present?
|
||||||
|
@artist = @post.tags.select(&:artist?).first if @post.present?
|
||||||
|
|
||||||
|
render_error_page(404, nil, template: "static/not_found", message: "Page not found")
|
||||||
end
|
end
|
||||||
|
|
||||||
def error
|
def error
|
||||||
@@ -38,7 +44,7 @@ class StaticController < ApplicationController
|
|||||||
@search = { is_deleted: "false" }
|
@search = { is_deleted: "false" }
|
||||||
when "posts"
|
when "posts"
|
||||||
@relation = Post.order(id: :asc)
|
@relation = Post.order(id: :asc)
|
||||||
@serach = {}
|
@search = {}
|
||||||
when "tags"
|
when "tags"
|
||||||
@relation = Tag.nonempty
|
@relation = Tag.nonempty
|
||||||
@search = {}
|
@search = {}
|
||||||
|
|||||||
8
app/controllers/status_controller.rb
Normal file
8
app/controllers/status_controller.rb
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
class StatusController < ApplicationController
|
||||||
|
respond_to :html, :json, :xml
|
||||||
|
|
||||||
|
def show
|
||||||
|
@status = ServerStatus.new
|
||||||
|
respond_with(@status)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -12,18 +12,6 @@ class TagsController < ApplicationController
|
|||||||
respond_with(@tags)
|
respond_with(@tags)
|
||||||
end
|
end
|
||||||
|
|
||||||
def autocomplete
|
|
||||||
if CurrentUser.is_builder?
|
|
||||||
# limit rollout
|
|
||||||
@tags = TagAutocomplete.search(params[:search][:name_matches])
|
|
||||||
else
|
|
||||||
@tags = Tag.names_matches_with_aliases(params[:search][:name_matches], params.fetch(:limit, 10).to_i)
|
|
||||||
end
|
|
||||||
|
|
||||||
# XXX
|
|
||||||
respond_with(@tags.map(&:attributes))
|
|
||||||
end
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@tag = authorize Tag.find(params[:id])
|
@tag = authorize Tag.find(params[:id])
|
||||||
respond_with(@tag)
|
respond_with(@tag)
|
||||||
|
|||||||
@@ -1,64 +1,59 @@
|
|||||||
class UserUpgradesController < ApplicationController
|
class UserUpgradesController < ApplicationController
|
||||||
helper_method :user
|
respond_to :js, :html, :json, :xml
|
||||||
skip_before_action :verify_authenticity_token, only: [:create]
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if params[:stripeToken]
|
@user_upgrade = authorize UserUpgrade.create(recipient: recipient, purchaser: CurrentUser.user, status: "pending", upgrade_type: params[:upgrade_type])
|
||||||
create_stripe
|
@country = params[:country] || CurrentUser.country || "US"
|
||||||
end
|
@allow_promotion_codes = params[:promo].to_s.truthy?
|
||||||
|
@checkout = @user_upgrade.create_checkout!(country: @country, allow_promotion_codes: @allow_promotion_codes)
|
||||||
|
|
||||||
|
respond_with(@user_upgrade)
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
|
@user_upgrade = authorize UserUpgrade.new(recipient: recipient, purchaser: CurrentUser.user)
|
||||||
|
@recipient = @user_upgrade.recipient
|
||||||
|
|
||||||
|
respond_with(@user_upgrade)
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
@user_upgrades = authorize UserUpgrade.visible(CurrentUser.user).paginated_search(params, count_pages: true)
|
||||||
|
@user_upgrades = @user_upgrades.includes(:recipient, :purchaser) if request.format.html?
|
||||||
|
|
||||||
|
respond_with(@user_upgrades)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
authorize User, :upgrade?
|
@user_upgrade = authorize UserUpgrade.find(params[:id])
|
||||||
|
respond_with(@user_upgrade)
|
||||||
end
|
end
|
||||||
|
|
||||||
def user
|
def refund
|
||||||
|
@user_upgrade = authorize UserUpgrade.find(params[:id])
|
||||||
|
@user_upgrade.refund!
|
||||||
|
flash[:notice] = "Upgrade refunded"
|
||||||
|
|
||||||
|
respond_with(@user_upgrade)
|
||||||
|
end
|
||||||
|
|
||||||
|
def receipt
|
||||||
|
@user_upgrade = authorize UserUpgrade.find(params[:id])
|
||||||
|
redirect_to @user_upgrade.receipt_url
|
||||||
|
end
|
||||||
|
|
||||||
|
def payment
|
||||||
|
@user_upgrade = authorize UserUpgrade.find(params[:id])
|
||||||
|
redirect_to @user_upgrade.payment_url
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def recipient
|
||||||
if params[:user_id]
|
if params[:user_id]
|
||||||
User.find(params[:user_id])
|
User.find(params[:user_id])
|
||||||
else
|
else
|
||||||
CurrentUser.user
|
CurrentUser.user
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def create_stripe
|
|
||||||
@user = user
|
|
||||||
|
|
||||||
if params[:desc] == "Upgrade to Gold"
|
|
||||||
level = User::Levels::GOLD
|
|
||||||
cost = UserUpgrade.gold_price
|
|
||||||
elsif params[:desc] == "Upgrade to Platinum"
|
|
||||||
level = User::Levels::PLATINUM
|
|
||||||
cost = UserUpgrade.platinum_price
|
|
||||||
elsif params[:desc] == "Upgrade Gold to Platinum" && @user.level == User::Levels::GOLD
|
|
||||||
level = User::Levels::PLATINUM
|
|
||||||
cost = UserUpgrade.upgrade_price
|
|
||||||
else
|
|
||||||
raise "Invalid desc"
|
|
||||||
end
|
|
||||||
|
|
||||||
begin
|
|
||||||
charge = Stripe::Charge.create(
|
|
||||||
:amount => cost,
|
|
||||||
:currency => "usd",
|
|
||||||
:card => params[:stripeToken],
|
|
||||||
:description => params[:desc]
|
|
||||||
)
|
|
||||||
@user.promote_to!(level, is_upgrade: true)
|
|
||||||
flash[:success] = true
|
|
||||||
rescue Stripe::CardError => e
|
|
||||||
DanbooruLogger.log(e)
|
|
||||||
flash[:error] = e.message
|
|
||||||
end
|
|
||||||
|
|
||||||
if @user == CurrentUser.user
|
|
||||||
redirect_to user_upgrade_path
|
|
||||||
else
|
|
||||||
redirect_to user_upgrade_path(user_id: params[:user_id])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -38,9 +38,6 @@ class UsersController < ApplicationController
|
|||||||
respond_with(@users)
|
respond_with(@users)
|
||||||
end
|
end
|
||||||
|
|
||||||
def search
|
|
||||||
end
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@user = authorize User.find(params[:id])
|
@user = authorize User.find(params[:id])
|
||||||
respond_with(@user, methods: @user.full_attributes) do |format|
|
respond_with(@user, methods: @user.full_attributes) do |format|
|
||||||
@@ -62,7 +59,7 @@ class UsersController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
requires_verification = IpLookup.new(CurrentUser.ip_addr).is_proxy? || IpBan.hit!(:partial, CurrentUser.ip_addr)
|
requires_verification = UserVerifier.new(CurrentUser.user, request).requires_verification?
|
||||||
|
|
||||||
@user = authorize User.new(
|
@user = authorize User.new(
|
||||||
last_ip_addr: CurrentUser.ip_addr,
|
last_ip_addr: CurrentUser.ip_addr,
|
||||||
@@ -80,7 +77,7 @@ class UsersController < ApplicationController
|
|||||||
flash[:notice] = "Sign up failed"
|
flash[:notice] = "Sign up failed"
|
||||||
elsif @user.email_address&.invalid?(:deliverable)
|
elsif @user.email_address&.invalid?(:deliverable)
|
||||||
flash[:notice] = "Sign up failed: email address is invalid or doesn't exist"
|
flash[:notice] = "Sign up failed: email address is invalid or doesn't exist"
|
||||||
@user.errors[:base] << @user.email_address.errors.full_messages.join("; ")
|
@user.errors.add(:base, @user.email_address.errors.full_messages.join("; "))
|
||||||
elsif !@user.save
|
elsif !@user.save
|
||||||
flash[:notice] = "Sign up failed: #{@user.errors.full_messages.join("; ")}"
|
flash[:notice] = "Sign up failed: #{@user.errors.full_messages.join("; ")}"
|
||||||
else
|
else
|
||||||
|
|||||||
13
app/controllers/webhooks_controller.rb
Normal file
13
app/controllers/webhooks_controller.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
class WebhooksController < ApplicationController
|
||||||
|
skip_forgery_protection only: :receive
|
||||||
|
rescue_with Stripe::SignatureVerificationError, status: 400
|
||||||
|
|
||||||
|
def receive
|
||||||
|
if params[:source] == "stripe"
|
||||||
|
UserUpgrade.receive_webhook(request)
|
||||||
|
head 200
|
||||||
|
else
|
||||||
|
head 400
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -189,7 +189,7 @@ module ApplicationHelper
|
|||||||
to_sentence(links, **options)
|
to_sentence(links, **options)
|
||||||
end
|
end
|
||||||
|
|
||||||
def link_to_user(user)
|
def link_to_user(user, text = nil)
|
||||||
return "anonymous" if user.blank?
|
return "anonymous" if user.blank?
|
||||||
|
|
||||||
user_class = "user user-#{user.level_string.downcase}"
|
user_class = "user user-#{user.level_string.downcase}"
|
||||||
@@ -197,8 +197,9 @@ module ApplicationHelper
|
|||||||
user_class += " user-post-uploader" if user.can_upload_free?
|
user_class += " user-post-uploader" if user.can_upload_free?
|
||||||
user_class += " user-banned" if user.is_banned?
|
user_class += " user-banned" if user.is_banned?
|
||||||
|
|
||||||
|
text = user.pretty_name if text.blank?
|
||||||
data = { "user-id": user.id, "user-name": user.name, "user-level": user.level }
|
data = { "user-id": user.id, "user-name": user.name, "user-level": user.level }
|
||||||
link_to(user.pretty_name, user_path(user), class: user_class, data: data)
|
link_to(text, user, class: user_class, data: data)
|
||||||
end
|
end
|
||||||
|
|
||||||
def mod_link_to_user(user, positive_or_negative)
|
def mod_link_to_user(user, positive_or_negative)
|
||||||
@@ -272,6 +273,7 @@ module ApplicationHelper
|
|||||||
{
|
{
|
||||||
lang: "en",
|
lang: "en",
|
||||||
class: "c-#{controller_param} a-#{action_param}",
|
class: "c-#{controller_param} a-#{action_param}",
|
||||||
|
spellcheck: "false",
|
||||||
data: {
|
data: {
|
||||||
controller: controller_param,
|
controller: controller_param,
|
||||||
action: action_param,
|
action: action_param,
|
||||||
|
|||||||
@@ -48,21 +48,10 @@ module PostsHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def post_favlist(post)
|
|
||||||
post.favorited_users.reverse_each.map {|user| link_to_user(user)}.join(", ").html_safe
|
|
||||||
end
|
|
||||||
|
|
||||||
def is_pool_selected?(pool)
|
def is_pool_selected?(pool)
|
||||||
return false if params.key?(:q)
|
return false if params.key?(:q)
|
||||||
return false if params.key?(:favgroup_id)
|
return false if params.key?(:favgroup_id)
|
||||||
return false if !params.key?(:pool_id)
|
return false if !params.key?(:pool_id)
|
||||||
return params[:pool_id].to_i == pool.id
|
return params[:pool_id].to_i == pool.id
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def nav_params_for(page)
|
|
||||||
query_params = params.except(:controller, :action, :id).merge(page: page).permit!
|
|
||||||
{ params: query_params }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,12 +3,4 @@ module TagsHelper
|
|||||||
return nil if tag.blank?
|
return nil if tag.blank?
|
||||||
"tag-type-#{tag.category}"
|
"tag-type-#{tag.category}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def tag_alias_for_pattern(tag, pattern)
|
|
||||||
return nil if pattern.blank?
|
|
||||||
|
|
||||||
tag.consequent_aliases.find do |tag_alias|
|
|
||||||
!tag.name.ilike?(pattern) && tag_alias.antecedent_name.ilike?(pattern)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,24 +1,4 @@
|
|||||||
module UserUpgradesHelper
|
module UserUpgradesHelper
|
||||||
def stripe_button(desc, cost, user)
|
|
||||||
html = %{
|
|
||||||
<form action="#{user_upgrade_path}" method="POST" class="stripe">
|
|
||||||
<input type="hidden" name="authenticity_token" value="#{form_authenticity_token}">
|
|
||||||
#{hidden_field_tag(:desc, desc)}
|
|
||||||
#{hidden_field_tag(:user_id, user.id)}
|
|
||||||
<script
|
|
||||||
src="https://checkout.stripe.com/checkout.js" class="stripe-button"
|
|
||||||
data-key="#{Danbooru.config.stripe_publishable_key}"
|
|
||||||
data-name="#{Danbooru.config.canonical_app_name}"
|
|
||||||
data-description="#{desc}"
|
|
||||||
data-label="#{desc}"
|
|
||||||
data-amount="#{cost}">
|
|
||||||
</script>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
|
|
||||||
raw(html)
|
|
||||||
end
|
|
||||||
|
|
||||||
def cents_to_usd(cents)
|
def cents_to_usd(cents)
|
||||||
number_to_currency(cents / 100, precision: 0)
|
number_to_currency(cents / 100, precision: 0)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,16 +3,10 @@ import CurrentUser from './current_user'
|
|||||||
let Autocomplete = {};
|
let Autocomplete = {};
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
Autocomplete.METATAGS = <%= PostQueryBuilder::METATAGS.to_json.html_safe %>;
|
|
||||||
Autocomplete.TAG_CATEGORIES = <%= TagCategory.mapping.to_json.html_safe %>;
|
Autocomplete.TAG_CATEGORIES = <%= TagCategory.mapping.to_json.html_safe %>;
|
||||||
Autocomplete.ORDER_METATAGS = <%= PostQueryBuilder::ORDER_METATAGS.to_json.html_safe %>;
|
|
||||||
Autocomplete.DISAPPROVAL_REASONS = <%= PostDisapproval::REASONS.to_json.html_safe %>;
|
|
||||||
/* eslint-enable */
|
/* eslint-enable */
|
||||||
|
|
||||||
Autocomplete.MISC_STATUSES = ["deleted", "active", "pending", "flagged", "banned", "modqueue", "unmoderated", "appealed"];
|
|
||||||
Autocomplete.TAG_PREFIXES = "-|~|" + Object.keys(Autocomplete.TAG_CATEGORIES).map(category => category + ":").join("|");
|
Autocomplete.TAG_PREFIXES = "-|~|" + Object.keys(Autocomplete.TAG_CATEGORIES).map(category => category + ":").join("|");
|
||||||
Autocomplete.METATAGS_REGEX = Autocomplete.METATAGS.concat(Object.keys(Autocomplete.TAG_CATEGORIES)).join("|");
|
|
||||||
Autocomplete.TERM_REGEX = new RegExp(`([-~]*)(?:(${Autocomplete.METATAGS_REGEX}):)?(\\S*)$`, "i");
|
|
||||||
Autocomplete.MAX_RESULTS = 10;
|
Autocomplete.MAX_RESULTS = 10;
|
||||||
|
|
||||||
Autocomplete.initialize_all = function() {
|
Autocomplete.initialize_all = function() {
|
||||||
@@ -39,20 +33,20 @@ Autocomplete.initialize_all = function() {
|
|||||||
|
|
||||||
this.initialize_tag_autocomplete();
|
this.initialize_tag_autocomplete();
|
||||||
this.initialize_mention_autocomplete($("form div.input.dtext textarea"));
|
this.initialize_mention_autocomplete($("form div.input.dtext textarea"));
|
||||||
this.initialize_fields($('[data-autocomplete="tag"]'), Autocomplete.tag_source);
|
this.initialize_fields($('[data-autocomplete="tag"]'), "tag");
|
||||||
this.initialize_fields($('[data-autocomplete="artist"]'), Autocomplete.artist_source);
|
this.initialize_fields($('[data-autocomplete="artist"]'), "artist");
|
||||||
this.initialize_fields($('[data-autocomplete="pool"]'), Autocomplete.pool_source);
|
this.initialize_fields($('[data-autocomplete="pool"]'), "pool");
|
||||||
this.initialize_fields($('[data-autocomplete="user"]'), Autocomplete.user_source);
|
this.initialize_fields($('[data-autocomplete="user"]'), "user");
|
||||||
this.initialize_fields($('[data-autocomplete="wiki-page"]'), Autocomplete.wiki_source);
|
this.initialize_fields($('[data-autocomplete="wiki-page"]'), "wiki_page");
|
||||||
this.initialize_fields($('[data-autocomplete="favorite-group"]'), Autocomplete.favorite_group_source);
|
this.initialize_fields($('[data-autocomplete="favorite-group"]'), "favorite_group");
|
||||||
this.initialize_fields($('[data-autocomplete="saved-search-label"]'), Autocomplete.saved_search_source);
|
this.initialize_fields($('[data-autocomplete="saved-search-label"]'), "saved_search_label");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Autocomplete.initialize_fields = function($fields, autocomplete) {
|
Autocomplete.initialize_fields = function($fields, type) {
|
||||||
$fields.autocomplete({
|
$fields.autocomplete({
|
||||||
source: async function(request, respond) {
|
source: async function(request, respond) {
|
||||||
let results = await autocomplete(request.term);
|
let results = await Autocomplete.autocomplete_source(request.term, type);
|
||||||
respond(results);
|
respond(results);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -84,7 +78,7 @@ Autocomplete.initialize_mention_autocomplete = function($fields) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
let results = await Autocomplete.user_source(name, "@");
|
let results = await Autocomplete.autocomplete_source(name, "mention");
|
||||||
resp(results);
|
resp(results);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,76 +100,18 @@ Autocomplete.initialize_tag_autocomplete = function() {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
source: async function(req, resp) {
|
source: async function(req, resp) {
|
||||||
var query = Autocomplete.parse_query(req.term, this.element.get(0).selectionStart);
|
let term = Autocomplete.current_term(this.element);
|
||||||
var metatag = query.metatag;
|
let results = await Autocomplete.autocomplete_source(term, "tag_query");
|
||||||
var term = query.term;
|
|
||||||
var results = [];
|
|
||||||
|
|
||||||
switch (metatag) {
|
|
||||||
case "order":
|
|
||||||
case "status":
|
|
||||||
case "rating":
|
|
||||||
case "locked":
|
|
||||||
case "child":
|
|
||||||
case "parent":
|
|
||||||
case "filetype":
|
|
||||||
case "disapproved":
|
|
||||||
case "embedded":
|
|
||||||
results = Autocomplete.static_metatag_source(term, metatag);
|
|
||||||
break;
|
|
||||||
case "user":
|
|
||||||
case "approver":
|
|
||||||
case "commenter":
|
|
||||||
case "comm":
|
|
||||||
case "noter":
|
|
||||||
case "noteupdater":
|
|
||||||
case "commentaryupdater":
|
|
||||||
case "artcomm":
|
|
||||||
case "fav":
|
|
||||||
case "ordfav":
|
|
||||||
case "appealer":
|
|
||||||
case "flagger":
|
|
||||||
case "upvote":
|
|
||||||
case "downvote":
|
|
||||||
results = await Autocomplete.user_source(term, metatag + ":");
|
|
||||||
break;
|
|
||||||
case "pool":
|
|
||||||
case "ordpool":
|
|
||||||
results = await Autocomplete.pool_source(term, metatag + ":");
|
|
||||||
break;
|
|
||||||
case "favgroup":
|
|
||||||
case "ordfavgroup":
|
|
||||||
results = await Autocomplete.favorite_group_source(term, metatag + ":", CurrentUser.data("id"));
|
|
||||||
break;
|
|
||||||
case "search":
|
|
||||||
results = await Autocomplete.saved_search_source(term, metatag + ":");
|
|
||||||
break;
|
|
||||||
case "tag":
|
|
||||||
results = await Autocomplete.tag_source(term);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
results = [];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
resp(results);
|
resp(results);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Autocomplete.parse_query = function(text, caret) {
|
Autocomplete.current_term = function($input) {
|
||||||
let before_caret_text = text.substring(0, caret);
|
let query = $input.get(0).value;
|
||||||
let match = before_caret_text.match(Autocomplete.TERM_REGEX);
|
let caret = $input.get(0).selectionStart;
|
||||||
|
let match = query.substring(0, caret).match(/\S*$/);
|
||||||
let operator = match[1];
|
return match[0];
|
||||||
let metatag = match[2] ? match[2].toLowerCase() : "tag";
|
|
||||||
let term = match[3];
|
|
||||||
|
|
||||||
if (metatag in Autocomplete.TAG_CATEGORIES) {
|
|
||||||
metatag = "tag";
|
|
||||||
}
|
|
||||||
|
|
||||||
return { operator: operator, metatag: metatag, term: term };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the input field with the item currently focused in the
|
// Update the input field with the item currently focused in the
|
||||||
@@ -244,7 +180,7 @@ Autocomplete.render_item = function(list, item) {
|
|||||||
$link.append($post_count);
|
$link.append($post_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.type === "tag") {
|
if (/^tag/.test(item.type)) {
|
||||||
$link.addClass("tag-type-" + item.category);
|
$link.addClass("tag-type-" + item.category);
|
||||||
} else if (item.type === "user") {
|
} else if (item.type === "user") {
|
||||||
var level_class = "user-" + item.level.toLowerCase();
|
var level_class = "user-" + item.level.toLowerCase();
|
||||||
@@ -256,7 +192,7 @@ Autocomplete.render_item = function(list, item) {
|
|||||||
var $menu_item = $("<div/>").append($link);
|
var $menu_item = $("<div/>").append($link);
|
||||||
var $list_item = $("<li/>").data("item.autocomplete", item).append($menu_item);
|
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 = ["type", "antecedent", "value", "category", "post_count"];
|
||||||
data_attributes.forEach(attr => {
|
data_attributes.forEach(attr => {
|
||||||
$list_item.attr(`data-autocomplete-${attr.replace(/_/g, "-")}`, item[attr]);
|
$list_item.attr(`data-autocomplete-${attr.replace(/_/g, "-")}`, item[attr]);
|
||||||
});
|
});
|
||||||
@@ -264,166 +200,12 @@ Autocomplete.render_item = function(list, item) {
|
|||||||
return $list_item.appendTo(list);
|
return $list_item.appendTo(list);
|
||||||
};
|
};
|
||||||
|
|
||||||
Autocomplete.static_metatags = {
|
Autocomplete.autocomplete_source = function(query, type) {
|
||||||
order: Autocomplete.ORDER_METATAGS,
|
return $.getJSON("/autocomplete.json", {
|
||||||
status: ["any"].concat(Autocomplete.MISC_STATUSES),
|
"search[query]": query,
|
||||||
rating: [
|
"search[type]": type,
|
||||||
"safe", "questionable", "explicit"
|
|
||||||
],
|
|
||||||
locked: [
|
|
||||||
"rating", "note", "status"
|
|
||||||
],
|
|
||||||
embedded: [
|
|
||||||
"true", "false"
|
|
||||||
],
|
|
||||||
child: ["any", "none"].concat(Autocomplete.MISC_STATUSES),
|
|
||||||
parent: ["any", "none"].concat(Autocomplete.MISC_STATUSES),
|
|
||||||
filetype: [
|
|
||||||
"jpg", "png", "gif", "swf", "zip", "webm", "mp4"
|
|
||||||
],
|
|
||||||
commentary: [
|
|
||||||
"true", "false", "translated", "untranslated"
|
|
||||||
],
|
|
||||||
disapproved: Autocomplete.DISAPPROVAL_REASONS
|
|
||||||
}
|
|
||||||
|
|
||||||
Autocomplete.static_metatag_source = function(term, metatag) {
|
|
||||||
var sub_metatags = this.static_metatags[metatag];
|
|
||||||
|
|
||||||
var matches = sub_metatags.filter(sub_metatag => sub_metatag.startsWith(term.toLowerCase()));
|
|
||||||
matches = matches.map(sub_metatag => `${metatag}:${sub_metatag}`).sort().slice(0, Autocomplete.MAX_RESULTS);
|
|
||||||
|
|
||||||
return matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
Autocomplete.tag_source = async function(term) {
|
|
||||||
if (term === "") {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
let tags = await $.getJSON("/tags/autocomplete", {
|
|
||||||
"search[name_matches]": term,
|
|
||||||
"limit": Autocomplete.MAX_RESULTS,
|
|
||||||
"expiry": 7
|
|
||||||
});
|
|
||||||
|
|
||||||
return tags.map(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
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Autocomplete.artist_source = async function(term) {
|
|
||||||
let artists = await $.getJSON("/artists", {
|
|
||||||
"search[name_like]": term.trim().replace(/\s+/g, "_") + "*",
|
|
||||||
"search[is_deleted]": false,
|
|
||||||
"search[order]": "post_count",
|
|
||||||
"limit": Autocomplete.MAX_RESULTS,
|
|
||||||
"expiry": 7
|
|
||||||
});
|
|
||||||
|
|
||||||
return artists.map(function(artist) {
|
|
||||||
return {
|
|
||||||
type: "tag",
|
|
||||||
label: artist.name.replace(/_/g, " "),
|
|
||||||
value: artist.name,
|
|
||||||
category: Autocomplete.TAG_CATEGORIES.artist,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Autocomplete.wiki_source = async function(term) {
|
|
||||||
let wiki_pages = await $.getJSON("/wiki_pages", {
|
|
||||||
"search[title_normalize]": term + "*",
|
|
||||||
"search[hide_deleted]": "Yes",
|
|
||||||
"search[order]": "post_count",
|
|
||||||
"limit": Autocomplete.MAX_RESULTS,
|
|
||||||
"expiry": 7
|
|
||||||
});
|
|
||||||
|
|
||||||
return wiki_pages.map(function(wiki_page) {
|
|
||||||
return {
|
|
||||||
type: "tag",
|
|
||||||
label: wiki_page.title.replace(/_/g, " "),
|
|
||||||
value: wiki_page.title,
|
|
||||||
category: wiki_page.category_name
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Autocomplete.user_source = async function(term, prefix = "") {
|
|
||||||
let users = await $.getJSON("/users", {
|
|
||||||
"search[order]": "post_upload_count",
|
|
||||||
"search[current_user_first]": "true",
|
|
||||||
"search[name_matches]": term + "*",
|
|
||||||
"limit": Autocomplete.MAX_RESULTS
|
"limit": Autocomplete.MAX_RESULTS
|
||||||
});
|
});
|
||||||
|
|
||||||
return users.map(function(user) {
|
|
||||||
return {
|
|
||||||
type: "user",
|
|
||||||
label: user.name.replace(/_/g, " "),
|
|
||||||
value: prefix + user.name,
|
|
||||||
level: user.level_string
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Autocomplete.pool_source = async function(term, prefix = "") {
|
|
||||||
let pools = await $.getJSON("/pools", {
|
|
||||||
"search[name_matches]": term,
|
|
||||||
"search[is_deleted]": false,
|
|
||||||
"search[order]": "post_count",
|
|
||||||
"limit": Autocomplete.MAX_RESULTS
|
|
||||||
});
|
|
||||||
|
|
||||||
return pools.map(function(pool) {
|
|
||||||
return {
|
|
||||||
type: "pool",
|
|
||||||
label: pool.name.replace(/_/g, " "),
|
|
||||||
value: prefix + pool.name,
|
|
||||||
post_count: pool.post_count,
|
|
||||||
category: pool.category
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Autocomplete.favorite_group_source = async function(term, prefix = "", creator_id = null) {
|
|
||||||
let favgroups = await $.getJSON("/favorite_groups", {
|
|
||||||
"search[creator_id]": creator_id,
|
|
||||||
"search[name_matches]": term,
|
|
||||||
"limit": Autocomplete.MAX_RESULTS
|
|
||||||
});
|
|
||||||
|
|
||||||
return favgroups.map(function(favgroup) {
|
|
||||||
return {
|
|
||||||
label: favgroup.name.replace(/_/g, " "),
|
|
||||||
value: prefix + favgroup.name,
|
|
||||||
post_count: favgroup.post_count
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Autocomplete.saved_search_source = async function(term, prefix = "") {
|
|
||||||
let labels = await $.getJSON("/saved_searches/labels", {
|
|
||||||
"search[label]": term + "*",
|
|
||||||
"limit": Autocomplete.MAX_RESULTS
|
|
||||||
});
|
|
||||||
|
|
||||||
return labels.map(function(label) {
|
|
||||||
return {
|
|
||||||
label: label.replace(/_/g, " "),
|
|
||||||
value: prefix + label,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Cookie from './cookie'
|
|||||||
$(function() {
|
$(function() {
|
||||||
$("#hide-upgrade-account-notice").on("click.danbooru", function(e) {
|
$("#hide-upgrade-account-notice").on("click.danbooru", function(e) {
|
||||||
$("#upgrade-account-notice").hide();
|
$("#upgrade-account-notice").hide();
|
||||||
Cookie.put('hide_upgrade_account_notice', '1', 7);
|
Cookie.put('hide_upgrade_account_notice', '1', 7 * 24 * 60 * 60);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ $(function() {
|
|||||||
|
|
||||||
$("#hide-verify-account-notice").on("click.danbooru", function(e) {
|
$("#hide-verify-account-notice").on("click.danbooru", function(e) {
|
||||||
$("#verify-account-notice").hide();
|
$("#verify-account-notice").hide();
|
||||||
Cookie.put('hide_verify_account_notice', '1', 3);
|
Cookie.put('hide_verify_account_notice', '1', 3 * 24 * 60 * 60);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,17 @@
|
|||||||
import Utility from "./utility";
|
|
||||||
|
|
||||||
let Cookie = {};
|
let Cookie = {};
|
||||||
|
|
||||||
Cookie.put = function(name, value, days) {
|
Cookie.put = function(name, value, max_age_in_seconds = 60 * 60 * 24 * 365 * 20) {
|
||||||
var expires = "";
|
let cookie = `${name}=${encodeURIComponent(value)}; Path=/; SameSite=Lax;`;
|
||||||
if (days !== "session") {
|
|
||||||
if (!days) {
|
|
||||||
days = 365;
|
|
||||||
}
|
|
||||||
|
|
||||||
var date = new Date();
|
if (max_age_in_seconds) {
|
||||||
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
cookie += ` Max-Age=${max_age_in_seconds};`
|
||||||
expires = "expires=" + date.toGMTString() + "; ";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var new_val = name + "=" + encodeURIComponent(value) + "; " + expires + "path=/";
|
if (location.protocol === "https:") {
|
||||||
if (document.cookie.length < (4090 - new_val.length)) {
|
cookie += " Secure;";
|
||||||
document.cookie = new_val;
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
Utility.error("You have too many cookies on this site. Consider deleting them all.")
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.cookie = cookie;
|
||||||
}
|
}
|
||||||
|
|
||||||
Cookie.raw_get = function(name) {
|
Cookie.raw_get = function(name) {
|
||||||
|
|||||||
@@ -118,6 +118,17 @@ table tfoot {
|
|||||||
margin-top: 2em;
|
margin-top: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
details {
|
||||||
|
border-bottom: 1px solid var(--details-border);
|
||||||
|
|
||||||
|
summary {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
outline: none;
|
||||||
|
line-height: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.fineprint {
|
.fineprint {
|
||||||
color: var(--muted-text-color);
|
color: var(--muted-text-color);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
|||||||
@@ -35,9 +35,12 @@
|
|||||||
|
|
||||||
--quick-search-form-background: var(--body-background-color);
|
--quick-search-form-background: var(--body-background-color);
|
||||||
|
|
||||||
|
--user-upgrade-basic-background-color: #F5F5FF;
|
||||||
--user-upgrade-gold-background-color: #FFF380;
|
--user-upgrade-gold-background-color: #FFF380;
|
||||||
--user-upgrade-platinum-background-color: #EEE;
|
--user-upgrade-platinum-background-color: #EEE;
|
||||||
--user-upgrade-table-row-hover-background-color: #FEF;
|
--user-upgrade-button-text-color: white;
|
||||||
|
--user-upgrade-button-background-color: var(--link-color);
|
||||||
|
--user-upgrade-button-hover-background-color: hsl(213, 100%, 40%);
|
||||||
|
|
||||||
--table-header-border: 2px solid #666;
|
--table-header-border: 2px solid #666;
|
||||||
--table-row-border: 1px solid #CCC;
|
--table-row-border: 1px solid #CCC;
|
||||||
@@ -72,6 +75,8 @@
|
|||||||
|
|
||||||
--comment-sticky-background-color: var(--subnav-menu-background-color);
|
--comment-sticky-background-color: var(--subnav-menu-background-color);
|
||||||
|
|
||||||
|
--details-border: #DDD;
|
||||||
|
|
||||||
--post-tooltip-background-color: var(--body-background-color);
|
--post-tooltip-background-color: var(--body-background-color);
|
||||||
--post-tooltip-border-color: hsla(210, 100%, 3%, 0.15);
|
--post-tooltip-border-color: hsla(210, 100%, 3%, 0.15);
|
||||||
--post-tooltip-box-shadow: 0 4px 14px -2px hsla(210, 100%, 3%, 0.10);
|
--post-tooltip-box-shadow: 0 4px 14px -2px hsla(210, 100%, 3%, 0.10);
|
||||||
@@ -91,6 +96,7 @@
|
|||||||
--autocomplete-selected-background-color: var(--subnav-menu-background-color);
|
--autocomplete-selected-background-color: var(--subnav-menu-background-color);
|
||||||
--autocomplete-border: 1px solid #CCC;
|
--autocomplete-border: 1px solid #CCC;
|
||||||
--autocomplete-arrow-color: var(--text-color);
|
--autocomplete-arrow-color: var(--text-color);
|
||||||
|
--autocomplete-tag-autocorrect-underline: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAHElEQVQYV2NkQAL/GRj+M4IJBgY4zQhSABMEsQHMOAgCT5YN9gAAAABJRU5ErkJggg==);
|
||||||
|
|
||||||
--diff-list-added-color: green;
|
--diff-list-added-color: green;
|
||||||
--diff-list-removed-color: red;
|
--diff-list-removed-color: red;
|
||||||
@@ -157,7 +163,8 @@
|
|||||||
--bulk-update-request-failed-color: red;
|
--bulk-update-request-failed-color: red;
|
||||||
|
|
||||||
--login-link-color: #E00;
|
--login-link-color: #E00;
|
||||||
--footer-border: 1px solid #EEE;
|
--footer-border: 1px solid #DDD;
|
||||||
|
--details-border: #DDD;
|
||||||
|
|
||||||
--jquery-ui-widget-content-background: var(--body-background-color);
|
--jquery-ui-widget-content-background: var(--body-background-color);
|
||||||
--jquery-ui-widget-content-text-color: var(--text-color);
|
--jquery-ui-widget-content-text-color: var(--text-color);
|
||||||
@@ -201,6 +208,9 @@
|
|||||||
--user-member-color: var(--link-color);
|
--user-member-color: var(--link-color);
|
||||||
--user-banned-color: black;
|
--user-banned-color: black;
|
||||||
|
|
||||||
|
--user-verified-email-color: #0A0;
|
||||||
|
--user-unverified-email-color: #F80;
|
||||||
|
|
||||||
--news-updates-background: #EEE;
|
--news-updates-background: #EEE;
|
||||||
--news-updates-border: 2px solid #666;
|
--news-updates-border: 2px solid #666;
|
||||||
|
|
||||||
@@ -252,6 +262,7 @@ body[data-current-user-theme="dark"] {
|
|||||||
--subnav-menu-background-color: var(--grey-2);
|
--subnav-menu-background-color: var(--grey-2);
|
||||||
--responsive-menu-background-color: var(--grey-3);
|
--responsive-menu-background-color: var(--grey-3);
|
||||||
--footer-border: 1px solid var(--grey-3);
|
--footer-border: 1px solid var(--grey-3);
|
||||||
|
--details-border: var(--grey-3);
|
||||||
|
|
||||||
--table-header-border: 2px solid var(--grey-3);
|
--table-header-border: 2px solid var(--grey-3);
|
||||||
--table-even-row-background: var(--grey-2);
|
--table-even-row-background: var(--grey-2);
|
||||||
@@ -291,6 +302,9 @@ body[data-current-user-theme="dark"] {
|
|||||||
--user-moderator-color: var(--green-1);
|
--user-moderator-color: var(--green-1);
|
||||||
--user-admin-color: var(--red-1);
|
--user-admin-color: var(--red-1);
|
||||||
|
|
||||||
|
--user-verified-email-color: var(--green-1);
|
||||||
|
--user-unverified-email-color: var(--yellow-1);
|
||||||
|
|
||||||
/* misc specific colors */
|
/* misc specific colors */
|
||||||
--autocomplete-selected-background-color: var(--grey-3);
|
--autocomplete-selected-background-color: var(--grey-3);
|
||||||
--autocomplete-border: 1px solid var(--grey-4);
|
--autocomplete-border: 1px solid var(--grey-4);
|
||||||
@@ -413,9 +427,12 @@ body[data-current-user-theme="dark"] {
|
|||||||
--uploads-dropzone-progress-bar-foreground-color: var(--link-color);
|
--uploads-dropzone-progress-bar-foreground-color: var(--link-color);
|
||||||
--uploads-dropzone-progress-bar-background-color: var(--link-hover-color);
|
--uploads-dropzone-progress-bar-background-color: var(--link-hover-color);
|
||||||
|
|
||||||
--user-upgrade-gold-background-color: var(--yellow-0);
|
--user-upgrade-basic-background-color: var(--grey-2);
|
||||||
|
--user-upgrade-gold-background-color: var(--indigo-0);
|
||||||
--user-upgrade-platinum-background-color: var(--blue-0);
|
--user-upgrade-platinum-background-color: var(--blue-0);
|
||||||
--user-upgrade-table-row-hover-background-color: transparent;
|
--user-upgrade-button-text-color: white;
|
||||||
|
--user-upgrade-button-background-color: var(--link-color);
|
||||||
|
--user-upgrade-button-hover-background-color: var(--link-hover-color);
|
||||||
|
|
||||||
--wiki-page-other-name-background-color: var(--grey-3);
|
--wiki-page-other-name-background-color: var(--grey-3);
|
||||||
--wiki-page-versions-diff-del-background: var(--red-0);
|
--wiki-page-versions-diff-del-background: var(--red-0);
|
||||||
|
|||||||
@@ -21,4 +21,15 @@
|
|||||||
.autocomplete-arrow {
|
.autocomplete-arrow {
|
||||||
color: var(--autocomplete-arrow-color);
|
color: var(--autocomplete-arrow-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Display a red wavy underline beneath misspelled tags. */
|
||||||
|
/* https://stackoverflow.com/a/28152272 */
|
||||||
|
li[data-autocomplete-type="tag-autocorrect"] .autocomplete-antecedent {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--autocomplete-tag-autocorrect-underline);
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
background-position-y: 1.2em;
|
||||||
|
line-height: 1.5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ div.prose {
|
|||||||
div.expandable-content {
|
div.expandable-content {
|
||||||
display: none;
|
display: none;
|
||||||
padding: 0.4em;
|
padding: 0.4em;
|
||||||
border-top: 1px solid var(--dtext-expand-border-color);
|
|
||||||
|
|
||||||
> :last-child {
|
> :last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|||||||
@@ -320,3 +320,12 @@
|
|||||||
local("Anarchy"),
|
local("Anarchy"),
|
||||||
url("../../../../../public/fonts/Anarchy.ttf") format("truetype");
|
url("../../../../../public/fonts/Anarchy.ttf") format("truetype");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* https://www.1001freefonts.com/gisele-script.font */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Childlike";
|
||||||
|
font-display: swap;
|
||||||
|
src:
|
||||||
|
local("Gisele Script"),
|
||||||
|
url("../../../../../public/fonts/gisele.ttf") format("truetype");
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
body[data-current-user-style-usernames="true"] {
|
body[data-current-user-style-usernames="true"] {
|
||||||
|
a.user-owner {
|
||||||
|
color: var(--user-admin-color);
|
||||||
|
}
|
||||||
|
|
||||||
a.user-admin {
|
a.user-admin {
|
||||||
color: var(--user-admin-color);
|
color: var(--user-admin-color);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
margin-right: 0.25em;
|
margin-right: 0.25em;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&.user-tooltip-badge-owner { background-color: var(--user-admin-color); }
|
||||||
&.user-tooltip-badge-admin { background-color: var(--user-admin-color); }
|
&.user-tooltip-badge-admin { background-color: var(--user-admin-color); }
|
||||||
&.user-tooltip-badge-moderator { background-color: var(--user-moderator-color); }
|
&.user-tooltip-badge-moderator { background-color: var(--user-moderator-color); }
|
||||||
&.user-tooltip-badge-approver { background-color: var(--user-builder-color); }
|
&.user-tooltip-badge-approver { background-color: var(--user-builder-color); }
|
||||||
|
|||||||
@@ -1,43 +1,66 @@
|
|||||||
div#c-user-upgrades {
|
div#c-user-upgrades {
|
||||||
div#a-new {
|
div#a-new {
|
||||||
form.stripe {
|
margin: 0 auto;
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.section {
|
* {
|
||||||
margin-bottom: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
div#feature-comparison {
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
h1 {
|
||||||
width: 100%;
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
colgroup {
|
.login-button, form.button_to input[type="submit"] {
|
||||||
width: 10em;
|
display: inline-block;
|
||||||
}
|
|
||||||
|
|
||||||
colgroup#gold {
|
color: var(--user-upgrade-button-text-color);
|
||||||
background-color: var(--user-upgrade-gold-background-color);
|
background-color: var(--user-upgrade-button-background-color);
|
||||||
}
|
|
||||||
|
|
||||||
colgroup#platinum {
|
border: none;
|
||||||
background-color: var(--user-upgrade-platinum-background-color);
|
border-radius: 4px;
|
||||||
}
|
padding: 0.75em;
|
||||||
|
|
||||||
td, th {
|
transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, border 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
|
||||||
text-align: center;
|
box-shadow: 0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12);
|
||||||
vertical-align: top;
|
|
||||||
padding: 0.5em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody {
|
&:hover:not([disabled]) {
|
||||||
tr:hover {
|
background-color: var(--user-upgrade-button-hover-background-color);
|
||||||
background-color: var(--user-upgrade-table-row-hover-background-color);
|
box-shadow: 0px 2px 4px -1px rgba(0,0,0,0.2), 0px 4px 5px 0px rgba(0,0,0,0.14), 0px 1px 10px 0px rgba(0,0,0,0.12)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
&[disabled] {
|
||||||
|
background-color: grey;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table#feature-comparison {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
colgroup {
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
colgroup#basic {
|
||||||
|
background-color: var(--user-upgrade-basic-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
colgroup#gold {
|
||||||
|
background-color: var(--user-upgrade-gold-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
colgroup#platinum {
|
||||||
|
background-color: var(--user-upgrade-platinum-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
td, th {
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 0.5em 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ div#c-users {
|
|||||||
p {
|
p {
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-verified-email-icon {
|
||||||
|
color: var(--user-verified-email-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-unverified-email-icon {
|
||||||
|
color: var(--user-unverified-email-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ module ArtistFinder
|
|||||||
"www.artstation.com", # http://www.artstation.com/serafleur/
|
"www.artstation.com", # http://www.artstation.com/serafleur/
|
||||||
%r{cdn[ab]?\.artstation\.com/p/assets/images/images}i, # https://cdna.artstation.com/p/assets/images/images/001/658/068/large/yang-waterkuma-b402.jpg?1450269769
|
%r{cdn[ab]?\.artstation\.com/p/assets/images/images}i, # https://cdna.artstation.com/p/assets/images/images/001/658/068/large/yang-waterkuma-b402.jpg?1450269769
|
||||||
"ask.fm", # http://ask.fm/mikuroko_396
|
"ask.fm", # http://ask.fm/mikuroko_396
|
||||||
|
"baraag.net",
|
||||||
"bcyimg.com",
|
"bcyimg.com",
|
||||||
"bcyimg.com/drawer", # https://img9.bcyimg.com/drawer/32360/post/178vu/46229ec06e8111e79558c1b725ebc9e6.jpg
|
"bcyimg.com/drawer", # https://img9.bcyimg.com/drawer/32360/post/178vu/46229ec06e8111e79558c1b725ebc9e6.jpg
|
||||||
"bcy.net",
|
"bcy.net",
|
||||||
@@ -60,6 +61,7 @@ module ArtistFinder
|
|||||||
"iwara.tv/users", # http://ecchi.iwara.tv/users/marumega
|
"iwara.tv/users", # http://ecchi.iwara.tv/users/marumega
|
||||||
"kym-cdn.com",
|
"kym-cdn.com",
|
||||||
"livedoor.blogimg.jp",
|
"livedoor.blogimg.jp",
|
||||||
|
"blog.livedoor.jp", # http://blog.livedoor.jp/ac370ml
|
||||||
"monappy.jp",
|
"monappy.jp",
|
||||||
"monappy.jp/u", # https://monappy.jp/u/abara_bone
|
"monappy.jp/u", # https://monappy.jp/u/abara_bone
|
||||||
"mstdn.jp", # https://mstdn.jp/@oneb
|
"mstdn.jp", # https://mstdn.jp/@oneb
|
||||||
@@ -83,8 +85,8 @@ module ArtistFinder
|
|||||||
"pixiv.net", # https://www.pixiv.net/member.php?id=10442390
|
"pixiv.net", # https://www.pixiv.net/member.php?id=10442390
|
||||||
"pixiv.net/stacc", # https://www.pixiv.net/stacc/aaaninja2013
|
"pixiv.net/stacc", # https://www.pixiv.net/stacc/aaaninja2013
|
||||||
"pixiv.net/fanbox/creator", # https://www.pixiv.net/fanbox/creator/310630
|
"pixiv.net/fanbox/creator", # https://www.pixiv.net/fanbox/creator/310630
|
||||||
"pixiv.net/users", # https://www.pixiv.net/users/555603
|
%r{pixiv.net/(?:en/)?users}i, # https://www.pixiv.net/users/555603
|
||||||
"pixiv.net/en/users", # https://www.pixiv.net/en/users/555603
|
%r{pixiv.net/(?:en/)?artworks}i, # https://www.pixiv.net/en/artworks/85241178
|
||||||
"i.pximg.net",
|
"i.pximg.net",
|
||||||
"plurk.com", # http://www.plurk.com/a1amorea1a1
|
"plurk.com", # http://www.plurk.com/a1amorea1a1
|
||||||
"privatter.net",
|
"privatter.net",
|
||||||
|
|||||||
258
app/logical/autocomplete_service.rb
Normal file
258
app/logical/autocomplete_service.rb
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
class AutocompleteService
|
||||||
|
extend Memoist
|
||||||
|
|
||||||
|
POST_STATUSES = %w[active deleted pending flagged appealed banned modqueue unmoderated]
|
||||||
|
|
||||||
|
STATIC_METATAGS = {
|
||||||
|
status: %w[any] + POST_STATUSES,
|
||||||
|
child: %w[any none] + POST_STATUSES,
|
||||||
|
parent: %w[any none] + POST_STATUSES,
|
||||||
|
rating: %w[safe questionable explicit],
|
||||||
|
locked: %w[rating note status],
|
||||||
|
embedded: %w[true false],
|
||||||
|
filetype: %w[jpg png gif swf zip webm mp4],
|
||||||
|
commentary: %w[true false translated untranslated],
|
||||||
|
disapproved: PostDisapproval::REASONS,
|
||||||
|
order: PostQueryBuilder::ORDER_METATAGS
|
||||||
|
}
|
||||||
|
|
||||||
|
attr_reader :query, :type, :limit, :current_user
|
||||||
|
|
||||||
|
def initialize(query, type, current_user: User.anonymous, limit: 10)
|
||||||
|
@query = query.to_s
|
||||||
|
@type = type.to_s.to_sym
|
||||||
|
@current_user = current_user
|
||||||
|
@limit = limit
|
||||||
|
end
|
||||||
|
|
||||||
|
def autocomplete_results
|
||||||
|
case type
|
||||||
|
when :tag_query
|
||||||
|
autocomplete_tag_query(query)
|
||||||
|
when :tag
|
||||||
|
autocomplete_tag(query)
|
||||||
|
when :artist
|
||||||
|
autocomplete_artist(query)
|
||||||
|
when :wiki_page
|
||||||
|
autocomplete_wiki_page(query)
|
||||||
|
when :user
|
||||||
|
autocomplete_user(query)
|
||||||
|
when :mention
|
||||||
|
autocomplete_mention(query)
|
||||||
|
when :pool
|
||||||
|
autocomplete_pool(query)
|
||||||
|
when :favorite_group
|
||||||
|
autocomplete_favorite_group(query)
|
||||||
|
when :saved_search_label
|
||||||
|
autocomplete_saved_search_label(query)
|
||||||
|
when :opensearch
|
||||||
|
autocomplete_opensearch(query)
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def autocomplete_tag_query(string)
|
||||||
|
term = PostQueryBuilder.new(string).terms.first
|
||||||
|
return [] if term.nil?
|
||||||
|
|
||||||
|
case term.type
|
||||||
|
when :tag
|
||||||
|
autocomplete_tag(term.name)
|
||||||
|
when :metatag
|
||||||
|
autocomplete_metatag(term.name, term.value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def autocomplete_tag(string)
|
||||||
|
if string.starts_with?("/")
|
||||||
|
string = string + "*" unless string.include?("*")
|
||||||
|
|
||||||
|
results = tag_matches(string)
|
||||||
|
results += tag_abbreviation_matches(string)
|
||||||
|
results = results.sort_by do |r|
|
||||||
|
[r[:type] == "tag-alias" ? 0 : 1, r[:antecedent].to_s.size, -r[:post_count]]
|
||||||
|
end
|
||||||
|
|
||||||
|
results = results.uniq { |r| r[:value] }.take(limit)
|
||||||
|
elsif string.include?("*")
|
||||||
|
results = tag_matches(string)
|
||||||
|
results = tag_other_name_matches(string) if results.blank?
|
||||||
|
else
|
||||||
|
string += "*"
|
||||||
|
results = tag_matches(string)
|
||||||
|
results = tag_other_name_matches(string) if results.blank?
|
||||||
|
results = tag_autocorrect_matches(string) if results.blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
results
|
||||||
|
end
|
||||||
|
|
||||||
|
def tag_matches(string)
|
||||||
|
return [] if string =~ /[^[:ascii:]]/
|
||||||
|
|
||||||
|
name_matches = Tag.nonempty.name_matches(string).order(post_count: :desc).limit(limit)
|
||||||
|
alias_matches = Tag.nonempty.alias_matches(string).order(post_count: :desc).limit(limit)
|
||||||
|
union = "((#{name_matches.to_sql}) UNION (#{alias_matches.to_sql})) AS tags"
|
||||||
|
tags = Tag.from(union).order(post_count: :desc).limit(limit).includes(:consequent_aliases)
|
||||||
|
|
||||||
|
tags.map do |tag|
|
||||||
|
antecedent = tag.tag_alias_for_pattern(string)&.antecedent_name
|
||||||
|
type = antecedent.present? ? "tag-alias" : "tag"
|
||||||
|
{ type: type, label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count, antecedent: antecedent }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def tag_abbreviation_matches(string)
|
||||||
|
tags = Tag.nonempty.abbreviation_matches(string).order(post_count: :desc).limit(limit)
|
||||||
|
|
||||||
|
tags.map do |tag|
|
||||||
|
{ type: "tag-abbreviation", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count, antecedent: "/" + tag.abbreviation }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def tag_autocorrect_matches(string)
|
||||||
|
string = string.delete("*")
|
||||||
|
tags = Tag.nonempty.autocorrect_matches(string).limit(limit)
|
||||||
|
|
||||||
|
tags.map do |tag|
|
||||||
|
{ type: "tag-autocorrect", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count, antecedent: string }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def tag_other_name_matches(string)
|
||||||
|
return [] unless string =~ /[^[:ascii:]]/
|
||||||
|
|
||||||
|
artists = Artist.undeleted.any_other_name_like(string)
|
||||||
|
wikis = WikiPage.undeleted.other_names_match(string)
|
||||||
|
tags = Tag.where(name: wikis.select(:title)).or(Tag.where(name: artists.select(:name)))
|
||||||
|
tags = tags.nonempty.order(post_count: :desc).limit(limit).includes(:wiki_page, :artist)
|
||||||
|
|
||||||
|
tags.map do |tag|
|
||||||
|
other_names = tag.artist&.other_names.to_a + tag.wiki_page&.other_names.to_a
|
||||||
|
antecedent = other_names.find { |other_name| other_name.ilike?(string) }
|
||||||
|
{ type: "tag-other-name", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count, antecedent: antecedent }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def autocomplete_metatag(metatag, value)
|
||||||
|
results = case metatag.to_sym
|
||||||
|
when :user, :approver, :commenter, :comm, :noter, :noteupdater, :commentaryupdater,
|
||||||
|
:artcomm, :fav, :ordfav, :appealer, :flagger, :upvote, :downvote
|
||||||
|
autocomplete_user(value)
|
||||||
|
when :pool, :ordpool
|
||||||
|
autocomplete_pool(value)
|
||||||
|
when :favgroup, :ordfavgroup
|
||||||
|
autocomplete_favorite_group(value)
|
||||||
|
when :search
|
||||||
|
autocomplete_saved_search_label(value)
|
||||||
|
when *STATIC_METATAGS.keys
|
||||||
|
autocomplete_static_metatag(metatag, value)
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
results.map do |result|
|
||||||
|
{ **result, value: metatag + ":" + result[:value] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def autocomplete_static_metatag(metatag, value)
|
||||||
|
values = STATIC_METATAGS[metatag.to_sym]
|
||||||
|
results = values.select { |v| v.starts_with?(value) }.sort.take(limit)
|
||||||
|
|
||||||
|
results.map do |v|
|
||||||
|
{ label: metatag + ":" + v, value: v }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def autocomplete_pool(string)
|
||||||
|
string = "*" + string + "*" unless string.include?("*")
|
||||||
|
pools = Pool.undeleted.name_matches(string).search(order: "post_count").limit(limit)
|
||||||
|
|
||||||
|
pools.map do |pool|
|
||||||
|
{ type: "pool", label: pool.pretty_name, value: pool.name, post_count: pool.post_count, category: pool.category }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def autocomplete_favorite_group(string)
|
||||||
|
string = "*" + string + "*" unless string.include?("*")
|
||||||
|
favgroups = FavoriteGroup.visible(current_user).where(creator: current_user).name_matches(string).search(order: "post_count").limit(limit)
|
||||||
|
|
||||||
|
favgroups.map do |favgroup|
|
||||||
|
{ label: favgroup.pretty_name, value: favgroup.name, post_count: favgroup.post_count }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def autocomplete_saved_search_label(string)
|
||||||
|
string = "*" + string + "*" unless string.include?("*")
|
||||||
|
labels = current_user.saved_searches.labels_like(string).take(limit)
|
||||||
|
|
||||||
|
labels.map do |label|
|
||||||
|
{ label: label.tr("_", " "), value: label }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def autocomplete_artist(string)
|
||||||
|
string = string + "*" unless string.include?("*")
|
||||||
|
artists = Artist.undeleted.name_matches(string).search(order: "post_count").limit(limit)
|
||||||
|
|
||||||
|
artists.map do |artist|
|
||||||
|
{ type: "tag", label: artist.pretty_name, value: artist.name, category: Tag.categories.artist }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def autocomplete_wiki_page(string)
|
||||||
|
string = string + "*" unless string.include?("*")
|
||||||
|
wiki_pages = WikiPage.undeleted.title_matches(string).search(order: "post_count").limit(limit)
|
||||||
|
|
||||||
|
wiki_pages.map do |wiki_page|
|
||||||
|
{ type: "tag", label: wiki_page.pretty_title, value: wiki_page.title, category: wiki_page.tag&.category }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def autocomplete_user(string)
|
||||||
|
string = string + "*" unless string.include?("*")
|
||||||
|
users = User.search(name_matches: string, current_user_first: true, order: "post_upload_count").limit(limit)
|
||||||
|
|
||||||
|
users.map do |user|
|
||||||
|
{ type: "user", label: user.pretty_name, value: user.name, level: user.level_string }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def autocomplete_mention(string)
|
||||||
|
autocomplete_user(string).map do |result|
|
||||||
|
{ **result, value: "@" + result[:value] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def autocomplete_opensearch(string)
|
||||||
|
results = autocomplete_tag(string).map { |result| result[:value] }
|
||||||
|
[query, results]
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_duration
|
||||||
|
if autocomplete_results.size == limit
|
||||||
|
24.hours
|
||||||
|
else
|
||||||
|
1.hour
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Queries that don't depend on the current user are safe to cache publicly.
|
||||||
|
def cache_publicly?
|
||||||
|
if type == :tag_query && parsed_search&.type == :tag
|
||||||
|
true
|
||||||
|
elsif type.in?(%i[tag artist wiki_page pool opensearch])
|
||||||
|
true
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def parsed_search
|
||||||
|
PostQueryBuilder.new(query).terms.first
|
||||||
|
end
|
||||||
|
|
||||||
|
memoize :autocomplete_results
|
||||||
|
end
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
class BulkUpdateRequestProcessor
|
class BulkUpdateRequestProcessor
|
||||||
|
# Maximum tag size allowed by the rename command before an alias must be used.
|
||||||
MAXIMUM_RENAME_COUNT = 200
|
MAXIMUM_RENAME_COUNT = 200
|
||||||
|
|
||||||
|
# Maximum size of artist tags movable by builders.
|
||||||
|
MAXIMUM_BUILDER_MOVE_COUNT = 200
|
||||||
|
|
||||||
|
# Maximum number of lines a BUR may have.
|
||||||
MAXIMUM_SCRIPT_LENGTH = 100
|
MAXIMUM_SCRIPT_LENGTH = 100
|
||||||
|
|
||||||
include ActiveModel::Validations
|
include ActiveModel::Validations
|
||||||
@@ -55,20 +61,20 @@ class BulkUpdateRequestProcessor
|
|||||||
tag_alias = TagAlias.new(creator: User.system, antecedent_name: args[0], consequent_name: args[1])
|
tag_alias = TagAlias.new(creator: User.system, antecedent_name: args[0], consequent_name: args[1])
|
||||||
tag_alias.save(context: validation_context)
|
tag_alias.save(context: validation_context)
|
||||||
if tag_alias.errors.present?
|
if tag_alias.errors.present?
|
||||||
errors[:base] << "Can't create alias #{tag_alias.antecedent_name} -> #{tag_alias.consequent_name} (#{tag_alias.errors.full_messages.join("; ")})"
|
errors.add(:base, "Can't create alias #{tag_alias.antecedent_name} -> #{tag_alias.consequent_name} (#{tag_alias.errors.full_messages.join("; ")})")
|
||||||
end
|
end
|
||||||
|
|
||||||
when :create_implication
|
when :create_implication
|
||||||
tag_implication = TagImplication.new(creator: User.system, antecedent_name: args[0], consequent_name: args[1], status: "active")
|
tag_implication = TagImplication.new(creator: User.system, antecedent_name: args[0], consequent_name: args[1], status: "active")
|
||||||
tag_implication.save(context: validation_context)
|
tag_implication.save(context: validation_context)
|
||||||
if tag_implication.errors.present?
|
if tag_implication.errors.present?
|
||||||
errors[:base] << "Can't create implication #{tag_implication.antecedent_name} -> #{tag_implication.consequent_name} (#{tag_implication.errors.full_messages.join("; ")})"
|
errors.add(:base, "Can't create implication #{tag_implication.antecedent_name} -> #{tag_implication.consequent_name} (#{tag_implication.errors.full_messages.join("; ")})")
|
||||||
end
|
end
|
||||||
|
|
||||||
when :remove_alias
|
when :remove_alias
|
||||||
tag_alias = TagAlias.active.find_by(antecedent_name: args[0], consequent_name: args[1])
|
tag_alias = TagAlias.active.find_by(antecedent_name: args[0], consequent_name: args[1])
|
||||||
if tag_alias.nil?
|
if tag_alias.nil?
|
||||||
errors[:base] << "Can't remove alias #{args[0]} -> #{args[1]} (alias doesn't exist)"
|
errors.add(:base, "Can't remove alias #{args[0]} -> #{args[1]} (alias doesn't exist)")
|
||||||
else
|
else
|
||||||
tag_alias.update(status: "deleted")
|
tag_alias.update(status: "deleted")
|
||||||
end
|
end
|
||||||
@@ -76,7 +82,7 @@ class BulkUpdateRequestProcessor
|
|||||||
when :remove_implication
|
when :remove_implication
|
||||||
tag_implication = TagImplication.active.find_by(antecedent_name: args[0], consequent_name: args[1])
|
tag_implication = TagImplication.active.find_by(antecedent_name: args[0], consequent_name: args[1])
|
||||||
if tag_implication.nil?
|
if tag_implication.nil?
|
||||||
errors[:base] << "Can't remove implication #{args[0]} -> #{args[1]} (implication doesn't exist)"
|
errors.add(:base, "Can't remove implication #{args[0]} -> #{args[1]} (implication doesn't exist)")
|
||||||
else
|
else
|
||||||
tag_implication.update(status: "deleted")
|
tag_implication.update(status: "deleted")
|
||||||
end
|
end
|
||||||
@@ -84,22 +90,22 @@ class BulkUpdateRequestProcessor
|
|||||||
when :change_category
|
when :change_category
|
||||||
tag = Tag.find_by_name(args[0])
|
tag = Tag.find_by_name(args[0])
|
||||||
if tag.nil?
|
if tag.nil?
|
||||||
errors[:base] << "Can't change category #{args[0]} -> #{args[1]} (the '#{args[0]}' tag doesn't exist)"
|
errors.add(:base, "Can't change category #{args[0]} -> #{args[1]} (the '#{args[0]}' tag doesn't exist)")
|
||||||
end
|
end
|
||||||
|
|
||||||
when :rename
|
when :rename
|
||||||
tag = Tag.find_by_name(args[0])
|
tag = Tag.find_by_name(args[0])
|
||||||
if tag.nil?
|
if tag.nil?
|
||||||
errors[:base] << "Can't rename #{args[0]} -> #{args[1]} (the '#{args[0]}' tag doesn't exist)"
|
errors.add(:base, "Can't rename #{args[0]} -> #{args[1]} (the '#{args[0]}' tag doesn't exist)")
|
||||||
elsif tag.post_count > MAXIMUM_RENAME_COUNT
|
elsif tag.post_count > MAXIMUM_RENAME_COUNT
|
||||||
errors[:base] << "Can't rename #{args[0]} -> #{args[1]} ('#{args[0]}' has more than #{MAXIMUM_RENAME_COUNT} posts, use an alias instead)"
|
errors.add(:base, "Can't rename #{args[0]} -> #{args[1]} ('#{args[0]}' has more than #{MAXIMUM_RENAME_COUNT} posts, use an alias instead)")
|
||||||
end
|
end
|
||||||
|
|
||||||
when :mass_update, :nuke
|
when :mass_update, :nuke
|
||||||
# okay
|
# okay
|
||||||
|
|
||||||
when :invalid_line
|
when :invalid_line
|
||||||
errors[:base] << "Invalid line: #{args[0]}"
|
errors.add(:base, "Invalid line: #{args[0]}")
|
||||||
|
|
||||||
else
|
else
|
||||||
# should never happen
|
# should never happen
|
||||||
@@ -113,7 +119,7 @@ class BulkUpdateRequestProcessor
|
|||||||
|
|
||||||
def validate_script_length
|
def validate_script_length
|
||||||
if commands.size > MAXIMUM_SCRIPT_LENGTH
|
if commands.size > MAXIMUM_SCRIPT_LENGTH
|
||||||
errors[:base] << "Bulk update request is too long (maximum size: #{MAXIMUM_SCRIPT_LENGTH} lines). Split your request into smaller chunks and try again."
|
errors.add(:base, "Bulk update request is too long (maximum size: #{MAXIMUM_SCRIPT_LENGTH} lines). Split your request into smaller chunks and try again.")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -212,11 +218,18 @@ class BulkUpdateRequestProcessor
|
|||||||
end.join("\n")
|
end.join("\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Tag move is allowed if:
|
||||||
|
#
|
||||||
|
# * The antecedent tag is an artist tag.
|
||||||
|
# * The consequent_tag is a nonexistent tag, an empty tag (of any type), or an artist tag.
|
||||||
|
# * Both tags have less than 200 posts.
|
||||||
def self.is_tag_move_allowed?(antecedent_name, consequent_name)
|
def self.is_tag_move_allowed?(antecedent_name, consequent_name)
|
||||||
antecedent_tag = Tag.find_by_name(Tag.normalize_name(antecedent_name))
|
antecedent_tag = Tag.find_by_name(Tag.normalize_name(antecedent_name))
|
||||||
consequent_tag = Tag.find_by_name(Tag.normalize_name(consequent_name))
|
consequent_tag = Tag.find_by_name(Tag.normalize_name(consequent_name))
|
||||||
|
|
||||||
(antecedent_tag.blank? || antecedent_tag.empty? || (antecedent_tag.artist? && antecedent_tag.post_count <= 100)) &&
|
antecedent_allowed = antecedent_tag.present? && antecedent_tag.artist? && antecedent_tag.post_count < MAXIMUM_BUILDER_MOVE_COUNT
|
||||||
(consequent_tag.blank? || consequent_tag.empty? || (consequent_tag.artist? && consequent_tag.post_count <= 100))
|
consequent_allowed = consequent_tag.nil? || consequent_tag.empty? || (consequent_tag.artist? && consequent_tag.post_count < MAXIMUM_BUILDER_MOVE_COUNT)
|
||||||
|
|
||||||
|
antecedent_allowed && consequent_allowed
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
18
app/logical/concerns/normalizable.rb
Normal file
18
app/logical/concerns/normalizable.rb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
module Normalizable
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
class_methods do
|
||||||
|
def normalize(attribute, method_name)
|
||||||
|
define_method("#{attribute}=") do |value|
|
||||||
|
normalized_value = self.class.send(method_name, value)
|
||||||
|
super(normalized_value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def normalize_text(text)
|
||||||
|
text.unicode_normalize(:nfc).normalize_whitespace.strip
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,12 +10,13 @@ module Searchable
|
|||||||
1 + params.values.map { |v| parameter_hash?(v) ? parameter_depth(v) : 1 }.max
|
1 + params.values.map { |v| parameter_hash?(v) ? parameter_depth(v) : 1 }.max
|
||||||
end
|
end
|
||||||
|
|
||||||
def negate(kind = :nand)
|
def negate_relation
|
||||||
unscoped.where(all.where_clause.invert(kind).ast)
|
unscoped.where(all.where_clause.invert.ast)
|
||||||
end
|
end
|
||||||
|
|
||||||
# XXX hacky method to AND two relations together.
|
# XXX hacky method to AND two relations together.
|
||||||
def and(relation)
|
# XXX Replace with ActiveRecord#and (cf https://github.com/rails/rails/pull/39328)
|
||||||
|
def and_relation(relation)
|
||||||
q = all
|
q = all
|
||||||
q = q.where(relation.where_clause.ast) if relation.where_clause.present?
|
q = q.where(relation.where_clause.ast) if relation.where_clause.present?
|
||||||
q = q.joins(relation.joins_values + q.joins_values) if relation.joins_values.present?
|
q = q.joins(relation.joins_values + q.joins_values) if relation.joins_values.present?
|
||||||
@@ -52,7 +53,7 @@ module Searchable
|
|||||||
end
|
end
|
||||||
|
|
||||||
def where_iequals(attr, value)
|
def where_iequals(attr, value)
|
||||||
where_ilike(attr, value.gsub(/\\/, '\\\\').gsub(/\*/, '\*'))
|
where_ilike(attr, value.escape_wildcards)
|
||||||
end
|
end
|
||||||
|
|
||||||
# https://www.postgresql.org/docs/current/static/functions-matching.html#FUNCTIONS-POSIX-REGEXP
|
# https://www.postgresql.org/docs/current/static/functions-matching.html#FUNCTIONS-POSIX-REGEXP
|
||||||
@@ -101,16 +102,11 @@ module Searchable
|
|||||||
where_operator(qualified_column, *range)
|
where_operator(qualified_column, *range)
|
||||||
end
|
end
|
||||||
|
|
||||||
def search_boolean_attribute(attribute, params)
|
def search_boolean_attribute(attr, params)
|
||||||
return all unless params.key?(attribute)
|
if params[attr].present?
|
||||||
|
boolean_attribute_matches(attr, params[attr])
|
||||||
value = params[attribute].to_s
|
|
||||||
if value.truthy?
|
|
||||||
where(attribute => true)
|
|
||||||
elsif value.falsy?
|
|
||||||
where(attribute => false)
|
|
||||||
else
|
else
|
||||||
raise ArgumentError, "value must be truthy or falsy"
|
all
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -132,6 +128,18 @@ module Searchable
|
|||||||
where_operator(qualified_column, *range)
|
where_operator(qualified_column, *range)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def boolean_attribute_matches(attribute, value)
|
||||||
|
value = value.to_s
|
||||||
|
|
||||||
|
if value.truthy?
|
||||||
|
where(attribute => true)
|
||||||
|
elsif value.falsy?
|
||||||
|
where(attribute => false)
|
||||||
|
else
|
||||||
|
raise ArgumentError, "value must be truthy or falsy"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def text_attribute_matches(attribute, value, index_column: nil, ts_config: "english")
|
def text_attribute_matches(attribute, value, index_column: nil, ts_config: "english")
|
||||||
return all unless value.present?
|
return all unless value.present?
|
||||||
|
|
||||||
@@ -182,7 +190,7 @@ module Searchable
|
|||||||
when :boolean
|
when :boolean
|
||||||
search_boolean_attribute(name, params)
|
search_boolean_attribute(name, params)
|
||||||
when :integer, :datetime
|
when :integer, :datetime
|
||||||
numeric_attribute_matches(name, params[name])
|
search_numeric_attribute(name, params)
|
||||||
when :inet
|
when :inet
|
||||||
search_inet_attribute(name, params)
|
search_inet_attribute(name, params)
|
||||||
when :enum
|
when :enum
|
||||||
@@ -195,6 +203,26 @@ module Searchable
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def search_numeric_attribute(attr, params)
|
||||||
|
if params[attr].present?
|
||||||
|
numeric_attribute_matches(attr, params[attr])
|
||||||
|
elsif params[:"#{attr}_eq"].present?
|
||||||
|
where_operator(attr, :eq, params[:"#{attr}_eq"])
|
||||||
|
elsif params[:"#{attr}_not_eq"].present?
|
||||||
|
where_operator(attr, :not_eq, params[:"#{attr}_not_eq"])
|
||||||
|
elsif params[:"#{attr}_gt"].present?
|
||||||
|
where_operator(attr, :gt, params[:"#{attr}_gt"])
|
||||||
|
elsif params[:"#{attr}_gteq"].present?
|
||||||
|
where_operator(attr, :gteq, params[:"#{attr}_gteq"])
|
||||||
|
elsif params[:"#{attr}_lt"].present?
|
||||||
|
where_operator(attr, :lt, params[:"#{attr}_lt"])
|
||||||
|
elsif params[:"#{attr}_lteq"].present?
|
||||||
|
where_operator(attr, :lteq, params[:"#{attr}_lteq"])
|
||||||
|
else
|
||||||
|
all
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def search_text_attribute(attr, params)
|
def search_text_attribute(attr, params)
|
||||||
if params[attr].present?
|
if params[attr].present?
|
||||||
where(attr => params[attr])
|
where(attr => params[attr])
|
||||||
@@ -385,14 +413,6 @@ module Searchable
|
|||||||
where(id: ids).order(Arel.sql(order_clause.join(', ')))
|
where(id: ids).order(Arel.sql(order_clause.join(', ')))
|
||||||
end
|
end
|
||||||
|
|
||||||
def search(params = {})
|
|
||||||
params ||= {}
|
|
||||||
|
|
||||||
default_attributes = (attribute_names.map(&:to_sym) & %i[id created_at updated_at])
|
|
||||||
all_attributes = default_attributes + searchable_includes
|
|
||||||
search_attributes(params, *all_attributes)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def qualified_column_for(attr)
|
def qualified_column_for(attr)
|
||||||
|
|||||||
@@ -40,6 +40,14 @@ class CurrentUser
|
|||||||
RequestStore[:current_ip_addr] = ip_addr
|
RequestStore[:current_ip_addr] = ip_addr
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.country
|
||||||
|
RequestStore[:country]
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.country=(country)
|
||||||
|
RequestStore[:country] = country
|
||||||
|
end
|
||||||
|
|
||||||
def self.root_url
|
def self.root_url
|
||||||
RequestStore[:current_root_url] || "https://#{Danbooru.config.hostname}"
|
RequestStore[:current_root_url] || "https://#{Danbooru.config.hostname}"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -109,11 +109,11 @@ class DText
|
|||||||
end
|
end
|
||||||
|
|
||||||
if obj.is_approved?
|
if obj.is_approved?
|
||||||
"The \"bulk update request ##{obj.id}\":/bulk_update_requests/#{obj.id} has been approved by <@#{obj.approver.name}>.\n\n#{embedded_script}"
|
"The \"bulk update request ##{obj.id}\":#{Routes.bulk_update_request_path(obj)} has been approved by <@#{obj.approver.name}>.\n\n#{embedded_script}"
|
||||||
elsif obj.is_pending?
|
elsif obj.is_pending?
|
||||||
"The \"bulk update request ##{obj.id}\":/bulk_update_requests/#{obj.id} is pending approval.\n\n#{embedded_script}"
|
"The \"bulk update request ##{obj.id}\":#{Routes.bulk_update_request_path(obj)} is pending approval.\n\n#{embedded_script}"
|
||||||
elsif obj.is_rejected?
|
elsif obj.is_rejected?
|
||||||
"The \"bulk update request ##{obj.id}\":/bulk_update_requests/#{obj.id} has been rejected.\n\n#{embedded_script}"
|
"The \"bulk update request ##{obj.id}\":#{Routes.bulk_update_request_path(obj)} has been rejected.\n\n#{embedded_script}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
require "danbooru/http/application_client"
|
||||||
require "danbooru/http/html_adapter"
|
require "danbooru/http/html_adapter"
|
||||||
require "danbooru/http/xml_adapter"
|
require "danbooru/http/xml_adapter"
|
||||||
require "danbooru/http/cache"
|
require "danbooru/http/cache"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
class DanbooruLogger
|
class DanbooruLogger
|
||||||
|
HEADERS = %w[referer sec-fetch-dest sec-fetch-mode sec-fetch-site sec-fetch-user]
|
||||||
|
|
||||||
def self.info(message, params = {})
|
def self.info(message, params = {})
|
||||||
Rails.logger.info(message)
|
Rails.logger.info(message)
|
||||||
|
|
||||||
@@ -22,25 +24,52 @@ class DanbooruLogger
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.add_session_attributes(request, session, user)
|
def self.add_session_attributes(request, session, user)
|
||||||
request_params = request.parameters.with_indifferent_access.except(:controller, :action)
|
add_attributes("request", { path: request.path })
|
||||||
session_params = session.to_h.with_indifferent_access.slice(:session_id, :started_at)
|
add_attributes("request.headers", header_params(request))
|
||||||
user_params = { id: user&.id, name: user&.name, level: user&.level_string, ip: request.remote_ip, safe_mode: CurrentUser.safe_mode? }
|
add_attributes("request.params", request_params(request))
|
||||||
|
add_attributes("session.params", session_params(session))
|
||||||
|
add_attributes("user", user_params(request, user))
|
||||||
|
end
|
||||||
|
|
||||||
add_attributes("request.params", request_params)
|
def self.header_params(request)
|
||||||
add_attributes("session.params", session_params)
|
headers = request.headers.to_h.select { |header, value| header.match?(/\AHTTP_/) }
|
||||||
add_attributes("user", user_params)
|
headers = headers.transform_keys { |header| header.delete_prefix("HTTP_").downcase }
|
||||||
|
headers = headers.select { |header, value| header.in?(HEADERS) }
|
||||||
|
headers
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.request_params(request)
|
||||||
|
request.parameters.with_indifferent_access.except(:controller, :action)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.session_params(session)
|
||||||
|
session.to_h.with_indifferent_access.slice(:session_id, :started_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.user_params(request, user)
|
||||||
|
{
|
||||||
|
id: user&.id,
|
||||||
|
name: user&.name,
|
||||||
|
level: user&.level_string,
|
||||||
|
ip: request.remote_ip,
|
||||||
|
country: CurrentUser.country,
|
||||||
|
safe_mode: CurrentUser.safe_mode?
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.add_attributes(prefix, hash)
|
def self.add_attributes(prefix, hash)
|
||||||
return unless defined?(::NewRelic)
|
|
||||||
|
|
||||||
attributes = flatten_hash(hash).transform_keys { |key| "#{prefix}.#{key}" }
|
attributes = flatten_hash(hash).transform_keys { |key| "#{prefix}.#{key}" }
|
||||||
attributes.delete_if { |key, value| key.end_with?(*Rails.application.config.filter_parameters.map(&:to_s)) }
|
attributes.delete_if { |key, value| key.end_with?(*Rails.application.config.filter_parameters.map(&:to_s)) }
|
||||||
::NewRelic::Agent.add_custom_attributes(attributes)
|
add_custom_attributes(attributes)
|
||||||
end
|
end
|
||||||
|
|
||||||
private_class_method
|
private_class_method
|
||||||
|
|
||||||
|
def self.add_custom_attributes(attributes)
|
||||||
|
return unless defined?(::NewRelic)
|
||||||
|
::NewRelic::Agent.add_custom_attributes(attributes)
|
||||||
|
end
|
||||||
|
|
||||||
# flatten_hash({ foo: { bar: { baz: 42 } } })
|
# flatten_hash({ foo: { bar: { baz: 42 } } })
|
||||||
# => { "foo.bar.baz" => 42 }
|
# => { "foo.bar.baz" => 42 }
|
||||||
def self.flatten_hash(hash)
|
def self.flatten_hash(hash)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class DtextInput < SimpleForm::Inputs::Base
|
|||||||
t = template
|
t = template
|
||||||
merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)
|
merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)
|
||||||
|
|
||||||
t.tag.div(class: "dtext-previewable") do
|
t.tag.div(class: "dtext-previewable", spellcheck: true) do
|
||||||
if options[:inline]
|
if options[:inline]
|
||||||
t.concat @builder.text_field(attribute_name, merged_input_options)
|
t.concat @builder.text_field(attribute_name, merged_input_options)
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ require 'resolv'
|
|||||||
module EmailValidator
|
module EmailValidator
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
|
# https://www.regular-expressions.info/email.html
|
||||||
|
EMAIL_REGEX = /\A[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/
|
||||||
|
|
||||||
IGNORE_DOTS = %w[gmail.com]
|
IGNORE_DOTS = %w[gmail.com]
|
||||||
IGNORE_PLUS_ADDRESSING = %w[gmail.com hotmail.com outlook.com live.com]
|
IGNORE_PLUS_ADDRESSING = %w[gmail.com hotmail.com outlook.com live.com]
|
||||||
IGNORE_MINUS_ADDRESSING = %w[yahoo.com]
|
IGNORE_MINUS_ADDRESSING = %w[yahoo.com]
|
||||||
@@ -80,10 +83,17 @@ module EmailValidator
|
|||||||
"#{name}@#{domain}"
|
"#{name}@#{domain}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def nondisposable?(address)
|
def is_valid?(address)
|
||||||
return true if Danbooru.config.email_domain_verification_list.blank?
|
address.match?(EMAIL_REGEX)
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_restricted?(address)
|
||||||
|
return false if Danbooru.config.email_domain_verification_list.blank?
|
||||||
|
|
||||||
domain = Mail::Address.new(address).domain
|
domain = Mail::Address.new(address).domain
|
||||||
domain.in?(Danbooru.config.email_domain_verification_list.to_a)
|
!domain.in?(Danbooru.config.email_domain_verification_list.to_a)
|
||||||
|
rescue Mail::Field::IncompleteParseError
|
||||||
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def undeliverable?(to_address, from_address: Danbooru.config.contact_email, timeout: 3)
|
def undeliverable?(to_address, from_address: Danbooru.config.contact_email, timeout: 3)
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ require "strscan"
|
|||||||
class PostQueryBuilder
|
class PostQueryBuilder
|
||||||
extend Memoist
|
extend Memoist
|
||||||
|
|
||||||
|
# How many tags a `blah*` search should match.
|
||||||
|
MAX_WILDCARD_TAGS = 100
|
||||||
|
|
||||||
COUNT_METATAGS = %w[
|
COUNT_METATAGS = %w[
|
||||||
comment_count deleted_comment_count active_comment_count
|
comment_count deleted_comment_count active_comment_count
|
||||||
note_count deleted_note_count active_note_count
|
note_count deleted_note_count active_note_count
|
||||||
@@ -77,9 +80,9 @@ class PostQueryBuilder
|
|||||||
optional_tags = optional_tags.map(&:name)
|
optional_tags = optional_tags.map(&:name)
|
||||||
required_tags = required_tags.map(&:name)
|
required_tags = required_tags.map(&:name)
|
||||||
|
|
||||||
negated_tags += negated_wildcard_tags.flat_map { |tag| Tag.wildcard_matches(tag.name) }
|
negated_tags += negated_wildcard_tags.flat_map { |tag| Tag.wildcard_matches(tag.name).limit(MAX_WILDCARD_TAGS).pluck(:name) }
|
||||||
optional_tags += optional_wildcard_tags.flat_map { |tag| Tag.wildcard_matches(tag.name) }
|
optional_tags += optional_wildcard_tags.flat_map { |tag| Tag.wildcard_matches(tag.name).limit(MAX_WILDCARD_TAGS).pluck(:name) }
|
||||||
optional_tags += required_wildcard_tags.flat_map { |tag| Tag.wildcard_matches(tag.name) }
|
optional_tags += required_wildcard_tags.flat_map { |tag| Tag.wildcard_matches(tag.name).limit(MAX_WILDCARD_TAGS).pluck(:name) }
|
||||||
|
|
||||||
tsquery << "!(#{negated_tags.sort.uniq.map(&:to_escaped_for_tsquery).join(" | ")})" if negated_tags.present?
|
tsquery << "!(#{negated_tags.sort.uniq.map(&:to_escaped_for_tsquery).join(" | ")})" if negated_tags.present?
|
||||||
tsquery << "(#{optional_tags.sort.uniq.map(&:to_escaped_for_tsquery).join(" | ")})" if optional_tags.present?
|
tsquery << "(#{optional_tags.sort.uniq.map(&:to_escaped_for_tsquery).join(" | ")})" if optional_tags.present?
|
||||||
@@ -92,8 +95,8 @@ class PostQueryBuilder
|
|||||||
def metatags_match(metatags, relation)
|
def metatags_match(metatags, relation)
|
||||||
metatags.each do |metatag|
|
metatags.each do |metatag|
|
||||||
clause = metatag_matches(metatag.name, metatag.value, quoted: metatag.quoted)
|
clause = metatag_matches(metatag.name, metatag.value, quoted: metatag.quoted)
|
||||||
clause = clause.negate if metatag.negated
|
clause = clause.negate_relation if metatag.negated
|
||||||
relation = relation.and(clause)
|
relation = relation.and_relation(clause)
|
||||||
end
|
end
|
||||||
|
|
||||||
relation
|
relation
|
||||||
@@ -390,7 +393,8 @@ class PostQueryBuilder
|
|||||||
favuser = User.find_by_name(username)
|
favuser = User.find_by_name(username)
|
||||||
|
|
||||||
if favuser.present? && Pundit.policy!([current_user, nil], favuser).can_see_favorites?
|
if favuser.present? && Pundit.policy!([current_user, nil], favuser).can_see_favorites?
|
||||||
tags_include("fav:#{favuser.id}")
|
favorites = Favorite.from("favorites_#{favuser.id % 100} AS favorites").where(user: favuser)
|
||||||
|
Post.where(id: favorites.select(:post_id))
|
||||||
else
|
else
|
||||||
Post.none
|
Post.none
|
||||||
end
|
end
|
||||||
@@ -399,8 +403,8 @@ class PostQueryBuilder
|
|||||||
def ordfav_matches(username)
|
def ordfav_matches(username)
|
||||||
user = User.find_by_name(username)
|
user = User.find_by_name(username)
|
||||||
|
|
||||||
if user.present?
|
if user.present? && Pundit.policy!([current_user, nil], user).can_see_favorites?
|
||||||
favorites_include(username).joins(:favorites).merge(Favorite.for_user(user.id)).order("favorites.id DESC")
|
Post.joins(:favorites).merge(Favorite.for_user(user.id)).order("favorites.id DESC")
|
||||||
else
|
else
|
||||||
Post.none
|
Post.none
|
||||||
end
|
end
|
||||||
@@ -985,6 +989,11 @@ class PostQueryBuilder
|
|||||||
def is_wildcard_search?
|
def is_wildcard_search?
|
||||||
is_single_tag? && tags.first.wildcard
|
is_single_tag? && tags.first.wildcard
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def simple_tag
|
||||||
|
return nil if !is_simple_tag?
|
||||||
|
Tag.find_by_name(tags.first.name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
memoize :split_query, :normalized_query
|
memoize :split_query, :normalized_query
|
||||||
|
|||||||
@@ -187,16 +187,19 @@ module PostSets
|
|||||||
RelatedTagCalculator.frequent_tags_for_post_array(posts).take(MAX_SIDEBAR_TAGS)
|
RelatedTagCalculator.frequent_tags_for_post_array(posts).take(MAX_SIDEBAR_TAGS)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Wildcard searches can show up to 100 tags in the sidebar, not 25,
|
||||||
|
# because that's how many tags the search itself will use.
|
||||||
def wildcard_tags
|
def wildcard_tags
|
||||||
Tag.wildcard_matches(tag_string)
|
Tag.wildcard_matches(tag_string).limit(PostQueryBuilder::MAX_WILDCARD_TAGS).pluck(:name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def saved_search_tags
|
def saved_search_tags
|
||||||
["search:all"] + SavedSearch.labels_for(CurrentUser.user.id).map {|x| "search:#{x}"}
|
searches = ["search:all"] + SavedSearch.labels_for(CurrentUser.user.id).map {|x| "search:#{x}"}
|
||||||
|
searches.take(MAX_SIDEBAR_TAGS)
|
||||||
end
|
end
|
||||||
|
|
||||||
def tag_set_presenter
|
def tag_set_presenter
|
||||||
@tag_set_presenter ||= TagSetPresenter.new(related_tags.take(MAX_SIDEBAR_TAGS))
|
@tag_set_presenter ||= TagSetPresenter.new(related_tags)
|
||||||
end
|
end
|
||||||
|
|
||||||
def tag_list_html(**options)
|
def tag_list_html(**options)
|
||||||
|
|||||||
@@ -71,9 +71,12 @@ class RelatedTagQuery
|
|||||||
end
|
end
|
||||||
|
|
||||||
def other_wiki_pages
|
def other_wiki_pages
|
||||||
if Tag.category_for(query) == Tag.categories.copyright
|
tag = post_query.simple_tag
|
||||||
|
return [] if tag.nil?
|
||||||
|
|
||||||
|
if tag.copyright?
|
||||||
copyright_other_wiki_pages
|
copyright_other_wiki_pages
|
||||||
elsif Tag.category_for(query) == Tag.categories.general
|
elsif tag.general?
|
||||||
general_other_wiki_pages
|
general_other_wiki_pages
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
|
|||||||
11
app/logical/routes.rb
Normal file
11
app/logical/routes.rb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Allow Rails URL helpers to be used outside of views.
|
||||||
|
# Example: Routes.posts_path(tags: "touhou") => /posts?tags=touhou
|
||||||
|
|
||||||
|
class Routes
|
||||||
|
include Singleton
|
||||||
|
include Rails.application.routes.url_helpers
|
||||||
|
|
||||||
|
class << self
|
||||||
|
delegate_missing_to :instance
|
||||||
|
end
|
||||||
|
end
|
||||||
109
app/logical/server_status.rb
Normal file
109
app/logical/server_status.rb
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
class ServerStatus
|
||||||
|
extend Memoist
|
||||||
|
include ActiveModel::Serializers::JSON
|
||||||
|
include ActiveModel::Serializers::Xml
|
||||||
|
|
||||||
|
def serializable_hash(*options)
|
||||||
|
{
|
||||||
|
status: {
|
||||||
|
hostname: hostname,
|
||||||
|
uptime: uptime,
|
||||||
|
loadavg: loadavg,
|
||||||
|
ruby_version: RUBY_VERSION,
|
||||||
|
distro_version: distro_version,
|
||||||
|
kernel_version: kernel_version,
|
||||||
|
libvips_version: libvips_version,
|
||||||
|
ffmpeg_version: ffmpeg_version,
|
||||||
|
mkvmerge_version: mkvmerge_version,
|
||||||
|
redis_version: redis_version,
|
||||||
|
postgres_version: postgres_version,
|
||||||
|
},
|
||||||
|
postgres: {
|
||||||
|
connection_stats: postgres_connection_stats,
|
||||||
|
},
|
||||||
|
redis: {
|
||||||
|
info: redis_info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
concerning :InfoMethods do
|
||||||
|
def hostname
|
||||||
|
Socket.gethostname
|
||||||
|
end
|
||||||
|
|
||||||
|
def uptime
|
||||||
|
seconds = File.read("/proc/uptime").split[0].to_f
|
||||||
|
"#{seconds.seconds.in_days.round} days"
|
||||||
|
end
|
||||||
|
|
||||||
|
def loadavg
|
||||||
|
File.read("/proc/loadavg").chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
def kernel_version
|
||||||
|
File.read("/proc/version").chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
def distro_version
|
||||||
|
`. /etc/os-release; echo "$NAME $VERSION"`.chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
def libvips_version
|
||||||
|
Vips::LIBRARY_VERSION
|
||||||
|
end
|
||||||
|
|
||||||
|
def ffmpeg_version
|
||||||
|
version = `ffmpeg -version`
|
||||||
|
version[/ffmpeg version ([0-9.]+)/, 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
def mkvmerge_version
|
||||||
|
`mkvmerge --version`.chomp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
concerning :RedisMethods do
|
||||||
|
def redis_info
|
||||||
|
return {} if Rails.cache.try(:redis).nil?
|
||||||
|
Rails.cache.redis.info
|
||||||
|
end
|
||||||
|
|
||||||
|
def redis_used_memory
|
||||||
|
redis_info["used_memory_rss_human"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def redis_version
|
||||||
|
redis_info["redis_version"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
concerning :PostgresMethods do
|
||||||
|
def postgres_version
|
||||||
|
ApplicationRecord.connection.select_value("SELECT version()")
|
||||||
|
end
|
||||||
|
|
||||||
|
def postgres_active_connections
|
||||||
|
ApplicationRecord.connection.select_value("SELECT COUNT(*) FROM pg_stat_activity WHERE state = 'active'")
|
||||||
|
end
|
||||||
|
|
||||||
|
def postgres_connection_stats
|
||||||
|
run_query("SELECT pid, state, query_start, state_change, xact_start, backend_start, backend_type FROM pg_stat_activity ORDER BY state, query_start DESC, backend_type")
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_query(query)
|
||||||
|
result = ApplicationRecord.connection.select_all(query)
|
||||||
|
serialize_result(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialize_result(result)
|
||||||
|
result.rows.map do |row|
|
||||||
|
row.each_with_index.map do |col, i|
|
||||||
|
[result.columns[i], col]
|
||||||
|
end.to_h
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
memoize :redis_info
|
||||||
|
end
|
||||||
@@ -34,6 +34,7 @@ class SessionLoader
|
|||||||
update_last_logged_in_at
|
update_last_logged_in_at
|
||||||
update_last_ip_addr
|
update_last_ip_addr
|
||||||
set_time_zone
|
set_time_zone
|
||||||
|
set_country
|
||||||
set_safe_mode
|
set_safe_mode
|
||||||
initialize_session_cookies
|
initialize_session_cookies
|
||||||
CurrentUser.user.unban! if CurrentUser.user.ban_expired?
|
CurrentUser.user.unban! if CurrentUser.user.ban_expired?
|
||||||
@@ -87,7 +88,7 @@ class SessionLoader
|
|||||||
|
|
||||||
def update_last_logged_in_at
|
def update_last_logged_in_at
|
||||||
return if CurrentUser.is_anonymous?
|
return if CurrentUser.is_anonymous?
|
||||||
return if CurrentUser.last_logged_in_at && CurrentUser.last_logged_in_at > 1.week.ago
|
return if CurrentUser.last_logged_in_at && CurrentUser.last_logged_in_at > 1.hour.ago
|
||||||
CurrentUser.user.update_attribute(:last_logged_in_at, Time.now)
|
CurrentUser.user.update_attribute(:last_logged_in_at, Time.now)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -101,6 +102,12 @@ class SessionLoader
|
|||||||
Time.zone = CurrentUser.user.time_zone
|
Time.zone = CurrentUser.user.time_zone
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Depends on Cloudflare
|
||||||
|
# https://support.cloudflare.com/hc/en-us/articles/200168236-Configuring-Cloudflare-IP-Geolocation
|
||||||
|
def set_country
|
||||||
|
CurrentUser.country = request.headers["CF-IPCountry"]
|
||||||
|
end
|
||||||
|
|
||||||
def set_safe_mode
|
def set_safe_mode
|
||||||
safe_mode = request.host.match?(/safebooru/i) || params[:safe_mode].to_s.truthy? || CurrentUser.user.enable_safe_mode?
|
safe_mode = request.host.match?(/safebooru/i) || params[:safe_mode].to_s.truthy? || CurrentUser.user.enable_safe_mode?
|
||||||
CurrentUser.safe_mode = safe_mode
|
CurrentUser.safe_mode = safe_mode
|
||||||
|
|||||||
@@ -65,15 +65,15 @@ module Sources
|
|||||||
|
|
||||||
text = text.gsub(%r{https?://www\.pixiv\.net/member_illust\.php\?mode=medium&illust_id=([0-9]+)}i) do |_match|
|
text = text.gsub(%r{https?://www\.pixiv\.net/member_illust\.php\?mode=medium&illust_id=([0-9]+)}i) do |_match|
|
||||||
pixiv_id = $1
|
pixiv_id = $1
|
||||||
%(pixiv ##{pixiv_id} "»":[/posts?tags=pixiv:#{pixiv_id}])
|
%(pixiv ##{pixiv_id} "»":[#{Routes.posts_path(tags: "pixiv:#{pixiv_id}")}])
|
||||||
end
|
end
|
||||||
|
|
||||||
text = text.gsub(%r{https?://www\.pixiv\.net/member\.php\?id=([0-9]+)}i) do |_match|
|
text = text.gsub(%r{https?://www\.pixiv\.net/member\.php\?id=([0-9]+)}i) do |_match|
|
||||||
member_id = $1
|
member_id = $1
|
||||||
profile_url = "https://www.pixiv.net/users/#{member_id}"
|
profile_url = "https://www.pixiv.net/users/#{member_id}"
|
||||||
search_params = {"search[url_matches]" => profile_url}.to_param
|
artist_search_url = Routes.artists_path(search: { url_matches: profile_url })
|
||||||
|
|
||||||
%("user/#{member_id}":[#{profile_url}] "»":[/artists?#{search_params}])
|
%("user/#{member_id}":[#{profile_url}] "»":[#{artist_search_url}])
|
||||||
end
|
end
|
||||||
|
|
||||||
text = text.gsub(/\r\n|\r|\n/, "<br>")
|
text = text.gsub(/\r\n|\r|\n/, "<br>")
|
||||||
|
|||||||
@@ -90,6 +90,10 @@ module Sources
|
|||||||
image_urls.map { |img| img.gsub(%r{.cn/\w+/(\w+)}, '.cn/orj360/\1') }
|
image_urls.map { |img| img.gsub(%r{.cn/\w+/(\w+)}, '.cn/orj360/\1') }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def headers
|
||||||
|
{ "Referer" => "https://weibo.com" }
|
||||||
|
end
|
||||||
|
|
||||||
def page_url
|
def page_url
|
||||||
if api_response.present?
|
if api_response.present?
|
||||||
artist_id = api_response["user"]["id"]
|
artist_id = api_response["user"]["id"]
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
module TagAutocomplete
|
|
||||||
module_function
|
|
||||||
|
|
||||||
PREFIX_BOUNDARIES = "(_/:;-"
|
|
||||||
LIMIT = 10
|
|
||||||
|
|
||||||
Result = Struct.new(:name, :post_count, :category, :antecedent_name, :source) do
|
|
||||||
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)
|
|
||||||
|
|
||||||
count_sort(
|
|
||||||
search_exact(query, 8) +
|
|
||||||
search_prefix(query, 4) +
|
|
||||||
search_correct(query, 2) +
|
|
||||||
search_aliases(query, 3)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def count_sort(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_like(:name, query + "*")
|
|
||||||
.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_like(:name, query[0] + "*")
|
|
||||||
.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_like(:antecedent_name, wildcard_name)
|
|
||||||
.active
|
|
||||||
.where_not_like("tags.name", wildcard_name)
|
|
||||||
.where("tags.post_count > 0")
|
|
||||||
.order("tags.post_count desc")
|
|
||||||
.limit(n)
|
|
||||||
.pluck(:name, :post_count, :category, :antecedent_name)
|
|
||||||
.map {|row| Result.new(*row, :alias)}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,38 +1,40 @@
|
|||||||
class TagNameValidator < ActiveModel::EachValidator
|
class TagNameValidator < ActiveModel::EachValidator
|
||||||
def validate_each(record, attribute, value)
|
def validate_each(record, attribute, value)
|
||||||
case Tag.normalize_name(value)
|
value = Tag.normalize_name(value)
|
||||||
|
|
||||||
|
if value.size > 170
|
||||||
|
record.errors.add(attribute, "'#{value}' cannot be more than 255 characters long")
|
||||||
|
end
|
||||||
|
|
||||||
|
case value
|
||||||
when /\A_*\z/
|
when /\A_*\z/
|
||||||
record.errors[attribute] << "'#{value}' cannot be blank"
|
record.errors.add(attribute, "'#{value}' cannot be blank")
|
||||||
when /\*/
|
when /\*/
|
||||||
record.errors[attribute] << "'#{value}' cannot contain asterisks ('*')"
|
record.errors.add(attribute, "'#{value}' cannot contain asterisks ('*')")
|
||||||
when /,/
|
when /,/
|
||||||
record.errors[attribute] << "'#{value}' cannot contain commas (',')"
|
record.errors.add(attribute, "'#{value}' cannot contain commas (',')")
|
||||||
when /\A~/
|
when /\A[-~_`%){}\]\/]/
|
||||||
record.errors[attribute] << "'#{value}' cannot begin with a tilde ('~')"
|
record.errors.add(attribute, "'#{value}' cannot begin with a '#{value[0]}'")
|
||||||
when /\A-/
|
|
||||||
record.errors[attribute] << "'#{value}' cannot begin with a dash ('-')"
|
|
||||||
when /\A_/
|
|
||||||
record.errors[attribute] << "'#{value}' cannot begin with an underscore"
|
|
||||||
when /_\z/
|
when /_\z/
|
||||||
record.errors[attribute] << "'#{value}' cannot end with an underscore"
|
record.errors.add(attribute, "'#{value}' cannot end with an underscore")
|
||||||
when /__/
|
when /__/
|
||||||
record.errors[attribute] << "'#{value}' cannot contain consecutive underscores"
|
record.errors.add(attribute, "'#{value}' cannot contain consecutive underscores")
|
||||||
when /[^[:graph:]]/
|
when /[^[:graph:]]/
|
||||||
record.errors[attribute] << "'#{value}' cannot contain non-printable characters"
|
record.errors.add(attribute, "'#{value}' cannot contain non-printable characters")
|
||||||
when /[^[:ascii:]]/
|
when /[^[:ascii:]]/
|
||||||
record.errors[attribute] << "'#{value}' must consist of only ASCII characters"
|
record.errors.add(attribute, "'#{value}' must consist of only ASCII characters")
|
||||||
when /\A(#{PostQueryBuilder::METATAGS.join("|")}):(.+)\z/i
|
when /\A(#{PostQueryBuilder::METATAGS.join("|")}):(.+)\z/i
|
||||||
record.errors[attribute] << "'#{value}' cannot begin with '#{$1}:'"
|
record.errors.add(attribute, "'#{value}' cannot begin with '#{$1}:'")
|
||||||
when /\A(#{Tag.categories.regexp}):(.+)\z/i
|
when /\A(#{Tag.categories.regexp}):(.+)\z/i
|
||||||
record.errors[attribute] << "'#{value}' cannot begin with '#{$1}:'"
|
record.errors.add(attribute, "'#{value}' cannot begin with '#{$1}:'")
|
||||||
when "new", "search"
|
when "new", "search"
|
||||||
record.errors[attribute] << "'#{value}' is a reserved name and cannot be used"
|
record.errors.add(attribute, "'#{value}' is a reserved name and cannot be used")
|
||||||
when /\A(.+)_\(cosplay\)\z/i
|
when /\A(.+)_\(cosplay\)\z/i
|
||||||
tag_name = TagAlias.to_aliased([$1]).first
|
tag_name = TagAlias.to_aliased([$1]).first
|
||||||
tag = Tag.find_by_name(tag_name)
|
tag = Tag.find_by_name(tag_name)
|
||||||
|
|
||||||
if tag.present? && !tag.empty? && !tag.character?
|
if tag.present? && !tag.empty? && !tag.character?
|
||||||
record.errors[attribute] << "#{tag_name} must be a character tag"
|
record.errors.add(attribute, "#{tag_name} must be a character tag")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class UploadService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def comment_replacement_message(post, replacement)
|
def comment_replacement_message(post, replacement)
|
||||||
%("#{replacement.creator.name}":[/users/#{replacement.creator.id}] replaced this post with a new file:\n\n#{replacement_message(post, replacement)})
|
%("#{replacement.creator.name}":[#{Routes.user_path(replacement.creator)}] replaced this post with a new file:\n\n#{replacement_message(post, replacement)})
|
||||||
end
|
end
|
||||||
|
|
||||||
def replacement_message(post, replacement)
|
def replacement_message(post, replacement)
|
||||||
|
|||||||
@@ -61,11 +61,11 @@ class UserDeletion
|
|||||||
|
|
||||||
def validate_deletion
|
def validate_deletion
|
||||||
if !user.authenticate_password(password)
|
if !user.authenticate_password(password)
|
||||||
errors[:base] << "Password is incorrect"
|
errors.add(:base, "Password is incorrect")
|
||||||
end
|
end
|
||||||
|
|
||||||
if user.level >= User::Levels::ADMIN
|
if user.is_admin?
|
||||||
errors[:base] << "Admins cannot delete their account"
|
errors.add(:base, "Admins cannot delete their account")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ class UserNameValidator < ActiveModel::EachValidator
|
|||||||
def validate_each(rec, attr, value)
|
def validate_each(rec, attr, value)
|
||||||
name = value
|
name = value
|
||||||
|
|
||||||
rec.errors[attr] << "already exists" if User.find_by_name(name).present?
|
rec.errors.add(attr, "already exists") if User.find_by_name(name).present?
|
||||||
rec.errors[attr] << "must be 2 to 100 characters long" if !name.length.between?(2, 100)
|
rec.errors.add(attr, "must be 2 to 100 characters long") if !name.length.between?(2, 100)
|
||||||
rec.errors[attr] << "cannot have whitespace or colons" if name =~ /[[:space:]]|:/
|
rec.errors.add(attr, "cannot have whitespace or colons") if name =~ /[[:space:]]|:/
|
||||||
rec.errors[attr] << "cannot begin or end with an underscore" if name =~ /\A_|_\z/
|
rec.errors.add(attr, "cannot begin or end with an underscore") if name =~ /\A_|_\z/
|
||||||
rec.errors[attr] << "is not allowed" if name =~ Regexp.union(Danbooru.config.user_name_blacklist)
|
rec.errors.add(attr, "is not allowed") if name =~ Regexp.union(Danbooru.config.user_name_blacklist)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,33 +1,27 @@
|
|||||||
class UserPromotion
|
class UserPromotion
|
||||||
attr_reader :user, :promoter, :new_level, :options, :old_can_approve_posts, :old_can_upload_free
|
attr_reader :user, :promoter, :new_level, :old_can_approve_posts, :old_can_upload_free, :can_upload_free, :can_approve_posts
|
||||||
|
|
||||||
def initialize(user, promoter, new_level, options = {})
|
def initialize(user, promoter, new_level, can_upload_free: nil, can_approve_posts: nil)
|
||||||
@user = user
|
@user = user
|
||||||
@promoter = promoter
|
@promoter = promoter
|
||||||
@new_level = new_level
|
@new_level = new_level.to_i
|
||||||
@options = options
|
@can_upload_free = can_upload_free
|
||||||
|
@can_approve_posts = can_approve_posts
|
||||||
end
|
end
|
||||||
|
|
||||||
def promote!
|
def promote!
|
||||||
validate
|
validate!
|
||||||
|
|
||||||
@old_can_approve_posts = user.can_approve_posts?
|
@old_can_approve_posts = user.can_approve_posts?
|
||||||
@old_can_upload_free = user.can_upload_free?
|
@old_can_upload_free = user.can_upload_free?
|
||||||
|
|
||||||
user.level = new_level
|
user.level = new_level
|
||||||
|
user.can_upload_free = can_upload_free unless can_upload_free.nil?
|
||||||
|
user.can_approve_posts = can_approve_posts unless can_approve_posts.nil?
|
||||||
|
user.inviter = promoter
|
||||||
|
|
||||||
if options.key?(:can_approve_posts)
|
create_user_feedback
|
||||||
user.can_approve_posts = options[:can_approve_posts]
|
create_dmail
|
||||||
end
|
|
||||||
|
|
||||||
if options.key?(:can_upload_free)
|
|
||||||
user.can_upload_free = options[:can_upload_free]
|
|
||||||
end
|
|
||||||
|
|
||||||
user.inviter_id = promoter.id
|
|
||||||
|
|
||||||
create_user_feedback unless options[:is_upgrade]
|
|
||||||
create_dmail unless options[:skip_dmail]
|
|
||||||
create_mod_actions
|
create_mod_actions
|
||||||
|
|
||||||
user.save
|
user.save
|
||||||
@@ -37,28 +31,28 @@ class UserPromotion
|
|||||||
|
|
||||||
def create_mod_actions
|
def create_mod_actions
|
||||||
if old_can_approve_posts != user.can_approve_posts?
|
if old_can_approve_posts != user.can_approve_posts?
|
||||||
ModAction.log("\"#{promoter.name}\":/users/#{promoter.id} changed approval privileges for \"#{user.name}\":/users/#{user.id} from #{old_can_approve_posts} to [b]#{user.can_approve_posts?}[/b]", :user_approval_privilege)
|
ModAction.log("\"#{promoter.name}\":#{Routes.user_path(promoter)} changed approval privileges for \"#{user.name}\":#{Routes.user_path(user)} from #{old_can_approve_posts} to [b]#{user.can_approve_posts?}[/b]", :user_approval_privilege, promoter)
|
||||||
end
|
end
|
||||||
|
|
||||||
if old_can_upload_free != user.can_upload_free?
|
if old_can_upload_free != user.can_upload_free?
|
||||||
ModAction.log("\"#{promoter.name}\":/users/#{promoter.id} changed unlimited upload privileges for \"#{user.name}\":/users/#{user.id} from #{old_can_upload_free} to [b]#{user.can_upload_free?}[/b]", :user_upload_privilege)
|
ModAction.log("\"#{promoter.name}\":#{Routes.user_path(promoter)} changed unlimited upload privileges for \"#{user.name}\":#{Routes.user_path(user)} from #{old_can_upload_free} to [b]#{user.can_upload_free?}[/b]", :user_upload_privilege, promoter)
|
||||||
end
|
end
|
||||||
|
|
||||||
if user.level_changed?
|
if user.level_changed?
|
||||||
category = options[:is_upgrade] ? :user_account_upgrade : :user_level_change
|
ModAction.log(%{"#{user.name}":#{Routes.user_path(user)} level changed #{user.level_string_was} -> #{user.level_string}}, :user_level_change, promoter)
|
||||||
ModAction.log(%{"#{user.name}":/users/#{user.id} level changed #{user.level_string_was} -> #{user.level_string}}, category)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate
|
def validate!
|
||||||
# admins can do anything
|
if !promoter.is_moderator?
|
||||||
return if promoter.is_admin?
|
raise User::PrivilegeError, "You can't promote or demote other users"
|
||||||
|
elsif promoter == user
|
||||||
# can't promote/demote moderators
|
raise User::PrivilegeError, "You can't promote or demote yourself"
|
||||||
raise User::PrivilegeError if user.is_moderator?
|
elsif new_level >= promoter.level
|
||||||
|
raise User::PrivilegeError, "You can't promote other users to your rank or above"
|
||||||
# can't promote to admin
|
elsif user.level >= promoter.level
|
||||||
raise User::PrivilegeError if new_level.to_i >= User::Levels::ADMIN
|
raise User::PrivilegeError, "You can't promote or demote other users at your rank or above"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_messages
|
def build_messages
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
class UserUpgrade
|
|
||||||
def self.gold_price
|
|
||||||
2000
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.platinum_price
|
|
||||||
4000
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.upgrade_price
|
|
||||||
2000
|
|
||||||
end
|
|
||||||
end
|
|
||||||
51
app/logical/user_verifier.rb
Normal file
51
app/logical/user_verifier.rb
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Checks whether a new account seems suspicious and should require email verification.
|
||||||
|
|
||||||
|
class UserVerifier
|
||||||
|
attr_reader :current_user, :request
|
||||||
|
|
||||||
|
# current_user is the user creating the new account, not the new account itself.
|
||||||
|
def initialize(current_user, request)
|
||||||
|
@current_user, @request = current_user, request
|
||||||
|
end
|
||||||
|
|
||||||
|
def requires_verification?
|
||||||
|
return false if !Danbooru.config.new_user_verification?
|
||||||
|
return false if is_local_ip?
|
||||||
|
|
||||||
|
# we check for IP bans first to make sure we bump the IP ban hit count
|
||||||
|
is_ip_banned? || is_logged_in? || is_recent_signup? || is_proxy?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ip_address
|
||||||
|
@ip_address ||= IPAddress.parse(request.remote_ip)
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_local_ip?
|
||||||
|
if ip_address.ipv4?
|
||||||
|
ip_address.loopback? || ip_address.link_local? || ip_address.private?
|
||||||
|
elsif ip_address.ipv6?
|
||||||
|
ip_address.loopback? || ip_address.link_local? || ip_address.unique_local?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_logged_in?
|
||||||
|
!current_user.is_anonymous?
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_recent_signup?(age: 24.hours)
|
||||||
|
subnet_len = ip_address.ipv4? ? 24 : 64
|
||||||
|
subnet = "#{ip_address}/#{subnet_len}"
|
||||||
|
|
||||||
|
User.where("last_ip_addr <<= ?", subnet).where("created_at > ?", age.ago).exists?
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_ip_banned?
|
||||||
|
IpBan.hit!(:partial, ip_address.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_proxy?
|
||||||
|
IpLookup.new(ip_address).is_proxy?
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
class UserMailer < ApplicationMailer
|
class UserMailer < ApplicationMailer
|
||||||
add_template_helper ApplicationHelper
|
helper :application
|
||||||
add_template_helper UsersHelper
|
helper :users
|
||||||
|
|
||||||
def dmail_notice(dmail)
|
def dmail_notice(dmail)
|
||||||
@dmail = dmail
|
@dmail = dmail
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ class ApplicationRecord < ActiveRecord::Base
|
|||||||
|
|
||||||
include Deletable
|
include Deletable
|
||||||
include Mentionable
|
include Mentionable
|
||||||
|
include Normalizable
|
||||||
extend HasBitFlags
|
extend HasBitFlags
|
||||||
extend Searchable
|
extend Searchable
|
||||||
|
|
||||||
@@ -93,10 +94,6 @@ class ApplicationRecord < ActiveRecord::Base
|
|||||||
|
|
||||||
concerning :SearchMethods do
|
concerning :SearchMethods do
|
||||||
class_methods do
|
class_methods do
|
||||||
def searchable_includes
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
|
|
||||||
def model_restriction(table)
|
def model_restriction(table)
|
||||||
table.project(1)
|
table.project(1)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ class Artist < ApplicationRecord
|
|||||||
return unless !is_deleted? && name_changed? && tag.present?
|
return unless !is_deleted? && name_changed? && tag.present?
|
||||||
|
|
||||||
if tag.category_name != "Artist" && !tag.empty?
|
if tag.category_name != "Artist" && !tag.empty?
|
||||||
errors[:base] << "'#{name}' is a #{tag.category_name.downcase} tag; artist entries can only be created for artist tags"
|
errors.add(:base, "'#{name}' is a #{tag.category_name.downcase} tag; artist entries can only be created for artist tags")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -203,6 +203,10 @@ class Artist < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
module SearchMethods
|
module SearchMethods
|
||||||
|
def name_matches(query)
|
||||||
|
where_like(:name, normalize_name(query))
|
||||||
|
end
|
||||||
|
|
||||||
def any_other_name_matches(regex)
|
def any_other_name_matches(regex)
|
||||||
where(id: Artist.from("unnest(other_names) AS other_name").where_regex("other_name", regex))
|
where(id: Artist.from("unnest(other_names) AS other_name").where_regex("other_name", regex))
|
||||||
end
|
end
|
||||||
@@ -246,9 +250,7 @@ class Artist < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def search(params)
|
def search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :is_banned, :name, :group_name, :other_names, :urls, :wiki_page, :tag_alias, :tag)
|
||||||
|
|
||||||
q = q.search_attributes(params, :is_deleted, :is_banned, :name, :group_name, :other_names)
|
|
||||||
|
|
||||||
if params[:any_other_name_like]
|
if params[:any_other_name_like]
|
||||||
q = q.any_other_name_like(params[:any_other_name_like])
|
q = q.any_other_name_like(params[:any_other_name_like])
|
||||||
@@ -293,10 +295,6 @@ class Artist < ApplicationRecord
|
|||||||
super.where(table[:is_deleted].eq(false))
|
super.where(table[:is_deleted].eq(false))
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:urls, :wiki_page, :tag_alias, :tag]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:members, :urls, :wiki_page, :tag_alias, :tag]
|
[:members, :urls, :wiki_page, :tag_alias, :tag]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -31,9 +31,7 @@ class ArtistCommentary < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def search(params)
|
def search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :original_title, :original_description, :translated_title, :translated_description, :post)
|
||||||
|
|
||||||
q = q.search_attributes(params, :original_title, :original_description, :translated_title, :translated_description)
|
|
||||||
|
|
||||||
if params[:text_matches].present?
|
if params[:text_matches].present?
|
||||||
q = q.text_matches(params[:text_matches])
|
q = q.text_matches(params[:text_matches])
|
||||||
@@ -146,10 +144,6 @@ class ArtistCommentary < ApplicationRecord
|
|||||||
extend SearchMethods
|
extend SearchMethods
|
||||||
include VersionMethods
|
include VersionMethods
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:post]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:post]
|
[:post]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ class ArtistCommentaryVersion < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.search(params)
|
def self.search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :original_title, :original_description, :translated_title, :translated_description, :post, :updater)
|
||||||
q = q.search_attributes(params, :original_title, :original_description, :translated_title, :translated_description)
|
|
||||||
|
|
||||||
if params[:text_matches].present?
|
if params[:text_matches].present?
|
||||||
q = q.text_matches(params[:text_matches])
|
q = q.text_matches(params[:text_matches])
|
||||||
@@ -56,10 +55,6 @@ class ArtistCommentaryVersion < ApplicationRecord
|
|||||||
self[field].strip.empty? && (previous.nil? || previous[field].strip.empty?)
|
self[field].strip.empty? && (previous.nil? || previous[field].strip.empty?)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:post, :updater]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:post, :updater]
|
[:post, :updater]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -40,9 +40,7 @@ class ArtistUrl < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.search(params = {})
|
def self.search(params = {})
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :url, :normalized_url, :is_active, :artist)
|
||||||
|
|
||||||
q = q.search_attributes(params, :url, :normalized_url, :is_active)
|
|
||||||
|
|
||||||
q = q.url_matches(params[:url_matches])
|
q = q.url_matches(params[:url_matches])
|
||||||
q = q.normalized_url_matches(params[:normalized_url_matches])
|
q = q.normalized_url_matches(params[:normalized_url_matches])
|
||||||
@@ -113,11 +111,11 @@ class ArtistUrl < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def validate_scheme(uri)
|
def validate_scheme(uri)
|
||||||
errors[:url] << "'#{uri}' must begin with http:// or https:// " unless uri.scheme.in?(%w[http https])
|
errors.add(:url, "'#{uri}' must begin with http:// or https:// ") unless uri.scheme.in?(%w[http https])
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_hostname(uri)
|
def validate_hostname(uri)
|
||||||
errors[:url] << "'#{uri}' has a hostname '#{uri.host}' that does not contain a dot" unless uri.host&.include?('.')
|
errors.add(:url, "'#{uri}' has a hostname '#{uri.host}' that does not contain a dot") unless uri.host&.include?('.')
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_url_format
|
def validate_url_format
|
||||||
@@ -125,11 +123,7 @@ class ArtistUrl < ApplicationRecord
|
|||||||
validate_scheme(uri)
|
validate_scheme(uri)
|
||||||
validate_hostname(uri)
|
validate_hostname(uri)
|
||||||
rescue Addressable::URI::InvalidURIError => error
|
rescue Addressable::URI::InvalidURIError => error
|
||||||
errors[:url] << "'#{uri}' is malformed: #{error}"
|
errors.add(:url, "'#{uri}' is malformed: #{error}")
|
||||||
end
|
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:artist]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ class ArtistVersion < ApplicationRecord
|
|||||||
|
|
||||||
module SearchMethods
|
module SearchMethods
|
||||||
def search(params)
|
def search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :is_banned, :name, :group_name, :urls, :other_names, :updater, :artist)
|
||||||
|
|
||||||
q = q.search_attributes(params, :is_deleted, :is_banned, :name, :group_name, :urls, :other_names)
|
|
||||||
q = q.text_attribute_matches(:name, params[:name_matches])
|
q = q.text_attribute_matches(:name, params[:name_matches])
|
||||||
q = q.text_attribute_matches(:group_name, params[:group_name_matches])
|
q = q.text_attribute_matches(:group_name, params[:group_name_matches])
|
||||||
|
|
||||||
@@ -105,10 +103,6 @@ class ArtistVersion < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:updater, :artist]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:updater, :artist]
|
[:updater, :artist]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ class Ban < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.search(params)
|
def self.search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :expires_at, :reason, :user, :banner)
|
||||||
|
|
||||||
q = q.search_attributes(params, :expires_at, :reason)
|
|
||||||
q = q.text_attribute_matches(:reason, params[:reason_matches])
|
q = q.text_attribute_matches(:reason, params[:reason_matches])
|
||||||
|
|
||||||
q = q.expired if params[:expired].to_s.truthy?
|
q = q.expired if params[:expired].to_s.truthy?
|
||||||
@@ -45,7 +43,7 @@ class Ban < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def validate_user_is_bannable
|
def validate_user_is_bannable
|
||||||
self.errors[:user] << "is already banned" if user.is_banned?
|
errors.add(:user, "is already banned") if user.is_banned?
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_user_on_create
|
def update_user_on_create
|
||||||
@@ -89,10 +87,6 @@ class Ban < ApplicationRecord
|
|||||||
ModAction.log(%{Unbanned <@#{user_name}>}, :user_unban)
|
ModAction.log(%{Unbanned <@#{user_name}>}, :user_unban)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:user, :banner]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:user, :banner]
|
[:user, :banner]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -31,9 +31,7 @@ class BulkUpdateRequest < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def search(params = {})
|
def search(params = {})
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :script, :tags, :user, :forum_topic, :forum_post, :approver)
|
||||||
|
|
||||||
q = q.search_attributes(params, :script, :tags)
|
|
||||||
q = q.text_attribute_matches(:script, params[:script_matches])
|
q = q.text_attribute_matches(:script, params[:script_matches])
|
||||||
|
|
||||||
if params[:status].present?
|
if params[:status].present?
|
||||||
@@ -91,13 +89,13 @@ class BulkUpdateRequest < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def bulk_update_request_link
|
def bulk_update_request_link
|
||||||
%{"bulk update request ##{id}":/bulk_update_requests?search%5Bid%5D=#{id}}
|
%{"bulk update request ##{id}":#{Routes.bulk_update_requests_path(search: { id: id })}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_script
|
def validate_script
|
||||||
if processor.invalid?(:request)
|
if processor.invalid?(:request)
|
||||||
errors[:base] << processor.errors.full_messages.join("; ")
|
errors.add(:base, processor.errors.full_messages.join("; "))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -128,10 +126,6 @@ class BulkUpdateRequest < ApplicationRecord
|
|||||||
status == "rejected"
|
status == "rejected"
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:user, :forum_topic, :forum_post, :approver]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:user, :forum_topic, :forum_post, :approver]
|
[:user, :forum_topic, :forum_post, :approver]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,14 +21,12 @@ class Comment < ApplicationRecord
|
|||||||
mentionable(
|
mentionable(
|
||||||
:message_field => :body,
|
:message_field => :body,
|
||||||
:title => ->(user_name) {"#{creator.name} mentioned you in a comment on post ##{post_id}"},
|
:title => ->(user_name) {"#{creator.name} mentioned you in a comment on post ##{post_id}"},
|
||||||
:body => ->(user_name) {"@#{creator.name} mentioned you in a \"comment\":/posts/#{post_id}#comment-#{id} on post ##{post_id}:\n\n[quote]\n#{DText.extract_mention(body, "@" + user_name)}\n[/quote]\n"}
|
:body => ->(user_name) {"@#{creator.name} mentioned you in a \"comment\":#{Routes.post_path(post, anchor: "comment-#{id}")} on post ##{post_id}:\n\n[quote]\n#{DText.extract_mention(body, "@" + user_name)}\n[/quote]\n"}
|
||||||
)
|
)
|
||||||
|
|
||||||
module SearchMethods
|
module SearchMethods
|
||||||
def search(params)
|
def search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :is_sticky, :do_not_bump_post, :body, :score, :post, :creator, :updater)
|
||||||
|
|
||||||
q = q.search_attributes(params, :is_deleted, :is_sticky, :do_not_bump_post, :body, :score)
|
|
||||||
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :body_index)
|
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :body_index)
|
||||||
|
|
||||||
case params[:order]
|
case params[:order]
|
||||||
@@ -139,10 +137,6 @@ class Comment < ApplicationRecord
|
|||||||
DText.quote(body, creator.name)
|
DText.quote(body, creator.name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:post, :creator, :updater]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:post, :creator, :updater]
|
[:post, :creator, :updater]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,14 +19,13 @@ class CommentVote < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.search(params)
|
def self.search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :score, :comment, :user)
|
||||||
q = q.search_attributes(params, :score)
|
|
||||||
q.apply_default_order(params)
|
q.apply_default_order(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_comment_can_be_down_voted
|
def validate_comment_can_be_down_voted
|
||||||
if is_positive? && comment.creator == CurrentUser.user
|
if is_positive? && comment.creator == CurrentUser.user
|
||||||
errors.add :base, "You cannot upvote your own comments"
|
errors.add(:base, "You cannot upvote your own comments")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -38,10 +37,6 @@ class CommentVote < ApplicationRecord
|
|||||||
score == -1
|
score == -1
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:comment, :user]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:comment, :user]
|
[:comment, :user]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -98,9 +98,7 @@ class Dmail < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def search(params)
|
def search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :is_read, :is_deleted, :title, :body, :to, :from)
|
||||||
|
|
||||||
q = q.search_attributes(params, :is_read, :is_deleted, :title, :body)
|
|
||||||
q = q.text_attribute_matches(:title, params[:title_matches])
|
q = q.text_attribute_matches(:title, params[:title_matches])
|
||||||
q = q.text_attribute_matches(:body, params[:message_matches], index_column: :message_index)
|
q = q.text_attribute_matches(:body, params[:message_matches], index_column: :message_index)
|
||||||
|
|
||||||
@@ -158,7 +156,7 @@ class Dmail < ApplicationRecord
|
|||||||
return if from.blank? || from.is_gold?
|
return if from.blank? || from.is_gold?
|
||||||
|
|
||||||
if from.dmails.where("created_at > ?", 1.hour.ago).group(:to).reorder(nil).count.size >= 10
|
if from.dmails.where("created_at > ?", 1.hour.ago).group(:to).reorder(nil).count.size >= 10
|
||||||
errors[:base] << "You can't send dmails to more than 10 users per hour"
|
errors.add(:base, "You can't send dmails to more than 10 users per hour")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -182,10 +180,6 @@ class Dmail < ApplicationRecord
|
|||||||
key ? "dmail ##{id}/#{self.key}" : "dmail ##{id}"
|
key ? "dmail ##{id}/#{self.key}" : "dmail ##{id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:to, :from]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:owner, :to, :from]
|
[:owner, :to, :from]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ class DtextLink < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.search(params)
|
def self.search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :link_type, :link_target, :model, :linked_wiki, :linked_tag)
|
||||||
q = q.search_attributes(params, :link_type, :link_target)
|
|
||||||
q.apply_default_order(params)
|
q.apply_default_order(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -49,10 +48,6 @@ class DtextLink < ApplicationRecord
|
|||||||
where(link_type: :wiki_link)
|
where(link_type: :wiki_link)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:model, :linked_wiki, :linked_tag]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:model, :linked_wiki, :linked_tag]
|
[:model, :linked_wiki, :linked_tag]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,32 +1,67 @@
|
|||||||
class EmailAddress < ApplicationRecord
|
class EmailAddress < ApplicationRecord
|
||||||
# https://www.regular-expressions.info/email.html
|
|
||||||
EMAIL_REGEX = /\A[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/
|
|
||||||
|
|
||||||
belongs_to :user, inverse_of: :email_address
|
belongs_to :user, inverse_of: :email_address
|
||||||
|
|
||||||
validates :address, presence: true, confirmation: true, format: { with: EMAIL_REGEX }
|
validates :address, presence: true, confirmation: true, format: { with: EmailValidator::EMAIL_REGEX }
|
||||||
validates :normalized_address, uniqueness: true
|
validates :normalized_address, uniqueness: true
|
||||||
validates :user_id, uniqueness: true
|
validates :user_id, uniqueness: true
|
||||||
validate :validate_deliverable, on: :deliverable
|
validate :validate_deliverable, on: :deliverable
|
||||||
after_save :update_user
|
after_save :update_user
|
||||||
|
|
||||||
|
def self.visible(user)
|
||||||
|
if user.is_moderator?
|
||||||
|
where(user: User.where("level < ?", user.level).or(User.where(id: user.id)))
|
||||||
|
else
|
||||||
|
none
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def address=(value)
|
def address=(value)
|
||||||
self.normalized_address = EmailValidator.normalize(value) || address
|
self.normalized_address = EmailValidator.normalize(value) || address
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
def nondisposable?
|
def is_restricted?
|
||||||
EmailValidator.nondisposable?(normalized_address)
|
EmailValidator.is_restricted?(normalized_address)
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_normalized?
|
||||||
|
address == normalized_address
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_valid?
|
||||||
|
EmailValidator.is_valid?(address)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.restricted(restricted = true)
|
||||||
|
domains = Danbooru.config.email_domain_verification_list
|
||||||
|
domain_regex = domains.map { |domain| Regexp.escape(domain) }.join("|")
|
||||||
|
|
||||||
|
if restricted.to_s.truthy?
|
||||||
|
where_not_regex(:normalized_address, "@(#{domain_regex})$")
|
||||||
|
elsif restricted.to_s.falsy?
|
||||||
|
where_regex(:normalized_address, "@(#{domain_regex})$")
|
||||||
|
else
|
||||||
|
all
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.search(params)
|
||||||
|
q = search_attributes(params, :id, :created_at, :updated_at, :user, :address, :normalized_address, :is_verified, :is_deliverable)
|
||||||
|
|
||||||
|
q = q.restricted(params[:is_restricted])
|
||||||
|
q = q.apply_default_order(params)
|
||||||
|
|
||||||
|
q
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_deliverable
|
def validate_deliverable
|
||||||
if EmailValidator.undeliverable?(address)
|
if EmailValidator.undeliverable?(address)
|
||||||
errors[:address] << "is invalid or does not exist"
|
errors.add(:address, "is invalid or does not exist")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_user
|
def update_user
|
||||||
user.update!(is_verified: is_verified? && nondisposable?)
|
user.update!(is_verified: is_verified? && !is_restricted?)
|
||||||
end
|
end
|
||||||
|
|
||||||
concerning :VerificationMethods do
|
concerning :VerificationMethods do
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ class Favorite < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.search(params)
|
def self.search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :post)
|
||||||
q = q.search_attributes(params, :post)
|
|
||||||
|
|
||||||
if params[:user_id].present?
|
if params[:user_id].present?
|
||||||
q = q.for_user(params[:user_id])
|
q = q.for_user(params[:user_id])
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ class FavoriteGroup < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def search(params)
|
def search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :name, :is_public, :post_ids, :creator)
|
||||||
q = q.search_attributes(params, :name, :is_public, :post_ids)
|
|
||||||
|
|
||||||
if params[:name_matches].present?
|
if params[:name_matches].present?
|
||||||
q = q.name_matches(params[:name_matches])
|
q = q.name_matches(params[:name_matches])
|
||||||
@@ -56,13 +55,13 @@ class FavoriteGroup < ApplicationRecord
|
|||||||
if !creator.is_platinum?
|
if !creator.is_platinum?
|
||||||
error += " Upgrade your account to create more."
|
error += " Upgrade your account to create more."
|
||||||
end
|
end
|
||||||
self.errors.add(:base, error)
|
errors.add(:base, error)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_number_of_posts
|
def validate_number_of_posts
|
||||||
if post_count > 10_000
|
if post_count > 10_000
|
||||||
errors[:base] << "Favorite groups can have up to 10,000 posts each"
|
errors.add(:base, "Favorite groups can have up to 10,000 posts each")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -72,12 +71,12 @@ class FavoriteGroup < ApplicationRecord
|
|||||||
nonexisting_post_ids = added_post_ids - existing_post_ids
|
nonexisting_post_ids = added_post_ids - existing_post_ids
|
||||||
|
|
||||||
if nonexisting_post_ids.present?
|
if nonexisting_post_ids.present?
|
||||||
errors[:base] << "Cannot add invalid post(s) to favgroup: #{nonexisting_post_ids.to_sentence}"
|
errors.add(:base, "Cannot add invalid post(s) to favgroup: #{nonexisting_post_ids.to_sentence}")
|
||||||
end
|
end
|
||||||
|
|
||||||
duplicate_post_ids = post_ids.group_by(&:itself).transform_values(&:size).select { |id, count| count > 1 }.keys
|
duplicate_post_ids = post_ids.group_by(&:itself).transform_values(&:size).select { |id, count| count > 1 }.keys
|
||||||
if duplicate_post_ids.present?
|
if duplicate_post_ids.present?
|
||||||
errors[:base] << "Favgroup already contains post #{duplicate_post_ids.to_sentence}"
|
errors.add(:base, "Favgroup already contains post #{duplicate_post_ids.to_sentence}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -164,10 +163,6 @@ class FavoriteGroup < ApplicationRecord
|
|||||||
post_ids.include?(post_id)
|
post_ids.include?(post_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:creator]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:creator]
|
[:creator]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class ForumPost < ApplicationRecord
|
|||||||
mentionable(
|
mentionable(
|
||||||
:message_field => :body,
|
:message_field => :body,
|
||||||
:title => ->(user_name) {%{#{creator.name} mentioned you in topic ##{topic_id} (#{topic.title})}},
|
:title => ->(user_name) {%{#{creator.name} mentioned you in topic ##{topic_id} (#{topic.title})}},
|
||||||
:body => ->(user_name) {%{@#{creator.name} mentioned you in topic ##{topic_id} ("#{topic.title}":[/forum_topics/#{topic_id}?page=#{forum_topic_page}]):\n\n[quote]\n#{DText.extract_mention(body, "@" + user_name)}\n[/quote]\n}}
|
:body => ->(user_name) {%{@#{creator.name} mentioned you in topic ##{topic_id} ("#{topic.title}":[#{Routes.forum_topic_path(topic, page: forum_topic_page)}]):\n\n[quote]\n#{DText.extract_mention(body, "@" + user_name)}\n[/quote]\n}}
|
||||||
)
|
)
|
||||||
|
|
||||||
module SearchMethods
|
module SearchMethods
|
||||||
@@ -36,13 +36,16 @@ class ForumPost < ApplicationRecord
|
|||||||
where(topic_id: ForumTopic.visible(user))
|
where(topic_id: ForumTopic.visible(user))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def wiki_link_matches(title)
|
||||||
|
where(id: DtextLink.forum_post.wiki_link.where(link_target: WikiPage.normalize_title(title)).select(:model_id))
|
||||||
|
end
|
||||||
|
|
||||||
def search(params)
|
def search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :body, :creator, :updater, :topic, :dtext_links, :votes, :tag_alias, :tag_implication, :bulk_update_request)
|
||||||
q = q.search_attributes(params, :is_deleted, :body)
|
|
||||||
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :text_index)
|
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :text_index)
|
||||||
|
|
||||||
if params[:linked_to].present?
|
if params[:linked_to].present?
|
||||||
q = q.where(id: DtextLink.forum_post.wiki_link.where(link_target: params[:linked_to]).select(:model_id))
|
q = q.wiki_link_matches(params[:linked_to])
|
||||||
end
|
end
|
||||||
|
|
||||||
q.apply_default_order(params)
|
q.apply_default_order(params)
|
||||||
@@ -160,10 +163,6 @@ class ForumPost < ApplicationRecord
|
|||||||
"forum ##{id}"
|
"forum ##{id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:creator, :updater, :topic, :dtext_links, :votes, :tag_alias, :tag_implication, :bulk_update_request]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:creator, :updater, :topic, :dtext_links, :votes, :tag_alias, :tag_implication, :bulk_update_request]
|
[:creator, :updater, :topic, :dtext_links, :votes, :tag_alias, :tag_implication, :bulk_update_request]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ class ForumPostVote < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.search(params)
|
def self.search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :score, :creator, :forum_post)
|
||||||
q = q.search_attributes(params, :score)
|
|
||||||
q = q.forum_post_matches(params[:forum_post])
|
q = q.forum_post_matches(params[:forum_post])
|
||||||
q.apply_default_order(params)
|
q.apply_default_order(params)
|
||||||
end
|
end
|
||||||
@@ -59,10 +58,6 @@ class ForumPostVote < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:creator, :forum_post]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:creator, :forum_post]
|
[:creator, :forum_post]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -86,8 +86,7 @@ class ForumTopic < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def search(params)
|
def search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :is_sticky, :is_locked, :is_deleted, :category_id, :title, :response_count, :creator, :updater, :forum_posts, :bulk_update_requests, :tag_aliases, :tag_implications)
|
||||||
q = q.search_attributes(params, :is_sticky, :is_locked, :is_deleted, :category_id, :title, :response_count)
|
|
||||||
q = q.text_attribute_matches(:title, params[:title_matches], index_column: :text_index)
|
q = q.text_attribute_matches(:title, params[:title_matches], index_column: :text_index)
|
||||||
|
|
||||||
if params[:is_private].to_s.truthy?
|
if params[:is_private].to_s.truthy?
|
||||||
@@ -190,10 +189,6 @@ class ForumTopic < ApplicationRecord
|
|||||||
title.gsub(/\A\[APPROVED\]|\[REJECTED\]/, "")
|
title.gsub(/\A\[APPROVED\]|\[REJECTED\]/, "")
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:creator, :updater, :forum_posts, :bulk_update_requests, :tag_aliases, :tag_implications]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:creator, :updater, :original_post]
|
[:creator, :updater, :original_post]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ class ForumTopicVisit < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.search(params)
|
def self.search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :user, :forum_topic_id, :last_read_at)
|
||||||
q = q.search_attributes(params, :user, :forum_topic_id, :last_read_at)
|
|
||||||
q.apply_default_order(params)
|
q.apply_default_order(params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ class IpAddress < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.search(params)
|
def self.search(params)
|
||||||
q = super
|
q = search_attributes(params, :ip_addr, :user, :model)
|
||||||
q = q.search_attributes(params, :ip_addr)
|
|
||||||
q.order(created_at: :desc)
|
q.order(created_at: :desc)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -50,10 +49,6 @@ class IpAddress < ApplicationRecord
|
|||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:user, :model]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:user, :model]
|
[:user, :model]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,15 +25,18 @@ class IpBan < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.search(params)
|
def self.search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :ip_addr, :reason, :is_deleted, :category, :hit_count, :last_hit_at, :creator)
|
||||||
q = q.search_attributes(params, :reason)
|
|
||||||
q = q.text_attribute_matches(:reason, params[:reason_matches])
|
q = q.text_attribute_matches(:reason, params[:reason_matches])
|
||||||
|
|
||||||
if params[:ip_addr].present?
|
case params[:order]
|
||||||
q = q.where("ip_addr = ?", params[:ip_addr])
|
when /\A(created_at|updated_at|last_hit_at)(?:_(asc|desc))?\z/i
|
||||||
|
dir = $2 || :desc
|
||||||
|
q = q.order($1 => dir).order(id: :desc)
|
||||||
|
else
|
||||||
|
q = q.apply_default_order(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
q.apply_default_order(params)
|
q
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_mod_action
|
def create_mod_action
|
||||||
@@ -48,19 +51,19 @@ class IpBan < ApplicationRecord
|
|||||||
|
|
||||||
def validate_ip_addr
|
def validate_ip_addr
|
||||||
if ip_addr.blank?
|
if ip_addr.blank?
|
||||||
errors[:ip_addr] << "is invalid"
|
errors.add(:ip_addr, "is invalid")
|
||||||
elsif ip_addr.private? || ip_addr.loopback? || ip_addr.link_local?
|
elsif ip_addr.private? || ip_addr.loopback? || ip_addr.link_local?
|
||||||
errors[:ip_addr] << "must be a public address"
|
errors.add(:ip_addr, "must be a public address")
|
||||||
elsif full_ban? && ip_addr.ipv4? && ip_addr.prefix < 24
|
elsif full_ban? && ip_addr.ipv4? && ip_addr.prefix < 24
|
||||||
errors[:ip_addr] << "may not have a subnet bigger than /24"
|
errors.add(:ip_addr, "may not have a subnet bigger than /24")
|
||||||
elsif partial_ban? && ip_addr.ipv4? && ip_addr.prefix < 8
|
elsif partial_ban? && ip_addr.ipv4? && ip_addr.prefix < 8
|
||||||
errors[:ip_addr] << "may not have a subnet bigger than /8"
|
errors.add(:ip_addr, "may not have a subnet bigger than /8")
|
||||||
elsif full_ban? && ip_addr.ipv6? && ip_addr.prefix < 64
|
elsif full_ban? && ip_addr.ipv6? && ip_addr.prefix < 64
|
||||||
errors[:ip_addr] << "may not have a subnet bigger than /64"
|
errors.add(:ip_addr, "may not have a subnet bigger than /64")
|
||||||
elsif partial_ban? && ip_addr.ipv6? && ip_addr.prefix < 20
|
elsif partial_ban? && ip_addr.ipv6? && ip_addr.prefix < 20
|
||||||
errors[:ip_addr] << "may not have a subnet bigger than /20"
|
errors.add(:ip_addr, "may not have a subnet bigger than /20")
|
||||||
elsif new_record? && IpBan.active.ip_matches(subnetted_ip).exists?
|
elsif new_record? && IpBan.active.ip_matches(subnetted_ip).exists?
|
||||||
errors[:ip_addr] << "is already banned"
|
errors.add(:ip_addr, "is already banned")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -78,10 +81,6 @@ class IpBan < ApplicationRecord
|
|||||||
super(ip_addr.strip)
|
super(ip_addr.strip)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:creator]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:creator]
|
[:creator]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -61,9 +61,7 @@ class ModAction < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.search(params)
|
def self.search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :category, :description, :creator)
|
||||||
|
|
||||||
q = q.search_attributes(params, :category, :description)
|
|
||||||
q = q.text_attribute_matches(:description, params[:description_matches])
|
q = q.text_attribute_matches(:description, params[:description_matches])
|
||||||
|
|
||||||
q.apply_default_order(params)
|
q.apply_default_order(params)
|
||||||
@@ -77,10 +75,6 @@ class ModAction < ApplicationRecord
|
|||||||
create(creator: user, description: desc, category: categories[cat])
|
create(creator: user, description: desc, category: categories[cat])
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:creator]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:creator]
|
[:creator]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -82,17 +82,12 @@ class ModerationReport < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.search(params)
|
def self.search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :reason, :creator, :model)
|
||||||
q = q.search_attributes(params, :reason)
|
|
||||||
q = q.text_attribute_matches(:reason, params[:reason_matches])
|
q = q.text_attribute_matches(:reason, params[:reason_matches])
|
||||||
|
|
||||||
q.apply_default_order(params)
|
q.apply_default_order(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:creator, :model]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:creator, :model]
|
[:creator, :model]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ class Note < ApplicationRecord
|
|||||||
|
|
||||||
module SearchMethods
|
module SearchMethods
|
||||||
def search(params)
|
def search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :is_active, :x, :y, :width, :height, :body, :version, :post)
|
||||||
|
|
||||||
q = q.search_attributes(params, :is_active, :x, :y, :width, :height, :body, :version)
|
|
||||||
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :body_index)
|
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :body_index)
|
||||||
|
|
||||||
q.apply_default_order(params)
|
q.apply_default_order(params)
|
||||||
@@ -26,13 +24,13 @@ class Note < ApplicationRecord
|
|||||||
extend SearchMethods
|
extend SearchMethods
|
||||||
|
|
||||||
def validate_post_is_not_locked
|
def validate_post_is_not_locked
|
||||||
errors[:post] << "is note locked" if post.is_note_locked?
|
errors.add(:post, "is note locked") if post.is_note_locked?
|
||||||
end
|
end
|
||||||
|
|
||||||
def note_within_image
|
def note_within_image
|
||||||
return false unless post.present?
|
return false unless post.present?
|
||||||
if x < 0 || y < 0 || (x > post.image_width) || (y > post.image_height) || width < 0 || height < 0 || (x + width > post.image_width) || (y + height > post.image_height)
|
if x < 0 || y < 0 || (x > post.image_width) || (y > post.image_height) || width < 0 || height < 0 || (x + width > post.image_width) || (y + height > post.image_height)
|
||||||
self.errors.add(:note, "must be inside the image")
|
errors.add(:note, "must be inside the image")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -129,10 +127,6 @@ class Note < ApplicationRecord
|
|||||||
new_note.save
|
new_note.save
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:post]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:post]
|
[:post]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ class NoteVersion < ApplicationRecord
|
|||||||
belongs_to_updater :counter_cache => "note_update_count"
|
belongs_to_updater :counter_cache => "note_update_count"
|
||||||
|
|
||||||
def self.search(params)
|
def self.search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :is_active, :x, :y, :width, :height, :body, :version, :updater, :note, :post)
|
||||||
|
|
||||||
q = q.search_attributes(params, :is_active, :x, :y, :width, :height, :body, :version)
|
|
||||||
q = q.text_attribute_matches(:body, params[:body_matches])
|
q = q.text_attribute_matches(:body, params[:body_matches])
|
||||||
|
|
||||||
q.apply_default_order(params)
|
q.apply_default_order(params)
|
||||||
@@ -71,10 +69,6 @@ class NoteVersion < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:updater, :note, :post]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:updater, :note, :post]
|
[:updater, :note, :post]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,17 +4,12 @@ class PixivUgoiraFrameData < ApplicationRecord
|
|||||||
serialize :data
|
serialize :data
|
||||||
before_validation :normalize_data, on: :create
|
before_validation :normalize_data, on: :create
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:post]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:post]
|
[:post]
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.search(params)
|
def self.search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :data, :content_type, :post)
|
||||||
q = q.search_attributes(params, :data, :content_type)
|
|
||||||
q.apply_default_order(params)
|
q.apply_default_order(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,7 @@ class Pool < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def search(params)
|
def search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :name, :description, :post_ids)
|
||||||
|
|
||||||
q = q.search_attributes(params, :is_deleted, :name, :description, :post_ids)
|
|
||||||
q = q.text_attribute_matches(:description, params[:description_matches])
|
q = q.text_attribute_matches(:description, params[:description_matches])
|
||||||
|
|
||||||
if params[:post_tags_match]
|
if params[:post_tags_match]
|
||||||
@@ -147,7 +145,7 @@ class Pool < ApplicationRecord
|
|||||||
|
|
||||||
def updater_can_edit_deleted
|
def updater_can_edit_deleted
|
||||||
if is_deleted? && !Pundit.policy!([CurrentUser.user, nil], self).update?
|
if is_deleted? && !Pundit.policy!([CurrentUser.user, nil], self).update?
|
||||||
errors[:base] << "You cannot update pools that are deleted"
|
errors.add(:base, "You cannot update pools that are deleted")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -254,23 +252,23 @@ class Pool < ApplicationRecord
|
|||||||
def validate_name
|
def validate_name
|
||||||
case name
|
case name
|
||||||
when /\A(any|none|series|collection)\z/i
|
when /\A(any|none|series|collection)\z/i
|
||||||
errors[:name] << "cannot be any of the following names: any, none, series, collection"
|
errors.add(:name, "cannot be any of the following names: any, none, series, collection")
|
||||||
when /,/
|
when /,/
|
||||||
errors[:name] << "cannot contain commas"
|
errors.add(:name, "cannot contain commas")
|
||||||
when /\*/
|
when /\*/
|
||||||
errors[:name] << "cannot contain asterisks"
|
errors.add(:name, "cannot contain asterisks")
|
||||||
when /\A_/
|
when /\A_/
|
||||||
errors[:name] << "cannot begin with an underscore"
|
errors.add(:name, "cannot begin with an underscore")
|
||||||
when /_\z/
|
when /_\z/
|
||||||
errors[:name] << "cannot end with an underscore"
|
errors.add(:name, "cannot end with an underscore")
|
||||||
when /__/
|
when /__/
|
||||||
errors[:name] << "cannot contain consecutive underscores"
|
errors.add(:name, "cannot contain consecutive underscores")
|
||||||
when /[^[:graph:]]/
|
when /[^[:graph:]]/
|
||||||
errors[:name] << "cannot contain non-printable characters"
|
errors.add(:name, "cannot contain non-printable characters")
|
||||||
when ""
|
when ""
|
||||||
errors[:name] << "cannot be blank"
|
errors.add(:name, "cannot be blank")
|
||||||
when /\A[0-9]+\z/
|
when /\A[0-9]+\z/
|
||||||
errors[:name] << "cannot contain only digits"
|
errors.add(:name, "cannot contain only digits")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ class PoolVersion < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def search(params)
|
def search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :pool_id, :post_ids, :added_post_ids, :removed_post_ids, :updater_id, :description, :description_changed, :name, :name_changed, :version, :is_active, :is_deleted, :category)
|
||||||
q = q.search_attributes(params, :pool_id, :post_ids, :added_post_ids, :removed_post_ids, :updater_id, :description, :description_changed, :name, :name_changed, :version, :is_active, :is_deleted, :category)
|
|
||||||
|
|
||||||
if params[:post_id]
|
if params[:post_id]
|
||||||
q = q.for_post_id(params[:post_id].to_i)
|
q = q.for_post_id(params[:post_id].to_i)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class Post < ApplicationRecord
|
|||||||
before_validation :remove_parent_loops
|
before_validation :remove_parent_loops
|
||||||
validates_uniqueness_of :md5, :on => :create, message: ->(obj, data) { "duplicate: #{Post.find_by_md5(obj.md5).id}"}
|
validates_uniqueness_of :md5, :on => :create, message: ->(obj, data) { "duplicate: #{Post.find_by_md5(obj.md5).id}"}
|
||||||
validates_inclusion_of :rating, in: %w(s q e), message: "rating must be s, q, or e"
|
validates_inclusion_of :rating, in: %w(s q e), message: "rating must be s, q, or e"
|
||||||
|
validates :source, length: { maximum: 1200 }
|
||||||
validate :added_tags_are_valid
|
validate :added_tags_are_valid
|
||||||
validate :removed_tags_are_valid
|
validate :removed_tags_are_valid
|
||||||
validate :has_artist_tag
|
validate :has_artist_tag
|
||||||
@@ -55,6 +56,7 @@ class Post < ApplicationRecord
|
|||||||
has_many :approvals, :class_name => "PostApproval", :dependent => :destroy
|
has_many :approvals, :class_name => "PostApproval", :dependent => :destroy
|
||||||
has_many :disapprovals, :class_name => "PostDisapproval", :dependent => :destroy
|
has_many :disapprovals, :class_name => "PostDisapproval", :dependent => :destroy
|
||||||
has_many :favorites
|
has_many :favorites
|
||||||
|
has_many :favorited_users, through: :favorites, source: :user
|
||||||
has_many :replacements, class_name: "PostReplacement", :dependent => :destroy
|
has_many :replacements, class_name: "PostReplacement", :dependent => :destroy
|
||||||
|
|
||||||
attr_accessor :old_tag_string, :old_parent_id, :old_source, :old_rating, :has_constraints, :disable_versioning, :view_count
|
attr_accessor :old_tag_string, :old_parent_id, :old_source, :old_rating, :has_constraints, :disable_versioning, :view_count
|
||||||
@@ -490,7 +492,7 @@ class Post < ApplicationRecord
|
|||||||
|
|
||||||
invalid_tags.each do |tag|
|
invalid_tags.each do |tag|
|
||||||
tag.errors.messages.each do |attribute, messages|
|
tag.errors.messages.each do |attribute, messages|
|
||||||
warnings[:base] << "Couldn't add tag: #{messages.join(';')}"
|
warnings.add(:base, "Couldn't add tag: #{messages.join(';')}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -760,12 +762,11 @@ class Post < ApplicationRecord
|
|||||||
update_column(:fav_count, fav_count)
|
update_column(:fav_count, fav_count)
|
||||||
end
|
end
|
||||||
|
|
||||||
def favorited_by?(user_id = CurrentUser.id)
|
def favorited_by?(user)
|
||||||
fav_string.match?(/(?:\A| )fav:#{user_id}(?:\Z| )/)
|
return false if user.is_anonymous?
|
||||||
|
Favorite.exists?(post: self, user: user)
|
||||||
end
|
end
|
||||||
|
|
||||||
alias is_favorited? favorited_by?
|
|
||||||
|
|
||||||
def append_user_to_fav_string(user_id)
|
def append_user_to_fav_string(user_id)
|
||||||
update_column(:fav_string, (fav_string + " fav:#{user_id}").strip)
|
update_column(:fav_string, (fav_string + " fav:#{user_id}").strip)
|
||||||
clean_fav_string! if clean_fav_string?
|
clean_fav_string! if clean_fav_string?
|
||||||
@@ -801,14 +802,11 @@ class Post < ApplicationRecord
|
|||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
# users who favorited this post, ordered by users who favorited it first
|
# Users who publicly favorited this post, ordered by time of favorite.
|
||||||
def favorited_users
|
def visible_favorited_users(viewer)
|
||||||
favorited_user_ids = fav_string.scan(/\d+/).map(&:to_i)
|
favorited_users.order("favorites.id DESC").select do |fav_user|
|
||||||
visible_users = User.find(favorited_user_ids).select do |user|
|
Pundit.policy!([viewer, nil], fav_user).can_see_favorites?
|
||||||
Pundit.policy!([CurrentUser.user, nil], user).can_see_favorites?
|
|
||||||
end
|
end
|
||||||
ordered_users = visible_users.index_by(&:id).slice(*favorited_user_ids).values
|
|
||||||
ordered_users
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def favorite_groups
|
def favorite_groups
|
||||||
@@ -976,7 +974,7 @@ class Post < ApplicationRecord
|
|||||||
module DeletionMethods
|
module DeletionMethods
|
||||||
def expunge!
|
def expunge!
|
||||||
if is_status_locked?
|
if is_status_locked?
|
||||||
self.errors.add(:is_status_locked, "; cannot delete post")
|
errors.add(:is_status_locked, "; cannot delete post")
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1082,11 +1080,11 @@ class Post < ApplicationRecord
|
|||||||
def copy_notes_to(other_post, copy_tags: NOTE_COPY_TAGS)
|
def copy_notes_to(other_post, copy_tags: NOTE_COPY_TAGS)
|
||||||
transaction do
|
transaction do
|
||||||
if id == other_post.id
|
if id == other_post.id
|
||||||
errors.add :base, "Source and destination posts are the same"
|
errors.add(:base, "Source and destination posts are the same")
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
unless has_notes?
|
unless has_notes?
|
||||||
errors.add :post, "has no notes"
|
errors.add(:post, "has no notes")
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1289,14 +1287,17 @@ class Post < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def search(params)
|
def search(params)
|
||||||
q = super
|
q = search_attributes(
|
||||||
|
|
||||||
q = q.search_attributes(
|
|
||||||
params,
|
params,
|
||||||
:rating, :source, :pixiv_id, :fav_count, :score, :up_score, :down_score, :md5, :file_ext,
|
:id, :created_at, :updated_at, :rating, :source, :pixiv_id, :fav_count,
|
||||||
:file_size, :image_width, :image_height, :tag_count, :has_children, :has_active_children,
|
:score, :up_score, :down_score, :md5, :file_ext, :file_size, :image_width,
|
||||||
:is_note_locked, :is_rating_locked, :is_status_locked, :is_pending, :is_flagged, :is_deleted,
|
:image_height, :tag_count, :has_children, :has_active_children,
|
||||||
:is_banned, :last_comment_bumped_at, :last_commented_at, :last_noted_at
|
:is_note_locked, :is_rating_locked, :is_status_locked, :is_pending,
|
||||||
|
:is_flagged, :is_deleted, :is_banned, :last_comment_bumped_at,
|
||||||
|
:last_commented_at, :last_noted_at, :uploader_ip_addr,
|
||||||
|
:uploader, :updater, :approver, :parent, :upload, :artist_commentary,
|
||||||
|
:flags, :appeals, :notes, :comments, :children, :approvals,
|
||||||
|
:replacements, :pixiv_ugoira_frame_data
|
||||||
)
|
)
|
||||||
|
|
||||||
if params[:tags].present?
|
if params[:tags].present?
|
||||||
@@ -1357,8 +1358,7 @@ class Post < ApplicationRecord
|
|||||||
module ValidationMethods
|
module ValidationMethods
|
||||||
def post_is_not_its_own_parent
|
def post_is_not_its_own_parent
|
||||||
if !new_record? && id == parent_id
|
if !new_record? && id == parent_id
|
||||||
errors[:base] << "Post cannot have itself as a parent"
|
errors.add(:base, "Post cannot have itself as a parent")
|
||||||
false
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1372,30 +1372,23 @@ class Post < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def uploader_is_not_limited
|
def uploader_is_not_limited
|
||||||
errors[:uploader] << uploader.upload_limit.limit_reason if uploader.upload_limit.limited?
|
errors.add(:uploader, uploader.upload_limit.limit_reason) if uploader.upload_limit.limited?
|
||||||
end
|
end
|
||||||
|
|
||||||
def added_tags_are_valid
|
def added_tags_are_valid
|
||||||
new_tags = added_tags.select(&:empty?)
|
new_tags = added_tags.select(&:empty?)
|
||||||
new_general_tags = new_tags.select(&:general?)
|
new_artist_tags, new_general_tags = new_tags.partition(&:artist?)
|
||||||
new_artist_tags = new_tags.select(&:artist?)
|
|
||||||
repopulated_tags = new_tags.select { |t| !t.general? && !t.meta? && (t.created_at < 1.hour.ago) }
|
|
||||||
|
|
||||||
if new_general_tags.present?
|
if new_general_tags.present?
|
||||||
n = new_general_tags.size
|
n = new_general_tags.size
|
||||||
tag_wiki_links = new_general_tags.map { |tag| "[[#{tag.name}]]" }
|
tag_wiki_links = new_general_tags.map { |tag| "[[#{tag.name}]]" }
|
||||||
self.warnings[:base] << "Created #{n} new #{(n == 1) ? "tag" : "tags"}: #{tag_wiki_links.join(", ")}"
|
warnings.add(:base, "Created #{n} new #{(n == 1) ? "tag" : "tags"}: #{tag_wiki_links.join(", ")}")
|
||||||
end
|
|
||||||
|
|
||||||
if repopulated_tags.present?
|
|
||||||
n = repopulated_tags.size
|
|
||||||
tag_wiki_links = repopulated_tags.map { |tag| "[[#{tag.name}]]" }
|
|
||||||
self.warnings[:base] << "Repopulated #{n} old #{(n == 1) ? "tag" : "tags"}: #{tag_wiki_links.join(", ")}"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
new_artist_tags.each do |tag|
|
new_artist_tags.each do |tag|
|
||||||
if tag.artist.blank?
|
if tag.artist.blank?
|
||||||
self.warnings[:base] << "Artist [[#{tag.name}]] requires an artist entry. \"Create new artist entry\":[/artists/new?artist%5Bname%5D=#{CGI.escape(tag.name)}]"
|
new_artist_path = Routes.new_artist_path(artist: { name: tag.name })
|
||||||
|
warnings.add(:base, "Artist [[#{tag.name}]] requires an artist entry. \"Create new artist entry\":[#{new_artist_path}]")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1406,7 +1399,7 @@ class Post < ApplicationRecord
|
|||||||
|
|
||||||
if unremoved_tags.present?
|
if unremoved_tags.present?
|
||||||
unremoved_tags_list = unremoved_tags.map { |t| "[[#{t}]]" }.to_sentence
|
unremoved_tags_list = unremoved_tags.map { |t| "[[#{t}]]" }.to_sentence
|
||||||
self.warnings[:base] << "#{unremoved_tags_list} could not be removed. Check for implications and try again"
|
warnings.add(:base, "#{unremoved_tags_list} could not be removed. Check for implications and try again")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1417,21 +1410,22 @@ class Post < ApplicationRecord
|
|||||||
return if tags.any?(&:artist?)
|
return if tags.any?(&:artist?)
|
||||||
return if Sources::Strategies.find(source).is_a?(Sources::Strategies::Null)
|
return if Sources::Strategies.find(source).is_a?(Sources::Strategies::Null)
|
||||||
|
|
||||||
self.warnings[:base] << "Artist tag is required. \"Create new artist tag\":[/artists/new?artist%5Bsource%5D=#{CGI.escape(source)}]. Ask on the forum if you need naming help"
|
new_artist_path = Routes.new_artist_path(artist: { source: source })
|
||||||
|
warnings.add(:base, "Artist tag is required. \"Create new artist tag\":[#{new_artist_path}]. Ask on the forum if you need naming help")
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_copyright_tag
|
def has_copyright_tag
|
||||||
return if !new_record?
|
return if !new_record?
|
||||||
return if has_tag?("copyright_request") || tags.any?(&:copyright?)
|
return if has_tag?("copyright_request") || tags.any?(&:copyright?)
|
||||||
|
|
||||||
self.warnings[:base] << "Copyright tag is required. Consider adding [[copyright request]] or [[original]]"
|
warnings.add(:base, "Copyright tag is required. Consider adding [[copyright request]] or [[original]]")
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_enough_tags
|
def has_enough_tags
|
||||||
return if !new_record?
|
return if !new_record?
|
||||||
|
|
||||||
if tags.count(&:general?) < 10
|
if tags.count(&:general?) < 10
|
||||||
self.warnings[:base] << "Uploads must have at least 10 general tags. Read [[howto:tag]] for guidelines on tagging your uploads"
|
warnings.add(:base, "Uploads must have at least 10 general tags. Read [[howto:tag]] for guidelines on tagging your uploads")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1508,10 +1502,6 @@ class Post < ApplicationRecord
|
|||||||
super.where(table[:is_pending].eq(false)).where(table[:is_flagged].eq(false)).where(table[:is_deleted].eq(false))
|
super.where(table[:is_pending].eq(false)).where(table[:is_flagged].eq(false)).where(table[:is_deleted].eq(false))
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:uploader, :updater, :approver, :parent, :upload, :artist_commentary, :flags, :appeals, :notes, :comments, :children, :approvals, :replacements, :pixiv_ugoira_frame_data]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:uploader, :updater, :approver, :parent, :upload, :artist_commentary, :flags, :appeals, :notes, :comments, :children, :approvals, :replacements, :pixiv_ugoira_frame_data]
|
[:uploader, :updater, :approver, :parent, :upload, :artist_commentary, :flags, :appeals, :notes, :comments, :children, :approvals, :replacements, :pixiv_ugoira_frame_data]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ class PostAppeal < ApplicationRecord
|
|||||||
|
|
||||||
module SearchMethods
|
module SearchMethods
|
||||||
def search(params)
|
def search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :reason, :status, :creator, :post)
|
||||||
q = q.search_attributes(params, :reason, :status)
|
|
||||||
q = q.text_attribute_matches(:reason, params[:reason_matches])
|
q = q.text_attribute_matches(:reason, params[:reason_matches])
|
||||||
|
|
||||||
q.apply_default_order(params)
|
q.apply_default_order(params)
|
||||||
@@ -28,15 +27,11 @@ class PostAppeal < ApplicationRecord
|
|||||||
extend SearchMethods
|
extend SearchMethods
|
||||||
|
|
||||||
def validate_creator_is_not_limited
|
def validate_creator_is_not_limited
|
||||||
errors[:creator] << "have reached your appeal limit" if creator.is_appeal_limited?
|
errors.add(:creator, "have reached your appeal limit") if creator.is_appeal_limited?
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_post_is_appealable
|
def validate_post_is_appealable
|
||||||
errors[:post] << "cannot be appealed" if post.is_status_locked? || !post.is_appealable?
|
errors.add(:post, "cannot be appealed") if post.is_status_locked? || !post.is_appealable?
|
||||||
end
|
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:creator, :post]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
|
|||||||
@@ -38,14 +38,10 @@ class PostApproval < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.search(params)
|
def self.search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :user, :post)
|
||||||
q.apply_default_order(params)
|
q.apply_default_order(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:user, :post]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:user, :post]
|
[:user, :post]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ class PostDisapproval < ApplicationRecord
|
|||||||
concerning :SearchMethods do
|
concerning :SearchMethods do
|
||||||
class_methods do
|
class_methods do
|
||||||
def search(params)
|
def search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :message, :reason, :user, :post)
|
||||||
|
|
||||||
q = q.search_attributes(params, :message, :reason)
|
|
||||||
q = q.text_attribute_matches(:message, params[:message_matches])
|
q = q.text_attribute_matches(:message, params[:message_matches])
|
||||||
|
|
||||||
q = q.with_message if params[:has_message].to_s.truthy?
|
q = q.with_message if params[:has_message].to_s.truthy?
|
||||||
@@ -41,17 +39,13 @@ class PostDisapproval < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:user, :post]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:user, :post]
|
[:user, :post]
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_disapproval
|
def validate_disapproval
|
||||||
if post.is_active?
|
if post.is_active?
|
||||||
errors[:post] << "is already active and cannot be disapproved"
|
errors.add(:post, "is already active and cannot be disapproved")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -56,9 +56,7 @@ class PostFlag < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def search(params)
|
def search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :reason, :status, :post)
|
||||||
|
|
||||||
q = q.search_attributes(params, :reason, :status)
|
|
||||||
q = q.text_attribute_matches(:reason, params[:reason_matches])
|
q = q.text_attribute_matches(:reason, params[:reason_matches])
|
||||||
|
|
||||||
if params[:creator_id].present?
|
if params[:creator_id].present?
|
||||||
@@ -95,17 +93,17 @@ class PostFlag < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def validate_creator_is_not_limited
|
def validate_creator_is_not_limited
|
||||||
errors[:creator] << "have reached your flag limit" if creator.is_flag_limited? && !is_deletion
|
errors.add(:creator, "have reached your flag limit") if creator.is_flag_limited? && !is_deletion
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_post
|
def validate_post
|
||||||
errors[:post] << "is pending and cannot be flagged" if post.is_pending? && !is_deletion
|
errors.add(:post, "is pending and cannot be flagged") if post.is_pending? && !is_deletion
|
||||||
errors[:post] << "is deleted and cannot be flagged" if post.is_deleted? && !is_deletion
|
errors.add(:post, "is deleted and cannot be flagged") if post.is_deleted? && !is_deletion
|
||||||
errors[:post] << "is locked and cannot be flagged" if post.is_status_locked?
|
errors.add(:post, "is locked and cannot be flagged") if post.is_status_locked?
|
||||||
|
|
||||||
flag = post.flags.in_cooldown.last
|
flag = post.flags.in_cooldown.last
|
||||||
if !is_deletion && flag.present?
|
if !is_deletion && flag.present?
|
||||||
errors[:post] << "cannot be flagged more than once every #{Danbooru.config.moderation_period.inspect} (last flagged: #{flag.created_at.to_s(:long)})"
|
errors.add(:post, "cannot be flagged more than once every #{Danbooru.config.moderation_period.inspect} (last flagged: #{flag.created_at.to_s(:long)})")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -113,10 +111,6 @@ class PostFlag < ApplicationRecord
|
|||||||
post.uploader_id
|
post.uploader_id
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:post]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:post]
|
[:post]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,18 +11,17 @@ class PostReplacement < ApplicationRecord
|
|||||||
self.original_url = post.source
|
self.original_url = post.source
|
||||||
self.tags = post.tag_string + " " + self.tags.to_s
|
self.tags = post.tag_string + " " + self.tags.to_s
|
||||||
|
|
||||||
self.file_ext_was = post.file_ext
|
self.old_file_ext = post.file_ext
|
||||||
self.file_size_was = post.file_size
|
self.old_file_size = post.file_size
|
||||||
self.image_width_was = post.image_width
|
self.old_image_width = post.image_width
|
||||||
self.image_height_was = post.image_height
|
self.old_image_height = post.image_height
|
||||||
self.md5_was = post.md5
|
self.old_md5 = post.md5
|
||||||
end
|
end
|
||||||
|
|
||||||
concerning :Search do
|
concerning :Search do
|
||||||
class_methods do
|
class_methods do
|
||||||
def search(params = {})
|
def search(params = {})
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :md5, :old_md5, :file_ext, :old_file_ext, :original_url, :replacement_url, :creator, :post)
|
||||||
q = q.search_attributes(params, :md5, :md5_was, :file_ext, :file_ext_was, :original_url, :replacement_url)
|
|
||||||
q.apply_default_order(params)
|
q.apply_default_order(params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -39,10 +38,6 @@ class PostReplacement < ApplicationRecord
|
|||||||
tags.join(" ")
|
tags.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:creator, :post]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:creator, :post]
|
[:creator, :post]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -38,8 +38,7 @@ class PostVersion < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def search(params)
|
def search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :updated_at, :updater_id, :post_id, :tags, :added_tags, :removed_tags, :rating, :rating_changed, :parent_id, :parent_changed, :source, :source_changed, :version)
|
||||||
q = q.search_attributes(params, :updater_id, :post_id, :tags, :added_tags, :removed_tags, :rating, :rating_changed, :parent_id, :parent_changed, :source, :source_changed, :version)
|
|
||||||
|
|
||||||
if params[:changed_tags]
|
if params[:changed_tags]
|
||||||
q = q.changed_tags_include_all(params[:changed_tags].scan(/[^[:space:]]+/))
|
q = q.changed_tags_include_all(params[:changed_tags].scan(/[^[:space:]]+/))
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ class PostVote < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.search(params)
|
def self.search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :score, :user, :post)
|
||||||
q = q.search_attributes(params, :score)
|
|
||||||
q.apply_default_order(params)
|
q.apply_default_order(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -50,10 +49,6 @@ class PostVote < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
|
||||||
[:user, :post]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
[:user, :post]
|
[:user, :post]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -63,17 +63,12 @@ class SavedSearch < ApplicationRecord
|
|||||||
.gsub(/[[:space:]]/, "_")
|
.gsub(/[[:space:]]/, "_")
|
||||||
end
|
end
|
||||||
|
|
||||||
def search_labels(user_id, params)
|
def all_labels
|
||||||
labels = labels_for(user_id)
|
select(Arel.sql("distinct unnest(labels) as label")).order(:label)
|
||||||
|
end
|
||||||
|
|
||||||
if params[:label].present?
|
def labels_like(label)
|
||||||
query = Regexp.escape(params[:label]).gsub("\\*", ".*")
|
all_labels.select { |ss| ss.label.ilike?(label) }.map(&:label)
|
||||||
query = ".*#{query}.*" unless query.include?("*")
|
|
||||||
query = /\A#{query}\z/
|
|
||||||
labels = labels.grep(query)
|
|
||||||
end
|
|
||||||
|
|
||||||
labels
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def labels_for(user_id)
|
def labels_for(user_id)
|
||||||
@@ -105,8 +100,7 @@ class SavedSearch < ApplicationRecord
|
|||||||
concerning :Search do
|
concerning :Search do
|
||||||
class_methods do
|
class_methods do
|
||||||
def search(params)
|
def search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :query)
|
||||||
q = q.search_attributes(params, :query)
|
|
||||||
|
|
||||||
if params[:label]
|
if params[:label]
|
||||||
q = q.labeled(params[:label])
|
q = q.labeled(params[:label])
|
||||||
@@ -174,7 +168,7 @@ class SavedSearch < ApplicationRecord
|
|||||||
|
|
||||||
def validate_count
|
def validate_count
|
||||||
if user.saved_searches.count >= user.max_saved_searches
|
if user.saved_searches.count >= user.max_saved_searches
|
||||||
self.errors[:user] << "can only have up to #{user.max_saved_searches} " + "saved search".pluralize(user.max_saved_searches)
|
errors.add(:user, "can only have up to #{user.max_saved_searches} " + "saved search".pluralize(user.max_saved_searches))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
class Tag < ApplicationRecord
|
class Tag < ApplicationRecord
|
||||||
|
ABBREVIATION_REGEXP = /([a-z0-9])[a-z0-9']*($|[^a-z0-9']+)/
|
||||||
|
|
||||||
has_one :wiki_page, :foreign_key => "title", :primary_key => "name"
|
has_one :wiki_page, :foreign_key => "title", :primary_key => "name"
|
||||||
has_one :artist, :foreign_key => "name", :primary_key => "name"
|
has_one :artist, :foreign_key => "name", :primary_key => "name"
|
||||||
has_one :antecedent_alias, -> {active}, :class_name => "TagAlias", :foreign_key => "antecedent_name", :primary_key => "name"
|
has_one :antecedent_alias, -> {active}, :class_name => "TagAlias", :foreign_key => "antecedent_name", :primary_key => "name"
|
||||||
@@ -125,18 +127,6 @@ class Tag < ApplicationRecord
|
|||||||
Tag.where(name: tag_name).pick(:category).to_i
|
Tag.where(name: tag_name).pick(:category).to_i
|
||||||
end
|
end
|
||||||
|
|
||||||
def category_for(tag_name, options = {})
|
|
||||||
return Tag.categories.general if tag_name.blank?
|
|
||||||
|
|
||||||
if options[:disable_caching]
|
|
||||||
select_category_for(tag_name)
|
|
||||||
else
|
|
||||||
Cache.get("tc:#{Cache.hash(tag_name)}") do
|
|
||||||
select_category_for(tag_name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def categories_for(tag_names, options = {})
|
def categories_for(tag_names, options = {})
|
||||||
if options[:disable_caching]
|
if options[:disable_caching]
|
||||||
Array(tag_names).inject({}) do |hash, tag_name|
|
Array(tag_names).inject({}) do |hash, tag_name|
|
||||||
@@ -232,16 +222,19 @@ class Tag < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
module SearchMethods
|
module SearchMethods
|
||||||
|
def autocorrect_matches(name)
|
||||||
|
tags = fuzzy_name_matches(name).order_similarity(name)
|
||||||
|
end
|
||||||
|
|
||||||
# ref: https://www.postgresql.org/docs/current/static/pgtrgm.html#idm46428634524336
|
# ref: https://www.postgresql.org/docs/current/static/pgtrgm.html#idm46428634524336
|
||||||
def order_similarity(name)
|
def order_similarity(name)
|
||||||
# trunc(3 * sim) reduces the similarity score from a range of 0.0 -> 1.0 to just 0, 1, or 2.
|
order(Arel.sql("levenshtein(left(name, 255), #{connection.quote(name)}), tags.post_count DESC, tags.name ASC"))
|
||||||
# This groups tags first by approximate similarity, then by largest tags within groups of similar tags.
|
|
||||||
order(Arel.sql("trunc(3 * similarity(name, #{connection.quote(name)})) DESC"), "post_count DESC", "name DESC")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# ref: https://www.postgresql.org/docs/current/static/pgtrgm.html#idm46428634524336
|
# ref: https://www.postgresql.org/docs/current/static/pgtrgm.html#idm46428634524336
|
||||||
def fuzzy_name_matches(name)
|
def fuzzy_name_matches(name)
|
||||||
where("tags.name % ?", name)
|
max_distance = [name.size / 4, 3].max.floor.to_i
|
||||||
|
where("tags.name % ?", name).where("levenshtein(left(name, 255), ?) < ?", name, max_distance)
|
||||||
end
|
end
|
||||||
|
|
||||||
def name_matches(name)
|
def name_matches(name)
|
||||||
@@ -249,21 +242,29 @@ class Tag < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def alias_matches(name)
|
def alias_matches(name)
|
||||||
where(name: TagAlias.active.where_ilike(:antecedent_name, normalize_name(name)).select(:consequent_name))
|
where(name: TagAlias.active.where_like(:antecedent_name, normalize_name(name)).select(:consequent_name))
|
||||||
end
|
end
|
||||||
|
|
||||||
def name_or_alias_matches(name)
|
def name_or_alias_matches(name)
|
||||||
name_matches(name).or(alias_matches(name))
|
name_matches(name).or(alias_matches(name))
|
||||||
end
|
end
|
||||||
|
|
||||||
def wildcard_matches(tag, limit: 25)
|
def wildcard_matches(tag)
|
||||||
nonempty.name_matches(tag).order(post_count: :desc, name: :asc).limit(limit).pluck(:name)
|
nonempty.name_matches(tag).order(post_count: :desc, name: :asc)
|
||||||
|
end
|
||||||
|
|
||||||
|
def abbreviation_matches(abbrev)
|
||||||
|
abbrev = abbrev.delete_prefix("/")
|
||||||
|
where("regexp_replace(tags.name, ?, '\\1', 'g') LIKE ?", ABBREVIATION_REGEXP.source, abbrev.to_escaped_for_sql_like)
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_by_abbreviation(abbrev)
|
||||||
|
abbrev = abbrev.delete_prefix("/")
|
||||||
|
abbreviation_matches(abbrev.escape_wildcards).order(post_count: :desc).first
|
||||||
end
|
end
|
||||||
|
|
||||||
def search(params)
|
def search(params)
|
||||||
q = super
|
q = search_attributes(params, :id, :created_at, :updated_at, :is_locked, :category, :post_count, :name, :wiki_page, :artist, :antecedent_alias, :consequent_aliases, :antecedent_implications, :consequent_implications, :dtext_links)
|
||||||
|
|
||||||
q = q.search_attributes(params, :is_locked, :category, :post_count, :name)
|
|
||||||
|
|
||||||
if params[:fuzzy_name_matches].present?
|
if params[:fuzzy_name_matches].present?
|
||||||
q = q.fuzzy_name_matches(params[:fuzzy_name_matches])
|
q = q.fuzzy_name_matches(params[:fuzzy_name_matches])
|
||||||
@@ -352,12 +353,20 @@ class Tag < ApplicationRecord
|
|||||||
Post.system_tag_match(name)
|
Post.system_tag_match(name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.model_restriction(table)
|
def abbreviation
|
||||||
super.where(table[:post_count].gt(0))
|
name.gsub(ABBREVIATION_REGEXP, "\\1")
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchable_includes
|
def tag_alias_for_pattern(pattern)
|
||||||
[:wiki_page, :artist, :antecedent_alias, :consequent_aliases, :antecedent_implications, :consequent_implications, :dtext_links]
|
return nil if pattern.blank?
|
||||||
|
|
||||||
|
consequent_aliases.find do |tag_alias|
|
||||||
|
!name.ilike?(pattern) && tag_alias.antecedent_name.ilike?(pattern)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.model_restriction(table)
|
||||||
|
super.where(table[:post_count].gt(0))
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.available_includes
|
def self.available_includes
|
||||||
|
|||||||
@@ -7,7 +7,15 @@ class TagAlias < TagRelationship
|
|||||||
def self.to_aliased(names)
|
def self.to_aliased(names)
|
||||||
names = Array(names).map(&:to_s)
|
names = Array(names).map(&:to_s)
|
||||||
return [] if names.empty?
|
return [] if names.empty?
|
||||||
|
|
||||||
aliases = active.where(antecedent_name: names).map { |ta| [ta.antecedent_name, ta.consequent_name] }.to_h
|
aliases = active.where(antecedent_name: names).map { |ta| [ta.antecedent_name, ta.consequent_name] }.to_h
|
||||||
|
|
||||||
|
abbreviations = names.select { |name| name.starts_with?("/") && !aliases.has_key?(name) }
|
||||||
|
abbreviations.each do |abbrev|
|
||||||
|
tag = Tag.nonempty.find_by_abbreviation(abbrev)
|
||||||
|
aliases[abbrev] = tag.name if tag.present?
|
||||||
|
end
|
||||||
|
|
||||||
names.map { |name| aliases[name] || name }
|
names.map { |name| aliases[name] || name }
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -23,7 +31,7 @@ class TagAlias < TagRelationship
|
|||||||
|
|
||||||
tag_alias = TagAlias.active.find_by(antecedent_name: consequent_name)
|
tag_alias = TagAlias.active.find_by(antecedent_name: consequent_name)
|
||||||
if tag_alias.present? && tag_alias.consequent_name != antecedent_name
|
if tag_alias.present? && tag_alias.consequent_name != antecedent_name
|
||||||
errors[:base] << "#{tag_alias.antecedent_name} is already aliased to #{tag_alias.consequent_name}"
|
errors.add(:base, "#{tag_alias.antecedent_name} is already aliased to #{tag_alias.consequent_name}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user