Merge branch 'master' into logs_search
commit
4c44a3c9e2
|
@ -13,7 +13,6 @@ package-lock.json
|
|||
public/assets/
|
||||
public/direct_upload/temp/*.jpg
|
||||
public/dist
|
||||
public/system
|
||||
tmp
|
||||
api_docs.md
|
||||
erd_diagram.png
|
||||
|
@ -22,8 +21,11 @@ erd.pdf
|
|||
scratchpad.rb
|
||||
/config/master.key
|
||||
config/credentials.yml.enc
|
||||
# ActiveStorage blobs:
|
||||
storage/*
|
||||
|
||||
# === Legacy
|
||||
public/system
|
||||
config/application.yml
|
||||
coverage
|
||||
mqtt/
|
||||
|
|
18
Gemfile
18
Gemfile
|
@ -7,47 +7,39 @@ gem "delayed_job_active_record" # TODO: Get off of SQL backed jobs. Use Redis
|
|||
gem "delayed_job"
|
||||
gem "devise"
|
||||
gem "discard"
|
||||
gem "figaro"
|
||||
gem "fog-google"
|
||||
gem "font-awesome-rails"
|
||||
gem "foreman"
|
||||
gem "google-cloud-storage", "~> 1.11", require: false
|
||||
gem "jwt"
|
||||
gem "mutations"
|
||||
gem "paperclip"
|
||||
gem "pg"
|
||||
gem "polymorphic_constraints"
|
||||
gem "rabbitmq_http_api_client"
|
||||
gem "rack-attack"
|
||||
gem "rack-cors"
|
||||
gem "rails_12factor"
|
||||
gem "rails"
|
||||
gem "redis", "~> 4.0"
|
||||
gem "request_store"
|
||||
gem "rollbar"
|
||||
gem "scenic"
|
||||
gem "secure_headers"
|
||||
gem "tzinfo" # For validation of user selected timezone names
|
||||
gem "valid_url"
|
||||
gem "rabbitmq_http_api_client"
|
||||
gem "zero_downtime_migrations"
|
||||
gem "redis", "~> 4.0"
|
||||
|
||||
group :development, :test do
|
||||
gem "thin"
|
||||
gem "climate_control"
|
||||
gem "codecov", require: false
|
||||
gem "simplecov"
|
||||
gem "database_cleaner"
|
||||
gem "factory_bot_rails"
|
||||
gem "faker"
|
||||
gem "hashdiff"
|
||||
gem "letter_opener"
|
||||
gem "lol_dba"
|
||||
gem "pry-rails"
|
||||
gem "pry"
|
||||
gem "rails-erd"
|
||||
gem "rspec-rails"
|
||||
gem "rspec"
|
||||
gem "selenium-webdriver"
|
||||
gem "simplecov"
|
||||
gem "smarf_doc", git: "https://github.com/RickCarlino/smarf_doc.git"
|
||||
gem "climate_control"
|
||||
end
|
||||
|
||||
group :production do
|
||||
|
|
91
Gemfile.lock
91
Gemfile.lock
|
@ -65,7 +65,6 @@ GEM
|
|||
activesupport
|
||||
childprocess (1.0.1)
|
||||
rake (< 13.0)
|
||||
choice (0.2.0)
|
||||
climate_control (0.2.0)
|
||||
codecov (0.1.14)
|
||||
json
|
||||
|
@ -74,7 +73,6 @@ GEM
|
|||
coderay (1.1.2)
|
||||
concurrent-ruby (1.1.5)
|
||||
crass (1.0.4)
|
||||
daemons (1.3.1)
|
||||
database_cleaner (1.7.0)
|
||||
declarative (0.0.10)
|
||||
declarative-option (0.1.0)
|
||||
|
@ -90,12 +88,11 @@ GEM
|
|||
responders
|
||||
warden (~> 1.2.3)
|
||||
diff-lcs (1.3)
|
||||
digest-crc (0.4.1)
|
||||
discard (1.1.0)
|
||||
activerecord (>= 4.2, < 7)
|
||||
docile (1.3.2)
|
||||
erubi (1.8.0)
|
||||
eventmachine (1.2.7)
|
||||
excon (0.64.0)
|
||||
factory_bot (5.0.2)
|
||||
activesupport (>= 4.2.0)
|
||||
factory_bot_rails (5.0.2)
|
||||
|
@ -107,47 +104,37 @@ GEM
|
|||
multipart-post (>= 1.2, < 3)
|
||||
faraday_middleware (0.13.1)
|
||||
faraday (>= 0.7.4, < 1.0)
|
||||
figaro (1.1.1)
|
||||
thor (~> 0.14)
|
||||
fog-core (2.1.0)
|
||||
builder
|
||||
excon (~> 0.58)
|
||||
formatador (~> 0.2)
|
||||
mime-types
|
||||
fog-google (1.9.1)
|
||||
fog-core (<= 2.1.0)
|
||||
fog-json (~> 1.2)
|
||||
fog-xml (~> 0.1.0)
|
||||
google-api-client (~> 0.23.0)
|
||||
fog-json (1.2.0)
|
||||
fog-core
|
||||
multi_json (~> 1.10)
|
||||
fog-xml (0.1.3)
|
||||
fog-core
|
||||
nokogiri (>= 1.5.11, < 2.0.0)
|
||||
font-awesome-rails (4.7.0.5)
|
||||
railties (>= 3.2, < 6.1)
|
||||
foreman (0.85.0)
|
||||
thor (~> 0.19.1)
|
||||
formatador (0.2.5)
|
||||
globalid (0.4.2)
|
||||
activesupport (>= 4.2.0)
|
||||
google-api-client (0.23.9)
|
||||
google-api-client (0.30.7)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.5, < 0.7.0)
|
||||
googleauth (>= 0.5, < 0.10.0)
|
||||
httpclient (>= 2.8.1, < 3.0)
|
||||
mime-types (~> 3.0)
|
||||
mini_mime (~> 1.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.0)
|
||||
signet (~> 0.9)
|
||||
googleauth (0.6.7)
|
||||
signet (~> 0.10)
|
||||
google-cloud-core (1.3.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-env (1.2.0)
|
||||
faraday (~> 0.11)
|
||||
google-cloud-storage (1.19.0)
|
||||
addressable (~> 2.5)
|
||||
digest-crc (~> 0.4)
|
||||
google-api-client (~> 0.26)
|
||||
google-cloud-core (~> 1.2)
|
||||
googleauth (>= 0.6.2, < 0.10.0)
|
||||
mime-types (~> 3.0)
|
||||
googleauth (0.8.1)
|
||||
faraday (~> 0.12)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.7)
|
||||
hashdiff (0.4.0)
|
||||
hashdiff (1.0.0)
|
||||
hashie (3.6.0)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.6.0)
|
||||
|
@ -155,14 +142,6 @@ GEM
|
|||
json (2.2.0)
|
||||
jsonapi-renderer (0.2.2)
|
||||
jwt (2.2.1)
|
||||
launchy (2.4.3)
|
||||
addressable (~> 2.3)
|
||||
letter_opener (1.7.0)
|
||||
launchy (~> 2.2)
|
||||
lol_dba (2.1.8)
|
||||
actionpack (>= 3.0, < 6.0)
|
||||
activerecord (>= 3.0, < 6.0)
|
||||
railties (>= 3.0, < 6.0)
|
||||
loofah (2.2.3)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
|
@ -188,18 +167,10 @@ GEM
|
|||
mini_portile2 (~> 2.4.0)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.0.1)
|
||||
paperclip (6.1.0)
|
||||
activemodel (>= 4.2.0)
|
||||
activesupport (>= 4.2.0)
|
||||
mime-types
|
||||
mimemagic (~> 0.3.0)
|
||||
terrapin (~> 0.6.0)
|
||||
passenger (6.0.2)
|
||||
rack
|
||||
rake (>= 0.8.1)
|
||||
pg (1.1.4)
|
||||
polymorphic_constraints (1.0.0)
|
||||
rails
|
||||
pry (0.12.2)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.9.0)
|
||||
|
@ -233,11 +204,6 @@ GEM
|
|||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-erd (1.6.0)
|
||||
activerecord (>= 4.2)
|
||||
activesupport (>= 4.2)
|
||||
choice (~> 0.2.0)
|
||||
ruby-graphviz (~> 1.2)
|
||||
rails-html-sanitizer (1.0.4)
|
||||
loofah (~> 2.2, >= 2.2.2)
|
||||
rails_12factor (0.0.3)
|
||||
|
@ -251,7 +217,7 @@ GEM
|
|||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.19.0, < 2.0)
|
||||
rake (12.3.2)
|
||||
rake (12.3.3)
|
||||
redis (4.1.2)
|
||||
representable (3.0.4)
|
||||
declarative (< 0.1.0)
|
||||
|
@ -285,7 +251,6 @@ GEM
|
|||
rspec-mocks (~> 3.8.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-support (3.8.2)
|
||||
ruby-graphviz (1.2.4)
|
||||
rubyzip (1.2.3)
|
||||
scenic (1.5.1)
|
||||
activerecord (>= 4.0.0)
|
||||
|
@ -311,13 +276,7 @@ GEM
|
|||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
terrapin (0.6.0)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
thin (1.7.2)
|
||||
daemons (~> 1.0, >= 1.0.9)
|
||||
eventmachine (~> 1.0, >= 1.0.4)
|
||||
rack (>= 1, < 3)
|
||||
thor (0.19.4)
|
||||
thor (0.20.3)
|
||||
thread_safe (0.3.6)
|
||||
tzinfo (1.2.5)
|
||||
thread_safe (~> 0.1)
|
||||
|
@ -349,26 +308,19 @@ DEPENDENCIES
|
|||
discard
|
||||
factory_bot_rails
|
||||
faker
|
||||
figaro
|
||||
fog-google
|
||||
font-awesome-rails
|
||||
foreman
|
||||
google-cloud-storage (~> 1.11)
|
||||
hashdiff
|
||||
jwt
|
||||
letter_opener
|
||||
lol_dba
|
||||
mutations
|
||||
paperclip
|
||||
passenger
|
||||
pg
|
||||
polymorphic_constraints
|
||||
pry
|
||||
pry-rails
|
||||
rabbitmq_http_api_client
|
||||
rack-attack
|
||||
rack-cors
|
||||
rails
|
||||
rails-erd
|
||||
rails_12factor
|
||||
redis (~> 4.0)
|
||||
request_store
|
||||
|
@ -380,7 +332,6 @@ DEPENDENCIES
|
|||
selenium-webdriver
|
||||
simplecov
|
||||
smarf_doc!
|
||||
thin
|
||||
tzinfo
|
||||
valid_url
|
||||
zero_downtime_migrations
|
||||
|
|
2
Procfile
2
Procfile
|
@ -2,4 +2,4 @@ worker: bundle exec rake jobs:work
|
|||
rabbit_workers: bin/rails r lib/rabbit_workers.rb
|
||||
web: bundle exec passenger start -p $PORT -e $RAILS_ENV --max-pool-size 2
|
||||
# This will perform a hard refresh on all connected browsers.
|
||||
release: rails r "User.delay(run_at: 5.minutes.from_now).refresh_everyones_ui"
|
||||
release: rails r "User.refresh_everyones_ui"
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Please see our [official support policy](http://support-policy.farm.bot).
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please see our [guidelines for responsibly disclosing security vulnerabilities](http://vulnerabilities.farm.bot/).
|
|
@ -24,7 +24,7 @@ module Api
|
|||
after_action :skip_set_cookies_header
|
||||
|
||||
rescue_from(CeleryScript::TypeCheckError) do |err|
|
||||
sorry err.message, 422
|
||||
sorry err.message
|
||||
end
|
||||
|
||||
rescue_from(ActionController::RoutingError) { sorry "Not found", 404 }
|
||||
|
@ -32,25 +32,25 @@ module Api
|
|||
|
||||
rescue_from(JWT::VerificationError) { |e| auth_err }
|
||||
|
||||
rescue_from(ActionDispatch::Http::Parameters::ParseError) { sorry NOT_JSON, 422 }
|
||||
rescue_from(ActionDispatch::Http::Parameters::ParseError) { sorry NOT_JSON }
|
||||
|
||||
rescue_from(ActiveRecord::ValueTooLong) do
|
||||
sorry "Please use reasonable lengths on string inputs", 422
|
||||
sorry "Please use reasonable lengths on string inputs"
|
||||
end
|
||||
|
||||
rescue_from Errors::Forbidden do |exc|
|
||||
sorry "You can't perform that action. #{exc.message}", 403
|
||||
end
|
||||
|
||||
ONLY_JSON = "This is a JSON API. "\
|
||||
ONLY_JSON = "This is a JSON API. " \
|
||||
"Please use a _valid_ JSON object or array. " \
|
||||
"Validate JSON objects at https://jsonlint.com/"
|
||||
rescue_from OnlyJson do |e|
|
||||
sorry ONLY_JSON, 422
|
||||
sorry ONLY_JSON
|
||||
end
|
||||
|
||||
rescue_from Errors::NoBot do |exc|
|
||||
sorry "You need to register a device first.", 422
|
||||
sorry "You need to register a device first."
|
||||
end
|
||||
|
||||
rescue_from ActiveRecord::RecordNotFound do |exc|
|
||||
|
@ -67,9 +67,14 @@ module Api
|
|||
|
||||
rescue_from ActiveModel::RangeError do |_|
|
||||
sorry "One of those numbers was too big/small. " +
|
||||
"If you need larger numbers, let us know.", 422
|
||||
"If you need larger numbers, let us know."
|
||||
end
|
||||
|
||||
TOO_MUCH_DATA = "The resource exceeds database limits. " \
|
||||
"Please reduce the amount of data stored in a single resource"
|
||||
|
||||
rescue_from(PG::ProgramLimitExceeded) { sorry TOO_MUCH_DATA }
|
||||
|
||||
def default_serializer_options
|
||||
{ root: false, user: current_user }
|
||||
end
|
||||
|
@ -147,7 +152,7 @@ module Api
|
|||
" provide a JSON Web Token in the `Authorization:` header.", 401)
|
||||
end
|
||||
|
||||
def sorry(msg, status)
|
||||
def sorry(msg, status = 422)
|
||||
render json: { error: msg }, status: status
|
||||
end
|
||||
|
||||
|
|
|
@ -9,8 +9,8 @@ module Api
|
|||
attr_reader :cache
|
||||
|
||||
CACHE_KEY_TPL = "mqtt_limiter:%s"
|
||||
TTL = 60 * 10 # Ten Minutes
|
||||
PER_DEVICE_MAX = 10
|
||||
TTL = 60 * 5 # Five Minutes
|
||||
PER_DEVICE_MAX = 20
|
||||
MAX_GUEST_COUNT = 256
|
||||
WARNING = "'%s' was rate limited."
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ class ApplicationRecord < ActiveRecord::Base
|
|||
def self.auto_sync_debounce
|
||||
@auto_sync_paused = true
|
||||
result = yield
|
||||
result.update_attributes!(updated_at: Time.now)
|
||||
@auto_sync_paused = false
|
||||
result.broadcast!
|
||||
result
|
||||
|
|
|
@ -74,7 +74,10 @@ module CeleryScriptSettingsBag
|
|||
RESOURCE_UPDATE_ARGS = [:resource_type, :resource_id, :label, :value]
|
||||
SCOPE_DECLARATIONS = [:variable_declaration, :parameter_declaration]
|
||||
MISC_ENUM_ERR = '"%s" is not valid. Allowed values: %s'
|
||||
|
||||
MAX_WAIT_MS = 1000 * 60 * 3 # Three Minutes
|
||||
MAX_WAIT_MS_EXCEEDED =
|
||||
"A single wait node cannot exceed #{MAX_WAIT_MS / 1000 / 60} minutes. " +
|
||||
"Consider lowering the wait time or using multiple WAIT blocks."
|
||||
Corpus = CeleryScript::Corpus.new
|
||||
|
||||
CORPUS_VALUES = {
|
||||
|
@ -453,6 +456,11 @@ module CeleryScriptSettingsBag
|
|||
wait: {
|
||||
args: [:milliseconds],
|
||||
tags: [:function],
|
||||
blk: ->(node) do
|
||||
ms_arg = node.args[:milliseconds]
|
||||
ms = (ms_arg && ms_arg.value) || 0
|
||||
node.invalidate!(MAX_WAIT_MS_EXCEEDED) if ms > MAX_WAIT_MS
|
||||
end,
|
||||
},
|
||||
zero: {
|
||||
args: [:axis],
|
||||
|
|
|
@ -2,8 +2,8 @@ require "open-uri"
|
|||
# A set of image URLs (thumbs) + Associated meta data.
|
||||
class Image < ApplicationRecord
|
||||
belongs_to :device
|
||||
validates :device, presence: true
|
||||
serialize :meta
|
||||
validates :device, presence: true
|
||||
serialize :meta
|
||||
|
||||
# http://stackoverflow.com/a/5127684/1064917
|
||||
after_initialize :set_defaults
|
||||
|
@ -12,37 +12,45 @@ class Image < ApplicationRecord
|
|||
self.meta ||= {}
|
||||
end
|
||||
|
||||
PROTO = ENV["FORCE_SSL"] ? "https:" : "http:"
|
||||
PLACEHOLDER = "/placeholder_farmbot.jpg\?text=Processing..."
|
||||
CONFIG = {
|
||||
default_url: "#{PROTO}#{$API_URL}#{PLACEHOLDER}",
|
||||
styles: { x1280: "1280x1280>",
|
||||
x640: "640x640>",
|
||||
x320: "320x320>",
|
||||
x160: "160x160>",
|
||||
x80: "80x80>" },
|
||||
size: { in: 0..7.megabytes } # Worst case scenario for 1280x1280 BMP.
|
||||
PROTO = ENV["FORCE_SSL"] ? "https:" : "http:"
|
||||
DEFAULT_URL = "#{PROTO}#{$API_URL}#{PLACEHOLDER}"
|
||||
RMAGICK_STYLES = {
|
||||
x1280: "1280x1280>",
|
||||
x640: "640x640>",
|
||||
x320: "320x320>",
|
||||
x160: "160x160>",
|
||||
x80: "80x80>",
|
||||
}
|
||||
MAX_IMAGE_SIZE = 7.megabytes
|
||||
CONFIG = { default_url: DEFAULT_URL,
|
||||
styles: RMAGICK_STYLES,
|
||||
size: { in: 0..MAX_IMAGE_SIZE } }
|
||||
BUCKET = ENV["GCS_BUCKET"]
|
||||
|
||||
bucket = ENV["GCS_BUCKET"]
|
||||
CONFIG.merge!({
|
||||
storage: :fog,
|
||||
fog_host: "http://#{bucket}.storage.googleapis.com",
|
||||
fog_directory: bucket,
|
||||
fog_credentials: { provider: "Google",
|
||||
google_storage_access_key_id: ENV.fetch("GCS_KEY"),
|
||||
google_storage_secret_access_key: ENV.fetch("GCS_ID")}
|
||||
}) if bucket
|
||||
ROOT_PATH = BUCKET ?
|
||||
"https://#{BUCKET}.storage.googleapis.com" : "/system"
|
||||
IMAGE_URL_TPL =
|
||||
ROOT_PATH + "/images/attachments/%{chunks}/%{size}/%{filename}?%{timestamp}"
|
||||
|
||||
has_attached_file :attachment, CONFIG
|
||||
CONTENT_TYPES = ["image/jpg", "image/jpeg", "image/png", "image/gif"]
|
||||
GCS_ACCESS_KEY_ID = ENV.fetch("GCS_KEY") { puts "Not using Google Cloud" }
|
||||
GCS_HOST = "http://#{BUCKET}.storage.googleapis.com"
|
||||
GCS_SECRET_ACCESS_KEY = ENV.fetch("GCS_ID") { puts "Not using Google Cloud" }
|
||||
# Worst case scenario for 1280x1280 BMP.
|
||||
GCS_BUCKET_NAME = ENV["GCS_BUCKET"]
|
||||
|
||||
validates_attachment_content_type :attachment,
|
||||
content_type: ["image/jpg", "image/jpeg", "image/png", "image/gif"]
|
||||
# ========= 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
|
||||
self.attachment = open(url)
|
||||
attachment.attach(io: open(url), filename: "image_#{self.id}")
|
||||
self.attachment_processed_at = Time.now
|
||||
self
|
||||
end
|
||||
|
@ -59,4 +67,47 @@ class Image < ApplicationRecord
|
|||
image = find_by(id: id)
|
||||
image.destroy! if image
|
||||
end
|
||||
|
||||
def legacy_image?
|
||||
!!self.attachment_file_size # This is a now-unused legacy field.
|
||||
end
|
||||
|
||||
def regular_image?
|
||||
attachment && attachment.attached?
|
||||
end
|
||||
|
||||
def regular_url
|
||||
if BUCKET
|
||||
# Not sure why. TODO: Investigate why Rails URL helpers don't work here.
|
||||
"https://storage.googleapis.com/#{BUCKET}/#{attachment.key}"
|
||||
else
|
||||
Rails
|
||||
.application
|
||||
.routes
|
||||
.url_helpers
|
||||
.rails_blob_url(attachment)
|
||||
end
|
||||
end
|
||||
|
||||
def legacy_url(size)
|
||||
url = IMAGE_URL_TPL % {
|
||||
chunks: id.to_s.rjust(9, "0").scan(/.{3}/).join("/"),
|
||||
filename: attachment_file_name,
|
||||
size: size,
|
||||
timestamp: attachment_updated_at.to_i,
|
||||
}
|
||||
return ENV["GCS_KEY"].present? ? url.gsub("http://", "https://") : url
|
||||
end
|
||||
|
||||
def attachment_url(size = "x640")
|
||||
# Detect legacy attachments by way of
|
||||
# superceded PaperClip-related field.
|
||||
# If it has an `attachment_file_size`,
|
||||
# it was made with paperclip.
|
||||
return regular_url if regular_image?
|
||||
|
||||
return legacy_url(size) if legacy_image?
|
||||
|
||||
return DEFAULT_URL
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,50 +1,57 @@
|
|||
require "google/cloud/storage"
|
||||
require "google/cloud/storage/file"
|
||||
|
||||
module Images
|
||||
class GeneratePolicy < Mutations::Command
|
||||
BUCKET = ENV.fetch("GCS_BUCKET") { "YOU_MUST_CONFIG_GOOGLE_CLOUD_STORAGE" }
|
||||
KEY = ENV.fetch("GCS_KEY") { "YOU_MUST_CONFIG_GCS_KEY" }
|
||||
SECRET = ENV.fetch("GCS_ID") { "YOU_MUST_CONFIG_GCS_ID" }
|
||||
BUCKET_NAME = ENV.fetch("GCS_BUCKET") { "YOU_MUST_CONFIG_GOOGLE_CLOUD_STORAGE" }
|
||||
JSON_KEY = ENV["GOOGLE_CLOUD_KEYFILE_JSON"]
|
||||
BUCKET = JSON_KEY && Google::Cloud::Storage.new.bucket(BUCKET_NAME)
|
||||
HMM = "GCS NOT SETUP!"
|
||||
# # Is there a better way to reach in and grab the ActiveStorage configs?
|
||||
# CONFIG = YAML.load(ERB.new(File.read("config/storage.yml")).result(binding)).fetch("google")
|
||||
|
||||
def execute
|
||||
{
|
||||
verb: "POST",
|
||||
url: "//storage.googleapis.com/#{BUCKET}/",
|
||||
verb: "POST",
|
||||
url: "//storage.googleapis.com/#{BUCKET_NAME || HMM}/",
|
||||
form_data: {
|
||||
"key" => random_filename,
|
||||
"acl" => "public-read",
|
||||
"Content-Type" => "image/jpeg",
|
||||
"policy" => policy,
|
||||
"signature" => policy_signature,
|
||||
"GoogleAccessId" => KEY,
|
||||
"file" => "REPLACE_THIS_WITH_A_BINARY_JPEG_FILE"
|
||||
"key" => file_path,
|
||||
"acl" => "public-read",
|
||||
"Content-Type" => "image/jpeg",
|
||||
"policy" => post_object[:policy] || HMM,
|
||||
"signature" => post_object[:signature] || HMM,
|
||||
"GoogleAccessId" => post_object[:GoogleAccessId] || HMM,
|
||||
"file" => "REPLACE_THIS_WITH_A_BINARY_JPEG_FILE",
|
||||
},
|
||||
instructions: "Send a 'from-data' request to the URL provided."\
|
||||
"Then POST the resulting URL as an 'attachment_url' "\
|
||||
"(json) to api/images/."
|
||||
instructions: "Send a 'from-data' request to the URL provided." \
|
||||
"Then POST the resulting URL as an 'attachment_url' " \
|
||||
"(json) to api/images/.",
|
||||
}
|
||||
end
|
||||
private
|
||||
# The image URL in the "untrusted bucket" in Google Cloud Storage
|
||||
def random_filename
|
||||
@range ||= "temp1/#{SecureRandom.uuid}.jpg"
|
||||
|
||||
private
|
||||
|
||||
def post_object
|
||||
@post_object ||= BUCKET ?
|
||||
BUCKET.post_object(file_path, policy: policy).fields : {}
|
||||
end
|
||||
|
||||
def policy
|
||||
@policy ||= Base64.encode64(
|
||||
{ 'expiration' => 1.hour.from_now.utc.xmlschema,
|
||||
'conditions' => [
|
||||
{ 'bucket' => BUCKET },
|
||||
{ 'key' => random_filename},
|
||||
{ 'acl' => 'public-read' },
|
||||
{ 'Content-Type' => "image/jpeg"},
|
||||
['content-length-range', 1, 7.megabytes]
|
||||
]}.to_json).gsub(/\n/, '')
|
||||
@policy ||= {
|
||||
expiration: (Time.now + 1.hour).utc.xmlschema,
|
||||
conditions: [
|
||||
{ bucket: BUCKET_NAME },
|
||||
{ key: file_path },
|
||||
{ acl: "public-read" },
|
||||
[:eq, "$Content-Type", "image/jpeg"],
|
||||
["content-length-range", 1, 7.megabytes],
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
def policy_signature
|
||||
@policy_signature ||= Base64.encode64(
|
||||
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'),
|
||||
SECRET,
|
||||
policy)).gsub("\n",'')
|
||||
# The image URL in the "untrusted bucket" in Google Cloud Storage
|
||||
def file_path
|
||||
@range ||= "temp1/#{SecureRandom.uuid}.jpg"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ module Sequences
|
|||
using CanonicalCeleryHelpers
|
||||
|
||||
required do
|
||||
model :device, class: Device
|
||||
model :device, class: Device
|
||||
string :name
|
||||
body
|
||||
end
|
||||
|
@ -19,18 +19,21 @@ module Sequences
|
|||
end
|
||||
|
||||
def execute
|
||||
ActiveRecord::Base.transaction do
|
||||
p = inputs
|
||||
.merge(migrated_nodes: true)
|
||||
.without(:body, :args, "body", "args")
|
||||
seq = Sequence.create!(p)
|
||||
x = CeleryScript::FirstPass.run!(sequence: seq,
|
||||
scope_hoist = {}
|
||||
Sequence.auto_sync_debounce do
|
||||
ActiveRecord::Base.transaction do
|
||||
p = inputs
|
||||
.merge(migrated_nodes: true)
|
||||
.without(:body, :args, "body", "args")
|
||||
seq = Sequence.create!(p)
|
||||
x = CeleryScript::FirstPass.run!(sequence: seq,
|
||||
args: args || {},
|
||||
body: body || [])
|
||||
result = CeleryScript::FetchCelery.run!(sequence: seq)
|
||||
seq.manually_sync! # We must manually sync this resource.
|
||||
result
|
||||
scope_hoist[:result] = CeleryScript::FetchCelery.run!(sequence: seq)
|
||||
seq
|
||||
end
|
||||
end
|
||||
scope_hoist[:result]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -37,22 +37,23 @@ module Sequences
|
|||
end
|
||||
|
||||
def execute
|
||||
ActiveRecord::Base.transaction do
|
||||
sequence.migrated_nodes = true
|
||||
sequence.update_attributes!(inputs.except(*BLACKLIST))
|
||||
CeleryScript::StoreCelery.run!(sequence: sequence,
|
||||
args: args,
|
||||
body: body)
|
||||
Sequence.auto_sync_debounce do
|
||||
ActiveRecord::Base.transaction do
|
||||
sequence.migrated_nodes = true
|
||||
sequence.update_attributes!(inputs.except(*BLACKLIST))
|
||||
CeleryScript::StoreCelery.run!(sequence: sequence,
|
||||
args: args,
|
||||
body: body)
|
||||
end
|
||||
sequence
|
||||
end
|
||||
sequence.manually_sync! # We must manually sync this resource.
|
||||
CeleryScript::FetchCelery
|
||||
.run!(sequence: sequence, args: args, body: body)
|
||||
CeleryScript::FetchCelery.run!(sequence: sequence, args: args, body: body)
|
||||
end
|
||||
|
||||
BASE = "Can't add 'parent' to sequence because "
|
||||
EXPL = {
|
||||
FarmEvent => BASE + "it is in use by FarmEvents on these dates: %{items}",
|
||||
Regimen => BASE + "the following Regimen(s) are using it: %{items}",
|
||||
Regimen => BASE + "the following Regimen(s) are using it: %{items}",
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
class ImageSerializer < ApplicationSerializer
|
||||
attributes :device_id, :attachment_processed_at, :attachment_url, :meta
|
||||
|
||||
def attachment_url
|
||||
url_ = object.attachment.url("x640")
|
||||
# Force google cloud users to use HTTPS://
|
||||
return ENV["GCS_KEY"].present? ? url_.gsub("http://", "https://") : url_
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,6 +12,10 @@ module FarmBot
|
|||
Delayed::Worker.max_attempts = 4
|
||||
REDIS_ENV_KEY = ENV.fetch("WHERE_IS_REDIS_URL", "REDIS_URL")
|
||||
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.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
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Paperclip.io_adapters.register(Paperclip::StringioAdapter)
|
|
@ -13,6 +13,10 @@ class Rack::Attack
|
|||
throttle("demo_accounts/ip", limit: 10, period: 10.minutes) do |req|
|
||||
req.ip if req.path.downcase == "/demo"
|
||||
end
|
||||
|
||||
throttle("password_resets/ip", limit: 3, period: 1.hour) do |req|
|
||||
req.ip if req.path.downcase == "api/password_resets"
|
||||
end
|
||||
end
|
||||
|
||||
# Always allow requests from localhost
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
# Self-hosters, CI servers, etc:
|
||||
local:
|
||||
service: Disk
|
||||
root: <%= Rails.root.join("storage") %>
|
||||
|
||||
google:
|
||||
service: GCS
|
||||
credentials: <%= ENV["GOOGLE_CLOUD_KEYFILE_JSON"] %>
|
||||
project: <%= ENV["GCS_PROJECT"] %>
|
||||
bucket: <%= ENV["GCS_BUCKET"] %>
|
|
@ -0,0 +1,27 @@
|
|||
# This migration comes from active_storage (originally 20170806125915)
|
||||
class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :active_storage_blobs do |t|
|
||||
t.string :key, null: false
|
||||
t.string :filename, null: false
|
||||
t.string :content_type
|
||||
t.text :metadata
|
||||
t.bigint :byte_size, null: false
|
||||
t.string :checksum, null: false
|
||||
t.datetime :created_at, null: false
|
||||
|
||||
t.index [ :key ], unique: true
|
||||
end
|
||||
|
||||
create_table :active_storage_attachments do |t|
|
||||
t.string :name, null: false
|
||||
t.references :record, null: false, polymorphic: true, index: false
|
||||
t.references :blob, null: false
|
||||
|
||||
t.datetime :created_at, null: false
|
||||
|
||||
t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
|
||||
t.foreign_key :active_storage_blobs, column: :blob_id
|
||||
end
|
||||
end
|
||||
end
|
130
db/structure.sql
130
db/structure.sql
|
@ -43,6 +43,74 @@ SET default_tablespace = '';
|
|||
|
||||
SET default_with_oids = false;
|
||||
|
||||
--
|
||||
-- Name: active_storage_attachments; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public.active_storage_attachments (
|
||||
id bigint NOT NULL,
|
||||
name character varying NOT NULL,
|
||||
record_type character varying NOT NULL,
|
||||
record_id bigint NOT NULL,
|
||||
blob_id bigint NOT NULL,
|
||||
created_at timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: active_storage_attachments_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.active_storage_attachments_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
--
|
||||
-- Name: active_storage_attachments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.active_storage_attachments_id_seq OWNED BY public.active_storage_attachments.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: active_storage_blobs; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public.active_storage_blobs (
|
||||
id bigint NOT NULL,
|
||||
key character varying NOT NULL,
|
||||
filename character varying NOT NULL,
|
||||
content_type character varying,
|
||||
metadata text,
|
||||
byte_size bigint NOT NULL,
|
||||
checksum character varying NOT NULL,
|
||||
created_at timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: active_storage_blobs_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.active_storage_blobs_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
--
|
||||
-- Name: active_storage_blobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.active_storage_blobs_id_seq OWNED BY public.active_storage_blobs.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: alerts; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -1651,6 +1719,20 @@ CREATE SEQUENCE public.webcam_feeds_id_seq
|
|||
ALTER SEQUENCE public.webcam_feeds_id_seq OWNED BY public.webcam_feeds.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: active_storage_attachments id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.active_storage_attachments ALTER COLUMN id SET DEFAULT nextval('public.active_storage_attachments_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: active_storage_blobs id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.active_storage_blobs ALTER COLUMN id SET DEFAULT nextval('public.active_storage_blobs_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: alerts id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -1917,6 +1999,22 @@ ALTER TABLE ONLY public.web_app_configs ALTER COLUMN id SET DEFAULT nextval('pub
|
|||
ALTER TABLE ONLY public.webcam_feeds ALTER COLUMN id SET DEFAULT nextval('public.webcam_feeds_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: active_storage_attachments active_storage_attachments_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.active_storage_attachments
|
||||
ADD CONSTRAINT active_storage_attachments_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: active_storage_blobs active_storage_blobs_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.active_storage_blobs
|
||||
ADD CONSTRAINT active_storage_blobs_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: alerts alerts_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -2244,6 +2342,27 @@ ALTER TABLE ONLY public.webcam_feeds
|
|||
CREATE INDEX delayed_jobs_priority ON public.delayed_jobs USING btree (priority, run_at);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_active_storage_attachments_on_blob_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_active_storage_attachments_on_blob_id ON public.active_storage_attachments USING btree (blob_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_active_storage_attachments_uniqueness; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE UNIQUE INDEX index_active_storage_attachments_uniqueness ON public.active_storage_attachments USING btree (record_type, record_id, name, blob_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_active_storage_blobs_on_key; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE UNIQUE INDEX index_active_storage_blobs_on_key ON public.active_storage_blobs USING btree (key);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_alerts_on_device_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -2818,6 +2937,14 @@ ALTER TABLE ONLY public.alerts
|
|||
ADD CONSTRAINT fk_rails_c0132c78be FOREIGN KEY (device_id) REFERENCES public.devices(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: active_storage_attachments fk_rails_c3b3935057; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.active_storage_attachments
|
||||
ADD CONSTRAINT fk_rails_c3b3935057 FOREIGN KEY (blob_id) REFERENCES public.active_storage_blobs(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: diagnostic_dumps fk_rails_c5df7fdc83; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -3021,6 +3148,7 @@ INSERT INTO "schema_migrations" (version) VALUES
|
|||
('20190621202204'),
|
||||
('20190701155706'),
|
||||
('20190709194037'),
|
||||
('20190715214412');
|
||||
('20190715214412'),
|
||||
('20190722160305');
|
||||
|
||||
|
||||
|
|
|
@ -187,6 +187,15 @@ fieldset {
|
|||
}
|
||||
}
|
||||
|
||||
.throttle-display {
|
||||
.throttle-row {
|
||||
display: flex;
|
||||
.saucer {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wifi-strength-display {
|
||||
position: relative;
|
||||
.percent-bar {
|
||||
|
@ -1020,6 +1029,14 @@ ul {
|
|||
line-height: 1.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
p {
|
||||
display: block;
|
||||
color: $dark_gray;
|
||||
text-overflow: inherit;
|
||||
overflow: inherit;
|
||||
width: inherit;
|
||||
white-space: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.tools-widget,
|
||||
|
|
|
@ -69,6 +69,8 @@
|
|||
}
|
||||
|
||||
.first-ticker {
|
||||
height: 3rem;
|
||||
overflow: hidden;
|
||||
padding-bottom: 0.5rem;
|
||||
background: $black;
|
||||
.status-ticker-created-at {
|
||||
|
|
|
@ -264,8 +264,11 @@ describe("isLog()", function () {
|
|||
|
||||
it("filters sensitive logs", () => {
|
||||
const log = { message: "NERVESPSKWPASSWORD" };
|
||||
console.error = jest.fn();
|
||||
const result = actions.isLog(log);
|
||||
expect(result).toBe(false);
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Refusing to display log"));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -33,8 +33,7 @@ export const FEATURE_MIN_VERSIONS_URL =
|
|||
"https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/" +
|
||||
"FEATURE_MIN_VERSIONS.json";
|
||||
// Already filtering messages in FarmBot OS and the API- this is just for
|
||||
// an additional layer of safety. If sensitive data ever hits a client, it will
|
||||
// be reported to Rollbar for investigation.
|
||||
// an additional layer of safety.
|
||||
const BAD_WORDS = ["WPA", "PSK", "PASSWORD", "NERVES"];
|
||||
|
||||
export function isLog(x: unknown): x is Log {
|
||||
|
|
|
@ -9,6 +9,7 @@ import { SourceFbosConfig, ShouldDisplay, Feature } from "../../interfaces";
|
|||
import { ConfigurationName } from "farmbot";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { LastSeen } from "./last_seen_row";
|
||||
import { Popover } from "@blueprintjs/core";
|
||||
|
||||
/** Return an indicator color for the given temperature (C). */
|
||||
export const colorFromTemp = (temp: number | undefined): string => {
|
||||
|
@ -28,10 +29,15 @@ export const colorFromTemp = (temp: number | undefined): string => {
|
|||
}
|
||||
};
|
||||
|
||||
interface ChipTemperatureDisplayProps {
|
||||
chip?: string;
|
||||
temperature: number | undefined;
|
||||
}
|
||||
|
||||
/** RPI CPU temperature display row: label, temperature, indicator. */
|
||||
export function ChipTemperatureDisplay({ chip, temperature }: {
|
||||
chip?: string, temperature: number | undefined
|
||||
}): JSX.Element {
|
||||
export function ChipTemperatureDisplay(
|
||||
{ chip, temperature }: ChipTemperatureDisplayProps
|
||||
): JSX.Element {
|
||||
return <div className="chip-temp-display">
|
||||
<p>
|
||||
<b>{chip && chip.toUpperCase()} {t("CPU temperature")}: </b>
|
||||
|
@ -41,15 +47,20 @@ export function ChipTemperatureDisplay({ chip, temperature }: {
|
|||
</div>;
|
||||
}
|
||||
|
||||
interface WiFiStrengthDisplayProps {
|
||||
wifiStrength: number | undefined;
|
||||
wifiStrengthPercent?: number | undefined;
|
||||
}
|
||||
|
||||
/** WiFi signal strength display row: label, strength, indicator. */
|
||||
export function WiFiStrengthDisplay({ wifiStrength }: {
|
||||
wifiStrength: number | undefined
|
||||
}): JSX.Element {
|
||||
export function WiFiStrengthDisplay(
|
||||
{ wifiStrength, wifiStrengthPercent }: WiFiStrengthDisplayProps
|
||||
): JSX.Element {
|
||||
const percent = wifiStrength
|
||||
? Math.round(-0.0154 * wifiStrength ** 2 - 0.4 * wifiStrength + 98)
|
||||
: 0;
|
||||
const dbString = `${wifiStrength || 0}dBm`;
|
||||
const percentString = `${percent}%`;
|
||||
const percentString = `${wifiStrengthPercent || percent}%`;
|
||||
return <div className="wifi-strength-display">
|
||||
<p>
|
||||
<b>{t("WiFi Strength")}: </b>
|
||||
|
@ -100,6 +111,36 @@ export const colorFromThrottle =
|
|||
}
|
||||
};
|
||||
|
||||
const THROTTLE_COLOR_KEY = () => ({
|
||||
red: t("active"),
|
||||
yellow: t("occurred"),
|
||||
green: t("clear")
|
||||
});
|
||||
|
||||
interface ThrottleIndicatorProps {
|
||||
throttleDataString: string;
|
||||
throttleType: ThrottleType;
|
||||
}
|
||||
|
||||
/** Saucer with color and title indicating throttle state. */
|
||||
const ThrottleIndicator = (props: ThrottleIndicatorProps) => {
|
||||
const { throttleDataString, throttleType } = props;
|
||||
const throttleColor = colorFromThrottle(throttleDataString, throttleType);
|
||||
return <Saucer className={"small-inline"}
|
||||
title={THROTTLE_COLOR_KEY()[throttleColor]}
|
||||
color={throttleColor} />;
|
||||
};
|
||||
|
||||
/** Visual representation of throttle state. */
|
||||
const ThrottleDisplay = (dataString: string) =>
|
||||
<div className="throttle-display">
|
||||
{Object.keys(THROTTLE_BIT_LOOKUP).map((key: ThrottleType) =>
|
||||
<div className="throttle-row" key={key}>
|
||||
<ThrottleIndicator throttleDataString={dataString} throttleType={key} />
|
||||
<p>{key}</p>
|
||||
</div>)}
|
||||
</div>;
|
||||
|
||||
interface VoltageDisplayProps {
|
||||
chip?: string;
|
||||
throttled: string | undefined;
|
||||
|
@ -112,17 +153,27 @@ export const VoltageDisplay = ({ chip, throttled }: VoltageDisplayProps) =>
|
|||
<p>
|
||||
<b>{chip && chip.toUpperCase()} {t("Voltage")}: </b>
|
||||
</p>
|
||||
<Saucer className={"small-inline"}
|
||||
color={colorFromThrottle(throttled, ThrottleType.UnderVoltage)} />
|
||||
<Popover usePortal={false}>
|
||||
<ThrottleIndicator
|
||||
throttleDataString={throttled}
|
||||
throttleType={ThrottleType.UnderVoltage} />
|
||||
{ThrottleDisplay(throttled)}
|
||||
</Popover>
|
||||
</div> : <div className="voltage-display" />;
|
||||
|
||||
/** Get the first 8 characters of a commit. */
|
||||
const shortenCommit = (longCommit: string) => (longCommit || "").slice(0, 8);
|
||||
|
||||
interface CommitDisplayProps {
|
||||
title: string;
|
||||
repo: string;
|
||||
commit: string;
|
||||
}
|
||||
|
||||
/** GitHub commit display row: label, commit link. */
|
||||
const CommitDisplay = ({ title, repo, commit }: {
|
||||
title: string, repo: string, commit: string
|
||||
}): JSX.Element => {
|
||||
const CommitDisplay = (
|
||||
{ title, repo, commit }: CommitDisplayProps
|
||||
): JSX.Element => {
|
||||
const shortCommit = shortenCommit(commit);
|
||||
return <p>
|
||||
<b>{title}: </b>
|
||||
|
@ -136,8 +187,12 @@ const CommitDisplay = ({ title, repo, commit }: {
|
|||
</p>;
|
||||
};
|
||||
|
||||
interface UptimeDisplayProps {
|
||||
uptime_sec: number;
|
||||
}
|
||||
|
||||
/** FBOS uptime display row: label and uptime in relevant unit. */
|
||||
const UptimeDisplay = ({ uptime_sec }: { uptime_sec: number }): JSX.Element => {
|
||||
const UptimeDisplay = ({ uptime_sec }: UptimeDisplayProps): JSX.Element => {
|
||||
const convertUptime = (seconds: number) => {
|
||||
if (seconds >= 172800) {
|
||||
return `${Math.round(seconds / 86400)} ${t("days")}`;
|
||||
|
@ -152,9 +207,15 @@ const UptimeDisplay = ({ uptime_sec }: { uptime_sec: number }): JSX.Element => {
|
|||
return <p><b>{t("Uptime")}: </b>{convertUptime(uptime_sec)}</p>;
|
||||
};
|
||||
|
||||
export const betaReleaseOptIn = ({ sourceFbosConfig, shouldDisplay }: {
|
||||
sourceFbosConfig: SourceFbosConfig, shouldDisplay: ShouldDisplay
|
||||
}) => {
|
||||
interface BetaReleaseOptInParams {
|
||||
sourceFbosConfig: SourceFbosConfig;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
}
|
||||
|
||||
/** Generate params for BetaReleaseOptInButton. */
|
||||
export const betaReleaseOptIn = (
|
||||
{ sourceFbosConfig, shouldDisplay }: BetaReleaseOptInParams
|
||||
) => {
|
||||
if (shouldDisplay(Feature.use_update_channel)) {
|
||||
const betaOptIn = sourceFbosConfig("update_channel" as ConfigurationName);
|
||||
const betaOptInValue = betaOptIn.value !== "stable";
|
||||
|
@ -172,34 +233,39 @@ export const betaReleaseOptIn = ({ sourceFbosConfig, shouldDisplay }: {
|
|||
}
|
||||
};
|
||||
|
||||
interface BetaReleaseOptInButtonProps {
|
||||
dispatch: Function;
|
||||
sourceFbosConfig: SourceFbosConfig;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
}
|
||||
|
||||
/** Label and toggle button for opting in to FBOS beta releases. */
|
||||
const BetaReleaseOptInButton =
|
||||
({ dispatch, sourceFbosConfig, shouldDisplay }: {
|
||||
dispatch: Function,
|
||||
sourceFbosConfig: SourceFbosConfig,
|
||||
shouldDisplay: ShouldDisplay,
|
||||
}): JSX.Element => {
|
||||
const { betaOptIn, betaOptInValue, update } =
|
||||
betaReleaseOptIn({ sourceFbosConfig, shouldDisplay });
|
||||
return <fieldset>
|
||||
<label style={{ marginTop: "0.75rem" }}>
|
||||
{t("Beta release Opt-In")}
|
||||
</label>
|
||||
<ToggleButton
|
||||
toggleValue={betaOptInValue}
|
||||
dim={!betaOptIn.consistent}
|
||||
toggleAction={() =>
|
||||
(betaOptInValue || confirm(Content.OS_BETA_RELEASES)) &&
|
||||
dispatch(updateConfig(update))} />
|
||||
</fieldset>;
|
||||
};
|
||||
const BetaReleaseOptInButton = (
|
||||
{ dispatch, sourceFbosConfig, shouldDisplay }: BetaReleaseOptInButtonProps
|
||||
): JSX.Element => {
|
||||
const { betaOptIn, betaOptInValue, update } =
|
||||
betaReleaseOptIn({ sourceFbosConfig, shouldDisplay });
|
||||
return <fieldset>
|
||||
<label style={{ marginTop: "0.75rem" }}>
|
||||
{t("Beta release Opt-In")}
|
||||
</label>
|
||||
<ToggleButton
|
||||
toggleValue={betaOptInValue}
|
||||
dim={!betaOptIn.consistent}
|
||||
toggleAction={() =>
|
||||
(betaOptInValue || confirm(Content.OS_BETA_RELEASES)) &&
|
||||
dispatch(updateConfig(update))} />
|
||||
</fieldset>;
|
||||
};
|
||||
|
||||
/** Current technical information about FarmBot OS running on the device. */
|
||||
export function FbosDetails(props: FbosDetailsProps) {
|
||||
const {
|
||||
env, commit, target, node_name, firmware_version, firmware_commit,
|
||||
soc_temp, wifi_level, uptime, memory_usage, disk_usage, throttled
|
||||
} = props.botInfoSettings;
|
||||
soc_temp, wifi_level, uptime, memory_usage, disk_usage, throttled,
|
||||
wifi_level_percent,
|
||||
// tslint:disable-next-line:no-any
|
||||
} = props.botInfoSettings as any;
|
||||
|
||||
return <div>
|
||||
<LastSeen
|
||||
|
@ -219,7 +285,8 @@ export function FbosDetails(props: FbosDetailsProps) {
|
|||
<p><b>{t("Memory usage")}: </b>{memory_usage}MB</p>}
|
||||
{isNumber(disk_usage) && <p><b>{t("Disk usage")}: </b>{disk_usage}%</p>}
|
||||
<ChipTemperatureDisplay chip={target} temperature={soc_temp} />
|
||||
<WiFiStrengthDisplay wifiStrength={wifi_level} />
|
||||
<WiFiStrengthDisplay
|
||||
wifiStrength={wifi_level} wifiStrengthPercent={wifi_level_percent} />
|
||||
<VoltageDisplay chip={target} throttled={throttled} />
|
||||
<BetaReleaseOptInButton
|
||||
dispatch={props.dispatch}
|
||||
|
|
|
@ -23,16 +23,15 @@ import { isString, isFunction } from "lodash";
|
|||
import { repeatOptions } from "../map_state_to_props_add_edit";
|
||||
import { SpecialStatus, ParameterApplication } from "farmbot";
|
||||
import moment from "moment";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import { history } from "../../../history";
|
||||
import {
|
||||
buildResourceIndex
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import { fakeVariableNameSet } from "../../../__test_support__/fake_variables";
|
||||
import { clickButton } from "../../../__test_support__/helpers";
|
||||
import { destroy } from "../../../api/crud";
|
||||
import { destroy, save } from "../../../api/crud";
|
||||
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
|
||||
import { error, success, warning } from "../../../toast/toast";
|
||||
import { error, success } from "../../../toast/toast";
|
||||
|
||||
const mockSequence = fakeSequence();
|
||||
|
||||
|
@ -275,10 +274,14 @@ describe("<FarmEventForm/>", () => {
|
|||
|
||||
it("warns about missed regimen items", async () => {
|
||||
const p = props();
|
||||
const state = fakeState();
|
||||
state.resources.index.references = { [p.farmEvent.uuid]: p.farmEvent };
|
||||
p.dispatch = jest.fn(x => { isFunction(x) && x(); return Promise.resolve(); });
|
||||
p.farmEvent.body.executable_type = "Regimen";
|
||||
const regimen = fakeRegimen();
|
||||
regimen.body.regimen_items = [
|
||||
{ sequence_id: -1, time_offset: 0 },
|
||||
{ sequence_id: -1, time_offset: 1000000000 },
|
||||
];
|
||||
p.findExecutable = () => regimen;
|
||||
p.dispatch = jest.fn(x => { isFunction(x) && x(); return Promise.resolve(); });
|
||||
p.farmEvent.body.start_time = "2017-05-22T05:00:00.000Z";
|
||||
p.farmEvent.body.end_time = "2017-05-22T06:00:00.000Z";
|
||||
const i = instance(p);
|
||||
|
@ -334,14 +337,15 @@ describe("<FarmEventForm/>", () => {
|
|||
expectStartTimeToBeRejected();
|
||||
});
|
||||
|
||||
it("doesn't display error message on edit: start time has passed", () => {
|
||||
it("displays error message on edit: start time has passed", () => {
|
||||
const p = props();
|
||||
p.title = "edit";
|
||||
p.farmEvent.body.start_time = "2017-05-22T05:00:00.000Z";
|
||||
p.farmEvent.body.end_time = "2017-05-22T06:00:00.000Z";
|
||||
const i = instance(p);
|
||||
i.commitViewModel(moment("2017-06-22T05:00:00.000Z"));
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
expect(error).toHaveBeenCalledWith(expect.stringContaining(
|
||||
"Nothing to run."), "Unable to save event.");
|
||||
});
|
||||
|
||||
it("displays error message on save: no items", async () => {
|
||||
|
@ -351,8 +355,22 @@ describe("<FarmEventForm/>", () => {
|
|||
p.farmEvent.body.end_time = "2017-05-22T06:00:00.000Z";
|
||||
const i = instance(p);
|
||||
await i.commitViewModel(moment("2017-06-22T05:00:00.000Z"));
|
||||
expect(warning).toHaveBeenCalledWith(expect.stringContaining(
|
||||
"Nothing to run."));
|
||||
expect(error).toHaveBeenCalledWith(expect.stringContaining(
|
||||
"Nothing to run."), "Unable to save event.");
|
||||
});
|
||||
|
||||
it("displays error message on save: save error", async () => {
|
||||
const p = props();
|
||||
p.dispatch = jest.fn()
|
||||
.mockResolvedValueOnce("")
|
||||
.mockRejectedValueOnce("error");
|
||||
p.shouldDisplay = () => true;
|
||||
p.farmEvent.body.start_time = "2017-07-22T05:00:00.000Z";
|
||||
p.farmEvent.body.end_time = "2017-07-22T06:00:00.000Z";
|
||||
const i = instance(p);
|
||||
await i.commitViewModel(moment("2017-06-22T05:00:00.000Z"));
|
||||
await expect(save).toHaveBeenCalled();
|
||||
expect(error).toHaveBeenCalledWith("Unable to save event.");
|
||||
});
|
||||
|
||||
it("allows start time: edit with unsupported OS", () => {
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { maybeWarnAboutMissedTasks } from "../util";
|
||||
import { fakeFarmEvent } from "../../../__test_support__/fake_state/resources";
|
||||
import moment from "moment";
|
||||
import { ExecutableType } from "farmbot/dist/resources/api_resources";
|
||||
|
||||
describe("maybeWarnAboutMissedTasks()", () => {
|
||||
function testWarn(time: string): () => void {
|
||||
function testWarn(
|
||||
time: string, executableType: ExecutableType = "Regimen"
|
||||
): () => void {
|
||||
const callback = jest.fn();
|
||||
const fe = fakeFarmEvent("Regimen", 1);
|
||||
const fe = fakeFarmEvent(executableType, 1);
|
||||
fe.body.start_time = "2017-05-21T22:00:00.000";
|
||||
maybeWarnAboutMissedTasks(fe,
|
||||
() => callback("missed event warning"),
|
||||
|
@ -21,4 +24,9 @@ describe("maybeWarnAboutMissedTasks()", () => {
|
|||
const cb = testWarn("2017-05-01T22:00:00.000");
|
||||
expect(cb).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("doesn't warn when not a regimen", () => {
|
||||
const cb = testWarn("2017-05-21T22:00:00.000", "Sequence");
|
||||
expect(cb).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
import moment from "moment";
|
||||
import { success, error, warning } from "../../toast/toast";
|
||||
import { success, error } from "../../toast/toast";
|
||||
import {
|
||||
TaggedFarmEvent, SpecialStatus, TaggedSequence, TaggedRegimen,
|
||||
ParameterApplication
|
||||
|
@ -94,6 +94,13 @@ const startTimeWarning = () => {
|
|||
error(message, title);
|
||||
};
|
||||
|
||||
const nothingToRunWarning = () => {
|
||||
const message =
|
||||
t("All items scheduled before the start time. Nothing to run.");
|
||||
const title = t("Unable to save event.");
|
||||
error(message, title);
|
||||
};
|
||||
|
||||
type RecombineOptions = { forceRegimensToMidnight: boolean };
|
||||
|
||||
/** Take a FormViewModel and recombine the fields into a FarmEvent
|
||||
|
@ -323,9 +330,8 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
|
|||
return recombine(vm, opts);
|
||||
}
|
||||
|
||||
/** Use the next item run time to display toast messages and return to
|
||||
* the form if necessary. */
|
||||
nextRunTimeActions = (now = moment()): boolean => {
|
||||
/** Use the next item run time to display toast messages. */
|
||||
nextRunTimeActions = (now = moment()) => {
|
||||
const nextRun = this.nextItemTime(this.props.farmEvent.body, now);
|
||||
if (nextRun) {
|
||||
const nextRunText = this.props.autoSyncEnabled
|
||||
|
@ -335,10 +341,6 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
|
|||
you must first SYNC YOUR DEVICE. If you do not sync, the event will
|
||||
not run.`.replace(/\s+/g, " "), { timeFromNow: nextRun.from(now) });
|
||||
success(nextRunText);
|
||||
return true;
|
||||
} else {
|
||||
warning(t("All items scheduled before the start time. Nothing to run."));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -346,24 +348,27 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
|
|||
* - Regimen Farm Event:
|
||||
* * If scheduled for today, warn about the possibility of missing tasks.
|
||||
* * Display the start time difference from now and maybe prompt to sync.
|
||||
* * Return to calendar view if items exist to be run past the start time.
|
||||
* * Return to calendar view.
|
||||
* - Sequence Farm Event:
|
||||
* * Determine the time for the next item to be run.
|
||||
* * If auto-sync is disabled, prompt the user to sync.
|
||||
* * Return to calendar view only if more items exist to be run.
|
||||
* * Return to calendar view.
|
||||
*/
|
||||
commitViewModel = (now = moment()) => {
|
||||
if (this.maybeRejectStartTime(this.updatedFarmEvent)) {
|
||||
return startTimeWarning();
|
||||
}
|
||||
if (!this.nextItemTime(this.updatedFarmEvent, now)) {
|
||||
return nothingToRunWarning();
|
||||
}
|
||||
this.dispatch(overwrite(this.props.farmEvent, this.updatedFarmEvent));
|
||||
this.dispatch(save(this.props.farmEvent.uuid))
|
||||
.then(() => {
|
||||
this.setState({ specialStatusLocal: SpecialStatus.SAVED });
|
||||
this.dispatch(maybeWarnAboutMissedTasks(this.props.farmEvent,
|
||||
() => alert(t(Content.REGIMEN_TODAY_SKIPPED_ITEM_RISK)), now));
|
||||
const itemsScheduled = this.nextRunTimeActions(now);
|
||||
if (itemsScheduled) { history.push("/app/designer/events"); }
|
||||
this.nextRunTimeActions(now);
|
||||
history.push("/app/designer/events");
|
||||
})
|
||||
.catch(() => {
|
||||
error(t("Unable to save event."));
|
||||
|
|
|
@ -91,7 +91,7 @@ export function mapStateToProps(props: Everything): FarmwareProps {
|
|||
shouldDisplay(Feature.api_farmware_installations) &&
|
||||
taggedFarmwareInstallations.map(x => {
|
||||
const name = namePendingInstall(x.body.package, x.body.id);
|
||||
const alreadyAdded = Object.keys(farmwares).includes(name);
|
||||
const alreadyAdded = Object.keys(farmwares).includes(x.body.package || name);
|
||||
const alreadyInstalled = Object.values(farmwares)
|
||||
.map(fw => fw.url).includes(x.body.url);
|
||||
if (x.body.id && !alreadyAdded && !alreadyInstalled) {
|
||||
|
|
|
@ -17,7 +17,7 @@ describe("Contextual `Alert` creation", () => {
|
|||
expect(results[0]).toEqual({
|
||||
created_at: 1,
|
||||
problem_tag: "farmbot_os.firmware.missing",
|
||||
priority: 0,
|
||||
priority: 500,
|
||||
slug: "firmware-missing",
|
||||
});
|
||||
});
|
||||
|
|
|
@ -23,7 +23,7 @@ const toggleAlert = (s: State, body: TaggedFbosConfig["body"]) => {
|
|||
s.alerts[FIRMWARE_MISSING] = {
|
||||
created_at: 1,
|
||||
problem_tag: FIRMWARE_MISSING,
|
||||
priority: 0,
|
||||
priority: 500,
|
||||
slug: "firmware-missing",
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,11 +4,12 @@ export interface SaucerProps {
|
|||
color?: string;
|
||||
active?: boolean;
|
||||
className?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/** A colored UI disc/circle. */
|
||||
export function Saucer({ color, active, className }: SaucerProps) {
|
||||
export function Saucer({ color, active, className, title }: SaucerProps) {
|
||||
const classes = ["saucer", color, className];
|
||||
if (active) { classes.push("active"); }
|
||||
return <div className={classes.join(" ")} />;
|
||||
return <div className={classes.join(" ")} title={title} />;
|
||||
}
|
||||
|
|
14
package.json
14
package.json
|
@ -24,20 +24,20 @@
|
|||
"author": "farmbot.io",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "7.5.4",
|
||||
"@babel/core": "7.5.5",
|
||||
"@blueprintjs/core": "3.17.1",
|
||||
"@blueprintjs/datetime": "3.11.0",
|
||||
"@blueprintjs/select": "3.9.0",
|
||||
"@types/enzyme": "3.10.2",
|
||||
"@types/enzyme": "3.10.3",
|
||||
"@types/jest": "24.0.15",
|
||||
"@types/lodash": "4.14.136",
|
||||
"@types/markdown-it": "0.0.8",
|
||||
"@types/moxios": "0.4.8",
|
||||
"@types/node": "12.6.2",
|
||||
"@types/node": "12.6.8",
|
||||
"@types/promise-timeout": "1.3.0",
|
||||
"@types/react": "16.8.23",
|
||||
"@types/react-color": "3.0.1",
|
||||
"@types/react-dom": "16.8.4",
|
||||
"@types/react-dom": "16.8.5",
|
||||
"@types/react-redux": "7.1.1",
|
||||
"axios": "0.19.0",
|
||||
"boxed_value": "1.0.0",
|
||||
|
@ -47,7 +47,7 @@
|
|||
"enzyme-adapter-react-16": "1.14.0",
|
||||
"farmbot": "8.0.3-rc1",
|
||||
"i18next": "17.0.6",
|
||||
"lodash": "4.17.14",
|
||||
"lodash": "4.17.15",
|
||||
"markdown-it": "9.0.1",
|
||||
"markdown-it-emoji": "1.4.0",
|
||||
"moment": "2.24.0",
|
||||
|
@ -60,14 +60,14 @@
|
|||
"react-addons-test-utils": "15.6.2",
|
||||
"react-color": "2.17.3",
|
||||
"react-dom": "16.8.6",
|
||||
"react-joyride": "2.1.0",
|
||||
"react-joyride": "2.1.1",
|
||||
"react-redux": "7.1.0",
|
||||
"react-test-renderer": "16.8.6",
|
||||
"react-transition-group": "4.2.1",
|
||||
"redux": "4.0.4",
|
||||
"redux-immutable-state-invariant": "2.1.0",
|
||||
"redux-thunk": "2.3.0",
|
||||
"sass": "1.22.4",
|
||||
"sass": "1.22.7",
|
||||
"sass-lint": "1.13.1",
|
||||
"takeme": "0.11.1",
|
||||
"ts-jest": "24.0.2",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -18,19 +18,19 @@ npm run translation-check
|
|||
|
||||
See the [README](https://github.com/FarmBot/Farmbot-Web-App#translating-the-web-app-into-your-language) for contribution instructions.
|
||||
|
||||
Total number of phrases identified by the language helper for translation: __1017__
|
||||
Total number of phrases identified by the language helper for translation: __1023__
|
||||
|
||||
|Language|Percent translated|Translated|Untranslated|Other Translations|
|
||||
|:---:|---:|---:|---:|---:|
|
||||
|da|11%|111|906|22|
|
||||
|de|42%|425|592|122|
|
||||
|es|77%|781|236|152|
|
||||
|fr|71%|719|298|180|
|
||||
|it|9%|88|929|174|
|
||||
|nl|8%|80|937|142|
|
||||
|pt|7%|72|945|161|
|
||||
|ru|59%|605|412|206|
|
||||
|zh|9%|87|930|142|
|
||||
|da|11%|111|912|22|
|
||||
|de|42%|425|598|122|
|
||||
|es|100%|1023|0|152|
|
||||
|fr|70%|719|304|180|
|
||||
|it|9%|88|935|174|
|
||||
|nl|8%|80|943|142|
|
||||
|pt|7%|72|951|161|
|
||||
|ru|59%|606|417|205|
|
||||
|zh|9%|87|936|142|
|
||||
|
||||
**Percent translated** refers to the percent of phrases identified by the
|
||||
language helper that have been translated. Additional phrases not identified
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
Please see our guidelines for responsibly disclosing security vulnerabilities:
|
||||
http://vulnerabilities.farm.bot/
|
|
@ -5,20 +5,20 @@ describe Api::FarmEventsController do
|
|||
|
||||
describe "#update" do
|
||||
let(:user) { FactoryBot.create(:user) }
|
||||
let(:fe) { FactoryBot.create(:farm_event, device: user.device) }
|
||||
let(:fe) { FactoryBot.create(:farm_event, device: user.device) }
|
||||
it "allows authorized modification" do
|
||||
sign_in user
|
||||
id = FactoryBot.create(:farm_event, device: user.device).id
|
||||
input = { id: id, farm_event: { repeat: 66 } }
|
||||
patch :update, format: :json, body: input.to_json, params: {id: id}
|
||||
patch :update, format: :json, body: input.to_json, params: { id: id }
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
it "prevents unauthorized modification" do
|
||||
sign_in user
|
||||
id = FactoryBot.create(:farm_event).id
|
||||
id = FactoryBot.create(:farm_event).id
|
||||
input = { id: id, repeat: 66 }
|
||||
patch :update, format: :json, body: input.to_json, params: {id: id}
|
||||
patch :update, format: :json, body: input.to_json, params: { id: id }
|
||||
expect(response.status).to eq(403)
|
||||
expect(json[:error]).to include("Not your farm_event")
|
||||
end
|
||||
|
@ -27,8 +27,8 @@ describe Api::FarmEventsController do
|
|||
sign_in user
|
||||
id = FactoryBot.create(:farm_event, device: user.device).id
|
||||
patch :update,
|
||||
format: :json,
|
||||
body: { id: id, repeat: 1, time_unit: FarmEvent::NEVER }.to_json,
|
||||
format: :json,
|
||||
body: { id: id, repeat: 1, time_unit: FarmEvent::NEVER }.to_json,
|
||||
params: { id: id }
|
||||
fe = FarmEvent.find(id)
|
||||
expect(response.status).to eq(200)
|
||||
|
@ -41,7 +41,7 @@ describe Api::FarmEventsController do
|
|||
id = FactoryBot.create(:farm_event, device: user.device).id
|
||||
patch :update,
|
||||
format: :json,
|
||||
body: { id: id, end_time: "+045633-08-18T13:25:00.000Z" }.to_json,
|
||||
body: { id: id, end_time: "+045633-08-18T13:25:00.000Z" }.to_json,
|
||||
params: { id: id }
|
||||
expect(response.status).to eq(422)
|
||||
expect(json[:end_time]).to include("too far in the future")
|
||||
|
@ -49,19 +49,20 @@ describe Api::FarmEventsController do
|
|||
|
||||
def create_fe_with_fragment
|
||||
fragment = Fragment.from_celery(
|
||||
owner: fe,
|
||||
owner: fe,
|
||||
device: user.device,
|
||||
kind: "internal_farm_event",
|
||||
args: {},
|
||||
body: [
|
||||
kind: "internal_farm_event",
|
||||
args: {},
|
||||
body: [
|
||||
{
|
||||
kind: "parameter_application",
|
||||
args: {
|
||||
label: "foo",
|
||||
data_value: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } }
|
||||
}
|
||||
}
|
||||
])
|
||||
data_value: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } },
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
return fe
|
||||
end
|
||||
|
||||
|
@ -69,35 +70,37 @@ describe Api::FarmEventsController do
|
|||
sign_in user
|
||||
patch :update,
|
||||
format: :json,
|
||||
body: { body: body }.to_json,
|
||||
body: { body: body }.to_json,
|
||||
params: { id: fe.id }
|
||||
end
|
||||
|
||||
it "ignores fragment when body is `nil`" do
|
||||
fe = create_fe_with_fragment
|
||||
fe = create_fe_with_fragment
|
||||
fragment_b4 = fe.fragment.id
|
||||
expect(fe.fragment).not_to be(nil)
|
||||
update_body(fe, nil)
|
||||
expect(response.status).to eq(200)
|
||||
expect(fe.reload.fragment).not_to be(nil)
|
||||
expect(fe.fragment.id).to eq(fragment_b4)
|
||||
expect(fe.fragment.id).to eq(fragment_b4)
|
||||
end
|
||||
|
||||
it "deletes old fragment when body is `[]`" do
|
||||
fe = create_fe_with_fragment
|
||||
expect(fe.fragment).not_to be(nil)
|
||||
update_body(fe, [])
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.status).to eq(200)
|
||||
expect(fe.reload.fragment).to be(nil)
|
||||
expect(json.fetch(:body)).to eq([])
|
||||
expect(json.fetch(:body)).to eq([])
|
||||
end
|
||||
|
||||
it "replaces old fragment when given a new one" do
|
||||
fe = create_fe_with_fragment
|
||||
expect(fe.fragment).not_to be(nil)
|
||||
old_timestamp = fe.updated_at
|
||||
update_body(fe, nil)
|
||||
expect(response.status).to eq(200)
|
||||
expect(fe.reload.fragment).not_to be(nil)
|
||||
expect(fe.updated_at).to be > old_timestamp
|
||||
end
|
||||
|
||||
it "inserts new fragment when there originally was none" do
|
||||
|
@ -113,11 +116,11 @@ describe Api::FarmEventsController do
|
|||
args: {
|
||||
x: 1,
|
||||
y: 2,
|
||||
z: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
z: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
update_body(fe, body)
|
||||
expect(response.status).to eq(200)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
require 'spec_helper'
|
||||
require "spec_helper"
|
||||
|
||||
describe Api::ImagesController do
|
||||
include Devise::Test::ControllerHelpers
|
||||
let(:user) { FactoryBot.create(:user) }
|
||||
|
||||
it "Creates a policy object" do
|
||||
sign_in user
|
||||
b4 = Api::ImagesController.store_locally
|
||||
|
@ -15,8 +16,7 @@ describe Api::ImagesController do
|
|||
expect(json[:verb]).to eq("POST")
|
||||
expect(json[:url]).to include("googleapis")
|
||||
expect(json[:form_data].keys.sort).to include(:signature)
|
||||
expect(json[:instructions])
|
||||
.to include("POST the resulting URL as an 'attachment_url'")
|
||||
expect(json[:instructions]).to include("POST the resulting URL as an 'attachment_url'")
|
||||
end
|
||||
|
||||
it "Creates a (stub) policy object" do
|
||||
|
@ -29,13 +29,13 @@ describe Api::ImagesController do
|
|||
expect(json).to be_kind_of(Hash)
|
||||
expect(json[:verb]).to eq("POST")
|
||||
expect(json[:url]).to include($API_URL)
|
||||
[ :policy, :signature, :GoogleAccessId ]
|
||||
[:policy, :signature, :GoogleAccessId]
|
||||
.map { |key| expect(json.dig(:form_data, key)).to eq("N/A") }
|
||||
expect(json[:form_data].keys.sort).to include(:signature)
|
||||
end
|
||||
|
||||
describe '#index' do
|
||||
it 'shows only the max images allowed' do
|
||||
describe "#index" do
|
||||
it "shows only the max images allowed" do
|
||||
sign_in user
|
||||
device = user.device
|
||||
# Using the *real* value (10) was super slow (~30 seconds)
|
||||
|
@ -48,8 +48,8 @@ describe Api::ImagesController do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#show' do
|
||||
it 'shows image meta data' do
|
||||
describe "#show" do
|
||||
it "shows image meta data" do
|
||||
sign_in user
|
||||
image = FactoryBot.create(:image, device: user.device)
|
||||
get :show, params: { id: image.id }
|
||||
|
@ -62,13 +62,13 @@ describe Api::ImagesController do
|
|||
end
|
||||
|
||||
describe "#create" do
|
||||
it 'creates one image', :slow do
|
||||
it "creates one image", :slow do
|
||||
sign_in user
|
||||
before_count = Image.count
|
||||
post :create,
|
||||
body: { attachment_url: FAKE_ATTACHMENT_URL,
|
||||
meta: { x: 1, z: 3 } }.to_json,
|
||||
params: {format: :json}
|
||||
params: { format: :json }
|
||||
expect(response.status).to eq(200)
|
||||
expect(Image.count).to be > before_count
|
||||
expect(json[:device_id]).to eq(user.device.id)
|
||||
|
@ -79,8 +79,8 @@ describe Api::ImagesController do
|
|||
expect(json.dig :meta, :z).to eq(3)
|
||||
end
|
||||
|
||||
describe '#delete' do
|
||||
it 'deletes an image' do
|
||||
describe "#delete" do
|
||||
it "deletes an image" do
|
||||
sign_in user
|
||||
image = FactoryBot.create(:image, device: user.device)
|
||||
before_count = Image.count
|
||||
|
|
|
@ -30,16 +30,15 @@ describe Api::PointsController do
|
|||
pointer_type: "Plant",
|
||||
openfarm_slug: "mung-bean",
|
||||
planted_at: time,
|
||||
plant_stage: "sprouted"
|
||||
}
|
||||
plant_stage: "sprouted" }
|
||||
post :create, body: p.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(200)
|
||||
plant = Plant.last
|
||||
expect(plant.x).to eq(p[:x])
|
||||
expect(plant.y).to eq(p[:y])
|
||||
expect(plant.name).to eq(p[:name])
|
||||
expect(plant.plant_stage).to eq("sprouted")
|
||||
expect(p[:plant_stage]).to eq("sprouted")
|
||||
expect(plant.x).to eq(p[:x])
|
||||
expect(plant.y).to eq(p[:y])
|
||||
expect(plant.name).to eq(p[:name])
|
||||
expect(plant.plant_stage).to eq("sprouted")
|
||||
expect(p[:plant_stage]).to eq("sprouted")
|
||||
expect(plant.openfarm_slug).to eq(p[:openfarm_slug])
|
||||
expect(plant.created_at).to be_truthy
|
||||
p.keys.each do |key|
|
||||
|
@ -52,20 +51,20 @@ describe Api::PointsController do
|
|||
body = { pointer_type: "TypoPointer", x: 0, y: 0 }
|
||||
post :create, body: body.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(422)
|
||||
expected = "Please provide a JSON object "\
|
||||
expected = "Please provide a JSON object " \
|
||||
"with a `pointer_type` that matches"
|
||||
expect(json.fetch(:pointer_type)).to include(expected)
|
||||
end
|
||||
|
||||
it "creates a point" do
|
||||
sign_in user
|
||||
body = { x: 1,
|
||||
y: 2,
|
||||
z: 3,
|
||||
radius: 3,
|
||||
name: "YOLO",
|
||||
body = { x: 1,
|
||||
y: 2,
|
||||
z: 3,
|
||||
radius: 3,
|
||||
name: "YOLO",
|
||||
pointer_type: "GenericPointer",
|
||||
meta: { foo: "BAR" } }
|
||||
meta: { foo: "BAR" } }
|
||||
post :create, body: body.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(200)
|
||||
expect(json[:name]).to eq(body[:name])
|
||||
|
@ -80,11 +79,11 @@ describe Api::PointsController do
|
|||
|
||||
it "requires x" do
|
||||
sign_in user
|
||||
body = { y: 2,
|
||||
z: 3,
|
||||
radius: 3,
|
||||
body = { y: 2,
|
||||
z: 3,
|
||||
radius: 3,
|
||||
pointer_type: "GenericPointer",
|
||||
meta: { foo: "BAR" } }
|
||||
meta: { foo: "BAR" } }
|
||||
post :create, body: body.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(422)
|
||||
expect(json[:x]).to be
|
||||
|
@ -97,8 +96,7 @@ describe Api::PointsController do
|
|||
SmarfDoc.note("This is what happens when you post bad JSON")
|
||||
post :create, body: "{'x': 0, 'this isnt': 'JSON'}", params: { format: :json }
|
||||
expect(response.status).to eq(422)
|
||||
expect(json[:error])
|
||||
.to include("Please use a _valid_ JSON object or array")
|
||||
expect(json[:error]).to include("Please use a _valid_ JSON object or array")
|
||||
end
|
||||
|
||||
it "creates a toolslot with an valid pullout direction" do
|
||||
|
@ -106,15 +104,15 @@ describe Api::PointsController do
|
|||
sign_in user
|
||||
before_count = ToolSlot.count
|
||||
post :create,
|
||||
body: {
|
||||
pointer_type: "ToolSlot",
|
||||
name: "foo",
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
pullout_direction: direction
|
||||
}.to_json,
|
||||
params: { format: :json }
|
||||
body: {
|
||||
pointer_type: "ToolSlot",
|
||||
name: "foo",
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
pullout_direction: direction,
|
||||
}.to_json,
|
||||
params: { format: :json }
|
||||
expect(response.status).to eq(200)
|
||||
expect(ToolSlot.count).to be > before_count
|
||||
expect(json[:pullout_direction]).to eq(direction)
|
||||
|
@ -128,7 +126,7 @@ describe Api::PointsController do
|
|||
y: 0,
|
||||
z: 0 }
|
||||
old_tool_count = ToolSlot.count
|
||||
post :create, body: payload.to_json, params: {format: :json}
|
||||
post :create, body: payload.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(200)
|
||||
expect(ToolSlot.count).to be > old_tool_count
|
||||
expect(json[:pullout_direction]).to eq(0)
|
||||
|
@ -137,16 +135,32 @@ describe Api::PointsController do
|
|||
it "disallows bad `tool_id`s" do
|
||||
sign_in user
|
||||
payload = { pointer_type: "ToolSlot",
|
||||
name: "foo",
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
tool_id: (Tool.count + 100) }
|
||||
name: "foo",
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
tool_id: (Tool.count + 100) }
|
||||
old_tool_count = ToolSlot.count
|
||||
post :create, body: payload.to_json, params: {format: :json}
|
||||
post :create, body: payload.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(422)
|
||||
expect(ToolSlot.count).to eq old_tool_count
|
||||
expect(json[:tool_id]).to include("Can't find tool")
|
||||
end
|
||||
|
||||
it "gracefully handles PG::ProgramLimitExceeded" do
|
||||
absurdly_large_metadata =
|
||||
{ key: (1..85).to_a.map { |x| SecureRandom.hex }.join }
|
||||
sign_in user
|
||||
payload = { pointer_type: "GenericPointer",
|
||||
name: "foo",
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
meta: absurdly_large_metadata }
|
||||
old_tool_count = ToolSlot.count
|
||||
post :create, body: payload.to_json, params: { format: :json }
|
||||
expect(response.status).to eq(422)
|
||||
expect(json.fetch(:error)).to eq(Api::AbstractController::TOO_MUCH_DATA)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Api::RegimensController do
|
||||
|
||||
include Devise::Test::ControllerHelpers
|
||||
|
||||
describe "#update" do
|
||||
let(:user) { FactoryBot.create(:user) }
|
||||
let(:sequence) { FakeSequence.create( device: user.device) }
|
||||
let(:sequence) { FakeSequence.create(device: user.device) }
|
||||
|
||||
it "updates an old regimen" do
|
||||
sign_in user
|
||||
|
@ -17,20 +16,20 @@ describe Api::RegimensController do
|
|||
"name" => "something new",
|
||||
"color" => "blue",
|
||||
"regimen_items" => [
|
||||
{
|
||||
"time_offset" => 1555500000,
|
||||
"sequence_id" => sequence.id
|
||||
},
|
||||
{
|
||||
"time_offset" => 864300000,
|
||||
"sequence_id" => sequence.id
|
||||
},
|
||||
{
|
||||
"time_offset" => 950700000,
|
||||
"sequence_id" => sequence.id
|
||||
}
|
||||
]
|
||||
}
|
||||
{
|
||||
"time_offset" => 1555500000,
|
||||
"sequence_id" => sequence.id,
|
||||
},
|
||||
{
|
||||
"time_offset" => 864300000,
|
||||
"sequence_id" => sequence.id,
|
||||
},
|
||||
{
|
||||
"time_offset" => 950700000,
|
||||
"sequence_id" => sequence.id,
|
||||
},
|
||||
],
|
||||
}
|
||||
put :update, body: payload.to_json, params: { id: existing.id }
|
||||
expect(response.status).to eq(200)
|
||||
expect(existing.reload.regimen_items.count).to eq(payload["regimen_items"].length)
|
||||
|
@ -40,7 +39,7 @@ describe Api::RegimensController do
|
|||
|
||||
it "changes variable assignments in `body`" do
|
||||
sequence = FakeSequence.with_parameters
|
||||
user = FactoryBot.create(:user, device: sequence.device)
|
||||
user = FactoryBot.create(:user, device: sequence.device)
|
||||
sign_in user
|
||||
|
||||
var_declr = [
|
||||
|
@ -50,21 +49,21 @@ describe Api::RegimensController do
|
|||
label: "parent",
|
||||
data_value: {
|
||||
kind: "coordinate",
|
||||
args: { x: 0, y: 0, z: 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
args: { x: 0, y: 0, z: 0 },
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
existing = Regimens::Create.run!(device: user.device,
|
||||
name: "x",
|
||||
color: "red",
|
||||
existing = Regimens::Create.run!(device: user.device,
|
||||
name: "x",
|
||||
color: "red",
|
||||
regimen_items: [],
|
||||
body: var_declr)
|
||||
body: var_declr)
|
||||
payload = {
|
||||
id: existing.id,
|
||||
name: "something new",
|
||||
id: existing.id,
|
||||
name: "something new",
|
||||
color: "blue",
|
||||
body: [
|
||||
body: [
|
||||
{
|
||||
kind: "parameter_application",
|
||||
args: {
|
||||
|
@ -72,19 +71,20 @@ describe Api::RegimensController do
|
|||
data_value: {
|
||||
kind: "tool",
|
||||
args: {
|
||||
tool_id: FactoryBot.create(:tool, device: sequence.device).id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tool_id: FactoryBot.create(:tool, device: sequence.device).id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
regimen_items: [
|
||||
{
|
||||
time_offset: 950700000,
|
||||
sequence_id: sequence.id
|
||||
}
|
||||
]
|
||||
sequence_id: sequence.id,
|
||||
},
|
||||
],
|
||||
}
|
||||
old_timestamp = existing.created_at.as_json
|
||||
put :update,
|
||||
body: payload.to_json,
|
||||
format: :json,
|
||||
|
@ -92,22 +92,22 @@ describe Api::RegimensController do
|
|||
expect(response.status).to eq(200)
|
||||
path = [:body, 0, :args, :data_value, :kind]
|
||||
expect(json.dig(*path)).to eq(payload.dig(*path))
|
||||
expect(json.fetch(:updated_at)).to_not eq(old_timestamp)
|
||||
end
|
||||
|
||||
it "catches bad regimen_items" do
|
||||
sign_in user
|
||||
existing = Regimens::Create.run!(device: user.device, name: "x", color: "red", regimen_items: [])
|
||||
payload = {
|
||||
"id" => existing.id,
|
||||
"name" => "something new",
|
||||
"color" => "blue",
|
||||
"regimen_items" => [ { "time_offset" => 950700000, "sequence_id" => 0 } ]
|
||||
}
|
||||
"id" => existing.id,
|
||||
"name" => "something new",
|
||||
"color" => "blue",
|
||||
"regimen_items" => [{ "time_offset" => 950700000, "sequence_id" => 0 }],
|
||||
}
|
||||
put :update, body: payload.to_json, params: { id: existing.id }, format: :json
|
||||
expect(response.status).to eq(422)
|
||||
expect(json[:regimen_items]).to be
|
||||
expect(json[:regimen_items])
|
||||
.to include("Failed to instantiate nested RegimenItem.")
|
||||
expect(json[:regimen_items]).to include("Failed to instantiate nested RegimenItem.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,7 +15,7 @@ describe Api::RmqUtilsController do
|
|||
password: token }
|
||||
end
|
||||
|
||||
it "limits users to 10 connections per 10 minutes" do
|
||||
it "limits users to 20 connections per 5 minutes" do
|
||||
empty_mail_bag
|
||||
u = credentials.fetch(:username)
|
||||
p = credentials.fetch(:password)
|
||||
|
@ -24,7 +24,7 @@ describe Api::RmqUtilsController do
|
|||
.redis
|
||||
.set("mqtt_limiter:" + u.split("_").last, 0)
|
||||
|
||||
10.times do
|
||||
20.times do
|
||||
post :user_action, params: { username: u, password: p }
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.body).to include("allow")
|
||||
|
|
|
@ -220,4 +220,14 @@ describe CeleryScript::Corpus do
|
|||
expect(value.fetch("tags").first).to eq("great")
|
||||
expect(value.fetch("docs")).to eq("spectacular")
|
||||
end
|
||||
|
||||
it "sets a MAX_WAIT_MS limit for `wait` nodes" do
|
||||
bad = CeleryScript::AstNode.new({
|
||||
kind: "wait",
|
||||
args: { milliseconds: CeleryScriptSettingsBag::MAX_WAIT_MS + 10 },
|
||||
})
|
||||
check = CeleryScript::Checker.new(bad, corpus, device)
|
||||
expect(check.valid?).to be_falsey
|
||||
expect(check.error.message).to include("cannot exceed 3 minutes")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,12 +6,31 @@ describe Image do
|
|||
it "adds URL attachments", :slow do
|
||||
image = Image.create(device: device)
|
||||
expect(image.attachment_processed_at).to be_nil
|
||||
expect(image.attachment.exists?).to be_falsy
|
||||
expect(image.attachment.attached?).to be false
|
||||
|
||||
image.set_attachment_by_url(FAKE_ATTACHMENT_URL)
|
||||
image.save!
|
||||
|
||||
expect(image.attachment.exists?).to be_truthy
|
||||
expect(image.attachment.attached?).to be true
|
||||
expect(image.attachment_processed_at).to be_truthy
|
||||
end
|
||||
|
||||
it "generates legacy URLs for images generated via (deprecated) PaperClip" do
|
||||
now = Time.now
|
||||
i = Image.new
|
||||
i.id = 123
|
||||
i.attachment_file_name = "foo.jpg"
|
||||
i.attachment_updated_at = now
|
||||
url = i.legacy_url("x640")
|
||||
expect(url).to include("/images/attachments/000/000/123/x640/foo.jpg?")
|
||||
expect(url).to include(now.to_i.to_s)
|
||||
end
|
||||
|
||||
it "generates a URL when BUCKET is set" do
|
||||
const_reassign(Image, :BUCKET, "foo") do
|
||||
i = Image.new
|
||||
expect(i).to receive(:attachment).and_return(Struct.new(:key).new("bar"))
|
||||
url = i.regular_url
|
||||
expect(url).to eq("https://storage.googleapis.com/foo/bar")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Images::GeneratePolicy do
|
||||
it "has a policy object (Hash)" do
|
||||
policy = Images::GeneratePolicy.new.send(:policy)
|
||||
expiration = Time.parse(policy.fetch(:expiration))
|
||||
one_hour = (Time.now + 1.hour).utc
|
||||
time_diff = (one_hour - expiration).round
|
||||
expect(time_diff).to be >= 0
|
||||
expect(time_diff).to be <= 1
|
||||
|
||||
conditions = policy.fetch(:conditions).map(&:to_a).map(&:flatten)
|
||||
{
|
||||
0 => eq([:bucket, "YOU_MUST_CONFIG_GOOGLE_CLOUD_STORAGE"]),
|
||||
2 => eq([:acl, "public-read"]),
|
||||
3 => eq([:eq, "$Content-Type", "image/jpeg"]),
|
||||
4 => eq(["content-length-range", 1, 7340032]),
|
||||
}.map do |(index, meet_expectation)|
|
||||
expect(conditions[index]).to meet_expectation
|
||||
end
|
||||
end
|
||||
end
|
|
@ -117,8 +117,14 @@ end
|
|||
# Reassign constants without getting a bunch of warnings to STDOUT.
|
||||
# This is just for testing purposes, so NBD.
|
||||
def const_reassign(target, const, value)
|
||||
b4 = target.const_get(const)
|
||||
target.send(:remove_const, const)
|
||||
target.const_set(const, value)
|
||||
if block_given?
|
||||
yield
|
||||
target.send(:remove_const, const)
|
||||
target.const_set(const, b4)
|
||||
end
|
||||
end
|
||||
|
||||
class StubResp
|
||||
|
|
Loading…
Reference in New Issue