Merge pull request #1665 from FarmBot/criteria_groups

Criteria-based groups (API Only)
dep_upgrades
Rick Carlino 2020-01-17 08:44:14 -06:00 committed by GitHub
commit 4451c4decd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 208 additions and 28 deletions

View File

@ -83,7 +83,7 @@ GEM
url
coderay (1.1.2)
concurrent-ruby (1.1.5)
crass (1.0.5)
crass (1.0.6)
database_cleaner (1.7.0)
declarative (0.0.10)
declarative-option (0.1.0)
@ -109,8 +109,8 @@ GEM
factory_bot_rails (5.1.1)
factory_bot (~> 5.1.0)
railties (>= 4.2.0)
faker (2.10.0)
i18n (>= 1.6, < 1.8)
faker (2.10.1)
i18n (>= 1.6, < 2)
faraday (0.15.4)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.13.1)
@ -148,7 +148,7 @@ GEM
hashdiff (1.0.0)
hashie (3.6.0)
httpclient (2.8.3)
i18n (1.7.0)
i18n (1.8.2)
concurrent-ruby (~> 1.0)
json (2.3.0)
jsonapi-renderer (0.2.2)
@ -165,7 +165,7 @@ GEM
mimemagic (0.3.3)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.13.0)
minitest (5.14.0)
multi_json (1.13.1)
multipart-post (2.1.1)
mutations (0.9.0)
@ -178,7 +178,7 @@ GEM
passenger (6.0.4)
rack
rake (>= 0.8.1)
pg (1.2.1)
pg (1.2.2)
pry (0.12.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
@ -190,7 +190,7 @@ GEM
faraday_middleware (~> 0.13.0)
hashie (~> 3.6)
multi_json (~> 1.13.1)
rack (2.0.8)
rack (2.1.1)
rack-attack (6.2.2)
rack (>= 1.0, < 3)
rack-cors (1.1.1)
@ -240,7 +240,7 @@ GEM
actionpack (>= 5.0)
railties (>= 5.0)
retriable (3.1.2)
rollbar (2.23.1)
rollbar (2.23.2)
rspec (3.9.0)
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
@ -265,7 +265,7 @@ GEM
scenic (1.5.1)
activerecord (>= 4.0.0)
railties (>= 4.0.0)
secure_headers (6.1.1)
secure_headers (6.1.2)
signet (0.12.0)
addressable (~> 2.3)
faraday (~> 0.9)

View File

@ -529,7 +529,7 @@ module CeleryScriptSettingsBag
resource_id = n.args.fetch(:point_group_id).value
check_resource_type(n, "PointGroup", resource_id, Device.current)
end,
},
}
}.map { |(name, list)| Corpus.node(name, **list) }
HASH = Corpus.as_json

View File

@ -3,9 +3,17 @@ class PointGroup < ApplicationRecord
%w(xy_ascending xy_descending yx_ascending yx_descending random).sort
BAD_SORT = "%{value} is not valid. Valid options are: " +
SORT_TYPES.map(&:inspect).join(", ")
DEFAULT_CRITERIA = {
day: { op: "<", days: 0 },
string_eq: {},
number_eq: {},
number_lt: {},
number_gt: {},
}
belongs_to :device
has_many :point_group_items, dependent: :destroy
validates_inclusion_of :sort_type, in: SORT_TYPES,
message: BAD_SORT
serialize :criteria
end

View File

@ -8,10 +8,13 @@ module PointGroups
array :point_ids, class: Integer
end
criteria
optional do
string :sort_type
end
def validate
validate_point_ids
validate_sort_type
@ -20,14 +23,21 @@ module PointGroups
def execute
PointGroup.transaction do
PointGroupItem.transaction do
pg = PointGroup.new(name: name, device: device)
point_ids.uniq.map do |id|
pg.point_group_items << PointGroupItem.new(point_id: id)
end
pg = PointGroup.new(name: name,
device: device,
criteria: PointGroup::DEFAULT_CRITERIA.merge(criteria || {})
)
add_point_group_items(pg)
pg.save!
pg
end
end
end
def add_point_group_items(pg)
point_ids.uniq.map do |id|
pg.point_group_items << PointGroupItem.new(point_id: id)
end
end
end
end

View File

@ -1,6 +1,26 @@
module PointGroups
module ClassLevelHelpers
def criteria
self.optional do
hash :criteria do
hash(:day) do
string :op, in: [">", "<"]
integer :days
end
hash(:string_eq) { array :*, class: String }
hash(:number_eq) { array :*, class: Integer }
hash(:number_lt) { integer :* }
hash(:number_gt) { integer :* }
end
end
end
end
module Helpers
BAD_POINT_IDS = "The group contains invalid points."
def self.included(base)
base.extend PointGroups::ClassLevelHelpers
end
def points
@points ||= Point.where(id: point_ids, device: device)

