osm 0.0.11 → 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,32 @@
1
+ ## Version 0.0.12
2
+
3
+ * EveningActivity class renamed to Evening::Activity
4
+ * Change of method return types
5
+ * Activity.badges now returns an array of Osm::Activity::Badge objects
6
+ * Activity.files now returns an array of Osm::Activity::File objects
7
+ * Activity.versions now returns an array of Osm::Activity::Version objects
8
+ * Section.flexi_records now returns an array of Osm::Section::FlexiRecord objects
9
+ * Api.get\_register\_structure now returns an array of RegisterField objects
10
+ * Api.get\_register becomes Api.get\_register\_data and now returns an array of RegisterData objects
11
+ * Attribute name changes:
12
+ * Activity::Badge.section becomes section\_type
13
+ * Activity::File.file\_id becomes id
14
+ * Section.extra\_records becomes flexi\_records
15
+ * Member.joined\_in\_years attribute becomes joining\_in\_years
16
+ * from\_api method added to:
17
+ * Activity and sub classes
18
+ * ApiAccess
19
+ * DueBadges
20
+ * Evening and Evening::Activity
21
+ * Event
22
+ * Grouping
23
+ * Member
24
+ * RegisterData
25
+ * RegisterField
26
+ * Role
27
+ * Section
28
+ * Term
29
+
1
30
  ## Version 0.0.11
2
31
 
3
32
  * Fix undefined variable in id\_for\_term
data/lib/osm.rb CHANGED
@@ -27,7 +27,7 @@ module Osm
27
27
  end
28
28
  end
29
29
 
30
- raise Error.new('There is no current term for the section.')
30
+ raise Error, 'There is no current term for the section.'
31
31
  end
32
32
 
33
33
  def self.make_datetime(date, time)
@@ -69,7 +69,7 @@ module Osm
69
69
  end
70
70
 
71
71
  def self.symbolize_hash(hash_in)
72
- raise ArgumentError.new('You did not pass in a hash') unless hash_in.is_a?(Hash)
72
+ raise ArgumentError, 'You did not pass in a hash' unless hash_in.is_a?(Hash)
73
73
 
74
74
  hash_out = {}
75
75
  hash_in.each do |key, value|
@@ -78,4 +78,12 @@ module Osm
78
78
  hash_out
79
79
  end
80
80
 
81
+ def self.is_array_of?(ar, ty)
82
+ return false unless ar.is_a?(Array)
83
+ ar.each do |it|
84
+ return false unless it.is_a?(ty)
85
+ end
86
+ return true
87
+ end
88
+
81
89
  end
data/lib/osm/activity.rb CHANGED
@@ -34,53 +34,209 @@ module Osm
34
34
  # @!attribute [r] used
35
35
  # @return [Fixnum] How many times this activity has been used (total accross all of OSM)
36
36
  # @!attribute [r] versions
37
- # @return [Array<Hash>] ? (:value - version, :firstname - created by, :label - label, :user_id - OSM user ID of creator)
37
+ # @return [Array<Osm::Activity::Version>]
38
38
  # @!attribute [r] sections
39
39
  # @return [Array<Symbol>] the sections the activity is appropriate for
40
40
  # @!attribute [r] tags
41
41
  # @return [Array<String>] the tags attached to the activity
42
42
  # @!attribute [r] files
43
- # @return [Array<Hash> ? ('fileid', 'filename', 'name')
43
+ # @return [Array<Osm::Activity::File>
44
44
  # @!attribute [r] badges
