diff --git a/app/lib/presenters.rb b/app/lib/presenters.rb index e7d08d53..7e1bb077 100644 --- a/app/lib/presenters.rb +++ b/app/lib/presenters.rb @@ -1,9 +1,26 @@ module Presenters + # This is work in progress we're releasing early so + # that it can be used in forwarding to send the current + # values as they're received. + # TODO: add presenter tests, finish refactor following + # spec in your spreadsheet, remove unneeded options, + # use in appropriate views, add unauthorized_fields logic + # delete unneeded code in models and views. PRESENTERS = { - Device => Presenters::DevicePresenter + Device => Presenters::DevicePresenter, + User => Presenters::UserPresenter, + Component => Presenters::ComponentPresenter, + Sensor => Presenters::SensorPresenter, + Measurement => Presenters::MeasurementPresenter, } - def self.present(model, user, render_context, options={}) - PRESENTERS[model.class]&.new(model, user, render_context, options).as_json + def self.present(model_or_collection, user, render_context, options={}) + if model_or_collection.is_a?(Enumerable) + model_or_collection.map { |model| present(model, user, render_context, options) } + else + PRESENTERS[model_or_collection.class]&.new( + model_or_collection, user, render_context, options + ).as_json + end end end diff --git a/app/lib/presenters/base_presenter.rb b/app/lib/presenters/base_presenter.rb index 7b09e20d..27674423 100644 --- a/app/lib/presenters/base_presenter.rb +++ b/app/lib/presenters/base_presenter.rb @@ -1,26 +1,42 @@ module Presenters class BasePresenter + + def default_options + {} + end + + def exposed_fields + [] + end + + def initialize(model, current_user=nil, render_context=nil, options={}) + @model = model + @current_user = current_user + @render_context = render_context + @options = self.default_options.merge(options) + end + def as_json(_opts=nil) - self.class.exposed_fields.inject({}) { |h, m| - h.merge(m => self.send(m)) + self.exposed_fields.inject({}) { |hash, field| + value = self.send(field) + value.nil? ? hash : hash.merge(field => value) } end def method_missing(method, *args, &block) - if self.class.exposed_fields.include?(method) - device.public_send(method, *args, &block) + if self.exposed_fields.include?(method) + model.public_send(method, *args, &block) else super end end - - def present(model, options={}) - Presenters.present(model, current_user, render_context, options) + def present(other_model, options={}) + Presenters.present(other_model, current_user, render_context, options) end private - attr_reader :current_user, :options, :render_context + attr_reader :model, :current_user, :options, :render_context end end diff --git a/app/lib/presenters/component_presenter.rb b/app/lib/presenters/component_presenter.rb new file mode 100644 index 00000000..62dd9aa3 --- /dev/null +++ b/app/lib/presenters/component_presenter.rb @@ -0,0 +1,50 @@ +module Presenters + class ComponentPresenter < BasePresenter + + alias_method :component, :model + + def default_options + { readings: nil } + end + + def exposed_fields + %i{key sensor last_reading_at latest_value previous_value readings} + end + + def sensor + present(component.sensor) + end + + def latest_value + data = component.device.data + data[component.sensor_id.to_s] if data + end + + def previous_value + old_data = component.device.old_data + old_data[component.sensor_id.to_s] if old_data + end + + def readings + readings = options[:readings] + if readings + readings.flat_map { |reading| format_reading(reading) }.compact + end + end + + private + + def format_reading(reading) + # TODO sort out the mess of multiple reading formats used ini + # DataParser, RawStorer, etc, etc. + reading.data.map { |entry| + timestamp = entry.timestamp + value = entry.sensors&.find { |sensor| + sensor["id"] == component.sensor_id + }.dig("value") + { timestamp: timestamp, value: value } if value + }.compact + + end + end +end diff --git a/app/lib/presenters/device_presenter.rb b/app/lib/presenters/device_presenter.rb index 3a3f5072..f6a4456a 100644 --- a/app/lib/presenters/device_presenter.rb +++ b/app/lib/presenters/device_presenter.rb @@ -1,35 +1,21 @@ module Presenters class DevicePresenter < BasePresenter + alias_method :device, :model - DEFAULT_OPTIONS = { - with_owner: true, - with_data: true, - with_postprocessing: true, - with_location: true, - slim_owner: false, - never_authorized: false, - data: nil - } - - def initialize(device, current_user=nil, render_context=nil, options={}) - @device = device - @current_user = current_user - @render_context = render_context - - @options = DEFAULT_OPTIONS.merge(options) - end - - def self.exposed_fields - %i{id uuid name description state system_tags user_tags last_reading_at created_at updated_at notify device_token mac_address postprocessing location data_policy hardware owner data} - end - - def self.compactable_fields - %i{device_token} + def default_options + { + with_owner: true, + with_data: true, + with_postprocessing: true, + with_location: true, + slim_owner: false, + never_authorized: false, + readings: nil + } end - def data - # TODO allow for passing in fresh data - options[:data] || device.formatted_data if options[:with_data] + def exposed_fields + %i{id uuid name description state system_tags user_tags last_reading_at created_at updated_at notify device_token mac_address postprocessing location data_policy hardware owner components} end def notify @@ -73,24 +59,8 @@ def hardware end def owner - # TODO refactor this into its own presenter if options[:with_owner] && device.owner - owner = device.owner - owner_data = { - id: owner.id, - user_id: owner.id, - username: owner.username, - url: owner.url, - } - if options[:slim_owner] - owner_data - else - owner_data.merge({ - profile_picture: render_context&.profile_picture_url(owner), - location: owner.location, - device_ids: owner.cached_device_ids - }) - end + present(device.owner, with_devices: false) end end @@ -103,13 +73,14 @@ def device_token end def mac_address - # TODO deal with all this "FILTERED nonsense" authorized? ? device.mac_address : "[FILTERED]" end - private + def components + present(device.components) + end - attr_reader :device + private def authorized? !options[:never_authorized] && policy.show_private_info? diff --git a/app/lib/presenters/measurement_presenter.rb b/app/lib/presenters/measurement_presenter.rb new file mode 100644 index 00000000..0c3cb98f --- /dev/null +++ b/app/lib/presenters/measurement_presenter.rb @@ -0,0 +1,10 @@ +module Presenters + class MeasurementPresenter < BasePresenter + + alias_method :measurement, :model + + def exposed_fields + %i{id name description unit uuid definition} + end + end +end diff --git a/app/lib/presenters/sensor_presenter.rb b/app/lib/presenters/sensor_presenter.rb new file mode 100644 index 00000000..da50c42a --- /dev/null +++ b/app/lib/presenters/sensor_presenter.rb @@ -0,0 +1,14 @@ +module Presenters + class SensorPresenter < BasePresenter + + alias_method :sensor, :model + + def exposed_fields + %i{id parent_id name description unit created_at updated_at uuid default_key datasheet unit_definition measurement tags} + end + + def measurement + present(sensor.measurement) + end + end +end diff --git a/app/lib/presenters/user_presenter.rb b/app/lib/presenters/user_presenter.rb new file mode 100644 index 00000000..e8991bb8 --- /dev/null +++ b/app/lib/presenters/user_presenter.rb @@ -0,0 +1,42 @@ +module Presenters + class UserPresenter < BasePresenter + + alias_method :user, :model + + def default_options + { + with_devices: true + } + end + + def exposed_fields + %i{id uuid role username profile_picture url location email legacy_api_key devices created_at updated_at} + end + + def profile_picture + render_context&.profile_picture_url(user) + end + + def email + user.email if authorized? + end + + def legacy_api_key + user.legacy_api_key if authorized? + end + + def devices + present(user.devices) if options[:with_devices] + end + + private + + def authorized? + policy.show_private_info? + end + + def policy + @policy ||= UserPolicy.new(current_user, user) + end + end +end