View File

@ -8,6 +8,8 @@ module PointGroups
model :point_group, class: PointGroup
end
criteria
optional do
string :name
array :point_ids, class: Integer
@ -34,7 +36,9 @@ module PointGroups
private
def update_attributes
@update_attributes ||= inputs.except(*BLACKLISTED_FIELDS)
@update_attributes ||= inputs
.except(*BLACKLISTED_FIELDS)
.merge(criteria: criteria || point_group.criteria)
end
def maybe_reconcile_points

View File

@ -1,5 +1,5 @@
class PointGroupSerializer < ApplicationSerializer
attributes :name, :point_ids, :sort_type
attributes :name, :point_ids, :sort_type, :criteria
def point_ids
object.point_group_items.pluck(:point_id)

View File

@ -0,0 +1,5 @@
class AddCriteriaToPointGroups < ActiveRecord::Migration[6.0]
def change
add_column :point_groups, :criteria, :text, limit: 1000
end
end

View File

@ -1185,7 +1185,8 @@ CREATE TABLE public.point_groups (
device_id bigint NOT NULL,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
sort_type character varying(20) DEFAULT 'xy_ascending'::character varying
sort_type character varying(20) DEFAULT 'xy_ascending'::character varying,
criteria text
);
@ -1406,7 +1407,7 @@ CREATE VIEW public.resource_update_steps AS
edge_nodes.kind,
edge_nodes.value
FROM public.edge_nodes
WHERE (((edge_nodes.kind)::text = 'resource_type'::text) AND ((edge_nodes.value)::text = ANY ((ARRAY['"GenericPointer"'::character varying, '"ToolSlot"'::character varying, '"Plant"'::character varying])::text[])))
WHERE (((edge_nodes.kind)::text = 'resource_type'::text) AND ((edge_nodes.value)::text = ANY (ARRAY[('"GenericPointer"'::character varying)::text, ('"ToolSlot"'::character varying)::text, ('"Plant"'::character varying)::text])))
), resource_id AS (
SELECT edge_nodes.primary_node_id,
edge_nodes.kind,
@ -1686,7 +1687,8 @@ CREATE TABLE public.users (
agreed_to_terms_at timestamp without time zone,
confirmation_sent_at timestamp without time zone,
unconfirmed_email character varying,
inactivity_warning_sent_at timestamp without time zone
inactivity_warning_sent_at timestamp without time zone,
inactivity_warning_count integer
);
@ -3445,6 +3447,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20191119204916'),
('20191203163621'),
('20191219212755'),
('20191220010646');
('20191220010646'),
('20200116140201');

View File

@ -475,6 +475,13 @@ export function fakePointGroup(): TaggedPointGroup {
return fakeResource("PointGroup", {
name: "Fake",
sort_type: "xy_ascending",
point_ids: []
point_ids: [],
criteria: {
day: { op: ">", days: 0 },
number_eq: {},
number_gt: {},
number_lt: {},
string_eq: {}
}
});
}

View File

@ -34,11 +34,18 @@ describe("group action creators and thunks", () => {
const thunk = createGroup({ points, name: "Name123" });
await thunk(dispatch, () => fakeS as Everything);
expect(init).toHaveBeenCalledWith("PointGroup", {
expect(init).toHaveBeenCalledWith("PointGroup", expect.objectContaining({
name: "Name123",
point_ids: [1, 2],
sort_type: "xy_ascending"
});
sort_type: "xy_ascending",
criteria: {
day: { days: 0, op: ">" },
number_eq: {},
number_gt: {},
number_lt: {},
string_eq: {},
},
}));
expect(save).toHaveBeenCalledWith("???");
expect(history.push)
.toHaveBeenCalledWith("/app/designer/groups/323232332");

View File

@ -69,6 +69,13 @@ describe("<GroupDetailActive/>", () => {
name: "XYZ",
point_ids: [1],
sort_type: "xy_ascending",
criteria: {
day: { days: 0, op: ">" },
number_eq: {},
number_gt: {},
number_lt: {},
string_eq: {},
}
},
kind: "PointGroup",
specialStatus: "DIRTY",

View File

@ -69,7 +69,18 @@ describe("<PointGroupItem/>", () => {
i.click();
expect(i.props.dispatch).toHaveBeenCalledTimes(2);
expect(overwrite).toHaveBeenCalledWith({
body: { name: "Fake", point_ids: [], sort_type: "xy_ascending" },
body: {
name: "Fake",
point_ids: [],
sort_type: "xy_ascending",
criteria: {
day: { days: 0, op: ">" },
number_eq: {},
number_gt: {},
number_lt: {},
string_eq: {},
}
},
kind: "PointGroup",
specialStatus: "",
uuid: expect.any(String),
@ -77,6 +88,13 @@ describe("<PointGroupItem/>", () => {
name: "Fake",
point_ids: [],
sort_type: "xy_ascending",
criteria: {
day: { days: 0, op: ">" },
number_eq: {},
number_gt: {},
number_lt: {},
string_eq: {},
}
});
expect(setHoveredPlant).toHaveBeenCalledWith(undefined);
});

View File

@ -22,8 +22,18 @@ export const createGroup = ({ points, name }: CreateGroupProps) => {
.map(x => references[x])
.map(x => x ? x.body.id : undefined);
const point_ids = betterCompact(possiblyNil);
const group: PointGroup =
({ name: name || UNTITLED(), point_ids, sort_type: "xy_ascending" });
const group: PointGroup = {
name: name || UNTITLED(),
point_ids,
sort_type: "xy_ascending",
criteria: {
day: { op: ">", days: 0 },
number_eq: {},
number_gt: {},
number_lt: {},
string_eq: {}
}
};
const action = init("PointGroup", group);
dispatch(action);
return dispatch(save(action.payload.uuid)).then(() => {

View File

@ -45,7 +45,7 @@
"coveralls": "3.0.9",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.2",
"farmbot": "9.0.1-rc0",
"farmbot": "9.0.1-rc1",
"i18next": "19.0.3",
"install": "0.13.0",
"lodash": "4.17.15",

View File

@ -47,4 +47,45 @@ describe Api::PointGroupsController do
expect(response.status).to eq(422)
expect(json[:sort_type]).to include(PointGroup::BAD_SORT.split("}").last)
end
it "adds criteria to a group" do
sign_in user
payload = {
name: "Criteria group",
point_ids: point_ids,
criteria: {
string_eq: {
openfarm_slug: ["carrot"],
},
number_eq: {
z: [24, 25, 26],
},
number_lt: {
x: 4,
y: 4,
},
number_gt: {
x: 1,
y: 1,
},
day: {
op: "<",
days: 0,
},
},
}
post :create, body: payload.to_json, format: :json
expect(response.status).to eq(200)
hash = json[:criteria]
expect(hash).to be_kind_of(Hash)
expect(hash.dig(:number_eq, :z)).to eq([24, 25, 26])
expect(hash.dig(:number_lt, :x)).to eq(4)
expect(hash.dig(:number_lt, :y)).to eq(4)
expect(hash.dig(:number_gt, :x)).to eq(1)
expect(hash.dig(:number_gt, :y)).to eq(1)
expect(hash.dig(:day, :op)).to eq("<")
expect(hash.dig(:day, :days)).to eq(0)
expect(hash.dig(:string_eq, :openfarm_slug)).to eq(["carrot"])
end
end

View File

@ -44,4 +44,44 @@ describe Api::PointGroupsController do
json2 = JSON.parse(call1.first, symbolize_names: true).fetch(:body)
expect(json).to eq(json2)
end
it "updates criteria of a group" do
sign_in user
initial_params = {
device: device,
name: "XYZ",
point_ids: [],
criteria: {
string_eq: { openfarm_slug: ["carrot"] },
number_eq: { z: [24, 25, 26] },
number_lt: { x: 4, y: 4 },
number_gt: { x: 1, y: 1 },
day: { op: "<", days: 0 },
},
}
pg = PointGroups::Create.run!(initial_params)
payload = {
point_ids: [],
criteria: {
string_eq: { name: ["carrot"] },
number_eq: { x: [42, 52, 62] },
number_lt: { y: 8 },
number_gt: { z: 2 },
day: { op: ">", days: 10 },
},
}
put :update, body: payload.to_json, format: :json, params: { id: pg.id }
expect(response.status).to eq(200)
expect(json.dig(:criteria, :day, :days)).to eq(10)
expect(json.dig(:criteria, :day, :op)).to eq(">")
expect(json.dig(:criteria, :number_eq, :x)).to eq([42, 52, 62])
expect(json.dig(:criteria, :number_eq, :z)).to eq(nil)
expect(json.dig(:criteria, :number_gt, :x)).to eq(nil)
expect(json.dig(:criteria, :number_gt, :y)).to eq(nil)
expect(json.dig(:criteria, :number_gt, :z)).to eq(2)
expect(json.dig(:criteria, :number_lt, :x)).to eq(nil)
expect(json.dig(:criteria, :number_lt, :y)).to eq(8)
expect(json.dig(:criteria, :string_eq, :name)).to eq(["carrot"])
expect(json.dig(:criteria, :string_eq, :openfarm_slug)).to eq(nil)
end
end