Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Postprocessing data read enhancements #376

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions app/controllers/application_api_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
class ApplicationAPIController < ActionController::API

include ActionController::ImplicitRender
include ActionController::Helpers
include ActionController::HttpAuthentication::Basic::ControllerMethods
include ActionController::HttpAuthentication::Token::ControllerMethods
include ActionController::Caching

include Pundit::Authorization
include PrettyJSON
include ErrorHandlers

include SharedControllerMethods

include ::UserHelper
helper ::UserHelper

include ::PresentationHelper
helper ::PresentationHelper

before_action :set_rate_limit_whitelist
after_action :verify_authorized, except: :index
respond_to :json

private

def set_rate_limit_whitelist
if current_user(false)&.is_admin_or_researcher?
Rack::Attack.cache.write("throttle_whitelist_#{request.remote_ip}", true, 5.minutes)
end
end

def check_if_authorized!
if current_user.nil?
if params[:access_token]
raise Smartcitizen::Unauthorized.new("Invalid OAuth2 Params")
else
raise Smartcitizen::Unauthorized.new("Authorization required")
end
end
end

def raise_ransack_errors_as_bad_request(&block)
begin
block.call
rescue ArgumentError => e
render json: { message: e.message, status: 400 }, status: 400
end
end

def check_missing_params *params_list
missing_params = []
params_list.each do |param|
individual_params = param.split("||")
missing_params << individual_params.join(" OR ") unless (params.keys & individual_params).any?
end
raise ActionController::ParameterMissing.new(missing_params.to_sentence) if missing_params.any?
end

def check_date_param_format(param_name)
return true if !params[param_name]
return true if params[param_name] =~ /^\d+$/
begin
Time.parse(params[param_name])
return true
rescue
message = "The #{param_name} parameter must be an ISO8601 format date or datetime or an integer number of seconds since the start of the UNIX epoch."
render json: { message: message, status: 400 }, status: 400
return false
end
end
end
70 changes: 1 addition & 69 deletions app/controllers/v0/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,80 +1,12 @@
require_relative '../../helpers/user_helper'
module V0
class ApplicationController < ActionController::API

include ActionController::HttpAuthentication::Basic::ControllerMethods
include ActionController::HttpAuthentication::Token::ControllerMethods
include ActionController::Helpers
include ActionController::ImplicitRender
include ActionController::Caching

include PrettyJSON
include ErrorHandlers

helper ::UserHelper
include ::UserHelper

include SharedControllerMethods

respond_to :json

class ApplicationController < ::ApplicationAPIController
before_action :prepend_view_paths
before_action :set_rate_limit_whitelist
after_action :verify_authorized, except: :index

protected

def check_missing_params *params_list
missing_params = []
params_list.each do |param|
individual_params = param.split("||")
missing_params << individual_params.join(" OR ") unless (params.keys & individual_params).any?
end
raise ActionController::ParameterMissing.new(missing_params.to_sentence) if missing_params.any?
end

def check_date_param_format(param_name)
return true if !params[param_name]
return true if params[param_name] =~ /^\d+$/
begin
Time.parse(params[param_name])
return true
rescue
message = "The #{param_name} parameter must be an ISO8601 format date or datetime or an integer number of seconds since the start of the UNIX epoch."
render json: { message: message, status: 400 }, status: 400
return false
end
end

private

def raise_ransack_errors_as_bad_request(&block)
begin
block.call
rescue ArgumentError => e
render json: { message: e.message, status: 400 }, status: 400
end
end

def prepend_view_paths
# is this still necessary?
prepend_view_path "app/views/v0"
end

def set_rate_limit_whitelist
if current_user(false)&.is_admin_or_researcher?
Rack::Attack.cache.write("throttle_whitelist_#{request.remote_ip}", true, 5.minutes)
end
end

def check_if_authorized!
if current_user.nil?
if params[:access_token]
raise Smartcitizen::Unauthorized.new("Invalid OAuth2 Params")
else
raise Smartcitizen::Unauthorized.new("Authorization required")
end
end
end
end
end
4 changes: 4 additions & 0 deletions app/controllers/v1/application_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module V1
class ApplicationController < ::ApplicationAPIController
end
end
35 changes: 35 additions & 0 deletions app/controllers/v1/devices_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module V1
class DevicesController < ApplicationController
def show
@device = Device.includes(
:owner,:tags, {sensors: :measurement}).find(params[:id])
authorize @device
render json: present(@device)
end

#TODO Document breaking API change as detailed in https://github.com/fablabbcn/smartcitizen-api/issues/186
def index
raise_ransack_errors_as_bad_request do
@q = policy_scope(Device)
.includes(:owner, :tags, :components, {sensors: :measurement})
.ransack(params[:q], auth_object: (current_user&.is_admin? ? :admin : nil))

