Skip to content

Commit 9d5c4b4

Browse files
committed
date_range_field
1 parent b0bc461 commit 9d5c4b4

File tree

8 files changed

+553
-1
lines changed

8 files changed

+553
-1
lines changed

test/dummy/app/models/fields.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module Fields
44
%w[
55
text boolean decimal integer
66
date datetime
7-
datetime_range
7+
date_range datetime_range
88
attachment multiple_attachment
99
choice multiple_choice
1010
resource_select multiple_resource_select
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# frozen_string_literal: true
2+
3+
module Fields
4+
class DateRangeField < Field
5+
serialize :validations, Validations::DateRangeField
6+
serialize :options, Options::DateRangeField
7+
8+
def interpret_to(model, overrides: {})
9+
check_model_validity!(model)
10+
11+
accessibility = overrides.fetch(:accessibility, self.accessibility)
12+
return model if accessibility == :hidden
13+
14+
nested_model = Class.new(::Fields::DateRangeField::DateRange)
15+
16+
model.nested_models[name] = nested_model
17+
18+
model.embeds_one name, anonymous_class: nested_model, validate: true
19+
model.accepts_nested_attributes_for name, reject_if: :all_blank
20+
21+
interpret_validations_to model, accessibility, overrides
22+
interpret_extra_to model, accessibility, overrides
23+
24+
model
25+
end
26+
27+
class DateRange < VirtualModel
28+
attribute :start_date, :datetime
29+
attribute :finish_date, :datetime
30+
31+
validates :start_date, :finish_date,
32+
presence: true
33+
34+
validates :finish_date,
35+
timeliness: {
36+
after: :start_date,
37+
type: :date
38+
},
39+
allow_blank: false
40+
end
41+
end
42+
end
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
# frozen_string_literal: true
2+
3+
module Fields::Options
4+
class DateRangeField < FieldOptions
5+
attribute :start_from, :string, default: "unlimited"
6+
enum start_from: {
7+
unlimited: "unlimited",
8+
today: "today",
9+
date: "date",
10+
days_before_finish: "days_before_finish"
11+
}, _prefix: :start_from
12+
13+
attribute :start_date, :date
14+
attribute :fixed_start_date, :boolean, default: false
15+
attribute :start_from_today_days_offset, :integer, default: 0
16+
attribute :days_before_finish, :integer, default: 1
17+
18+
attribute :finish_to, :string, default: "unlimited"
19+
enum finish_to: {
20+
unlimited: "unlimited",
21+
today: "today",
22+
date: "date",
23+
days_since_start: "days_since_start"
24+
}, _prefix: :finish_to
25+
26+
attribute :finish_date, :date
27+
attribute :fixed_finish_date, :boolean, default: false
28+
attribute :finish_to_today_days_offset, :integer, default: 0
29+
attribute :days_since_start, :integer, default: 1
30+
31+
attribute :minimum_distance, :integer, default: 0
32+
attribute :maximum_distance, :integer, default: 0
33+
34+
validates :start_from, :finish_to,
35+
presence: true
36+
37+
validates :start_date,
38+
presence: true,
39+
if: :start_from_date?
40+
41+
validates :finish_date,
42+
presence: true,
43+
if: :finish_to_date?
44+
45+
validates :days_before_finish,
46+
numericality: {
47+
only_integer: true,
48+
greater_than: 0
49+
},
50+
allow_blank: false,
51+
if: :start_from_days_before_finish?
52+
53+
validates :days_since_start,
54+
numericality: {
55+
only_integer: true,
56+
greater_than: 0
57+
},
58+
allow_blank: false,
59+
if: :finish_to_days_since_start?
60+
61+
validates :start_from_today_days_offset, :finish_to_today_days_offset,
62+
presence: true,
63+
numericality: {
64+
only_integer: true
65+
}
66+
67+
validates :start_date,
68+
timeliness: {
69+
before: ->(r) { Time.zone.today + r.finish_to_today_days_offset.days },
70+
type: :date
71+
},
72+
allow_blank: false,
73+
if: [:start_from_date?, :finish_to_today?]
74+
75+
validates :finish_date,
76+
timeliness: {
77+
after: -> { Time.zone.today + r.start_from_today_days_offset.days },
78+
type: :date
79+
},
80+
allow_blank: false,
81+
if: [:start_from_today?, :finish_to_date?]
82+
83+
validates :finish_date,
84+
timeliness: {
85+
after: :start_date,
86+
type: :date
87+
},
88+
allow_blank: false,
89+
if: [:start_from_date?, :finish_to_date?]
90+
91+
validates :finish_to,
92+
exclusion: {in: %w[today]},
93+
if: [:start_from_today?]
94+
95+
validates :finish_to,
96+
exclusion: {in: %w[days_since_start]},
97+
if: [:start_from_days_before_finish?]
98+
99+
validates :fixed_finish_date,
100+
absence: true,
101+
if: [:fixed_start_date]
102+
103+
validates :fixed_start_date,
104+
absence: true,
105+
if: ->(r) { r.start_from_days_before_finish? || r.start_from_unlimited? }
106+
107+
validates :fixed_finish_date,
108+
absence: true,
109+
if: ->(r) { r.finish_to_days_since_start? || r.finish_to_unlimited? }
110+
111+
validates :minimum_distance,
112+
numericality: {
113+
only_integer: true,
114+
greater_than_or_equal_to: 0
115+
},
116+
allow_blank: false
117+
118+
validates :maximum_distance,
119+
numericality: {
120+
only_integer: true,
121+
greater_than_or_equal_to: :minimum_distance
122+
},
123+
allow_blank: false,
124+
unless: -> (r) { r.maximum_distance.to_i == 0 }
125+
126+
def interpret_to(model, field_name, accessibility, _options = {})
127+
return unless accessibility == :read_and_write
128+
129+
klass = model.nested_models[field_name]
130+
131+
if start_from_today?
132+
start_date_days_offset = self.start_from_today_days_offset.days.to_i
133+
134+
klass.validates :start_date,
135+
timeliness: {
136+
on_or_after: -> { Time.zone.today + start_date_days_offset },
137+
type: :date
138+
},
139+
allow_blank: true
140+
klass.default_value_for :start_date,
141+
-> (_) { Time.zone.today + start_date_days_offset },
142+
allow_nil: false
143+
if fixed_start_date
144+
klass.attr_readonly :start_date
145+
end
146+
elsif start_from_date?
147+
klass.validates :start_date,
148+
timeliness: {
149+
on_or_after: start_date,
150+
type: :date
151+
},
152+
allow_blank: true
153+
klass.default_value_for :start_date,
154+
start_date,
155+
allow_nil: false
156+
if fixed_start_date
157+
klass.attr_readonly :start_date
158+
end
159+
elsif start_from_days_before_finish?
160+
days_before_finish = self.days_before_finish.days.to_i
161+
klass.validates :start_date,
162+
timeliness: {
163+
on_or_after: ->(r) { r.finish_date - days_before_finish },
164+
type: :date
165+
},
166+
allow_blank: true
167+
end
168+
169+
if finish_to_today?
170+
finish_date_days_offset = self.finish_to_today_days_offset.days.to_i
171+
172+
klass.validates :finish_date,
173+
timeliness: {
174+
on_or_before: -> { Time.zone.today + finish_date_days_offset },
175+
type: :date
176+
},
177+
allow_blank: true
178+
klass.default_value_for :finish_date,
179+
-> (_) { Time.zone.today + finish_date_days_offset },
180+
allow_nil: false
181+
if fixed_finish_date
182+
klass.attr_readonly :finish_date
183+
end
184+
elsif finish_to_date?
185+
klass.validates :finish_date,
186+
timeliness: {
187+
on_or_before: finish_date,
188+
type: :date
189+
},
190+
allow_blank: true
191+
klass.default_value_for :finish_date,
192+
finish_date,
193+
allow_nil: false
194+
if fixed_finish_date
195+
klass.attr_readonly :finish_date
196+
end
197+
elsif finish_to_days_since_start?
198+
days_since_start = self.days_since_start.days.to_i
199+
klass.validates :finish_date,
200+
timeliness: {
201+
on_or_before: ->(r) { r.start_date + days_since_start },
202+
type: :date
203+
},
204+
allow_blank: true
205+
end
206+
207+
if minimum_distance > 0
208+
minimum_distance_days = minimum_distance.days
209+
if fixed_start_date || start_from_today? || start_from_date? || finish_to_days_since_start?
210+
klass.validates :finish_date,
211+
timeliness: {
212+
on_or_after: ->(r) { r.start_date + minimum_distance_days },
213+
type: :date
214+
},
215+
allow_blank: false
216+
elsif fixed_finish_date || finish_to_today? || finish_to_date? || start_from_days_before_finish?
217+
klass.validates :start_date,
218+
timeliness: {
219+
on_or_before: ->(r) { r.finish_date - minimum_distance_days },
220+
type: :date
221+
},
222+
allow_blank: false
223+
else
224+
klass.validates :finish_date,
225+
timeliness: {
226+
on_or_after: ->(r) { r.start_date + minimum_distance_days },
227+
type: :date
228+
},
229+
allow_blank: false
230+
end
231+
end
232+
233+
if maximum_distance > 0
234+
maximum_distance_days = maximum_distance.days
235+
if fixed_start_date || start_from_today? || start_from_date? || finish_to_days_since_start?
236+
klass.validates :finish_date,
237+
timeliness: {
238+
on_or_before: ->(r) { r.start_date + maximum_distance_days },
239+
type: :date
240+
},
241+
allow_blank: false
242+
elsif fixed_finish_date || finish_to_today? || finish_to_date? || start_from_days_before_finish?
243+
klass.validates :finish_date,
244+
timeliness: {
245+
on_or_after: ->(r) { r.finish_date - maximum_distance_days },
246+
type: :date
247+
},
248+
allow_blank: false
249+
else
250+
klass.validates :finish_date,
251+
timeliness: {
252+
on_or_before: ->(r) { r.start_date + maximum_distance_days },
253+
type: :date
254+
},
255+
allow_blank: false
256+
end
257+
end
258+
end
259+
end
260+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
module Fields::Validations
4+
class DateRangeField < FieldOptions
5+
prepend Concerns::Fields::Validations::Presence
6+
end
7+
end

0 commit comments

Comments
 (0)