Merge branch 'staging' of https://github.com/FarmBot/Farmbot-Web-App into resource_service
commit
71e0c7edf0
|
@ -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 )
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
19
db/schema.rb
19
db/schema.rb
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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", () => {
|
||||
|
|
|
@ -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" }
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue