0

I have an app where there are quotes from books with notes or thoughts on the quotes. I want to list out the quotes in order of the book order, chapter order and then page order.

Is there a way to move this to a scope within the model in order to keep the ActiveRelation?

like

class Quote
  scope :sorted, ->(order_of_books|book|) { where("reference_book = ?", book) }
end

I have code like the following in my Controller that does the job of ordering the quotes by book order.

# /app/controllers/quotes_controller.rb

def quotes
    @quotes = Quotes.all
    @sorted_quotes = []
        
    order_of_books.each do |book|
        @temp_array = []
        if @quotes.any? { |quote| quote[:reference_book] == book}
            @temp_array << @quotes.detect { |quote| quote[:reference_book] == book}
            # @temp_array.sort_by! { |quote| quote.reference_paragraph, quote.reference_sentence }
            @sorted_quotes << @temp_array
        end
    end
end

# /app/models/concerns/book_concern.rb

def order_of_books
   [ 
    "Book A",
    "Book B",
    "Book C",
   ]
end

This is the database table for reference.

# db/schema.rb
create_table "quotes", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "text", null: false
    t.string "reference_book", null: false
    t.integer "reference_chapter", null: false
    t.integer "reference_paragraph", null: false
    t.integer "reference_sentence", null: false
    t.string "image"
    t.text "notes"
end

Errors

The issue is now that I am trying to sort the quotes, all my other code is breaking when in my views when I try to call something like quote.image and I get this error:

undefined method `image' for [#<Quote id: 4, created_at: "2023-11-21 15:19:....

Side Note

The line in my controller where I try to sort_by! the paragraph and sentence isn't working so I just commented it out. Right now that isn't as important for me.

`

5
  • Is the "book order" meant to be static (hardcoded), dynamic (passed in by the user), or actually just alphabetical (i.e. there's really no need to be so explicit about the names of books)? Commented Nov 21, 2023 at 20:08
  • 1
    There's several things I could say about improving your current method, however, even if you were to improve the above method's implementation, it would't make any difference to the error you mentioned about calling quotes.image: Because it seems you're actually trying to call the method on a collection of quotes, rather than a single quote (despite the misleadingly named variable). Instead, you'd need to use something like quotes.each { |quote| .... quote.image .... } to iterate over the list. And thirdly, Commented Nov 21, 2023 at 20:09
  • @TomLord, The quote.image is on the view page for each individual quote where it shows the quote, the image that's associated and the note. There shouldn't be a need to iterate through the quote list for that. The time I am iterating through the quotes list is to sort the list by the static (hardcoded) book list to put the quotes in order of the book order; in my case the books are those in the Bible, and so I am following that order. Does that make sense? Commented Nov 21, 2023 at 20:51
  • Could you answer my question above about the ordering? I can solve all three versions, but the code for a simple alphabetical ordering is much more straightforward, so I'd rather not over-engineer it. Commented Nov 21, 2023 at 23:03
  • I am sorry, I thought that I had answered your question. The book order would be a static, hardcoded, order. Take for instance The Lord of the Rings and Prequel. The book order would be The Hobbit, The Fellowship of the Ring, The Two Towers, The Return of the King. Not alphabetical. Commented Nov 24, 2023 at 5:35

2 Answers 2

1

Assuming you may have multiple quotes per book (and will be adding more books over time - without wanting to add new lines to your code), you might want to normalise your data a little better by having a books table, storing the book sort field to capture the preferred sort order for the books (instead of that living in code directly);

# models/book.rb
class Book < ApplicationRecord
  validates :sort, presence: true
end

# models/quote.rb
class Quote < ApplicationRecord
  belongs_to :book
end

# db/schema.rb
create_table "books", force: :cascade do |t|
    t.string "title", null: false
    t.string "author"
    t.integer "sort", null: false
end
create_table "quotes", force: :cascade do |t|
    t.string "text", null: false
    t.bigint "book_id", null: false # < new column
    t.integer "reference_chapter", null: false
    t.integer "reference_paragraph", null: false
    t.integer "reference_sentence", null: false
    t.string "image"
    t.text "notes"
    # TODO: you will also want a foreign key constraint here for quote.book_id
end

Then you could use this relationship to sort quotes, passing multiple columns to the order method - i.e. sort by book.sort first, then by reference_chapter (for any quotes in the same book), then by reference_paragraph (for any quotes in the same chapter) etc. As a best practice, I would suggest using quotes.id as a final disambiguator in case there were multiple quotes from the same book+chapter+paragraph+sentence.

# models/quote.rb
class Quote < ApplicationRecord
  scope :sorted, -> { joins(:book).order('books.sort ASC, reference_chapter ASC, reference_paragraph ASC, reference_sentence ASC, id ASC') }
end
Sign up to request clarification or add additional context in comments.

Comments

0

I found a workaround. It does not answer my initial question of creating a scope, but it does solve the issue of my code not working for instances like quote.image

Solution

In my controller when I coded this:

@temp_array << @quotes.select { |quote| quote[:reference_book] == book}
@sorted_quotes << @temp_array

it would put the ActiveRelation inside an array element and then save it to the @temp_array; this is because select pulls all of the instances out of one array that matches the search terms. So, I just needed to undo this by iterating through the @temp_array and adding each element to the @sorted_quotes instead of adding the entire thing.

Here are my changes (also changing some variables to not be 'global' as that's just not needed in this situation.

def quotes
    quotes = Quote.all
    @sorted_quotes = []
    
    order_of_books.each do |book|
        temp_array = []
        if quotes.any? { |quote| quote[:reference_book] == book}
            temp_array << quotes.select { |quote| quote[:reference_book] == book}
            temp_array[0].each do |v|
                @sorted_quotes << v
            end
        end
    end
end

This fixed the issue. I can leave the question open as this solution does not technically answer the initial question about making a scope to do this same thing.

2 Comments

In my controller when I coded this: @temp_array << @quotes.select { |quote| quote[:reference_book] == book} -- that's not the same code you shared in your original question 😈
You are correct, I had originally posted with the detect in my OP which only pulls the first match. I forgot that I found that bug and fixed it before finding this workaround. 😉

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.