yiffspace 0.0.1 → 0.0.2
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 +4 -4
- 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/current_methods.rb +60 -0
- data/lib/yiffspace/concerns/has_bit_flags.rb +59 -0
- data/lib/yiffspace/concerns/user_methods.rb +77 -0
- data/lib/yiffspace/concerns/user_name_methods.rb +53 -0
- data/lib/yiffspace/configuration.rb +70 -10
- data/lib/yiffspace/core_ext/all.rb +0 -1
- data/lib/yiffspace/include/all.rb +16 -0
- data/lib/yiffspace/include/cache.rb +5 -0
- data/lib/yiffspace/include/current.rb +5 -0
- data/lib/yiffspace/include/duration_parser.rb +5 -0
- data/lib/yiffspace/include/helpers.rb +5 -0
- data/lib/yiffspace/{core_ext → include}/open_hash.rb +2 -0
- data/lib/yiffspace/include/parameter_builder.rb +5 -0
- data/lib/yiffspace/include/parse_value.rb +5 -0
- data/lib/yiffspace/include/query_builder.rb +5 -0
- data/lib/yiffspace/include/query_dsl.rb +5 -0
- data/lib/yiffspace/include/query_helper.rb +5 -0
- data/lib/yiffspace/include/routes.rb +5 -0
- data/lib/yiffspace/include/table_builder.rb +5 -0
- data/lib/yiffspace/include/trace_logger.rb +5 -0
- data/lib/yiffspace/include/user_attribute.rb +5 -0
- data/lib/yiffspace/search/query_builder.rb +83 -0
- data/lib/yiffspace/search/query_dsl.rb +119 -0
- data/lib/yiffspace/search/query_helper.rb +49 -0
- data/lib/yiffspace/utils/cache.rb +40 -0
- data/lib/yiffspace/utils/current.rb +43 -0
- data/lib/yiffspace/utils/duration_parser.rb +24 -0
- data/lib/yiffspace/utils/helpers.rb +24 -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 +32 -0
- data/lib/yiffspace/utils/table_builder.rb +136 -0
- data/lib/yiffspace/utils/trace_logger.rb +91 -0
- data/lib/yiffspace/utils/user_attribute.rb +271 -0
- data/lib/yiffspace/version.rb +1 -1
- data/lib/yiffspace.rb +11 -1
- metadata +68 -3
|
@@ -2,6 +2,76 @@
|
|
|
2
2
|
|
|
3
3
|
module YiffSpace
|
|
4
4
|
class Configuration
|
|
5
|
+
# Maximum number of comma-separated values allowed in a multi-value query parameter.
|
|
6
|
+
# Used by ParseValue.range and 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 User model class. Used by Utils::Current, Search::QueryDSL, etc.
|
|
13
|
+
# Falls back to ::User at call time when nil.
|
|
14
|
+
attr_accessor(:user_class)
|
|
15
|
+
|
|
16
|
+
# The application's UserResolvable class. Used by Utils::Current.
|
|
17
|
+
# Falls back to ::UserResolvable at call time when nil.
|
|
18
|
+
attr_accessor(:user_resolvable_class)
|
|
19
|
+
|
|
20
|
+
# The CurrentAttributes class used to access the current user in model concerns.
|
|
21
|
+
# Defaults to YiffSpace::Utils::Current.
|
|
22
|
+
attr_writer(:current_class)
|
|
23
|
+
|
|
24
|
+
# The default IP address assigned to Utils::Current when none is present.
|
|
25
|
+
attr_accessor(:default_ip_address)
|
|
26
|
+
|
|
27
|
+
# Override the proc used to fetch the anonymous user. Must respond to #call.
|
|
28
|
+
# Default: -> { (user_class || ::User).anonymous }
|
|
29
|
+
attr_writer(:anonymous_user_getter)
|
|
30
|
+
|
|
31
|
+
# Override the proc used to fetch the system user. Must respond to #call.
|
|
32
|
+
# Default: -> { (user_class || ::User).system }
|
|
33
|
+
attr_writer(:system_user_getter)
|
|
34
|
+
|
|
35
|
+
# The anonymous user name
|
|
36
|
+
attr_reader(:anonymous_user_name)
|
|
37
|
+
|
|
38
|
+
def initialize
|
|
39
|
+
@max_multi_count = -> { 100 }
|
|
40
|
+
@redis_url = -> {}
|
|
41
|
+
@user_class = nil
|
|
42
|
+
@user_resolvable_class = nil
|
|
43
|
+
@current_class = nil
|
|
44
|
+
@default_ip_address = "127.0.0.1"
|
|
45
|
+
@anonymous_user_name = -> { "Anonymous" }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns the configured current class, defaulting to YiffSpace::Utils::Current.
|
|
49
|
+
def current_class
|
|
50
|
+
@current_class || Utils::Current
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Lazily built: calls user_class (or ::User) at invocation time, not config time.
|
|
54
|
+
def anonymous_user_getter
|
|
55
|
+
@anonymous_user_getter ||= -> { (user_class || ::User).anonymous }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Lazily built: calls user_class (or ::User) at invocation time, not config time.
|
|
59
|
+
def system_user_getter
|
|
60
|
+
@system_user_getter ||= -> { (user_class || ::User).system }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def redis_url=(value)
|
|
64
|
+
@redis_url = value.is_a?(Proc) ? value : -> { value }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def max_multi_count=(value)
|
|
68
|
+
@max_multi_count = value.is_a?(Proc) ? value : -> { value }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def anonymous_user_name=(value)
|
|
72
|
+
@anonymous_user_name = value.is_a?(Proc) ? value : -> { value }
|
|
73
|
+
end
|
|
74
|
+
|
|
5
75
|
def auth(&block)
|
|
6
76
|
client = YiffSpace::Auth.register(Auth::DEFAULT_CLIENT_NAME) unless YiffSpace::Auth.instance_variable_get(:@clients).key?(Auth::DEFAULT_CLIENT_NAME)
|
|
7
77
|
client ||= YiffSpace::Auth[Auth::DEFAULT_CLIENT_NAME]
|
|
@@ -17,14 +87,4 @@ module YiffSpace
|
|
|
17
87
|
@images ||= Images.new
|
|
18
88
|
end
|
|
19
89
|
end
|
|
20
|
-
|
|
21
|
-
class << self
|
|
22
|
-
def config
|
|
23
|
-
@config ||= Configuration.new
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def configure
|
|
27
|
-
yield(config)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
90
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative("cache")
|
|
4
|
+
require_relative("current")
|
|
5
|
+
require_relative("duration_parser")
|
|
6
|
+
require_relative("helpers")
|
|
7
|
+
require_relative("open_hash")
|
|
8
|
+
require_relative("parameter_builder")
|
|
9
|
+
require_relative("parse_value")
|
|
10
|
+
require_relative("query_builder")
|
|
11
|
+
require_relative("query_dsl")
|
|
12
|
+
require_relative("query_helper")
|
|
13
|
+
require_relative("routes")
|
|
14
|
+
require_relative("table_builder")
|
|
15
|
+
require_relative("trace_logger")
|
|
16
|
+
require_relative("user_attribute")
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Search
|
|
5
|
+
class QueryBuilder
|
|
6
|
+
attr_reader(:dsl, :klass, :relation, :user, :q)
|
|
7
|
+
|
|
8
|
+
# [param, db_field, type]
|
|
9
|
+
def initialize(dsl, klass, relation, user)
|
|
10
|
+
dsl ||= []
|
|
11
|
+
@dsl = dsl
|
|
12
|
+
@klass = klass
|
|
13
|
+
@relation = relation
|
|
14
|
+
@user = user
|
|
15
|
+
@q = relation
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def process_dsl(dsl, params)
|
|
19
|
+
dsl[:fields].each do |param, field, type = nil, block = nil, options = {}|
|
|
20
|
+
value = params[param]
|
|
21
|
+
multi = options.fetch(:multi, nil) || false
|
|
22
|
+
like = options.fetch(:like, nil) || false
|
|
23
|
+
ilike = options.fetch(:ilike, nil) || false
|
|
24
|
+
normalize = options.fetch(:normalize, nil) || ->(value) { value }
|
|
25
|
+
next if value.nil?
|
|
26
|
+
|
|
27
|
+
value = normalize.call(value)
|
|
28
|
+
@q = block.call(q) if block
|
|
29
|
+
if type.is_a?(Proc)
|
|
30
|
+
args = [q, value, user, params]
|
|
31
|
+
@q = type.call(*args.first(type.arity))
|
|
32
|
+
next
|
|
33
|
+
end
|
|
34
|
+
field ||= param
|
|
35
|
+
if like
|
|
36
|
+
@q = q.where.like(field => value)
|
|
37
|
+
next
|
|
38
|
+
elsif ilike
|
|
39
|
+
@q = q.where.ilike(field => value)
|
|
40
|
+
next
|
|
41
|
+
end
|
|
42
|
+
type ||= QueryHelper.get_column(field, klass.table_name).sql_type_metadata.type
|
|
43
|
+
case type
|
|
44
|
+
when :boolean
|
|
45
|
+
@q = q.boolean_attribute_matches(field, value)
|
|
46
|
+
when :integer
|
|
47
|
+
# multiple comma separated values are implicitly supported
|
|
48
|
+
# trimming them seems unneeded
|
|
49
|
+
# value = value[0.. value.index(",") - 1] unless multi
|
|
50
|
+
@q = q.numeric_attribute_matches(field, value)
|
|
51
|
+
when :datetime
|
|
52
|
+
@q = q.datetime_attribute_matches(field, value)
|
|
53
|
+
when :text, :string
|
|
54
|
+
value = value.split(",").first(YiffSpace.config.max_multi_count.call) if multi # explicitly supports arrays
|
|
55
|
+
@q = q.text_attribute_matches(field, value)
|
|
56
|
+
when :inet
|
|
57
|
+
@q = q.ip_attribute_matches(field, value)
|
|
58
|
+
when :present
|
|
59
|
+
@q = q.attribute_present(field, value)
|
|
60
|
+
else
|
|
61
|
+
raise(ArgumentError, "Unknown type: #{type} for field: #{field}")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
dsl[:associations].each do |attribute, klass, nested_dsl, join|
|
|
66
|
+
next if params[attribute].nil?
|
|
67
|
+
|
|
68
|
+
@q = q.joins(join)
|
|
69
|
+
nested_relation = klass.all
|
|
70
|
+
nested_builder = QueryBuilder.new(nested_dsl, klass, nested_relation, user)
|
|
71
|
+
nested_relation = nested_builder.search(params[attribute])
|
|
72
|
+
@q = q.merge(nested_relation)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def search(params = {})
|
|
77
|
+
params.transform_keys!(&:to_sym)
|
|
78
|
+
process_dsl(dsl, params)
|
|
79
|
+
q
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Search
|
|
5
|
+
class QueryDSL
|
|
6
|
+
attr_accessor(:relation, :fields, :associations, :no_id, :no_dates)
|
|
7
|
+
|
|
8
|
+
# [param, field, type/proc, Proc(relation)]
|
|
9
|
+
def initialize(relation)
|
|
10
|
+
@relation = relation
|
|
11
|
+
@fields = []
|
|
12
|
+
@associations = []
|
|
13
|
+
@no_id = false
|
|
14
|
+
@no_dates = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def custom(param, proc, not: false, multi: false, &block)
|
|
18
|
+
@fields << [param, nil, proc, block, { not: binding.local_variable_get(:not), multi: multi }]
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def field(param, db_field = param, type = nil, not: false, multi: false, wildcard: false, like: false, ilike: false, normalize: nil, &block)
|
|
23
|
+
type ||= QueryHelper.get_column(db_field, relation.table_name).sql_type_metadata.type
|
|
24
|
+
@fields << [param, db_field, type, block, { not: binding.local_variable_get(:not), multi: multi, wildcard: wildcard, like: like, ilike: ilike, normalize: normalize }]
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def user(param, association = nil, not: false, &)
|
|
29
|
+
if param.is_a?(Array)
|
|
30
|
+
raise(ArgumentError, "You must specify an association when passing an array of parameters") if association.nil?
|
|
31
|
+
else
|
|
32
|
+
association ||= param
|
|
33
|
+
param = [:"#{param}_id", :"#{param}_name"]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
case association
|
|
37
|
+
when Symbol, String
|
|
38
|
+
association = association.to_sym
|
|
39
|
+
associations = relation.reflect_on_all_associations(:belongs_to).map(&:name)
|
|
40
|
+
if associations.include?(association)
|
|
41
|
+
column = association_column(association)
|
|
42
|
+
else
|
|
43
|
+
column = association
|
|
44
|
+
end
|
|
45
|
+
when Array
|
|
46
|
+
column = association.first
|
|
47
|
+
association = association.second
|
|
48
|
+
end
|
|
49
|
+
field(param.first, column, not: binding.local_variable_get(:not), &) if param.first
|
|
50
|
+
custom(param.second, ->(q, v) { q.user_name_matches(association, v) }, not: binding.local_variable_get(:not), &) if param.second
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def present(param, db_field = param, &)
|
|
55
|
+
field(param, db_field, :present, &)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# [attribute, class, dsl, join]
|
|
59
|
+
def association(*args, **kwargs)
|
|
60
|
+
if args.any?
|
|
61
|
+
attribute = args.first
|
|
62
|
+
as = args.second || attribute
|
|
63
|
+
ref = relation.reflect_on_association(attribute)
|
|
64
|
+
raise("Association #{attribute} does not exist on #{relation.name}") if ref.nil?
|
|
65
|
+
|
|
66
|
+
klass = ref.klass
|
|
67
|
+
dsl = klass.query_dsl.build
|
|
68
|
+
@associations << [as, klass, dsl, attribute]
|
|
69
|
+
user_class = YiffSpace.config.user_class
|
|
70
|
+
user(as, attribute) if klass == user_class && @fields.none? { |f| f.second == association_column(attribute) }
|
|
71
|
+
elsif kwargs.any?
|
|
72
|
+
attribute = kwargs.delete(:as)
|
|
73
|
+
to_array = lambda { |h|
|
|
74
|
+
k, v = h.first
|
|
75
|
+
[k, v.is_a?(Hash) ? to_array.call(v) : v].flatten
|
|
76
|
+
}
|
|
77
|
+
through = to_array.call(kwargs)
|
|
78
|
+
attribute ||= through.last
|
|
79
|
+
klass = relation
|
|
80
|
+
through.each { |k| klass = klass.reflect_on_association(k).klass }
|
|
81
|
+
dsl = klass.query_dsl.build
|
|
82
|
+
join = through.reverse.reduce { |acc, key| { key => acc } }
|
|
83
|
+
@associations << [attribute, klass, dsl, join]
|
|
84
|
+
else
|
|
85
|
+
raise(ArgumentError, "Missing association")
|
|
86
|
+
end
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def no_id!
|
|
91
|
+
@no_id = true
|
|
92
|
+
@fields.reject! { |field| field.first == :id }
|
|
93
|
+
self
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def no_dates!
|
|
97
|
+
@no_dates = true
|
|
98
|
+
@fields.reject! { |field| %i[created_at updated_at].include?(field.first) }
|
|
99
|
+
self
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def build
|
|
103
|
+
@fields << %i[id id integer] if @fields.none? { |field| field.first == :id }
|
|
104
|
+
@fields << %i[created_at created_at datetime] if @fields.none? { |field| field.first == :created_at } && relation.attribute_names.include?("created_at")
|
|
105
|
+
@fields << %i[updated_at updated_at datetime] if @fields.none? { |field| field.first == :updated_at } && relation.attribute_names.include?("updated_at")
|
|
106
|
+
{
|
|
107
|
+
fields: fields,
|
|
108
|
+
associations: associations,
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def association_column(association)
|
|
115
|
+
:"#{relation.table_name}.#{relation.reflect_on_association(association).foreign_key}"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Search
|
|
5
|
+
module QueryHelper
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def parse_conditions(conditions, model, table_name = nil)
|
|
9
|
+
pairs = []
|
|
10
|
+
|
|
11
|
+
conditions.each do |key, value|
|
|
12
|
+
case key
|
|
13
|
+
when String, Symbol
|
|
14
|
+
key = key.to_s
|
|
15
|
+
if value.is_a?(Hash)
|
|
16
|
+
pairs.concat(parse_conditions(value, model, key))
|
|
17
|
+
elsif key.include?(".")
|
|
18
|
+
table, col = key.split(".", 2)
|
|
19
|
+
pairs << [[table, col], value]
|
|
20
|
+
else
|
|
21
|
+
pairs << [[table_name || model.table_name, key], value]
|
|
22
|
+
end
|
|
23
|
+
else
|
|
24
|
+
raise(ArgumentError, "Unsupported key type: #{key.class}")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
pairs
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def get_column(attribute, table = nil)
|
|
32
|
+
attribute = attribute.to_s
|
|
33
|
+
if attribute.include?(".")
|
|
34
|
+
table, column = attribute.split(".", 2)
|
|
35
|
+
else
|
|
36
|
+
column = attribute
|
|
37
|
+
end
|
|
38
|
+
raise(ArgumentError, "Missing table") if table.nil?
|
|
39
|
+
|
|
40
|
+
c = ActiveRecord::Base.connection.columns(table).find { |c| c.name == column }
|
|
41
|
+
raise(StandardError, "Column #{column} does not exist in table #{table}") unless c
|
|
42
|
+
|
|
43
|
+
c
|
|
44
|
+
rescue ActiveRecord::StatementInvalid
|
|
45
|
+
raise(StandardError, "Table #{table} does not exist")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Utils
|
|
5
|
+
module Cache
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def read_multi(keys, prefix)
|
|
9
|
+
sanitized_key_to_key_hash = keys.index_by { |key| "#{prefix}:#{key}" }
|
|
10
|
+
|
|
11
|
+
sanitized_keys = sanitized_key_to_key_hash.keys
|
|
12
|
+
sanitized_key_to_value_hash = Rails.cache.read_multi(*sanitized_keys)
|
|
13
|
+
|
|
14
|
+
sanitized_key_to_value_hash.transform_keys(&sanitized_key_to_key_hash)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def fetch(key, expires_in: nil, &)
|
|
18
|
+
Rails.cache.fetch(key, expires_in: expires_in, &)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def write(key, value, expires_in: nil)
|
|
22
|
+
Rails.cache.write(key, value, expires_in: expires_in)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def delete(key)
|
|
26
|
+
Rails.cache.delete(key)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def clear
|
|
30
|
+
Rails.cache.clear
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def redis
|
|
34
|
+
# Using a shared variable like this here is OK
|
|
35
|
+
# since pitchfork spawns a new process for each worker
|
|
36
|
+
@redis ||= Redis.new(url: YiffSpace.config.redis_url.call)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Utils
|
|
5
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
6
|
+
attribute(:user, :ip_addr, :request)
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
super
|
|
10
|
+
reset
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
after_reset do
|
|
14
|
+
attributes[:user] = YiffSpace.config.anonymous_user_getter.call
|
|
15
|
+
attributes[:ip_addr] = YiffSpace.config.default_ip_address
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def user
|
|
19
|
+
value = super
|
|
20
|
+
return value if value.is_a?(YiffSpace.config.user_resolvable_class) || !value.is_a?(YiffSpace.config.user_class)
|
|
21
|
+
return YiffSpace.config.user_resolvable_class.new(value, ip_addr) if ip_addr.present?
|
|
22
|
+
|
|
23
|
+
value
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def user=(value)
|
|
27
|
+
if value.is_a?(YiffSpace.config.user_resolvable_class)
|
|
28
|
+
self.ip_addr = value.ip_addr
|
|
29
|
+
value = value.user
|
|
30
|
+
end
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.scoped(user, ip_addr = YiffSpace.config.default_ip_address, &)
|
|
35
|
+
set(user: user, ip_addr: ip_addr, &)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.as_system(&)
|
|
39
|
+
scoped(YiffSpace.config.system_user_getter.call, &)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require("abbrev")
|
|
4
|
+
|
|
5
|
+
module YiffSpace
|
|
6
|
+
module Utils
|
|
7
|
+
module DurationParser
|
|
8
|
+
def self.parse(string)
|
|
9
|
+
abbrevs = Abbrev.abbrev(%w[seconds minutes hours days weeks months years])
|
|
10
|
+
|
|
11
|
+
raise unless string =~ /(.*?)([a-z]+)\z/i
|
|
12
|
+
|
|
13
|
+
size = Float($1)
|
|
14
|
+
unit = abbrevs.fetch($2.downcase)
|
|
15
|
+
|
|
16
|
+
raise(NotImplementedError) unless %w[seconds minutes hours days weeks months years].include?(unit)
|
|
17
|
+
|
|
18
|
+
size.public_send(unit)
|
|
19
|
+
rescue # rubocop:disable Style/RescueStandardError
|
|
20
|
+
raise(ArgumentError, "'#{string}' is not a valid duration")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Utils
|
|
5
|
+
module Helpers
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def method_missing(name, *, &)
|
|
9
|
+
target.send(name, *, &)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def respond_to_missing?(...)
|
|
13
|
+
target.respond_to?(...)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# Lazily resolved so ApplicationController is not referenced at load time.
|
|
19
|
+
def target
|
|
20
|
+
ApplicationController.helpers
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|