yiffspace-core 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +46 -0
- data/LICENSE +20 -0
- data/README.md +27 -0
- data/Rakefile +16 -0
- data/app/assets/config/yiffspace_manifest.js +1 -0
- data/app/assets/stylesheets/yiffspace/application.css +30 -0
- data/app/controllers/yiff_space/application_controller.rb +6 -0
- data/app/helpers/yiff_space/application_helper.rb +6 -0
- data/app/models/yiff_space/application_record.rb +7 -0
- data/app/views/layouts/yiff_space/application.html.erb +17 -0
- data/app/views/yiff_space/error.html.erb +3 -0
- data/lib/yiffspace/concerns/active_record_extensions.rb +45 -0
- data/lib/yiffspace/concerns/api_methods.rb +101 -0
- data/lib/yiffspace/concerns/attribute_matchers.rb +100 -0
- data/lib/yiffspace/concerns/attribute_methods.rb +39 -0
- data/lib/yiffspace/concerns/concurrency_methods.rb +20 -0
- data/lib/yiffspace/concerns/conditional_includes.rb +43 -0
- data/lib/yiffspace/concerns/has_bit_flags.rb +59 -0
- data/lib/yiffspace/configuration.rb +43 -0
- data/lib/yiffspace/core/version.rb +7 -0
- data/lib/yiffspace/core.rb +30 -0
- data/lib/yiffspace/engine.rb +13 -0
- data/lib/yiffspace/utils/cache.rb +40 -0
- data/lib/yiffspace/utils/duration_parser.rb +24 -0
- data/lib/yiffspace/utils/helpers.rb +25 -0
- data/lib/yiffspace/utils/open_hash.rb +63 -0
- data/lib/yiffspace/utils/parameter_builder.rb +121 -0
- data/lib/yiffspace/utils/parse_value.rb +174 -0
- data/lib/yiffspace/utils/routes.rb +28 -0
- data/lib/yiffspace/utils/set_env_constraint.rb +18 -0
- data/lib/yiffspace/utils/trace_logger.rb +91 -0
- data/lib/yiffspace/utils.rb +6 -0
- metadata +135 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 616274421a5c76dd24f3280528dba48846a7d8ffdbf2b6b77a8a9bc6ba243f5e
|
|
4
|
+
data.tar.gz: 49ef59c885784dc479dcc475a4bf55e24bf208a53e9a4288354a68072b2cfd1b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ae0ca898066aeb4781fc565f168189a42601476610b234bdf3fb3431a97dc275e2163a19db67fff2b10718b549bead50a715bcd721635f461d11028ae8b931d4
|
|
7
|
+
data.tar.gz: 7b358ed2a0c766ded8b197596eb768bcdfdc062d1f523f19bb271eda4259f484ce2aeaa7c48192600b1c11d44b91136772eae108a3a9ca594bb445305d475b70
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
## Unreleased
|
|
2
|
+
|
|
3
|
+
- **Breaking:** renamed the gem from `yiffspace` to `yiffspace-core`, and split
|
|
4
|
+
it further into standalone gems for complete modularity:
|
|
5
|
+
[`yiffspace-config`](https://github.com/YiffSpace/Gem/tree/master/yiffspace-config)
|
|
6
|
+
(`YiffSpace::ConfigBuilder`),
|
|
7
|
+
[`yiffspace-ext`](https://github.com/YiffSpace/Gem/tree/master/yiffspace-ext)
|
|
8
|
+
(generic Object/Hash/Enumerable/String core extensions + `WhereChain`),
|
|
9
|
+
[`yiffspace-arel`](https://github.com/YiffSpace/Gem/tree/master/yiffspace-arel)
|
|
10
|
+
(Postgres lateral-join/unnest extensions),
|
|
11
|
+
[`yiffspace-images`](https://github.com/YiffSpace/Gem/tree/master/yiffspace-images)
|
|
12
|
+
(avatar/banner URLs),
|
|
13
|
+
[`yiffspace-tables`](https://github.com/YiffSpace/Gem/tree/master/yiffspace-tables)
|
|
14
|
+
(`YiffSpace::Utils::TableBuilder`),
|
|
15
|
+
[`yiffspace-search`](https://github.com/YiffSpace/Gem/tree/master/yiffspace-search)
|
|
16
|
+
(`YiffSpace::Search::*`),
|
|
17
|
+
[`yiffspace-user`](https://github.com/YiffSpace/Gem/tree/master/yiffspace-user)
|
|
18
|
+
(the User/Current/UserResolvable concerns), and
|
|
19
|
+
[`yiffspace-include`](https://github.com/YiffSpace/Gem/tree/master/yiffspace-include)
|
|
20
|
+
(the deprecated global-alias layer). The `yiffspace` gem is now a husk that
|
|
21
|
+
just depends on all of the above, so `gem "yiffspace"` keeps working
|
|
22
|
+
unchanged for apps that want everything; apps that only need a subset can
|
|
23
|
+
depend on the specific gems instead.
|
|
24
|
+
- **Breaking:** extracted the one-time db/fixes system (`YiffSpace::FixTracker`,
|
|
25
|
+
`YiffSpace::FixerTemplate`, `YiffSpace::Configuration::FixerTemplates`, the
|
|
26
|
+
`yiffspace:fixer`/`yiffspace:install:fixes` generators, and the `fixes:*`
|
|
27
|
+
rake tasks) into a separate gem,
|
|
28
|
+
[`yiffspace-fixers`](https://github.com/YiffSpace/Gem/tree/master/yiffspace-fixers).
|
|
29
|
+
Apps using the fixes system need to add that gem alongside `yiffspace`.
|
|
30
|
+
- `YiffSpace::Configuration#fixer_templates`/`#fixer_templates_path` moved to
|
|
31
|
+
`yiffspace-fixers`.
|
|
32
|
+
- The dummy test app no longer needs Postgres - only the fixes table's
|
|
33
|
+
`nulls_not_distinct` index required it, and that lives in `yiffspace-fixers`
|
|
34
|
+
now. Back to plain sqlite3, `docker-compose.yml` removed.
|
|
35
|
+
|
|
36
|
+
## 0.1.0
|
|
37
|
+
|
|
38
|
+
- **Breaking:** extracted the Logto auth engine (`YiffSpace::Auth::*`) into a
|
|
39
|
+
separate gem, [`yiffspace-auth`](https://github.com/YiffSpace/Auth.rb). Apps
|
|
40
|
+
using auth need to add that gem alongside `yiffspace`.
|
|
41
|
+
- `YiffSpace::Configuration#auth`/`#add_auth`/`#add_default_auth` and the
|
|
42
|
+
`logto_api_*`/`discord_bot_token` settings moved to `yiffspace-auth`.
|
|
43
|
+
- Replaced the dead `YiffSpace::Railtie` with a real `YiffSpace::Engine <
|
|
44
|
+
Rails::Engine`, so the shared `app/` directory (ApplicationController,
|
|
45
|
+
layout, error view, CSS, ActiveJob serializers) actually loads into host
|
|
46
|
+
apps again.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright Donovan_DMC
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# YiffSpace::Core
|
|
2
|
+
|
|
3
|
+
Core Rails engine, `YiffSpace::Configuration`, and shared concerns/utils for
|
|
4
|
+
https://yiff.space and related projects. Everything else in the `yiffspace`
|
|
5
|
+
family builds on this gem.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "yiffspace-core"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
And then execute:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
$ bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Contributing
|
|
22
|
+
|
|
23
|
+
Go away
|
|
24
|
+
|
|
25
|
+
## License
|
|
26
|
+
|
|
27
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require("bundler/setup")
|
|
4
|
+
|
|
5
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
|
6
|
+
load("rails/tasks/engine.rake")
|
|
7
|
+
|
|
8
|
+
load("rails/tasks/statistics.rake")
|
|
9
|
+
|
|
10
|
+
require("bundler/gem_tasks")
|
|
11
|
+
|
|
12
|
+
# CI releases from an already-tagged, already-pushed commit checked out at a detached HEAD, so
|
|
13
|
+
# the default release task's git tag/push step has nothing to do and fails trying to push a
|
|
14
|
+
# branch ref that does not exist in that checkout.
|
|
15
|
+
Rake::Task["release"].clear
|
|
16
|
+
task("release" => "release:rubygem_push")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//= link_directory ../stylesheets/yiffspace .css
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
3
|
+
* listed below.
|
|
4
|
+
*
|
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
|
7
|
+
*
|
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
|
11
|
+
* It is generally better to create a new file per style scope.
|
|
12
|
+
*
|
|
13
|
+
*= require_tree .
|
|
14
|
+
*= require_self
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
body {
|
|
18
|
+
background-color: #2C2F33;
|
|
19
|
+
color: #FFFDD0;
|
|
20
|
+
font-size: 2rem;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
body div#page {
|
|
24
|
+
text-align: center;
|
|
25
|
+
|
|
26
|
+
ul.permissions-list {
|
|
27
|
+
display: inline-block;
|
|
28
|
+
text-align: left;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>YiffSpace</title>
|
|
5
|
+
<%= csrf_meta_tags %>
|
|
6
|
+
<%= csp_meta_tag %>
|
|
7
|
+
|
|
8
|
+
<%= stylesheet_link_tag("yiffspace/application", media: "all") %>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
|
|
12
|
+
<div id="page">
|
|
13
|
+
<%= yield %>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Concerns
|
|
5
|
+
module ActiveRecordExtensions
|
|
6
|
+
extend(ActiveSupport::Concern)
|
|
7
|
+
|
|
8
|
+
module ClassMethods
|
|
9
|
+
def without_timeout
|
|
10
|
+
original = statement_timeout
|
|
11
|
+
connection.execute("SET STATEMENT_TIMEOUT = 0") unless Rails.env.test?
|
|
12
|
+
yield
|
|
13
|
+
ensure
|
|
14
|
+
connection.execute("SET STATEMENT_TIMEOUT = #{original}") unless Rails.env.test?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def with_timeout(time, default_value = nil)
|
|
18
|
+
original = statement_timeout
|
|
19
|
+
connection.execute("SET STATEMENT_TIMEOUT = #{time}") unless Rails.env.test?
|
|
20
|
+
yield
|
|
21
|
+
rescue ::ActiveRecord::StatementInvalid => e
|
|
22
|
+
Rails.logger.warn("Statement timeout exceeded: #{e.message}")
|
|
23
|
+
default_value
|
|
24
|
+
ensure
|
|
25
|
+
connection.execute("SET STATEMENT_TIMEOUT = #{original}") unless Rails.env.test?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def statement_timeout
|
|
29
|
+
YiffSpace.config.application_record_class.connection.select_one("SELECT setting FROM pg_settings WHERE name = 'statement_timeout'")["setting"].to_i
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# CrossJoinLateral, LeftJoinLateral, nil
|
|
33
|
+
def unnest(column, name: column.singularize, type: ::Arel::Nodes::LeftJoinLateral)
|
|
34
|
+
function = ::Arel::Nodes::NamedFunction.new("unnest", [arel(column)], name)
|
|
35
|
+
return function if type.nil?
|
|
36
|
+
|
|
37
|
+
joins(type.new(
|
|
38
|
+
function,
|
|
39
|
+
::Arel::Nodes::On.new(Arel.sql("TRUE")),
|
|
40
|
+
))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Concerns
|
|
5
|
+
module ApiMethods
|
|
6
|
+
class NotVisibleError < StandardError; end
|
|
7
|
+
extend(ActiveSupport::Concern)
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
def available_includes
|
|
11
|
+
[]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def multiple_includes
|
|
15
|
+
reflections.select { |_, v| v.macro == :has_many }.keys.map(&:to_sym)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def associated_models(name)
|
|
19
|
+
if reflections[name].options[:polymorphic]
|
|
20
|
+
reflections[name].active_record.try(:model_types) || []
|
|
21
|
+
else
|
|
22
|
+
[reflections[name].class_name]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
delegate(:available_includes, to: :class)
|
|
28
|
+
|
|
29
|
+
# XXX deprecated, shouldn't expose this as an instance method.
|
|
30
|
+
def api_attributes(user)
|
|
31
|
+
policy = Pundit.policy(user, self) || ApplicationPolicy.new(user, self)
|
|
32
|
+
policy.api_attributes
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# XXX deprecated, shouldn't expose this as an instance method.
|
|
36
|
+
def html_data_attributes(user)
|
|
37
|
+
policy = Pundit.policy(user, self) || ApplicationPolicy.new(user, self)
|
|
38
|
+
policy.html_data_attributes
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def process_api_attributes(options, underscore: false)
|
|
42
|
+
options[:methods] ||= []
|
|
43
|
+
attributes, methods = api_attributes(options[:user]).partition { |attr| has_attribute?(attr) }
|
|
44
|
+
methods += options[:methods]
|
|
45
|
+
if underscore && options[:only].blank?
|
|
46
|
+
options[:only] = attributes + methods
|
|
47
|
+
else
|
|
48
|
+
options[:only] ||= attributes + methods
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
attributes &= options[:only]
|
|
52
|
+
methods &= options[:only]
|
|
53
|
+
|
|
54
|
+
options[:only] = attributes
|
|
55
|
+
options[:methods] = methods
|
|
56
|
+
|
|
57
|
+
options.delete(:methods) if options[:methods].empty?
|
|
58
|
+
options
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def serializable_hash(options = {})
|
|
62
|
+
options ||= {}
|
|
63
|
+
options[:user] ||= CurrentUser.user || User.anonymous
|
|
64
|
+
return :not_visible unless visible?(options[:user])
|
|
65
|
+
|
|
66
|
+
if options[:only].is_a?(String)
|
|
67
|
+
options.delete(:methods)
|
|
68
|
+
options.delete(:include)
|
|
69
|
+
options.merge!(ParameterBuilder.serial_parameters(options[:only], self, options))
|
|
70
|
+
if options[:only].include?("_")
|
|
71
|
+
options[:only].delete("_")
|
|
72
|
+
options = process_api_attributes(options, underscore: true)
|
|
73
|
+
end
|
|
74
|
+
else
|
|
75
|
+
options = process_api_attributes(options)
|
|
76
|
+
end
|
|
77
|
+
options[:only] += [SecureRandom.hex(6)]
|
|
78
|
+
|
|
79
|
+
hash = super
|
|
80
|
+
hash.transform_keys! { |key| key.delete("?").delete_prefix("apionly_") }
|
|
81
|
+
deep_reject_hash(hash) { |_, v| v == :not_visible }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def visible?(_user)
|
|
85
|
+
true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def deep_reject_hash(hash, &block)
|
|
89
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
90
|
+
if value.is_a?(Hash)
|
|
91
|
+
result[key] = deep_reject_hash(value, &block)
|
|
92
|
+
elsif value.is_a?(Array)
|
|
93
|
+
result[key] = value.map { |v| v.is_a?(Hash) ? deep_reject_hash(v, &block) : v }.reject { |i| block.call(nil, i) }
|
|
94
|
+
elsif !block.call(key, value)
|
|
95
|
+
result[key] = value
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Concerns
|
|
5
|
+
module AttributeMatchers
|
|
6
|
+
def boolean_attribute_matches(attribute, value, &)
|
|
7
|
+
return all if value.nil?
|
|
8
|
+
|
|
9
|
+
if value.to_s.truthy?
|
|
10
|
+
value = true
|
|
11
|
+
elsif value.to_s.falsy?
|
|
12
|
+
value = false
|
|
13
|
+
else
|
|
14
|
+
raise(ArgumentError, "value must be truthy or falsy")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
q = block_given? ? yield : all
|
|
18
|
+
q.where(attribute => value)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# range: "5", ">5", "<5", ">=5", "<=5", "5..10", "5,6,7"
|
|
22
|
+
def numeric_attribute_matches(attribute, value, &)
|
|
23
|
+
return all if value.nil?
|
|
24
|
+
|
|
25
|
+
value = value.to_s.strip
|
|
26
|
+
column = QueryHelper.get_column(attribute, table_name)
|
|
27
|
+
parsed_range = ParseValue.range(value, column.type)
|
|
28
|
+
|
|
29
|
+
add_range_relation(parsed_range, attribute, &)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# datetime columns return ActiveSupport::TimeWithZone
|
|
33
|
+
def datetime_attribute_matches(attribute, range, &)
|
|
34
|
+
numeric_attribute_matches(attribute, range, &)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def add_range_relation(arr, field, &)
|
|
38
|
+
return all if arr.nil?
|
|
39
|
+
|
|
40
|
+
q = block_given? ? yield : all
|
|
41
|
+
case arr[0]
|
|
42
|
+
when :eq
|
|
43
|
+
if arr[1].is_a?(Time)
|
|
44
|
+
q.where.between(field => arr[1].all_day)
|
|
45
|
+
else
|
|
46
|
+
q.where(field => arr[1])
|
|
47
|
+
end
|
|
48
|
+
when :gt
|
|
49
|
+
q.where.gt(field => arr[1])
|
|
50
|
+
when :gte
|
|
51
|
+
q.where.gte(field => arr[1])
|
|
52
|
+
when :lt
|
|
53
|
+
q.where.lt(field => arr[1])
|
|
54
|
+
when :lte
|
|
55
|
+
q.where.lte(field => arr[1])
|
|
56
|
+
when :in
|
|
57
|
+
q.where.in(field => arr[1])
|
|
58
|
+
when :between
|
|
59
|
+
q.where.between(field => arr[1]..arr[2])
|
|
60
|
+
else
|
|
61
|
+
q.none
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def text_attribute_matches(attribute, value, convert_to_wildcard: false, &)
|
|
66
|
+
return all if value.nil?
|
|
67
|
+
|
|
68
|
+
value = "*#{value}*" if convert_to_wildcard && value.exclude?("*")
|
|
69
|
+
q = block_given? ? yield : all
|
|
70
|
+
if value.is_a?(Array)
|
|
71
|
+
q.where.ilike_any(attribute => value)
|
|
72
|
+
elsif value =~ /\*/
|
|
73
|
+
q.where.ilike(attribute => value)
|
|
74
|
+
else
|
|
75
|
+
q.where.tsquery(attribute => value)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def ip_attribute_matches(attribute, value, &)
|
|
80
|
+
return all if value.nil?
|
|
81
|
+
|
|
82
|
+
q = block_given? ? yield : all
|
|
83
|
+
q.where.inet_contained_within_or_equals(attribute => value)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def attribute_present(attribute, value, &)
|
|
87
|
+
return all if value.nil?
|
|
88
|
+
|
|
89
|
+
q = block_given? ? yield : all
|
|
90
|
+
if value.to_s.truthy?
|
|
91
|
+
q.where.not(attribute => nil)
|
|
92
|
+
elsif value.to_s.falsy?
|
|
93
|
+
q.where(attribute => nil)
|
|
94
|
+
else
|
|
95
|
+
q.none
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Concerns
|
|
5
|
+
module AttributeMethods
|
|
6
|
+
extend(ActiveSupport::Concern)
|
|
7
|
+
|
|
8
|
+
module ClassMethods
|
|
9
|
+
# Defines `<attribute>_string`, `<attribute>_string=`, and `<attribute>=`
|
|
10
|
+
# methods for converting an array attribute to or from a string.
|
|
11
|
+
#
|
|
12
|
+
# The `<attribute>=` setter parses strings into an array using the
|
|
13
|
+
# `parse` regex. The resulting strings can be converted to another type
|
|
14
|
+
# with the `cast` option.
|
|
15
|
+
def array_attribute(name, parse: /[^[:space:]]+/, join_character: " ", cast: :itself)
|
|
16
|
+
define_method("#{name}_string") do
|
|
17
|
+
send(name).join(join_character)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
define_method("#{name}_string=") do |value|
|
|
21
|
+
raise(ArgumentError, "#{name} must be a String") unless value.respond_to?(:to_str)
|
|
22
|
+
|
|
23
|
+
send("#{name}=", value)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
define_method("#{name}=") do |value|
|
|
27
|
+
if value.respond_to?(:to_str)
|
|
28
|
+
super(value.to_str.scan(parse).flatten.map(&cast))
|
|
29
|
+
elsif value.respond_to?(:to_a)
|
|
30
|
+
super(value.to_a)
|
|
31
|
+
else
|
|
32
|
+
raise(ArgumentError, "#{name} must be a String or an Array")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Concerns
|
|
5
|
+
module ConcurrencyMethods
|
|
6
|
+
extend(ActiveSupport::Concern)
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def parallel_find_each(**, &)
|
|
10
|
+
# XXX We may deadlock if a transaction is open; do a non-parallel each.
|
|
11
|
+
return find_each(&) if connection.transaction_open?
|
|
12
|
+
|
|
13
|
+
find_in_batches(error_on_ignore: true, **) do |batch|
|
|
14
|
+
batch.parallel_each(&)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Concerns
|
|
5
|
+
module ConditionalIncludes
|
|
6
|
+
extend(ActiveSupport::Concern)
|
|
7
|
+
|
|
8
|
+
module ClassMethods
|
|
9
|
+
def html_includes(request, *)
|
|
10
|
+
includes_if(request.format.html?, *)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def includes_if(condition, *)
|
|
14
|
+
return all unless condition
|
|
15
|
+
|
|
16
|
+
includes(*)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def includes_unless(condition, *)
|
|
20
|
+
return all if condition
|
|
21
|
+
|
|
22
|
+
includes(*)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def html_preload(request, *)
|
|
26
|
+
preload_if(request.format.html?, *)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def preload_if(condition, *)
|
|
30
|
+
return all unless condition
|
|
31
|
+
|
|
32
|
+
preload(*)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def preload_unless(condition, *)
|
|
36
|
+
return all if condition
|
|
37
|
+
|
|
38
|
+
preload(*)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Concerns
|
|
5
|
+
module HasBitFlags
|
|
6
|
+
extend(ActiveSupport::Concern)
|
|
7
|
+
|
|
8
|
+
module ClassMethods
|
|
9
|
+
# NOTE: the ordering of attributes has to be fixed
|
|
10
|
+
# new attributes should be appended to the end.
|
|
11
|
+
def has_bit_flags(attributes, options = {})
|
|
12
|
+
field = options[:field] || :bit_flags
|
|
13
|
+
|
|
14
|
+
define_singleton_method("flag_value_for") do |key|
|
|
15
|
+
value = attributes[key.to_s]
|
|
16
|
+
return value if value
|
|
17
|
+
|
|
18
|
+
raise(ArgumentError, "Invalid flag: #{key}")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
attributes.each do |name, value|
|
|
22
|
+
scope(name.to_sym, -> { where.has_bits(field => value) })
|
|
23
|
+
define_method(name) do
|
|
24
|
+
send(field) & value == value
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
define_method("#{name}=") do |val|
|
|
28
|
+
if val.to_s =~ /[t1y]/
|
|
29
|
+
send("#{field}=", send(field) | value)
|
|
30
|
+
else
|
|
31
|
+
send("#{field}=", send(field) & ~value)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
define_method("#{name}_was") do
|
|
36
|
+
send("#{field}_was") & value == value
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
define_method("#{name}_before_last_save") do
|
|
40
|
+
send("#{field}_before_last_save") & value == value
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
alias_method("#{name}?", name)
|
|
44
|
+
alias_method("#{name}_before_last_save?", "#{name}_before_last_save")
|
|
45
|
+
alias_method("#{name}_was?", "#{name}_was")
|
|
46
|
+
|
|
47
|
+
define_method("#{name}_changed?") do
|
|
48
|
+
send("#{name}_was") != send(name)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
define_method("saved_change_to_#{name}?") do
|
|
52
|
+
send("#{name}_before_last_save") != send(name)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
class Configuration
|
|
5
|
+
# Maximum number of comma-separated values allowed in a multi-value query parameter.
|
|
6
|
+
# Used by ParseValue.range and (in yiffspace-search) QueryBuilder.
|
|
7
|
+
attr_reader(:max_multi_count)
|
|
8
|
+
|
|
9
|
+
# Redis URL used by Utils::Cache for direct Redis connections.
|
|
10
|
+
attr_reader(:redis_url)
|
|
11
|
+
|
|
12
|
+
# The application's ApplicationRecord class. Used by Concerns::ActiveRecordExtensions & (in
|
|
13
|
+
# yiffspace-tables) Utils::TableBuilder.
|
|
14
|
+
# Falls back to ::ApplicationRecord at call time when nil.
|
|
15
|
+
attr_writer(:application_record_class)
|
|
16
|
+
|
|
17
|
+
# The application's ApplicationController class. Used by Utils::Helpers & (in
|
|
18
|
+
# yiffspace-tables) Utils::TableBuilder.
|
|
19
|
+
# Falls back to ::ApplicationController at call time when nil.
|
|
20
|
+
attr_writer(:application_controller_class)
|
|
21
|
+
|
|
22
|
+
def initialize
|
|
23
|
+
@max_multi_count = -> { 100 }
|
|
24
|
+
@redis_url = -> {}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def redis_url=(value)
|
|
28
|
+
@redis_url = value.is_a?(Proc) ? value : -> { value }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def max_multi_count=(value)
|
|
32
|
+
@max_multi_count = value.is_a?(Proc) ? value : -> { value }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def application_record_class
|
|
36
|
+
@application_record_class || ::ApplicationRecord
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def application_controller_class
|
|
40
|
+
@application_controller_class || ::ApplicationController
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|