diff --git a/Gemfile.lock b/Gemfile.lock
index 51c95b965..4234fff0b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -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)
diff --git a/app/models/celery_script_settings_bag.rb b/app/models/celery_script_settings_bag.rb
index 10fc8d3d9..3d3dba3ca 100644
--- a/app/models/celery_script_settings_bag.rb
+++ b/app/models/celery_script_settings_bag.rb
@@ -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
diff --git a/app/models/point_group.rb b/app/models/point_group.rb
index 194023b95..eef492243 100644
--- a/app/models/point_group.rb
+++ b/app/models/point_group.rb
@@ -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
diff --git a/app/mutations/point_groups/create.rb b/app/mutations/point_groups/create.rb
index b7610d315..b516a5404 100644
--- a/app/mutations/point_groups/create.rb
+++ b/app/mutations/point_groups/create.rb
@@ -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
diff --git a/app/mutations/point_groups/helpers.rb b/app/mutations/point_groups/helpers.rb
index 379d43ad6..0f5a3d7f5 100644
--- a/app/mutations/point_groups/helpers.rb
+++ b/app/mutations/point_groups/helpers.rb
@@ -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)
diff --git a/app/mutations/point_groups/update.rb b/app/mutations/point_groups/update.rb
index fc676a72b..516d3590e 100644
--- a/app/mutations/point_groups/update.rb
+++ b/app/mutations/point_groups/update.rb
@@ -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
diff --git a/app/serializers/point_group_serializer.rb b/app/serializers/point_group_serializer.rb
index 5010de0d2..66e128aa4 100644
--- a/app/serializers/point_group_serializer.rb
+++ b/app/serializers/point_group_serializer.rb
@@ -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)
diff --git a/db/migrate/20200116140201_add_criteria_to_point_groups.rb b/db/migrate/20200116140201_add_criteria_to_point_groups.rb
new file mode 100644
index 000000000..d2c262776
--- /dev/null
+++ b/db/migrate/20200116140201_add_criteria_to_point_groups.rb
@@ -0,0 +1,5 @@
+class AddCriteriaToPointGroups < ActiveRecord::Migration[6.0]
+ def change
+ add_column :point_groups, :criteria, :text, limit: 1000
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index a0b70c2fc..6592bfcd4 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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');
diff --git a/frontend/__test_support__/fake_state/resources.ts b/frontend/__test_support__/fake_state/resources.ts
index 729a5ecb0..1c254d97f 100644
--- a/frontend/__test_support__/fake_state/resources.ts
+++ b/frontend/__test_support__/fake_state/resources.ts
@@ -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: {}
+ }
});
}
diff --git a/frontend/farm_designer/point_groups/__tests__/actions_test.ts b/frontend/farm_designer/point_groups/__tests__/actions_test.ts
index 91414f3c5..daec97c41 100644
--- a/frontend/farm_designer/point_groups/__tests__/actions_test.ts
+++ b/frontend/farm_designer/point_groups/__tests__/actions_test.ts
@@ -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");
diff --git a/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx b/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx
index 132cfbfeb..63cbbf3a3 100644
--- a/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx
+++ b/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx
@@ -69,6 +69,13 @@ describe("", () => {
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",
diff --git a/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx b/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx
index 05332c0a5..d50464923 100644
--- a/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx
+++ b/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx
@@ -69,7 +69,18 @@ describe("", () => {
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("", () => {
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);
});
diff --git a/frontend/farm_designer/point_groups/actions.ts b/frontend/farm_designer/point_groups/actions.ts
index 6abea2f4a..d7e6c125a 100644
--- a/frontend/farm_designer/point_groups/actions.ts
+++ b/frontend/farm_designer/point_groups/actions.ts
@@ -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(() => {
diff --git a/package.json b/package.json
index 896d59024..4127b1053 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/spec/controllers/api/point_groups/create_spec.rb b/spec/controllers/api/point_groups/create_spec.rb
index 2dd3be287..2a534624f 100644
--- a/spec/controllers/api/point_groups/create_spec.rb
+++ b/spec/controllers/api/point_groups/create_spec.rb
@@ -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
diff --git a/spec/controllers/api/point_groups/update_spec.rb b/spec/controllers/api/point_groups/update_spec.rb
index cc5f8d15c..62209a039 100644
--- a/spec/controllers/api/point_groups/update_spec.rb
+++ b/spec/controllers/api/point_groups/update_spec.rb
@@ -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