Merge conflict, schema.rb

pull/814/head
Rick Carlino 2018-04-28 08:54:37 -05:00
commit 7169ee78a4
23 changed files with 112 additions and 100 deletions

View File

@ -2,7 +2,7 @@ source "https://rubygems.org"
ruby "2.5.1"
gem "active_model_serializers"
gem "bullet"
# gem "bullet"
gem "bunny"
gem "delayed_job_active_record"
gem "delayed_job"
@ -30,6 +30,9 @@ gem "thin"
gem "tzinfo" # For validation of user selected timezone names
gem "valid_url"
gem "webpack-rails"
# Probably safe to remove after next rails upgrade.
# https://nvd.nist.gov/vuln/detail/CVE-2018-3741
gem "rails-html-sanitizer", "~> 1.0.4"
group :development, :test do
gem "capybara"

View File

@ -65,9 +65,6 @@ GEM
arel (8.0.0)
bcrypt (3.1.11)
builder (3.2.3)
bullet (5.7.5)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11.0)
bunny (2.9.2)
amq-protocol (~> 2.3.0)
capybara (3.0.1)
@ -327,7 +324,6 @@ GEM
tzinfo (1.2.5)
thread_safe (~> 0.1)
uber (0.1.0)
uniform_notifier (1.11.0)
url (0.3.2)
useragent (0.16.10)
valid_url (0.0.4)
@ -348,7 +344,6 @@ PLATFORMS
DEPENDENCIES
active_model_serializers
bullet
bunny
capybara
codecov
@ -377,6 +372,7 @@ DEPENDENCIES
rack-cors
rails
rails-erd
rails-html-sanitizer (~> 1.0.4)
rails_12factor
request_store
rollbar

View File

@ -1,7 +1,9 @@
module Points
class Destroy < Mutations::Command
STILL_IN_USE = "Could not delete the following item(s): %s. Item(s) are "\
"in use by the following sequence(s): %s."
STILL_IN_USE = "Could not delete the following item(s): %s. Item(s) are "\
"in use by the following sequence(s): %s."
JUST_ONE = "Could not delete %s. Item is in use by the following "\
"sequence(s): %s."
required do
model :device, class: Device
@ -15,7 +17,7 @@ module Points
def validate
# Collect names of sequences that still use this point.
errors = (tool_seq + point_seq)
problems = (tool_seq + point_seq)
.group_by(&:sequence_name)
.to_a
.reduce({S => [], P => []}) do |total, (seq_name, data)|
@ -24,13 +26,14 @@ module Points
total
end
points = errors[P].sort.uniq.join(", ")
p = problems[P].sort.uniq.join(", ")
if points.present?
sequences = errors[S].sort.uniq.join(", ")
errors = STILL_IN_USE % [points, sequences]
if p.present?
sequences = problems[S].sort.uniq.join(", ")
message = (point_ids.count > 1) ? STILL_IN_USE : JUST_ONE
problems = message % [p, sequences]
add_error :whoops, :in_use, errors
add_error :whoops, :in_use, problems
end
end
@ -66,7 +69,8 @@ module Points
def every_tool_id_as_json
points
.where.not(tool_id: nil)
.where
.not(tool_id: nil)
.pluck(:tool_id)
.uniq
.map(&:to_json)

View File

@ -9,10 +9,10 @@ Bundler.require(:default, Rails.env)
module FarmBot
class Application < Rails::Application
Delayed::Worker.max_attempts = 4
config.after_initialize do
Bullet.enable = true
Bullet.console = true
end
# config.after_initialize do
# Bullet.enable = true
# Bullet.console = true
# end
config.active_job.queue_adapter = :delayed_job
config.action_dispatch.perform_deep_munge = false
I18n.enforce_available_locales = false

View File

