osm 0.2.2 → 0.3.0

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