Rails 5.2.3 - Показать дерево категорий, упорядоченное по сумме их связанных записей - PullRequest
0 голосов
/ 25 сентября 2019

В настоящее время я использую камень Ancestry для визуализации более 500 категорий и подкатегорий (они могут достигать 3-х уровней глубиной).

Теперь, что я пытаюсьсделать это:

  1. Показать только те категории / подкатегории, с которыми связаны транзакции.
  2. Упорядочить эти категории по:
    • Сумма :amount изэти связанные транзакции.
    • А также упорядочить их по иерархии.
  3. Вывести сумму рядом с именем каждой категории.

Вот примериз того, чего я надеюсь достичь:

Travel = $1500
Travel > Air = $1000
Travel > Ground = $250
Business = $500
Business > Services = $250
Business > Services > Marketing = $75
# etc...

Вот как выглядят мои модели:

class Category < ApplicationRecord
  has_many :transactions
  has_ancestry
end

class Transaction < ApplicationRecord
  belongs_to :account
  belongs_to :category
end

Пока я смог ПОЧТИ добраться тудавыполнив:

# app/controllers/categories_controller.rb
def index
  # Get all of the root categories
  @primary_categories = Category.where(ancestry: nil)
end

# app/views/categories/index.html.erb
<% @primary_categories.each do |primary_category| %>
  <% primary_category_total = Transaction.where(account_id: current_user, category_id: primary_category.subtree).sum(:amount) %>

  <% if primary_category_total != 0.0 %> 
    <%= link_to primary_category.name, category_path(primary_category) %>
    <%= number_to_currency primary_category_total %>

    <% if primary_category.has_children? && primary_category_total != 0.0 %> 
      <% primary_category.children.each do |secondary_category| %>
        <% secondary_category_total = Transaction.where(account_id: current_user, category_id: primary_category.subtree).sum(:amount) %>

        <% if secondary_category_total != 0.0 %> 
          <%= link_to secondary_category.name, category_path(secondary_category) %>
          <%= number_to_currency secondary_category_total %>

          <% if secondary_category.has_children? && secondary_category_total != 0.0 %>
            <% secondary_category.children.each do |tertiary_category| %>
            <% tertiary_category_total = Transaction.where(account_id: current_user, category_id: primary_category.subtree).sum(:amount) %>

            <% if tertiary_category_total != 0.0 %> 
              <%= link_to tertiary_category.name, category_path(tertiary_category) %>
              <%= number_to_currency tertiary_category.transactions.sum(:amount) %>
# etc...

Но это генерирует безумное количество запросов, которые мучительно медленны, не говоря уже о том, что у меня теперь есть куча сложного кода на мой взгляд.И конечно, они не отсортированы по общему количеству.

Как еще мне подойти к этому?

Ответы [ 2 ]

0 голосов
/ 27 сентября 2019

После небольшой пробной ошибки (на основе первого ответа Джейкоба ) я придумал решение, которое работает значительно лучше, убрал сложность с контроллера и просмотрел и сделал все из моего списка требований из моегопервый пост.

Я все еще думаю, что есть место для оптимизации и очистки, но здесь это идет:

app / models /action.rb

  scope :by_user_category, ->(user, category) do 
    where(account: user.accounts, category: category.subtree)
  end