@devices = @q.result(distinct: true)

if params[:near]
if params[:near] =~ /\A(\-?\d+(\.\d+)?),\s*(\-?\d+(\.\d+)?)\z/
@devices = @devices.near(
params[:near].split(','), (params[:within] || 1000))
else
return render json: { id: "bad_request",
message: "Malformed near parameter",
url: 'https://developer.smartcitizen.me/#get-all-devices',
errors: nil }, status: :bad_request
end
end
@devices = paginate(@devices)
render json: present(@devices)
end
end
end
end
5 changes: 5 additions & 0 deletions app/helpers/presentation_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module PresentationHelper
def present(model, options={})
Presenters.present(model, current_user, self, options)
end
end
24 changes: 7 additions & 17 deletions app/jobs/mqtt_forwarding_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,22 @@ class MQTTForwardingJob < ApplicationJob

queue_as :mqtt_forward

def perform(device_id, reading)
def perform(device_id, data)
readings = data[:readings]
device = Device.find(device_id)
begin
device = Device.find(device_id)
forwarder = MQTTForwarder.new(mqtt_client)
payload = payload_for(device, reading)
forwarder.forward_reading(device.forwarding_token, device.id, payload)
payload = payload_for(device, readings)
forwarder.forward_readings(device.forwarding_token, device.id, payload)
ensure
disconnect_mqtt!
end
end

private

def payload_for(device, reading)
renderer.render(
partial: "v0/devices/device",
locals: {
device: device.reload,
current_user: nil,
slim_owner: true
}
)
def payload_for(device, readings)
Presenters.present(device, device.owner, nil, readings: readings).to_json
end

def mqtt_client
Expand All @@ -32,10 +26,6 @@ def mqtt_client
})
end

def renderer
@renderer ||= ActionController::Base.new.view_context
end

def disconnect_mqtt!
@mqtt_client&.disconnect
end
Expand Down
8 changes: 2 additions & 6 deletions app/jobs/send_to_datastore_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@ class SendToDatastoreJob < ApplicationJob

def perform(data_param, device_id)
@device = Device.includes(:components).find(device_id)
the_data = JSON.parse(data_param)
the_data.sort_by {|a| a['recorded_at']}.reverse.each_with_index do |reading, index|
# move to async method call
do_update = index == 0
storer.store(@device, reading, do_update)
end
readings = JSON.parse(data_param)
storer.store(@device, readings)
end

def storer
Expand Down
2 changes: 1 addition & 1 deletion app/lib/mqtt_forwarder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ def initialize(client)
@suffix = suffix
end

def forward_reading(token, device_id, reading)
def forward_readings(token, device_id, reading)
topic = topic_path(token, device_id)
client.publish(topic, reading)
end
Expand Down
4 changes: 1 addition & 3 deletions app/lib/mqtt_messages_handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ def handle_readings(device, message)
parsed = JSON.parse(message) if message
data = parsed["data"] if parsed
return nil if data.nil? or data&.empty?
data.each do |reading|
storer.store(device, reading)
end
storer.store(device, data)
return true
rescue Exception => e
Sentry.capture_exception(e)
Expand Down
24 changes: 24 additions & 0 deletions app/lib/presenters.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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
# use in appropriate views, delete unneeded code in models and views.
PRESENTERS = {
Device => Presenters::DevicePresenter,
User => Presenters::UserPresenter,
Component => Presenters::ComponentPresenter,
Sensor => Presenters::SensorPresenter,
Measurement => Presenters::MeasurementPresenter,
}

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
64 changes: 64 additions & 0 deletions app/lib/presenters/base_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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
@unauthorized_fields = []
@options = self.default_options.merge(options)
end

def as_json(_opts=nil)
values = self.exposed_fields.inject({}) { |hash, field|
value = self.send(field)
value.nil? ? hash : hash.merge(field => value)
}
unauthorized_fields.each do |field_path|
parent_path = field_path.dup
field_name = parent_path.pop
parent = parent_path.inject(values) { |vals, key| vals[key] }
parent[:unauthorized_fields] ||= []
parent[:unauthorized_fields] << field_name
end
values
end

def method_missing(method, *args, &block)
if self.exposed_fields.include?(method)
model.public_send(method, *args, &block)
else
super
end
end

def present(other_model, options={})
Presenters.present(other_model, current_user, render_context, options)
end

def authorized?
true
end

def authorize!(*field_path, &block)
if authorized?
block.call
else
unauthorized_fields << field_path
nil
end
end

private

attr_reader :model, :current_user, :options, :render_context, :unauthorized_fields
end
end
Loading