ama_layout 5.2.0 → 5.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c88882b36b0a2d7a3934d821d418f6dbb04dc7fb
4
- data.tar.gz: 9e8f31189b00d70f47c974da2c2d8fed0bea6da0
3
+ metadata.gz: 3c84576ab846039685676fea5a445c6bde97a418
4
+ data.tar.gz: 898f53f578677836a9f708c44117142217b49d3e
5
5
  SHA512:
6
- metadata.gz: 9eb109d937fd157a2b3d255a8ec39e3e21912fae3f13bfb72815b1313a8bd71d099691a2a5820c277ffcd8bea4c80809a6f716ef3cfa1d4b447c38e8358eaf7e
7
- data.tar.gz: 47023ecc3639248c175231fe82208e86e0fa81cea06fe64ebabda18924b02a32552dca1c4f59a21b38f10ea77408c5e9d0aef880f7fb8163f7aa478e5c63b158
6
+ metadata.gz: 1d15a754ed97115d0312f123885c591bb28498f6c7f17a99b00e5316256026ec9e2e0a25ca8a468a0fea441b26a8ebc0a622a6bd4b8c3e66f4df480254fa4fb2
7
+ data.tar.gz: 0fc6b11bd21ee5739d94dc2e3188bf9030302bdcb5eff52c4ffbf364e90045da1ec01d7bb9ab252deac6ffdb418203c543814c149ed51173cd1948091acfa505
@@ -2,3 +2,5 @@ language: ruby
2
2
  rvm:
3
3
  - 2.2.2
4
4
  sudo: false
5
+ services:
6
+ - redis-server
@@ -4,31 +4,33 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'ama_layout/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "ama_layout"
7
+ spec.name = 'ama_layout'
8
8
  spec.version = AmaLayout::VERSION
9
- spec.authors = ["Michael van den Beuken", "Ruben Estevez", "Jordan Babe", "Mathieu Gilbert", "Ryan Jones", "Darko Dosenovic", "Jonathan Weyermann", "Adam Melnyk", "Kayt Campbell", "Kathleen Robertson", "Jesse Doyle"]
10
- spec.email = ["michael.beuken@gmail.com", "ruben.a.estevez@gmail.com", "jorbabe@gmail.com", "mathieu.gilbert@ama.ab.ca", "ryan.michael.jones@gmail.com", "darko.dosenovic@ama.ab.ca", "jonathan.weyermann@ama.ab.ca", "adam.melnyk@ama.ab.ca", "kayt.campbell@ama.ab.ca", "kathleen.robertson@ama.ab.ca", "jesse.doyle@ama.ab.ca"]
9
+ spec.authors = ['Michael van den Beuken', 'Ruben Estevez', 'Jordan Babe', 'Mathieu Gilbert', 'Ryan Jones', 'Darko Dosenovic', 'Jonathan Weyermann', 'Adam Melnyk', 'Kayt Campbell', 'Kathleen Robertson', 'Jesse Doyle']
10
+ spec.email = ['michael.beuken@gmail.com', 'ruben.a.estevez@gmail.com', 'jorbabe@gmail.com', 'mathieu.gilbert@ama.ab.ca', 'ryan.michael.jones@gmail.com', 'darko.dosenovic@ama.ab.ca', 'jonathan.weyermann@ama.ab.ca', 'adam.melnyk@ama.ab.ca', 'kayt.campbell@ama.ab.ca', 'kathleen.robertson@ama.ab.ca', 'jesse.doyle@ama.ab.ca']
11
11
  spec.summary = %q{.ama.ab.ca site layouts}
12
12
  spec.description = %q{.ama.ab.ca site layouts}
13
- spec.homepage = ""
14
- spec.license = "MIT"
13
+ spec.homepage = 'https://github.com/amaabca/ama_layout'
14
+ spec.license = 'MIT'
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0")
17
17
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
19
+ spec.require_paths = ['lib']
20
20
 
21
- spec.add_dependency "foundation-rails", "~> 6.3.1.0"
22
- spec.add_dependency "rails", "~> 4.2"
23
- spec.add_dependency "draper", "~> 2.1"
24
- spec.add_dependency "browser", "~> 2.0"
25
- spec.add_dependency "breadcrumbs_on_rails", "~> 3.0.1"
26
- spec.add_development_dependency "bundler", "~> 1.11"
27
- spec.add_development_dependency "rake", "~> 11.0"
28
- spec.add_development_dependency "rspec-rails"
29
- spec.add_development_dependency "factory_girl"
30
- spec.add_development_dependency "simplecov"
31
- spec.add_development_dependency "pry"
32
- spec.add_development_dependency "combustion"
33
- spec.add_development_dependency "sqlite3"
21
+ spec.add_dependency 'foundation-rails', '~> 6.3.1.0'
22
+ spec.add_dependency 'rails', '~> 4.2'
23
+ spec.add_dependency 'draper', '~> 2.1'
24
+ spec.add_dependency 'browser', '~> 2.0'
25
+ spec.add_dependency 'breadcrumbs_on_rails', '~> 3.0.1'
26
+ spec.add_dependency 'redis-rails'
27
+ spec.add_development_dependency 'bundler', '~> 1.11'
28
+ spec.add_development_dependency 'rake', '>= 11.0'
29
+ spec.add_development_dependency 'rspec-rails'
30
+ spec.add_development_dependency 'factory_girl'
31
+ spec.add_development_dependency 'simplecov'
32
+ spec.add_development_dependency 'pry'
33
+ spec.add_development_dependency 'combustion'
34
+ spec.add_development_dependency 'sqlite3'
35
+ spec.add_development_dependency 'timecop'
34
36
  end
