robertgauld/OSMExtender

View on GitHub
app/models/programme_review.rb

Summary

Maintainability
C
1 day
Test Coverage
class ProgrammeReview

  # Hashes for tags, each key is a symbol refering to a section each value an array
  # of arrays containging the label and alternative tags

  # Programme Zones for each section
  ZONES = {
    beavers: [
      ['Beliefs and attitudes', 'belief', 'attitude', 'beliefs', 'attitudes'],
      ['Community'],
      ['Fitness'],
      ['Creative'],
      ['Global'],
      ['Outdoor and adventure', 'outdoor', 'adventure', 'outdoors']
    ],
    cubs: [
      ['Beliefs and attitudes', 'belief', 'attitude', 'beliefs', 'attitudes'],
      ['Community'],
      ['Fitness'],
      ['Creative'],
      ['Global'],
      ['Outdoor and adventure', 'outdoor', 'adventure', 'outdoors']
    ],
    scouts: [
      ['Beliefs and attitudes', 'belief', 'attitude', 'beliefs', 'attitudes'],
      ['Community'],
      ['Fit for life'],
      ['Creative expression', 'creative', 'expression', 'outdoors'],
      ['Global'],
      ['Outdoor and adventure', 'outdoor', 'adventure', 'outdoors']
    ],
    explorers: [
      ['Values and relationships', 'value', 'relationship', 'values', 'relationships'],
      ['Community service', 'community', 'service'],
      ['Physical recreation'],
      ['Skills'],
      ['Global'],
      ['Outdoor and adventure', 'outdoor', 'adventure']
    ]
  }.freeze

  # Programme Methods for each section
  METHODS = {
    beavers: [
      ['Help others'],
      ['Go on visits', 'visit'],
      ['Play games', 'game'],
      ['Make things', 'make'],
      ['Meet new people'],
      ['Act, sing and make music', 'act', 'sing', 'music', 'acting', 'singing'],
      ['Listen to stories', 'stories', 'story'],
      ['Prayer and worship', 'prayer', 'worship'],
      ['Chat'],
      ['Follow themes', 'themes'],
      ['Go outdoors', 'outdoors']
    ],
    cubs: [
      ['Make things', 'make'],
      ['Singing, stories and drama', 'act', 'sing', 'music', 'acting', 'singing', 'drama', 'story', 'stories'],
      ['Visits and visitors', 'visits', 'visit', 'visitor', 'visitors'],
      ['Outdoors'],
      ['Activities with others'],
      ['Help other people', 'help others'],
      ['Themes'],
      ['Prayer, worship and reflection', 'prayer', 'worship', 'reflection'],
      ['Team challenges', 'team challenge'],
      ['Try new things']
    ],
    scouts: [
      ['Activities outdoors', 'outdoors'],
      ['Games', 'game'],
      ['Design and creativity', 'design', 'creativity'],
      ['Visits and visitors', 'visits', 'visit', 'visitor', 'visitors'],
      ['Service'],
      ['Technology and new skills', 'technology', 'new skill', 'new skills'],
      ['Team building activities', 'Team builiding'],
      ['Activities with others'],
      ['Themes'],
      ['Prayer, worship and reflection', 'prayer', 'worship', 'reflection']
    ],
    explorers: [
      ['Visits'],
      ['Games', 'game'],
      ['Outdoor activities', 'outdoors'],
      ['Residential experiances', 'residential'],
      ['Technology'],
      ['Networking activities', 'networking'],
      ['Discussion']
    ]
  }.freeze

  #Section duration - the number of time a young member will expect to be in each section
  SECTION_DURATION = {
    beavers: 2.years,
    cubs: 30.months,   # 2 and a half years
    scouts: 42.months, # 3 and a half years
    explorers: 4.years,
  }.freeze


  def initialize(user, section)
    @user = user
    @section = section
  end


  def balanced(start=SECTION_DURATION[@section.type].ago, finish=SECTION_DURATION[@section.type].from_now)
    zones = {:number => {}, :time => {}}
    methods = {:number => {}, :time => {}}
    earliest = Date.current
    latest = Date.current

    terms = Osm::Term.get_for_section(@user.osm_api, @section.id)
                     .reject { |term| term.before?(start) || term.after?(finish) }

    cached_terms = ProgrammeReviewBalancedCache.where(term_id: terms)
                                               .group_by(&:term_id)

    Parallel.each(terms, in_threads: 2) do |term|
      earliest = term.start if term.start < earliest
      latest = term.finish if term.finish > latest

      # Get/generate cached data
      cached_term = cached_terms[term.id]&.first
      if cached_term.nil?
        # Didn't exist, make a temporary one
        cached_term = ProgrammeReviewBalancedCache.new(
          term_id: term.id,
          term_name: term.name,
          term_start: term.start,
          term_finish: term.finish,
          section_id: term.section_id,
          last_used_at: Time.now.utc,
        )
      end

      if cached_term.data[:version] != 1
        # Make the data
        cached_term.get_balanced_term_data(@user, @section)
      end

      term_methods = cached_term.data[:methods]
      term_zones = cached_term.data[:zones]

      # Add term data into totals
      [:number, :time].each do |num_or_time|
        term_methods[:number].each_key do |date_key|
          methods[num_or_time][date_key] = blank_hash_for(METHODS) if methods[num_or_time][date_key].nil?
          term_methods[num_or_time][date_key].each_key do |method|
            methods[num_or_time][date_key][method] += term_methods[num_or_time][date_key][method]
          end
        end
        term_zones[num_or_time].each_key do |date_key|
          zones[num_or_time][date_key] = blank_hash_for(ZONES) if zones[num_or_time][date_key].nil?
          term_zones[:number][date_key].each_key do |zone|
            zones[num_or_time][date_key][zone] += term_zones[num_or_time][date_key][zone]
          end
        end
      end
    end

    # Ensure that all months are covered
    (earliest.year..latest.year).each do |year|
      (((earliest.year==year) ? earliest.month : 1)..((latest.year==year) ? latest.month : 12)).each do |month|
        date_key = "#{year}_#{"%02d" % month}"
        zones[:number][date_key] = blank_hash_for(ZONES) if zones[:number][date_key].nil?
        methods[:number][date_key] = blank_hash_for(METHODS) if methods[:number][date_key].nil?
        zones[:time][date_key] = blank_hash_for(ZONES) if zones[:time][date_key].nil?
        methods[:time][date_key] = blank_hash_for(METHODS) if methods[:time][date_key].nil?
      end
    end

    # Update used at
    ProgrammeReviewBalancedCache.where(term_id: terms).update_all(last_used_at: Time.now.utc)

    {
      zones: zones,
      methods: methods,
      zone_labels: ZONES[@section.type].clone,
      method_labels: METHODS[@section.type].clone,
      statistics: {
        earliest_date: earliest,
        latest_date: latest,
        zones: get_balanced_statistics(zones, ZONES),
        methods: get_balanced_statistics(methods, METHODS)
      }
    }
  end


  private

  def get_balanced_statistics(data, type)
    statistics = {}

    [:number, :time].each do |num_or_time|
      statistics[num_or_time] = {
        :max_value => 0,
        :standard_deviation => 0,
        :count_tags => 0,
        :total_tags => 0,
        :totals => blank_hash_for(type),
        :mean => 0,
        :standard_deviation => 0,
      }

      # Get information from data
      data[num_or_time].each_key do |date_key|
        data[num_or_time][date_key].each_key do |tag|
          statistics[num_or_time][:totals][tag] += data[num_or_time][date_key][tag]
          statistics[num_or_time][:total_tags] += data[num_or_time][date_key][tag]
          statistics[num_or_time][:max_value] = data[num_or_time][date_key][tag] if (statistics[num_or_time][:max_value] < data[num_or_time][date_key][tag])
        end
      end

      # Calculate mean
      statistics[num_or_time][:totals].each_key do |key|
        statistics[num_or_time][:count_tags] += statistics[num_or_time][:totals][key]
      end
      statistics[num_or_time][:mean] = statistics[num_or_time][:total_tags] / statistics[num_or_time][:count_tags] unless (statistics[num_or_time][:count_tags] == 0)

      # Calculate standard deviation
      unless statistics[num_or_time][:count_tags] == 0
        variance = 0
        statistics[num_or_time][:totals].each_key do |key|
          variance += (statistics[num_or_time][:totals][key] - statistics[num_or_time][:mean]) ** 2
        end
        variance = variance / statistics[num_or_time][:count_tags]
        statistics[num_or_time][:standard_deviation] = Math.sqrt(variance)
      end
    end

    statistics[:time][:totals].each_key do |tag|
      minutes = statistics[:time][:totals][tag]
      statistics[:time][:totals][tag] = "#{minutes / 60} hrs  #{minutes % 60} min"
    end

    return statistics
  end

  def blank_hash_for(zones_or_methods)
    zones_or_methods[@section.type].each_with_object({}) do |key, hash|
      hash[key.first] = 0
    end
  end
end