45
- # @return [Array<Hash> ? ('section', 'badgetype', 'badge', 'columnname', 'label')
46
-
47
-
48
- # Initialize a new Activity using the hash returned by the API call
49
- # @param data the hash of data for the object returned by the API
50
- def initialize(data)
51
- @id = data['details']['activityid'].to_i
52
- @version = data['details']['version'].to_i
53
- @group_id = data['details']['groupid'].to_i
54
- @user_id = data['details']['userid'].to_i
55
- @title = data['details']['title']
56
- @description = data['details']['description']
57
- @resources = data['details']['resources']
58
- @instructions = data['details']['instructions']
59
- @running_time = data['details']['runningtime'].to_i
60
- @location = data['details']['location'].to_sym
61
- @shared = data['details']['shared'].to_i
62
- @rating = data['details']['rating'].to_i
63
- @editable = data['editable']
64
- @deletable = data['deletable'] ? true : false
65
- @used = data['used'].to_i
66
- @versions = data['versions']
67
- @sections = data['sections'].is_a?(Array) ? Osm::make_array_of_symbols(data['sections']) : []
68
- @tags = data['tags'].is_a?(Array) ? data['tags'] : []
69
- @files = data['files'].is_a?(Array) ? data['files'] : []
70
- @badges = data['badges'].is_a?(Array) ? data['badges'] : []
71
-
72
- # Clean versions hashes
73
- @versions.each do |version|
74
- version.keys.each do |key|
75
- version[(key.to_sym rescue key) || key] = version.delete(key)
76
- end
77
- version[:value] = version[:value].to_i
78
- version[:user_id] = version[:userid].to_i
79
- version.delete(:userid)
80
- version[:selected] = (version[:selected] == 'selected')
45
+ # @return [Array<Osm::Activity::Badge>
46
+
47
+
48
+ # Initialize a new Activity
49
+ # @param [Hash] attributes the hash of attributes (see attributes for descriptions, use Symbol of attribute name as the key)
50
+ def initialize(attributes={})
51
+ [:id, :group_id, :user_id].each do |attribute|
52
+ it = attributes[attribute]
53
+ raise ArgumentError, ":#{attribute} must be nil or a Fixnum > 0" unless it.nil? || (it.is_a?(Fixnum) && it > 0)
54
+ end
55
+ [:version, :running_time, :shared].each do |attribute|
56
+ it = attributes[attribute]
57
+ raise ArgumentError, ":#{attribute} must be nil or a Fixnum >= 0" unless it.nil? || (it.is_a?(Fixnum) && it >= 0)
58
+ end
59
+
60
+ [:title, :description, :resources, :instructions].each do |attribute|
61
+ it = attributes[attribute]
62
+ raise ArgumentError, ":#{attribute} must be nil or a String" unless it.nil? || it.is_a?(String)
63
+ end
64
+
65
+ raise ArgumentError, ':location must be either :indoors, :outdoors or :both' unless [:indoors, :outdoors, :both].include?(attributes[:location])
66
+
67
+ raise ArgumentError, ':editable must be a Boolean' unless attributes[:editable].is_a?(TrueClass) || attributes[:editable].is_a?(FalseClass)
68
+ raise ArgumentError, ':deletable must be a Boolean' unless attributes[:deletable].is_a?(TrueClass) || attributes[:deletable].is_a?(FalseClass)
69
+
70
+ raise ArgumentError, ':rating must be a FixNum' unless attributes[:rating].is_a?(Fixnum)
71
+ raise ArgumentError, ':used must be a FixNum' unless attributes[:used].is_a?(Fixnum)
72
+
73
+ raise ArgumentError, ':sections must be an Array of Symbol' unless Osm::is_array_of?(attributes[:sections], Symbol)
74
+ raise ArgumentError, ':tags must be an Array of String' unless Osm::is_array_of?(attributes[:tags], String)
75
+ raise ArgumentError, ':versions must be an Array of Osm::Activity::Version' unless Osm::is_array_of?(attributes[:versions], Osm::Activity::Version)
76
+ raise ArgumentError, ':files must be an Array of Osm::Activity::File' unless Osm::is_array_of?(attributes[:files], Osm::Activity::File)
77
+ raise ArgumentError, ':badges must be an Array of Osm::Activity::Badge' unless Osm::is_array_of?(attributes[:badges], Osm::Activity::Badge)
78
+
79
+ attributes.each { |k,v| instance_variable_set("@#{k}", v) }
80
+ end
81
+
82
+ # Initialize a new Activity from api data
83
+ # @param [Hash] data the hash of data provided by the API
84
+ def self.from_api(data)
85
+ attributes = {}
86
+ attributes[:id] = Osm::to_i_or_nil(data['details']['activityid'])
87
+ attributes[:version] = data['details']['version'].to_i
88
+ attributes[:group_id] = Osm::to_i_or_nil(data['details']['groupid'])
89
+ attributes[:user_id] = Osm::to_i_or_nil(data['details']['userid'])
90
+ attributes[:title] = data['details']['title']
91
+ attributes[:description] = data['details']['description']
92
+ attributes[:resources] = data['details']['resources']
93
+ attributes[:instructions] = data['details']['instructions']
94
+ attributes[:running_time] = Osm::to_i_or_nil(data['details']['runningtime'])
95
+ attributes[:location] = data['details']['location'].to_sym
96
+ attributes[:shared] = Osm::to_i_or_nil(data['details']['shared'])
97
+ attributes[:rating] = data['details']['rating'].to_i
98
+ attributes[:editable] = data['editable']
99
+ attributes[:deletable] = data['deletable'] ? true : false
100
+ attributes[:used] = data['used'].to_i
101
+ attributes[:sections] = data['sections'].is_a?(Array) ? Osm::make_array_of_symbols(data['sections']) : []
102
+ attributes[:tags] = data['tags'].is_a?(Array) ? data['tags'] : []
103
+ attributes[:versions] = []
104
+ attributes[:files] = []
105
+ attributes[:badges] = []
106
+
107
+ # Populate Arrays
108
+ (data['files'].is_a?(Array) ? data['files'] : []).each do |file_data|
109
+ attributes[:files].push File.from_api(file_data)
110
+ end
111
+ (data['badges'].is_a?(Array) ? data['badges'] : []).each do |badge_data|
112
+ attributes[:badges].push Badge.from_api(badge_data)
113
+ end
114
+ (data['versions'].is_a?(Array) ? data['versions'] : []).each do |version_data|
115
+ attributes[:versions].push Version.from_api(version_data)
81
116
  end
