diff --git a/Gemfile b/Gemfile index 35f82ff56..8dc3ac399 100755 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,7 @@ source "https://rubygems.org" ruby "~> 2.6.5" +gem "rails" gem "active_model_serializers" gem "bunny" gem "delayed_job_active_record" # TODO: Get off of SQL backed jobs. Use Redis @@ -16,7 +17,6 @@ gem "rabbitmq_http_api_client" gem "rack-attack" gem "rack-cors" gem "rails_12factor" -gem "rails", "5.2.3" # TODO: Upgrade to Rails 6 gem "redis", "~> 4.0" gem "request_store" gem "rollbar" @@ -35,7 +35,7 @@ group :development, :test do gem "hashdiff" gem "pry-rails" gem "pry" - gem "rspec-rails" + gem "rspec-rails", "4.0.0.beta3" gem "rspec" gem "simplecov" gem "smarf_doc", git: "https://github.com/RickCarlino/smarf_doc.git" diff --git a/Gemfile.lock b/Gemfile.lock index 8476d79dc..12a5c635c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,56 +7,69 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (5.2.3) - actionpack (= 5.2.3) + actioncable (6.0.0) + actionpack (= 6.0.0) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.3) - actionpack (= 5.2.3) - actionview (= 5.2.3) - activejob (= 5.2.3) + actionmailbox (6.0.0) + actionpack (= 6.0.0) + activejob (= 6.0.0) + activerecord (= 6.0.0) + activestorage (= 6.0.0) + activesupport (= 6.0.0) + mail (>= 2.7.1) + actionmailer (6.0.0) + actionpack (= 6.0.0) + actionview (= 6.0.0) + activejob (= 6.0.0) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.2.3) - actionview (= 5.2.3) - activesupport (= 5.2.3) + actionpack (6.0.0) + actionview (= 6.0.0) + activesupport (= 6.0.0) rack (~> 2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.3) - activesupport (= 5.2.3) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (6.0.0) + actionpack (= 6.0.0) + activerecord (= 6.0.0) + activestorage (= 6.0.0) + activesupport (= 6.0.0) + nokogiri (>= 1.8.5) + actionview (6.0.0) + activesupport (= 6.0.0) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) + rails-html-sanitizer (~> 1.1, >= 1.2.0) active_model_serializers (0.10.10) actionpack (>= 4.1, < 6.1) activemodel (>= 4.1, < 6.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (5.2.3) - activesupport (= 5.2.3) + activejob (6.0.0) + activesupport (= 6.0.0) globalid (>= 0.3.6) - activemodel (5.2.3) - activesupport (= 5.2.3) - activerecord (5.2.3) - activemodel (= 5.2.3) - activesupport (= 5.2.3) - arel (>= 9.0) - activestorage (5.2.3) - actionpack (= 5.2.3) - activerecord (= 5.2.3) + activemodel (6.0.0) + activesupport (= 6.0.0) + activerecord (6.0.0) + activemodel (= 6.0.0) + activesupport (= 6.0.0) + activestorage (6.0.0) + actionpack (= 6.0.0) + activejob (= 6.0.0) + activerecord (= 6.0.0) marcel (~> 0.3.1) - activesupport (5.2.3) + activesupport (6.0.0) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) + zeitwerk (~> 2.1, >= 2.1.8) addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) amq-protocol (2.3.0) - arel (9.0.0) bcrypt (3.1.13) builder (3.2.3) bunny (2.14.3) @@ -106,7 +119,7 @@ GEM railties (>= 3.2, < 6.1) globalid (0.4.2) activesupport (>= 4.2.0) - google-api-client (0.33.1) + google-api-client (0.33.2) addressable (~> 2.5, >= 2.5.1) googleauth (~> 0.9) httpclient (>= 2.8.1, < 3.0) @@ -114,9 +127,9 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.0) signet (~> 0.12) - google-cloud-core (1.3.2) + google-cloud-core (1.4.0) google-cloud-env (~> 1.0) - google-cloud-env (1.2.1) + google-cloud-env (1.3.0) faraday (~> 0.11) google-cloud-storage (1.21.1) addressable (~> 2.5) @@ -183,18 +196,20 @@ GEM rack-cors (1.0.3) rack-test (1.1.0) rack (>= 1.0, < 3) - rails (5.2.3) - actioncable (= 5.2.3) - actionmailer (= 5.2.3) - actionpack (= 5.2.3) - actionview (= 5.2.3) - activejob (= 5.2.3) - activemodel (= 5.2.3) - activerecord (= 5.2.3) - activestorage (= 5.2.3) - activesupport (= 5.2.3) + rails (6.0.0) + actioncable (= 6.0.0) + actionmailbox (= 6.0.0) + actionmailer (= 6.0.0) + actionpack (= 6.0.0) + actiontext (= 6.0.0) + actionview (= 6.0.0) + activejob (= 6.0.0) + activemodel (= 6.0.0) + activerecord (= 6.0.0) + activestorage (= 6.0.0) + activesupport (= 6.0.0) bundler (>= 1.3.0) - railties (= 5.2.3) + railties (= 6.0.0) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) @@ -206,12 +221,12 @@ GEM rails_stdout_logging rails_serve_static_assets (0.0.5) rails_stdout_logging (0.0.5) - railties (5.2.3) - actionpack (= 5.2.3) - activesupport (= 5.2.3) + railties (6.0.0) + actionpack (= 6.0.0) + activesupport (= 6.0.0) method_source rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) + thor (>= 0.20.3, < 2.0) rake (13.0.0) redis (4.1.3) representable (3.0.4) @@ -237,14 +252,14 @@ GEM rspec-mocks (3.9.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.9.0) - rspec-rails (3.9.0) - actionpack (>= 3.0) - activesupport (>= 3.0) - railties (>= 3.0) - rspec-core (~> 3.9.0) - rspec-expectations (~> 3.9.0) - rspec-mocks (~> 3.9.0) - rspec-support (~> 3.9.0) + rspec-rails (4.0.0.beta3) + actionpack (>= 4.2) + activesupport (>= 4.2) + railties (>= 4.2) + rspec-core (~> 3.8) + rspec-expectations (~> 3.8) + rspec-mocks (~> 3.8) + rspec-support (~> 3.8) rspec-support (3.9.0) scenic (1.5.1) activerecord (>= 4.0.0) @@ -281,6 +296,7 @@ GEM websocket-driver (0.7.1) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.4) + zeitwerk (2.2.0) zero_downtime_migrations (0.0.7) activerecord @@ -311,13 +327,13 @@ DEPENDENCIES rabbitmq_http_api_client rack-attack rack-cors - rails (= 5.2.3) + rails rails_12factor redis (~> 4.0) request_store rollbar rspec - rspec-rails + rspec-rails (= 4.0.0.beta3) scenic secure_headers simplecov diff --git a/app/controllers/api/abstract_controller.rb b/app/controllers/api/abstract_controller.rb index 4756eac88..a60d5de1a 100644 --- a/app/controllers/api/abstract_controller.rb +++ b/app/controllers/api/abstract_controller.rb @@ -136,7 +136,7 @@ module Api strategy = Auth::DetermineAuthStrategy.run!(context) case strategy when :jwt - sign_in(Auth::FromJWT.run!(context).require_consent!) + sign_in(Auth::FromJwt.run!(context).require_consent!) when :already_connected # Probably provided a cookie. # 9 times out of 10, it's a unit test. diff --git a/app/controllers/api/demo_accounts_controller.rb b/app/controllers/api/demo_accounts_controller.rb index e21a5ee4e..2d6b729b3 100644 --- a/app/controllers/api/demo_accounts_controller.rb +++ b/app/controllers/api/demo_accounts_controller.rb @@ -44,7 +44,7 @@ module Api end def update_fields - user.update_attributes!(confirmed_at: Time.now) + user.update!(confirmed_at: Time.now) end def seed_user diff --git a/app/controllers/api/points_controller.rb b/app/controllers/api/points_controller.rb index 26fdd6fe6..99c098c7d 100644 --- a/app/controllers/api/points_controller.rb +++ b/app/controllers/api/points_controller.rb @@ -1,4 +1,4 @@ -require_relative "../../lib/hstore_filter" +# require_relative "../../lib/mutations/hstore_filter" module Api class PointsController < Api::AbstractController diff --git a/app/controllers/api/rmq_utils_controller.rb b/app/controllers/api/rmq_utils_controller.rb index f60494758..a25c4c191 100644 --- a/app/controllers/api/rmq_utils_controller.rb +++ b/app/controllers/api/rmq_utils_controller.rb @@ -273,7 +273,7 @@ module Api end def current_device - @current_device ||= Auth::FromJWT.run!(jwt: password_param).device + @current_device ||= Auth::FromJwt.run!(jwt: password_param).device rescue Mutations::ValidationException => e raise JWT::VerificationError, "RMQ Provided bad token" end diff --git a/app/lib/amqp_log_parser.rb b/app/lib/amqp_log_parser.rb index d92c974dc..95613422d 100644 --- a/app/lib/amqp_log_parser.rb +++ b/app/lib/amqp_log_parser.rb @@ -11,16 +11,18 @@ class AmqpLogParser < Mutations::Command # I keep a Ruby copy of the JSON here for reference. # This is what a log will look like after JSON.parse() EXAMPLE_JSON = { - "meta" => { - "x" => 0, - "y" => 0, - "z" => 0, - "type" => "info", - }, - "major_version" => 6, # <= up-to-date bots do this - "message" => "HQ FarmBot TEST 123 Pin 13 is 0", - "created_at" => 1512585641, "channels" => [], + "created_at" => 1572015955, + "major_version" => 8, + "message" => "Syncing", + "meta" => {}, + "minor_version" => 1, + "patch_version" => 1, + "type" => "info", + "verbosity" => 3, + "x" => 0.0, + "y" => 0.0, + "z" => 0.0, } required do @@ -102,8 +104,19 @@ class AmqpLogParser < Mutations::Command end def find_problems! - @output.problems.push(NOT_HASH) and return if not_hash? - @output.problems.push(TOO_OLD) and return if major_version < 6 - @output.problems.push(DISCARD) and return if discard? + if not_hash? + @output.problems.push(NOT_HASH) + return + end + + if (major_version || 0) < 7 + @output.problems.push(TOO_OLD) + return + end + + if discard? + @output.problems.push(DISCARD) + return + end end end diff --git a/app/lib/celery_script/csheap.rb b/app/lib/celery_script/cs_heap.rb similarity index 90% rename from app/lib/celery_script/csheap.rb rename to app/lib/celery_script/cs_heap.rb index c3e98cd4d..4bf00626a 100644 --- a/app/lib/celery_script/csheap.rb +++ b/app/lib/celery_script/cs_heap.rb @@ -9,8 +9,8 @@ # * You need to create "traces" of where you are in a sequence (using numbers) # MORE INFO: https://github.com/FarmBot-Labs/Celery-Slicer module CeleryScript - # Supporting class for CSHeap (below this class) - # PROBLEM: CSHeap uses numbers to address sibling/parent nodes. + # Supporting class for CsHeap (below this class) + # PROBLEM: CsHeap uses numbers to address sibling/parent nodes. # PROBLEM: Numbers are very easy to mix up. Is it an array index? A SQL # primary key? A primitive value? It's not always easy to say. # SOLUTION: Create a `HeapAddress` value type to remove ambiguity. @@ -60,21 +60,22 @@ module CeleryScript end end - class CSHeap - class BadAddress < Exception; end; + class CsHeap + class BadAddress < Exception; end + BAD_ADDR = "Bad node address: " # Nodes that point to other nodes rather than primitive data types (eg: # `locals` and friends) will be prepended with a LINK. - LINK = "__" + LINK = "__" # Points to the originator (parent) of an `arg` or `body` node. - PARENT = (LINK + "parent").to_sym + PARENT = (LINK + "parent").to_sym # Points to the first element in the `body`` - BODY = (LINK + "body").to_sym + BODY = (LINK + "body").to_sym # Points to the next node in the body chain. Pointing to NOTHING indicates # the end of the body linked list. - NEXT = (LINK + "next").to_sym + NEXT = (LINK + "next").to_sym # Unique key name. See `celery_script_settings_bag.rb` - KIND = :__KIND__ + KIND = :__KIND__ COMMENT = :__COMMENT__ # Keys that primary nodes must have @@ -82,17 +83,16 @@ module CeleryScript # Index 0 of the heap represents a null pointer of sorts. # If a field points to this address, it is considered empty. - NULL = HeapAddress[0] + NULL = HeapAddress[0] # What you will find at index 0 of the heap: NOTHING = { - KIND => "nothing", + KIND => "nothing", PARENT => NULL, - BODY => NULL, - NEXT => NULL + BODY => NULL, + NEXT => NULL, } - # A dictionary of nodes in the CeleryScript tree, as stored in the heap. # Nodes will have: # * A `KIND` field - What kind of node is it? @@ -113,7 +113,7 @@ module CeleryScript # Set "here" to "null". Prepopulates "here" with an empty entry. def initialize - @here = NULL + @here = NULL @entries = { @here => NOTHING } end diff --git a/app/lib/celery_script/fetch_celery.rb b/app/lib/celery_script/fetch_celery.rb index ef21848de..8a9031aec 100644 --- a/app/lib/celery_script/fetch_celery.rb +++ b/app/lib/celery_script/fetch_celery.rb @@ -1,4 +1,4 @@ -require_relative "./csheap" +require_relative "./cs_heap" # Service object that: # 1. Pulls out all PrimaryNodes and EdgeNodes for a sequence node (AST Flat IR form) @@ -8,7 +8,7 @@ require_relative "./csheap" # DEFAULT. module CeleryScript class FetchCelery < Mutations::Command - private # = = = = = = = + private # = = = = = = = # This class is too CPU intensive to make multiple SQL requests. # To speed up querying, we create an in-memory index for frequently # looked up attributes such as :id, :kind, :parent_id, :primary_node_id @@ -60,7 +60,7 @@ module CeleryScript # that node's children (or an empty array, since body is always optional). def get_body_elements(origin) next_node = find_by_id_in_memory(origin.body_id) - results = [] + results = [] until next_node.kind == "nothing" results.push(next_node) next_node = find_by_id_in_memory(next_node[:next_id]) @@ -71,7 +71,7 @@ module CeleryScript # Top level function call for converting a single EdgeNode into a JSON # document. Returns Ruby hash that conforms to CeleryScript semantics. def recurse_into_node(node) - out = { kind: node.kind, args: recurse_into_args(node) } + out = { kind: node.kind, args: recurse_into_args(node) } body = get_body_elements(node) if body.empty? # Legacy sequences *must* have body on sequence. Others are fine. @@ -87,16 +87,17 @@ module CeleryScript # Eg: color, id, etc. def misc_fields return { - id: sequence.id, - created_at: sequence.created_at, - updated_at: sequence.updated_at, - args: Sequence::DEFAULT_ARGS, - color: sequence.color, - name: sequence.name - } + id: sequence.id, + created_at: sequence.created_at, + updated_at: sequence.updated_at, + args: Sequence::DEFAULT_ARGS, + color: sequence.color, + name: sequence.name, + } end - public # = = = = = = = + public # = = = = = = = + NO_SEQUENCE = "You must have a root node `sequence` at a minimum." required do diff --git a/app/lib/celery_script/first_pass.rb b/app/lib/celery_script/first_pass.rb index 64cf19f5e..fd020e474 100644 --- a/app/lib/celery_script/first_pass.rb +++ b/app/lib/celery_script/first_pass.rb @@ -1,7 +1,7 @@ -require_relative "./csheap" +require_relative "./cs_heap" # ABOUT THIS CLASS: -# CSHeap creates an in memory representation of a Flat IR tree using array +# CsHeap creates an in memory representation of a Flat IR tree using array # indexes (HeapAddress instances, really). This class takes a flat IR tree # from memory and converts `HeapAddress`es to SQL primary/foreign keys. module CeleryScript @@ -10,14 +10,14 @@ module CeleryScript # The following constants are abbreviations of the full name, since the # full name is quite long and they are referenced frequently in the code. # Just remember that "B" is "BODY", "K" is "KIND", etc... - B = CeleryScript::CSHeap::BODY - C = CeleryScript::CSHeap::COMMENT - K = CeleryScript::CSHeap::KIND - L = CeleryScript::CSHeap::LINK - N = CeleryScript::CSHeap::NEXT - P = CeleryScript::CSHeap::PARENT - NULL = CeleryScript::CSHeap::NULL - I = :instance + B = CeleryScript::CsHeap::BODY + C = CeleryScript::CsHeap::COMMENT + K = CeleryScript::CsHeap::KIND + L = CeleryScript::CsHeap::LINK + N = CeleryScript::CsHeap::NEXT + P = CeleryScript::CsHeap::PARENT + NULL = CeleryScript::CsHeap::NULL + I = :instance required do model :sequence, class: Sequence @@ -28,7 +28,7 @@ module CeleryScript def validate # IF YOU REMOVE THIS BAD STUFF WILL HAPPEN: # version is never user definable! - sequence_hash[:args] = \ + sequence_hash[:args] = Sequence::DEFAULT_ARGS.merge(sequence_hash[:args] || {}) # See comment above ^ TODO: Investigate removal now that EdgeNodes exist. end @@ -37,67 +37,67 @@ module CeleryScript Sequence.transaction do flat_ir .each do |node| - # Step 1- instantiate records. - node[I] = PrimaryNode.create!(kind: node[K], - sequence: sequence, - comment: node[C] || nil) - end + # Step 1- instantiate records. + node[I] = PrimaryNode.create!(kind: node[K], + sequence: sequence, + comment: node[C] || nil) + end .each_with_index do |node, index| - # Step 2- Assign SQL ids (not to be confused with array index IDs or - # instances of HeapAddress), also sets parent_arg_name - model = node[I] - model.parent_arg_name = parent_arg_name_for(node, index) - model.body_id = fetch_sql_id_for(B, node) - model.parent_id = fetch_sql_id_for(P, node) - model.next_id = fetch_sql_id_for(N, node) - node - end + # Step 2- Assign SQL ids (not to be confused with array index IDs or + # instances of HeapAddress), also sets parent_arg_name + model = node[I] + model.parent_arg_name = parent_arg_name_for(node, index) + model.body_id = fetch_sql_id_for(B, node) + model.parent_id = fetch_sql_id_for(P, node) + model.next_id = fetch_sql_id_for(N, node) + node + end .map do |node| - # Step 3- Set edge nodes - pairs = node - .to_a - .select do |x| - key = x.first.to_s - (x.first != I) && !key.starts_with?(L) - end - .map do |(key, value)| - EdgeNode.create!(kind: key, - value: value, - sequence_id: sequence.id, - primary_node_id: node[:instance].id) - end - node[:instance] + # Step 3- Set edge nodes + pairs = node + .to_a + .select do |x| + key = x.first.to_s + (x.first != I) && !key.starts_with?(L) end - .tap { |x| sequence.update_attributes(migrated_nodes: true) unless sequence.migrated_nodes } + .map do |(key, value)| + EdgeNode.create!(kind: key, + value: value, + sequence_id: sequence.id, + primary_node_id: node[:instance].id) + end + node[:instance] + end + .tap { |x| sequence.update(migrated_nodes: true) unless sequence.migrated_nodes } .map { |x| - x.save! if x.changed? - x - } + x.save! if x.changed? + x + } end end -private + private # Index every primary node in memory by its `HeapAddress`. # We need this info in order to fill out the `parent_arg_name` of a node. def every_primary_link @every_primary_link ||= flat_ir .map do |x| - x - .except(B,C,I,K,L,N,P) - .invert - .to_a - .select{|(k,v)| k.is_a?(HeapAddress)} - end + x + .except(B, C, I, K, L, N, P) + .invert + .to_a + .select { |(k, v)| k.is_a?(HeapAddress) } + end .map(&:to_h) .reduce({}, :merge) end def parent_arg_name_for(node, index) - resides_in_args = (node[N] == NULL) && (node[P] != NULL) - link_symbol = every_primary_link[HeapAddress[index]] + resides_in_args = (node[N] == NULL) && (node[P] != NULL) + link_symbol = every_primary_link[HeapAddress[index]] needs_p_arg_name = (resides_in_args && link_symbol) - parent_arg_name = (needs_p_arg_name ? link_symbol.to_s.gsub(L, "") : nil) + parent_arg_name = (needs_p_arg_name ? link_symbol.to_s.gsub(L, "") : nil) return parent_arg_name end @@ -107,7 +107,7 @@ private end def sequence_hash - @sequence_hash ||= \ + @sequence_hash ||= HashWithIndifferentAccess.new(kind: "sequence", args: args, body: body) end diff --git a/app/lib/celery_script/json_climber.rb b/app/lib/celery_script/json_climber.rb index 35cd5c934..8d9837090 100644 --- a/app/lib/celery_script/json_climber.rb +++ b/app/lib/celery_script/json_climber.rb @@ -2,18 +2,18 @@ module CeleryScript # THIS IS A MORE MINIMAL VERSION OF CeleryScript::TreeClimber. # It is a NON-VALIDATING tree climber. # Don't use this on unverified data structures. - class JSONClimber + class JsonClimber HASH_ONLY = "Expected a Hash." - NOT_NODE = "Expected hash with at least a `kind` and `args` prop." + NOT_NODE = "Expected hash with at least a `kind` and `args` prop." def self.climb(thing, &callable) raise HASH_ONLY unless thing.is_a?(Hash) - raise NOT_NODE unless is_node?(thing) + raise NOT_NODE unless is_node?(thing) go(thing, callable) thing end - private + private def self.is_node?(maybe) maybe.is_a?(Hash) && diff --git a/app/lib/celery_script/slicer.rb b/app/lib/celery_script/slicer.rb index 6a421cc08..e09d93614 100644 --- a/app/lib/celery_script/slicer.rb +++ b/app/lib/celery_script/slicer.rb @@ -1,4 +1,4 @@ -require_relative "./csheap.rb" +require_relative "./cs_heap.rb" # ORIGINAL IMPLEMENTATION HERE: https://github.com/FarmBot-Labs/Celery-Slicer # Take a nested ("canonical") representation of a CeleryScript sequence and # transforms it to a flat/homogenous intermediate representation which is better @@ -11,12 +11,12 @@ module CeleryScript raise "Not a hash" unless node.is_a?(Hash) @nesting_level = 0 @root_node = node - heap = CSHeap.new() - allocate(heap, node, CSHeap::NULL) + heap = CsHeap.new() + allocate(heap, node, CsHeap::NULL) @heap_values = heap.values @heap_values.map do |x| - x[CSHeap::BODY] ||= CSHeap::NULL - x[CSHeap::NEXT] ||= CSHeap::NULL + x[CsHeap::BODY] ||= CsHeap::NULL + x[CsHeap::NEXT] ||= CsHeap::NULL end heap.dump() end @@ -31,8 +31,8 @@ module CeleryScript def allocate(h, s, parentAddr) addr = h.allot(s[:kind]) - h.put(addr, CSHeap::PARENT, parentAddr) - h.put(addr, CSHeap::COMMENT, s[:comment]) if s[:comment] + h.put(addr, CsHeap::PARENT, parentAddr) + h.put(addr, CsHeap::COMMENT, s[:comment]) if s[:comment] iterate_over_body(h, s, addr) iterate_over_args(h, s, addr) addr @@ -44,7 +44,7 @@ module CeleryScript .map do |key| v = s[:args][key] if (is_celery_script(v)) - k = CSHeap::LINK + key.to_s + k = CsHeap::LINK + key.to_s h.put(parentAddr, k, allocate(h, v, parentAddr)) else h.put(parentAddr, key, v) @@ -64,12 +64,12 @@ module CeleryScript is_head = index == 0 # BE CAREFUL EDITING THIS LINE, YOU MIGHT BREAK `BODY` NODES: heap # See note above! - .put(previous_address, CSHeap::BODY, previous_address + 1) if is_head + .put(previous_address, CsHeap::BODY, previous_address + 1) if is_head my_heap_address = allocate(heap, canonical_list[index], previous_address) - prev_next_key = is_head ? CSHeap::NULL : my_heap_address - heap.put(previous_address, CSHeap::NEXT, prev_next_key) + prev_next_key = is_head ? CsHeap::NULL : my_heap_address + heap.put(previous_address, CsHeap::NEXT, prev_next_key) recurse_into_body(heap, canonical_list, my_heap_address, index + 1) end diff --git a/app/lib/log_service.rb b/app/lib/log_service.rb index 6ada163dc..87803cceb 100644 --- a/app/lib/log_service.rb +++ b/app/lib/log_service.rb @@ -2,10 +2,11 @@ # Listens to *ALL* incoming logs and stores them to the DB. # Also handles throttling. class LogService < AbstractServiceRunner - T = ThrottlePolicy::TimePeriod - THROTTLE_POLICY = ThrottlePolicy.new T.new(1.minute) => 0.5 * 1_000, - T.new(1.hour) => 0.5 * 10_000, - T.new(1.day) => 0.5 * 100_000 + THROTTLE_POLICY = ThrottlePolicy.new(name, { + 1.minute => 0.5 * 1_000, + 1.hour => 0.5 * 10_000, + 1.day => 0.5 * 100_000, + }) LOG_TPL = Rails.env.test? ? "\e[32m.\e[0m" : "FBOS LOG (device_%s): %s\n" diff --git a/app/lib/hstore_filter.rb b/app/lib/mutations/hstore_filter.rb similarity index 100% rename from app/lib/hstore_filter.rb rename to app/lib/mutations/hstore_filter.rb diff --git a/app/lib/telemetry_service.rb b/app/lib/telemetry_service.rb index e49696d56..3a61ec4c1 100644 --- a/app/lib/telemetry_service.rb +++ b/app/lib/telemetry_service.rb @@ -4,10 +4,10 @@ class TelemetryService < AbstractServiceRunner MESSAGE = "TELEMETRY MESSAGE FROM %s" FAILURE = "FAILED TELEMETRY MESSAGE FROM %s" - THROTTLE_POLICY = ThrottlePolicy.new({ - ThrottlePolicy::TimePeriod.new(1.minute) => 25, - ThrottlePolicy::TimePeriod.new(1.hour) => 250, - ThrottlePolicy::TimePeriod.new(1.day) => 1500, + THROTTLE_POLICY = ThrottlePolicy.new(name, { + 1.minute => 25, + 1.hour => 250, + 1.day => 1500, }) def process(delivery_info, payload) diff --git a/app/lib/throttle_policy.rb b/app/lib/throttle_policy.rb index 9473de435..978db660e 100644 --- a/app/lib/throttle_policy.rb +++ b/app/lib/throttle_policy.rb @@ -1,27 +1,30 @@ # Handles devices that spin out of control and send too many logs to the server. # Class Hierarchy: -# ThrottlePolicy has => Rules creates => Violation -# Violation has => Rule has => TimePeriod +# ThrottlePolicy +# \ +# +----> Rule --> TimePeriod +# |\ +# | `--> Rule --> TimePeriod +# \_ +# `-> Rule --> TimePeriod class ThrottlePolicy attr_reader :rules # Dictionary - def initialize(policy_rules) - @rules = policy_rules.map { |rule_set| Rule.new(*rule_set) } + def initialize(namespace, rule_map, now = Time.now) + @rules = rule_map + .map { |(period, limit)| Rule.new(namespace, period, limit, now) } end def track(unique_id, now = Time.now) - rules.each { |r| r.time_period.record_event(unique_id, now) } + rules.each { |r| r.record_event(unique_id, now) } end # If throttled, returns the timeperiod when device will be unthrottled # returns nil if not throttled def is_throttled(unique_id) rules - .map do |rule| - is_violation = rule.time_period.usage_count_for(unique_id) > rule.limit - is_violation ? Violation.new(rule) : nil - end + .map { |rule| rule.violation?(unique_id) } .compact .max end diff --git a/app/lib/throttle_policy/rule.rb b/app/lib/throttle_policy/rule.rb index a7fbde3f9..32e3ab736 100644 --- a/app/lib/throttle_policy/rule.rb +++ b/app/lib/throttle_policy/rule.rb @@ -3,8 +3,21 @@ class ThrottlePolicy class Rule attr_reader :time_period, :limit - def initialize(time_period, limit) - @time_period, @limit = time_period, limit + def initialize(namespace, time_period, limit, now = Time.now) + @time_period = ThrottlePolicy::TimePeriod.new(namespace, time_period, now) + @limit = limit + end + + # returns the timeperiod when device will be + # unthrottled. returns `nil` if not throttled + def violation?(unique_id) + if (time_period.usage_count_for(unique_id) > limit) + Violation.new(self) + end + end + + def record_event(unique_id, now) + time_period.record_event(unique_id, now) end end end diff --git a/app/lib/throttle_policy/time_period.rb b/app/lib/throttle_policy/time_period.rb index ad77b8b6d..ea47aec4f 100644 --- a/app/lib/throttle_policy/time_period.rb +++ b/app/lib/throttle_policy/time_period.rb @@ -10,48 +10,66 @@ # the initiator ID. class ThrottlePolicy class TimePeriod - attr_reader :time_unit, - :current_period, # Slice time into fixed size windows - :entries + attr_reader :time_unit, :current_period - def initialize(active_support_duration, now = Time.now) - @time_unit = active_support_duration - reset_everything now + def initialize(namespace, duration, now = Time.now) + @time_unit = duration + @namespace = namespace + reset_everything(now) end - def record_event(unique_id, now = Time.now) period = calculate_period(now) case period <=> current_period - when -1 then return # Out of date- don't record. - when 0 then increment_count_for(unique_id) # Right on schedule. - when 1 then reset_everything(now) # Clear out old data. + when -1 then return # Out of date- don't record. + when 0 then increment_count_for(unique_id) # Right on schedule. + when 1 then reset_everything(now) # Clear out old data. end end def usage_count_for(unique_id) - @entries[unique_id] || 0 + fetch(unique_id) end def when_does_next_period_start? Time.at(current_period * time_unit.to_i) + time_unit end - private + private def reset_everything(now) @current_period = calculate_period(now) - @entries = {} + reset_cache end def increment_count_for(unique_id) - @entries[unique_id] ||= 0 - @entries[unique_id] += 1 + incr(unique_id) end # Returns integer representation of current clock period def calculate_period(time) (time.to_i / @time_unit) end + + def redis + Rails.cache.redis + end + + def cache_key(unique_id) + [@namespace, current_period.to_i, unique_id].join(":") + end + + def incr(unique_id) + redis.incr(cache_key(unique_id)) + end + + def fetch(unique_id) + (redis.get(cache_key(unique_id)) || "0").to_i + end + + def reset_cache + keys = redis.keys([@namespace, "*"].join(":")) + redis.del(*keys) unless keys.empty? + end end end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 7b464f456..aa5793e0b 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,6 +1,7 @@ class ApplicationRecord < ActiveRecord::Base self.abstract_class = true - after_save :maybe_broadcast, on: [:create, :update] + after_create :maybe_broadcast + after_update :maybe_broadcast after_destroy :maybe_broadcast class << self @@ -37,7 +38,7 @@ class ApplicationRecord < ActiveRecord::Base def self.auto_sync_debounce @auto_sync_paused = true result = yield - result.update_attributes!(updated_at: Time.now) + result.update!(updated_at: Time.now) @auto_sync_paused = false result.broadcast! result @@ -94,7 +95,7 @@ class ApplicationRecord < ActiveRecord::Base def manually_sync! device && (device.auto_sync_transaction do - update_attributes!(updated_at: Time.now) + update!(updated_at: Time.now) end) self end diff --git a/app/models/device.rb b/app/models/device.rb index f93e87c40..f1c4409b9 100644 --- a/app/models/device.rb +++ b/app/models/device.rb @@ -118,8 +118,8 @@ class Device < ApplicationRecord end_t = violation.ends_at # Some log validation errors will result in until_time being `nil`. if (throttled_until.nil? || end_t > throttled_until) - reload.update_attributes!(throttled_until: end_t, - throttled_at: Time.now) + reload.update!(throttled_until: end_t, + throttled_at: Time.now) refresh_cache cooldown = end_t.in_time_zone(self.timezone || "UTC").strftime("%I:%M%p") info = [violation.explanation, cooldown] @@ -131,7 +131,7 @@ class Device < ApplicationRecord if throttled_until.present? old_time = throttled_until reload # <= WHY!?! TODO: Find out why it crashes without this. - .update_attributes!(throttled_until: nil, throttled_at: nil) + .update!(throttled_until: nil, throttled_at: nil) refresh_cache cooldown_notice(THROTTLE_OFF, old_time, "info") end @@ -203,7 +203,7 @@ class Device < ApplicationRecord last_sent_at = device.mqtt_rate_limit_email_sent_at || 4.years.ago if last_sent_at < 1.day.ago - device.update_attributes!(mqtt_rate_limit_email_sent_at: Time.now) + device.update!(mqtt_rate_limit_email_sent_at: Time.now) device.tell(TOO_MANY_CONNECTIONS, ["fatal_email"]) end end diff --git a/app/models/farmware_installation.rb b/app/models/farmware_installation.rb index ff57fb74d..0847f8709 100644 --- a/app/models/farmware_installation.rb +++ b/app/models/farmware_installation.rb @@ -40,7 +40,7 @@ class FarmwareInstallation < ApplicationRecord known_error = KNOWN_PROBLEMS[error.class] description = \ known_error || (OTHER_PROBLEM % error.class) - update_attributes!(package_error: description, + update!(package_error: description, package: nil) unless known_error.present? raise error @@ -54,7 +54,7 @@ class FarmwareInstallation < ApplicationRecord string = string_io.read(MAX_JSON_SIZE) json = JSON.parse(string) pkg_name = json.fetch("package") - update_attributes!(package: pkg_name, package_error: nil) + update!(package: pkg_name, package_error: nil) rescue => error maybe_recover_from_fetch_error(error) end diff --git a/app/models/fbos_config.rb b/app/models/fbos_config.rb index 1317118ae..ba189245a 100644 --- a/app/models/fbos_config.rb +++ b/app/models/fbos_config.rb @@ -3,7 +3,8 @@ class FbosConfig < ApplicationRecord class MissingSerial < StandardError; end belongs_to :device - after_save :maybe_sync_nerves, on: [:create, :update] + after_create :maybe_sync_nerves + after_update :maybe_sync_nerves FIRMWARE_HARDWARE = [ NOT_SET = nil, diff --git a/app/models/global_config.rb b/app/models/global_config.rb index 928d445c9..48ebb0d72 100644 --- a/app/models/global_config.rb +++ b/app/models/global_config.rb @@ -28,7 +28,7 @@ class GlobalConfig < ApplicationRecord # Remove it if the demo tour does not require it. "MQTT_WS" => SessionToken::MQTT_WS, }.map do |(key, value)| - self.find_or_create_by(key: key).update_attributes(key: key, value: value) + self.find_or_create_by(key: key).update(key: key, value: value) end # Memoized version of every GlobalConfig, with key/values layed out in a hash. diff --git a/app/models/image.rb b/app/models/image.rb index 6f5f387cf..81a154849 100644 --- a/app/models/image.rb +++ b/app/models/image.rb @@ -40,16 +40,9 @@ class Image < ApplicationRecord # Worst case scenario for 1280x1280 BMP. GCS_BUCKET_NAME = ENV["GCS_BUCKET"] - # ========= DEPRECATED PAPERCLIP STUFF ========= - # has_attached_file :attachment, CONFIG - # validates_attachment_content_type :attachment, - # content_type: CONTENT_TYPES - # ========= /DEPRECATED PAPERCLIP STUFF ======== has_one_attached :attachment def set_attachment_by_url(url) - # File - # URI::HTTPS attachment.attach(io: open(url), filename: "image_#{self.id}") self.attachment_processed_at = Time.now self diff --git a/app/models/tool_slot.rb b/app/models/tool_slot.rb index e67a9dade..22a0abd90 100644 --- a/app/models/tool_slot.rb +++ b/app/models/tool_slot.rb @@ -11,7 +11,9 @@ class ToolSlot < Point MIN_PULLOUT = PULLOUT_DIRECTIONS.min PULLOUT_ERR = "must be a value between #{MIN_PULLOUT} and #{MAX_PULLOUT}. "\ "%{value} is not valid." - IN_USE = "already in use by another tool slot" + IN_USE = "already in use by another tool slot. "\ + "Please un-assign the tool from its current slot"\ + " before reassigning." belongs_to :tool validates_uniqueness_of :tool, diff --git a/app/mutations/auth/create_token.rb b/app/mutations/auth/create_token.rb index 4e9886486..00be498cb 100644 --- a/app/mutations/auth/create_token.rb +++ b/app/mutations/auth/create_token.rb @@ -26,7 +26,7 @@ module Auth end def execute - @user.update_attributes(agreed_to_terms_at: Time.now) if agree_to_terms + @user.update(agreed_to_terms_at: Time.now) if agree_to_terms SessionToken.as_json(@user, aud, fbos_version) end diff --git a/app/mutations/auth/from_jwt.rb b/app/mutations/auth/from_jwt.rb index ed016d570..c71865eb9 100644 --- a/app/mutations/auth/from_jwt.rb +++ b/app/mutations/auth/from_jwt.rb @@ -1,7 +1,7 @@ module Auth # The API supports a number of authentication strategies (Cookies, Bot token, # JWT). This service helps determine which auth strategy to use. - class FromJWT < Mutations::Command + class FromJwt < Mutations::Command required { string :jwt } def execute diff --git a/app/mutations/configs/update.rb b/app/mutations/configs/update.rb index 12fe971b5..75aa9f0c8 100644 --- a/app/mutations/configs/update.rb +++ b/app/mutations/configs/update.rb @@ -5,7 +5,7 @@ module Configs GOOD = 5556 required do - duck :target, methods: [:update_attributes!] + duck :target, methods: [:update!] duck :update_attrs, methods: [:deep_symbolize_keys] end diff --git a/app/mutations/device_certs/create.rb b/app/mutations/device_certs/create.rb index 3b6679aaf..b4ae96c01 100644 --- a/app/mutations/device_certs/create.rb +++ b/app/mutations/device_certs/create.rb @@ -10,7 +10,7 @@ module DeviceCerts SendNervesHubInfoJob.perform_later(device_id: device.id, serial_number: serial_number, tags: tags) - return device.update_attributes!(serial_number: serial_number) && device + return device.update!(serial_number: serial_number) && device end end end diff --git a/app/mutations/devices/create.rb b/app/mutations/devices/create.rb index 6dccc0980..b591359d1 100644 --- a/app/mutations/devices/create.rb +++ b/app/mutations/devices/create.rb @@ -24,7 +24,7 @@ module Devices # when we were using MongoDB. This can be # safely removed now. - RC 11-APR-19 old_device = user.device - user.update_attributes!(device_id: device.id) # Detach from old one. + user.update!(device_id: device.id) # Detach from old one. # Remove userless devices. old_device.destroy! if old_device && device.users.count < 1 end diff --git a/app/mutations/devices/destroy.rb b/app/mutations/devices/destroy.rb index bd2e170ac..0b714a8b3 100644 --- a/app/mutations/devices/destroy.rb +++ b/app/mutations/devices/destroy.rb @@ -7,7 +7,7 @@ module Devices def execute ActiveRecord::Base.transaction do - user.update_attributes!(device: Devices::Create.run!(user: user)) + user.update!(device: Devices::Create.run!(user: user)) device.destroy! if device.reload.users.count < 1 end true diff --git a/app/mutations/devices/reset.rb b/app/mutations/devices/reset.rb index a42be27e3..e5be64e4d 100644 --- a/app/mutations/devices/reset.rb +++ b/app/mutations/devices/reset.rb @@ -20,7 +20,7 @@ module Devices def run_it ActiveRecord::Base.transaction do - device.update_attributes!(name: "FarmBot") + device.update!(name: "FarmBot") Device::SINGULAR_RESOURCES.keys.map do |resource| device.send(resource).destroy! end diff --git a/app/mutations/devices/seeders/abstract_express.rb b/app/mutations/devices/seeders/abstract_express.rb index 8d5eb7ead..61456fad7 100644 --- a/app/mutations/devices/seeders/abstract_express.rb +++ b/app/mutations/devices/seeders/abstract_express.rb @@ -2,14 +2,14 @@ module Devices module Seeders class AbstractExpress < AbstractGenesis def settings_device_name - device.update_attributes!(name: "FarmBot Express") + device.update!(name: "FarmBot Express") end def sensors_soil_sensor; end def sensors_tool_verification; end def settings_enable_encoders - device.firmware_config.update_attributes!(encoder_enabled_x: 0, + device.firmware_config.update!(encoder_enabled_x: 0, encoder_enabled_y: 0, encoder_enabled_z: 0) end @@ -17,7 +17,7 @@ module Devices def settings_firmware device .fbos_config - .update_attributes!(firmware_hardware: FbosConfig::EXPRESS_K10) + .update!(firmware_hardware: FbosConfig::EXPRESS_K10) end def tool_slots_slot_1 @@ -91,11 +91,11 @@ module Devices def sequences_unmount_tool; end def settings_default_map_size_y - device.web_app_config.update_attributes!(map_size_y: 1_200) + device.web_app_config.update!(map_size_y: 1_200) end def settings_hide_sensors - device.web_app_config.update_attributes!(hide_sensors: true) + device.web_app_config.update!(hide_sensors: true) end private diff --git a/app/mutations/devices/seeders/abstract_genesis.rb b/app/mutations/devices/seeders/abstract_genesis.rb index 19c137140..b52665768 100644 --- a/app/mutations/devices/seeders/abstract_genesis.rb +++ b/app/mutations/devices/seeders/abstract_genesis.rb @@ -18,11 +18,11 @@ module Devices end def settings_device_name - device.update_attributes!(name: "FarmBot Genesis") + device.update!(name: "FarmBot Genesis") end def settings_enable_encoders - device.firmware_config.update_attributes!(encoder_enabled_x: 1, + device.firmware_config.update!(encoder_enabled_x: 1, encoder_enabled_y: 1, encoder_enabled_z: 1) end @@ -146,15 +146,15 @@ module Devices def settings_firmware device .fbos_config - .update_attributes!(firmware_hardware: FbosConfig::FARMDUINO) + .update!(firmware_hardware: FbosConfig::FARMDUINO) end def settings_default_map_size_x - device.web_app_config.update_attributes!(map_size_x: 2_900) + device.web_app_config.update!(map_size_x: 2_900) end def settings_default_map_size_y - device.web_app_config.update_attributes!(map_size_y: 1_400) + device.web_app_config.update!(map_size_y: 1_400) end def pin_bindings_button_1 diff --git a/app/mutations/devices/seeders/abstract_seeder.rb b/app/mutations/devices/seeders/abstract_seeder.rb index d14636a2d..fd6bc4ba1 100644 --- a/app/mutations/devices/seeders/abstract_seeder.rb +++ b/app/mutations/devices/seeders/abstract_seeder.rb @@ -72,7 +72,7 @@ module Devices end def settings_hide_sensors - device.web_app_config.update_attributes!(hide_sensors: false) + device.web_app_config.update!(hide_sensors: false) end def plants diff --git a/app/mutations/devices/seeders/demo_account_seeder.rb b/app/mutations/devices/seeders/demo_account_seeder.rb index 44b6312f8..91bfaa5d7 100644 --- a/app/mutations/devices/seeders/demo_account_seeder.rb +++ b/app/mutations/devices/seeders/demo_account_seeder.rb @@ -55,10 +55,10 @@ module Devices .map { |p| p.merge(device: device) } .map { |p| Alerts::Create.run!(p) } device - .update_attributes!(fbos_version: READ_COMMENT_ABOVE) + .update!(fbos_version: READ_COMMENT_ABOVE) device .web_app_config - .update_attributes!(discard_unsaved: true) + .update!(discard_unsaved: true) end end end diff --git a/app/mutations/devices/seeders/express_xl_one_zero.rb b/app/mutations/devices/seeders/express_xl_one_zero.rb index 0dc760b20..c951e0274 100644 --- a/app/mutations/devices/seeders/express_xl_one_zero.rb +++ b/app/mutations/devices/seeders/express_xl_one_zero.rb @@ -2,15 +2,15 @@ module Devices module Seeders class ExpressXlOneZero < AbstractExpress def settings_device_name - device.update_attributes!(name: "FarmBot Express XL") + device.update!(name: "FarmBot Express XL") end def settings_default_map_size_x - device.web_app_config.update_attributes!(map_size_x: 6_000) + device.web_app_config.update!(map_size_x: 6_000) end def settings_default_map_size_y - device.web_app_config.update_attributes!(map_size_y: 2_400) + device.web_app_config.update!(map_size_y: 2_400) end end end diff --git a/app/mutations/devices/seeders/genesis_one_four.rb b/app/mutations/devices/seeders/genesis_one_four.rb index 07dff1684..c4976559f 100644 --- a/app/mutations/devices/seeders/genesis_one_four.rb +++ b/app/mutations/devices/seeders/genesis_one_four.rb @@ -4,7 +4,7 @@ module Devices def settings_firmware device .fbos_config - .update_attributes!(firmware_hardware: FbosConfig::FARMDUINO_K14) + .update!(firmware_hardware: FbosConfig::FARMDUINO_K14) end end end diff --git a/app/mutations/devices/seeders/genesis_one_two.rb b/app/mutations/devices/seeders/genesis_one_two.rb index 6495c2641..f6e7b6764 100644 --- a/app/mutations/devices/seeders/genesis_one_two.rb +++ b/app/mutations/devices/seeders/genesis_one_two.rb @@ -12,7 +12,7 @@ module Devices def settings_firmware device .fbos_config - .update_attributes!(firmware_hardware: FbosConfig::ARDUINO) + .update!(firmware_hardware: FbosConfig::ARDUINO) end def peripherals_lighting; end diff --git a/app/mutations/devices/seeders/genesis_xl_one_four.rb b/app/mutations/devices/seeders/genesis_xl_one_four.rb index 79050e136..f15316983 100644 --- a/app/mutations/devices/seeders/genesis_xl_one_four.rb +++ b/app/mutations/devices/seeders/genesis_xl_one_four.rb @@ -4,19 +4,19 @@ module Devices def settings_firmware device .fbos_config - .update_attributes!(firmware_hardware: FbosConfig::FARMDUINO_K14) + .update!(firmware_hardware: FbosConfig::FARMDUINO_K14) end def settings_device_name - device.update_attributes!(name: "FarmBot Genesis XL") + device.update!(name: "FarmBot Genesis XL") end def settings_default_map_size_x - device.web_app_config.update_attributes!(map_size_x: 5_900) + device.web_app_config.update!(map_size_x: 5_900) end def settings_default_map_size_y - device.web_app_config.update_attributes!(map_size_y: 2_900) + device.web_app_config.update!(map_size_y: 2_900) end end end diff --git a/app/mutations/devices/sync.rb b/app/mutations/devices/sync.rb index 0343fcea3..88ba3fbd4 100644 --- a/app/mutations/devices/sync.rb +++ b/app/mutations/devices/sync.rb @@ -2,6 +2,7 @@ module Devices class Sync < Mutations::Command SEL = "SELECT id, updated_at FROM" WHERE = "WHERE device_id = " + FORMAT = "%Y-%m-%d %H:%M:%S.%5N" def self.basic_query(plural_resource, where = WHERE) [SEL, plural_resource, where].join(" ") diff --git a/app/mutations/devices/update.rb b/app/mutations/devices/update.rb index 922507542..e6da689b1 100644 --- a/app/mutations/devices/update.rb +++ b/app/mutations/devices/update.rb @@ -21,7 +21,7 @@ module Devices def execute p = inputs.except(:device).merge(mounted_tool_data) - device.update_attributes!(p) + device.update!(p) device end diff --git a/app/mutations/farm_events/fragment_helpers.rb b/app/mutations/farm_events/fragment_helpers.rb index 4cd0a2367..653f92c54 100644 --- a/app/mutations/farm_events/fragment_helpers.rb +++ b/app/mutations/farm_events/fragment_helpers.rb @@ -56,7 +56,7 @@ module FarmEvents FarmEvents => ->() { farm_event }, Regimens => ->() { regimen }, } - options.fetch(self.class.parent).call() + options.fetch(self.class.module_parent).call() end end end diff --git a/app/mutations/farm_events/update.rb b/app/mutations/farm_events/update.rb index f7e48b1b3..38720ce3c 100644 --- a/app/mutations/farm_events/update.rb +++ b/app/mutations/farm_events/update.rb @@ -33,7 +33,7 @@ module FarmEvents FarmEvent.auto_sync_debounce do FarmEvent.transaction do handle_body_field - farm_event.update_attributes!(p) + farm_event.update!(p) farm_event end end diff --git a/app/mutations/farmware_envs/update.rb b/app/mutations/farmware_envs/update.rb index 07be7bef0..4986ebe39 100644 --- a/app/mutations/farmware_envs/update.rb +++ b/app/mutations/farmware_envs/update.rb @@ -10,7 +10,7 @@ module FarmwareEnvs end def execute - farmware_env.update_attributes!(inputs.except(:farmware_env)) && farmware_env + farmware_env.update!(inputs.except(:farmware_env)) && farmware_env end end end diff --git a/app/mutations/password_resets/update.rb b/app/mutations/password_resets/update.rb index 479774122..86c3f831d 100644 --- a/app/mutations/password_resets/update.rb +++ b/app/mutations/password_resets/update.rb @@ -14,7 +14,7 @@ module PasswordResets end def execute - user.update_attributes!(password: password, + user.update!(password: password, password_confirmation: password_confirmation) Auth::CreateToken.run!(email: user.email, password: password, diff --git a/app/mutations/peripherals/update.rb b/app/mutations/peripherals/update.rb index d11f00be3..086911b68 100644 --- a/app/mutations/peripherals/update.rb +++ b/app/mutations/peripherals/update.rb @@ -11,7 +11,7 @@ module Peripherals end def execute - peripheral.update_attributes!(inputs.except(:peripheral, :device)) + peripheral.update!(inputs.except(:peripheral, :device)) peripheral end end diff --git a/app/mutations/pin_bindings/update.rb b/app/mutations/pin_bindings/update.rb index 3b64f0c6f..468d15238 100644 --- a/app/mutations/pin_bindings/update.rb +++ b/app/mutations/pin_bindings/update.rb @@ -21,7 +21,7 @@ module PinBindings def execute x = inputs.except(:pin_binding, :device) - pin_binding.update_attributes!(x) && pin_binding + pin_binding.update!(x) && pin_binding end end end diff --git a/app/mutations/plant_templates/update.rb b/app/mutations/plant_templates/update.rb index fbbbe1fec..6e6086dea 100644 --- a/app/mutations/plant_templates/update.rb +++ b/app/mutations/plant_templates/update.rb @@ -16,7 +16,7 @@ module PlantTemplates end def execute - plant_template.update_attributes!(update_params) + plant_template.update!(update_params) plant_template end diff --git a/app/mutations/point_groups/update.rb b/app/mutations/point_groups/update.rb index d322cc96f..fc676a72b 100644 --- a/app/mutations/point_groups/update.rb +++ b/app/mutations/point_groups/update.rb @@ -15,7 +15,7 @@ module PointGroups end def validate - validate_point_ids if point_ids.any? + validate_point_ids if point_ids.present? validate_sort_type end @@ -24,7 +24,7 @@ module PointGroups PointGroup.transaction do PointGroupItem.transaction do maybe_reconcile_points - point_group.update_attributes!(update_attributes) + point_group.update!(update_attributes) point_group.reload # <= Because PointGroupItem caching? end end @@ -38,19 +38,26 @@ module PointGroups end def maybe_reconcile_points + # Nil means "ignore" + # [] means "reset" + return if point_ids.nil? + # STEP 0: Setup - @old_point_ids = Set.new(point_group.point_group_items.pluck(:id)) + @old_point_ids = Set.new(point_group.point_group_items.pluck(:point_id)) @new_point_ids = Set.new(point_ids) @dont_delete = @new_point_ids & @old_point_ids @do_delete = (@old_point_ids - @dont_delete).to_a # STEP 1: "Garbage Collection" of PGIs that are no longer used. - PointGroupItem.where(id: @do_delete).map(&:destroy!) + PointGroupItem + .where(point_group_id: point_group.id) + .where(point_id: @do_delete) + .destroy_all # STEP 2: Create missing PGIs @do_create = (@new_point_ids - @dont_delete) - PointGroupItem.create!(@do_create.to_a.uniq.map do |id| - { point_id: id, point_group_id: point_group.id } + PointGroupItem.create!(@do_create.to_a.uniq.map do |point_id| + { point_id: point_id, point_group_id: point_group.id } end) end end diff --git a/app/mutations/points/create.rb b/app/mutations/points/create.rb index aae403f14..b84ac4a2c 100644 --- a/app/mutations/points/create.rb +++ b/app/mutations/points/create.rb @@ -1,5 +1,5 @@ -require_relative "../../lib/hstore_filter" -# WHY??? ^ +require_relative "../../lib/mutations/hstore_filter.rb" + module Points class Create < Mutations::Command # WHY 1000?: diff --git a/app/mutations/points/destroy.rb b/app/mutations/points/destroy.rb index 605698fd8..b76996d14 100644 --- a/app/mutations/points/destroy.rb +++ b/app/mutations/points/destroy.rb @@ -73,7 +73,7 @@ module Points # a fresh session_id, the frontend will # think it is an "echo" and cancel it out. # """ - Rick - x.update_attributes!(updated_at: Time.now) + x.update!(updated_at: Time.now) x.broadcast!(SecureRandom.uuid) end end diff --git a/app/mutations/points/query.rb b/app/mutations/points/query.rb index 84b97347b..4c860258e 100644 --- a/app/mutations/points/query.rb +++ b/app/mutations/points/query.rb @@ -1,47 +1,47 @@ -require_relative "../../lib/hstore_filter" +# require_relative "../../lib/hstore_filter" # WHY??? ^ module Points - class Query < Mutations::Command - H_QUERY = "meta -> :key = :value" + class Query < Mutations::Command + H_QUERY = "meta -> :key = :value" - required do - duck :points, method: [:where] - end + required do + duck :points, method: [:where] + end - optional do - float :radius - float :x - float :y - float :z - hstore :meta - string :name - string :pointer_type, in: Point::POINTER_KINDS - string :plant_stage, in: CeleryScriptSettingsBag::PLANT_STAGES - string :openfarm_slug - end + optional do + float :radius + float :x + float :y + float :z + hstore :meta + string :name + string :pointer_type, in: Point::POINTER_KINDS + string :plant_stage, in: CeleryScriptSettingsBag::PLANT_STAGES + string :openfarm_slug + end - def execute - search_results - end + def execute + search_results + end - def search_results - @search_results ||= conditions.reduce(points) do |collection, query| - collection.where(query) - end - end - - def conditions - @conditions ||= regular_conditions + meta_conditions - end - - def meta_conditions - @meta_conditions ||= (meta || {}).map do |(k,v)| - [H_QUERY, {key: k, value: v}] - end - end - - def regular_conditions - @regular_conditions ||= [inputs.except(:points, :meta)] + def search_results + @search_results ||= conditions.reduce(points) do |collection, query| + collection.where(query) end end + + def conditions + @conditions ||= regular_conditions + meta_conditions + end + + def meta_conditions + @meta_conditions ||= (meta || {}).map do |(k, v)| + [H_QUERY, { key: k, value: v }] + end + end + + def regular_conditions + @regular_conditions ||= [inputs.except(:points, :meta)] + end + end end diff --git a/app/mutations/points/update.rb b/app/mutations/points/update.rb index 712a08605..b70691e59 100644 --- a/app/mutations/points/update.rb +++ b/app/mutations/points/update.rb @@ -1,24 +1,24 @@ -require_relative "../../lib/hstore_filter" +# require_relative "../../lib/hstore_filter" module Points class Update < Mutations::Command required do model :device, class: Device - model :point, class: Point + model :point, class: Point end optional do integer :tool_id, nils: true, empty_is_nil: true - float :x - float :y - float :z - float :radius - string :name - string :openfarm_slug + float :x + float :y + float :z + float :radius + string :name + string :openfarm_slug integer :pullout_direction, in: ToolSlot::PULLOUT_DIRECTIONS - string :plant_stage, in: CeleryScriptSettingsBag::PLANT_STAGES - time :planted_at - hstore :meta + string :plant_stage, in: CeleryScriptSettingsBag::PLANT_STAGES + time :planted_at + hstore :meta boolean :gantry_mounted end @@ -27,10 +27,10 @@ module Points end def execute - Point.transaction { point.update_attributes!(inputs.except(:point)) && point } + Point.transaction { point.update!(inputs.except(:point)) && point } end - private + private def new_tool_id? raw_inputs.key?("tool_id") diff --git a/app/mutations/regimens/update.rb b/app/mutations/regimens/update.rb index 9424c5581..9ad90a877 100644 --- a/app/mutations/regimens/update.rb +++ b/app/mutations/regimens/update.rb @@ -27,7 +27,7 @@ module Regimens RegimenItem.new(ri).tap { |r| r.validate! } end handle_body_field - regimen.update_attributes!(inputs.slice(:name, :color, :regimen_items)) + regimen.update!(inputs.slice(:name, :color, :regimen_items)) regimen end end diff --git a/app/mutations/saved_gardens/update.rb b/app/mutations/saved_gardens/update.rb index d3a3b01c9..84e9a358f 100644 --- a/app/mutations/saved_gardens/update.rb +++ b/app/mutations/saved_gardens/update.rb @@ -9,7 +9,7 @@ module SavedGardens end def execute - saved_garden.update_attributes!(inputs.except(:saved_garden)) + saved_garden.update!(inputs.except(:saved_garden)) saved_garden end end diff --git a/app/mutations/sensors/update.rb b/app/mutations/sensors/update.rb index 4b2214ff4..33172cc66 100644 --- a/app/mutations/sensors/update.rb +++ b/app/mutations/sensors/update.rb @@ -9,7 +9,7 @@ module Sensors end def execute - sensor.update_attributes!(inputs.except(:sensor)) + sensor.update!(inputs.except(:sensor)) sensor end end diff --git a/app/mutations/sequences/update.rb b/app/mutations/sequences/update.rb index c234bc44d..6ddaad40a 100644 --- a/app/mutations/sequences/update.rb +++ b/app/mutations/sequences/update.rb @@ -40,7 +40,7 @@ module Sequences Sequence.auto_sync_debounce do ActiveRecord::Base.transaction do sequence.migrated_nodes = true - sequence.update_attributes!(inputs.except(*BLACKLIST)) + sequence.update!(inputs.except(*BLACKLIST)) CeleryScript::StoreCelery.run!(sequence: sequence, args: args, body: body) diff --git a/app/mutations/tools/update.rb b/app/mutations/tools/update.rb index 4337f4ce7..add2d9f4f 100644 --- a/app/mutations/tools/update.rb +++ b/app/mutations/tools/update.rb @@ -9,7 +9,7 @@ module Tools end def execute - tool.update_attributes!(inputs.except(:tool)) && tool + tool.update!(inputs.except(:tool)) && tool end end end diff --git a/app/mutations/users/reverify.rb b/app/mutations/users/reverify.rb index 69b0b7e83..590b5032a 100644 --- a/app/mutations/users/reverify.rb +++ b/app/mutations/users/reverify.rb @@ -3,7 +3,7 @@ module Users required { model :user, class: User } def execute - user.update_attributes!(confirmed_at: Time.now, + user.update!(confirmed_at: Time.now, email: user.unconfirmed_email, unconfirmed_email: nil) fbos_vers = Gem::Version.new("99.9.9") # Not relevant here, stubbing out. diff --git a/app/mutations/users/update.rb b/app/mutations/users/update.rb index df8f4d7fc..e1e8e8e1f 100644 --- a/app/mutations/users/update.rb +++ b/app/mutations/users/update.rb @@ -21,7 +21,7 @@ module Users def execute maybe_perform_password_reset - user.update_attributes!(calculated_update) + user.update!(calculated_update) user.reload end diff --git a/app/mutations/webcam_feeds/update.rb b/app/mutations/webcam_feeds/update.rb index 8cee99f90..de23cde1b 100644 --- a/app/mutations/webcam_feeds/update.rb +++ b/app/mutations/webcam_feeds/update.rb @@ -8,7 +8,7 @@ module WebcamFeeds end def execute - webcam_feed.update_attributes!(inputs.except(:webcam_feed)) && webcam_feed + webcam_feed.update!(inputs.except(:webcam_feed)) && webcam_feed end end end diff --git a/config/application.rb b/config/application.rb index 69481c2e6..13022cf91 100755 --- a/config/application.rb +++ b/config/application.rb @@ -1,6 +1,6 @@ require_relative "../app/models/transport.rb" require File.expand_path("../boot", __FILE__) -require_relative "../app/lib/celery_script/csheap" +require_relative "../app/lib/celery_script/cs_heap" require "rails/all" # Require the gems listed in Gemfile, including any gems @@ -14,11 +14,13 @@ module FarmBot REDIS_URL = ENV.fetch(REDIS_ENV_KEY, "redis://redis:6379/0") gcs_enabled = %w[ GOOGLE_CLOUD_KEYFILE_JSON GCS_PROJECT GCS_BUCKET ].all? { |s| ENV.key? s } + config.load_defaults 6.0 config.active_storage.service = gcs_enabled ? :google : :local config.cache_store = :redis_cache_store, { url: REDIS_URL } config.middleware.use Rack::Attack config.active_record.schema_format = :sql + config.active_record.belongs_to_required_by_default = false config.active_job.queue_adapter = :delayed_job config.action_dispatch.perform_deep_munge = false I18n.enforce_available_locales = false diff --git a/db/migrate/20190103213956_deprecate_device_serial_number_table.rb b/db/migrate/20190103213956_deprecate_device_serial_number_table.rb index d2207415e..983a234e4 100644 --- a/db/migrate/20190103213956_deprecate_device_serial_number_table.rb +++ b/db/migrate/20190103213956_deprecate_device_serial_number_table.rb @@ -10,7 +10,7 @@ class DeprecateDeviceSerialNumberTable < ActiveRecord::Migration[5.2] def change DeviceSerialNumber.preload(:devices) do |x| - x.device.update_attributes!(serial_number: x.serial_number) + x.device.update!(serial_number: x.serial_number) end end end diff --git a/db/seeds.rb b/db/seeds.rb index 6bf4a745b..3f9407844 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -29,9 +29,10 @@ if Rails.env == "development" SensorReading, FarmwareInstallation, Tool, - Device, Delayed::Job, Delayed::Backend::ActiveRecord::Job, + Fragment, + Device, ].map(&:delete_all) Users::Create.run!(name: "Test", email: "test@test.com", @@ -43,7 +44,7 @@ if Rails.env == "development" User.update_all(confirmed_at: Time.now, agreed_to_terms_at: Time.now) u = User.last - u.update_attributes(device: Devices::Create.run!(user: u)) + u.update(device: Devices::Create.run!(user: u)) # === Parameterized Sequence stuff json = JSON.parse(File.read("spec/lib/celery_script/ast_fixture5.json"), symbolize_names: true) Sequences::Create.run!(json, device: u.device) diff --git a/frontend/__test_support__/fake_designer_state.ts b/frontend/__test_support__/fake_designer_state.ts index 84e23f6bc..b0b33ad2f 100644 --- a/frontend/__test_support__/fake_designer_state.ts +++ b/frontend/__test_support__/fake_designer_state.ts @@ -13,4 +13,5 @@ export const fakeDesignerState = (): DesignerState => ({ chosenLocation: { x: undefined, y: undefined, z: undefined }, currentPoint: undefined, openedSavedGarden: undefined, + tryGroupSortType: undefined, }); diff --git a/frontend/account/dev/__tests__/dev_widget_test.tsx b/frontend/account/dev/__tests__/dev_widget_test.tsx index 360112b69..cbd670afa 100644 --- a/frontend/account/dev/__tests__/dev_widget_test.tsx +++ b/frontend/account/dev/__tests__/dev_widget_test.tsx @@ -6,7 +6,9 @@ jest.mock("../../../config_storage/actions", () => ({ import * as React from "react"; import { mount, shallow } from "enzyme"; -import { DevWidget, DevWidgetFERow, DevWidgetFBOSRow } from "../dev_widget"; +import { + DevWidget, DevWidgetFERow, DevWidgetFBOSRow, DevWidgetDelModeRow +} from "../dev_widget"; import { DevSettings } from "../dev_support"; import { setWebAppConfigValue } from "../../../config_storage/actions"; @@ -52,4 +54,12 @@ describe("", () => { wrapper.find("button").simulate("click"); expect(setWebAppConfigValue).toHaveBeenCalledWith("internal_use", "{}"); }); + + it("disables delete mode", () => { + delete mockDevSettings[DevSettings.FUTURE_FE_FEATURES]; + mockDevSettings[DevSettings.QUICK_DELETE_MODE] = "true"; + const wrapper = mount(); + wrapper.find("button").simulate("click"); + expect(setWebAppConfigValue).toHaveBeenCalledWith("internal_use", "{}"); + }); }); diff --git a/frontend/account/dev/dev_support.ts b/frontend/account/dev/dev_support.ts index 57eaf6a63..0c6a0e414 100644 --- a/frontend/account/dev/dev_support.ts +++ b/frontend/account/dev/dev_support.ts @@ -10,6 +10,7 @@ namespace devStorage { export enum Key { FUTURE_FE_FEATURES = "FUTURE_FE_FEATURES", FBOS_VERSION_OVERRIDE = "FBOS_VERSION_OVERRIDE", + QUICK_DELETE_MODE = "QUICK_DELETE_MODE", } type Storage = { [K in Key]: string }; @@ -64,4 +65,11 @@ export namespace DevSettings { export const setMaxFbosVersionOverride = () => devStorage.setItem(FBOS_VERSION_OVERRIDE, MAX_FBOS_VERSION_OVERRIDE); + export const QUICK_DELETE_MODE = devStorage.Key.QUICK_DELETE_MODE; + export const quickDeleteEnabled = () => + !!devStorage.getItem(QUICK_DELETE_MODE); + export const enableQuickDelete = () => + devStorage.setItem(QUICK_DELETE_MODE, "true"); + export const disableQuickDelete = () => + devStorage.removeItem(QUICK_DELETE_MODE); } diff --git a/frontend/account/dev/dev_widget.tsx b/frontend/account/dev/dev_widget.tsx index 9e34a0986..2a6ad1ea8 100644 --- a/frontend/account/dev/dev_widget.tsx +++ b/frontend/account/dev/dev_widget.tsx @@ -47,6 +47,22 @@ export const DevWidgetFBOSRow = () => { ; }; +export const DevWidgetDelModeRow = () => + + + + + + + + ; + export const DevWidget = ({ dispatch }: { dispatch: Function }) => @@ -62,6 +78,7 @@ export const DevWidget = ({ dispatch }: { dispatch: Function }) => + ; diff --git a/frontend/constants.ts b/frontend/constants.ts index 090c4be50..a9d5f97bd 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -974,6 +974,7 @@ export enum Actions { CHOOSE_LOCATION = "CHOOSE_LOCATION", SET_CURRENT_POINT_DATA = "SET_CURRENT_POINT_DATA", CHOOSE_SAVED_GARDEN = "CHOOSE_SAVED_GARDEN", + TRY_SORT_TYPE = "TRY_SORT_TYPE", // Regimens PUSH_WEEK = "PUSH_WEEK", diff --git a/frontend/css/buttons.scss b/frontend/css/buttons.scss index 3eea82c6f..d2cb0b51a 100644 --- a/frontend/css/buttons.scss +++ b/frontend/css/buttons.scss @@ -303,6 +303,20 @@ opacity: 1; } } + &.quick-del { + &:hover { + background: lighten($red, 10%) !important; + border: none; + box-shadow: inset 0px 0px 0px 4px $darkest_red !important; + &:after { + content: "x"; + position: absolute; + right: 1rem; + color: $darkest_red; + font-size: 2rem; + } + } + } } .block-control { diff --git a/frontend/css/global.scss b/frontend/css/global.scss index 8ad1e1886..28c0a5dbb 100644 --- a/frontend/css/global.scss +++ b/frontend/css/global.scss @@ -1469,3 +1469,16 @@ textarea { textarea:focus { box-shadow: 0 0 10px rgba(0,0,0,.2); } + +.sort-path-info-bar { + background: lightgray; + cursor: pointer; + font-size: 1.1rem; + margin-top: 0.25rem; + margin-bottom: 0.25rem; + white-space: nowrap; + line-height: 1.75rem; + &:hover { + background: darken(lightgray, 10%); + } +} diff --git a/frontend/css/image_flipper.scss b/frontend/css/image_flipper.scss index bcaeaf15a..48573d577 100644 --- a/frontend/css/image_flipper.scss +++ b/frontend/css/image_flipper.scss @@ -101,6 +101,7 @@ .farmware-button{ position: relative; top: 0rem; + z-index: 9; height: 0px; float: right; } diff --git a/frontend/css/sequences.scss b/frontend/css/sequences.scss index 031af3da2..8590e20e1 100644 --- a/frontend/css/sequences.scss +++ b/frontend/css/sequences.scss @@ -209,7 +209,7 @@ } .farmware-list-items, -.sequence-list-items, +.sequence-list-item, .regimen-list { button { text-align: left; @@ -220,6 +220,7 @@ text-overflow: ellipsis; margin-top: 0.5rem; float: left; + cursor: pointer; } } i { @@ -228,7 +229,7 @@ } } -.sequence-list-items { +.sequence-list-item { margin-right: 15px; } diff --git a/frontend/devices/components/__tests__/farmbot_os_settings_test.tsx b/frontend/devices/components/__tests__/farmbot_os_settings_test.tsx index e6f986844..9233969b5 100644 --- a/frontend/devices/components/__tests__/farmbot_os_settings_test.tsx +++ b/frontend/devices/components/__tests__/farmbot_os_settings_test.tsx @@ -1,6 +1,6 @@ -let mockReleaseNoteData = {}; +let mockReleaseNoteResponse = Promise.resolve({ data: "" }); jest.mock("axios", () => ({ - get: jest.fn(() => Promise.resolve(mockReleaseNoteData)) + get: jest.fn(() => mockReleaseNoteResponse) })); jest.mock("../../../api/crud", () => ({ @@ -53,24 +53,44 @@ describe("", () => { }); it("fetches OS release notes", async () => { - mockReleaseNoteData = { data: "intro\n\n# v6\n\n* note" }; + mockReleaseNoteResponse = Promise.resolve({ + data: "intro\n\n# v6\n\n* note" + }); const osSettings = await mount(); await expect(axios.get).toHaveBeenCalledWith( expect.stringContaining("RELEASE_NOTES.md")); - expect(osSettings.instance().state.osReleaseNotesHeading) + expect(osSettings.instance().osReleaseNotes.heading) .toEqual("FarmBot OS v6"); - expect(osSettings.instance().state.osReleaseNotes) + expect(osSettings.instance().osReleaseNotes.notes) .toEqual("* note"); }); it("doesn't fetch OS release notes", async () => { - mockReleaseNoteData = { data: "empty notes" }; + mockReleaseNoteResponse = Promise.resolve({ data: "" }); const osSettings = await mount(); await expect(axios.get).toHaveBeenCalledWith( expect.stringContaining("RELEASE_NOTES.md")); - expect(osSettings.instance().state.osReleaseNotes) + expect(osSettings.instance().state.allOsReleaseNotes) + .toEqual(""); + expect(osSettings.instance().osReleaseNotes.heading) + .toEqual("FarmBot OS v6"); + expect(osSettings.instance().osReleaseNotes.notes) + .toEqual("Could not get release notes."); + }); + + it("errors while fetching OS release notes", async () => { + mockReleaseNoteResponse = Promise.reject({ error: "" }); + const osSettings = await mount(); + await expect(axios.get).toHaveBeenCalledWith( + expect.stringContaining("RELEASE_NOTES.md")); + expect(osSettings.instance().state.allOsReleaseNotes) + .toEqual(""); + expect(osSettings.instance().osReleaseNotes.heading) + .toEqual("FarmBot OS v6"); + expect(osSettings.instance().osReleaseNotes.notes) .toEqual("Could not get release notes."); }); @@ -82,4 +102,11 @@ describe("", () => { .simulate("change", { currentTarget: { value: newName } }); expect(edit).toHaveBeenCalledWith(p.deviceAccount, { name: newName }); }); + + it("displays boot sequence selector", () => { + const p = fakeProps(); + p.shouldDisplay = () => true; + const osSettings = shallow(); + expect(osSettings.find("BootSequenceSelector").length).toEqual(1); + }); }); diff --git a/frontend/devices/components/farmbot_os_settings.tsx b/frontend/devices/components/farmbot_os_settings.tsx index f52d9a198..b60d35cff 100644 --- a/frontend/devices/components/farmbot_os_settings.tsx +++ b/frontend/devices/components/farmbot_os_settings.tsx @@ -29,27 +29,31 @@ const OS_RELEASE_NOTES_URL = export class FarmbotOsSettings extends React.Component { - state = { osReleaseNotesHeading: "", osReleaseNotes: "" }; + state: FarmbotOsState = { allOsReleaseNotes: "" }; componentDidMount() { - this.fetchReleaseNotes(OS_RELEASE_NOTES_URL, - (this.props.bot.hardware.informational_settings - .controller_version || "6").split(".")[0]); + this.fetchReleaseNotes(OS_RELEASE_NOTES_URL); } - fetchReleaseNotes = (url: string, osMajorVersion: string) => { + get osMajorVersion() { + return (this.props.bot.hardware.informational_settings + .controller_version || "6").split(".")[0]; + } + + fetchReleaseNotes = (url: string) => { axios .get(url) - .then(resp => { - const osReleaseNotes = resp.data - .split("# v") - .filter(x => x.startsWith(osMajorVersion))[0] - .split("\n\n").slice(1).join("\n"); - const osReleaseNotesHeading = "FarmBot OS v" + osMajorVersion; - this.setState({ osReleaseNotesHeading, osReleaseNotes }); - }) - .catch(() => - this.setState({ osReleaseNotes: "Could not get release notes." })); + .then(resp => this.setState({ allOsReleaseNotes: resp.data })) + .catch(() => this.setState({ allOsReleaseNotes: "" })); + } + + get osReleaseNotes() { + const notes = (this.state.allOsReleaseNotes + .split("# v") + .filter(x => x.startsWith(this.osMajorVersion))[0] || "") + .split("\n\n").slice(1).join("\n") || t("Could not get release notes."); + const heading = "FarmBot OS v" + this.osMajorVersion; + return { heading, notes }; } changeBot = (e: React.FormEvent) => { @@ -119,8 +123,8 @@ export class FarmbotOsSettings || this.props.isValidFbosConfig}> { const oldState = fakeDesignerState; @@ -112,4 +113,14 @@ describe("designer reducer", () => { const newState = designer(state, action); expect(newState.cropSearchInProgress).toEqual(false); }); + + it("starts group sort type trial", () => { + const state = oldState(); + state.tryGroupSortType = undefined; + const action: ReduxAction = { + type: Actions.TRY_SORT_TYPE, payload: "random" + }; + const newState = designer(state, action); + expect(newState.tryGroupSortType).toEqual("random"); + }); }); diff --git a/frontend/farm_designer/interfaces.ts b/frontend/farm_designer/interfaces.ts index a79792402..bdec17d3f 100644 --- a/frontend/farm_designer/interfaces.ts +++ b/frontend/farm_designer/interfaces.ts @@ -20,7 +20,7 @@ import { AxisNumberProperty, BotSize, TaggedPlant } from "./map/interfaces"; import { SelectionBoxData } from "./map/background"; import { GetWebAppConfigValue } from "../config_storage/actions"; import { - ExecutableType, PlantPointer + ExecutableType, PlantPointer, PointGroupSortType } from "farmbot/dist/resources/api_resources"; import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app"; import { TimeSettings } from "../interfaces"; @@ -108,6 +108,7 @@ export interface DesignerState { chosenLocation: BotPosition; currentPoint: CurrentPointPayl | undefined; openedSavedGarden: string | undefined; + tryGroupSortType: PointGroupSortType | "nn" | undefined; } export type TaggedExecutable = TaggedSequence | TaggedRegimen; diff --git a/frontend/farm_designer/map/garden_map.tsx b/frontend/farm_designer/map/garden_map.tsx index 3b7f90471..6b7531265 100644 --- a/frontend/farm_designer/map/garden_map.tsx +++ b/frontend/farm_designer/map/garden_map.tsx @@ -28,6 +28,7 @@ import { } from "./layers/plants/plant_actions"; import { chooseLocation } from "../move_to"; import { GroupOrder } from "../point_groups/group_order_visual"; +import { NNPath } from "../point_groups/paths"; export class GardenMap extends React.Component> { @@ -347,6 +348,8 @@ export class GardenMap extends GroupOrder = () => + NNPath = () => Bugs = () => showBugs() ? : @@ -370,6 +373,7 @@ export class GardenMap extends + diff --git a/frontend/farm_designer/map/interfaces.ts b/frontend/farm_designer/map/interfaces.ts index eb6dbd1fa..4aa8a966f 100644 --- a/frontend/farm_designer/map/interfaces.ts +++ b/frontend/farm_designer/map/interfaces.ts @@ -54,6 +54,7 @@ export interface GardenPlantProps { dispatch: Function; plant: Readonly; selected: boolean; + editing: boolean; dragging: boolean; zoomLvl: number; activeDragXY: BotPosition | undefined; diff --git a/frontend/farm_designer/map/layers/plants/__tests__/garden_plant_test.tsx b/frontend/farm_designer/map/layers/plants/__tests__/garden_plant_test.tsx index 2c381d773..9a9b9857a 100644 --- a/frontend/farm_designer/map/layers/plants/__tests__/garden_plant_test.tsx +++ b/frontend/farm_designer/map/layers/plants/__tests__/garden_plant_test.tsx @@ -14,6 +14,7 @@ describe("", () => { mapTransformProps: fakeMapTransformProps(), plant: fakePlant(), selected: false, + editing: false, multiselected: false, dragging: false, dispatch: jest.fn(), diff --git a/frontend/farm_designer/map/layers/plants/garden_plant.tsx b/frontend/farm_designer/map/layers/plants/garden_plant.tsx index 072dfad6b..513d84921 100644 --- a/frontend/farm_designer/map/layers/plants/garden_plant.tsx +++ b/frontend/farm_designer/map/layers/plants/garden_plant.tsx @@ -44,7 +44,7 @@ export class GardenPlant extends render() { const { selected, dragging, plant, multiselected, mapTransformProps, - activeDragXY, zoomLvl, animate } = this.props; + activeDragXY, zoomLvl, animate, editing } = this.props; const { id, radius, x, y } = plant.body; const { icon } = this.state; @@ -65,7 +65,7 @@ export class GardenPlant extends fill={Color.soilCloud} fillOpacity={0} />} - {multiselected && + {multiselected && !editing && ({ toggleHoveredPlant: jest.fn() })); +let mockDev = false; +jest.mock("../../../account/dev/dev_support", () => ({ + DevSettings: { + futureFeaturesEnabled: () => mockDev, + } +})); + import React from "react"; import { GroupDetailActive } from "../group_detail_active"; import { mount, shallow } from "enzyme"; @@ -81,4 +88,19 @@ describe("", () => { el.componentWillUnmount && el.componentWillUnmount(); expect(clearInterval).toHaveBeenCalledWith(123); }); + + it("shows paths", () => { + mockDev = true; + const p = fakeProps(); + p.plants = [fakePlant(), fakePlant()]; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("optimized"); + }); + + it("doesn't show paths", () => { + mockDev = false; + const p = fakeProps(); + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).not.toContain("optimized"); + }); }); diff --git a/frontend/farm_designer/point_groups/__tests__/paths_test.tsx b/frontend/farm_designer/point_groups/__tests__/paths_test.tsx new file mode 100644 index 000000000..c6ae33359 --- /dev/null +++ b/frontend/farm_designer/point_groups/__tests__/paths_test.tsx @@ -0,0 +1,94 @@ +jest.mock("../../../api/crud", () => ({ edit: jest.fn() })); + +import * as React from "react"; +import { mount, shallow } from "enzyme"; +import { PathInfoBar, nn, NNPath, PathInfoBarProps } from "../paths"; +import { + fakePlant, fakePointGroup +} from "../../../__test_support__/fake_state/resources"; +import { + fakeMapTransformProps +} from "../../../__test_support__/map_transform_props"; +import { Actions } from "../../../constants"; +import { edit } from "../../../api/crud"; +import { error } from "../../../toast/toast"; + +describe("", () => { + const fakeProps = (): PathInfoBarProps => ({ + sortTypeKey: "random", + dispatch: jest.fn(), + group: fakePointGroup(), + pathData: { random: 123 }, + }); + + it("hovers path", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.simulate("mouseEnter"); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.TRY_SORT_TYPE, payload: "random" + }); + }); + + it("unhovers path", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.simulate("mouseLeave"); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.TRY_SORT_TYPE, payload: undefined + }); + }); + + it("selects path", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.simulate("click"); + expect(edit).toHaveBeenCalledWith(p.group, { sort_type: "random" }); + }); + + it("selects new path", () => { + const p = fakeProps(); + p.sortTypeKey = "nn"; + const wrapper = shallow(); + wrapper.simulate("click"); + expect(edit).not.toHaveBeenCalled(); + expect(error).toHaveBeenCalledWith("Not supported yet."); + }); +}); + +describe("nearest neighbor algorithm", () => { + it("returns optimized array", () => { + const p1 = fakePlant(); + p1.body.x = 100; + p1.body.y = 100; + const p2 = fakePlant(); + p2.body.x = 200; + p2.body.y = 200; + const p3 = fakePlant(); + p3.body.x = 175; + p3.body.y = 1000; + const p4 = fakePlant(); + p4.body.x = 1000; + p4.body.y = 150; + const points = nn([p4, p2, p3, p1]); + expect(points).toEqual([p1, p2, p3, p4]); + }); +}); + +describe("", () => { + const fakeProps = () => ({ + plants: [], + mapTransformProps: fakeMapTransformProps(), + }); + + it("doesn't render optimized path", () => { + const wrapper = mount(); + expect(wrapper.html()).toEqual(""); + }); + + it("renders optimized path", () => { + localStorage.setItem("try_it", "ok"); + const wrapper = mount(); + expect(wrapper.html()).not.toEqual(""); + }); +}); diff --git a/frontend/farm_designer/point_groups/group_detail_active.tsx b/frontend/farm_designer/point_groups/group_detail_active.tsx index aea3ecd87..b8b5365dd 100644 --- a/frontend/farm_designer/point_groups/group_detail_active.tsx +++ b/frontend/farm_designer/point_groups/group_detail_active.tsx @@ -13,6 +13,8 @@ import { TaggedPlant } from "../map/interfaces"; import { PointGroupSortSelector, sortGroupBy } from "./point_group_sort_selector"; import { PointGroupSortType } from "farmbot/dist/resources/api_resources"; import { PointGroupItem } from "./point_group_item"; +import { Paths } from "./paths"; +import { DevSettings } from "../../account/dev/dev_support"; interface GroupDetailActiveProps { dispatch: Function; @@ -97,6 +99,11 @@ export class GroupDetailActive
{this.icons}
+ {DevSettings.futureFeaturesEnabled() && + } { - const group = fetchGroupFromUrl(store.getState().resources.index); + const { resources } = store.getState(); + const group = fetchGroupFromUrl(resources.index); if (isUndefined(group)) { return []; } const groupPlants = plants .filter(p => group.body.point_ids.includes(p.body.id || 0)); - return sortGroupBy(group.body.sort_type, groupPlants) - .map(p => ({ x: p.body.x, y: p.body.y })); + const groupSortType = resources.consumers.farm_designer.tryGroupSortType + || group.body.sort_type; + const sorted = groupSortType == "nn" + ? nn(groupPlants) + : sortGroupBy(groupSortType, groupPlants); + return sorted.map(p => ({ x: p.body.x, y: p.body.y })); }; -export const GroupOrder = (props: GroupOrderProps) => { - const points = sortedPointCoordinates(props.plants); - return - {points.map((p, i) => { - const prev = i > 0 ? points[i - 1] : p; +export interface PointsPathLineProps { + orderedPoints: { x: number, y: number }[]; + mapTransformProps: MapTransformProps; + color?: Color; + dash?: number; + strokeWidth?: number; +} + +export const PointsPathLine = (props: PointsPathLineProps) => + + {props.orderedPoints.map((p, i) => { + const prev = i > 0 ? props.orderedPoints[i - 1] : p; const one = transformXY(prev.x, prev.y, props.mapTransformProps); const two = transformXY(p.x, p.y, props.mapTransformProps); return ; })} ; -}; + +export const GroupOrder = (props: GroupOrderProps) => + ; diff --git a/frontend/farm_designer/point_groups/paths.tsx b/frontend/farm_designer/point_groups/paths.tsx new file mode 100644 index 000000000..1b9693eba --- /dev/null +++ b/frontend/farm_designer/point_groups/paths.tsx @@ -0,0 +1,129 @@ +import * as React from "react"; +import { TaggedPlant, MapTransformProps } from "../map/interfaces"; +import { sortGroupBy, sortOptionsTable } from "./point_group_sort_selector"; +import { sortBy } from "lodash"; +import { PointsPathLine } from "./group_order_visual"; +import { Color } from "../../ui"; +import { PointGroupSortType } from "farmbot/dist/resources/api_resources"; +import { t } from "../../i18next_wrapper"; +import { Actions } from "../../constants"; +import { edit } from "../../api/crud"; +import { TaggedPointGroup } from "farmbot"; +import { error } from "../../toast/toast"; + +const xy = (point: TaggedPlant) => ({ x: point.body.x, y: point.body.y }); + +const distance = (p1: { x: number, y: number }, p2: { x: number, y: number }) => + Math.pow(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2), 0.5); + +const pathDistance = (points: TaggedPlant[]) => { + let total = 0; + let prev: { x: number, y: number } | undefined = undefined; + points.map(xy) + .map(p => { + prev ? total += distance(p, prev) : 0; + prev = p; + }); + return Math.round(total); +}; + +const findNearest = + (from: { x: number, y: number }, available: TaggedPlant[]) => { + const distances = available.map(p => ({ + point: p, distance: distance(xy(p), from) + })); + return sortBy(distances, "distance")[0].point; + }; + +export const nn = (points: TaggedPlant[]) => { + let available = points.slice(0); + const ordered: TaggedPlant[] = []; + let from = { x: 0, y: 0 }; + points.map(() => { + const nearest = findNearest(from, available); + ordered.push(nearest); + from = { x: nearest.body.x, y: nearest.body.y }; + available = available.filter(p => p.uuid !== nearest.uuid); + }); + return ordered; +}; + +const SORT_TYPES: (PointGroupSortType | "nn")[] = [ + "random", "xy_ascending", "xy_descending", "yx_ascending", "yx_descending"]; + +export interface PathInfoBarProps { + sortTypeKey: PointGroupSortType | "nn"; + dispatch: Function; + group: TaggedPointGroup; + pathData: { [key: string]: number }; +} + +export const PathInfoBar = (props: PathInfoBarProps) => { + const { sortTypeKey, dispatch, group } = props; + const pathLength = props.pathData[sortTypeKey]; + const maxLength = Math.max(...Object.values(props.pathData)); + const normalizedLength = pathLength / maxLength * 100; + const sortLabel = + sortTypeKey == "nn" ? "Optimized" : sortOptionsTable()[sortTypeKey]; + return
+ dispatch({ type: Actions.TRY_SORT_TYPE, payload: sortTypeKey })} + onMouseLeave={() => + dispatch({ type: Actions.TRY_SORT_TYPE, payload: undefined })} + onClick={() => + sortTypeKey == "nn" + ? error(t("Not supported yet.")) + : dispatch(edit(group, { sort_type: sortTypeKey }))} + style={{ width: `${normalizedLength}%` }}> + {`${sortLabel}: ${Math.round(pathLength / 10) / 100}m`} +
; +}; + +interface PathsProps { + points: TaggedPlant[]; + dispatch: Function; + group: TaggedPointGroup; +} + +interface PathsState { + pathData: { [key: string]: number }; +} + +export class Paths extends React.Component { + state: PathsState = { pathData: {} }; + + generatePathData = (points: TaggedPlant[]) => { + SORT_TYPES.map((sortType: PointGroupSortType) => + this.state.pathData[sortType] = + pathDistance(sortGroupBy(sortType, points))); + this.state.pathData.nn = pathDistance(nn(points)); + }; + + render() { + if (!this.state.pathData.nn) { this.generatePathData(this.props.points); } + return
+ + {SORT_TYPES.concat("nn").map(st => + )} +
; + } +} + +interface NNPathProps { + plants: TaggedPlant[]; + mapTransformProps: MapTransformProps; +} + +export const NNPath = (props: NNPathProps) => + localStorage.getItem("try_it") == "ok" + ? + : ; diff --git a/frontend/farm_designer/point_groups/point_group_sort_selector.tsx b/frontend/farm_designer/point_groups/point_group_sort_selector.tsx index 86b3d142c..5b75ab9d6 100644 --- a/frontend/farm_designer/point_groups/point_group_sort_selector.tsx +++ b/frontend/farm_designer/point_groups/point_group_sort_selector.tsx @@ -11,7 +11,7 @@ interface Props { value: PointGroupSortType; } -const optionsTable = (): Record => ({ +export const sortOptionsTable = (): Record => ({ "random": t("Random Order"), "xy_ascending": t("X/Y, Ascending"), "xy_descending": t("X/Y, Descending"), @@ -21,7 +21,7 @@ const optionsTable = (): Record => ({ const optionPlusDescriptions = () => (Object - .entries(optionsTable()) as [PointGroupSortType, string][]) + .entries(sortOptionsTable()) as [PointGroupSortType, string][]) .map(x => ({ label: x[1], value: x[0] })); const optionList = @@ -32,7 +32,7 @@ export const isSortType = (x: unknown): x is PointGroupSortType => { }; const selected = (value: PointGroupSortType) => ({ - label: t(optionsTable()[value] || value), + label: t(sortOptionsTable()[value] || value), value: value }); @@ -50,6 +50,7 @@ export function PointGroupSortSelector(p: Props) { diff --git a/frontend/farm_designer/reducer.ts b/frontend/farm_designer/reducer.ts index 9c6ebe470..45a84179e 100644 --- a/frontend/farm_designer/reducer.ts +++ b/frontend/farm_designer/reducer.ts @@ -5,6 +5,7 @@ import { cloneDeep } from "lodash"; import { TaggedResource } from "farmbot"; import { Actions } from "../constants"; import { BotPosition } from "../devices/interfaces"; +import { PointGroupSortType } from "farmbot/dist/resources/api_resources"; export let initialState: DesignerState = { selectedPlants: undefined, @@ -19,6 +20,7 @@ export let initialState: DesignerState = { chosenLocation: { x: undefined, y: undefined, z: undefined }, currentPoint: undefined, openedSavedGarden: undefined, + tryGroupSortType: undefined, }; export let designer = generateReducer(initialState) @@ -69,4 +71,8 @@ export let designer = generateReducer(initialState) .add(Actions.CHOOSE_SAVED_GARDEN, (s, { payload }) => { s.openedSavedGarden = payload; return s; + }) + .add(Actions.TRY_SORT_TYPE, (s, { payload }) => { + s.tryGroupSortType = payload; + return s; }); diff --git a/frontend/farm_designer/search_selectors.ts b/frontend/farm_designer/search_selectors.ts index ca5a985bb..4638f4b9d 100644 --- a/frontend/farm_designer/search_selectors.ts +++ b/frontend/farm_designer/search_selectors.ts @@ -9,7 +9,7 @@ export function findBySlug( return crop || { crop: { name: startCase((slug || t("Name")).split("-").join(" ")), - slug: "slug", + slug: slug || "slug", binomial_name: t("Binomial Name"), common_names: [t("Common Names")], description: t("Description"), diff --git a/frontend/open_farm/__tests__/cached_crop_test.ts b/frontend/open_farm/__tests__/cached_crop_test.ts index 18219ae08..c82107f58 100644 --- a/frontend/open_farm/__tests__/cached_crop_test.ts +++ b/frontend/open_farm/__tests__/cached_crop_test.ts @@ -13,20 +13,24 @@ const mockResponse: { promise: Promise<{}> } = { }; jest.mock("axios", () => ({ - get: () => mockResponse.promise + get: jest.fn(() => mockResponse.promise) })); jest.unmock("../cached_crop"); import { cachedCrop } from "../cached_crop"; +import axios from "axios"; +import { times } from "lodash"; describe("cachedIcon()", () => { it("does an HTTP request if the icon can't be found locally", async () => { + times(10, () => cachedCrop("lettuce")); const item1 = await cachedCrop("lettuce"); expect(item1.svg_icon).toContain("Wow"); const item2 = await cachedCrop("lettuce"); expect(item2.slug).toBe(item1.slug); expect(item2.svg_icon).toBe(item1.svg_icon); expect(item2.spread).toBe(undefined); + expect(axios.get).toHaveBeenCalledTimes(1); }); it("handles unexpected responses from OpenFarm", async () => { diff --git a/frontend/open_farm/cached_crop.ts b/frontend/open_farm/cached_crop.ts index 0faecd30a..790e5a22d 100644 --- a/frontend/open_farm/cached_crop.ts +++ b/frontend/open_farm/cached_crop.ts @@ -37,7 +37,8 @@ function localStorageIconSet(icon: OFIcon): void { * and the garlic icon is not cached locally, and you try to render 10 garlic * icons in the first 100ms, and HTTP requests take more than 100ms, you will * end up performing 10 HTTP requests at application start time. Not very - * efficient */ + * efficient. + * SOLUTION: Keep a record of open requests to avoid duplicate requests. */ const promiseCache: Dictionary>> = {}; const cacheTheIcon = (slug: string) => @@ -60,6 +61,8 @@ const cacheTheIcon = (slug: string) => function HTTPIconFetch(slug: string) { const url = OpenFarmAPI.OFBaseURL + slug; + // Avoid duplicate requests. + if (promiseCache[url]) { return promiseCache[url]; } promiseCache[url] = axios .get(url) .then(cacheTheIcon(slug), cacheTheIcon(slug)); diff --git a/frontend/sequences/__tests__/sequences_list_test.tsx b/frontend/sequences/__tests__/sequences_list_test.tsx index fb93c6d09..4ce3777f0 100644 --- a/frontend/sequences/__tests__/sequences_list_test.tsx +++ b/frontend/sequences/__tests__/sequences_list_test.tsx @@ -8,7 +8,13 @@ jest.mock("../set_active_sequence_by_name", () => ({ })); jest.mock("../../api/crud", () => ({ - init: jest.fn() + init: jest.fn(), + destroy: jest.fn(), +})); + +let mockDev = false; +jest.mock("../../account/dev/dev_support", () => ({ + DevSettings: { quickDeleteEnabled: () => mockDev, } })); import * as React from "react"; @@ -17,7 +23,7 @@ import { SequencesList } from "../sequences_list"; import { fakeSequence } from "../../__test_support__/fake_state/resources"; import { SequencesListProps } from "../interfaces"; import { Actions } from "../../constants"; -import { init } from "../../api/crud"; +import { init, destroy } from "../../api/crud"; import { push } from "../../history"; import { resourceUsageList } from "../../resources/in_use"; import { buildResourceIndex } from "../../__test_support__/resource_index_builder"; @@ -25,6 +31,7 @@ import { resourceReducer } from "../../resources/reducer"; import { resourceReady } from "../../sync/actions"; import { setActiveSequenceByName } from "../set_active_sequence_by_name"; import { inputEvent } from "../../__test_support__/fake_input_event"; +import { Link } from "../../link"; describe("", () => { const fakeSequences = () => { @@ -113,8 +120,28 @@ describe("", () => { it("opens sequence", () => { const p = fakeProps(); - const wrapper = shallow(); - wrapper.find("Link").first().simulate("click"); + const wrapper = mount(); + wrapper.find(Link).first().simulate("click"); expect(setActiveSequenceByName).toHaveBeenCalled(); }); + + it("doesn't delete sequence", () => { + mockDev = false; + const p = fakeProps([fakeSequence()]); + const wrapper = mount(); + const button = wrapper.find("button").last(); + expect(button.hasClass("quick-del")).toBeFalsy(); + button.simulate("click"); + expect(destroy).not.toHaveBeenCalled(); + }); + + it("deletes sequence", () => { + mockDev = true; + const p = fakeProps([fakeSequence()]); + const wrapper = mount(); + const button = wrapper.find("button").last(); + expect(button.hasClass("quick-del")).toBeTruthy(); + button.simulate("click"); + expect(destroy).toHaveBeenCalledWith(p.sequences[0].uuid); + }); }); diff --git a/frontend/sequences/sequences_list.tsx b/frontend/sequences/sequences_list.tsx index 8de7b03c8..4d59ac7b1 100644 --- a/frontend/sequences/sequences_list.tsx +++ b/frontend/sequences/sequences_list.tsx @@ -4,7 +4,7 @@ import { SequencesListProps, SequencesListState } from "./interfaces"; import { sortResourcesById, urlFriendly, lastUrlChunk } from "../util"; import { Row, Col } from "../ui/index"; import { TaggedSequence } from "farmbot"; -import { init } from "../api/crud"; +import { init, destroy } from "../api/crud"; import { Content } from "../constants"; import { StepDragger, NULL_DRAGGER_ID } from "../draggable/step_dragger"; import { Link } from "../link"; @@ -13,48 +13,79 @@ import { UUID, VariableNameSet } from "../resources/interfaces"; import { variableList } from "./locals_list/variable_support"; import { t } from "../i18next_wrapper"; import { EmptyStateWrapper, EmptyStateGraphic } from "../ui/empty_state_wrapper"; +import { DevSettings } from "../account/dev/dev_support"; -const filterFn = (searchTerm: string) => (seq: TaggedSequence): boolean => seq - .body - .name - .toLowerCase() - .includes(searchTerm); +const filterFn = (searchTerm: string) => (seq: TaggedSequence): boolean => + seq.body.name.toLowerCase().includes(searchTerm); -const sequenceList = (props: { - dispatch: Function, - resourceUsage: Record, - sequenceMetas: Record -}) => +interface SequenceButtonWrapperProps { + ts: TaggedSequence; + dispatch: Function; + variableData: VariableNameSet | undefined; + children: React.ReactChild; +} + +/** Sequence list item wrapper for drag action and link to sequence. */ +const SequenceButtonWrapper = (props: SequenceButtonWrapperProps) => +
+ + + {props.children} + + +
; + +interface SequenceButtonProps { + ts: TaggedSequence; + inUse: boolean; + deleteFunc?: () => void; +} + +/** Sequence list item label and indicators. */ +const SequenceButton = (props: SequenceButtonProps) => { + const { color, name } = props.ts.body; + const css = [`fb-button`, `block`, `full-width`, `${color || "purple"}`]; + lastUrlChunk() === urlFriendly(name) && css.push("active"); + props.deleteFunc && css.push("quick-del"); + const nameWithSaveIndicator = name + (props.ts.specialStatus ? "*" : ""); + return ; +}; + +interface SequenceListItemProps { + dispatch: Function; + resourceUsage: Record; + sequenceMetas: Record; +} + +const SequenceListItem = (props: SequenceListItemProps) => (ts: TaggedSequence) => { - const css = - [`fb-button`, `block`, `full-width`, `${ts.body.color || "purple"}`]; - lastUrlChunk() === urlFriendly(ts.body.name) && css.push("active"); - const { uuid } = ts; - const nameWithSaveIndicator = ts.body.name + (ts.specialStatus ? "*" : ""); - const inUse = !!props.resourceUsage[uuid]; - const variableData = props.sequenceMetas[uuid]; + const inUse = !!props.resourceUsage[ts.uuid]; + const variableData = props.sequenceMetas[ts.uuid]; + const deleteSeq = () => props.dispatch(destroy(ts.uuid)); - return
- - - - - + return
+ {DevSettings.quickDeleteEnabled() + ? + : + + }
; }; @@ -126,7 +157,9 @@ export class SequencesList extends
{sortResourcesById(sequences) .filter(filterFn(searchTerm)) - .map(sequenceList({ dispatch, resourceUsage, sequenceMetas }))} + .map(SequenceListItem({ + dispatch, resourceUsage, sequenceMetas + }))}
} diff --git a/lib/tasks/api.rake b/lib/tasks/api.rake index 5ee72b73d..d05a13a2c 100644 --- a/lib/tasks/api.rake +++ b/lib/tasks/api.rake @@ -141,7 +141,7 @@ namespace :api do puts "Setting new support target to #{data.to_s}" GlobalConfig # Set the new oldest support version. .find_by(key: "FBOS_END_OF_LIFE_VERSION") - .update_attributes!(value: data.to_s) + .update!(value: data.to_s) end end end diff --git a/lib/tasks/coverage.rake b/lib/tasks/coverage.rake index 5e31912d0..aac54eaf1 100644 --- a/lib/tasks/coverage.rake +++ b/lib/tasks/coverage.rake @@ -14,7 +14,7 @@ FRACTION_DELIM = "/" def open_json(url) begin JSON.parse(open(url).read) - rescue OpenURI::HTTPError => exception + rescue *[OpenURI::HTTPError, SocketError] => exception puts exception.message return {} end @@ -80,7 +80,7 @@ end # Fetch a page of build coverage report results. def fetch_builds_for_page(page_number) - open_json("#{LATEST_COV_URL}?page=#{page_number}")["builds"] + open_json("#{LATEST_COV_URL}?page=#{page_number}")["builds"] || [] end # Number of coverage build data pages required to fetch the desired build count. diff --git a/spec/controllers/api/configs/fbos_configs_controller_spec.rb b/spec/controllers/api/configs/fbos_configs_controller_spec.rb index 726e564b0..fa1eb6325 100644 --- a/spec/controllers/api/configs/fbos_configs_controller_spec.rb +++ b/spec/controllers/api/configs/fbos_configs_controller_spec.rb @@ -86,7 +86,7 @@ describe Api::FbosConfigsController do it 'resets everything to the defaults' do sign_in user old_conf = device.fbos_config - old_conf.update_attributes(arduino_debug_messages: 23) + old_conf.update(arduino_debug_messages: 23) delete :destroy, params: {} expect(response.status).to eq(200) new_conf = device.reload.fbos_config diff --git a/spec/controllers/api/configs/firmware_configs_controller_spec.rb b/spec/controllers/api/configs/firmware_configs_controller_spec.rb index d71c1830f..ff8fe138b 100644 --- a/spec/controllers/api/configs/firmware_configs_controller_spec.rb +++ b/spec/controllers/api/configs/firmware_configs_controller_spec.rb @@ -138,7 +138,7 @@ describe Api::FirmwareConfigsController do it 'resets everything to the defaults' do sign_in user old_conf = device.firmware_config - old_conf.update_attributes(pin_guard_5_pin_nr: 23) + old_conf.update(pin_guard_5_pin_nr: 23) delete :destroy, params: {} expect(response.status).to eq(200) new_conf = device.reload.firmware_config diff --git a/spec/controllers/api/configs/web_app_configs_controller_spec.rb b/spec/controllers/api/configs/web_app_configs_controller_spec.rb index ceea005fb..33dd06184 100644 --- a/spec/controllers/api/configs/web_app_configs_controller_spec.rb +++ b/spec/controllers/api/configs/web_app_configs_controller_spec.rb @@ -90,7 +90,7 @@ describe Api::WebAppConfigsController do it "resets everything to the defaults" do sign_in user old_conf = device.web_app_config - old_conf.update_attributes(zoom_level: 23) + old_conf.update(zoom_level: 23) delete :destroy, params: {} expect(response.status).to eq(200) new_conf = device.reload.web_app_config diff --git a/spec/controllers/api/devices/devices_controller_destroy_spec.rb b/spec/controllers/api/devices/devices_controller_destroy_spec.rb index 07ff9bf86..c9951f156 100644 --- a/spec/controllers/api/devices/devices_controller_destroy_spec.rb +++ b/spec/controllers/api/devices/devices_controller_destroy_spec.rb @@ -22,7 +22,7 @@ describe Api::DevicesController do expect(device.send(resource.pluralize).reload.count).to be > 0 end - device.update_attributes(name: "#{SecureRandom.hex(10)}") + device.update(name: "#{SecureRandom.hex(10)}") run_jobs_now { post :reset, body: { password: password }.to_json } diff --git a/spec/controllers/api/devices/devices_controller_show_spec.rb b/spec/controllers/api/devices/devices_controller_show_spec.rb index f7a7e5f40..6ed5e25c1 100644 --- a/spec/controllers/api/devices/devices_controller_show_spec.rb +++ b/spec/controllers/api/devices/devices_controller_show_spec.rb @@ -8,7 +8,7 @@ describe Api::DevicesController do describe '#show' do it 'handles deviceless requests' do - user.update_attributes(device: nil) + user.update(device: nil) sign_in user get :show, params: {}, session: { format: :json } expect(response.status).to eq(422) @@ -27,7 +27,7 @@ describe Api::DevicesController do it 'reminds users to agree to TOS' do b4 = User::ENFORCE_TOS const_reassign(User, :ENFORCE_TOS, "http://farm.bot/tos") - user.update_attributes!(agreed_to_terms_at: nil) + user.update!(agreed_to_terms_at: nil) sign_in user get :show, params: {}, session: { format: :json } const_reassign(User, :ENFORCE_TOS, b4) diff --git a/spec/controllers/api/devices/devices_controller_sync_spec.rb b/spec/controllers/api/devices/devices_controller_sync_spec.rb index 433fa8f39..6f06e1306 100644 --- a/spec/controllers/api/devices/devices_controller_sync_spec.rb +++ b/spec/controllers/api/devices/devices_controller_sync_spec.rb @@ -36,7 +36,8 @@ describe Api::DevicesController do pair = json[:devices].first expect(pair.first).to eq(device.id) real_time = device.updated_at.strftime(FORMAT).sub(/0+$/, "") - expect(pair.last).to include(real_time) + diff = (device.updated_at - DateTime.parse(pair.last)) + expect(diff).to be < 1 expect(pair.last.first(8)).to eq(device.updated_at.as_json.first(8)) json.keys.without(*EDGE_CASES).map do |key| array = json[key] @@ -56,7 +57,10 @@ describe Api::DevicesController do json[key].each_with_index do |item, index| comparison = expected[index] expect(item.first).to eq(comparison.first) - expect(Time.parse(item.last)).to eq(comparison.last.to_time) + left = Time.parse(item.last) + right = comparison.last.to_time.utc + expect((right - left).seconds).to be < 1 + expect((right - left).seconds).to be >= 0 end end end diff --git a/spec/controllers/api/devices/devices_controller_update_spec.rb b/spec/controllers/api/devices/devices_controller_update_spec.rb index fd80108b3..27ebdd56d 100644 --- a/spec/controllers/api/devices/devices_controller_update_spec.rb +++ b/spec/controllers/api/devices/devices_controller_update_spec.rb @@ -75,7 +75,7 @@ describe Api::DevicesController do it "dismounts a tool" do sign_in user - device.update_attributes!(mounted_tool_id: tool.id) + device.update!(mounted_tool_id: tool.id) expect(device.mounted_tool_id).to be put :update, body: { id: user.device.id, mounted_tool_id: 0 }.to_json, diff --git a/spec/controllers/api/global_config/global_config_controller_spec.rb b/spec/controllers/api/global_config/global_config_controller_spec.rb index f58c57a4f..c3084f0f6 100644 --- a/spec/controllers/api/global_config/global_config_controller_spec.rb +++ b/spec/controllers/api/global_config/global_config_controller_spec.rb @@ -15,7 +15,7 @@ describe Api::GlobalConfigController do it 'changes configs dynamically' do value = SecureRandom.hex - conf.update_attributes!(value: value) + conf.update!(value: value) GlobalConfig.reload_ get :show expect(json[:PING]).to eq(value) diff --git a/spec/controllers/api/images/images_spec.rb b/spec/controllers/api/images/images_spec.rb index 1b4a8c5a8..0ce99a6d9 100644 --- a/spec/controllers/api/images/images_spec.rb +++ b/spec/controllers/api/images/images_spec.rb @@ -39,7 +39,7 @@ describe Api::ImagesController do sign_in user device = user.device # Using the *real* value (10) was super slow (~30 seconds) - device.update_attributes!(max_images_count: 1) + device.update!(max_images_count: 1) FactoryBot.create_list(:image, 2, device: user.device) get :index expect(response.status).to eq(200) diff --git a/spec/controllers/api/logs/logs_spec.rb b/spec/controllers/api/logs/logs_spec.rb index 9b1a97112..c803b82ff 100644 --- a/spec/controllers/api/logs/logs_spec.rb +++ b/spec/controllers/api/logs/logs_spec.rb @@ -109,7 +109,7 @@ describe Api::LogsController do Log.destroy_all 100.times { Log.create!(device: user.device) } sign_in user - user.device.update_attributes!(max_log_count: 15) + user.device.update!(max_log_count: 15) get :index, params: { format: :json } expect(response.status).to eq(200) expect(json.length).to eq(user.device.max_log_count) @@ -187,7 +187,7 @@ describe Api::LogsController do verbosity: verbosity, type: type) end - conf.update_attributes(success_log: 3, + conf.update(success_log: 3, busy_log: 3, warn_log: 3, error_log: 3, @@ -208,7 +208,7 @@ describe Api::LogsController do verbosity: verbosity, type: type) end - conf.update_attributes(success_log: 0, + conf.update(success_log: 0, busy_log: 0, warn_log: 0, error_log: 0, diff --git a/spec/controllers/api/sequences/sequences_update_spec.rb b/spec/controllers/api/sequences/sequences_update_spec.rb index 7f50ebbfe..298a379f1 100644 --- a/spec/controllers/api/sequences/sequences_update_spec.rb +++ b/spec/controllers/api/sequences/sequences_update_spec.rb @@ -93,7 +93,7 @@ describe Api::SequencesController do it 'updates existing sequences' do sign_in user sequence = FakeSequence.create(device: user.device) - sequence.update_attributes!(updated_at: 2.days.ago) + sequence.update!(updated_at: 2.days.ago) updated_at_before = sequence.updated_at.to_i input = { sequence: { name: "Scare Birds", args: {}, body: [] } } params = { id: sequence.id } diff --git a/spec/controllers/api/tools/destroy_spec.rb b/spec/controllers/api/tools/destroy_spec.rb index 7785521ed..d860940ba 100644 --- a/spec/controllers/api/tools/destroy_spec.rb +++ b/spec/controllers/api/tools/destroy_spec.rb @@ -14,7 +14,7 @@ describe Api::ToolsController do it 'destroy a tool' do sign_in user before = Tool.count - tool.tool_slot.update_attributes(tool: nil) + tool.tool_slot.update(tool: nil) delete :destroy, params: { id: tool.id } after = Tool.count expect(response.status).to eq(200) @@ -44,7 +44,7 @@ describe Api::ToolsController do sign_in user # If we dont do this, it will trigger the wrong error. # We test this elsewhere - RC - tool.tool_slot.update_attributes(tool: nil) + tool.tool_slot.update(tool: nil) before = Tool.count delete :destroy, params: { id: tool.id } diff --git a/spec/controllers/dashboarad_failures_spec.rb b/spec/controllers/dashboarad_failures_spec.rb index fc29a5589..82ddfc6b3 100644 --- a/spec/controllers/dashboarad_failures_spec.rb +++ b/spec/controllers/dashboarad_failures_spec.rb @@ -4,7 +4,7 @@ describe DashboardController do render_views it 'can not re-verify' do - user.update_attributes(confirmed_at: Time.now) + user.update(confirmed_at: Time.now) sign_in user get :confirmation_page, params: { token: user.confirmation_token } expect(response.status).to eq(409) diff --git a/spec/controllers/dashboard_spec.rb b/spec/controllers/dashboard_spec.rb index 6d51fa3cd..2d1b4d394 100644 --- a/spec/controllers/dashboard_spec.rb +++ b/spec/controllers/dashboard_spec.rb @@ -43,7 +43,7 @@ describe DashboardController do it "verifies email changes" do email = "foo@bar.com" - user.update_attributes!(unconfirmed_email: "foo@bar.com") + user.update!(unconfirmed_email: "foo@bar.com") params = { token: user.confirmation_token } get :confirmation_page, params: params expect(user.reload.unconfirmed_email).to be nil diff --git a/spec/factories/images.rb b/spec/factories/images.rb index ea9d5edd0..d8a369042 100644 --- a/spec/factories/images.rb +++ b/spec/factories/images.rb @@ -1,8 +1,7 @@ FactoryBot.define do factory :image do - attachment { StringIO.new(File.open("./spec/fixture.jpg").read) } - meta {({x: 1, y: 2, z: 3})} + meta { ({ x: 1, y: 2, z: 3 }) } device end end diff --git a/spec/lib/celery_script/json_climber_spec.rb b/spec/lib/celery_script/json_climber_spec.rb index c3522d656..d74864c72 100644 --- a/spec/lib/celery_script/json_climber_spec.rb +++ b/spec/lib/celery_script/json_climber_spec.rb @@ -4,15 +4,15 @@ fixture = { body: [ { kind: "child", - args: { grandchild: { kind: "grandchild", args: {} } } - } - ] + args: { grandchild: { kind: "grandchild", args: {} } }, + }, + ], } -describe "JSONClimber" do - it 'Climbs JSON' do +describe "JsonClimber" do + it "Climbs JSON" do results = [] - CeleryScript::JSONClimber.climb(fixture) do |hmm| + CeleryScript::JsonClimber.climb(fixture) do |hmm| results.push(hmm[:kind]) end expect(results).to eq(["parent", "child", "grandchild"]) diff --git a/spec/lib/session_token_spec.rb b/spec/lib/session_token_spec.rb index c0cf31df9..e63aa8083 100644 --- a/spec/lib/session_token_spec.rb +++ b/spec/lib/session_token_spec.rb @@ -38,19 +38,19 @@ describe SessionToken do end it "doesn't honor expired tokens" do - user.update_attributes!(confirmed_at: Time.now) + user.update!(confirmed_at: Time.now) token = SessionToken.issue_to(user, iat: 000, exp: 1, iss: "//lycos.com:9867", fbos_version: Gem::Version.new("9.9.9")) - result = Auth::FromJWT.run(jwt: token.encoded) + result = Auth::FromJwt.run(jwt: token.encoded) expect(result.success?).to be(false) expect(result.errors.values.first.message).to eq(Auth::ReloadToken::BAD_SUB) end unless ENV["NO_EMAILS"] it "doesn't mint tokens for unverified users" do - user.update_attributes!(confirmed_at: nil) + user.update!(confirmed_at: nil) expect { SessionToken.issue_to(user, iat: 000, exp: 1, diff --git a/spec/lib/throttle_policy_spec.rb b/spec/lib/throttle_policy_spec.rb index 54b7dcd5d..3f249562f 100644 --- a/spec/lib/throttle_policy_spec.rb +++ b/spec/lib/throttle_policy_spec.rb @@ -3,25 +3,25 @@ NOW = Time.new("2018-05-18T09:38:02.259-05:00") klass = ThrottlePolicy::TimePeriod describe klass do - let(:policy) do - ThrottlePolicy.new klass.new(1.minute, NOW) => 1, - klass.new(1.hour, NOW) => 10, - klass.new(1.day, NOW) => 100 + let(:policy) do + ThrottlePolicy.new("rspec", { 1.minute => 1, + 1.hour => 10, + 1.day => 100 }, NOW) end it "initializes" do expect(policy.rules).to be expect(policy.rules.map(&:limit).sort).to eq([1, 10, 100]) - actual = policy.rules.map(&:time_period).map(&:time_unit).sort + actual = policy.rules.map(&:time_period).map(&:time_unit).sort expected = [1.minute, 1.hour, 1.day] expect(actual).to eq(expected) end it "tracks things" do - count1 = policy.rules.map(&:time_period).map{|t| t.usage_count_for(123)} + count1 = policy.rules.map(&:time_period).map { |t| t.usage_count_for(123) } expect(count1).to eq([0, 0, 0]) 5.times { policy.track(123, NOW + 1) } - count2 = policy.rules.map(&:time_period).map{|t| t.usage_count_for(123)} + count2 = policy.rules.map(&:time_period).map { |t| t.usage_count_for(123) } expect(count2).to eq([5, 5, 5]) end @@ -34,5 +34,4 @@ describe klass do it "ignores the block when it's over the limit" do expect(policy.is_throttled 123).to be nil end - end diff --git a/spec/lib/time_period_spec.rb b/spec/lib/time_period_spec.rb index 79980d721..1b50c782e 100644 --- a/spec/lib/time_period_spec.rb +++ b/spec/lib/time_period_spec.rb @@ -5,14 +5,13 @@ describe ThrottlePolicy::TimePeriod do it "sets a time unit window size" do expected_time_period = stub_time.to_i / 1.minute.to_i - one_min = ThrottlePolicy::TimePeriod.new(1.minute, stub_time) + one_min = ThrottlePolicy::TimePeriod.new("RSPEC0", 1.minute, stub_time) expect(one_min.current_period).to eq(expected_time_period) expect(one_min.time_unit).to eq(60) - expect(one_min.entries).to eq({}) end it "increments the count" do - t = ThrottlePolicy::TimePeriod.new(1.minute, stub_time) + t = ThrottlePolicy::TimePeriod.new("RSPEC1", 1.minute, stub_time) uid = 123 # Ignore events from the past. @@ -33,10 +32,10 @@ describe ThrottlePolicy::TimePeriod do end it "tells you when the next time period starts" do - one_hour = ThrottlePolicy::TimePeriod.new(1.hour, stub_time) + one_hour = ThrottlePolicy::TimePeriod.new("RSPEC2", 1.hour, stub_time) next_hour = one_hour.when_does_next_period_start? expect(next_hour).to be_kind_of(Time) expect(next_hour.hour).to be(stub_time.hour + 1) - expect(next_hour.min).to be(0) + expect(next_hour.min).to be(0) end end diff --git a/spec/lib/violation_spec.rb b/spec/lib/violation_spec.rb index 56e28fdaa..60197ea62 100644 --- a/spec/lib/violation_spec.rb +++ b/spec/lib/violation_spec.rb @@ -1,15 +1,15 @@ describe ThrottlePolicy::Violation do - violation = ThrottlePolicy::Violation - rule = ThrottlePolicy::Rule + violation = ThrottlePolicy::Violation + rule = ThrottlePolicy::Rule time_period = ThrottlePolicy::TimePeriod - it 'is comparable' do - smaller = violation.new(rule.new(time_period.new(1.minute), 10)) - bigger = violation.new(rule.new(time_period.new(1.day), 10)) - medium = violation.new(rule.new(time_period.new(1.hour), 10)) - violations = [medium, smaller, bigger] - result = violations.sort + it "is comparable" do + smaller = violation.new(rule.new("X", 1.minute, 10, Time.now)) + bigger = violation.new(rule.new("X", 1.day, 10, Time.now)) + medium = violation.new(rule.new("X", 1.hour, 10, Time.now)) + violations = [medium, smaller, bigger] + result = violations.sort expect(result.first).to be(smaller) - expect(result.last).to be(bigger) + expect(result.last).to be(bigger) end end diff --git a/spec/models/device_spec.rb b/spec/models/device_spec.rb index 37a0e9b4b..86c814168 100644 --- a/spec/models/device_spec.rb +++ b/spec/models/device_spec.rb @@ -68,10 +68,10 @@ describe Device do it "throttles a device that sends too many logs" do expect(device).to receive(:tell).and_return(Log.new) - device.update_attributes!(throttled_until: nil) + device.update!(throttled_until: nil) expect(device.throttled_until).to be(nil) - five_minutes = ThrottlePolicy::TimePeriod.new(5.minutes, Time.now + 1.minute) - rule = ThrottlePolicy::Rule.new(five_minutes, 500) + now = Time.now + 1.minute + rule = ThrottlePolicy::Rule.new("X", 5.minutes, 500, now) violation = ThrottlePolicy::Violation.new(rule) device.maybe_throttle(violation) expect(device.throttled_until).to eq(violation.ends_at) @@ -80,10 +80,9 @@ describe Device do it "increases a device throttle time period" do expect(device).to receive(:tell).and_return(Log.new) previous_throttle = Time.now - 1.minute - device.update_attributes!(throttled_until: previous_throttle) + device.update!(throttled_until: previous_throttle) expect(device.throttled_until).to eq(previous_throttle) - five_minutes = ThrottlePolicy::TimePeriod.new(5.minutes, Time.now + 1.minute) - rule = ThrottlePolicy::Rule.new(five_minutes, 500) + rule = ThrottlePolicy::Rule.new("X", 5.minutes, 500, Time.now + 1.minute) violation = ThrottlePolicy::Violation.new(rule) device.maybe_throttle(violation) expect(device.throttled_until).to eq(violation.ends_at) @@ -92,7 +91,7 @@ describe Device do it "unthrottles a runaway device" do expect(device).to receive(:tell).and_return(Log.new) example = Time.now - 1.minute - device.update_attributes!(throttled_until: example) + device.update!(throttled_until: example) expect(device.throttled_until).to eq(example) device.maybe_unthrottle expect(device.throttled_until).to eq(nil) @@ -114,7 +113,7 @@ describe Device do end it "throttled emails about MQTT rate limiting" do - device.update_attributes!(mqtt_rate_limit_email_sent_at: 2.days.ago) + device.update!(mqtt_rate_limit_email_sent_at: 2.days.ago) Device.connection_warning("device_#{device.id.to_s}") time = device.reload.mqtt_rate_limit_email_sent_at expect(time).to be > 1.minute.ago diff --git a/spec/models/fbos_config_spec.rb b/spec/models/fbos_config_spec.rb index 642e62a21..563d94682 100644 --- a/spec/models/fbos_config_spec.rb +++ b/spec/models/fbos_config_spec.rb @@ -16,7 +16,7 @@ describe FbosConfig do it "notifies us of broken production data" do # Remove this test by May 2019. - config.device.update_attributes!(serial_number: nil) + config.device.update!(serial_number: nil) conn = fake_conn("Report broke data") NervesHub.set_conn(conn) problem = "Device #{device.id} missing serial" @@ -37,7 +37,7 @@ describe FbosConfig do expect(NervesHub.conn).to(receive(:put).with(*params).and_return(resp2)) run_jobs_now do - config.update_attributes!(update_channel: "beta") + config.update!(update_channel: "beta") end end diff --git a/spec/models/point_group_spec.rb b/spec/models/point_group_spec.rb index 1d148b485..0e6ba31b5 100644 --- a/spec/models/point_group_spec.rb +++ b/spec/models/point_group_spec.rb @@ -97,8 +97,8 @@ describe PointGroup do expect(error).to eq("Can't delete group because it is in use by sequence 'Wrapper'") end - fit "refuses to delete groups in-use by regimens" do - point_group.update_attributes!(name: "@@@") + it "refuses to delete groups in-use by regimens" do + point_group.update!(name: "@@@") Regimens::Create.run!(name: "Wrapper 26", device: device, color: "red", diff --git a/spec/models/webcam_feed_spec.rb b/spec/models/webcam_feed_spec.rb index 782b260e0..c086dc360 100644 --- a/spec/models/webcam_feed_spec.rb +++ b/spec/models/webcam_feed_spec.rb @@ -4,7 +4,7 @@ describe WebcamFeed do let(:feed) { FactoryBot.create(:webcam_feed) } it "requires a URL" do - result = feed.update_attributes(url: nil) + result = feed.update(url: nil) expect(result).to be(false) expect(result).to be(false) expect(feed.errors.messages[:url]).to include("can't be blank") diff --git a/spec/mutations/auth/create_token_from_credentials_spec.rb b/spec/mutations/auth/create_token_from_credentials_spec.rb index c73ac3cc6..1a1956adf 100644 --- a/spec/mutations/auth/create_token_from_credentials_spec.rb +++ b/spec/mutations/auth/create_token_from_credentials_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe Auth::FromJWT do +describe Auth::FromJwt do let(:user) { FactoryBot.create(:user) } def fake_credentials(email, password) diff --git a/spec/mutations/auth/from_jwt_spec.rb b/spec/mutations/auth/from_jwt_spec.rb index a385f685e..5d25e9232 100644 --- a/spec/mutations/auth/from_jwt_spec.rb +++ b/spec/mutations/auth/from_jwt_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe Auth::FromJWT do +describe Auth::FromJwt do FAKE_VERS = Gem::Version.new("99.9.9") let(:user) { FactoryBot.create(:user) } let(:token) do @@ -19,7 +19,7 @@ describe Auth::FromJWT do } it "gets user from jwt" do - result = Auth::FromJWT.run!(jwt: token) + result = Auth::FromJwt.run!(jwt: token) expect(result).to eq(user) end end diff --git a/spec/mutations/devices/dump_spec.rb b/spec/mutations/devices/dump_spec.rb index 42289aff7..777e00c97 100644 --- a/spec/mutations/devices/dump_spec.rb +++ b/spec/mutations/devices/dump_spec.rb @@ -17,7 +17,7 @@ describe Devices::Dump do .points .where(pointer_type: "ToolSlot") .last - .update_attributes(tool_id: tools.last.id) + .update(tool_id: tools.last.id) plant = device .points .where(pointer_type: "Plant") diff --git a/spec/mutations/flat_ir_stuff/cs_heap_spec.rb b/spec/mutations/flat_ir_stuff/cs_heap_spec.rb index 26684a686..fc21920be 100644 --- a/spec/mutations/flat_ir_stuff/cs_heap_spec.rb +++ b/spec/mutations/flat_ir_stuff/cs_heap_spec.rb @@ -1,9 +1,9 @@ require "spec_helper" -describe CeleryScript::CSHeap do +describe CeleryScript::CsHeap do it "raises if address is bad" do expect do - CeleryScript::CSHeap.new.put(CeleryScript::HeapAddress[99], "no", "no") - end.to raise_error(CeleryScript::CSHeap::BadAddress) + CeleryScript::CsHeap.new.put(CeleryScript::HeapAddress[99], "no", "no") + end.to raise_error(CeleryScript::CsHeap::BadAddress) end end \ No newline at end of file diff --git a/spec/mutations/flat_ir_stuff/first_pass_spec.rb b/spec/mutations/flat_ir_stuff/first_pass_spec.rb index 4ba135ca7..05b4a08c1 100644 --- a/spec/mutations/flat_ir_stuff/first_pass_spec.rb +++ b/spec/mutations/flat_ir_stuff/first_pass_spec.rb @@ -11,10 +11,10 @@ describe CeleryScript::FirstPass do CeleryScript::FlatIrHelpers.fake_first_pass.primary_nodes end - kind = CeleryScript::CSHeap::KIND - parent = CeleryScript::CSHeap::PARENT - next_ = CeleryScript::CSHeap::NEXT - body = CeleryScript::CSHeap::BODY + kind = CeleryScript::CsHeap::KIND + parent = CeleryScript::CsHeap::PARENT + next_ = CeleryScript::CsHeap::NEXT + body = CeleryScript::CsHeap::BODY EXPECTATIONS = { # Came from the JS implementation which is known good. 0 => { kind => "nothing", parent => 0, next_ => 0 }, diff --git a/spec/mutations/flat_ir_stuff/slicer_spec.rb b/spec/mutations/flat_ir_stuff/slicer_spec.rb index 9f9782497..73e76e719 100644 --- a/spec/mutations/flat_ir_stuff/slicer_spec.rb +++ b/spec/mutations/flat_ir_stuff/slicer_spec.rb @@ -2,11 +2,11 @@ require "spec_helper" require_relative "./flat_ir_helpers" describe CeleryScript::Slicer do - kind = CeleryScript::CSHeap::KIND - parent = CeleryScript::CSHeap::PARENT - next_ = CeleryScript::CSHeap::NEXT - body = CeleryScript::CSHeap::BODY - comment = CeleryScript::CSHeap::COMMENT + kind = CeleryScript::CsHeap::KIND + parent = CeleryScript::CsHeap::PARENT + next_ = CeleryScript::CsHeap::NEXT + body = CeleryScript::CsHeap::BODY + comment = CeleryScript::CsHeap::COMMENT n = "nothing"