Merge branch 'main' into change-collection
* main: (25 commits) Add controller to retarget links Use more complete regexp dep: bundle update refactor: Extract a Card::Entropy concern Autolink emails and URLs at rendering time Remove the "mirror" storage service Update production storage to write to the Pure blob store Clean up the purestorage config, and add explanatory comments. Drop the action_text_markdowns table Mount mission control under `/admin/jobs` dep: add mission_control-jobs Flip the storage mirror to write to Pure as the primary Production mirrors Active Storage to `:purestorage` Make sure Sentry is configured for the beta env. Update lexical update lexical update lexical Create a distinct "beta" environment Revert "Patch for cards missing description" Add a kamal "ssh" alias ...
This commit is contained in:
+2
-3
@@ -1,6 +1,5 @@
|
||||
SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Beta/SECRET_KEY_BASE Beta/OPEN_AI_API_KEY)
|
||||
SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Beta/RAILS_MASTER_KEY)
|
||||
|
||||
GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
|
||||
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
|
||||
SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE $SECRETS)
|
||||
OPEN_AI_API_KEY=$(kamal secrets extract OPEN_AI_API_KEY $SECRETS)
|
||||
RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Production/SECRET_KEY_BASE)
|
||||
SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Beta/RAILS_MASTER_KEY)
|
||||
|
||||
GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
|
||||
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
|
||||
SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE $SECRETS)
|
||||
RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Production/SECRET_KEY_BASE Production/OPEN_AI_API_KEY)
|
||||
SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Production/RAILS_MASTER_KEY)
|
||||
|
||||
GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
|
||||
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
|
||||
SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE $SECRETS)
|
||||
OPEN_AI_API_KEY=$(kamal secrets extract OPEN_AI_API_KEY $SECRETS)
|
||||
RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
|
||||
|
||||
@@ -2,7 +2,6 @@ source "https://rubygems.org"
|
||||
git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" }
|
||||
|
||||
gem "rails", github: "rails/rails", branch: "main"
|
||||
gem "active_record-tenanted", bc: "active_record-tenanted"
|
||||
|
||||
# Assets & front end
|
||||
gem "importmap-rails"
|
||||
@@ -11,6 +10,7 @@ gem "stimulus-rails"
|
||||
gem "turbo-rails"
|
||||
|
||||
# Deployment and drivers
|
||||
gem "active_record-tenanted", bc: "active_record-tenanted"
|
||||
gem "bootsnap", require: false
|
||||
gem "kamal", require: false
|
||||
gem "puma", ">= 5.0"
|
||||
@@ -30,8 +30,10 @@ gem "jbuilder"
|
||||
gem "actiontext-lexical", bc: "actiontext-lexical"
|
||||
gem "image_processing", "~> 1.14"
|
||||
gem "platform_agent"
|
||||
gem "aws-sdk-s3", require: false
|
||||
|
||||
# Telemetry and logging
|
||||
# Telemetry, logging, and operations
|
||||
gem "mission_control-jobs"
|
||||
gem "sentry-ruby"
|
||||
gem "sentry-rails"
|
||||
gem "rails_structured_logging", bc: "rails-structured-logging"
|
||||
|
||||
+56
-24
@@ -1,13 +1,13 @@
|
||||
GIT
|
||||
remote: https://github.com/basecamp/actiontext-lexical
|
||||
revision: 9328297709d532fcb4cb5e47cfbddefda34d7c32
|
||||
revision: 7f327ddd60f4902ac60bb426495e85b31b6f0ad2
|
||||
specs:
|
||||
actiontext-lexical (0.1.0)
|
||||
rails (>= 8.0.2)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/active_record-tenanted
|
||||
revision: 523947a0f0f49f66a8a62eb93e9ba1a345b3076b
|
||||
revision: f6fca189655ae6609d10ecad7cceed61592320c4
|
||||
specs:
|
||||
active_record-tenanted (0.1.0)
|
||||
activerecord (>= 8.1.alpha)
|
||||
@@ -17,28 +17,29 @@ GIT
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/rails-structured-logging
|
||||
revision: fc810cc48ac54a07722013149e104ceb8ec9a7e3
|
||||
revision: 76960cb5c15fc2b6b5f7542e05d7dcc031cef9e6
|
||||
specs:
|
||||
rails_structured_logging (0.2.0)
|
||||
rails_structured_logging (0.2.1)
|
||||
json
|
||||
rails (>= 6.0.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/crmne/ruby_llm.git
|
||||
revision: 8412d51e869f1ee8e5229831a9a0bb5e9b8f3847
|
||||
revision: 74305703dcdcdcf8b2adefd8236184bcffc83910
|
||||
specs:
|
||||
ruby_llm (1.2.0)
|
||||
ruby_llm (1.3.0rc1)
|
||||
base64
|
||||
event_stream_parser (~> 1)
|
||||
faraday (~> 2)
|
||||
faraday-multipart (~> 1)
|
||||
faraday-net_http (~> 3)
|
||||
faraday-retry (~> 2)
|
||||
faraday (>= 1.10.0)
|
||||
faraday-multipart (>= 1)
|
||||
faraday-net_http (>= 1)
|
||||
faraday-retry (>= 1)
|
||||
marcel (~> 1.0)
|
||||
zeitwerk (~> 2)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/rails/rails.git
|
||||
revision: c1d7d56e831acf0aff5f3e27a6b384f543f358d0
|
||||
revision: 0ac3fba6ee5f7d1680024a4925eee6069095487b
|
||||
branch: main
|
||||
specs:
|
||||
actioncable (8.1.0.alpha)
|
||||
@@ -144,11 +145,29 @@ GEM
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
ast (2.4.3)
|
||||
base64 (0.2.0)
|
||||
aws-eventstream (1.3.2)
|
||||
aws-partitions (1.1109.0)
|
||||
aws-sdk-core (3.224.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.101.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.188.0)
|
||||
aws-sdk-core (~> 3, >= 3.224.1)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.11.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
bcrypt_pbkdf (1.1.1)
|
||||
benchmark (0.4.0)
|
||||
bigdecimal (3.1.9)
|
||||
benchmark (0.4.1)
|
||||
bigdecimal (3.2.1)
|
||||
bootsnap (1.18.6)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (7.0.2)
|
||||
@@ -207,7 +226,7 @@ GEM
|
||||
addressable (>= 2.5.0)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
hashdiff (1.1.2)
|
||||
hashdiff (1.2.0)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.14.0)
|
||||
@@ -225,7 +244,8 @@ GEM
|
||||
jbuilder (2.13.0)
|
||||
actionview (>= 5.0.0)
|
||||
activesupport (>= 5.0.0)
|
||||
json (2.11.3)
|
||||
jmespath (1.6.2)
|
||||
json (2.12.2)
|
||||
kamal (2.6.1)
|
||||
activesupport (>= 7.0)
|
||||
base64 (~> 0.2)
|
||||
@@ -237,7 +257,7 @@ GEM
|
||||
sshkit (>= 1.23.0, < 2.0)
|
||||
thor (~> 1.3)
|
||||
zeitwerk (>= 2.6.18, < 3.0)
|
||||
language_server-protocol (3.17.0.4)
|
||||
language_server-protocol (3.17.0.5)
|
||||
lint_roller (1.1.0)
|
||||
logger (1.7.0)
|
||||
loofah (2.24.1)
|
||||
@@ -256,6 +276,16 @@ GEM
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.9)
|
||||
minitest (5.25.5)
|
||||
mission_control-jobs (1.0.2)
|
||||
actioncable (>= 7.1)
|
||||
actionpack (>= 7.1)
|
||||
activejob (>= 7.1)
|
||||
activerecord (>= 7.1)
|
||||
importmap-rails (>= 1.2.1)
|
||||
irb (~> 1.13)
|
||||
railties (>= 7.1)
|
||||
stimulus-rails
|
||||
turbo-rails
|
||||
msgpack (1.8.0)
|
||||
multipart-post (2.4.1)
|
||||
net-http (0.6.0)
|
||||
@@ -304,7 +334,7 @@ GEM
|
||||
psych (5.2.6)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.1)
|
||||
public_suffix (6.0.2)
|
||||
puma (6.6.0)
|
||||
nio4r (~> 2.0)
|
||||
raabro (1.4.0)
|
||||
@@ -327,7 +357,7 @@ GEM
|
||||
loofah (~> 2.21)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
rainbow (3.1.1)
|
||||
rake (13.2.1)
|
||||
rake (13.3.0)
|
||||
rb_sys (0.9.106)
|
||||
rdoc (6.14.0)
|
||||
erb
|
||||
@@ -342,7 +372,7 @@ GEM
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 2.0)
|
||||
rqrcode_core (2.0.0)
|
||||
rubocop (1.75.4)
|
||||
rubocop (1.75.8)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
@@ -360,12 +390,12 @@ GEM
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop-rails (2.31.0)
|
||||
rubocop-rails (2.32.0)
|
||||
activesupport (>= 4.2.0)
|
||||
lint_roller (~> 1.1)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
rubocop-rails-omakase (1.1.0)
|
||||
rubocop (>= 1.72)
|
||||
rubocop-performance (>= 1.24)
|
||||
@@ -376,7 +406,7 @@ GEM
|
||||
logger
|
||||
rubyzip (2.4.1)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.32.0)
|
||||
selenium-webdriver (4.33.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
@@ -452,7 +482,7 @@ GEM
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
websocket (1.2.11)
|
||||
websocket-driver (0.7.7)
|
||||
websocket-driver (0.8.0)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
@@ -470,6 +500,7 @@ PLATFORMS
|
||||
DEPENDENCIES
|
||||
actiontext-lexical!
|
||||
active_record-tenanted!
|
||||
aws-sdk-s3
|
||||
bcrypt (~> 3.1.7)
|
||||
bootsnap
|
||||
brakeman
|
||||
@@ -481,6 +512,7 @@ DEPENDENCIES
|
||||
importmap-rails
|
||||
jbuilder
|
||||
kamal
|
||||
mission_control-jobs
|
||||
platform_agent
|
||||
propshaft
|
||||
puma (>= 5.0)
|
||||
|
||||
@@ -23,12 +23,10 @@
|
||||
--border: 1px solid var(--color-ink-lighter);
|
||||
|
||||
/* Shadows */
|
||||
--shadow: 0 0 0 1px oklch(var(--lch-black) / 0.02),
|
||||
0 .2em 1.6em -0.8em oklch(var(--lch-black) / 0.2),
|
||||
0 .4em 2.4em -1em oklch(var(--lch-black) / 0.3),
|
||||
0 .4em .8em -1.2em oklch(var(--lch-black) / 0.4),
|
||||
0 .8em 1.2em -1.6em oklch(var(--lch-black) / 0.5),
|
||||
0 1.2em 1.6em -2em oklch(var(--lch-black) / 0.6);
|
||||
--shadow: 0 0 0 1px oklch(var(--lch-black) / 5%),
|
||||
0 0.2em 0.2em oklch(var(--lch-black) / 5%),
|
||||
0 0.4em 0.4em oklch(var(--lch-black) / 5%),
|
||||
0 0.8em 0.8em oklch(var(--lch-black) / 5%);
|
||||
|
||||
/* Components */
|
||||
--btn-size: 2.65em;
|
||||
@@ -186,7 +184,7 @@
|
||||
--color-code-token__punctuation: oklch(var(--lch-ink-dark));
|
||||
--color-code-token__selector: oklch(var(--lch-green-dark));
|
||||
--color-code-token__variable: oklch(var(--lch-red-dark));
|
||||
|
||||
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
--lch-canvas: 20% 0.0195 232.58;
|
||||
|
||||
@@ -52,12 +52,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bubble--closing {
|
||||
.bubble--considering {
|
||||
inset-inline-start: calc(-1 * var(--bubble-gap));
|
||||
translate: -100% -50%;
|
||||
}
|
||||
|
||||
.bubble--considering {
|
||||
.bubble--doing {
|
||||
inset-inline-end: calc(-1 * var(--bubble-gap));
|
||||
translate: 100% -50%;
|
||||
}
|
||||
|
||||
@@ -101,7 +101,6 @@
|
||||
--text-small: 1.1em;
|
||||
|
||||
background-color: var(--color-canvas);
|
||||
box-shadow: 0 0 0 1px var(--color-ink-lighter);
|
||||
line-height: 1.2;
|
||||
|
||||
.avatar {
|
||||
@@ -122,6 +121,22 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card__collection {
|
||||
background-color: var(--color-canvas);
|
||||
color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink));
|
||||
padding: 0 0 0 var(--inline-space);
|
||||
}
|
||||
|
||||
.card__collection-name {
|
||||
border-inline-start: 1px solid color-mix(in srgb, var(--color-ink) 33%, var(--color-canvas));
|
||||
color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink));
|
||||
}
|
||||
|
||||
.card__header {
|
||||
color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink));
|
||||
margin: 0 0 calc(-0.5 * var(--card-padding-inline)) 0;
|
||||
}
|
||||
|
||||
.card__title {
|
||||
margin-block-start: 0.1em;
|
||||
min-block-size: 0;
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
.avatar {
|
||||
--avatar-size: 2.75em;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
box-shadow: 0 0 0 1px var(--color-ink-lighter);
|
||||
}
|
||||
}
|
||||
|
||||
/* Children
|
||||
@@ -69,11 +73,6 @@
|
||||
margin-block-start: calc(-1 * var(--card-padding-block));
|
||||
margin-inline-start: calc(-1 * var(--card-padding-inline));
|
||||
min-inline-size: 0;
|
||||
|
||||
.cards--considering & {
|
||||
color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink));
|
||||
margin: 0 0 calc(-0.5 * var(--card-padding-inline)) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card__collection {
|
||||
@@ -211,6 +210,7 @@
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-link) 20%, var(--color-canvas));
|
||||
color: var(--btn-color);
|
||||
font-weight: 700;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.card__move-button {
|
||||
|
||||
@@ -11,7 +11,7 @@ class UploadsController < ApplicationController
|
||||
|
||||
def show
|
||||
expires_in 5.minutes, public: true
|
||||
redirect_to @attachment.url
|
||||
redirect_to @attachment.url, allow_other_host: true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
module HtmlHelper
|
||||
def format_html(html)
|
||||
fragment = Nokogiri::HTML.fragment(html)
|
||||
|
||||
auto_link(fragment)
|
||||
|
||||
fragment.to_html.html_safe
|
||||
end
|
||||
|
||||
private
|
||||
EXCLUDED_ELEMENTS = %w[ a figcaption pre code ]
|
||||
EMAIL_REGEXP = /\b[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\b/
|
||||
URL_REGEXP = URI::DEFAULT_PARSER.make_regexp(%w[http https])
|
||||
|
||||
def auto_link(fragment)
|
||||
fragment.traverse do |node|
|
||||
next unless auto_linkable_node?(node)
|
||||
|
||||
content = node.text
|
||||
linked_content = content.dup
|
||||
|
||||
auto_link_urls(linked_content)
|
||||
auto_link_emails(linked_content)
|
||||
|
||||
if linked_content != content
|
||||
node.replace(Nokogiri::HTML.fragment(linked_content))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def auto_linkable_node?(node)
|
||||
node.text? && node.ancestors.none? { |ancestor| EXCLUDED_ELEMENTS.include?(ancestor.name) }
|
||||
end
|
||||
|
||||
def auto_link_urls(linked_content)
|
||||
linked_content.gsub!(URL_REGEXP) do |match|
|
||||
%(<a href="#{match}" rel="noreferrer">#{match}</a>)
|
||||
end
|
||||
end
|
||||
|
||||
def auto_link_emails(text)
|
||||
text.gsub!(EMAIL_REGEXP) do |match|
|
||||
%(<a href="mailto:#{match}">#{match}</a>)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
this.element.querySelectorAll("a").forEach(this.#retargetLink.bind(this))
|
||||
}
|
||||
|
||||
#retargetLink(link) {
|
||||
link.target = this.#targetsSameDomain(link) ? "_top" : "_blank"
|
||||
}
|
||||
|
||||
#targetsSameDomain(link) {
|
||||
return link.href.startsWith(window.location.origin)
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
class Card < ApplicationRecord
|
||||
include Assignable, Colored, Engageable, Eventable, Golden,
|
||||
Mentions, Pinnable, Closeable, Readable, Searchable, Staged,
|
||||
Statuses, Taggable, Watchable
|
||||
include Assignable, Colored, Engageable, Entropy, Eventable,
|
||||
Golden, Mentions, Pinnable, Closeable, Readable, Searchable,
|
||||
Staged, Statuses, Taggable, Watchable
|
||||
|
||||
belongs_to :collection, touch: true
|
||||
belongs_to :creator, class_name: "User", default: -> { Current.user }
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
module Card::Closeable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
AUTO_CLOSE_REMINDER_BEFORE = 7.days
|
||||
|
||||
included do
|
||||
has_one :closure, dependent: :destroy
|
||||
|
||||
@@ -10,30 +8,6 @@ module Card::Closeable
|
||||
scope :open, -> { where.missing(:closure) }
|
||||
|
||||
scope :recently_closed_first, -> { closed.order("closures.created_at": :desc) }
|
||||
scope :in_auto_closing_collection, -> { joins(:collection).merge(Collection.auto_closing) }
|
||||
scope :due_to_be_closed, -> { considering.in_auto_closing_collection.where("last_active_at <= DATETIME('now', '-' || auto_close_period || ' seconds')") }
|
||||
|
||||
delegate :auto_closing?, :auto_close_period, to: :collection
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def auto_close_all_due
|
||||
due_to_be_closed.find_each do |card|
|
||||
card.close(user: User.system, reason: "Closed")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def auto_close_at
|
||||
last_active_at + auto_close_period if auto_closing? && last_active_at
|
||||
end
|
||||
|
||||
def days_until_close
|
||||
(auto_close_at.to_date - Date.current).to_i if auto_close_at
|
||||
end
|
||||
|
||||
def closing_soon?
|
||||
considering? && auto_closing? && Time.current >= auto_close_at - AUTO_CLOSE_REMINDER_BEFORE
|
||||
end
|
||||
|
||||
def closed?
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
module Card::Engageable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
STAGNATED_AFTER = 30.days
|
||||
AUTO_RECONSIDER_REMINDER_BEFORE = 7.days
|
||||
|
||||
included do
|
||||
has_one :engagement, dependent: :destroy, class_name: "Card::Engagement"
|
||||
|
||||
scope :considering, -> { published_or_drafted_by(Current.user).open.where.missing(:engagement) }
|
||||
scope :doing, -> { published.open.joins(:engagement) }
|
||||
scope :stagnated, -> { doing.where(last_active_at: ..STAGNATED_AFTER.ago) }
|
||||
|
||||
scope :by_engagement_status, ->(status) do
|
||||
case status.to_s
|
||||
@@ -19,16 +15,6 @@ module Card::Engageable
|
||||
end
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def auto_reconsider_all_stagnated
|
||||
stagnated.find_each(&:reconsider)
|
||||
end
|
||||
end
|
||||
|
||||
def auto_reconsider_at
|
||||
last_active_at + STAGNATED_AFTER if last_active_at
|
||||
end
|
||||
|
||||
def doing?
|
||||
open? && published? && engagement.present?
|
||||
end
|
||||
@@ -37,6 +23,14 @@ module Card::Engageable
|
||||
open? && published? && engagement.blank?
|
||||
end
|
||||
|
||||
def engagement_status
|
||||
if doing?
|
||||
"doing"
|
||||
elsif considering?
|
||||
"considering"
|
||||
end
|
||||
end
|
||||
|
||||
def engage
|
||||
unless doing?
|
||||
transaction do
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
module Card::Entropy
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
AUTO_RECONSIDER_PERIOD = 30.days
|
||||
ENTROPY_REMINDER_BEFORE = 7.days
|
||||
|
||||
included do
|
||||
scope :in_auto_closing_collection, -> { joins(:collection).merge(Collection.auto_closing) }
|
||||
|
||||
scope :stagnated, -> { doing.where(last_active_at: ..AUTO_RECONSIDER_PERIOD.ago) }
|
||||
scope :due_to_be_closed, -> { considering.in_auto_closing_collection.where("last_active_at <= DATETIME('now', '-' || auto_close_period || ' seconds')") }
|
||||
|
||||
delegate :auto_closing?, :auto_close_period, to: :collection
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def auto_close_all_due
|
||||
due_to_be_closed.find_each do |card|
|
||||
card.close(user: User.system, reason: "Closed")
|
||||
end
|
||||
end
|
||||
|
||||
def auto_reconsider_all_stagnated
|
||||
stagnated.find_each(&:reconsider)
|
||||
end
|
||||
end
|
||||
|
||||
def subject_to_entropy?
|
||||
doing? || (auto_closing? && considering?)
|
||||
end
|
||||
|
||||
def auto_close_at
|
||||
last_active_at + auto_close_period if auto_closing? && last_active_at
|
||||
end
|
||||
|
||||
def days_until_close
|
||||
(auto_close_at.to_date - Date.current).to_i if auto_close_at
|
||||
end
|
||||
|
||||
def auto_reconsider_at
|
||||
last_active_at + AUTO_RECONSIDER_PERIOD if last_active_at
|
||||
end
|
||||
|
||||
def days_until_reconsider
|
||||
(auto_reconsider_at.to_date - Date.current).to_i if auto_reconsider_at
|
||||
end
|
||||
end
|
||||
@@ -28,7 +28,7 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="comment__body rich-text-content txt-align-start" data-controller="syntax-highlight">
|
||||
<div class="comment__body rich-text-content txt-align-start" data-controller="syntax-highlight retarget-links">
|
||||
<%= comment.body %>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<h1 class="card__title flex align-start gap-half">
|
||||
<%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link" %>
|
||||
</h1>
|
||||
<div class="card__description rich-text-content margin-block-half rich-text-content" data-controller="syntax-highlight">
|
||||
<div class="card__description rich-text-content margin-block-half rich-text-content" data-controller="syntax-highlight retarget-links">
|
||||
<%= card.description %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,12 +29,8 @@
|
||||
|
||||
<%= render "cards/display/common/background", card: card %>
|
||||
|
||||
<% if card.auto_closing? %>
|
||||
<% if card.considering? %>
|
||||
<%= render "cards/display/preview/bubble", card: card, style: "closing" %>
|
||||
<% elsif card.doing? %>
|
||||
<%= render "cards/display/preview/bubble", card: card, style: "considering" %>
|
||||
<% end %>
|
||||
<% if card.subject_to_entropy? %>
|
||||
<%= render "cards/display/preview/bubble", card: card %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div hidden class="bubble bubble--<%= style %>"
|
||||
<div hidden class="bubble bubble--<%= card.engagement_status %>"
|
||||
data-controller="bubble" data-action="turbo:morph-element->bubble#update"
|
||||
data-bubble-reminder-before-value="<%= Card::AUTO_CLOSE_REMINDER_BEFORE.in_days.to_i %>"
|
||||
data-bubble-reminder-before-value="<%= Card::ENTROPY_REMINDER_BEFORE.in_days.to_i %>"
|
||||
data-bubble-closes-at-value="<%= card.auto_close_at.iso8601 %>"
|
||||
data-bubble-entropy-action-value="<%= card_entropy_action(card) %>">
|
||||
<svg viewBox="0 0 200 100">
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<div class="action-text-content">
|
||||
<%= yield -%>
|
||||
<%= format_html yield -%>
|
||||
</div>
|
||||
|
||||
+4
-1
@@ -12,5 +12,8 @@ development:
|
||||
test:
|
||||
adapter: test
|
||||
|
||||
production:
|
||||
production: &production
|
||||
<<: *cable
|
||||
|
||||
beta:
|
||||
<<: *production
|
||||
|
||||
+4
-1
@@ -17,6 +17,9 @@ development:
|
||||
test:
|
||||
<<: *default_options
|
||||
|
||||
production:
|
||||
production: &production
|
||||
<<: *default_connection
|
||||
<<: *default_options
|
||||
|
||||
beta:
|
||||
<<: *production
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
5ZoHBjwBHPiVvPcnI4khzagL3ZR54mfvv5VdRLjDicl0s/6hGKd88rwmSb/8WOITFSDQZntfFcJ4/xlH8ZLr2az51/7CJr9J1QSJUCBexDtTpqa4g7/+s2TW1YDrLg3LybtVzuGcTVL2YE4llcW+5RLI6JuioAQQ0Sx9hGqd6n5/safGW/OXrBPS198b3YmPeAkSm4uA4L7yOmQYaeweJp1p47WSrgh5bnesttx+ey0L7nMSwWKj/pBFEP5N0ELQXm4G38RYf3SUsoAQkUl+TIsMHvFz3/toeS0+tuEvKeDQYlvXCyVTK4XHsvbu8pkOvZGBpDgkvW3aMOBTf/Bsrv0ylU1gF3pGs2Oc90KOmKxBmHbuOOS5VpNTYhZ6IPEaMz9t5rDYlYVupPQ6TTNrn4jAvq6f--InMaBENysICzAQUu--mMpwA+kXMLs3j/zWoA0NMA==
|
||||
@@ -0,0 +1 @@
|
||||
I2oqW56RTDrRxQ5HUGXGUbSiVoeICiF6tjSo1oxmGuZKE6wu99S+RvQ6Mf0enCQdbHG0osoCpUvu7iuOMEA3X6vO7ku1YFbCDKugu6tY913bzRl166SWZdSlEwtcr6wtmUkXLoWfDzOt98lJ03nU8YbeDC2AzuBqcYWS2qjqpFrrDK8WQI7IukM4iFEDjesW2Noekyy6z+b5+DxB2lkCMFgim4ExW/ndFmrvDnt+QeR+rB9XW9BF6CEg5pPsBQ9EwtICsacCWdvFFOZ8EaNoOuF+hKIHoKP60UCfQj5EUir/r5pGE0Z83z91+UpdpVCuGvh62uSZoZOjdGAplg/DWWKpGcj5JOA7V2yyGEMlz7NueBInG8t/eEyeX/98fAy4TNUxxvncNB+xsmgPJIqlek7vXtYe06AMwrRvac4QSSpUuwWuKUV8JF4gYT5Gp92rzqmmwsTmjvEMiPlnnVYH83GCtd2f1Se7EkDVTvgLA3idnjLvFCelAU4Vdo9/pLqG+0xorSxQ4D/UXqykxtTSjz6pC2P8J1g6T91TQhcH4tDFuZICOYLyFV8F5OD3jPaP+ml01oME42FIKmyhvcNhMLqluUvFqqtOxgWp54jCnde6MBHJ9W7gejwfSmjyD+pr/5NXgaGaTq9h6NMdMkr0Bis6Cc/EgHZKBpJx6ARH1ulcp9uZ9tBjNCePBNxATrJLKwiXh0ZTLDfDTkgdznkj8u+FAKclxjIZ63td3hGe--5kL30BIpNtC7wcsf--YaV9PXL74Wl2ePaAdT5Crg==
|
||||
@@ -1 +1 @@
|
||||
TebKB7S5umO6AQRNOZWHMihQAlRay0wPXY637HdvynV9d34ALokzb765aykzZYsUmeHdvFnSHdweRQG60NR7V3sXNL09h38pWo0JhcGkwmtEAe908mZRjC0JjXamOx0AgohRdfmjwlI9MROfjTwrKq0CtdIien7hNZDqUxEbWOjSX2RiPL65Ro++iAnvA9ezUoHW47TPPnXDiFq4I37KPVhl1Ze1bPP02VebysUB/K+nsy4GsRdl3jxLXQ==--7ie1Lx8/EJvL2Jbc--WJAy/xEQeki9ceKjC5Si1A==
|
||||
e6HcyVNJvgdJGTCcvW8FDHQ9pHYmTgNW2/71Espdwxd5LQX/Xb9+VkeqpEAL8ceFJo6ro49mMs85G2YngKbbXEh//8+XSS0inMR/kh9wTV3f6YBWnLVBbhrOnYgPcQ8vedsgxLG2Cs4y7ipfeT/dp9bb5m4IxV0OPiUjVFFJuAuGkMJt3pE7D6alqL1ACRxazV9UKV5WKEBfaQYeKpJ++5f86Nls1EwKo5tB1lJed9P5n2yH7meX/QyxjgQd38CIaPwp+F92y5DLKJD5MZRMJGgWf6MT6096LhjEC3vvtS3wwT/eDVcVKTplRkEszj7BZEfOs9Ondb9EVtbXmeoHYSgpaOq9/nEwtho5Uw==--KlHZy1apg3u1XxFs--N2ehRobLHNz+Sepc8p4EIA==
|
||||
@@ -0,0 +1 @@
|
||||
Qkkt3krcGlOh76//7TmY5rXz31mhL3NNNiO6TCcRhx4lT6uZ8sLjW7X0yp2wZ30y2cJBeIsNMeOhL47JTpu0Z4d0FPtwgH4bzzXAiab0/YrfMjxyBt7/K1DzW/bePUvakJggIvVGGRA1NB1tPkwziQ4dgA34E/UUPAxUofFaoCIFVABmqt4DFg0DKYB8tImDDjZ5xUfSNxrKj3pH5mOIU9jqSK6LtBELFZcNxrJOI0UEgayRNwYAs44ujdM2/2zrAWlUCFZ3NnMU4bBJW02/d6LaxHtB809u8mx3bChEzWMtZB9CvpbRH7u6XfZJR1YGFztcNguzRbNX5x9pKZImAuAhVFKR7Fbi12Hg3w7U3A/YovwmyhHK6ksfOzYlYoDUTBnGYVwiqR16naaHCMekomK+uo0qtOn83RQkLuuqFJshPXn/lW8Rggt4dq/0THEl7Qj+dumadlWDVEgxhipI031RSBF1toNWw/zwaC31Ord1YXD8ZC09sPoj8Zd4hD2o2a0IUuIMXA8w5/2+AmdU8wF66opdKGdsZDRjM2Bul8aAbJ7/iV7xTuvlE4LmzXGArMvivIZ0GwrN3YAsDta2Y3ufibq7WZzSCOrpbPKJ9CEGg4a2AN8UGaGZ5ZEAYg8lhDgsZMjHRkGNuxk8QkDiVFDyNoI7H9ZQqbw+L1AJRTfDIcNB/9h5n+//Nk4ylLF1lnI7y/IQA/AuSQ+UN+okt3RhLugwGvWrs49MExJStLHpGveSoQ==--MQ2JycR3X6LIiKSY--45C7bleKX9FMGv4fc+G1yw==
|
||||
+4
-1
@@ -37,7 +37,7 @@ test:
|
||||
database: storage/tenants/<%= Rails.env %>/%{tenant}/db/main.sqlite3
|
||||
tenanted: true
|
||||
|
||||
production:
|
||||
production: &production
|
||||
primary:
|
||||
<<: *default
|
||||
database: storage/tenants/<%= Rails.env %>/%{tenant}/db/main.sqlite3
|
||||
@@ -62,3 +62,6 @@ production:
|
||||
<<: *default
|
||||
database: storage/production_queue.sqlite3
|
||||
migrations_paths: db/queue_migrate
|
||||
|
||||
beta:
|
||||
<<: *production
|
||||
|
||||
@@ -10,6 +10,9 @@ proxy:
|
||||
hosts:
|
||||
- fizzy.37signals.works # the single tenant is named "fizzy"
|
||||
|
||||
env:
|
||||
clear:
|
||||
RAILS_ENV: beta
|
||||
|
||||
x-beamer-accessory: &beamer-accessory
|
||||
image: basecamp/beamer
|
||||
@@ -22,7 +25,6 @@ x-beamer-accessory: &beamer-accessory
|
||||
volumes:
|
||||
- fizzy:/storage
|
||||
|
||||
|
||||
accessories:
|
||||
beamer-primary:
|
||||
<<: *beamer-accessory
|
||||
|
||||
@@ -16,6 +16,9 @@ proxy:
|
||||
ssh:
|
||||
user: app
|
||||
|
||||
env:
|
||||
clear:
|
||||
RAILS_ENV: production
|
||||
|
||||
x-beamer-accessory: &beamer-accessory
|
||||
image: basecamp/beamer
|
||||
|
||||
+2
-2
@@ -23,8 +23,8 @@ builder:
|
||||
|
||||
env:
|
||||
secret:
|
||||
- SECRET_KEY_BASE
|
||||
- OPEN_AI_API_KEY
|
||||
- RAILS_MASTER_KEY
|
||||
|
||||
aliases:
|
||||
console: app exec -i --reuse "bin/rails console"
|
||||
ssh: app exec -i --reuse /bin/bash
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
require_relative "production"
|
||||
|
||||
Rails.application.configure do
|
||||
config.action_mailer.default_url_options = { host: "%{tenant}.37signals.works" }
|
||||
|
||||
# Let's keep beta on local disk. See https://github.com/basecamp/fizzy/pull/557 for context.
|
||||
config.active_storage.service = :local
|
||||
end
|
||||
@@ -31,7 +31,7 @@ Rails.application.configure do
|
||||
# config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
|
||||
|
||||
# Store uploaded files on the local file system (see config/storage.yml for options).
|
||||
config.active_storage.service = :local
|
||||
config.active_storage.service = :purestorage
|
||||
|
||||
# Mount Action Cable outside main process or domain.
|
||||
# config.action_cable.mount_path = nil
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
if Rails.env.production? && ENV["SKIP_TELEMETRY"].blank?
|
||||
if (Rails.env.production? || Rails.env.beta?) && ENV["SKIP_TELEMETRY"].blank?
|
||||
Sentry.init do |config|
|
||||
config.dsn = "https://ca338fb1fe6f677d6aeec2336a86f0ee@o33603.ingest.us.sentry.io/4508093839179776"
|
||||
config.breadcrumbs_logger = %i[ active_support_logger http_logger ]
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
Rails.application.config.after_initialize do
|
||||
%w[ db files ].each do |dir|
|
||||
Rails.root.join("storage", dir).mkpath
|
||||
end
|
||||
end
|
||||
+4
-1
@@ -14,5 +14,8 @@ development:
|
||||
test:
|
||||
<<: *default
|
||||
|
||||
production:
|
||||
production: &production
|
||||
<<: *default
|
||||
|
||||
beta:
|
||||
<<: *production
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
production:
|
||||
production: &production
|
||||
auto_close_all_due:
|
||||
class: Card::AutoCloseAllDueJob
|
||||
schedule: every hour
|
||||
@@ -14,3 +14,6 @@ production:
|
||||
delete_unused_tags:
|
||||
class: DeleteUnusedTagsJob
|
||||
schedule: every day at 04:00
|
||||
|
||||
beta:
|
||||
<<: *production
|
||||
|
||||
@@ -123,4 +123,8 @@ Rails.application.routes.draw do
|
||||
|
||||
|
||||
root "events#index"
|
||||
|
||||
namespace :admin do
|
||||
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||
end
|
||||
end
|
||||
|
||||
+13
-28
@@ -1,36 +1,21 @@
|
||||
test:
|
||||
service: Disk
|
||||
root: <%= Rails.root.join("tmp/storage/%{tenant}") %>
|
||||
tenanted: true
|
||||
|
||||
local:
|
||||
service: Disk
|
||||
root: <%= Rails.root.join("storage", "tenants", Rails.env, "%{tenant}", "files") %>
|
||||
tenanted: true
|
||||
|
||||
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
|
||||
# amazon:
|
||||
# service: S3
|
||||
# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
|
||||
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
|
||||
# region: us-east-1
|
||||
# collection: your_own_collection-<%= Rails.env %>
|
||||
|
||||
# Remember not to checkin your GCS keyfile to a repository
|
||||
# google:
|
||||
# service: GCS
|
||||
# project: your_project
|
||||
# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
|
||||
# collection: your_own_collection-<%= Rails.env %>
|
||||
|
||||
# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
|
||||
# microsoft:
|
||||
# service: AzureStorage
|
||||
# storage_account_name: your_account_name
|
||||
# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
|
||||
# container: your_container_name-<%= Rails.env %>
|
||||
|
||||
# mirror:
|
||||
# service: Mirror
|
||||
# primary: local
|
||||
# mirrors: [ amazon, google, microsoft ]
|
||||
# We have "development", "staging", and "production" buckets configured. Note that we don't have a
|
||||
# "beta" bucket. (As of 2025-06-01.)
|
||||
purestorage:
|
||||
service: S3
|
||||
bucket: fizzy-<%= Rails.env %>-activestorage
|
||||
endpoint: "https://storage.basecamp.com"
|
||||
ssl_verify_peer: false # FIXME: using self-signed cert internally
|
||||
force_path_style: true
|
||||
request_checksum_calculation: when_required # default is when_supported with CRC64NVME checksum which FlashBlade doesn't support
|
||||
response_checksum_validation: when_required # default is when_supported with CRC64NVME checksum which FlashBlade doesn't support
|
||||
region: us-east-1 # default region required for signer
|
||||
access_key_id: <%= Rails.application.credentials.dig(:active_storage, :purestorage_service, :access_key_id) %>
|
||||
secret_access_key: <%= Rails.application.credentials.dig(:active_storage, :purestorage_service, :secret_access_key) %>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class DropActionTextMarkdowns < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
drop_table :action_text_markdowns
|
||||
end
|
||||
end
|
||||
Generated
+1
-12
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 2025_05_23_084734) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2025_06_01_161653) do
|
||||
create_table "accesses", force: :cascade do |t|
|
||||
t.integer "collection_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
@@ -29,16 +29,6 @@ ActiveRecord::Schema[8.1].define(version: 2025_05_23_084734) do
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "action_text_markdowns", force: :cascade do |t|
|
||||
t.text "content", default: "", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.string "name", null: false
|
||||
t.integer "record_id", null: false
|
||||
t.string "record_type", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["record_type", "record_id"], name: "index_action_text_markdowns_on_record"
|
||||
end
|
||||
|
||||
create_table "action_text_rich_texts", force: :cascade do |t|
|
||||
t.text "body"
|
||||
t.datetime "created_at", null: false
|
||||
@@ -308,7 +298,6 @@ ActiveRecord::Schema[8.1].define(version: 2025_05_23_084734) do
|
||||
t.datetime "created_at", null: false
|
||||
t.string "title"
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["title"], name: "index_tags_on_account_id_and_title", unique: true
|
||||
end
|
||||
|
||||
create_table "users", force: :cascade do |t|
|
||||
|
||||
+32
-94
@@ -114,57 +114,22 @@ columns:
|
||||
collation:
|
||||
comment:
|
||||
- *9
|
||||
action_text_markdowns:
|
||||
action_text_rich_texts:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: content
|
||||
cast_type: &11 !ruby/object:ActiveRecord::Type::Text
|
||||
name: body
|
||||
cast_type: &15 !ruby/object:ActiveRecord::Type::Text
|
||||
true: t
|
||||
false: f
|
||||
precision:
|
||||
scale:
|
||||
limit:
|
||||
sql_type_metadata: &12 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type_metadata: &16 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type: TEXT
|
||||
type: :text
|
||||
limit:
|
||||
precision:
|
||||
scale:
|
||||
'null': false
|
||||
default: ''
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *5
|
||||
- *6
|
||||
- *10
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: record_id
|
||||
cast_type: *1
|
||||
sql_type_metadata: *2
|
||||
'null': false
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- &13 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: record_type
|
||||
cast_type: *7
|
||||
sql_type_metadata: *8
|
||||
'null': false
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *9
|
||||
action_text_rich_texts:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: body
|
||||
cast_type: *11
|
||||
sql_type_metadata: *12
|
||||
'null': true
|
||||
default:
|
||||
default_function:
|
||||
@@ -173,16 +138,16 @@ columns:
|
||||
- *5
|
||||
- *6
|
||||
- *10
|
||||
- &16 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- &13 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: record_id
|
||||
cast_type: &14 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer
|
||||
cast_type: &11 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer
|
||||
precision:
|
||||
scale:
|
||||
limit:
|
||||
max: 9223372036854775808
|
||||
min: -9223372036854775808
|
||||
sql_type_metadata: &15 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type_metadata: &12 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type: bigint
|
||||
type: :integer
|
||||
limit:
|
||||
@@ -193,14 +158,23 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *13
|
||||
- &14 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: record_type
|
||||
cast_type: *7
|
||||
sql_type_metadata: *8
|
||||
'null': false
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *9
|
||||
active_storage_attachments:
|
||||
- &17 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: blob_id
|
||||
cast_type: *14
|
||||
sql_type_metadata: *15
|
||||
cast_type: *11
|
||||
sql_type_metadata: *12
|
||||
'null': false
|
||||
default:
|
||||
default_function:
|
||||
@@ -209,8 +183,8 @@ columns:
|
||||
- *5
|
||||
- *6
|
||||
- *10
|
||||
- *16
|
||||
- *13
|
||||
- *14
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: slug
|
||||
@@ -225,8 +199,8 @@ columns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: byte_size
|
||||
cast_type: *14
|
||||
sql_type_metadata: *15
|
||||
cast_type: *11
|
||||
sql_type_metadata: *12
|
||||
'null': false
|
||||
default:
|
||||
default_function:
|
||||
@@ -277,8 +251,8 @@ columns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: metadata
|
||||
cast_type: *11
|
||||
sql_type_metadata: *12
|
||||
cast_type: *15
|
||||
sql_type_metadata: *16
|
||||
'null': true
|
||||
default:
|
||||
default_function:
|
||||
@@ -445,8 +419,8 @@ columns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: status
|
||||
cast_type: *11
|
||||
sql_type_metadata: *12
|
||||
cast_type: *15
|
||||
sql_type_metadata: *16
|
||||
'null': false
|
||||
default: creating
|
||||
default_function:
|
||||
@@ -524,8 +498,8 @@ columns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: auto_close_period
|
||||
cast_type: *14
|
||||
sql_type_metadata: *15
|
||||
cast_type: *11
|
||||
sql_type_metadata: *12
|
||||
'null': true
|
||||
default:
|
||||
default_function:
|
||||
@@ -573,8 +547,8 @@ columns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: line
|
||||
cast_type: *11
|
||||
sql_type_metadata: *12
|
||||
cast_type: *15
|
||||
sql_type_metadata: *16
|
||||
'null': true
|
||||
default:
|
||||
default_function:
|
||||
@@ -1005,7 +979,6 @@ columns:
|
||||
primary_keys:
|
||||
accesses: id
|
||||
accounts: id
|
||||
action_text_markdowns: id
|
||||
action_text_rich_texts: id
|
||||
active_storage_attachments: id
|
||||
active_storage_blobs: id
|
||||
@@ -1044,7 +1017,6 @@ primary_keys:
|
||||
data_sources:
|
||||
accesses: true
|
||||
accounts: true
|
||||
action_text_markdowns: true
|
||||
action_text_rich_texts: true
|
||||
active_storage_attachments: true
|
||||
active_storage_blobs: true
|
||||
@@ -1132,24 +1104,6 @@ indexes:
|
||||
comment:
|
||||
valid: true
|
||||
accounts: []
|
||||
action_text_markdowns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: action_text_markdowns
|
||||
name: index_action_text_markdowns_on_record
|
||||
unique: false
|
||||
columns:
|
||||
- record_type
|
||||
- record_id
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
action_text_rich_texts:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: action_text_rich_texts
|
||||
@@ -2098,23 +2052,7 @@ indexes:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
tags:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: tags
|
||||
name: index_tags_on_account_id_and_title
|
||||
unique: true
|
||||
columns:
|
||||
- title
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
tags: []
|
||||
users:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: users
|
||||
@@ -2199,4 +2137,4 @@ indexes:
|
||||
comment:
|
||||
valid: true
|
||||
workflows: []
|
||||
version: 20250523084734
|
||||
version: 20250601161653
|
||||
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#! /usr/bin/env ruby
|
||||
|
||||
require_relative "../config/environment"
|
||||
|
||||
def migrate(source_service_name, target_service_name)
|
||||
ApplicationRecord.with_each_tenant do |tenant|
|
||||
puts "\n## #{tenant}"
|
||||
report = { updated: 0, skipped: 0, errors: 0 }
|
||||
|
||||
if ActiveStorage::Blob.count == 0
|
||||
puts "No blobs found, skipping."
|
||||
next
|
||||
end
|
||||
|
||||
ActiveStorage::Blob.service = source_service = ActiveStorage::Blob.services.fetch(source_service_name)
|
||||
target_service = ActiveStorage::Blob.services.fetch(target_service_name)
|
||||
|
||||
ActiveStorage::Blob.find_each do |blob|
|
||||
if target_service.name.to_sym == blob.service_name.to_sym
|
||||
report[:skipped] += 1
|
||||
putc "-"
|
||||
elsif target_service.exist?(blob.key)
|
||||
report[:skipped] += 1
|
||||
putc "S"
|
||||
else
|
||||
begin
|
||||
blob.open do |stream|
|
||||
target_service.upload(blob.key, stream, checksum: blob.checksum)
|
||||
end
|
||||
report[:updated] += 1
|
||||
putc "."
|
||||
rescue ActiveStorage::FileNotFoundError
|
||||
report[:errors] += 1
|
||||
putc "E"
|
||||
end
|
||||
end
|
||||
|
||||
# Update the service name of the blob.
|
||||
blob.update_column :service_name, target_service_name
|
||||
end
|
||||
|
||||
puts
|
||||
pp report
|
||||
end
|
||||
end
|
||||
|
||||
migrate :local, :purestorage
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#! /usr/bin/env ruby
|
||||
|
||||
require_relative "../config/environment"
|
||||
|
||||
ApplicationRecord.with_each_tenant do |tenant|
|
||||
puts "\n## #{tenant}"
|
||||
report = { updated: 0, skipped: 0 }
|
||||
|
||||
ActiveStorage::Blob.find_each do |blob|
|
||||
if blob.key.start_with?("#{tenant}/")
|
||||
report[:skipped] += 1
|
||||
else
|
||||
blob.update_column :key, "#{tenant}/#{blob.key}"
|
||||
report[:updated] += 1
|
||||
end
|
||||
end
|
||||
pp report
|
||||
|
||||
disk_service = ActiveStorage::Blob.services.fetch(:local)
|
||||
new_root = File.join(disk_service.root, tenant)
|
||||
old_root = File.join("storage", "tenants", Rails.env, tenant, "files")
|
||||
|
||||
FileUtils.mkdir_p(new_root, verbose: true) unless File.directory?(new_root)
|
||||
|
||||
Dir.glob(File.join(old_root, "??")).each_slice(20) do |blob_dirs|
|
||||
FileUtils.mv(blob_dirs, new_root, verbose: true)
|
||||
end
|
||||
end
|
||||
@@ -1,271 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require_relative "../config/environment"
|
||||
|
||||
class EnactTenanting
|
||||
CURRENT_DOMAIN = "https://fizzy.37signals.com"
|
||||
TENANTED_DOMAIN = Rails.env.local? ? "http://%{tenant}.fizzy.localhost:3006" : "https://%{tenant}.fizzy.37signals.com"
|
||||
|
||||
PROJECT_MAPPING = {
|
||||
693169841 => "37s", # Writebook improvements
|
||||
693169850 => "37s", # Fizzy: Issues
|
||||
693169853 => "37s", # Fizzy: 💡 IDEAS
|
||||
693169858 => "37s", # Know It All: 💡 IDEAS
|
||||
693169859 => "37s", # Cycle 1: BC4 Client access for templates
|
||||
693169860 => "37s", # Know It All: Issues
|
||||
693169862 => "37s", # Active Record Tenanting
|
||||
693169863 => "37s", # Cycle 1: BC4 Hilltop View
|
||||
693169865 => "37s", # [BC iOS] Search refactor
|
||||
693169867 => "37s", # File icons refactor
|
||||
693169870 => "37s", # HEY Preload Replies
|
||||
693169872 => "37s", # [BC4 Android] Search Refactor
|
||||
693169873 => "37s", # BC4
|
||||
693169874 => "37s", # [HEY] ContentFilter pipeline rewrite
|
||||
693169875 => "37s", # [Hey Calendar Android] Search
|
||||
693169876 => "37s", # [HEY Calendar iOS] Search
|
||||
693169877 => "37s", # [BC4] Turbo for comment and answer forms
|
||||
|
||||
693169842 => "dev", # Foobar
|
||||
693169843 => "dev", # Test
|
||||
693169851 => "dev", # Kevin
|
||||
693169861 => "dev", # Testing space
|
||||
693169869 => "dev", # Flower ideas
|
||||
693169871 => "dev", # Mike's TODOs
|
||||
|
||||
693169856 => "qa" # QA Exploration
|
||||
}
|
||||
|
||||
TENANTS = PROJECT_MAPPING.values.uniq
|
||||
|
||||
attr_reader :creation_counter, :update_counter
|
||||
|
||||
def initialize
|
||||
@creation_counter = {}
|
||||
@update_counter = {}
|
||||
end
|
||||
|
||||
def up
|
||||
setup
|
||||
destroy_tenants
|
||||
|
||||
safety_check
|
||||
|
||||
copy_accounts_et_al
|
||||
copy_buckets_et_al
|
||||
copy_bubbles_et_al
|
||||
copy_filters
|
||||
copy_active_storage
|
||||
|
||||
cross_check
|
||||
|
||||
update_action_text_urls
|
||||
|
||||
pp [ "updated:", update_counter ]
|
||||
|
||||
hardlink_active_storage
|
||||
end
|
||||
|
||||
def setup
|
||||
ApplicationRecord.connects_to shards: { untenanted: { reading: :primary_original } }
|
||||
end
|
||||
|
||||
def with_original_db(&block)
|
||||
ApplicationRecord.connected_to(role: :reading, shard: :untenanted, &block)
|
||||
end
|
||||
|
||||
def safety_check
|
||||
buckets = with_original_db { Bucket.all.to_a }
|
||||
|
||||
unless buckets.map(&:id).sort == PROJECT_MAPPING.keys.sort
|
||||
unknown_buckets = buckets.map(&:id) - PROJECT_MAPPING.keys
|
||||
missing_buckets = PROJECT_MAPPING.keys - buckets.map(&:id)
|
||||
|
||||
raise "Surprising buckets. unknown #{unknown_buckets.inspect}, missing #{missing_buckets.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
def copy_accounts_et_al
|
||||
account = with_original_db { Account.first }
|
||||
users = with_original_db { User.all.to_a }
|
||||
workflows = with_original_db { account.workflows.to_a }
|
||||
workflow_stages = with_original_db { workflows.flat_map(&:stages) }
|
||||
tags = with_original_db { account.tags.to_a }
|
||||
|
||||
TENANTS.each do |tenant|
|
||||
ApplicationRecord.with_tenant(tenant) do
|
||||
Account.create! name: tenant, id: account.id
|
||||
|
||||
upsert_all users
|
||||
upsert_all workflows
|
||||
upsert_all workflow_stages
|
||||
upsert_all tags
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def copy_buckets_et_al
|
||||
PROJECT_MAPPING.each do |bucket_id, tenant|
|
||||
bucket = with_original_db { Bucket.where(id: bucket_id).first }
|
||||
accesses = with_original_db { bucket.accesses.to_a }
|
||||
subscriptions = with_original_db { bucket.subscriptions.to_a }
|
||||
|
||||
ApplicationRecord.with_tenant(tenant) do
|
||||
upsert_all bucket
|
||||
upsert_all accesses
|
||||
upsert_all subscriptions
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def copy_bubbles_et_al
|
||||
PROJECT_MAPPING.each do |bucket_id, tenant|
|
||||
bubbles = with_original_db { Bucket.find(bucket_id).bubbles.to_a }
|
||||
assignments = with_original_db { bubbles.flat_map(&:assignments) }
|
||||
pops = with_original_db { bubbles.filter_map(&:pop) }
|
||||
notifications = with_original_db { bubbles.flat_map(&:notifications) }
|
||||
events = with_original_db { bubbles.flat_map(&:events) }
|
||||
taggings = with_original_db { bubbles.flat_map(&:taggings) }
|
||||
watches = with_original_db { bubbles.flat_map(&:watches) }
|
||||
messages = with_original_db { bubbles.flat_map(&:messages) }
|
||||
messageables = with_original_db { messages.filter_map(&:messageable) }
|
||||
comments = messageables.select { _1.is_a?(Comment) }
|
||||
markdowns = with_original_db { comments.map(&:markdown_body) }
|
||||
reactions = with_original_db { comments.flat_map(&:reactions) }
|
||||
|
||||
ApplicationRecord.with_tenant(tenant) do
|
||||
upsert_all bubbles
|
||||
upsert_all assignments
|
||||
upsert_all pops
|
||||
upsert_all taggings
|
||||
upsert_all watches
|
||||
messageables.group_by(&:class).each do |klass, klass_messageables|
|
||||
upsert_all klass_messageables
|
||||
end
|
||||
upsert_all markdowns
|
||||
upsert_all reactions
|
||||
upsert_all messages
|
||||
upsert_all events
|
||||
upsert_all notifications
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def copy_filters
|
||||
creation_counter["Filter"] = 0
|
||||
TENANTS.each do |tenant|
|
||||
buckets = ApplicationRecord.with_tenant(tenant) { Bucket.all.to_a }
|
||||
|
||||
filters = with_original_db do
|
||||
Filter.all.filter_map do |filter| # hah no pun intended
|
||||
next unless filter.buckets.empty? || buckets.any? { |b| filter.buckets.include?(b) }
|
||||
|
||||
filter.attributes.merge(filter.as_params)
|
||||
end
|
||||
end
|
||||
|
||||
ApplicationRecord.with_tenant(tenant) do
|
||||
filters.each do |attr|
|
||||
Filter.create! attr
|
||||
end
|
||||
end
|
||||
creation_counter["Filter"] += filters.length
|
||||
end
|
||||
end
|
||||
|
||||
def copy_active_storage
|
||||
attachments = with_original_db { ActiveStorage::Attachment.all.to_a }
|
||||
blobs = with_original_db { ActiveStorage::Blob.all.to_a }
|
||||
|
||||
TENANTS.each do |tenant|
|
||||
ApplicationRecord.with_tenant(tenant) do
|
||||
upsert_all blobs
|
||||
upsert_all attachments
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy_tenants
|
||||
TENANTS.each do |tenant|
|
||||
ApplicationRecord.destroy_tenant(tenant)
|
||||
end
|
||||
FileUtils.rm_rf "storage/tenants"
|
||||
end
|
||||
|
||||
def cross_check
|
||||
pp [ "created:", creation_counter ]
|
||||
|
||||
with_original_db do
|
||||
assert_count User, User.count * TENANTS.length
|
||||
assert_count Workflow, Workflow.count * TENANTS.length
|
||||
assert_count Workflow::Stage, Workflow::Stage.count * TENANTS.length
|
||||
assert_count Tag, Tag.count * TENANTS.length
|
||||
|
||||
assert_count Bucket, Bucket.count
|
||||
assert_count Access, Access.count
|
||||
assert_count Subscription, Subscription.count
|
||||
|
||||
assert_count Bubble, Bubble.count
|
||||
assert_count Assignment, Assignment.count
|
||||
assert_count Pop, Pop.count
|
||||
assert_count EventSummary, EventSummary.count
|
||||
assert_count Comment, Comment.count
|
||||
assert_count Reaction, Reaction.count
|
||||
assert_count Message, Message.count
|
||||
assert_count Tagging, Tagging.count
|
||||
assert_count Watch, Watch.count
|
||||
|
||||
assert_count Event, Event.count
|
||||
assert_count Notification, Notification.count
|
||||
|
||||
assert_count ActiveStorage::Attachment, ActiveStorage::Attachment.count * TENANTS.length
|
||||
assert_count ActiveStorage::Blob, ActiveStorage::Blob.count * TENANTS.length
|
||||
|
||||
raise "Filter count is off" unless creation_counter["Filter"] >= Filter.count
|
||||
end
|
||||
end
|
||||
|
||||
def update_action_text_urls
|
||||
raise "No more markdown"
|
||||
end
|
||||
|
||||
def hardlink_active_storage
|
||||
files = Dir.glob("storage/files/*/*/*").map { _1.split("/")[2..].join("/") }
|
||||
|
||||
TENANTS.each do |tenant|
|
||||
ApplicationRecord.with_tenant(tenant) do
|
||||
destdir = ActiveStorage::Blob.service.root
|
||||
|
||||
files.each do |file|
|
||||
FileUtils.mkdir_p File.join(destdir, File.dirname(file))
|
||||
FileUtils.ln File.join("storage/files", file), File.join(destdir, file)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def upsert_all(originals)
|
||||
originals = Array(originals)
|
||||
return if originals.empty?
|
||||
|
||||
klass = originals.first.class
|
||||
|
||||
result = klass.upsert_all(originals.collect(&:attributes))
|
||||
raise "Error upserting" unless result.rows.length == originals.length
|
||||
|
||||
creation_counter[klass.name] ||= 0
|
||||
creation_counter[klass.name] += originals.length
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def assert_count(klass, expected)
|
||||
actual = creation_counter[klass.name]
|
||||
unless actual == expected
|
||||
raise "#{klass} count is off: expected #{expected}, got #{actual}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
EnactTenanting.new.up
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,35 @@
|
||||
require "test_helper"
|
||||
|
||||
class HtmlHelperTest < ActionView::TestCase
|
||||
test "converts URLs into anchor tags" do
|
||||
assert_equal_html \
|
||||
%(<p>Check this: <a href="https://example.com" rel="noreferrer">https://example.com</a></p>),
|
||||
format_html("<p>Check this: https://example.com</p>")
|
||||
end
|
||||
|
||||
test "respect existing links" do
|
||||
assert_equal_html \
|
||||
%(<p>Check this: <a href="https://example.com">https://example.com</a></p>),
|
||||
format_html("<p>Check this: <a href=\"https://example.com\">https://example.com</a></p>")
|
||||
end
|
||||
|
||||
test "converts email addresses into mailto links" do
|
||||
assert_equal_html \
|
||||
%(<p>Contact us at <a href="mailto:support@example.com">support@example.com</a></p>),
|
||||
format_html("<p>Contact us at support@example.com</p>")
|
||||
end
|
||||
|
||||
test "respect existing linked emails" do
|
||||
assert_equal_html \
|
||||
%(<p>Contact us at <a href="mailto:support@example.com">support@example.com</a></p>),
|
||||
format_html(%(<p>Contact us at <a href="mailto:support@example.com">support@example.com</a></p>))
|
||||
end
|
||||
|
||||
test "don't autolink content in excluded elements" do
|
||||
%w[ figcaption pre code ].each do |element|
|
||||
assert_equal_html \
|
||||
"<#{element}>Check this: https://example.com</#{element}>",
|
||||
format_html("<#{element}>Check this: https://example.com</#{element}>")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -26,30 +26,4 @@ class Card::CloseableTest < ActiveSupport::TestCase
|
||||
cards(:logo).update! last_active_at: 2.day.ago
|
||||
assert_equal (123-2).days.from_now, cards(:logo).auto_close_at
|
||||
end
|
||||
|
||||
test "auto close all due" do
|
||||
cards(:logo, :shipping).each(&:reconsider)
|
||||
|
||||
cards(:logo).update!(last_active_at: 1.day.ago - collections(:writebook).auto_close_period)
|
||||
cards(:shipping).update!(last_active_at: 1.day.from_now - collections(:writebook).auto_close_period)
|
||||
|
||||
assert_difference -> { Card.closed.count }, +1 do
|
||||
Card.auto_close_all_due
|
||||
end
|
||||
|
||||
assert cards(:logo).reload.closed?
|
||||
assert_not cards(:shipping).reload.closed?
|
||||
end
|
||||
|
||||
test "don't auto close those cards where the collection has no auto close period" do
|
||||
cards(:logo, :shipping).each(&:reconsider)
|
||||
|
||||
collections(:writebook).update auto_close_period: nil
|
||||
|
||||
assert_no_difference -> { Card.closed.count } do
|
||||
Card.auto_close_all_due
|
||||
end
|
||||
|
||||
assert_not cards(:logo).reload.closed?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,6 +11,9 @@ class Card::EngageableTest < ActiveSupport::TestCase
|
||||
|
||||
assert_not cards(:logo).considering?
|
||||
assert cards(:text).considering?
|
||||
|
||||
assert_equal "doing", cards(:logo).engagement_status
|
||||
assert_equal "considering", cards(:text).engagement_status
|
||||
end
|
||||
|
||||
test "change the engagement" do
|
||||
@@ -46,21 +49,4 @@ class Card::EngageableTest < ActiveSupport::TestCase
|
||||
assert_includes Card.considering, cards(:text)
|
||||
assert_not_includes Card.considering, cards(:logo)
|
||||
end
|
||||
|
||||
test "auto_reconsider_all_stagnated" do
|
||||
travel_to Time.current
|
||||
|
||||
cards(:logo, :shipping).each(&:engage)
|
||||
|
||||
cards(:logo).update!(last_active_at: 1.day.ago - Card::Engageable::STAGNATED_AFTER)
|
||||
cards(:shipping).update!(last_active_at: 1.day.from_now - Card::Engageable::STAGNATED_AFTER)
|
||||
|
||||
assert_difference -> { Card.considering.count }, +1 do
|
||||
Card.auto_reconsider_all_stagnated
|
||||
end
|
||||
|
||||
assert cards(:shipping).reload.doing?
|
||||
assert cards(:logo).reload.considering?
|
||||
assert_equal Time.current, cards(:logo).last_active_at
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
require "test_helper"
|
||||
|
||||
class Card::EntropyTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
Current.session = sessions(:david)
|
||||
end
|
||||
|
||||
test "auto close all due" do
|
||||
cards(:logo, :shipping).each(&:reconsider)
|
||||
|
||||
cards(:logo).update!(last_active_at: 1.day.ago - collections(:writebook).auto_close_period)
|
||||
cards(:shipping).update!(last_active_at: 1.day.from_now - collections(:writebook).auto_close_period)
|
||||
|
||||
assert_difference -> { Card.closed.count }, +1 do
|
||||
Card.auto_close_all_due
|
||||
end
|
||||
|
||||
assert cards(:logo).reload.closed?
|
||||
assert_not cards(:shipping).reload.closed?
|
||||
end
|
||||
|
||||
test "don't auto close those cards where the collection has no auto close period" do
|
||||
cards(:logo, :shipping).each(&:reconsider)
|
||||
|
||||
collections(:writebook).update auto_close_period: nil
|
||||
|
||||
assert_no_difference -> { Card.closed.count } do
|
||||
Card.auto_close_all_due
|
||||
end
|
||||
|
||||
assert_not cards(:logo).reload.closed?
|
||||
end
|
||||
|
||||
test "auto_reconsider_all_stagnated" do
|
||||
travel_to Time.current
|
||||
|
||||
cards(:logo, :shipping).each(&:engage)
|
||||
|
||||
cards(:logo).update!(last_active_at: 1.day.ago - Card::AUTO_RECONSIDER_PERIOD)
|
||||
cards(:shipping).update!(last_active_at: 1.day.from_now - Card::AUTO_RECONSIDER_PERIOD)
|
||||
|
||||
assert_difference -> { Card.considering.count }, +1 do
|
||||
Card.auto_reconsider_all_stagnated
|
||||
end
|
||||
|
||||
assert cards(:shipping).reload.doing?
|
||||
assert cards(:logo).reload.considering?
|
||||
assert_equal Time.current, cards(:logo).last_active_at
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user