app / models / category.rb

  def balance(user)
    Transaction.by_user_category(user, self).sum(:amount)
  end

  def self.spending_by(user)
    categories_with_spending = []

    categories_depth_0 = Category.where(ancestry: nil) # Get the root categories
    categories_depth_0.each do |cat_depth_0|
      category_depth_0_balance = cat_depth_0.balance(user)

      if category_depth_0_balance < 0  # "Root category exists and has a balance"
        categories_depth_1_with_spending = []
        categories_depth_1 = Category.find_by_id(cat_depth_0).children # Get the sub-categories

        if !categories_depth_1.empty? # "Sub-category exists"
          categories_depth_1.each do |cat_depth_1|
            category_depth_1_balance = cat_depth_1.balance(user)

            if category_depth_1_balance < 0 # "Sub-category exists and has a balance"
              categories_depth_2_with_spending = []
              categories_depth_2 = Category.find_by_id(cat_depth_1).children

              if !categories_depth_2.empty? # Sub-category has child
                categories_depth_2.each do |cat_depth_2|
                  category_depth_2_balance = cat_depth_2.balance(user)

                  if category_depth_2_balance < 0  # Sub-category child has a balance
                    categories_depth_2_with_spending << {
                      category: cat_depth_2,
                      balance: category_depth_2_balance
                    }
                  end
                end
              end

              if categories_depth_2_with_spending != nil
                # Passing child sub-categories to parent sub-categories
                categories_depth_1_with_spending << {
                  category: cat_depth_1,
                  balance: category_depth_1_balance,
                  sub_categories: categories_depth_2_with_spending.sort_by { |c| c[:balance] }
                }
              end
            end
          end

          if categories_depth_1_with_spending != nil
            # Passing sub-categories to root category
            categories_with_spending << {
              category: cat_depth_0,
              balance: category_depth_0_balance,
              sub_categories: categories_depth_1_with_spending.sort_by { |c| c[:balance] }
            }
          end
        else
          # "Root exists but has no sub-categories"
          categories_with_spending << {
            category: cat_depth_0,
            balance: category_depth_0_balance
          }
        end
      end
    end

    return categories_with_spending.sort_by { |c| c[:balance] }
  end

app / controllers /ateg_controller.rb

  def index
    @categories = Category.spending_by(current_user)
  end

app / views / Categories / index.html.erb

  <% @categories.each do |cat_depth_0| %>
    <section class="app-card app-amount-summary app-categories__card">
      <%= render 'category_depth_0', category: cat_depth_0 %>

      <% if cat_depth_0[:sub_categories] %>
        <ul class="app-category-list">
          <% cat_depth_0[:sub_categories].each do |cat_depth_1| %>
            <%= render 'category_depth_1', category: cat_depth_1 %>
          <% end %>
        </ul>
      <% end %>
    </section>
  <% end %>

app / views / Categories / _category_depth_0.html.erb

<header class="app-card-header">
  <h3 class="app-card-header__h3">
    <%= link_to category[:category].name, category_path(category[:category].id) %>
  </h3>
  <p class="app-card-header__balance"><%= number_to_currency category[:balance] %></p>
</header>

_category_depth_1.html.erb работает точно так же, как _category_depth_0.html.erb, но с другой структурой, поэтому я пропустил ее в этом примере.

0 голосов
/ 25 сентября 2019

Caveat emptor:

Все, что ниже, должно рассматриваться как псевдокод.Я не проверял ничего из этого, я не пытаюсь сделать его готовым к копированию и вставке.Это всего лишь отправная точка, чтобы помочь вам запустить рефакторинг.

В вашей модели

Вы хотите получить что-то вроде Transaction.where(account_id: current_user, category_id: primary_category.subtree).sum(:amount) из ваших представлений.

Вы можете создать метод и / или область действия для возврата суммы транзакции.Например:

# app/models/transaction.rb
scope :by_user_category, ->(user, category) do 
  where(account: user, category: category.subtree)
end

# app/models/category.rb
def user_transaction_amount(user)
  Transaction.by_user_category(user, self).sum(:amount)
end

В вашем контроллере

Вы должны нетерпеливо загрузить детей.Вы знаете, что они вам понадобятся, так что сделайте все сразу.( с указанием причины, см. Ниже )

Вы упоминаете более 500 категорий, это разбито на страницы?Если это так, делайте это в партиях .

в вашем представлении

Обратите внимание на повторяющийся код?Попробуйте использовать частичный , чтобы высушить это.Например:

# app/views/categories/_category.html.erb
<% @categories.each do |category| %>
  <% category_total = category.user_transaction_amount(current_user) %>
  <% return if category_total == 0.0 %>
  <%= link_to category.name, category_path(category) %>
  <%= number_to_currency category_total %>
  <% if category.has_children? && category_total != 0.0 %> 
      <% category.children.each do |secondary_category| %>
        <%= render category, category: secondary_category %>
      <% end %>
  <% end %>
<% end %>


# app/views/categories/index.html.erb
<% @primary_categories.each do |primary_category| %>
  <% render 'categories', category: primary_category %>
<% end %>

Это все еще может использовать некоторые улучшения.Шаблоны представления должны быть в основном html-заявлениями, и это 100% ruby.Возможно, вы могли бы переместить все это в помощника или даже отрисовать непосредственно из действия контроллера.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...