lib/osm/badge.rb in osm-1.2.15.dev.1 vs lib/osm/badge.rb in osm-1.2.15

- old
+ new

@@ -1,16 +1,19 @@ module Osm class Badge < Osm::Model class Requirement; end # Ensure the constant exists for the validators + class RequirementModule; end # Ensure the constant exists for the validators # @!attribute [rw] name # @return [String] the name of the badge # @!attribute [rw] requirement_notes # @return [String] a description of the badge # @!attribute [rw] requirements # @return [Array<Osm::Badge::Requirement>] the requirements of the badge + # @!attribute [rw] modules + # @return [Array<Hash>] Details of the modules which make up the badge # @!attribute [rw] id # @return [Fixnum] the badge's id in OSM # @!attribute [rw] version # @return [Fixnum] the version of the badge # @!attribute [rw] identifier @@ -23,10 +26,26 @@ # @return [Symbol] the sharing status of this badge (:draft, :private, :optin, :default_locked, :optin_locked) # @!attribute [rw] user_id # @return [Fixnum] the OSM user who created this (version of the) badge # @!attribute [rw] levels # @return [Array<Fixnum>, nil] the levels available, nil if it's a single level badge + # @!attribute [rw] min_modules_required + # @return [Fixnum] the minimum number of modules which must be completed to earn the badge + # @!attribute [rw] min_requirements_required + # @return [Fixnum] the minimum number of requirements which must be completed to earn the badge + # @!attribute [rw] add_columns_to_module + # @return [Fixnum, nil] the module to add columns to for nights away type badges + # @!attribute [rw] level_requirement + # @return [Fixnum, nil] the column which stores the currently earnt level of nights away type badges + # @!attribute [rw] requires_modules + # @return [Array<Array<String>>, nil] the module letters required to gain the badge, at least one from each inner Array + # @!attribute [rw] other_requirements_required + # @return [Array<Hash>] the requirements (from other badges) required to complete this badge, {id: field ID, min: the minimum numerical value of the field's data} + # @!attribute [rw] badges_required + # @return [Array<Hash>] the other badges required to complete this badge, {id: The ID of the badge, version: The version of the badge} + # @!attribute [rw] show_level_letters + # @return [Boolean] Whether to show letters not numbers for the levels of a staged badge attribute :name, :type => String attribute :requirement_notes, :type => String attribute :requirements, :type => Object attribute :id, :type => Integer @@ -35,26 +54,40 @@ attribute :group_name, :type => String attribute :latest, :type => Boolean attribute :sharing, :type => Object attribute :user_id, :type => Integer attribute :levels, :type => Object - attribute :completion_criteria, :type => Object + attribute :modules, :type => Object + attribute :min_modules_required, :type => Integer + attribute :min_requirements_required, :type => Integer + attribute :add_columns_to_module, :type => Integer + attribute :level_requirement, :type => Integer + attribute :requires_modules, :type => Object + attribute :other_requirements_required, :type => Object + attribute :badges_required, :type => Object + attribute :show_level_letters, :type => Boolean if ActiveModel::VERSION::MAJOR < 4 - attr_accessible :name, :requirement_notes, :requirements, :id, :version, :identifier, :group_name, :latest, :sharing, :user_id, :levels, :completion_criteria + attr_accessible :name, :requirement_notes, :requirements, :id, :version, :identifier, :group_name, :latest, :sharing, :user_id, :levels, :modules, :min_modules_required, :min_requirements_required, :add_columns_to_module, :level_requirement, :requires_modules, :other_requirements_required, :badges_required, :show_level_letters end validates_presence_of :name validates_presence_of :requirement_notes - validates_presence_of :id - validates_presence_of :version + validates_numericality_of :id, :only_integer=>true, :greater_than_or_equal_to=>1 + validates_numericality_of :version, :only_integer=>true, :greater_than_or_equal_to=>0 validates_presence_of :identifier validates_inclusion_of :sharing, :in => [:draft, :private, :optin, :optin_locked, :default_locked] validates_presence_of :user_id validates :requirements, :array_of => {:item_type => Osm::Badge::Requirement, :item_valid => true} + validates :modules, :array_of => {:item_type => Osm::Badge::RequirementModule, :item_valid => true} validates_inclusion_of :latest, :in => [true, false] validates :levels, :array_of => {:item_type => Fixnum}, :allow_nil => true + validates_numericality_of :min_modules_required, :only_integer=>true, :greater_than_or_equal_to=>0 + validates_numericality_of :min_requirements_required, :only_integer=>true, :greater_than_or_equal_to=>0 + validates_numericality_of :add_columns_to_module, :only_integer=>true, :greater_than=>0, :allow_nil=>true + validates_numericality_of :level_requirement, :only_integer=>true, :greater_than=>0, :allow_nil=>true + validates_inclusion_of :show_level_letters, :in => [true, false] # @!method initialize # Initialize a new Badge # @param [Hash] attributes The hash of attributes (see attributes for descriptions, use Symbol of attribute name as the key) @@ -105,35 +138,36 @@ :group_name => detail['group_name'], :latest => detail['latest'].to_i.eql?(1), :sharing => badge_sharing_map[detail['sharing']], :user_id => Osm.to_i_or_nil(detail['userid']), :levels => config['levelslist'], - :completion_criteria => { - :min_modules_required => config['numModulesRequired'].to_i, - :fields_required => (config['columnsRequired'] || []).map{ |i| {id: Osm.to_i_or_nil(i['id']), min: i['min'].to_i} }, - :badges_required => (config['badgesRequired'] || []).map{ |i| {id: Osm.to_i_or_nil(i['id']), version: i['version'].to_i} }, - :min_requirements_completed => config['minRequirementsCompleted'].to_i, - :requires => config['requires'], - :add_columns_to_module => Osm.to_i_or_nil(config['addcolumns']), - :levels_column => Osm.to_i_or_nil(config['levels_column_id']), - :show_letters => !!config['shownumbers'], - }, + :min_modules_required => config['numModulesRequired'].to_i, + :min_requirements_required => config['minRequirementsCompleted'].to_i, + :add_columns_to_module => Osm.to_i_or_nil(config['addcolumns']), + :level_requirement => Osm.to_i_or_nil(config['levels_column_id']), + :requires_modules => config['requires'], + :other_requirements_required => (config['columnsRequired'] || []).map{ |i| {id: Osm.to_i_or_nil(i['id']), min: i['min'].to_i} }, + :badges_required => (config['badgesRequired'] || []).map{ |i| {id: Osm.to_i_or_nil(i['id']), version: i['version'].to_i} }, + :show_level_letters => !!config['shownumbers'], ) + modules = module_completion_data(api, badge, options) + badge.modules = modules + modules = Hash[*modules.map{|m| [m.letter, m]}.flatten] + requirements = [] ((structure[1] || {})['rows'] || []).each do |r| requirements.push Osm::Badge::Requirement.new( :badge => badge, :name => r['name'], :description => r['tooltip'], - :module_letter => r['module'], - :field => Osm::to_i_or_nil(r['field']), + :mod => modules[r['module']], + :id => Osm::to_i_or_nil(r['field']), :editable => r['editable'].to_s.eql?('true'), ) end badge.requirements = requirements - badge.completion_criteria[:modules] = module_completion_data(api, badge, options) badges.push badge end cache_write(api, cache_key, badges) @@ -245,37 +279,37 @@ def has_levels? !levels.nil? end + def add_columns? + !add_columns_to_module.nil? + end + def module_map @module_map ||= Hash[ - *completion_criteria[:modules].map{ |m| - [m[:module_id], m[:module_letter], m[:module_letter], m[:module_id]] + *modules.map{ |m| + [m.id, m.letter, m.letter, m.id] }.flatten ].except('z') end def needed_per_module - @needed_per_module ||= Hash[*completion_criteria[:modules].map{ |m| - [m[:module_id], m[:min_required], m[:module_letter], m[:min_required]] + @needed_per_module ||= Hash[*modules.map{ |m| + [m.id, m.min_required, m.letter, m.min_required] }.flatten].except('z') end def module_letters - @module_letters ||= completion_criteria[:modules].map{ |m| m[:module_letter] }.sort + @module_letters ||= modules.map{ |m| m.letter }.sort end def module_ids - @module_ids ||= completion_criteria[:modules].map{ |m| m[:module_id] }.sort + @module_ids ||= modules.map{ |m| m.id }.sort end - def modules - completion_criteria[:modules] || [] - end - # Compare Badge based on name then id then version (desc) def <=>(another) result = self.name <=> another.try(:name) result = self.id <=> another.try(:id) if result == 0 result = another.try(:version) <=> self.version if result == 0 @@ -301,10 +335,12 @@ data = @module_completion_data[badge.id] fetched_this_time = true end data = data[badge.version] raise ArgumentError, "That badge does't exist (bad version)." if data.nil? + + data.each{ |i| i.badge = badge } return data end # Return a 2 dimensional hash/array (badge ID, badge version) of hashes representing the modules def self.get_module_completion_data(api, options={}) @@ -314,29 +350,30 @@ end osm_data = api.perform_query('ext/badges/records/?action=_getModuleDetails') osm_data = (osm_data || {})['items'] || [] osm_data.map! do |i| - { - badge_id: Osm.to_i_or_nil(i['badge_id']), - badge_version: Osm.to_i_or_nil(i['badge_version']), - module_id: Osm.to_i_or_nil(i['module_id']), - module_letter: i['module_letter'], - min_required: i['num_required'].to_i, - custom_columns: i['custom_columns'].to_i, - completed_into_column: i['completed_into_column_id'].to_i.eql?(0) ? nil : i['completed_into_column_id'].to_i, - numeric_into_column: i['numeric_into_column_id'].to_i.eql?(0) ? nil : i['numeric_into_column_id'].to_i, - add_column_id_to_numeric: i['add_column_id_to_numeric'].to_i.eql?(0) ? nil : i['add_column_id_to_numeric'].to_i, - } + [ + Osm.to_i_or_nil(i['badge_id']), + Osm.to_i_or_nil(i['badge_version']), + Osm::Badge::RequirementModule.new({ + id: Osm.to_i_or_nil(i['module_id']), + letter: i['module_letter'], + min_required: i['num_required'].to_i, + custom_columns: i['custom_columns'].to_i, + completed_into_column: i['completed_into_column_id'].to_i.eql?(0) ? nil : i['completed_into_column_id'].to_i, + numeric_into_column: i['numeric_into_column_id'].to_i.eql?(0) ? nil : i['numeric_into_column_id'].to_i, + add_column_id_to_numeric: i['add_column_id_to_numeric'].to_i.eql?(0) ? nil : i['add_column_id_to_numeric'].to_i, + }) + ] end data = {} - osm_data.each do |i| - id, version = i.values_at(:badge_id, :badge_version) + osm_data.each do |id, version, m| data[id] ||= [] data[id][version] ||= [] - data[id][version].push i + data[id][version].push m end cache_write(api, cache_key, data, {expires_in: 864000}) # Expire in 24 hours as this data changes really slowly return data end @@ -363,54 +400,118 @@ include ActiveAttr::Model # @!attribute [rw] badge # @return [Osm::Badge] the badge the requirement belongs to # @!attribute [rw] name - # @return [String] the name of the badge + # @return [String] the name of the badge requirement # @!attribute [rw] description - # @return [String] a description of the badge - # @!attribute [rw] field - # @return [Fixnum] the field for the requirement (passed to OSM) + # @return [String] a description of the badge requirement + # @!attribute [rw] id + # @return [Fixnum] the id for the requirement (passed to OSM) + # @!attribute [rw] mod + # @return [Osm::Badge::RequirementModule] the module the requirement belongs to # @!attribute [rw] editable # @return [Boolean] attribute :badge, :type => Object attribute :name, :type => String attribute :description, :type => String - attribute :module_letter, :type => String - attribute :field, :type => Integer + attribute :mod, :type => Object + attribute :id, :type => Integer attribute :editable, :type => Boolean if ActiveModel::VERSION::MAJOR < 4 - attr_accessible :name, :description, :field, :editable, :badge, :module_letter + attr_accessible :name, :description, :id, :editable, :badge, :mod end validates_presence_of :name validates_presence_of :description - validates_presence_of :module_letter - validates_presence_of :field + validates_presence_of :mod + validates_numericality_of :id, :only_integer=>true, :greater_than=>0 validates_presence_of :badge validates_inclusion_of :editable, :in => [true, false] # @!method initialize - # Initialize a new Badge + # Initialize a new Badge::Requirement # @param [Hash] attributes The hash of attributes (see attributes for descriptions, use Symbol of attribute name as the key) - # Compare Badge::Requirement based on badge then field + # Compare Badge::Requirement based on badge then requirement def <=>(another) result = self.badge <=> another.try(:badge) - result = self.field <=> another.try(:field) if result == 0 + result = self.id <=> another.try(:id) if result == 0 return result end def inspect - Osm.inspect_instance(self, options={:replace_with => {'badge' => :identifier}}) + Osm.inspect_instance(self, {:replace_with => {'badge' => :identifier}}) end end # Class Requirement + class RequirementModule + include ActiveModel::MassAssignmentSecurity if ActiveModel::VERSION::MAJOR < 4 + include ActiveAttr::Model + + # @!attribute [rw] badge + # @return [Osm::Badge] the badge the requirement module belongs to + # @!attribute [rw] letter + # @return [String] the letter of the module + # @!attribute [rw] id + # @return [Fixnum] the id for the module + # @!attribute [rw] min_required + # @return [Fixnum] the minimum number of requirements which must be met to achieve this module + # @!attribute [rw] custom_columns + # @return [Fixnum, nil] ? + # @!attribute [rw] completed_into_column + # @return [Fixnum, nil] ? + # @!attribute [rw] numeric_into_column + # @return [Fixnum, nil] ? + # @!attribute [rw] add_column_id_to_numeric + # @return [Fixnum, nil] ? + + attribute :badge, :type => Object + attribute :letter, :type => String + attribute :id, :type => Integer + attribute :min_required, :type => Integer + attribute :custom_columns, :type => Integer + attribute :completed_into_column, :type => Integer + attribute :numeric_into_column, :type => Integer + attribute :add_column_id_to_numeric, :type => Integer + + if ActiveModel::VERSION::MAJOR < 4 + attr_accessible :badge, :letter, :id, :min_required, :custom_columns, :completed_into_column, :numeric_into_column, :add_column_id_to_numeric + end + + validates_presence_of :badge + validates_presence_of :letter + validates_numericality_of :id, :only_integer=>true, :greater_than=>0 + validates_numericality_of :min_required, :only_integer=>true, :greater_than_or_equal_to=>0 + validates_numericality_of :custom_columns, :only_integer=>true, :greater_than_or_equal_to=>0, :allow_nil=>true + validates_numericality_of :completed_into_column, :only_integer=>true, :greater_than=>0, :allow_nil=>true + validates_numericality_of :numeric_into_column, :only_integer=>true, :greater_than=>0, :allow_nil=>true + validates_numericality_of :add_column_id_to_numeric, :only_integer=>true, :greater_than=>0, :allow_nil=>true + + # @!method initialize + # Initialize a new Badge::RequirementModule + # @param [Hash] attributes The hash of attributes (see attributes for descriptions, use Symbol of attribute name as the key) + + # Compare Badge::RequirementModule based on badge then letter + def <=>(another) + result = self.badge <=> another.try(:badge) + result = self.letter <=> another.try(:letter) if result == 0 + result = self.id <=> another.try(:id) if result == 0 + return result + end + + def inspect + Osm.inspect_instance(self, {:replace_with => {'badge' => :identifier}}) + end + + end # Class RequirementModule + + class Data < Osm::Model # @!attribute [rw] member_id # @return [Fixnum] ID of the member this data relates to # @!attribute [rw] first_name # @return [Fixnum] the member's first name @@ -452,11 +553,11 @@ validates_numericality_of :section_id, :only_integer=>true, :greater_than=>0 validates :requirements, :hash => {:key_type => Fixnum, :value_type => String} # @!method initialize - # Initialize a new Badge + # Initialize a new Badge::Data # @param [Hash] attributes The hash of attributes (see attributes for descriptions, use Symbol of attribute name as the key) # Override initialize to set @orig_attributes old_initialize = instance_method(:initialize) define_method :initialize do |*args| ret_val = old_initialize.bind(self).call(*args) @@ -469,42 +570,42 @@ # Get the total number of gained requirements # @return [Fixnum] the total number of requirements considered gained def total_gained count = 0 badge.requirements.each do |requirement| - next unless requirement_met?(requirement.field) + next unless requirement_met?(requirement.id) count += 1 end return count end - # Get the number of modules gained - # @return [Fixnum] + # Get the letters of modules gained + # @return [Array<Stirng>] def modules_gained - needed = badge.needed_per_module - modules = [] - - gained_in_modules.each do |module_id, gained| - next unless module_id.is_a?(Fixnum) - next if gained < needed[module_id] - module_letter = badge.module_map[module_id] - modules.push module_letter unless module_letter >= 'y' + g_i_m = gained_in_modules + gained = [] + badge.modules.each do |mod| + next if g_i_m[mod.id] < mod.min_required + gained.push mod.letter end - - return modules + gained end # Get the number of requirements gained in each module # @return [Hash] def gained_in_modules count = {} + badge.modules.each do |mod| + count[mod.id] ||= 0 + count[mod.letter] ||= 0 + end badge.requirements.each do |requirement| - count[requirement.module_letter] ||= 0 - next unless requirement_met?(requirement.field) - count[requirement.module_letter] += 1 + next unless requirement_met?(requirement.id) + count[requirement.mod.id] += 1 + count[requirement.mod.letter] += 1 end - Hash[*count.map{ |k,v| [badge.module_map[k], v, k, v] }.flatten] + count end # Check if this badge has been earnt # @return [Boolean] whether the badge has been earnt (ignores other badge's and their requirements which might be needed) @@ -513,35 +614,36 @@ return earnt > awarded else return false if (due.eql?(1) && awarded.eql?(1)) return true if (due.eql?(1) && awarded.eql?(0)) - criteria = badge.completion_criteria - earnt = true - if criteria[:min_modules_required] > 0 - earnt &= (modules_gained.size >= criteria[:min_modules_required]) + if badge.min_modules_required > 0 + return false unless modules_gained.size >= badge.min_modules_required end - if criteria[:min_requirements_completed] > 0 - earnt &= (total_gained >= criteria[:min_requirements_completed]) + if badge.min_requirements_required > 0 + return false unless total_gained >= badge.min_requirements_required end - if criteria[:requires] + if badge.requires_modules # [['a'], ['b', 'c']] = a and (b or c) - requires = criteria[:requires].clone + requires = badge.requires_modules.clone modules = modules_gained requires.map!{ |a| a.map{ |b| modules.include?(b) } } # Replace letters with true/false requires.map!{ |a| a.include?(true) } # Replace each combination with true/false - earnt &= !requires.include?(false) # Only earnt if all combinations are met + return false if requires.include?(false) # Only earnt if all combinations are met end - criteria[:badges_required].each do |b| + badge.other_requirements_required.each do |c| + # {:id => ###, :min => #} + if requirements.has_key?(c[:id]) # Only check it if the data is in the requirements Hash + return false unless requirement_met?(c[:id]) + return false if requirements[c[:id]].to_i < c[:min] + end + end + badge.badges_required.each do |b| # {:id => ###, :version => #} #TODO end - criteria[:fields_required].each do |c| - # {:id => ###, :min => #} - #TODO - end - return earnt + return true end end # Get what stage which has most recently been earnt @@ -550,12 +652,12 @@ def earnt unless badge.has_levels? return earnt? ? 1 : 0 end - levels_column = badge.completion_criteria[:levels_column_id] - unless badge.completion_criteria[:show_letters] # It's a hikes, nights type badge + levels_column = badge.level_requirement + unless badge.show_level_letters # It's a hikes, nights type badge badge.levels.reverse_each do |level| return level if requirements[levels_column].to_i >= level end else # It's an activity type badge modules = modules_gained @@ -586,13 +688,13 @@ # @return [Fixnum] which stage of the badge has been started by the member (lowest) def started unless badge.has_levels? return started? ? 1 : 0 end - unless badge.completion_criteria[:show_letters] + unless badge.show_level_letters # Nights, Hikes or Water - done = requirements[badge.completion_criteria[:levels_column_id]].to_i + done = requirements[badge.level_requirement].to_i levels = badge.levels # e.g. [0,1,2,3,4,5,10] return 0 if levels.include?(done) # Has achieved a level (and not started next ) return 0 if done >= levels[-1] # No more levels to do (1..(levels.size-1)).to_a.reverse_each do |i| # indexes from last to 2nd this_level = levels[i] @@ -605,12 +707,12 @@ letters = ('a'..'z').to_a top_level = badge.levels[-1] return 0 if due == top_level || awarded == top_level # No more levels to do ((due + 1)..top_level).reverse_each do |level| badge.requirements.each do |requirement| - next unless requirement.module_letter.eql?(letters[level - 1]) # Not interested in other levels - return level if requirement_met?(requirement.field) + next unless requirement.mod.letter.eql?(letters[level - 1]) # Not interested in other levels + return level if requirement_met?(requirement.id) end end return 0 # No levels started end end @@ -698,24 +800,24 @@ section = Osm::Section.get(api, section_id) require_ability_to(api, :write, :badge, section) # Update requirements that changed requirements_updated = true - editable_fields = badge.requirements.select{ |r| r.editable }.map{ |r| r.field } - requirements.changes.each do |field, (was,now)| - if editable_fields.include?(field) + editable_requirements = badge.requirements.select{ |r| r.editable }.map{ |r| r.id } + requirements.changes.each do |requirement, (was,now)| + if editable_requirements.include?(requirement) result = api.perform_query("ext/badges/records/?action=updateSingleRecord", { 'scoutid' => member_id, 'section_id' => section_id, 'badge_id' => badge.id, 'badge_version' => badge.version, - 'field' => field, + 'field' => requirement, 'value' => now }) requirements_updated = false unless result.is_a?(Hash) && (result['scoutid'].to_i == member_id) && - (result[field.to_s] == now) + (result[requirement.to_s] == now) end end if requirements_updated requirements.clean_up! @@ -748,17 +850,17 @@ result = self.member_id <=> another.try(:member_id) if result == 0 return result end def inspect - Osm.inspect_instance(self, options={:replace_with => {'badge' => :name}}) + Osm.inspect_instance(self, {:replace_with => {'badge' => :name}}) end # Work out if the requirmeent has been met - # @param [Fixnum, #to_i] field_id The id of the field to evaluate (e.g. "12", "xSomething", "Yes" or "") + # @param [Fixnum, #to_i] requirement_id The id of the requirement to evaluate (e.g. "12", "xSomething", "Yes" or "") # @return [Boolean] whether the requirmeent has been met - def requirement_met?(field_id) - data = requirements[field_id.to_i].to_s + def requirement_met?(requirement_id) + data = requirements[requirement_id.to_i].to_s return false if data == '0' !(data.blank? || data[0].downcase.eql?('x')) end end # Class Data