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