@@ -1,17 +1,23 @@
1
- require "ama_layout/version"
2
- require "rails/all"
3
- require "foundation-rails"
4
- require "draper"
5
- require "browser"
6
- require "breadcrumbs_on_rails"
7
- require "ama_layout/breadcrumb_builder"
8
- require "ama_layout/moneris"
9
- require "ama_layout/navigation"
10
- require "ama_layout/navigation_item"
11
- require "ama_layout/decorators/moneris_decorator"
12
- require "ama_layout/decorators/navigation_decorator"
13
- require "ama_layout/decorators/navigation_item_decorator"
14
- require "ama_layout/controllers/action_controller"
1
+ require 'ama_layout/version'
2
+ require 'rails/all'
3
+ require 'foundation-rails'
4
+ require 'draper'
5
+ require 'browser'
6
+ require 'breadcrumbs_on_rails'
7
+ require 'redis-rails'
8
+ require 'ama_layout/breadcrumb_builder'
9
+ require 'ama_layout/moneris'
10
+ require 'ama_layout/navigation'
11
+ require 'ama_layout/navigation_item'
12
+ require 'ama_layout/decorators/moneris_decorator'
13
+ require 'ama_layout/decorators/navigation_decorator'
14
+ require 'ama_layout/decorators/navigation_item_decorator'
15
+ require 'ama_layout/controllers/action_controller'
16
+ require 'ama_layout/notifications/abstract_store'
17
+ require 'ama_layout/notifications/redis_store'
18
+ require 'ama_layout/notification'
19
+ require 'ama_layout/notification_set'
20
+ require 'ama_layout/notifications'
15
21
 
16
22
  module AmaLayout
17
23
  module Rails
