Skip to content

Commit 0276b34

Browse files
authored
Merge pull request #3854 from AlchemyCMS/configurable-publishable-resolver
Configurable publishable resolver
2 parents ae069bb + 2681746 commit 0276b34

5 files changed

Lines changed: 291 additions & 33 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# frozen_string_literal: true
2+
3+
module Alchemy
4+
module Publishable
5+
# Default resolver for Publishable records.
6+
#
7+
# Uses the +public_on+ and +public_until+ timestamp columns to determine
8+
# whether a record is public, scheduled, or publishable.
9+
#
10+
# This class defines the interface that custom resolvers configured via
11+
# +Alchemy.config.publishable_resolver+ must implement.
12+
#
13+
class TimestampResolver
14+
class << self
15+
# Returns records without a +public_on+ date set.
16+
def draft(publishables)
17+
publishables.where(public_on: nil)
18+
end
19+
20+
# Returns records with a +public_on+ date set.
21+
def scheduled(publishables)
22+
publishables.where.not(public_on: nil)
23+
end
24+
25+
# Returns records that are public at the given time.
26+
def published(publishables, at:)
27+
scheduled(publishables)
28+
.where("#{publishables.table_name}.public_on <= :at", at:)
29+
.where(public_until: nil).or(
30+
publishables.where("#{publishables.table_name}.public_until > :at", at:)
31+
)
32+
end
33+
end
34+
35+
def initialize(publishable)
36+
@publishable = publishable
37+
end
38+
39+
# Determines if the record is public at the given time.
40+
def public?(at: Current.preview_time)
41+
already_public_for?(at:) && still_public_for?(at:)
42+
end
43+
44+
# Determines if the record has a future publication or expiration event.
45+
def scheduled?(at: Current.preview_time)
46+
(publishable.public_on.present? && publishable.public_on > at) ||
47+
(publishable.public_until.present? && publishable.public_until > at)
48+
end
49+
50+
# Determines if the record is publishable.
51+
#
52+
# A record is publishable if a +public_on+ timestamp is set and not
53+
# expired yet.
54+
def publishable?
55+
!publishable.public_on.nil? && still_public_for?
56+
end
57+
58+
private
59+
60+
attr_reader :publishable
61+
62+
def already_public_for?(at:)
63+
!publishable.public_on.nil? && publishable.public_on <= at
64+
end
65+
66+
def still_public_for?(at: Current.preview_time)
67+
publishable.public_until.nil? || publishable.public_until > at
68+
end
69+
end
70+
end
71+
end

app/models/concerns/alchemy/publishable.rb

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,10 @@ module Publishable
33
extend ActiveSupport::Concern
44

55
included do
6-
scope :draft, -> { where(public_on: nil) }
7-
scope :scheduled, -> { where.not(public_on: nil) }
8-
6+
scope :draft, -> { Alchemy.config.publishable_resolver.draft(all) }
7+
scope :scheduled, -> { Alchemy.config.publishable_resolver.scheduled(all) }
98
scope :published, ->(at: Current.preview_time) {
10-
scheduled
11-
.where("#{table_name}.public_on <= :at", at:)
12-
.where(public_until: nil).or(
13-
where("#{table_name}.public_until > :at", at:)
14-
)
9+
Alchemy.config.publishable_resolver.published(all, at:)
1510
}
1611

1712
validate do
@@ -25,42 +20,32 @@ module Publishable
2520

2621
# Determines if this record is public
2722
#
28-
# Takes the two timestamps +public_on+ and +public_until+
29-
# and returns true if the time given (+Time.current+ per default)
30-
# is in this timespan.
31-
#
32-
# @param at [DateTime] (Time.current)
23+
# @param at [DateTime] (Current.preview_time)
3324
# @returns Boolean
3425
def public?(at: Current.preview_time)
35-
already_public_for?(at:) && still_public_for?(at:)
26+
publishable_resolver.public?(at:)
3627
end
3728
alias_method :public, :public?
3829

30+
# Determines if this record has a future publication or expiration event
31+
#
32+
# @param at [DateTime] (Current.preview_time)
33+
# @returns Boolean
3934
def scheduled?(at: Current.preview_time)
40-
(public_on.present? && public_on > at) || (public_until.present? && public_until > at)
35+
publishable_resolver.scheduled?(at:)
4136
end
4237