@ -11,11 +11,11 @@ FarmBot::Application.configure do
config.log_formatter = ::Logger::Formatter.new
config.log_level = :info
config.public_file_server.enabled = false
config.after_initialize do
Bullet.enable = true
Bullet.console = true
Bullet.rollbar = true if ENV["ROLLBAR_ACCESS_TOKEN"]
end
# config.after_initialize do
# Bullet.enable = true
# Bullet.console = true
# Bullet.rollbar = true if ENV["ROLLBAR_ACCESS_TOKEN"]
# end
# HACK AHEAD! Here's why:
# 1. FarmBot Inc. Uses Sendgrid for email.
# 2. FarmBot is an open source project that must be vendor neutral.

View File

@ -1,9 +1,9 @@
FarmBot::Application.configure do
config.after_initialize do
Bullet.enable = true
Bullet.console = true
Bullet.raise = true
end
# config.after_initialize do
# Bullet.enable = true
# Bullet.console = true
# Bullet.raise = true
# end
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that

View File

@ -1,6 +1,9 @@
class AddMoreMissingIndexes < ActiveRecord::Migration[5.1]
def change
def up
add_index :logs, :verbosity
change_column :logs, :verbosity, :integer, default: 1
end
def down
end
end

View File

@ -534,7 +534,7 @@ ActiveRecord::Schema.define(version: 20180423202520) do
SELECT sequences.id AS sequence_id,
( SELECT count(*) AS count
FROM edge_nodes
WHERE ((edge_nodes.sequence_id = sequences.id) AND ((edge_nodes.kind)::text = 'sequence_id'::text) AND ((edge_nodes.value)::text = (sequences.id)::text))) AS edge_node_count,
WHERE (((edge_nodes.kind)::text = 'sequence_id'::text) AND ((edge_nodes.value)::integer = sequences.id))) AS edge_node_count,
( SELECT count(*) AS count
FROM farm_events
WHERE ((farm_events.executable_id = sequences.id) AND ((farm_events.executable_type)::text = 'Sequence'::text))) AS farm_event_count,

View File

@ -24,10 +24,10 @@ describe "Point deletion edge cases" do
offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0} }
},
}])
result = Points::Destroy.run(point_ids: [tool_slot.id], device: device)
errors = result.errors.message_list
expected = "Could not delete the following item(s): foo tool. Item(s) are "\
"in use by the following sequence(s): sequence."
result = Points::Destroy.run(point_ids: [tool_slot.id], device: device)
errors = result.errors.message_list
expected = "Could not delete foo tool. Item is in use by the following "\
"sequence(s): sequence."
expect(errors).to include(expected)
end
end

View File

@ -48,9 +48,8 @@ describe Points::Destroy do
point_ids = [s.tool_slot.id]
result = Points::Destroy.run(point_ids: point_ids, device: s.device)
expect(result.success?).to be(false)
expected = "Could not delete the following item(s): Scenario Tool. "\
"Item(s) are in use by the following sequence(s): Scenario "\
"Sequence."
expected = "Could not delete Scenario Tool. Item is in use by the "\
"following sequence(s): Scenario Sequence."
expect(result.errors.message_list).to include(expected)
end

View File

@ -110,7 +110,7 @@ export class Move extends React.Component<MoveProps, {}> {
</fieldset>
<p>
{t("Swap jog buttons")}
{t("Swap jog buttons (and rotate map)")}
</p>
<fieldset>
<label>

View File

@ -245,6 +245,7 @@
}
}
.virtual-bot-trail,
.virtual-peripherals {
pointer-events: none;
}

View File

@ -11,11 +11,12 @@ jest.mock("../../api/crud", () => ({
edit: jest.fn()
}));
import { movePlant, closePlantInfo } from "../actions";
import { movePlant, closePlantInfo, setDragIcon } from "../actions";
import { MovePlantProps } from "../interfaces";
import { fakePlant } from "../../__test_support__/fake_state/resources";
import { edit } from "../../api/crud";
import { Actions } from "../../constants";
import { DEFAULT_ICON } from "../../open_farm/icons";
describe("movePlant", () => {
beforeEach(() => {
@ -80,3 +81,23 @@ describe("closePlantInfo()", () => {
});
});
});
describe("setDragIcon()", () => {
it("sets the drag icon", () => {
const setDragImage = jest.fn();
const e = { currentTarget: new Image(), dataTransfer: { setDragImage } };
setDragIcon("icon")(e);
const img = new Image();
img.src = "data:image/svg+xml;utf8,icon";
expect(setDragImage).toHaveBeenCalledWith(img, 0, 0);
});
it("sets a default drag icon", () => {
const setDragImage = jest.fn();
const e = { currentTarget: new Image(), dataTransfer: { setDragImage } };
setDragIcon(undefined)(e);
const img = new Image();
img.src = DEFAULT_ICON;
expect(setDragImage).toHaveBeenCalledWith(img, 0, 0);
});
});