@@ -0,0 +1,84 @@
1
+ module AmaLayout
2
+ class Notification
3
+ TYPES = %i[notice warning alert].freeze
4
+ DEFAULT_LIFESPAN = 1.year.freeze
5
+ FORMAT_VERSION = '1.0.0'.freeze
6
+
7
+ # NOTE: The following attributes are designed to be immutable - you need
8
+ # make a new instance to change them. The only mutable attribute is :active.
9
+ attr_reader :id, :type, :header, :content, :created_at, :lifespan, :version
10
+ attr_accessor :active
11
+
12
+ def initialize(args = {})
13
+ args = args.with_indifferent_access
14
+ @id = args[:id]
15
+ @type = args.fetch(:type, :notice).to_sym
16
+ @header = args.fetch(:header)
17
+ @content = args.fetch(:content)
18
+ @created_at = parse_time(args.fetch(:created_at))
19
+ @lifespan = parse_duration(args.fetch(:lifespan, DEFAULT_LIFESPAN))
20
+ @version = args.fetch(:version, FORMAT_VERSION)
21
+ self.active = args.fetch(:active)
22
+ invalid_type! if TYPES.exclude?(type)
23
+ end
24
+
25
+ def <=>(other)
26
+ created_at <=> other.created_at
27
+ end
28
+
29
+ def active?
30
+ active
31
+ end
32
+
33
+ def dismissed?
34
+ !active?
35
+ end
36
+
37
+ def dismiss!
38
+ self.active = false
39
+ dismissed?
40
+ end
41
+
42
+ def digest
43
+ Digest::SHA256.hexdigest(
44
+ "#{type}#{header}#{content}#{lifespan.to_i}#{version}"
45
+ )
46
+ end
47
+
48
+ def stale?
49
+ Time.current > created_at + lifespan
50
+ end
51
+
52
+ def to_h
53
+ # NOTE: We want the following keys to be strings to provide
54
+ # consistency with the underlying data store.
55
+ {
56
+ 'type' => type.to_s,
57
+ 'header' => header,
58
+ 'content' => content,
59
+ 'created_at' => created_at.iso8601,
60
+ 'active' => active,
61
+ 'lifespan' => lifespan.to_i,
62
+ 'version' => version
63
+ }
64
+ end
65
+
66
+ private
67
+
68
+ def invalid_type!
69
+ raise ArgumentError, "invalid notification type: #{type}"
70
+ end
71
+
72
+ def parse_time(time)
73
+ time.is_a?(String) ? Time.zone.parse(time) : time
74
+ end
75
+
76
+ def parse_duration(duration)
77
+ if duration.is_a?(ActiveSupport::Duration)
78
+ duration
79
+ else
80
+ duration.seconds
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,130 @@
1
+ module AmaLayout
2
+ # An array-like object that handles the storage and retrieval of notifications
3
+ # from the underlying data store.
4
+ #
5
+ # The raw serialization format is JSON as follows (keys are SHA256 hashes):
6
+ #
7
+ # {
8
+ # "8ca9f850c18acc17643038b2341bee3ede8a24c0f3e92f56f2109ce49fdcb616": {
9
+ # "type": "notice",
10
+ # "header": "test",
11
+ # "content": "test",
12
+ # "created_at": "2017-06-19T11:26:57.730-06:00",
13
+ # "lifespan": 31557600,
14
+ # "active": true,
15
+ # "version": "1.0.0"
16
+ # }
17
+ # }
18
+ #
19
+ class NotificationSet
20
+ include Enumerable
21
+ attr_accessor :base, :data_store, :key
22
+
23
+ delegate :each, :first, :last, :size, :[], :empty?, :any?, to: :active
24
+
25
+ def initialize(data_store, key)
26
+ self.data_store = data_store
27
+ self.key = key
28
+ self.base = fetch
29
+ clean!
30
+ end
31
+
32
+ def active
33
+ all.select(&:active?)
34
+ end
35
+
36
+ def all
37
+ @all ||= normalize(base_notifications)
38
+ end
39
+
40
+ def create(args = {})
41
+ args[:created_at] = Time.current
42
+ args[:active] = true
43
+ notification = Notification.new(args)
44
+ # previously dismissed notifications always take precendence
45
+ all.push(notification) unless base.key?(notification.digest)
46
+ save
47
+ end
48
+
49
+ def destroy!
50
+ data_store.delete(key) && reload!
51
+ end
52
+
53
+ def find(digest)
54
+ all.find { |n| n.id == digest }
55
+ end
56
+
57
+ def save
58
+ data_store.transaction do |store|
59
+ normalized = normalize(all)
60
+ self.base = serialize(normalized)
61
+ store.set(key, base.to_json)
62
+ end
63
+ reload!
64
+ end
65
+
66
+ def inspect
67
+ "<#{self.class.name}>: #{all}"
68
+ end
69
+ alias_method :to_s, :inspect
70
+
71
+ private
72
+
73
+ def clean!
74
+ if dirty?
75
+ all.reject!(&:stale?)
76
+ save
77
+ end
78
+ end
79
+
80
+ def dirty?
81
+ all.any?(&:stale?)
82
+ end
83
+
84
+ def reload!
85
+ @all = nil
86
+ self.base = fetch
87
+ all
88
+ self
89
+ end
90
+
91
+ def base_notifications
92
+ base.map { |k, v| Notification.new(v.merge(id: k)) }
93
+ end
94
+
95
+ def serialize(data)
96
+ data.inject({}) do |hash, element|
97
+ hash[element.digest] = element.to_h
98
+ hash
99
+ end
100
+ end
101
+
102
+ def normalize(data)
103
+ # sort by reverse chronological order
104
+ data.sort { |a, b| b <=> a }
105
+ end
106
+
107
+ def fetch
108
+ result = data_store.get(key)
109
+ result.present? ? build(result) : {}
110
+ end
111
+
112
+ def build(raw)
113
+ JSON.parse(raw)
114
+ rescue JSON::ParserError
115
+ data_store.delete(key) # we should try to prevent further errors
116
+ ::Rails.logger.error json_message(__FILE__, __LINE__, raw)
117
+ {}
118
+ end
119
+
120
+ def json_message(file, line, raw)
121
+ {
122
+ error: "#{self.class.name} - Invalid JSON",
123
+ file: file,
124
+ line: line,
125
+ key: key,
126
+ raw: raw
127
+ }.to_json
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,73 @@
1
+ module AmaLayout
2
+ # Usage:
3
+ #
4
+ # class MyClass
5
+ # include AmaLayout::Notifications
6
+ #
7
+ # notification_store AmaLayout::Notifications::RedisStore.new(options)
8
+ # notification_foreign_key :a_method_name_or_proc # defaults to :id
9
+ #
10
+ # ...
11
+ # end
12
+ #
13
+ module Notifications
14
+ InvalidNotificationStore = Class.new(StandardError)
15
+
16
+ def self.included(base)
17
+ base.extend(ClassMethods)
18
+ base.include(InstanceMethods)
19
+ end
20
+
21
+ module InstanceMethods
22
+ def notifications
23
+ @notifications ||= NotificationSet.new(_store, _foreign_key)
24
+ end
25
+
26
+ def notifications=(other)
27
+ @notifications = other
28
+ end
29
+
30
+ private
31
+
32
+ def _store
33
+ self.class._notification_store || invalid_store!
34
+ end
35
+
36
+ def _foreign_key
37
+ self.class._notification_foreign_key.call(self)
38
+ end
39
+
40
+ def invalid_store!
41
+ raise InvalidNotificationStore, 'a notification store must be specified'
42
+ end
43
+ end
44
+
45
+ module ClassMethods
46
+ def notification_store(store)
47
+ self._notification_store = store
48
+ end
49
+
50
+ def notification_foreign_key(key)
51
+ self._notification_foreign_key = key
52
+ end
53
+
54
+ def _notification_foreign_key
55
+ @_notification_foreign_key || Proc.new(&:id)
56
+ end
57
+
58
+ def _notification_store
59
+ @_notification_store
60
+ end
61
+
62
+ private
63
+
64
+ def _notification_store=(store)
65
+ @_notification_store = store
66
+ end
67
+
68
+ def _notification_foreign_key=(key)
69
+ @_notification_foreign_key = key.to_proc
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,17 @@
1
+ module AmaLayout
2
+ module Notifications
3
+ class AbstractStore
4
+ def get(key, opts = {})
5
+ raise NotImplementedError, 'you must define a #get method in a subclass'
6
+ end
7
+
8
+ def set(key, value, opts = {})
9
+ raise NotImplementedError, 'you must define a #set method in a subclass'
10
+ end
11
+
12
+ def delete(key, opts = {})
13
+ raise NotImplementedError, 'you must define a #delete method in a subclass'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,38 @@
1
+ module AmaLayout
2
+ module Notifications
3
+ class RedisStore < AbstractStore
4
+ delegate :clear, to: :base
5
+
6
+ attr_accessor :base
7
+
8
+ def initialize(opts = {})
9
+ self.base = ActiveSupport::Cache.lookup_store(
10
+ :redis_store,
11
+ opts.merge(raw: true)
12
+ )
13
+ end
14
+
15
+ def get(key, opts = {})
16
+ if opts.fetch(:default, false)
17
+ base.fetch(key) { opts[:default] }
18
+ else
19
+ base.read(key)
20
+ end
21
+ end
22
+
23
+ def set(key, value, opts = {})
24
+ base.write(key, value, opts) == 'OK'
25
+ end
26
+
27
+ def delete(key, opts = {})
28
+ base.delete(key, opts) == 1
29
+ end
30
+
31
+ def transaction
32
+ base.data.multi do
33
+ yield self
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,3 +1,3 @@
1
1
  module AmaLayout