4338
# Determines if this record is publishable
4439
#
45-
# A record is publishable if a +public_on+ timestamp is set and not expired yet.
46-
#
4740
# @returns Boolean
4841
def publishable?
49-
!public_on.nil? && still_public_for?
42+
publishable_resolver.publishable?
5043
end
5144

52-
# Determines if this record is already public for given time
53-
# @param at [DateTime] (Time.current)
54-
# @returns Boolean
55-
def already_public_for?(at: Current.preview_time)
56-
!public_on.nil? && public_on <= at
57-
end
45+
private
5846

59-
# Determines if this record is still public for given time
60-
# @param at [DateTime] (Time.current)
61-
# @returns Boolean
62-
def still_public_for?(at: Current.preview_time)
63-
public_until.nil? || public_until > at
47+
def publishable_resolver
48+
Alchemy.config.publishable_resolver.new(self)
6449
end
6550
end
6651
end

lib/alchemy/configurations/main.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,20 @@ def user_class_name = "::#{raw_user_class}"
453453
# == Example
454454
# Alchemy.config.abilities.add("MyCustom::Ability")
455455
option :abilities, :collection, item_type: :class
456+
457+
# === Publishable resolver
458+
#
459+
# The class used to resolve publication state for Publishable records
460+
# (currently +Alchemy::Element+ and +Alchemy::PageVersion+).
461+
#
462+
# The default resolver uses the +public_on+ / +public_until+ timestamp
463+
# columns. Extension gems can provide a custom resolver that implements
464+
# the same interface — see +Alchemy::Publishable::TimestampResolver+
465+
# for the interface.
466+
#
467+
# == Example
468+
# Alchemy.config.publishable_resolver = "MyApp::CustomResolver"
469+
option :publishable_resolver, :class, default: "Alchemy::Publishable::TimestampResolver"
456470
end
457471
end
458472
end

lib/alchemy/test_support/shared_publishable_examples.rb

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@
3333
it "only includes records without public_on date" do
3434
expect(subject).to eq [nil]
3535
end
36+
37+
it "delegates to the resolver" do
38+
expect(Alchemy.config.publishable_resolver).to receive(:draft)
39+
described_class.draft
40+
end
3641
end
3742

3843
describe ".scheduled" do
@@ -50,13 +55,23 @@
5055
public_two
5156
])
5257
end
58+
59+
it "delegates to the resolver" do
60+
expect(Alchemy.config.publishable_resolver).to receive(:scheduled)
61+
subject
62+
end
5363
end
5464

5565
describe ".published" do
5666
let!(:public_one) { create(factory_name, public_on: Date.yesterday) }
5767
let!(:public_two) { create(factory_name, public_on: Date.tomorrow) }
5868
let!(:non_public) { create(factory_name, public_on: nil) }
5969

70+
it "delegates to the resolver" do
71+
expect(Alchemy.config.publishable_resolver).to receive(:published)
72+
described_class.published
73+
end
74+
6075
context "without time given" do
6176
subject { described_class.published }
6277

@@ -87,13 +102,16 @@
87102
subject { record.scheduled? }
88103

89104
let(:record) { build(factory_name, public_on:, public_until:) }
105+
let(:public_on) { nil }
106+
let(:public_until) { nil }
90107

91-
context "when public_on is nil" do
92-
let(:public_on) { nil }
108+
it "delegates to the resolver" do
109+
expect_any_instance_of(Alchemy.config.publishable_resolver).to receive(:scheduled?)
110+
subject
111+
end
93112

113+
context "when public_on is nil" do
94114
context "and public_until is nil" do
95-
let(:public_until) { nil }
96-
97115
it { expect(subject).to be(false) }
98116
end
99117

@@ -145,6 +163,13 @@
145163
describe "#public?" do
146164
subject { page_version.public? }
147165

166+
let(:page_version) { build(factory_name) }
167+
168+
it "delegates to the resolver" do
169+
expect_any_instance_of(Alchemy.config.publishable_resolver).to receive(:public?)
170+
subject
171+
end
172+
148173
context "when public_on is not set" do
149174
let(:page_version) { build(factory_name, public_on: nil) }
150175

0 commit comments

Comments
 (0)