Merge branch 'staging' of https://github.com/FarmBot/Farmbot-Web-App into resource_service

pull/915/head
Rick Carlino 2018-07-16 13:53:53 -05:00
commit 71e0c7edf0
23 changed files with 223 additions and 44 deletions

View File

@ -32,6 +32,8 @@ module CeleryScriptSettingsBag
ALLOWED_AXIS = %w(x y z all)
ALLOWED_LHS_TYPES = [String, :named_pin]
ALLOWED_LHS_STRINGS = [*(0..69)].map{|x| "pin#{x}"}.concat(%w(x y z))
ALLOWED_SPEC_ACTION = %w(dump_info emergency_lock emergency_unlock power_off
read_status reboot sync take_photo)
STEPS = %w(_if execute execute_script find_home move_absolute
move_relative read_pin send_message take_photo wait
write_pin )

View File

@ -79,11 +79,13 @@ class Device < ApplicationRecord
points.where(pointer_type: "Plant")
end
TIMEOUT = (Rails.env.test? ? 0.001 : 150).seconds
# Like Device.find, but with 150 seconds of caching to avoid DB calls.
def self.cached_find(id)
Rails
.cache
.fetch(CACHE_KEY % id, expires_in: 150.seconds) { Device.find(id) }
.fetch(CACHE_KEY % id, expires_in: TIMEOUT) { Device.find(id) }
end
def refresh_cache

View File

@ -1,7 +1,14 @@
class PinBinding < ApplicationRecord
belongs_to :device
belongs_to :sequence
enum special_action: { dump_info: "dump_info",
emergency_lock: "emergency_lock",
emergency_unlock: "emergency_unlock",
power_off: "power_off",
read_status: "read_status",
reboot: "reboot",
sync: "sync",
take_photo: "take_photo" }
def fancy_name
"pin #{pin_num}"
end

View File

@ -8,9 +8,9 @@ module Images
optional do
hash :meta do
optional do
integer :x
integer :y
integer :z
float :x
float :y
float :z
string :name
end
end

View File

@ -33,9 +33,9 @@ module Logs
#
# TODO: delete the `meta` field once FBOS < v6.4.0 reach EOL.
string :type, in: Log::TYPES
integer :x
integer :y
integer :z
float :x
float :y
float :z
integer :verbosity
integer :major_version
integer :minor_version
@ -44,9 +44,9 @@ module Logs
hash :meta do # This can be transitioned out soon.
string :type, in: Log::TYPES
optional do
integer :x
integer :y
integer :z
float :x
float :y
float :z
integer :verbosity
integer :major_version
integer :minor_version

View File

@ -4,12 +4,19 @@ module PinBindings
required do
model :device, class: Device
integer :sequence_id
integer :pin_num
end
optional do
integer :sequence_id
string :special_action, in: PinBinding.special_actions.values
end
def validate
validate_pin_num
validate_sequence_id
exactly_one_choice
not_both_actions
end
def execute

View File

@ -1,8 +1,32 @@
module PinBindings
module Helpers
BAD_SEQ_ID = "Sequence ID is not valid"
MUTUAL_EXCLUSION = "Pin Bindings require exactly one sequence or special " \
"action. Please pick one."
OFF_LIMITS = [17, 23]
BAD_PIN_NUM = "Pin numbers #{OFF_LIMITS.join(" and ")} cannot be used."
def validate_pin_num
if pin_num && OFF_LIMITS.include?(pin_num)
add_error :pin_num, :pin_num, BAD_PIN_NUM
end
end
def false_xor_sequence_id_special_actn
add_error :sequence_id, :sequence_id, MUTUAL_EXCLUSION
end
def exactly_one_choice
false_xor_sequence_id_special_actn if !(sequence_id || special_action)
end
def not_both_actions
false_xor_sequence_id_special_actn if sequence_id && special_action
end
def validate_sequence_id
unless device.sequences.exists?(sequence_id)
add_error :sequence_id, :sequence_id, "Sequence ID is not valid"
if sequence_id && !device.sequences.exists?(sequence_id)
add_error :sequence_id, :sequence_id, BAD_SEQ_ID
end
end
end

View File

@ -8,11 +8,14 @@ module PinBindings
end
optional do
string :special_action, in: PinBinding.special_actions.values
integer :sequence_id
integer :pin_num
end
def validate
validate_pin_num
not_both_actions
validate_sequence_id if sequence_id
end

View File

@ -5,9 +5,9 @@ module ToolSlots
required do
model :device, class: Device
string :name, default: "Untitled Slot"
integer :x
integer :y
integer :z
float :x
float :y
float :z
end
optional do

View File

@ -0,0 +1,13 @@
class PinBindingSerializer < ActiveModel::Serializer
attributes :id, :created_at, :updated_at, :device_id, :sequence_id,
:special_action, :pin_num, :binding_type
def binding_type
object.special_action ? "special" : "standard"
end
# `sequence_id` and `special_action` are mutually exclusive.
def sequence_id
object.special_action ? nil : object.sequence_id
end
end

