ama_layout 5.2.0 → 5.4.0

Sign up to get free protection for your applications and to get access to all the features.
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