osm 0.2.2 → 0.3.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.
@@ -1,3 +1,28 @@
1
+ ## Version 0.3.0
2
+
3
+ * Removal of DueBadges
4
+ * Removal of get\_badge\_stock method from sections
5
+ * Addition of Badges model:
6
+ * With get\_due\_badges(api, section, options={}) method
7
+ * With get\_stock(api, section, options={}) method
8
+ * With update\_stock(api, section, badge\_key, stock\_level) method
9
+ * Addition of Badge models:
10
+ * CoreBadge
11
+ * ChallengeBadge
12
+ * StagedBadge
13
+ * ActivityBadge
14
+ * All:
15
+ * Inherit from Badge (do not use this class directly)
16
+ * With get\_badges\_for\_section(api, section, options={}) method
17
+ * With get\_badge\_data\_for\_section(api, section, badge, term=nil, options={}) method
18
+ * Addition of Badge::Requirements class
19
+ * Addition of Badge::Data class
20
+ * With update(api) method
21
+ * With total\_gained method
22
+ * With sections\_gained method
23
+ * With gained\_in\_sections method
24
+ * FlexiRecord::Data now updates only changed fields
25
+
1
26
  ## Version 0.2.2
2
27
 
3
28
  * Add comparing and sorting (using <=>, <, <=, >, >= and between?) to each model
data/README.md CHANGED
@@ -24,7 +24,7 @@ Use the [Online Scout Manager](https://www.onlinescoutmanager.co.uk) API.
24
24
  Add to your Gemfile and run the `bundle` command to install it.
25
25
 
26
26
  ```ruby
27
- gem 'osm'
27
+ gem 'osm', '~> 0.3.0'
28
28
  ```
29
29
 
30
30
  Configure the gem during the initalization of the app (e.g. if using rails then config/initializers/osm.rb would look like):
@@ -41,7 +41,7 @@ ActionDispatch::Callbacks.to_prepare do
41
41
  },
42
42
  },
43
43
  :cache => {
44
- :cache => Rails.cache,
44
+ :cache => Rails.cache,
45
45
  },
46
46
  )
47
47
  end