View File

@ -0,0 +1,19 @@
class AddSpecialActionToPinBinding < ActiveRecord::Migration[5.2]
def up
execute <<-SQL
CREATE TYPE special_action AS
ENUM ('dump_info', 'emergency_lock', 'emergency_unlock', 'power_off',
'read_status', 'reboot', 'sync', 'take_photo');
SQL
add_column :pin_bindings, :special_action, :special_action, index: true
end
def down
remove_column :pin_bindings, :special_action
execute <<-SQL
DROP TYPE special_action;
SQL
end
end

View File

@ -0,0 +1,11 @@
class ChangeLogColumnsToFloats < ActiveRecord::Migration[5.2]
ALL = [ :x, :y, :z ]
def up
ALL.map { |ax| change_column :logs, ax, :float }
end
def down
ALL.map { |ax| change_column :logs, ax, :integer }
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2018_06_15_153318) do
ActiveRecord::Schema.define(version: 2018_07_16_163108) do
# These are extensions that must be enabled in order to support this database
enable_extension "hstore"
@ -252,9 +252,9 @@ ActiveRecord::Schema.define(version: 2018_06_15_153318) do
t.integer "major_version"
t.integer "minor_version"
t.integer "verbosity", default: 1
t.integer "x"
t.integer "y"
t.integer "z"
t.float "x"
t.float "y"
t.float "z"
t.datetime "sent_at"
t.index ["created_at"], name: "index_logs_on_created_at"
t.index ["device_id"], name: "index_logs_on_device_id"
@ -273,15 +273,8 @@ ActiveRecord::Schema.define(version: 2018_06_15_153318) do
t.index ["mode"], name: "index_peripherals_on_mode"
end
create_table "pin_bindings", force: :cascade do |t|
t.bigint "device_id"
t.integer "pin_num"
t.bigint "sequence_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["device_id"], name: "index_pin_bindings_on_device_id"
t.index ["sequence_id"], name: "index_pin_bindings_on_sequence_id"
end
# Could not dump table "pin_bindings" because of following StandardError
# Unknown type 'special_action' for column 'special_action'
create_table "plant_templates", force: :cascade do |t|
t.bigint "saved_garden_id", null: false

View File

@ -57,8 +57,9 @@ describe Api::PinBindingsController do
end
it 'updates pin bindings' do
puts "Blinky test"
sign_in user
s = FakeSequence.create( device: device)
s = FakeSequence.create(device: device)
input = { pin_num: pin_binding.pin_num + 1, sequence_id: s.id}
put :update,
body: input.to_json,
@ -70,5 +71,26 @@ describe Api::PinBindingsController do
expect(pin_binding[key]).to eq(input[key])
end
end
it 'disallows pin 17' do
sign_in user
s = FakeSequence.create( device: device)
input = { pin_num: 17, sequence_id: s.id}
b4 = PinBinding.count
post :create, body: input.to_json, params: { format: :json}
expect(response.status).to eq(422)
expect(json[:pin_num]).to include("Pin numbers 17 and 23 cannot be used.")
end
it 'disallows pin 23' do
sign_in user
s = FakeSequence.create( device: device)
input = { pin_num: 23, sequence_id: s.id}
put :update,
body: input.to_json,
params: { format: :json, id: pin_binding.id}
expect(response.status).to eq(422)
expect(json[:pin_num]).to include("Pin numbers 17 and 23 cannot be used.")
end
end
end

View File

@ -64,7 +64,7 @@ describe Api::SequencesController do
sign_in user
pb = PinBindings::Create.run!(device: user.device,
sequence_id: sequence.id,
pin_num: 23)
pin_num: 24)
delete :destroy, params: { id: sequence.id }
expect(response.status).to eq(422)
expect(json[:sequence]).to include("in use")

View File

@ -10,8 +10,10 @@ describe LogService do
'"message":"HQ FarmBot TEST 123 Pin 13 is 0","created_at":'+
'1512585641,"channels":[]}'
FakeDeliveryInfo = Struct.new(:routing_key)
device_id = FactoryBot.create(:device).id
fake_delivery_info = FakeDeliveryInfo.new("bot.device_#{device_id}.logs")
let!(:device_id) { FactoryBot.create(:device).id }
let!(:fake_delivery_info) do
FakeDeliveryInfo.new("bot.device_#{device_id}.logs")
end
class FakeLogChan
attr_reader :subcribe_calls
@ -36,6 +38,7 @@ describe LogService do
end
it "creates new messages in the DB when called" do
puts "Blinky test"
Log.destroy_all
b4 = Log.count
LogService.process(fake_delivery_info, normal_payl)

View File