117
+
118
+ return new(attributes)
82
119
  end
83
120
 
84
- end
85
121
 
86
- end
122
+ private
123
+ class File
124
+ attr_reader :id, :activity_id, :file_name, :name
125
+ # @!attribute [r] id
126
+ # @return [Fixnum] the OSM ID for the file
127
+ # @!attribute [r] activity_id
128
+ # @return [Fixnum] the OSM ID for the activity
129
+ # @!attribute [r] file_name
130
+ # @return [String] the file name of the file
131
+ # @!attribute [r] name
132
+ # @return [String] the name of the file (more human readable than file_name)
133
+
134
+ # Initialize a new File
135
+ # @param [Hash] attributes the hash of attributes (see attributes for descriptions, use Symbol of attribute name as the key)
136
+ def initialize(attributes={})
137
+ [:file_name, :name].each do |attribute|
138
+ raise ArgumentError, ":#{attribute} must a String" unless attributes[attribute].is_a?(String)
139
+ end
140
+ [:id, :activity_id].each do |attribute|
141
+ it = attributes[attribute]
142
+ raise ArgumentError, ":#{attribute} must be nil or a Fixnum > 0" unless it.nil? || (it.is_a?(Fixnum) && it > 0)
143
+ end
144
+
145
+ attributes.each { |k,v| instance_variable_set("@#{k}", v) }
146
+ end
147
+
148
+ # Initialize a new File from api data
149
+ # @param [Hash] data the hash of data provided by the API
150
+ def self.from_api(data)
151
+ return new({
152
+ :id => Osm::to_i_or_nil(data['fileid']),
153
+ :activity_id => Osm::to_i_or_nil(data['activityid']),
154
+ :file_name => data['filename'],
155
+ :name => data['name']
156
+ })
157
+ end
158
+
159
+ end # Activity::File
160
+
161
+ class Badge
162
+ attr_reader :activity_id, :section_type, :type, :badge, :requirement, :label
163
+ # @!attribute [r] activity_id
164
+ # @return [Fixnum] the activity being done
165
+ # @!attribute [r] section_type
166
+ # @return [Symbol] the section the badge 'belongs' to
167
+ # @!attribute [r] type
168
+ # @return [Symbol] the type of badge
169
+ # @!attribute [r] badge
170
+ # @return [String] short name of the badge
171
+ # @!attribute [r] requirement
172
+ # @return [String] OSM reference to this badge requirement
173
+ # @!attribute [r] label
174
+ # @return [String] human readable label for the requirement
175
+
176
+ # Initialize a new Badge
177
+ # @param [Hash] attributes the hash of attributes (see attributes for descriptions, use Symbol of attribute name as the key)
178
+ def initialize(attributes={})
179
+ raise ArgumentError, ':activity_id must be nil or a Fixnum > 0' unless attributes[:activity_id].nil? || (attributes[:activity_id].is_a?(Fixnum) && attributes[:activity_id] > 0)
180
+ [:type, :section_type].each do |attribute|
181
+ raise ArgumentError, ":#{attribute} must be a Symbol" unless attributes[attribute].is_a?(Symbol)
182
+ end
183
+ [:label, :requirement, :badge].each do |attribute|
184
+ raise ArgumentError, ":#{attribute} must a String" unless attributes[attribute].is_a?(String)
185
+ end
186
+
187
+ attributes.each { |k,v| instance_variable_set("@#{k}", v) }
188
+ end
189
+
190
+ # Initialize a new Badge from api data
191
+ # @param [Hash] data the hash of data provided by the API
192
+ def self.from_api(data)
193
+ return new({
194
+ :activity_id => Osm::to_i_or_nil(data['activityid']),
195
+ :section_type => data['section'].to_sym,
196
+ :type => data['badgetype'].to_sym,
197
+ :badge => data['badge'],
198
+ :requirement => data['columnname'],
199
+ :label => data['label']
200
+ })
201
+ end
202
+
203
+ end # Activity::Badge
204
+
205
+ class Version
206
+ attr_reader :version, :created_by, :created_by_name, :label
207
+ # @!attribute [r] version
208
+ # @return [Fixnum] the version of the activity
209
+ # @!attribute [r] created_by
210
+ # @return [Fixnum] the OSM user ID of the person who created this version
211
+ # @!attribute [r] created_by_name
212
+ # @return [String] the aname of the OSM user who created this version
213
+ # @!attribute [r] label
214
+ # @return [String] the human readable label to use for this version
215
+
216
+ # Initialize a new Version
217
+ # @param [Hash] attributes the hash of attributes (see attributes for descriptions, use Symbol of attribute name as the key)
218
+ def initialize(attributes={})
219
+ raise ArgumentError, ':version must be nil or a Fixnum > 0' unless attributes[:version].nil? || (attributes[:version].is_a?(Fixnum) && attributes[:version] >= 0)
220
+ raise ArgumentError, ':created_by must be nil or a Fixnum >= 0' unless attributes[:created_by].nil? || (attributes[:created_by].is_a?(Fixnum) && attributes[:created_by] > 0)
221
+ raise ArgumentError, ':created_by_name must be nil or a String' unless attributes[:created_by_name].nil? || attributes[:created_by_name].is_a?(String)
222
+ raise ArgumentError, ':label must be nil or a String' unless attributes[:label].nil? || attributes[:label].is_a?(String)
223
+
224
+ attributes.each { |k,v| instance_variable_set("@#{k}", v) }
225
+ end
226
+
227
+ # Initialize a new Version from api data
228
+ # @param [Hash] data the hash of data provided by the API
229
+ def self.from_api(data)
230
+ return new({
231
+ :version => Osm::to_i_or_nil(data['value']),
232
+ :created_by => Osm::to_i_or_nil(data['userid']),
233
+ :created_by_name => data['firstname'],
234
+ :label => data['label']
235
+ })
236
+ end
237
+
238
+ end # Activity::Version
239
+
240
+ end # Activity
241
+
242
+ end # Module
data/lib/osm/api.rb CHANGED
@@ -113,7 +113,7 @@ module Osm
113
113
 