2
- VERSION = '5.2.0'
2
+ VERSION = '5.4.0'
3
3
  end
@@ -0,0 +1,237 @@
1
+ describe AmaLayout::NotificationSet do
2
+ let(:store) do
3
+ AmaLayout::Notifications::RedisStore.new(
4
+ db: 4,
5
+ namespace: 'test_notifications',
6
+ host: 'localhost'
7
+ )
8
+ end
9
+ let(:key) { 1 }
10
+ let(:duration) { AmaLayout::Notification::DEFAULT_LIFESPAN.to_i }
11
+ let(:store_key) { key.to_s }
12
+ let(:json) do
13
+ <<-JSON
14
+ {
15
+ "8ca9f850c18acc17643038b2341bee3ede8a24c0f3e92f56f2109ce49fdcb616": {
16
+ "type": "notice",
17
+ "header": "test",
18
+ "content": "test",
19
+ "created_at": "2017-06-19T06:00:00.000Z",
20
+ "active": true,
21
+ "lifespan": #{duration},
22
+ "version": "1.0.0"
23
+ }
24
+ }
25
+ JSON
26
+ end
27
+ let(:stale_json) do
28
+ <<-JSON
29
+ {
30
+ "d3c2bc71904100674325791b371db7446097f956ea76a304e787abd5f2588665": {
31
+ "type": "notice",
32
+ "header": "stale",
33
+ "content": "stale",
34
+ "created_at": "2012-06-19T06:00:00.000Z",
35
+ "active": true,
36
+ "lifespan": #{duration},
37
+ "version": "1.0.0"
38
+ }
39
+ }
40
+ JSON
41
+ end
42
+
43
+ subject { described_class.new(store, key) }
44
+
45
+ around(:each) do |example|
46
+ Timecop.freeze(Time.zone.local(2017, 6, 19)) do
47
+ store.clear
48
+ example.run
49
+ store.clear
50
+ end
51
+ end
52
+
53
+ describe '#intialize' do
54
+ context 'with valid JSON in data store' do
55
+ before(:each) do
56
+ store.set(store_key, notification)
57
+ end
58
+
59
+ context 'without stale notifications in the data store' do
60
+ let(:notification) { json }
61
+
62
+ it 'fetches the notifications' do
63
+ expect(subject.size).to eq(1)
64
+ end
65
+ end
66
+
67
+ context 'with stale notifications in the data store' do
68
+ let(:notification) { stale_json }
69
+
70
+ it 'returns an empty set' do
71
+ expect(subject).to be_empty
72
+ end
73
+
74
+ it 'cleans out stale notifications from the data store' do
75
+ subject
76
+ expect(store.get(store_key)).to eq('{}')
77
+ end
78
+ end
79
+ end
80
+
81
+ context 'with invalid JSON in data store' do
82
+ before(:each) do
83
+ store.set(store_key, '{"invalid_json":')
84
+ end
85
+
86
+ it 'logs to Rails logger' do
87
+ expect(Rails.logger).to receive(:error).with(instance_of(String))
88
+ subject
89
+ end
90
+
91
+ it 'deletes the key in data store' do
92
+ subject
93
+ expect(store.get(store_key)).to be nil
94
+ end
95
+
96
+ it 'returns an empty set' do
97
+ expect(subject).to be_empty
98
+ end
99
+
100
+ it 'sets the base attribute to a hash' do
101
+ expect(subject.base).to be_a(Hash)
102
+ end
103
+ end
104
+
105
+ context 'with no entry in data store' do
106
+ it 'returns an empty set' do
107
+ expect(subject).to be_empty
108
+ end
109
+ end
110
+ end
111
+
112
+ describe '#create' do
113
+ it 'returns the NotificationSet instance' do
114
+ expect(subject.create(header: 'test', content: 'test')).to be_a(described_class)
115
+ end
116
+
117
+ it 'creates a new active notification' do
118
+ subject.create(header: 'test', content: 'test')
119
+ expect(subject.size).to eq(1)
120
+ end
121
+
122
+ it 'saves a notification in data store' do
123
+ subject.create(header: 'test', content: 'test')
124
+ expect(store.get(store_key)).to be_a(String)
125
+ end
126
+
127
+ context 'when the same notification exists but is dismissed' do
128
+ before(:each) do
129
+ store.set(store_key, json)
130
+ subject.first.dismiss!
131
+ subject.save
132
+ subject.create(header: 'test', content: 'test')
133
+ end
134
+
135
+ it 'does not overwrite the notification' do
136
+ expect(subject).to be_empty # we have only non-active notifications
137
+ end
138
+
139
+ it 'still has the dismissed notification in the data store' do
140
+ data = JSON.parse(store.get(store_key))
141
+ notification = data.values.first
142
+ expect(data.values.first['active']).to be false
143
+ end
144
+ end
145
+ end
146
+
147
+ describe '#destroy' do
148
+ context 'when data is removed' do
149
+ before(:each) do
150
+ subject.create(header: 'test', content: 'test', lifespan: 1.day)
151
+ end
152
+
153
+ it 'returns a NotificationSet instance' do
154
+ expect(subject.destroy!).to be_a(described_class)
155
+ end
156
+
157
+ it 'removes the notifications from the data store' do
158
+ subject.destroy!
159
+ expect(store.get(store_key)).to be nil
160
+ end
161
+
162
+ it 'returns an empty set' do
163
+ expect(subject.destroy!).to be_empty
164
+ end
165
+ end
166
+
167
+ context 'when data is not removed' do
168
+ it 'returns false' do
169
+ expect(subject.destroy!).to be false
170
+ end
171
+ end
172
+ end
173
+
174
+ describe '#find' do
175
+ context 'when id is not preset' do
176
+ it 'returns nil' do
177
+ expect(subject.find('invalid')).to be nil
178
+ end
179
+ end
180
+
181
+ context 'when id is present' do
182
+ let(:id) { subject.last.id }
183
+
184
+ before(:each) do
185
+ subject.create(header: 'test', content: 'test')
186
+ end
187
+
188
+ it 'returns the notification' do
189
+ expect(subject.find(id)).to eq(subject.last)
190
+ end
191
+ end
192
+ end
193
+
194
+ describe '#save' do
195
+ before(:each) do
196
+ subject.create(header: 'test', content: 'test')
197
+ end
198
+
199
+ it 'saves the notifications' do
200
+ expect(subject.last.active?).to be true
201
+ subject.last.dismiss!
202
+ subject.save
203
+ expect(subject).to be_empty
204
+ end
205
+
206
+ it 'returns the NotificationSet instance' do
207
+ expect(subject.save).to be_a(described_class)
208
+ end
209
+ end
210
+
211
+ describe '#inspect' do
212
+ it 'returns a stringified instance' do
213
+ expect(subject.inspect).to eq('<AmaLayout::NotificationSet>: []')
214
+ end
215
+ end
216
+
217
+ context 'scoping' do
218
+ before(:each) do
219
+ subject.create(header: 'test', content: 'test')
220
+ subject.create(header: 'inactive', content: 'inactive')
221
+ subject.last.dismiss!
222
+ subject.save
223
+ end
224
+
225
+ describe '#all' do
226
+ it 'returns both active and inactive notifications' do
227
+ expect(subject.all.size).to eq(2)
228
+ end
229
+ end
230
+
231
+ describe '#active' do
232
+ it 'returns only active notifications' do
233
+ expect(subject.active.size).to eq(1)
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,193 @@
1
+ describe AmaLayout::Notification do
2
+ let(:instance) do
3
+ described_class.new(
4
+ header: 'test',
5
+ content: 'test',
6
+ active: true,
7
+ created_at: Time.current
8
+ )
9
+ end
10
+
11
+ describe '#initialize' do
12
+ let(:lifespan) { 1.day }
13
+
14
+ subject do
15
+ described_class.new(
16
+ header: 'test',
17
+ 'content': 'test',
18
+ 'active': true,
19
+ 'created_at': time,
20
+ 'lifespan': lifespan
21
+ )
22
+ end
23
+
24
+ context 'with a time created_at attribute' do
25
+ let(:time) { Time.current }
26
+
27
+ it 'accepts both symbols and strings as hash keys' do
28
+ expect(subject.header).to eq('test')
29
+ expect(subject.active).to be true
30
+ end
31
+
32
+ it 'sets a version by default' do
33
+ expect(subject.version).to eq(described_class::FORMAT_VERSION)
34
+ end
35
+ end
36
+
37
+ context 'with a string created_at attribute' do
38
+ let(:time) { '1984-01-01' }
39
+
40
+ it 'parses the string as a time' do
41
+ expect(subject.created_at).to eq(Time.zone.parse(time))
42
+ end
43
+ end
44
+
45
+ context 'with an Integer/Fixnum lifespan attribute' do
46
+ let(:time) { Time.current }
47
+ let(:lifespan) { 3600 }
48
+
49
+ it 'parses the integer to a duration of seconds' do
50
+ expect(subject.lifespan).to eq(lifespan.seconds)
51
+ end
52
+ end
53
+
54
+ context 'with an invalid type attribute' do
55
+ let(:parameters) do
56
+ {
57
+ type: :invalid,
58
+ header: 'test',
59
+ content: 'test',
60
+ created_at: Time.current,
61
+ active: true
62
+ }
63
+ end
64
+
65
+ it 'raises ArgumentError' do
66
+ expect { described_class.new(parameters) }.to raise_error(ArgumentError)
67
+ end
68
+ end
69
+ end
70
+
71
+ describe '#<=>' do
72
+ let(:old) do
73
+ described_class.new(
74
+ header: 'test',
75
+ content: 'test',
76
+ active: true,
77
+ created_at: Date.new(1984)
78
+ )
79
+ end
80
+ let(:new) do
81
+ described_class.new(
82
+ header: 'test',
83
+ content: 'test',
84
+ active: true,
85
+ created_at: Date.new(2017)
86
+ )
87
+ end
88
+
89
+ it 'sorts by created_at date' do
90
+ expect(old <=> new).to eq(-1)
91
+ end
92
+ end
93
+
94
+ describe '#active?' do
95
+ subject do
96
+ described_class.new(
97
+ header: 'test',
98
+ content: 'test',
99
+ active: active,
100
+ created_at: Time.current
101
+ )
102
+ end
103
+
104
+ context 'when active' do
105
+ let(:active) { true }
106
+
107
+ it 'returns true' do
108
+ expect(subject.active?).to be true
109
+ end
110
+ end
111
+
112
+ context 'when inactive' do
113
+ let(:active) { false }
114
+
115
+ it 'returns false' do
116
+ expect(subject.active?).to be false
117
+ end
118
+ end
119
+ end
120
+
121
+ describe 'dismissed?' do
122
+ subject do
123
+ described_class.new(
124
+ header: 'test',
125
+ content: 'test',
126
+ active: active,
127
+ created_at: Time.current
128
+ )
129
+ end
130
+
131
+ context 'when active' do
132
+ let(:active) { true }
133
+
134
+ it 'returns false' do
135
+ expect(subject.dismissed?).to be false
136
+ end
137
+ end
138
+
139
+ context 'when inactive' do
140
+ let(:active) { false }
141
+
142
+ it 'returns true' do
143
+ expect(subject.dismissed?).to be true
144
+ end
145
+ end
146
+ end
147
+
148
+ describe '#dismiss!' do
149
+ it 'returns true' do
150
+ expect(instance.dismiss!).to be true
151
+ end
152
+
153
+ it 'sets the :active flag to false' do
154
+ instance.dismiss!
155
+ expect(instance.active?).to be false
156
+ end
157
+ end
158
+
159
+ describe '#digest' do
160
+ context 'with the same objects' do
161
+ let(:other) { instance.dup }
162
+
163
+ it 'produces the same digest' do
164
+ expect(instance.digest).to eq(other.digest)
165
+ end
166
+ end
167
+
168
+ context 'with different objects' do
169
+ let(:other) do
170
+ described_class.new(
171
+ header: 'other',
172
+ content: 'other',
173
+ active: true,
174
+ created_at: Time.current
175
+ )
176
+ end
177
+
178
+ it 'produces different digests' do
179
+ expect(instance.digest).to_not eq(other.digest)
180
+ end
181
+ end
182
+ end
183
+
184
+ describe '#to_h' do
185
+ it 'returns a hash' do
186
+ expect(instance.to_h).to be_a(Hash)
187
+ end
188
+
189
+ it 'is not empty' do
190
+ expect(instance.to_h).to_not be_empty
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,23 @@
1
+ describe AmaLayout::Notifications::AbstractStore do
2
+ context 'when inheriting' do
3
+ subject { Class.new(described_class).new }
4
+
5
+ describe '#get' do
6
+ it 'raises NotImplementedError' do
7
+ expect { subject.get('test') }.to raise_error(NotImplementedError)
8
+ end
9
+ end
10
+
11
+ describe '#set' do
12
+ it 'raises NotImplementedError' do
13
+ expect { subject.set('test', 'test') }.to raise_error(NotImplementedError)
14
+ end
15
+ end
16
+
17
+ describe '#delete' do
18
+ it 'raises NotImplementedError' do
19
+ expect { subject.delete('test') }.to raise_error(NotImplementedError)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,94 @@
1
+ describe AmaLayout::Notifications::RedisStore do
2
+ subject do
3
+ described_class.new(
4
+ db: 4,
5
+ namespace: 'test_notifications',
6
+ host: 'localhost'
7
+ )
8
+ end
9
+
10
+ around(:each) do |example|
11
+ subject.clear
12
+ example.run
13
+ subject.clear
14
+ end
15
+
16
+ describe '#get' do
17
+ context 'when a key is not present' do
18
+ it 'returns nil' do
19
+ expect(subject.get('missing')).to be nil
20
+ end
21
+ end
22
+
23
+ context 'when a key is present' do
24
+ before(:each) do
25
+ subject.set('key', 'value')
26
+ end
27
+
28
+ it 'returns the value' do
29
+ expect(subject.get('key')).to eq('value')
30
+ end
31
+ end
32
+
33
+ context 'with a default value' do
34
+ it 'sets a nil key to the default value' do
35
+ subject.get('missing', default: 'test')
36
+ expect(subject.get('missing')).to eq('test')
37
+ end
38
+ end
39
+ end
40
+
41
+ describe '#set' do
42
+ it 'sets the value for a given key' do
43
+ subject.set('test', 'value')
44
+ expect(subject.get('test')).to eq('value')
45
+ end
46
+
47
+ it 'returns true' do
48
+ expect(subject.set('test', 'value')).to be true
49
+ end
50
+ end
51
+
52
+ describe '#delete' do
53
+ context 'when a value is deleted' do
54
+ before(:each) do
55
+ subject.set('key', 'value')
56
+ end
57
+
58
+ it 'returns true' do
59
+ expect(subject.delete('key')).to be true
60
+ end
61
+
62
+ it 'deletes the key' do
63
+ subject.delete('key')
64
+ expect(subject.get('key')).to be nil
65
+ end
66
+ end
67
+
68
+ context 'when a value is not deleted' do
69
+ it 'returns false' do
70
+ expect(subject.delete('key')).to be false
71
+ end
72
+ end
73
+ end
74
+
75
+ describe '#transaction' do
76
+ it 'does not commit if an exception is raised' do
77
+ begin
78
+ subject.transaction do |store|
79
+ store.set('key', 'value')
80
+ raise StandardError
81
+ end
82
+ rescue StandardError
83
+ end
84
+ expect(subject.get('key')).to be nil
85
+ end
86
+
87
+ it 'commits to redis successfully' do
88
+ subject.transaction do |store|
89
+ store.set('key', 'value')
90
+ end
91
+ expect(subject.get('key')).to eq('value')
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,109 @@
1
+ describe AmaLayout::Notifications do
2
+ let(:store) do
3
+ AmaLayout::Notifications::RedisStore.new(
4
+ db: 4,
5
+ namespace: 'test_notifications',
6
+ host: 'localhost'
7
+ )
8
+ end
9
+ let(:json) do
10
+ <<-JSON
11
+ {
12
+ "8ca9f850c18acc17643038b2341bee3ede8a24c0f3e92f56f2109ce49fdcb616": {
13
+ "type": "notice",
14
+ "header": "test",
15
+ "content": "test",
16
+ "created_at": "2017-06-19T06:00:00.000Z",
17
+ "active": true,
18
+ "lifespan": 31557600,
19
+ "version": "1.0.0"
20
+ }
21
+ }
22
+ JSON
23
+ end
24
+
25
+ around(:each) do |example|
26
+ Timecop.freeze(Time.zone.local(2017, 6, 19)) do
27
+ store.clear
28
+ example.run
29
+ store.clear
30
+ end
31
+ end
32
+
33
+ context 'when including module' do
34
+ let(:klass) { Class.new.include(described_class) }
35
+
36
+ context 'class methods' do
37
+ before(:each) do
38
+ klass.class_eval do
39
+ notification_store AmaLayout::Notifications::RedisStore.new(
40
+ db: 4,
41
+ namespace: 'test_notifications',
42
+ host: 'localhost'
43
+ )
44
+ notification_foreign_key :my_id
45
+
46
+ def my_id
47
+ @id ||= SecureRandom.uuid
48
+ end
49
+ end
50
+ end
51
+
52
+ describe '#_notification_foreign_key' do
53
+ it 'returns the id method as a proc' do
54
+ expect(klass._notification_foreign_key).to be_a(Proc)
55
+ end
56
+ end
57
+
58
+ describe '#_notification_store' do
59
+ it 'returns the set data store' do
60
+ expect(klass._notification_store).to be_an(AmaLayout::Notifications::RedisStore)
61
+ end
62
+ end
63
+ end
64
+
65
+ context 'instance methods' do
66
+ context 'with a valid notification store' do
67
+ subject { klass.new }
68
+
69
+ before(:each) do
70
+ klass.class_eval do
71
+ notification_store AmaLayout::Notifications::RedisStore.new(
72
+ db: 4,
73
+ namespace: 'test_notifications',
74
+ host: 'localhost'
75
+ )
76
+ notification_foreign_key :my_id
77
+
78
+ def my_id
79
+ @id ||= SecureRandom.uuid
80
+ end
81
+ end
82
+ end
83
+
84
+ describe '#notifications' do
85
+ before(:each) do
86
+ store.set(subject.my_id, json)
87
+ end
88
+
89
+ it 'fetches notifications from data store' do
90
+ expect(subject.notifications.size).to eq(1)
91
+ end
92
+ end
93
+
94
+ describe '#notifications=' do
95
+ it 'resets the notifications to nil' do
96
+ subject.notifications = nil
97
+ expect(subject.notifications).to be_empty
98
+ end
99
+ end
100
+ end
101
+
102
+ context 'with an undefined notification store' do
103
+ it 'raises InvalidNotificationStore' do
104
+ expect { klass.new.notifications }.to raise_error(AmaLayout::Notifications::InvalidNotificationStore)
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -1,15 +1,16 @@
1
- require "simplecov"
2
- require "factory_girl"
3
- require "ama_layout"
4
- require "pry"
5
- require "rspec/rails"
6
- require "combustion"
1
+ require 'simplecov'
2
+ require 'factory_girl'
3
+ require 'ama_layout'
4
+ require 'pry'
5
+ require 'rspec/rails'
6
+ require 'combustion'
7
+ require 'timecop'
7
8
 
