Skip to content

Commit

Permalink
Nested Categories (#1561)
Browse files Browse the repository at this point in the history
* Prepare entry search for nested categories

* Subcategory implementation

* Remove caching for test stability
  • Loading branch information
zachgoll authored Dec 20, 2024
1 parent a4d1009 commit 77def1d
Show file tree
Hide file tree
Showing 31 changed files with 298 additions and 235 deletions.
17 changes: 13 additions & 4 deletions app/controllers/categories_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,29 @@ def index

def new
@category = Current.family.categories.new color: Category::COLORS.sample
@categories = Current.family.categories.alphabetically.where(parent_id: nil).where.not(id: @category.id)
end

def create
@category = Current.family.categories.new(category_params)

if @category.save
@transaction.update(category_id: @category.id) if @transaction
redirect_back_or_to transactions_path, notice: t(".success")

redirect_back_or_to categories_path, notice: t(".success")
else
redirect_back_or_to transactions_path, alert: t(".failure", error: @category.errors.full_messages.to_sentence)
render :new, status: :unprocessable_entity
end
end

def edit
@categories = Current.family.categories.alphabetically.where(parent_id: nil).where.not(id: @category.id)
end

def update
@category.update! category_params

redirect_back_or_to transactions_path, notice: t(".success")
redirect_back_or_to categories_path, notice: t(".success")
end

def destroy
Expand All @@ -38,6 +41,12 @@ def destroy
redirect_back_or_to categories_path, notice: t(".success")
end

def bootstrap
Current.family.categories.bootstrap_defaults

redirect_back_or_to categories_path, notice: t(".success")
end

private
def set_category
@category = Current.family.categories.find(params[:id])
Expand All @@ -50,6 +59,6 @@ def set_transaction
end

def category_params
params.require(:category).permit(:name, :color)
params.require(:category).permit(:name, :color, :parent_id)
end
end
1 change: 0 additions & 1 deletion app/controllers/registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ def create

if @user.save
@invitation&.update!(accepted_at: Time.current)
Category.create_default_categories(@user.family) unless @invitation
@session = create_session_for(@user)
redirect_to root_path, notice: t(".success")
else
Expand Down
10 changes: 5 additions & 5 deletions app/controllers/transactions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ class TransactionsController < ApplicationController

def index
@q = search_params
result = Current.family.entries.account_transactions.search(@q).reverse_chronological
@pagy, @transaction_entries = pagy(result, limit: params[:per_page] || "50")
search_query = Current.family.transactions.search(@q).includes(:entryable).reverse_chronological
@pagy, @transaction_entries = pagy(search_query, limit: params[:per_page] || "50")

@totals = {
count: result.select { |t| t.currency == Current.family.currency }.count,
income: result.income_total(Current.family.currency).abs,
expense: result.expense_total(Current.family.currency)
count: search_query.select { |t| t.currency == Current.family.currency }.count,
income: search_query.income_total(Current.family.currency).abs,
expense: search_query.expense_total(Current.family.currency)
}
end

Expand Down
46 changes: 4 additions & 42 deletions app/models/account/entry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ def display_name
end

class << self
def search(params)
Account::EntrySearch.new(params).build_query(all)
end

# arbitrary cutoff date to avoid expensive sync operations
def min_supported_date
30.years.ago.to_date
Expand Down Expand Up @@ -141,49 +145,7 @@ def expense_total(currency = "USD")
Money.new(total, currency)
end

def search(params)
query = all
query = query.where("account_entries.name ILIKE ?", "%#{sanitize_sql_like(params[:search])}%") if params[:search].present?
query = query.where("account_entries.date >= ?", params[:start_date]) if params[:start_date].present?
query = query.where("account_entries.date <= ?", params[:end_date]) if params[:end_date].present?

if params[:types].present?
query = query.where(marked_as_transfer: false) unless params[:types].include?("transfer")

if params[:types].include?("income") && !params[:types].include?("expense")
query = query.where("account_entries.amount < 0")
elsif params[:types].include?("expense") && !params[:types].include?("income")
query = query.where("account_entries.amount >= 0")
end
end

if params[:amount].present? && params[:amount_operator].present?
case params[:amount_operator]
when "equal"
query = query.where("ABS(ABS(account_entries.amount) - ?) <= 0.01", params[:amount].to_f.abs)
when "less"
query = query.where("ABS(account_entries.amount) < ?", params[:amount].to_f.abs)
when "greater"
query = query.where("ABS(account_entries.amount) > ?", params[:amount].to_f.abs)
end
end

if params[:accounts].present? || params[:account_ids].present?
query = query.joins(:account)
end

query = query.where(accounts: { name: params[:accounts] }) if params[:accounts].present?
query = query.where(accounts: { id: params[:account_ids] }) if params[:account_ids].present?

# Search attributes on each entryable to further refine results
entryable_ids = entryable_search(params)
query = query.where(entryable_id: entryable_ids) unless entryable_ids.nil?

query
end

private

def entryable_search(params)
entryable_ids = []
entryable_search_performed = false
Expand Down
59 changes: 59 additions & 0 deletions app/models/account/entry_search.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
class Account::EntrySearch
include ActiveModel::Model
include ActiveModel::Attributes

attribute :search, :string
attribute :amount, :string
attribute :amount_operator, :string
attribute :types, :string
attribute :accounts, :string
attribute :account_ids, :string
attribute :start_date, :string
attribute :end_date, :string

class << self
def from_entryable_search(entryable_search)
new(entryable_search.attributes.slice(*attribute_names))
end
end

def build_query(scope)
query = scope

query = query.where("account_entries.name ILIKE :search OR account_entries.enriched_name ILIKE :search",
search: "%#{ActiveRecord::Base.sanitize_sql_like(search)}%"
) if search.present?
query = query.where("account_entries.date >= ?", start_date) if start_date.present?
query = query.where("account_entries.date <= ?", end_date) if end_date.present?

if types.present?
query = query.where(marked_as_transfer: false) unless types.include?("transfer")

if types.include?("income") && !types.include?("expense")
query = query.where("account_entries.amount < 0")
elsif types.include?("expense") && !types.include?("income")
query = query.where("account_entries.amount >= 0")
end
end

if amount.present? && amount_operator.present?
case amount_operator
when "equal"
query = query.where("ABS(ABS(account_entries.amount) - ?) <= 0.01", amount.to_f.abs)
when "less"
query = query.where("ABS(account_entries.amount) < ?", amount.to_f.abs)
when "greater"
query = query.where("ABS(account_entries.amount) > ?", amount.to_f.abs)
end
end

if accounts.present? || account_ids.present?
query = query.joins(:account)
end

query = query.where(accounts: { name: accounts }) if accounts.present?
query = query.where(accounts: { id: account_ids }) if account_ids.present?

query
end
end
20 changes: 1 addition & 19 deletions app/models/account/trade.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,8 @@ class Account::Trade < ApplicationRecord
validates :qty, presence: true
validates :price, :currency, presence: true

class << self
def search(_params)
all
end

def requires_search?(_params)
false
end
end

def sell?
qty < 0
end

def buy?
qty > 0
end

def unrealized_gain_loss
return nil if sell?
return nil if qty.negative?
current_price = security.current_price
return nil if current_price.nil?

Expand Down
47 changes: 1 addition & 46 deletions app/models/account/transaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,52 +12,7 @@ class Account::Transaction < ApplicationRecord

class << self
def search(params)
query = all
if params[:categories].present?
if params[:categories].exclude?("Uncategorized")
query = query
.joins(:category)
.where(categories: { name: params[:categories] })
else
query = query
.left_joins(:category)
.where(categories: { name: params[:categories] })
.or(query.where(category_id: nil))
end
end

query = query.joins(:merchant).where(merchants: { name: params[:merchants] }) if params[:merchants].present?

if params[:tags].present?
query = query.joins(:tags)
.where(tags: { name: params[:tags] })
.distinct
end

query
Account::TransactionSearch.new(params).build_query(all)
end

def requires_search?(params)
searchable_keys.any? { |key| params.key?(key) }
end

private

def searchable_keys
%i[categories merchants tags]
end
end

def eod_balance
entry.amount_money
end

private
def account
entry.account
end

def daily_transactions
account.entries.account_transactions
end
end
42 changes: 42 additions & 0 deletions app/models/account/transaction_search.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
class Account::TransactionSearch
include ActiveModel::Model
include ActiveModel::Attributes

attribute :search, :string
attribute :amount, :string
attribute :amount_operator, :string
attribute :types, array: true
attribute :accounts, array: true
attribute :account_ids, array: true
attribute :start_date, :string
attribute :end_date, :string
attribute :categories, array: true
attribute :merchants, array: true
attribute :tags, array: true

# Returns array of Account::Entry objects to stay consistent with partials, which only deal with Account::Entry
def build_query(scope)
query = scope

if categories.present?
if categories.exclude?("Uncategorized")
query = query
.joins(:category)
.where(categories: { name: categories })
else
query = query
.left_joins(:category)
.where(categories: { name: categories })
.or(query.where(category_id: nil))
end
end

query = query.joins(:merchant).where(merchants: { name: merchants }) if merchants.present?

query = query.joins(:tags).where(tags: { name: tags }) if tags.present?

entries_scope = Account::Entry.account_transactions.where(entryable_id: query.select(:id))

Account::EntrySearch.from_entryable_search(self).build_query(entries_scope)
end
end
10 changes: 0 additions & 10 deletions app/models/account/valuation.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,3 @@
class Account::Valuation < ApplicationRecord
include Account::Entryable

class << self
def search(_params)
all
end

def requires_search?(_params)
false
end
end
end
Loading

0 comments on commit 77def1d

Please sign in to comment.