114
114
  result = Array.new
115
115
  data.each do |item|
116
- role = Osm::Role.new(item)
116
+ role = Osm::Role.from_api(item)
117
117
  result.push role
118
118
  cache_write("section-#{role.section.id}", role.section, :expires_in => @@default_cache_ttl*2)
119
119
  self.user_can_access :section, role.section.id, api_data
@@ -205,7 +205,7 @@ module Osm
205
205
 
206
206
  result = Array.new
207
207
  data['patrols'].each do |item|
208
- grouping = Osm::Grouping.new(item)
208
+ grouping = Osm::Grouping.from_api(item)
209
209
  result.push grouping
210
210
  cache_write("grouping-#{grouping.id}", grouping, :expires_in => @@default_cache_ttl*2)
211
211
  self.user_can_access :grouping, grouping.id, api_data
@@ -229,7 +229,7 @@ module Osm
229
229
  result = Array.new
230
230
  data.each_key do |key|
231
231
  data[key].each do |item|
232
- term = Osm::Term.new(item)
232
+ term = Osm::Term.from_api(item)
233
233
  result.push term
234
234
  cache_write("term-#{term.id}", term, :expires_in => @@default_cache_ttl*2)
235
235
  self.user_can_access :term, term.id, api_data
@@ -284,7 +284,7 @@ module Osm
284
284
  activities = data['activities'] || {}