View File

@ -27,11 +27,6 @@ describe("mapStateToProps()", () => {
it("stepsPerMm is defined", () => {
const state = fakeState();
state.bot.hardware.informational_settings.firmware_version = "5.0.0R";
state.bot.hardware.configuration.steps_per_mm_x = 1;
state.bot.hardware.configuration.steps_per_mm_y = 2;
expect(mapStateToProps(state).stepsPerMmXY).toEqual({ x: 1, y: 2 });
state.bot.hardware.informational_settings.firmware_version = "5.0.6R";
state.bot.hardware.mcu_params.movement_step_per_mm_x = 3;
state.bot.hardware.mcu_params.movement_step_per_mm_y = 4;
expect(mapStateToProps(state).stepsPerMmXY).toEqual({ x: 3, y: 4 });

View File

@ -1,9 +1,10 @@
import { MovePlantProps } from "./interfaces";
import { MovePlantProps, DraggableEvent } from "./interfaces";
import { defensiveClone } from "../util";
import { edit } from "../api/crud";
import * as _ from "lodash";
import { history, getPathArray } from "../history";
import { Actions } from "../constants";
import { svgToUrl, DEFAULT_ICON } from "../open_farm/icons";
export function movePlant(payload: MovePlantProps) {
const tr = payload.plant;
@ -32,3 +33,13 @@ export const closePlantInfo = (dispatch: Function) => () => {
history.push("/app/designer/plants");
}
};
export const setDragIcon =
(icon: string | undefined) => (e: DraggableEvent) => {
const dragImg = new Image();
dragImg.src = icon ? svgToUrl(icon) : DEFAULT_ICON;
const width = dragImg.naturalWidth;
const height = dragImg.naturalHeight;
e.dataTransfer.setDragImage
&& e.dataTransfer.setDragImage(dragImg, width / 2, height / 2);
};

View File

@ -373,8 +373,7 @@ describe("<GardenPlant/>", () => {
expect(eggs.find("Bugs").length).toEqual(1);
});
it(".drop-area: handles drag over", () => {
mockPath = "/app/designer/plants/crop_search";
const expectHandledDragOver = () => {
const wrapper = shallow(<GardenMap {...fakeProps()} />);
const e = {
dataTransfer: { dropEffect: undefined },
@ -382,6 +381,17 @@ describe("<GardenPlant/>", () => {
};
wrapper.find(".drop-area").simulate("dragOver", e);
expect(e.dataTransfer.dropEffect).toEqual("move");
expect(e.preventDefault).toHaveBeenCalled();
};
it(".drop-area: handles drag over (crop page)", () => {
mockPath = "/app/designer/plants/crop_search/mint";
expectHandledDragOver();
});
it(".drop-area: handles drag over (click-to-add mode)", () => {
mockPath = "/app/designer/plants/crop_search/mint/add";
expectHandledDragOver();
});
it(".drop-area: handles drag start", () => {

View File

@ -174,6 +174,7 @@ export class GardenMap extends
handleDragOver = (e: React.DragEvent<HTMLElement>) => {
switch (getMode()) {
case Mode.addPlant:
case Mode.clickToAdd:
e.preventDefault();
e.dataTransfer.dropEffect = "move";
}

View File

@ -23,7 +23,7 @@ describe("<BotTrail/>", () => {
const p = fakeProps();
p.mapTransformProps.quadrant = 2;
const wrapper = shallow(<BotTrail {...p} />);
const lines = wrapper.find("#trail").find("line");
const lines = wrapper.find(".virtual-bot-trail").find("line");
expect(lines.length).toEqual(4);
expect(lines.first().props()).toEqual({
id: "trail-line-1",
@ -44,7 +44,7 @@ describe("<BotTrail/>", () => {
it("shows default length trail", () => {
sessionStorage[VirtualTrail.length] = undefined;
const wrapper = shallow(<BotTrail {...fakeProps()} />);
const lines = wrapper.find("#trail").find("line");
const lines = wrapper.find(".virtual-bot-trail").find("line");
expect(lines.length).toEqual(5);
});
@ -53,13 +53,13 @@ describe("<BotTrail/>", () => {
const p = fakeProps();
p.position = { x: 4, y: 4, z: 0 };
const wrapper = shallow(<BotTrail {...p} />);
const lines = wrapper.find("#trail").find("line");
const lines = wrapper.find(".virtual-bot-trail").find("line");
expect(lines.length).toEqual(4);
});
it("shows water", () => {
const wrapper = shallow(<BotTrail {...fakeProps()} />);
const circles = wrapper.find("#trail").find("circle");
const circles = wrapper.find(".virtual-bot-trail").find("circle");
expect(circles.length).toEqual(2);
});
@ -68,7 +68,7 @@ describe("<BotTrail/>", () => {
p.position = { x: 4, y: 4, z: 0 };
p.peripherals = [{ label: "water", value: true }];
const wrapper = shallow(<BotTrail {...p} />);
const water = wrapper.find("#trail").find("circle").last();
const water = wrapper.find(".virtual-bot-trail").find("circle").last();
expect(water.props().r).toEqual(21);
});

View File

@ -48,7 +48,7 @@ export function BotTrail(props: BotTrailProps) {
const array = getNewTrailArray({ coord: { x, y }, water: 0 }, watering);
return <g id="trail">
return <g className="virtual-bot-trail">
{array.map((cur: TrailRecord, i: number) => {
const prev = (array[i - 1] || { coord: undefined }).coord; // prev coord
const opacity = _.round(Math.max(0.25, i / (array.length - 1)), 2);

View File

@ -7,6 +7,7 @@ import { history, getPathArray } from "../../history";
import { svgToUrl } from "../../open_farm/icons";
import { CropLiveSearchResult } from "../interfaces";
import { findBySlug } from "../search_selectors";
import { setDragIcon } from "../actions";
export function mapStateToProps(props: Everything): AddPlantProps {
return {
@ -48,11 +49,11 @@ export class AddPlant
</p>
<div className="panel-header-description">
<img className="crop-drag-info-image"
src={svgToUrl(result.crop.svg_icon)}
alt={t("plant icon")}
width={100}
height={100}
draggable={true}
src={svgToUrl(result.crop.svg_icon)} />
onDragStart={setDragIcon(result.crop.svg_icon)} />
<b>{t("Drag and drop")}</b> {t("the icon onto the map or ")}
<b>{t("CLICK anywhere within the grid")}</b> {t(`to add the plant
to the map. You can add the plant as many times as you need to

View File

@ -1,15 +1,15 @@
import * as React from "react";
import { t } from "i18next";
import * as _ from "lodash";
import { DATA_URI, DEFAULT_ICON, svgToUrl } from "../../open_farm/icons";
import { CropInfoProps, DraggableEvent } from "../interfaces";
import { svgToUrl } from "../../open_farm/icons";
import { CropInfoProps } from "../interfaces";
import { history, getPathArray } from "../../history";
import { connect } from "react-redux";
import { findBySlug } from "../search_selectors";
import { Everything } from "../../interfaces";
import { OpenFarm } from "../openfarm";
import { OFSearch } from "../util";
import { unselectPlant } from "../actions";
import { unselectPlant, setDragIcon } from "../actions";
interface InforFieldProps {
title: string;
@ -49,20 +49,6 @@ export class CropInfo extends React.Component<CropInfoProps, {}> {
unselectPlant(this.props.dispatch)();
}
handleDragStart = (e: DraggableEvent) => {
const icon = e.currentTarget.getAttribute("data-icon-url");
const img = document.createElement("img");
icon ? img.src = DATA_URI + icon : DEFAULT_ICON;
// TODO: Setting these doesn't work by default, needs a fix
// https://www.w3.org/TR/2011/WD-html5-20110405
// /dnd.html#dom-datatransfer-setdragimage
img.height = 50;
img.width = 50;
e.dataTransfer.setDragImage && e.dataTransfer.setDragImage(img, 50, 50);
}
render() {
const crop = getPathArray()[5];
const result =
@ -93,10 +79,8 @@ export class CropInfo extends React.Component<CropInfoProps, {}> {
<div className="panel-content">
<div className="crop-drag-info-tile">
<img className="crop-drag-info-image"
onDragStart={this.handleDragStart}
draggable={true}
src={result.image}
data-icon-url={result.crop.svg_icon} />
onDragStart={setDragIcon(result.crop.svg_icon)} />
<div className="crop-info-overlay">
{t("Drag and drop into map")}
</div>
@ -137,7 +121,8 @@ export class CropInfo extends React.Component<CropInfoProps, {}> {
<img
src={svgToUrl(value)}
width={100}
height={100} />
height={100}
onDragStart={setDragIcon(value)} />
</div>
:
<span>

View File

@ -9,10 +9,8 @@ import {
selectAllPeripherals,
getFirmwareConfig
} from "../resources/selectors";
import { StepsPerMmXY } from "../devices/interfaces";
import { isNumber } from "lodash";
import * as _ from "lodash";
import { minFwVersionCheck, validBotLocationData, validFwConfig } from "../util";
import { validBotLocationData, validFwConfig } from "../util";
import { getWebAppConfigValue } from "../config_storage/actions";
export function mapStateToProps(props: Everything) {
@ -26,27 +24,10 @@ export function mapStateToProps(props: Everything) {
const hoveredPlant = plants.filter(x => x.uuid === plantUUID)[0];
const fwConfig = validFwConfig(getFirmwareConfig(props.resources.index));
const {
mcu_params, configuration, informational_settings
} = props.bot.hardware;
const { mcu_params } = props.bot.hardware;
const firmwareSettings = fwConfig || mcu_params;
function stepsPerMmXY(): StepsPerMmXY {
const { steps_per_mm_x, steps_per_mm_y } = configuration;
const { firmware_version } = informational_settings;
const { movement_step_per_mm_x, movement_step_per_mm_y } = firmwareSettings;
const stepsPerMm = () => {
if (minFwVersionCheck(firmware_version, "5.0.5")) {
return { x: movement_step_per_mm_x, y: movement_step_per_mm_y };
} else {
return { x: steps_per_mm_x, y: steps_per_mm_y };
}
};
if (isNumber(stepsPerMm().x) && isNumber(stepsPerMm().y)) {
return stepsPerMm();
}
return { x: undefined, y: undefined };
}
const { movement_step_per_mm_x, movement_step_per_mm_y } = firmwareSettings;
const peripherals = _.uniq(selectAllPeripherals(props.resources.index))
.map(x => {
@ -87,7 +68,7 @@ export function mapStateToProps(props: Everything) {
plants,
botLocationData: validBotLocationData(props.bot.hardware.location_data),
botMcuParams: firmwareSettings,
stepsPerMmXY: stepsPerMmXY(),
stepsPerMmXY: { x: movement_step_per_mm_x, y: movement_step_per_mm_y },
peripherals,
eStopStatus: props.bot.hardware.informational_settings.locked,
latestImages,

View File

@ -51,6 +51,7 @@ export const NavLinks = (props: NavLinksProps) => {
to={fn(link.slug, childPath)}
className={`${isActive}`}
key={link.slug}
draggable={false}
onClick={props.close("mobileMenuOpen")}>
<i className={`fa fa-${link.icon}`} />
{t(link.name)}