8
- ENV["RAILS_ENV"] ||= "test"
9
+ ENV['RAILS_ENV'] ||= 'test'
9
10
 
10
11
  Combustion.initialize! :all
11
12
 
12
- Dir["./spec/support/**/*.rb"].sort.each { |file| require file }
13
+ Dir['./spec/support/**/*.rb'].sort.each { |file| require file }
13
14
 
14
15
  FactoryGirl.find_definitions
15
16
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ama_layout
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.2.0
4
+ version: 5.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael van den Beuken
@@ -18,7 +18,7 @@ authors:
18
18
  autorequire:
19
19
  bindir: bin
20
20
  cert_chain: []
21
- date: 2017-05-04 00:00:00.000000000 Z
21
+ date: 2017-06-22 00:00:00.000000000 Z
22
22
  dependencies:
23
23
  - !ruby/object:Gem::Dependency
24
24
  name: foundation-rails
@@ -90,6 +90,20 @@ dependencies:
90
90
  - - "~>"
91
91
  - !ruby/object:Gem::Version
92
92
  version: 3.0.1
93
+ - !ruby/object:Gem::Dependency
94
+ name: redis-rails
95
+ requirement: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ type: :runtime
101
+ prerelease: false
102
+ version_requirements: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
93
107
  - !ruby/object:Gem::Dependency
94
108
  name: bundler
95
109
  requirement: !ruby/object:Gem::Requirement
@@ -108,14 +122,14 @@ dependencies:
108
122
  name: rake
109
123
  requirement: !ruby/object:Gem::Requirement
110
124
  requirements:
111
- - - "~>"
125
+ - - ">="
112
126
  - !ruby/object:Gem::Version
113
127
  version: '11.0'
114
128
  type: :development
115
129
  prerelease: false
116
130
  version_requirements: !ruby/object:Gem::Requirement
117
131
  requirements:
118
- - - "~>"
132
+ - - ">="
119
133
  - !ruby/object:Gem::Version
120
134
  version: '11.0'
121
135
  - !ruby/object:Gem::Dependency
@@ -202,6 +216,20 @@ dependencies:
202
216
  - - ">="
203
217
  - !ruby/object:Gem::Version
204
218
  version: '0'
219
+ - !ruby/object:Gem::Dependency
220
+ name: timecop
221
+ requirement: !ruby/object:Gem::Requirement
222
+ requirements:
223
+ - - ">="
224
+ - !ruby/object:Gem::Version
225
+ version: '0'
226
+ type: :development
227
+ prerelease: false
228
+ version_requirements: !ruby/object:Gem::Requirement
229
+ requirements:
230
+ - - ">="
231
+ - !ruby/object:Gem::Version
232
+ version: '0'
205
233
  description: ".ama.ab.ca site layouts"