285
285
 
286
286
  items.each do |item|
287
- evening = Osm::Evening.new(item, activities[item['eveningid']])
287
+ evening = Osm::Evening.from_api(item, activities[item['eveningid']])
288
288
  result.push evening
289
289
  evening.activities.each do |activity|
290
290
  self.user_can_access :activity, activity.activity_id, api_data
@@ -313,7 +313,7 @@ module Osm
313
313
  data = perform_query("programme.php?action=getActivity&id=#{activity_id}&version=#{version}", api_data)
314
314
  end
315
315
 
316
- activity = Osm::Activity.new(data)
316
+ activity = Osm::Activity.from_api(data)
317
317
  cache_write("activity-#{activity_id}-#{nil}", activity, :expires_in => @@default_cache_ttl*2) if version.nil?
318
318
  cache_write("activity-#{activity_id}-#{activity.version}", activity, :expires_in => @@default_cache_ttl/2)
319
319
  self.user_can_access :activity, activity.id, api_data
@@ -339,7 +339,7 @@ module Osm
339
339
 
340
340
  result = Array.new
341
341
  data['items'].each do |item|
342
- result.push Osm::Member.new(item)
342
+ result.push Osm::Member.from_api(item)
343
343
  end
344
344
  self.user_can_access :member, section_id, api_data
345
345
  cache_write("members-#{section_id}-#{term_id}", result, :expires_in => @@default_cache_ttl)
@@ -363,7 +363,7 @@ module Osm
363
363
 
364
364
  result = Array.new
365
365
  data['apis'].each do |item|
366
- this_item = Osm::ApiAccess.new(item)
366
+ this_item = Osm::ApiAccess.from_api(item)
367
367
  result.push this_item
368
368
  self.user_can_access(:programme, section_id, api_data) if this_item.can_read?(:programme)
369
369
  self.user_can_access(:member, section_id, api_data) if this_item.can_read?(:member)
@@ -412,7 +412,7 @@ module Osm
412
412
  result = Array.new
413
413
  unless data['items'].nil?
414
414
  data['items'].each do |item|
415
- result.push Osm::Event.new(item)
415
+ result.push Osm::Event.from_api(item)
416
416
  end
417
417
  end
418
418
  self.user_can_access :programme, section_id, api_data
@@ -437,7 +437,7 @@ module Osm
437
437
  section_type = get_section(section_id, api_data).type.to_s
438
438
  data = perform_query("challenges.php?action=outstandingBadges&section=#{section_type}&sectionid=#{section_id}&termid=#{term_id}", api_data)
439
439
 
440
- data = Osm::DueBadges.new(data)
440
+ data = Osm::DueBadges.from_api(data)
441
441
  self.user_can_access :badge, section_id, api_data
442
442
  cache_write("due_badges-#{section_id}-#{term_id}", data, :expires_in => @@default_cache_ttl*2)
443
443
 
@@ -449,7 +449,7 @@ module Osm
449
449
  # @param [Osm:Term, Fixnum] section the term (or its ID) to get the structure for, passing nil causes the current term to be used
450
450
  # @!macro options_get
451
451
  # @!macro options_api_data
452
- # @return [Array<Hash>] representing the rows of the register
452
+ # @return [Array<Osm::RegisterField>] representing the fields of the register
453
453
  def get_register_structure(section, term=nil, options={}, api_data={})
454
454
  section_id = id_for_section(section)
455
455
  term_id = id_for_term(term, section, api_data)
@@ -460,25 +460,25 @@ module Osm
460
460
 
461
461
  data = perform_query("users.php?action=registerStructure&sectionid=#{section_id}&termid=#{term_id}", api_data)
462
462
 
