The tenanted db is the account scope

This commit is contained in:
David Heinemeier Hansson
2025-04-12 20:06:34 +02:00
parent 19dbd5b325
commit 6bbf68a4f9
57 changed files with 123 additions and 297 deletions
@@ -1,10 +1,10 @@
class Accounts::JoinCodesController < ApplicationController class Accounts::JoinCodesController < ApplicationController
def show def show
render svg: RQRCode::QRCode.new(join_url(Current.account.join_code)).as_svg(viewbox: true, fill: :white, color: :black) render svg: RQRCode::QRCode.new(join_url(Account.sole.join_code)).as_svg(viewbox: true, fill: :white, color: :black)
end end
def update def update
Current.account.reset_join_code Account.sole.reset_join_code
redirect_to account_users_path redirect_to users_path
end end
end end
+4 -4
View File
@@ -3,22 +3,22 @@ class Accounts::UsersController < ApplicationController
before_action :ensure_permission_to_administer_user, only: %i[ update destroy ] before_action :ensure_permission_to_administer_user, only: %i[ update destroy ]
def index def index
@users = Current.account.users.active @users = User.active
end end
def update def update
@user.update(role_params) @user.update(role_params)
redirect_to account_users_path redirect_to users_path
end end
def destroy def destroy
@user.deactivate @user.deactivate
redirect_to account_users_path redirect_to users_path
end end
private private
def set_user def set_user
@user = Current.account.users.active.find(params[:id]) @user = User.active.find(params[:id])
end end
def ensure_permission_to_administer_user def ensure_permission_to_administer_user
+1 -1
View File
@@ -3,7 +3,7 @@ class Cards::StagingsController < ApplicationController
def create def create
if params[:stage_id].present? if params[:stage_id].present?
@card.toggle_stage Current.account.stages.find(params[:stage_id]) @card.toggle_stage Stage.find(params[:stage_id])
else else
@card.update!(stage: nil) @card.update!(stage: nil)
end end
+1 -1
View File
@@ -2,7 +2,7 @@ class Cards::TaggingsController < ApplicationController
include CardScoped include CardScoped
def new def new
@tags = Current.account.tags.alphabetically @tags = Tag.alphabetically
end end
def create def create
@@ -11,6 +11,6 @@ class Collections::WorkflowsController < ApplicationController
private private
def set_workflow def set_workflow
@workflow = Current.account.workflows.find(params.expect(collection: [ :workflow_id ]).require(:workflow_id)) @workflow = Workflow.find(params.expect(collection: [ :workflow_id ]).require(:workflow_id))
end end
end end
+3 -3
View File
@@ -2,11 +2,11 @@ class CollectionsController < ApplicationController
before_action :set_collection, except: %i[ new create ] before_action :set_collection, except: %i[ new create ]
def new def new
@collection = Current.account.collections.build @collection = Collection.new
end end
def create def create
@collection = Current.account.collections.create! collection_params @collection = Collection.create! collection_params
redirect_to cards_path(collection_ids: [ @collection ]) redirect_to cards_path(collection_ids: [ @collection ])
end end
@@ -37,7 +37,7 @@ class CollectionsController < ApplicationController
end end
def grantees def grantees
Current.account.users.active.where id: grantee_ids User.active.where id: grantee_ids
end end
def revokees def revokees
+1 -1
View File
@@ -29,7 +29,7 @@ module EventsTimeline
end end
def user_events def user_events
Event.where(card: user_cards, creator: Current.account.users) Event.where(card: user_cards)
end end
def user_cards def user_cards
+1 -1
View File
@@ -7,6 +7,6 @@ module WorkflowScoped
private private
def set_workflow def set_workflow
@workflow = Current.account.workflows.find params[:workflow_id] @workflow = Workflow.find(params[:workflow_id])
end end
end end
+2 -1
View File
@@ -5,7 +5,8 @@ class UploadsController < ApplicationController
before_action :set_attachment, only: :show before_action :set_attachment, only: :show
def create def create
@upload = Current.account.uploads_attachments.create! blob: create_blob! # FIXME: Try to get upload attachments on root
@upload = Account.first.uploads_attachments.create! blob: create_blob!
end end
def show def show
+1 -1
View File
@@ -24,7 +24,7 @@ class Users::AvatarsController < ApplicationController
end end
def set_user def set_user
@user = Current.account.users.find(params[:user_id]) @user = User.find(params[:user_id])
end end
def render_avatar_or_initials def render_avatar_or_initials
+5 -5
View File
@@ -5,11 +5,11 @@ class UsersController < ApplicationController
before_action :set_account_from_join_code, only: %i[ new create ] before_action :set_account_from_join_code, only: %i[ new create ]
def new def new
@user = @account.users.build @user = User.new
end end
def create def create
user = @account.users.create!(user_params) user = User.create!(user_params)
start_new_session_for user start_new_session_for user
redirect_to root_path redirect_to root_path
end end
@@ -21,8 +21,8 @@ class UsersController < ApplicationController
end end
def update def update
@user.update user_params @user.update! user_params
redirect_to user_path(@user) redirect_to @user
end end
private private
@@ -31,7 +31,7 @@ class UsersController < ApplicationController
end end
def set_user def set_user
@user = Current.account.users.active.find(params[:id]) @user = User.active.find(params[:id])
end end
def user_params def user_params
+5 -5
View File
@@ -2,15 +2,15 @@ class WorkflowsController < ApplicationController
before_action :set_workflow, only: %i[ show edit update destroy ] before_action :set_workflow, only: %i[ show edit update destroy ]
def index def index
@workflows = Current.account.workflows @workflows = Workflow.all
end end
def new def new
@workflow = Current.account.workflows.new @workflow = Workflow.new
end end
def create def create
@workflow = Current.account.workflows.create! workflow_params @workflow = Workflow.create! workflow_params
# FIXME: this should definitely change. # FIXME: this should definitely change.
[ "Triage", "In progress", "On Hold", "Review" ].each { |name| @workflow.stages.create! name: name } [ "Triage", "In progress", "On Hold", "Review" ].each { |name| @workflow.stages.create! name: name }
redirect_to workflows_path redirect_to workflows_path
@@ -34,10 +34,10 @@ class WorkflowsController < ApplicationController
private private
def set_workflow def set_workflow
@workflow = Current.account.workflows.find params[:id] @workflow = Workflow.find params[:id]
end end
def workflow_params def workflow_params
params.expect workflow: [ :name ] params.expect workflow: :name
end end
end end
+1 -17
View File
@@ -1,21 +1,5 @@
class Account < ApplicationRecord class Account < ApplicationRecord
include ClosureReasons, Joinable include Joinable
has_many :collections, dependent: :destroy
has_many :cards, through: :collections
has_many :users, dependent: :destroy do
def system
find_or_create_system_user(proxy_association.owner)
end
end
has_many :comments, through: :users
has_many :workflows, dependent: :destroy
has_many :stages, through: :workflows, class_name: "Workflow::Stage"
has_many :tags, dependent: :destroy
has_many_attached :uploads has_many_attached :uploads
end end
-29
View File
@@ -1,29 +0,0 @@
module Account::ClosureReasons
extend ActiveSupport::Concern
DEFAULT_LABELS = [
"Completed",
"Duplicate",
"Maybe later",
"Working as intended"
]
FALLBACK_LABEL = "Done"
included do
has_many :closure_reasons, dependent: :destroy, class_name: "Closure::Reason" do
def labels
pluck(:label).presence || [ FALLBACK_LABEL ]
end
end
after_create :create_default_closure_reasons
end
private
def create_default_closure_reasons
DEFAULT_LABELS.each do |label|
closure_reasons.create! label: label
end
end
end
+2 -2
View File
@@ -16,7 +16,7 @@ module Card::Closeable
class_methods do class_methods do
def auto_close_all_due def auto_close_all_due
due_to_be_closed.find_each do |card| due_to_be_closed.find_each do |card|
card.close(user: card.collection.account.users.system, reason: "Closed") card.close(user: User.system, reason: "Closed")
end end
end end
end end
@@ -41,7 +41,7 @@ module Card::Closeable
closure&.created_at closure&.created_at
end end
def close(user: Current.user, reason: Account::ClosureReasons::FALLBACK_LABEL) def close(user: Current.user, reason: Closure::Reason::FALLBACK_LABEL)
unless closed? unless closed?
transaction do transaction do
create_closure! user: user, reason: reason create_closure! user: user, reason: reason
+1 -1
View File
@@ -9,7 +9,7 @@ module Card::Taggable
end end
def toggle_tag_with(title) def toggle_tag_with(title)
tag = collection.account.tags.find_or_create_by!(title: title) tag = Tag.find_or_create_by!(title: title)
transaction do transaction do
if tagged_with?(tag) if tagged_with?(tag)
+20 -1
View File
@@ -1,3 +1,22 @@
class Closure::Reason < ApplicationRecord class Closure::Reason < ApplicationRecord
belongs_to :account DEFAULT_LABELS = [
"Completed",
"Duplicate",
"Maybe later",
"Working as intended"
]
FALLBACK_LABEL = "Done"
class << self
def labels
pluck(:label).presence || [ FALLBACK_LABEL ]
end
def create_defaults
DEFAULT_LABELS.each do |label|
create! label: label
end
end
end
end end
-1
View File
@@ -1,7 +1,6 @@
class Collection < ApplicationRecord class Collection < ApplicationRecord
include Accessible, Broadcastable, Filterable include Accessible, Broadcastable, Filterable
belongs_to :account
belongs_to :creator, class_name: "User", default: -> { Current.user } belongs_to :creator, class_name: "User", default: -> { Current.user }
belongs_to :workflow, optional: true belongs_to :workflow, optional: true
+1 -1
View File
@@ -34,6 +34,6 @@ module Collection::Accessible
private private
def grant_access_to_everyone def grant_access_to_everyone
accesses.grant_to(account.users) if all_access_previously_changed?(to: true) accesses.grant_to(User.all) if all_access_previously_changed?(to: true)
end end
end end
+1 -1
View File
@@ -3,6 +3,6 @@ module Collection::Broadcastable
included do included do
broadcasts_refreshes broadcasts_refreshes
broadcasts_refreshes_to ->(collection) { [ collection.account, :collections ] } broadcasts_refreshes_to ->(collection) { :collections }
end end
end end
-1
View File
@@ -2,5 +2,4 @@ class Current < ActiveSupport::CurrentAttributes
attribute :session attribute :session
delegate :user, to: :session, allow_nil: true delegate :user, to: :session, allow_nil: true
delegate :account, to: :user, allow_nil: true
end end
+3 -2
View File
@@ -1,6 +1,7 @@
class FirstRun class FirstRun
def self.create!(user_attributes) def self.create!(user_attributes)
account = Account.create!(name: "Fizzy") Account.create!(name: "Fizzy")
account.users.member.create!(user_attributes) User.member.create!(user_attributes)
Closure::Reason.create_defaults
end end
end end
-2
View File
@@ -1,8 +1,6 @@
class Tag < ApplicationRecord class Tag < ApplicationRecord
include Filterable include Filterable
belongs_to :account, default: -> { Current.account }, touch: true
has_many :taggings, dependent: :destroy has_many :taggings, dependent: :destroy
has_many :cards, through: :taggings has_many :cards, through: :taggings
-2
View File
@@ -1,8 +1,6 @@
class User < ApplicationRecord class User < ApplicationRecord
include Accessor, Assignee, Avatar, Role, Transferable include Accessor, Assignee, Avatar, Role, Transferable
belongs_to :account
has_many :sessions, dependent: :destroy has_many :sessions, dependent: :destroy
has_secure_password validations: false has_secure_password validations: false
+1 -1
View File
@@ -11,6 +11,6 @@ module User::Accessor
private private
def grant_access_to_collections def grant_access_to_collections
Access.insert_all account.collections.all_access.pluck(:id).collect { |collection_id| { collection_id: collection_id, user_id: id } } Access.insert_all Collection.all_access.pluck(:id).collect { |collection_id| { collection_id: collection_id, user_id: id } }
end end
end end
+2 -4
View File
@@ -10,10 +10,8 @@ module User::Role
end end
class_methods do class_methods do
def find_or_create_system_user(account) def system
account.users.find_or_create_by!(role: :system) do |user| find_or_create_by!(role: :system) { it.name = "System" }
user.name = "System"
end
end end
end end
-1
View File
@@ -1,4 +1,3 @@
class Workflow < ApplicationRecord class Workflow < ApplicationRecord
belongs_to :account
has_many :stages, dependent: :delete_all has_many :stages, dependent: :delete_all
end end
+1 -1
View File
@@ -1,5 +1,5 @@
<div class="flex flex-column align-center gap"> <div class="flex flex-column align-center gap">
<% url = join_url(Current.account.join_code) %> <% url = join_url(Account.sole.join_code) %>
<label class="flex flex-column gap full-width txt-align-center"> <label class="flex flex-column gap full-width txt-align-center">
<strong id="invite_label" class="invite-label">Share to invite more people</strong> <strong id="invite_label" class="invite-label">Share to invite more people</strong>
+1 -1
View File
@@ -11,7 +11,7 @@
<%= icon_tag "caret-down" %> <%= icon_tag "caret-down" %>
</summary> </summary>
<div class="expander__content btn borderless"> <div class="expander__content btn borderless">
<% Current.account.closure_reasons.labels.each do |label| %> <% Closure::Reason.labels.each do |label| %>
<%= button_to card_closure_path(card, reason: label), class: "expander__item btn" do %> <%= button_to card_closure_path(card, reason: label), class: "expander__item btn" do %>
<span class="overflow-ellipsis"><%= label %></span> <span class="overflow-ellipsis"><%= label %></span>
<% end %> <% end %>
+1 -1
View File
@@ -75,7 +75,7 @@
<strong class="txt-large">Choose a workflow</strong> <strong class="txt-large">Choose a workflow</strong>
<p class="margin-none">Use a workflow to track progress in this collection.</p> <p class="margin-none">Use a workflow to track progress in this collection.</p>
<%= form_with model: @collection, url: collection_workflow_path(@collection), local: true, method: :patch, data: { controller: "form" } do |form| %> <%= form_with model: @collection, url: collection_workflow_path(@collection), local: true, method: :patch, data: { controller: "form" } do |form| %>
<%= form.select :workflow_id, Current.account.workflows.map { |w| [w.name, w.id] }, { include_blank: "Choose…" }, class: "input input--select full-width", data: { action: "change->form#submit" } %> <%= form.select :workflow_id, Workflow.all.map { |w| [w.name, w.id] }, { include_blank: "Choose…" }, class: "input input--select full-width", data: { action: "change->form#submit" } %>
<% end %> <% end %>
<p><%= link_to "Manage workflows", workflows_path, class: "btn btn--plain txt-link" %></p> <p><%= link_to "Manage workflows", workflows_path, class: "btn btn--plain txt-link" %></p>
</div> </div>
+2 -2
View File
@@ -5,11 +5,11 @@
<div class="btn btn--circle btn--placeholder flex-item-justify-start"></div> <div class="btn btn--circle btn--placeholder flex-item-justify-start"></div>
<div class="events__index-header flex-inline flex-column center gap-half margin-block-end-double txt-align-center"> <div class="events__index-header flex-inline flex-column center gap-half margin-block-end-double txt-align-center">
<h2 class="txt-large txt-nowrap center"><%= Current.account.name %> / Latest activity</h2> <h2 class="txt-large txt-nowrap center"><%= Account.sole.name %> / Latest activity</h2>
<%= render "events/filter" %> <%= render "events/filter" %>
</div> </div>
<%= link_to account_users_path, class: "btn flex-item-justify-end" do %> <%= link_to users_path, class: "btn flex-item-justify-end" do %>
<%= icon_tag "settings" %> <%= icon_tag "settings" %>
<span class="for-screen-reader">Account settings</span> <span class="for-screen-reader">Account settings</span>
<% end %> <% end %>
+1 -1
View File
@@ -35,7 +35,7 @@
<%= icon_tag "check", class: "checked flex-item-justify-end" %> <%= icon_tag "check", class: "checked flex-item-justify-end" %>
</div> </div>
<% Current.account.users.active.order(:name).each do |user| %> <% User.active.order(:name).each do |user| %>
<div class="btn popup__item"> <div class="btn popup__item">
<%= form.check_box "assignee_ids[]", { <%= form.check_box "assignee_ids[]", {
checked: filter.assignees.include?(user), checked: filter.assignees.include?(user),
+1 -1
View File
@@ -3,5 +3,5 @@
<%= turbo_stream_from collection %> <%= turbo_stream_from collection %>
<% end %> <% end %>
<% else %> <% else %>
<%= turbo_stream_from Current.account, :collections %> <%= turbo_stream_from :collections %>
<% end %> <% end %>
+1 -1
View File
@@ -37,7 +37,7 @@
<%= icon_tag "check", class: "checked flex-item-justify-end" %> <%= icon_tag "check", class: "checked flex-item-justify-end" %>
</div> </div>
<% Current.account.users.active.without(Current.user).order(:name).each do |user| %> <% User.active.without(Current.user).order(:name).each do |user| %>
<div class="btn popup__item"> <div class="btn popup__item">
<%= form.check_box :creator_ids, { <%= form.check_box :creator_ids, {
multiple: true, multiple: true,
+4 -4
View File
@@ -78,7 +78,7 @@
<% end %> <% end %>
</menu> </menu>
<% if workflow = Current.account.workflows.first %> <% if workflow = Workflow.first %>
<menu class="filter__menu"> <menu class="filter__menu">
<li class="filter__label"><strong>In Stage</strong></li> <li class="filter__label"><strong>In Stage</strong></li>
<% workflow.stages.each do |stage| %> <% workflow.stages.each do |stage| %>
@@ -93,7 +93,7 @@
</menu> </menu>
<% end %> <% end %>
<% if (tags = Current.account.tags.order(:title)).any? %> <% if (tags = Tag.all.order(:title)).any? %>
<div class="filter__menu" role="group" aria-label="Tagged"> <div class="filter__menu" role="group" aria-label="Tagged">
<div class="filter__label"><strong>Tagged</strong></div> <div class="filter__label"><strong>Tagged</strong></div>
@@ -133,7 +133,7 @@
</label> </label>
</div> </div>
<% Current.account.users.active.without(Current.user).order(:name).each do |user| %> <% User.active.without(Current.user).order(:name).each do |user| %>
<div> <div>
<label class="btn filter__button"> <label class="btn filter__button">
<%= check_box_tag "assignee_ids[]", user.id, filter.assignees.include?(user), class: "visually-hidden", <%= check_box_tag "assignee_ids[]", user.id, filter.assignees.include?(user), class: "visually-hidden",
@@ -157,7 +157,7 @@
</label> </label>
</div> </div>
<% Current.account.users.active.without(Current.user).order(:name).each do |user| %> <% User.active.without(Current.user).order(:name).each do |user| %>
<div> <div>
<label class="btn filter__button"> <label class="btn filter__button">
<%= check_box_tag "creator_ids[]", user.id, filter.creators.include?(user), class: "visually-hidden" %> <%= check_box_tag "creator_ids[]", user.id, filter.creators.include?(user), class: "visually-hidden" %>
+2 -3
View File
@@ -1,4 +1,4 @@
<% if Current.account.workflows.any? %> <% if Workflow.any? %>
<div class="quick-filter position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside"> <div class="quick-filter position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn input input--select flex-inline txt-small" data-action="click->dialog#open:stop"> <button class="btn input input--select flex-inline txt-small" data-action="click->dialog#open:stop">
<span class="overflow-ellipsis"> <span class="overflow-ellipsis">
@@ -20,8 +20,7 @@
<%= filter_hidden_field_tag key, value %> <%= filter_hidden_field_tag key, value %>
<% end %> <% end %>
<% workflow = Current.account.workflows.first %> <% Workflow.first.stages.each do |stage| %>
<% workflow.stages.each do |stage| %>
<div class="btn popup__item"> <div class="btn popup__item">
<%= form.check_box :stage_ids, { <%= form.check_box :stage_ids, {
multiple: true, multiple: true,
+1 -1
View File
@@ -22,7 +22,7 @@
<span class="overflow-ellipsis">Clear all</span> <span class="overflow-ellipsis">Clear all</span>
<% end %> <% end %>
<% Current.account.tags.order(:title).each do |tag| %> <% Tag.order(:title).each do |tag| %>
<div class="btn popup__item"> <div class="btn popup__item">
<%= form.check_box "tag_ids[]", { <%= form.check_box "tag_ids[]", {
checked: filter.tags.include?(tag), checked: filter.tags.include?(tag),
+1 -1
View File
@@ -2,7 +2,7 @@
<% content_for :header do %> <% content_for :header do %>
<nav> <nav>
<%= link_to_back fallback_path: account_users_path %> <%= link_to_back fallback_path: users_path %>
<span class="btn btn--placeholder"></span> <span class="btn btn--placeholder"></span>
+3 -4
View File
@@ -2,12 +2,11 @@ Rails.application.routes.draw do
resource :first_run resource :first_run
resource :account do resource :account do
scope module: :accounts do resource :join_code, module: :accounts
resource :join_code
resources :users
end
end end
resources :users
resources :collections do resources :collections do
scope module: :collections do scope module: :collections do
resource :subscriptions resource :subscriptions
@@ -0,0 +1,9 @@
class RemoveAccountIds < ActiveRecord::Migration[8.1]
def change
remove_column :closure_reasons, :account_id
remove_column :collections, :account_id
remove_column :tags, :account_id
remove_column :users, :account_id
remove_column :workflows, :account_id
end
end
Generated
+1 -13
View File
@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2025_04_12_162339) do ActiveRecord::Schema[8.1].define(version: 2025_04_12_170620) do
create_table "accesses", force: :cascade do |t| create_table "accesses", force: :cascade do |t|
t.integer "collection_id", null: false t.integer "collection_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
@@ -129,11 +129,9 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_12_162339) do
end end
create_table "closure_reasons", force: :cascade do |t| create_table "closure_reasons", force: :cascade do |t|
t.integer "account_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.string "label" t.string "label"
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_closure_reasons_on_account_id"
end end
create_table "closures", force: :cascade do |t| create_table "closures", force: :cascade do |t|
@@ -148,14 +146,12 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_12_162339) do
end end
create_table "collections", force: :cascade do |t| create_table "collections", force: :cascade do |t|
t.integer "account_id", null: false
t.boolean "all_access", default: false, null: false t.boolean "all_access", default: false, null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.integer "creator_id", null: false t.integer "creator_id", null: false
t.string "name", null: false t.string "name", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "workflow_id" t.integer "workflow_id"
t.index ["account_id"], name: "index_collections_on_account_id"
t.index ["creator_id"], name: "index_collections_on_creator_id" t.index ["creator_id"], name: "index_collections_on_creator_id"
t.index ["workflow_id"], name: "index_collections_on_workflow_id" t.index ["workflow_id"], name: "index_collections_on_workflow_id"
end end
@@ -287,15 +283,12 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_12_162339) do
end end
create_table "tags", force: :cascade do |t| create_table "tags", force: :cascade do |t|
t.integer "account_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.string "title" t.string "title"
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_tags_on_account_id"
end end
create_table "users", force: :cascade do |t| create_table "users", force: :cascade do |t|
t.integer "account_id", null: false
t.boolean "active", default: true, null: false t.boolean "active", default: true, null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.string "email_address" t.string "email_address"
@@ -303,7 +296,6 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_12_162339) do
t.string "password_digest" t.string "password_digest"
t.string "role", default: "member", null: false t.string "role", default: "member", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_users_on_account_id"
t.index ["email_address"], name: "index_users_on_email_address", unique: true t.index ["email_address"], name: "index_users_on_email_address", unique: true
t.index ["role"], name: "index_users_on_role" t.index ["role"], name: "index_users_on_role"
end end
@@ -328,11 +320,9 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_12_162339) do
end end
create_table "workflows", force: :cascade do |t| create_table "workflows", force: :cascade do |t|
t.integer "account_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.string "name", null: false t.string "name", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_workflows_on_account_id"
end end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
@@ -353,11 +343,9 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_12_162339) do
add_foreign_key "sessions", "users" add_foreign_key "sessions", "users"
add_foreign_key "taggings", "cards" add_foreign_key "taggings", "cards"
add_foreign_key "taggings", "tags" add_foreign_key "taggings", "tags"
add_foreign_key "users", "accounts"
add_foreign_key "watches", "cards" add_foreign_key "watches", "cards"
add_foreign_key "watches", "users" add_foreign_key "watches", "users"
add_foreign_key "workflow_stages", "workflows" add_foreign_key "workflow_stages", "workflows"
add_foreign_key "workflows", "accounts"
# Virtual tables defined in this database. # Virtual tables defined in this database.
# Note that virtual tables may not work with other database engines. Be careful if changing database. # Note that virtual tables may not work with other database engines. Be careful if changing database.
+12 -115
View File
@@ -463,7 +463,7 @@ columns:
default_function: default_function:
collation: collation:
comment: comment:
- &33 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column - &32 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment: auto_increment:
name: title name: title
cast_type: *7 cast_type: *7
@@ -486,16 +486,6 @@ columns:
comment: comment:
closure_reasons: closure_reasons:
- *5 - *5
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: account_id
cast_type: *1
sql_type_metadata: *2
'null': true
default:
default_function:
collation:
comment:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment: auto_increment:
name: label name: label
@@ -535,24 +525,14 @@ columns:
comment: comment:
collections: collections:
- *5 - *5
- &32 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: account_id
cast_type: *1
sql_type_metadata: *2
'null': false
default:
default_function:
collation:
comment:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment: auto_increment:
name: all_access name: all_access
cast_type: &34 !ruby/object:ActiveModel::Type::Boolean cast_type: &33 !ruby/object:ActiveModel::Type::Boolean
precision: precision:
scale: scale:
limit: limit:
sql_type_metadata: &35 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type_metadata: &34 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
sql_type: boolean sql_type: boolean
type: :boolean type: :boolean
limit: limit:
@@ -860,18 +840,16 @@ columns:
- *9 - *9
tags: tags:
- *5 - *5
- *32
- *6 - *6
- *33 - *32
- *9 - *9
users: users:
- *5 - *5
- *32
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment: auto_increment:
name: active name: active
cast_type: *34 cast_type: *33
sql_type_metadata: *35 sql_type_metadata: *34
'null': false 'null': false
default: true default: true
default_function: default_function:
@@ -919,8 +897,8 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment: auto_increment:
name: watching name: watching
cast_type: *34 cast_type: *33
sql_type_metadata: *35 sql_type_metadata: *34
'null': false 'null': false
default: true default: true
default_function: default_function:
@@ -953,7 +931,6 @@ columns:
comment: comment:
workflows: workflows:
- *5 - *5
- *32
- *6 - *6
- *10 - *10
- *9 - *9
@@ -1421,23 +1398,7 @@ indexes:
nulls_not_distinct: nulls_not_distinct:
comment: comment:
valid: true valid: true
closure_reasons: closure_reasons: []
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: closure_reasons
name: index_closure_reasons_on_account_id
unique: false
columns:
- account_id
lengths: {}
orders: {}
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
closures: closures:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: closures table: closures
@@ -1489,22 +1450,6 @@ indexes:
comment: comment:
valid: true valid: true
collections: collections:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: collections
name: index_collections_on_account_id
unique: false
columns:
- account_id
lengths: {}
orders: {}
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: collections table: collections
name: index_collections_on_creator_id name: index_collections_on_creator_id
@@ -1994,23 +1939,7 @@ indexes:
nulls_not_distinct: nulls_not_distinct:
comment: comment:
valid: true valid: true
tags: tags: []
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: tags
name: index_tags_on_account_id
unique: false
columns:
- account_id
lengths: {}
orders: {}
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
users: users:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: users table: users
@@ -2044,22 +1973,6 @@ indexes:
nulls_not_distinct: nulls_not_distinct:
comment: comment:
valid: true valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: users
name: index_users_on_account_id
unique: false
columns:
- account_id
lengths: {}
orders: {}
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
watches: watches:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: watches table: watches
@@ -2110,21 +2023,5 @@ indexes:
nulls_not_distinct: nulls_not_distinct:
comment: comment:
valid: true valid: true
workflows: workflows: []
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition version: 20250412170620
table: workflows
name: index_workflows_on_account_id
unique: false
columns:
- account_id
lengths: {}
orders: {}
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
version: 20250412162339
+1 -1
View File
@@ -10,7 +10,7 @@ Current.session = user.sessions.last
workflow = account.workflows.first workflow = account.workflows.first
COLLECTIONS_COUNT.times do |collection_index| COLLECTIONS_COUNT.times do |collection_index|
collection = account.collections.create!(name: "Collection #{collection_index}", creator: user, workflow: workflow) collection = Collection.create!(name: "Collection #{collection_index}", creator: user, workflow: workflow)
CARDS_PER_COLLECTION.times do |card_index| CARDS_PER_COLLECTION.times do |card_index|
collection.cards.create!(title: "Card #{card_index}", creator: user, status: :published) collection.cards.create!(title: "Card #{card_index}", creator: user, status: :published)
end end
+2 -2
View File
@@ -4,9 +4,9 @@ CARDS_COUNT = 200
ApplicationRecord.current_tenant = "development-tenant" ApplicationRecord.current_tenant = "development-tenant"
account = Account.first account = Account.first
user = account.users.first user = User.first
Current.session = user.sessions.last Current.session = user.sessions.last
collection = account.collections.first collection = Collection.first
# Doing # Doing
@@ -8,34 +8,34 @@ class Accounts::UsersControllerTest < ActionDispatch::IntegrationTest
test "update" do test "update" do
assert_not users(:david).admin? assert_not users(:david).admin?
put account_user_url(users(:david)), params: { user: { role: "admin" } } put user_url(users(:david)), params: { user: { role: "admin" } }
assert_redirected_to account_users_path assert_redirected_to users_path
assert users(:david).reload.admin? assert users(:david).reload.admin?
end end
test "can't promote to special roles" do test "can't promote to special roles" do
assert_no_changes -> { users(:david).reload.role } do assert_no_changes -> { users(:david).reload.role } do
put account_user_url(users(:david)), params: { user: { role: "system" } } put user_url(users(:david)), params: { user: { role: "system" } }
end end
end end
test "destroy" do test "destroy" do
assert_difference -> { User.active.count }, -1 do assert_difference -> { User.active.count }, -1 do
delete account_user_url(users(:david)) delete user_url(users(:david))
end end
assert_redirected_to account_users_path assert_redirected_to users_path
assert_nil User.active.find_by(id: users(:david).id) assert_nil User.active.find_by(id: users(:david).id)
end end
test "non-admins cannot perform actions" do test "non-admins cannot perform actions" do
sign_in_as :jz sign_in_as :jz
put account_user_url(users(:david)), params: { user: { role: "admin" } } put user_url(users(:david)), params: { user: { role: "admin" } }
assert_response :forbidden assert_response :forbidden
delete account_user_url(users(:david)) delete user_url(users(:david))
assert_response :forbidden assert_response :forbidden
end end
end end
@@ -43,7 +43,7 @@ class CollectionsControllerTest < ActionDispatch::IntegrationTest
test "update all access" do test "update all access" do
collection = Current.set(session: sessions(:kevin)) do collection = Current.set(session: sessions(:kevin)) do
accounts("37s").collections.create! name: "New collection", all_access: false Collection.create! name: "New collection", all_access: false
end end
assert_equal [ users(:kevin) ], collection.users assert_equal [ users(:kevin) ], collection.users
@@ -51,7 +51,7 @@ class CollectionsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to cards_path(collection_ids: [ collection ]) assert_redirected_to cards_path(collection_ids: [ collection ])
assert collection.reload.all_access? assert collection.reload.all_access?
assert_equal accounts("37s").users, collection.users assert_equal User.all, collection.users
end end
test "destroy" do test "destroy" do
-1
View File
@@ -1,6 +1,5 @@
writebook: writebook:
name: Writebook name: Writebook
account: 37s
creator: david creator: david
all_access: true all_access: true
workflow: qa workflow: qa
-2
View File
@@ -1,7 +1,5 @@
web: web:
account: 37s
title: web title: web
mobile: mobile:
account: 37s
title: mobile title: mobile
-3
View File
@@ -1,21 +1,18 @@
<% digest = BCrypt::Password.create("secret123456") %> <% digest = BCrypt::Password.create("secret123456") %>
david: david:
account: 37s
name: David name: David
email_address: david@37signals.com email_address: david@37signals.com
password_digest: <%= digest %> password_digest: <%= digest %>
role: member role: member
jz: jz:
account: 37s
name: JZ name: JZ
email_address: jz@37signals.com email_address: jz@37signals.com
password_digest: <%= digest %> password_digest: <%= digest %>
role: member role: member
kevin: kevin:
account: 37s
name: Kevin name: Kevin
email_address: kevin@37signals.com email_address: kevin@37signals.com
password_digest: <%= digest %> password_digest: <%= digest %>
-2
View File
@@ -1,7 +1,5 @@
qa: qa:
account: 37s
name: QA name: QA
on_call: on_call:
account: 37s
name: "On call" name: "On call"
@@ -1,8 +0,0 @@
require "test_helper"
class ClosureReasonsTest < ActiveSupport::TestCase
test "create defaults closure reasons on creation" do
account = Account.create! name: "Rails"
assert_equal Account::ClosureReasons::DEFAULT_LABELS, account.closure_reasons.pluck(:label)
end
end
+2 -2
View File
@@ -54,7 +54,7 @@ class CardTest < ActiveSupport::TestCase
end end
assert cards(:logo).tagged_with?(tags(:web)) assert cards(:logo).tagged_with?(tags(:web))
assert_difference %w[ cards(:logo).taggings.count accounts("37s").tags.count ], +1 do assert_difference %w[ cards(:logo).taggings.count Tag.count ], +1 do
cards(:logo).toggle_tag_with "prioritized" cards(:logo).toggle_tag_with "prioritized"
end end
assert_equal "prioritized", cards(:logo).taggings.last.tag.title assert_equal "prioritized", cards(:logo).taggings.last.tag.title
@@ -87,7 +87,7 @@ class CardTest < ActiveSupport::TestCase
end end
test "in collection" do test "in collection" do
new_collection = accounts("37s").collections.create! name: "New Collection", creator: users(:david) new_collection = Collection.create! name: "New Collection", creator: users(:david)
assert_equal cards(:logo, :shipping, :layout, :text), Card.in_collection(collections(:writebook)) assert_equal cards(:logo, :shipping, :layout, :text), Card.in_collection(collections(:writebook))
assert_empty Card.in_collection(new_collection) assert_empty Card.in_collection(new_collection)
end end
+4 -4
View File
@@ -16,18 +16,18 @@ class CollectionTest < ActiveSupport::TestCase
test "grants access to everyone after creation" do test "grants access to everyone after creation" do
collection = Current.set(session: sessions(:david)) do collection = Current.set(session: sessions(:david)) do
accounts("37s").collections.create! name: "New collection", all_access: true Collection.create! name: "New collection", all_access: true
end end
assert_equal accounts("37s").users, collection.users assert_equal User.all, collection.users
end end
test "grants access to everyone after update" do test "grants access to everyone after update" do
collection = Current.set(session: sessions(:david)) do collection = Current.set(session: sessions(:david)) do
accounts("37s").collections.create! name: "New collection" Collection.create! name: "New collection"
end end
assert_equal [ users(:david) ], collection.users assert_equal [ users(:david) ], collection.users
collection.update! all_access: true collection.update! all_access: true
assert_equal accounts("37s").users, collection.users.reload assert_equal User.all, collection.users.reload
end end
end end
+1 -1
View File
@@ -3,7 +3,7 @@ require "test_helper"
class FilterTest < ActiveSupport::TestCase class FilterTest < ActiveSupport::TestCase
test "cards" do test "cards" do
Current.set session: sessions(:david) do Current.set session: sessions(:david) do
@new_collection = accounts("37s").collections.create! name: "Inaccessible Collection" @new_collection = Collection.create! name: "Inaccessible Collection"
@new_card = @new_collection.cards.create! @new_card = @new_collection.cards.create!
@new_card.update!(stage: workflow_stages(:qa_triage)) @new_card.update!(stage: workflow_stages(:qa_triage))
+1 -1
View File
@@ -7,7 +7,7 @@ class NotifierTest < ActiveSupport::TestCase
test "generate does not create notifications if the event was system-generated" do test "generate does not create notifications if the event was system-generated" do
cards(:logo).drafted! cards(:logo).drafted!
events(:logo_published).update!(creator: accounts("37s").users.system) events(:logo_published).update!(creator: User.system)
assert_no_difference -> { Notification.count } do assert_no_difference -> { Notification.count } do
Notifier.for(events(:logo_published)).generate Notifier.for(events(:logo_published)).generate
+1 -15
View File
@@ -1,21 +1,7 @@
require "test_helper" require "test_helper"
class TagTest < ActiveSupport::TestCase class TagTest < ActiveSupport::TestCase
setup do
@account = accounts("37s")
end
test "creating or deleting a tag touches the account, so tags dialog fragment cache is invalidated" do
assert_changes -> { @account.reload.updated_at } do
@account.tags.create!(title: "ReleaseBlocker")
end
assert_changes -> { @account.reload.updated_at } do
@account.tags.find_by(title: "ReleaseBlocker").destroy
end
end
test "downcase title" do test "downcase title" do
assert_equal "a tag", @account.tags.create!(title: "A TAG").title assert_equal "a tag", Tag.create!(title: "A TAG").title
end end
end end
+1 -4
View File
@@ -3,19 +3,16 @@ require "test_helper"
class UserTest < ActiveSupport::TestCase class UserTest < ActiveSupport::TestCase
test "create" do test "create" do
user = User.create! \ user = User.create! \
account: accounts("37s"),
role: "member", role: "member",
name: "Victor Cooper", name: "Victor Cooper",
email_address: "victor@hey.com", email_address: "victor@hey.com",
password: "secret123456" password: "secret123456"
assert_equal accounts("37s"), user.account
assert_equal user, User.authenticate_by(email_address: "victor@hey.com", password: "secret123456") assert_equal user, User.authenticate_by(email_address: "victor@hey.com", password: "secret123456")
end end
test "creation gives access to all_access collections" do test "creation gives access to all_access collections" do
user = User.create! \ user = User.create! \
account: accounts("37s"),
role: "member", role: "member",
name: "Victor Cooper", name: "Victor Cooper",
email_address: "victor@hey.com", email_address: "victor@hey.com",
@@ -37,7 +34,7 @@ class UserTest < ActiveSupport::TestCase
end end
test "system user" do test "system user" do
system_user = accounts("37s").users.system system_user = User.system
assert system_user.system? assert system_user.system?
assert_equal "System", system_user.name assert_equal "System", system_user.name