206
234
  email:
207
235
  - michael.beuken@gmail.com
@@ -278,6 +306,11 @@ files:
278
306
  - lib/ama_layout/navigation.rb
279
307
  - lib/ama_layout/navigation.yml
280
308
  - lib/ama_layout/navigation_item.rb
309
+ - lib/ama_layout/notification.rb
310
+ - lib/ama_layout/notification_set.rb
311
+ - lib/ama_layout/notifications.rb
312
+ - lib/ama_layout/notifications/abstract_store.rb
313
+ - lib/ama_layout/notifications/redis_store.rb
281
314
  - lib/ama_layout/version.rb
282
315
  - spec/ama_layout/breadcrumb_builder_spec.rb
283
316
  - spec/ama_layout/controllers/pages_controller_spec.rb
@@ -290,6 +323,11 @@ files:
290
323
  - spec/ama_layout/moneris_spec.rb
291
324
  - spec/ama_layout/navigation_item_spec.rb
292
325
  - spec/ama_layout/navigation_spec.rb
326
+ - spec/ama_layout/notification_set_spec.rb
327
+ - spec/ama_layout/notification_spec.rb
328
+ - spec/ama_layout/notifications/abstract_store_spec.rb
329
+ - spec/ama_layout/notifications/redis_store_spec.rb
330
+ - spec/ama_layout/notifications_spec.rb
293
331
  - spec/factories/navigation.rb