463
- data.each_with_index do |item, item_index|
464
- data[item_index] = item = Osm::symbolize_hash(item)
465
- item[:rows].each_with_index do |row, row_index|
466
- item[:rows][row_index] = row = Osm::symbolize_hash(row)
463
+ structure = []
464
+ data.each do |item|
465
+ item['rows'].each do |row|
466
+ structure.push Osm::RegisterField.from_api(row)
467
467
  end
468
468
  end
469
469
  self.user_can_access :register, section_id, api_data
470
- cache_write("register_structure-#{section_id}-#{term_id}", data, :expires_in => @@default_cache_ttl/2)
470
+ cache_write("register_structure-#{section_id}-#{term_id}", structure, :expires_in => @@default_cache_ttl/2)
471
471
 
472
- return data
472
+ return structure
473
473
  end
474
474
 
475
- # Get register
475
+ # Get register data
476
476
  # @param [Osm:Section, Fixnum] section the section (or its ID) to get the register for
477
477
  # @param [Osm:Term, Fixnum] section the term (or its ID) to get the register for, passing nil causes the current term to be used
478
478
  # @!macro options_get
479
479
  # @!macro options_api_data
480
- # @return [Array<Hash>] representing the attendance of each member
481
- def get_register(section, term=nil, options={}, api_data={})
480
+ # @return [Array<RegisterData>] representing the attendance of each member
481
+ def get_register_data(section, term=nil, options={}, api_data={})
482
482
  section_id = id_for_section(section)
483
483
  term_id = id_for_term(term, section, api_data)
484
484
 
@@ -490,10 +490,7 @@ module Osm
490
490
 
491
491
  data = data['items']
492
492
  data.each do |item|
493
- item = Osm::symbolize_hash(item)
494
- item[:scoutid] = item[:scoutid].to_i
495
- item[:sectionid] = item[:sectionid].to_i
496
- item[:patrolid] = item[:patrolid].to_i
493
+ item = Osm::RegisterData.from_api(item)
497
494
  end
498
495
  self.user_can_access :register, section_id, api_data
499
496
  cache_write("register-#{section_id}-#{term_id}", data, :expires_in => @@default_cache_ttl/2)
@@ -528,7 +525,7 @@ module Osm
528
525
  # @!macro options_api_data
529
526
  # @return [Boolean] if the operation suceeded or not
530
527
  def update_evening(evening, api_data={})
531
- response = perform_query("programme.php?action=editEvening", api_data.merge(evening.data_for_saving))
528
+ response = perform_query("programme.php?action=editEvening", api_data.merge(evening.to_api))
532
529
 
533
530
  # The cached programmes for the section will be out of date - remove them
534
531
  get_terms(api_data).each do |term|
@@ -601,14 +598,14 @@ module Osm
601
598
  begin
602
599
  result = HTTParty.post("#{@base_url}/#{url}", {:body => api_data})
603
600
  rescue SocketError, TimeoutError, OpenSSL::SSL::SSLError
604
- raise ConnectionError.new('A problem occured on the internet.')
601
+ raise ConnectionError, 'A problem occured on the internet.'
605
602
  end
606
- raise ConnectionError.new("HTTP Status code was #{result.response.code}") if !result.response.code.eql?('200')
603
+ raise ConnectionError, "HTTP Status code was #{result.response.code}" if !result.response.code.eql?('200')
607
604
 
608
- raise Error.new(result.response.body) unless looks_like_json?(result.response.body)
605
+ raise Error, result.response.body unless looks_like_json?(result.response.body)
609
606
  decoded = ActiveSupport::JSON.decode(result.response.body)
610
607
  osm_error = get_osm_error(decoded)
611
- raise Error.new(osm_error) if osm_error
608
+ raise Error, osm_error if osm_error
612
609
  return decoded
613
610
  end
614
611
 
@@ -654,10 +651,10 @@ module Osm
654
651
  if value.is_a?(cl)
655
652
  value = value.send(id_method)
656
653
  else
657
- raise(ArgumentError, "Invalid type for #{error_name}") unless value.is_a?(Fixnum)
654
+ raise ArgumentError, "Invalid type for #{error_name}" unless value.is_a?(Fixnum)
658
655
  end
659
656
 
660
- raise(ArgumentError, "Invalid #{error_name} ID") unless value > 0
657
+ raise ArgumentError, "Invalid #{error_name} ID" unless value > 0
661
658
  return value
662
659
  end
663
660