@ -0,0 +1,37 @@
require "spec_helper"
module PinBindingSpecHelper
def self.test(mutation, has_seq, has_actn, expected_result)
actn = PinBinding.special_actions.values.sample
sequence = Sequence.last
device = sequence.device
params = { pin_num: 12,
device: device,
pin_binding: PinBinding.last }
params[:special_action] = actn if has_actn
params[:sequence_id] = sequence.id if has_seq
mut = (mutation == :create) ? PinBindings::Create : PinBindings::Update
result = mut.run(params).success?
raise "NO NO NO" if expected_result != result
end
end
describe "Pin Binding updates" do
it "enforces mutual exclusivity" do
puts "Blinky test"
PinBinding.destroy_all
Sequence.destroy_all
Device.destroy_all
device = FactoryBot.create(:device)
PinBinding.create!(device: device)
Sequence.create!(device: device, name: "test")
PinBindingSpecHelper.test(:create, false, false, false)
PinBindingSpecHelper.test(:create, false, true, true )
PinBindingSpecHelper.test(:create, true, false, true )
PinBindingSpecHelper.test(:create, true, true, false)
PinBindingSpecHelper.test(:update, false, false, true )
PinBindingSpecHelper.test(:update, false, true, true )
PinBindingSpecHelper.test(:update, true, false, true )
PinBindingSpecHelper.test(:update, true, true, false)
end
end

View File

@ -163,7 +163,8 @@ export function fakePinBinding(): TaggedPinBinding {
return fakeResource("PinBinding", {
id: idCounter++,
pin_num: 10,
sequence_id: 1
sequence_id: 1,
binding_type: "standard"
});
}

View File

@ -108,9 +108,15 @@ describe("<PinBindings/>", () => {
wrapper.setState({ pinNumberInput: 1, sequenceIdInput: 2 });
buttons.last().simulate("click");
expect(mockDevice.registerGpio).not.toHaveBeenCalled();
expect(initSave).toHaveBeenCalledWith(expect.objectContaining({
body: { pin_num: 1, sequence_id: 2 }, kind: "PinBinding"
}));
const expectedResult = expect.objectContaining({
kind: "PinBinding",
body: {
pin_num: 1,
sequence_id: 2,
binding_type: "standard"
}
});
expect(initSave).toHaveBeenCalledWith(expectedResult);
});
it("sets sequence id", () => {

View File

@ -91,9 +91,12 @@ export class PinBindings
if (this.props.shouldDisplay(Feature.api_pin_bindings)) {
return selectAllPinBindings(this.props.resources)
.map(x => {
const { body } = x;
const sequence_id = // TODO: Handle special bindings.
body.binding_type == "standard" ? body.sequence_id : 0;
return {
pin_number: x.body.pin_num,
sequence_id: x.body.sequence_id,
sequence_id,
uuid: x.uuid
};
});
@ -131,7 +134,7 @@ export class PinBindings
uuid: "WILL_BE_CHANGED_BY_REDUCER",
specialStatus: SpecialStatus.SAVED,
kind: "PinBinding",
body: { pin_num, sequence_id }
body: { pin_num, sequence_id, binding_type: "standard" }
};
}

View File

@ -41,4 +41,16 @@ describe("<Body/>", () => {
iw.onHslChange("H")([2, 8]);
expect(props.onChange).not.toHaveBeenCalled();
});
it("triggers numericChange()", () => {
jest.clearAllMocks();
const props = fakeProps();
const iw = new ImageWorkspace(props);
const trigger = iw.numericChange("blur");
const currentTarget: Partial<HTMLInputElement> = { value: "23" };
type PartialEv = Partial<React.SyntheticEvent<HTMLInputElement>>;
const e: PartialEv = { currentTarget: (currentTarget as HTMLInputElement) };
trigger(e as React.SyntheticEvent<HTMLInputElement>);
expect(props.onChange).toHaveBeenCalledWith("blur", 23);
});
});

View File

@ -11,10 +11,18 @@ import { ChannelName } from "./sequences/interfaces";
in the UI. Only certain colors are valid. */
export type Color = FarmBotJsColor;
export interface PinBinding {
id?: number;
export type PinBinding = StandardPinBinding | SpecialPinBinding;
interface PinBindingBase { id?: number; pin_num: number; }
interface StandardPinBinding extends PinBindingBase {
binding_type: "standard";
sequence_id: number;
pin_num: number;
}
export interface SpecialPinBinding extends PinBindingBase {
binding_type: "special";
special_action: string; // TODO: Maybe use enum? RC 15 JUL 18
}
export interface Sensor {

View File

@ -2,8 +2,9 @@ import * as React from "react";
import { TileSendMessage } from "../tile_send_message";
import { mount } from "enzyme";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { SendMessage } from "farmbot/dist";
import { SendMessage, Channel } from "farmbot/dist";
import { emptyState } from "../../../resources/reducer";
import { channel } from "../tile_send_message_support";
describe("<TileSendMessage/>", () => {
function bootstrapTest() {
@ -55,4 +56,9 @@ describe("<TileSendMessage/>", () => {
expect(inputs.at(5).props().checked).toBeFalsy();
expect(inputs.at(5).props().disabled).toBeFalsy();
});
it("creates a channel via helpers", () => {
const chan: Channel = { kind: "channel", args: { channel_name: "email" } };
expect(channel("email")).toEqual(chan);
});
});