294
332
  - spec/factories/navigation_item.rb
295
333
  - spec/helpers/ama_layout_breadcrumb_helper_spec.rb
@@ -303,8 +341,7 @@ files:
303
341
  - spec/internal/log/.gitignore
304
342
  - spec/internal/public/favicon.ico
305
343
  - spec/spec_helper.rb
306
- - styles.scss
307
- homepage: ''
344
+ homepage: https://github.com/amaabca/ama_layout
308
345
  licenses:
309
346
  - MIT
310
347
  metadata: {}
@@ -324,7 +361,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
324
361
  version: '0'
325
362
  requirements: []
326
363
  rubyforge_project:
327
- rubygems_version: 2.5.1
364
+ rubygems_version: 2.6.12
328
365
  signing_key:
329
366
  specification_version: 4
330
367
  summary: ".ama.ab.ca site layouts"
@@ -340,6 +377,11 @@ test_files:
340
377
  - spec/ama_layout/moneris_spec.rb
341
378
  - spec/ama_layout/navigation_item_spec.rb
342
379
  - spec/ama_layout/navigation_spec.rb
380
+ - spec/ama_layout/notification_set_spec.rb
381
+ - spec/ama_layout/notification_spec.rb
382
+ - spec/ama_layout/notifications/abstract_store_spec.rb
383
+ - spec/ama_layout/notifications/redis_store_spec.rb
384
+ - spec/ama_layout/notifications_spec.rb
343
385
  - spec/factories/navigation.rb
344
386
  - spec/factories/navigation_item.rb
345
387
  - spec/helpers/ama_layout_breadcrumb_helper_spec.rb
File without changes