@@ -53,7 +53,7 @@ end
53
53
  In order to use the OSM API you first need to authorize the api to be used by the user, to do this use the {Osm::Api#authorize} method to get a userid and secret.
54
54
 
55
55
  ```ruby
56
- Osm::Api.new.authorize(users_email_address, users_osm_password)
56
+ Osm::Api.authorize(users_email_address, users_osm_password)
57
57
  ```
58
58
 
59
59
  Once you have done this you should store the userid and secret somewhere, you can then create an {Osm::Api} object to start acting as the user.
@@ -77,7 +77,11 @@ however it should be noted that when the OSM API adds a feature it can be diffic
77
77
  * Activity
78
78
  * API Access
79
79
  * API Access for our app
80
- * Badge requirements for evening
80
+ * Badges (Silver required for activity, Bronze for core, challenge and staged):
81
+ * Which requirements each member has met
82
+ * Details for each badge
83
+ * Requirements for evening
84
+ * Badge stock levels
81
85
  * Due Badges
82
86
  * Evening
83
87
  * Event (Silver required)
@@ -101,6 +105,8 @@ however it should be noted that when the OSM API adds a feature it can be diffic
101
105
 
102
106
  ### Update
103
107
  * Activity
108
+ * Badges (Silver required for activity, Bronze for core, challenge and staged):
109
+ * Which requirements each member has met
104
110
  * Evening
105
111
  * Event (Silver required)
106
112
  * Event Attendance (Silver required)
@@ -114,6 +120,7 @@ however it should be noted that when the OSM API adds a feature it can be diffic
114
120
  ### Create
115
121
  * Evening
116
122
  * Event (Silver required)
123
+ * Event Column (Silver required)
117
124
  * Member
118
125
  * Flexi Record Column
119
126
 
@@ -130,13 +137,6 @@ however it should be noted that when the OSM API adds a feature it can be diffic
130
137
 
131
138
  ## Parts of the OSM API currently NOT supported (may not be an exhaustive list):
132
139
 
133
- * Badges (Silver required for activity, Bronze for core, challenge and staged):
134
- * Which requirements each member has met:
135
- * Retreive [issue 21]
136
- * Update [issue 22]
137
- * Retrieve details for each badge (stock, short column names etc.) [issue 20]
138
- * Update badge stock [issue 56]
139
- * Event - Create column (Silver required)
140
140
  * SMS:
141
141
  * Retrieval of delivery reports [issue 54]
142
142
  * Sending a message [issue 54]
data/lib/osm.rb CHANGED
@@ -3,6 +3,7 @@ require 'active_support'
3
3
  require 'active_model'
4
4
  require 'date'
5
5
  require 'httparty'
6
+ require 'dirty_hashy'
6
7
 
7
8
 
8
9
  module Osm
@@ -0,0 +1,357 @@
1
+ module Osm
2
+
3
+ class Badge < Osm::Model
4
+ class Requirement; end # Ensure the constant exists for the validators
5
+
6
+ # @!attribute [rw] name
7
+ # @return [String] the name of the badge
8
+ # @!attribute [rw] requirement_notes
9
+ # @return [String] a description of the badge
10
+ # @!attribute [rw] key
11
+ # @return [String] the key for the badge in OSM
12
+ # @!attribute [rw] sections_needed
13
+ # @return [Fixnum]
14
+ # @!attribute [rw] total_needed
15
+ # @return [Fixnum]
16
+ # @!attribute [rw] needed_from_section
17
+ # @return [Hash]
18
+ # @!attribute [rw] requirements
19
+ # @return [Array<Osm::Badge::Requirement>]
20
+
21
+ attribute :name, :type => String
22
+ attribute :requirement_notes, :type => String
23
+ attribute :osm_key, :type => String
24
+ attribute :sections_needed, :type => Integer
25
+ attribute :total_needed, :type => Integer
26
+ attribute :needed_from_section, :type => Object
27
+ attribute :requirements, :type => Object
28
+
29
+ attr_accessible :name, :requirement_notes, :osm_key, :sections_needed, :total_needed, :needed_from_section, :requirements
30
+
31
+ validates_numericality_of :sections_needed, :only_integer=>true, :greater_than_or_equal_to=>-1
32
+ validates_numericality_of :total_needed, :only_integer=>true, :greater_than_or_equal_to=>-1
33
+ validates_presence_of :name
34
+ validates_presence_of :requirement_notes
35
+ validates_presence_of :osm_key
36
+ validates :needed_from_section, :hash => {:key_type => String, :value_type => Fixnum}
37
+ validates :requirements, :array_of => {:item_type => Osm::Badge::Requirement, :item_valid => true}
38
+
39
+
40
+ # @!method initialize
41
+ # Initialize a new Badge
42
+ # @param [Hash] attributes The hash of attributes (see attributes for descriptions, use Symbol of attribute name as the key)
43
+
44
+
45
+ # Get badges
46
+ # @param [Osm::Api] api The api to use to make the request
47
+ # @param [Osm::Section, Fixnum, #to_i] section The section (or its ID) to get the due badges for
48
+ # @!macro options_get
49
+ # @return [Array<Osm::Badge>]
50
+ def self.get_badges_for_section(api, section, options={})
51
+ raise Error, 'This method must be called on one of the subclasses (CoreBadge, ChallengeBadge, StagedBadge or ActivityBadge)' if badge_type.nil?
52
+ require_ability_to(api, :read, :badge, section, options)
53
+ section = Osm::Section.get(api, section, options) unless section.is_a?(Osm::Section)
54
+ cache_key = ['badges', section.type, badge_type]
55
+
56
+ if !options[:no_cache] && Osm::Model.cache_exist?(api, cache_key)
57
+ return cache_read(api, cache_key)
58
+ end
59
+
60
+ term_id = Osm::Term.get_current_term_for_section(api, section, options).to_i
61
+ badges = []
62
+
63
+ data = api.perform_query("challenges.php?action=getInitialBadges&type=#{badge_type}&sectionid=#{section.id}&section=#{section.type}&termid=#{term_id}")
64
+ badge_order = data["badgeOrder"].to_s.split(',')
65
+ structures = data["structure"] || {}
66
+ details = data["details"] || {}
67
+ badge_order.each do |b|
68
+ structure = structures[b]
69
+ detail = details[b]
70
+ config = ActiveSupport::JSON.decode(detail['config'] || '{}')
71
+
72
+ badge = new(
73
+ :name => detail['name'],
74
+ :requirement_notes => detail['description'],
75
+ :osm_key => detail['shortname'],
76
+ :sections_needed => config['sectionsneeded'].to_i,
77
+ :total_needed => config['totalneeded'].to_i,
78
+ :needed_from_section => (config['sections'] || {}).inject({}) { |h,(k,v)| h[k] = v.to_i; h },
79
+ )
80
+
81
+ requirements = []
82
+ ((structure[1] || {})['rows'] || []).each do |r|
83
+ requirements.push Osm::Badge::Requirement.new(
84
+ :badge => badge,
85
+ :name => r['name'],
86
+ :description => r['tooltip'],
87
+ :field => r['field'],
88
+ :editable => r['editable'].eql?('true'),
89
+ )
90
+ end
91
+ badge.requirements = requirements
92
+
93
+ badges.push badge
94
+ end
95
+
96
+ cache_write(api, cache_key, badges)
97
+ return badges
98
+ end
99
+
100
+ # Get a list of badge requirements met by members
101
+ # @param [Osm::Api] api The api to use to make the request
102
+ # @param [Osm::Section, Fixnum, #to_i] section The section (or its ID) to get the due badges for
103
+ # @param [Osm::Badge] badge The badge to get data for
104
+ # @param [Osm::Term, Fixnum, #to_i, nil] term The term (or its ID) to get the due badges for, passing nil causes the current term to be used
105
+ # @!macro options_get
106
+ # @return [Array<Osm::Badge::Data>]
107
+ def self.get_badge_data_for_section(api, section, badge, term=nil, options={})
108
+ raise Error, 'This method must be called on one of the subclasses (CoreBadge, ChallengeBadge, StagedBadge or ActivityBadge)' if badge_type.nil?
109
+ Osm::Model.require_ability_to(api, :read, :badge, section, options)
110
+ section = Osm::Section.get(api, section, options) unless section.is_a?(Osm::Section)
111
+ term_id = (term.nil? ? Osm::Term.get_current_term_for_section(api, section, options) : term).to_i
112
+ cache_key = ['badge_data', section.id, term_id, badge.osm_key]
113
+
114
+ if !options[:no_cache] && cache_exist?(api, cache_key)
115
+ return cache_read(api, cache_key)
116
+ end
117
+
118
+ datas = []
119
+ data = api.perform_query("challenges.php?termid=#{term_id}&type=#{badge_type}&section=#{section.type}&c=#{badge.osm_key}&sectionid=#{section.id}")
120
+ data['items'].each do |d|
121
+ datas.push Osm::Badge::Data.new(
122
+ :member_id => d['scoutid'],
123
+ :completed => d['completed'].eql?('1'),
124
+ :awarded_date => Osm.parse_date(d['awardeddate']),
125
+ :requirements => d.select{ |k,v| k.include?('_') },
126
+ :section_id => section.id,
127
+ :badge => badge,
128
+ )
129
+ end
130
+
131
+ cache_write(api, cache_key, datas)
132
+ return datas
133
+ end
134
+
135
+ # Compare Badge based on name then osm_key
136
+ def <=>(another)
137
+ result = self.name <=> another.try(:name)
138
+ result = self.osm_key <=> another.try(:osm_key) if result == 0
139
+ return result
140
+ end
141
+
142
+
143
+ private
144
+ def self.badge_type
145
+ nil
146
+ end
147
+ def self.subscription_required
148
+ :bronze
149
+ end
150
+
151
+
152
+ class Requirement
153
+ include ::ActiveAttr::MassAssignmentSecurity
154
+ include ::ActiveAttr::Model
155
+
156
+ # @!attribute [rw] badge
157
+ # @return [Osm::Badge] the badge the requirement belongs to
158
+ # @!attribute [rw] name
159
+ # @return [String] the name of the badge
160
+ # @!attribute [rw] description
161
+ # @return [String] a description of the badge
162
+ # @!attribute [rw] field
163
+ # @return [String] the field for the requirement (passed to OSM)
164
+ # @!attribute [rw] editable
165
+ # @return [Boolean]
166
+
167
+ attribute :badge, :type => Object
168
+ attribute :name, :type => String
169
+ attribute :description, :type => String
170
+ attribute :field, :type => String
171
+ attribute :editable, :type => Boolean
172
+
173
+ attr_accessible :name, :description, :field, :editable, :badge
174
+
175
+ validates_presence_of :name
176
+ validates_presence_of :description
177
+ validates_presence_of :field
178
+ validates_presence_of :badge
179
+ validates_inclusion_of :editable, :in => [true, false]
180
+
181
+ # @!method initialize
182
+ # Initialize a new Badge
183
+ # @param [Hash] attributes The hash of attributes (see attributes for descriptions, use Symbol of attribute name as the key)
184
+
185
+ # Compare Badge::Requirement based on badge then field
186
+ def <=>(another)
187
+ result = self.badge <=> another.try(:badge)
188
+ result = self.field <=> another.try(:field) if result == 0
189
+ return result
190
+ end
191
+
192
+ end # Class Requirement
193
+
194
+
195
+ class Data
196
+ include ::ActiveAttr::MassAssignmentSecurity
197
+ include ::ActiveAttr::Model
198
+
199
+ # @!attribute [rw] member_id
200
+ # @return [Fixnum] ID of the member this data relates to
201
+ # @!attribute [rw] completed
202
+ # @return [Boolean] whether this badge has been completed (i.e. it is due?)
203
+ # @!attribute [rw] awarded_date
204
+ # @return [Date] when the badge was awarded
205
+ # @!attribute [rw] requirements
206
+ # @return [DirtyHashy] the data for each badge requirement
207
+ # @!attribute [rw] section_id
208
+ # @return [Fixnum] the ID of the section the member belongs to
209
+ # @!attribute [rw] badge
210
+ # @return [Osm::Badge] the badge that the data belongs to
211
+
212
+ attribute :member_id, :type => Integer
213
+ attribute :completed, :type => Boolean
214
+ attribute :awarded_date, :type => Date
215
+ attribute :requirements, :type => Object, :default => DirtyHashy.new
216
+ attribute :section_id, :type => Integer
217
+ attribute :badge, :type => Object
218
+
219
+ attr_accessible :member_id, :completed, :awarded_date, :requirements, :section_id, :badge
220
+
221
+ validates_presence_of :badge
222
+ validates_inclusion_of :completed, :in => [true, false]
223
+ validates_numericality_of :member_id, :only_integer=>true, :greater_than=>0
224
+ validates_numericality_of :section_id, :only_integer=>true, :greater_than=>0
225
+ validates :requirements, :hash => {:key_type => String, :value_type => String}
226
+
227
+ # @!method initialize
228
+ # Initialize a new Badge
229
+ # @param [Hash] attributes The hash of attributes (see attributes for descriptions, use Symbol of attribute name as the key)
230
+ # Override initialize to set @orig_attributes
231
+ old_initialize = instance_method(:initialize)
232
+ define_method :initialize do |*args|
233
+ ret_val = old_initialize.bind(self).call(*args)
234
+ self.requirements = DirtyHashy.new(self.requirements)
235
+ self.requirements.clean_up!
236
+ return ret_val
237
+ end
238
+
239
+
240
+ # Get the total number of gained requirements
241
+ # @return [Fixnum] the total number of requirements considered gained
242
+ def total_gained
243
+ count = 0
244
+ requirements.each do |field, data|
245
+ next if data.blank? || data.downcase[0].eql?('x')
246
+ count += 1
247
+ end
248
+ return count
249
+ end
250
+
251
+ # Get the total number of sections gained
252
+ # @return [Hash]
253
+ def sections_gained
254
+ required = badge.needed_from_section
255
+ gained = gained_in_sections
256
+ count = 0
257
+
258
+ required.each do |section, needed|
259
+ next if gained[section] >= needed
260
+ count += 1
261
+ end
262
+ return count
263
+ end
264
+
265
+ # Get the number of requirements gained in each section
266
+ # @return [Hash]
267
+ def gained_in_sections
268
+ count = {}
269
+ requirements.each do |field, data|
270
+ field = field.split('_')[0]
271
+ count[field] ||= 0
272
+ next if data.blank? || data.downcase[0].eql?('x')
273
+ count[field] += 1
274
+ end
275
+ return count
276
+ end
277
+
278
+ # Update data in OSM
279
+ # @param [Osm::Api] api The api to use to make the request
280
+ # @return [Boolean] whether the data was updated in OSM
281
+ # @raise [Osm::ObjectIsInvalid] If the Data is invalid
282
+ def update(api)
283
+ raise Osm::ObjectIsInvalid, 'data is invalid' unless valid?
284
+ section = Osm::Section.get(api, section_id)
285
+ Osm::Model.require_ability_to(api, :write, :badge, section)
286
+
287
+ updated = true
288
+ editable_fields = badge.requirements.select{ |r| r.editable }.map{ |r| r.field}
289
+ requirements.changes.each do |field, (was,now)|
290
+ if editable_fields.include?(field)
291
+ result = api.perform_query("challenges.php?type=#{badge.class.badge_type}&section=#{section.type}", {
292
+ 'action' => 'updatesingle',
293
+ 'id' => member_id,
294
+ 'col' => field,
295
+ 'value' => now,
296
+ 'chal' => badge.osm_key,
297
+ 'sectionid' => section_id,
298
+ })
299
+ updated = false unless result.is_a?(Hash) &&
300
+ (result['sid'].to_i == member_id) &&
301
+ (result[field] == now)
302
+ end
303
+ end
304
+
305
+
306
+ if updated
307
+ requirements.clean_up!
308
+ end
309
+
310
+ return updated
311
+ end
312
+
313
+ # Compare Badge::Data based on badge, section_id then member_id
314
+ def <=>(another)
315
+ result = self.badge <=> another.try(:badge)
316
+ result = self.section_id <=> another.try(:section_id) if result == 0
317
+ result = self.member_id <=> another.try(:member_id) if result == 0
318
+ return result
319
+ end
320
+
321
+ end # Class Data
322
+
323
+ end # Class Badge
324
+
325
+
326
+ class CoreBadge < Osm::Badge
327
+ private
328
+ def self.badge_type
329
+ :core
330
+ end
331
+ end # Class CoreBadge
332
+
333
+ class ChallengeBadge < Osm::Badge
334
+ private
335
+ def self.badge_type
336
+ :challenge
337
+ end
338
+ end # Class ChallengeBadge
339
+
340
+ class StagedBadge < Osm::Badge
341
+ private
342
+ def self.badge_type
343
+ :staged
344
+ end
345
+ end # Class StagedBadge
346
+
347
+ class ActivityBadge < Osm::Badge
348
+ private
349
+ def self.badge_type
350
+ :activity
351
+ end
352
+ def self.subscription_required
353
+ :silver
354
+ end
355
+ end # Class ActivityBadge
356
+
357
+ end # Module