book_reading_tracker_gem 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/config/database_connection.rb +51 -0
- data/db/book_reading_tracker_gem.rake.rb +122 -0
- data/db/migrate/20250326100001_create_books.rb +14 -0
- data/db/migrate/20250326100002_create_authors.rb +12 -0
- data/db/migrate/20250326100003_create_book_authors.rb +11 -0
- data/db/migrate/20250326100004_create_categories.rb +11 -0
- data/db/migrate/20250326100005_create_book_categories.rb +11 -0
- data/db/migrate/20250326100006_create_reading_progresses.rb +15 -0
- data/db/seeds.rb +38 -0
- data/exe/book_reading_tracker_gem +5 -0
- data/lib/book_reading_tracker_gem/clis/cli.rb +93 -0
- data/lib/book_reading_tracker_gem/models/author.rb +7 -0
- data/lib/book_reading_tracker_gem/models/book.rb +12 -0
- data/lib/book_reading_tracker_gem/models/book_author.rb +7 -0
- data/lib/book_reading_tracker_gem/models/book_category.rb +7 -0
- data/lib/book_reading_tracker_gem/models/category.rb +7 -0
- data/lib/book_reading_tracker_gem/models/reading_progress.rb +6 -0
- data/lib/book_reading_tracker_gem/services/author_service.rb +45 -0
- data/lib/book_reading_tracker_gem/services/book_service.rb +208 -0
- data/lib/book_reading_tracker_gem/services/category_service.rb +44 -0
- data/lib/book_reading_tracker_gem/utils/helpers.rb +14 -0
- data/lib/book_reading_tracker_gem/version.rb +5 -0
- data/lib/book_reading_tracker_gem.rb +18 -0
- metadata +71 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7f25d1c0dfc83b1badfa94890012e70c1d9001603cab9a6a753bef8b456f848e
|
4
|
+
data.tar.gz: d6c826406fb592038d48f1e404b9d3bfafbc93ebbd3f6c532fd712bfb3223968
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b59cfb93ae0890836b20aef5cfc854b5b2a97eee9c7b628105c2609f08c156543b80fa376bad2e3e64b134e319f8e4e5200feb53d29facbf16fe006974b004b2
|
7
|
+
data.tar.gz: 1f5b1608bc853e24ce88ada7d0d95ba3b7400950f1b3e22adddcc600199fe5d1683f63eff03e668f3d73720195aa11d5982d45e522dca61e0d6ddd9fc8ef7635
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
require 'dotenv/load'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
class DatabaseConnection
|
8
|
+
def self.connect
|
9
|
+
# Kiểm tra xem đã connect chưa để tránh connect lại
|
10
|
+
return if ActiveRecord::Base.connected?
|
11
|
+
|
12
|
+
database_mode = ENV.fetch('DATABASE_MODE', 'supabase')
|
13
|
+
|
14
|
+
database_url = case database_mode
|
15
|
+
when 'supabase'
|
16
|
+
ENV.fetch('DATABASE_URL_SUPABASE',
|
17
|
+
'postgresql://postgres.rfvveqejqtxfszsgdzml:Admin123%40@aws-0-ap-southeast-1.pooler.supabase.com:6543/postgres')
|
18
|
+
when 'local'
|
19
|
+
ENV.fetch('DATABASE_URL_LOCAL',
|
20
|
+
'postgresql://vantrong:Admin123%40@localhost:5432/book_reading_tracker')
|
21
|
+
end
|
22
|
+
|
23
|
+
if database_url.nil?
|
24
|
+
puts "DATABASE_URL not found for mode #{database_mode}"
|
25
|
+
return
|
26
|
+
end
|
27
|
+
|
28
|
+
begin
|
29
|
+
ActiveRecord::Base.establish_connection(database_url)
|
30
|
+
ActiveRecord::Base.logger = Logger.new($stdout)
|
31
|
+
puts "Successfully connected to the database in #{database_mode} mode!"
|
32
|
+
rescue StandardError => e
|
33
|
+
puts "Failed to connect to the database: #{e.message}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.disconnect
|
38
|
+
return unless ActiveRecord::Base.connected?
|
39
|
+
|
40
|
+
begin
|
41
|
+
ActiveRecord::Base.connection_pool.with_connection do |conn|
|
42
|
+
# Clear prepared statements ( để tranh lỗi khi chạy câu lệnh này mà tiếp tục chạy lệnh khác)
|
43
|
+
conn.execute('DISCARD ALL;')
|
44
|
+
end
|
45
|
+
ActiveRecord::Base.connection_pool.disconnect!
|
46
|
+
puts 'Successfully disconnected from the database!'
|
47
|
+
rescue StandardError => e
|
48
|
+
puts "Error while disconnecting: #{e.message}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
require 'dotenv/load'
|
5
|
+
require 'logger'
|
6
|
+
require 'pg'
|
7
|
+
require_relative '../config/database_connection'
|
8
|
+
require_relative '../db/seeds'
|
9
|
+
|
10
|
+
module BookReadingTrackerGem
|
11
|
+
class DatabaseTasks
|
12
|
+
def initialize
|
13
|
+
@db_name = ENV.fetch('DATABASE_NAME', nil)
|
14
|
+
@database_mode = ENV.fetch('DATABASE_MODE', 'local')
|
15
|
+
@database_url = if @database_mode == 'supabase'
|
16
|
+
ENV.fetch('DATABASE_URL_SUPABASE',
|
17
|
+
'postgresql://postgres.rfvveqejqtxfszsgdzml:Admin123%40@aws-0-ap-southeast-1.pooler.supabase.com:6543/postgres')
|
18
|
+
else
|
19
|
+
ENV.fetch('DATABASE_URL_LOCAL',
|
20
|
+
'postgresql://vantrong:Admin123%40@localhost:5432/book_reading_tracker')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def connect
|
25
|
+
DatabaseConnection.connect
|
26
|
+
end
|
27
|
+
|
28
|
+
def migration_context
|
29
|
+
migrations_paths = File.expand_path('../db/migrate', __dir__)
|
30
|
+
@migration_context ||= ActiveRecord::MigrationContext.new(migrations_paths)
|
31
|
+
end
|
32
|
+
|
33
|
+
def create
|
34
|
+
connect
|
35
|
+
|
36
|
+
begin
|
37
|
+
connection = PG.connect(dbname: 'postgres')
|
38
|
+
if database_exists?(connection, @db_name)
|
39
|
+
puts "Lỗi: Database '#{@db_name}' đã tồn tại."
|
40
|
+
else
|
41
|
+
connection.exec("CREATE DATABASE #{@db_name}")
|
42
|
+
puts "Đã tạo database: #{@db_name} thành công."
|
43
|
+
end
|
44
|
+
ensure
|
45
|
+
connection&.close
|
46
|
+
end
|
47
|
+
rescue StandardError => e
|
48
|
+
puts "Lỗi khi tạo database: #{e.message}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def database_exists?(connection, db_name)
|
52
|
+
result = connection.exec("SELECT 1 FROM pg_database WHERE datname = '#{db_name}'")
|
53
|
+
result.ntuples.positive?
|
54
|
+
end
|
55
|
+
|
56
|
+
def drop
|
57
|
+
begin
|
58
|
+
connection = PG.connect(dbname: 'postgres')
|
59
|
+
connection.exec("SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '#{@db_name}' AND pid <> pg_backend_pid();")
|
60
|
+
|
61
|
+
if database_exists?(connection, @db_name)
|
62
|
+
connection.exec("DROP DATABASE #{@db_name}")
|
63
|
+
puts "Đã xóa database: #{@db_name}."
|
64
|
+
else
|
65
|
+
puts "Lỗi khi xóa database: Database #{@db_name} không tồn tại."
|
66
|
+
end
|
67
|
+
ensure
|
68
|
+
connection&.close
|
69
|
+
end
|
70
|
+
rescue StandardError => e
|
71
|
+
puts "Lỗi khi xóa database: #{e.message}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def migrate
|
75
|
+
connect
|
76
|
+
|
77
|
+
begin
|
78
|
+
connection = PG.connect(@database_url)
|
79
|
+
unless database_exists?(connection, @db_name)
|
80
|
+
puts "Lỗi: Database '#{@db_name}' không tồn tại."
|
81
|
+
return
|
82
|
+
end
|
83
|
+
|
84
|
+
migration_context.migrate
|
85
|
+
puts 'Migrate thành công.'
|
86
|
+
ensure
|
87
|
+
connection&.close
|
88
|
+
end
|
89
|
+
rescue StandardError => e
|
90
|
+
puts "Lỗi khi migrate: #{e.message}"
|
91
|
+
end
|
92
|
+
|
93
|
+
def reset
|
94
|
+
drop
|
95
|
+
create
|
96
|
+
migrate
|
97
|
+
seed
|
98
|
+
puts 'Reset database thành công.'
|
99
|
+
rescue StandardError => e
|
100
|
+
puts "Lỗi khi reset database: #{e.message}"
|
101
|
+
end
|
102
|
+
|
103
|
+
def seed
|
104
|
+
connect
|
105
|
+
|
106
|
+
begin
|
107
|
+
connection = PG.connect(@database_url)
|
108
|
+
unless database_exists?(connection, @db_name)
|
109
|
+
puts "Lỗi: Database '#{@db_name}' không tồn tại."
|
110
|
+
return
|
111
|
+
end
|
112
|
+
|
113
|
+
Seeder.run
|
114
|
+
puts 'Seed dữ liệu thành công.'
|
115
|
+
ensure
|
116
|
+
connection&.close
|
117
|
+
end
|
118
|
+
rescue StandardError => e
|
119
|
+
puts "Lỗi khi seed dữ liệu: #{e.message}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateBooks < ActiveRecord::Migration[7.0]
|
4
|
+
def change
|
5
|
+
create_table :books do |t|
|
6
|
+
t.string :title, null: false
|
7
|
+
t.text :description
|
8
|
+
t.string :isbn
|
9
|
+
t.integer :published_year
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
add_index :books, :title
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateBookAuthors < ActiveRecord::Migration[7.0]
|
4
|
+
def change
|
5
|
+
create_table :book_authors do |t|
|
6
|
+
t.references :book, null: false, foreign_key: true
|
7
|
+
t.references :author, null: false, foreign_key: true
|
8
|
+
t.index %i[book_id author_id], unique: true
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateBookCategories < ActiveRecord::Migration[7.0]
|
4
|
+
def change
|
5
|
+
create_table :book_categories do |t|
|
6
|
+
t.references :book, null: false, foreign_key: true
|
7
|
+
t.references :category, null: false, foreign_key: true
|
8
|
+
t.index %i[book_id category_id], unique: true
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateReadingProgresses < ActiveRecord::Migration[7.0]
|
4
|
+
def change
|
5
|
+
create_table :reading_progresses do |t|
|
6
|
+
t.references :book, null: false, foreign_key: true
|
7
|
+
t.string :status, null: false, default: 'unread'
|
8
|
+
t.integer :pages_read, default: 0
|
9
|
+
t.integer :total_pages, default: 0
|
10
|
+
t.date :started_at
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
add_index :reading_progresses, :status
|
14
|
+
end
|
15
|
+
end
|
data/db/seeds.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../lib/book_reading_tracker_gem/models/book'
|
4
|
+
require_relative '../lib/book_reading_tracker_gem/models/author'
|
5
|
+
require_relative '../lib/book_reading_tracker_gem/models/book_author'
|
6
|
+
require_relative '../lib/book_reading_tracker_gem/models/category'
|
7
|
+
require_relative '../lib/book_reading_tracker_gem/models/book_category'
|
8
|
+
require_relative '../lib/book_reading_tracker_gem/models/reading_progress'
|
9
|
+
|
10
|
+
class Seeder
|
11
|
+
def self.run
|
12
|
+
dependencies = [ReadingProgress, BookCategory, BookAuthor, Category, Author, Book]
|
13
|
+
dependencies.each(&:destroy_all)
|
14
|
+
|
15
|
+
book1 = Book.create!(title: 'Ruby Basics', description: 'Learn Ruby', isbn: '1234567890', published_year: 2021)
|
16
|
+
book2 = Book.create!(title: 'Rails Advanced', description: 'Mastering Rails', isbn: '0987654321',
|
17
|
+
published_year: 2022)
|
18
|
+
|
19
|
+
author1 = Author.create!(author_name: 'John Doe', biography: 'Ruby developer')
|
20
|
+
author2 = Author.create!(author_name: 'Jane Smith', biography: 'Rails expert')
|
21
|
+
|
22
|
+
category1 = Category.create!(category_name: 'Programming')
|
23
|
+
category2 = Category.create!(category_name: 'Web Development')
|
24
|
+
|
25
|
+
BookAuthor.create!(book: book1, author: author1)
|
26
|
+
BookAuthor.create!(book: book1, author: author2)
|
27
|
+
BookAuthor.create!(book: book2, author: author2)
|
28
|
+
|
29
|
+
BookCategory.create!(book: book1, category: category1)
|
30
|
+
BookCategory.create!(book: book2, category: category2)
|
31
|
+
|
32
|
+
ReadingProgress.create!(book: book1, status: 'reading', pages_read: 50, total_pages: 200,
|
33
|
+
started_at: Date.today - 7)
|
34
|
+
ReadingProgress.create!(book: book2, status: 'unread', pages_read: 0, total_pages: 300)
|
35
|
+
|
36
|
+
puts 'Đã seed dữ liệu thành công!'
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
require_relative '../services/book_service'
|
5
|
+
require_relative '../services/author_service'
|
6
|
+
require_relative '../services/category_service'
|
7
|
+
|
8
|
+
module BookReadingTrackerGem
|
9
|
+
class CLI < Thor
|
10
|
+
# Quản lý sách
|
11
|
+
desc 'add_book TITLE --author AUTHOR --pages PAGES [--description DESCRIPTION] [--isbn ISBN] [--published_year YEAR]',
|
12
|
+
'Thêm sách mới vào danh sách'
|
13
|
+
option :author, required: true
|
14
|
+
option :pages, required: true, type: :numeric
|
15
|
+
option :description, required: false
|
16
|
+
option :isbn, required: false
|
17
|
+
option :published_year, required: false, type: :numeric
|
18
|
+
def add_book(title)
|
19
|
+
# ruby bin/book add_book "Ruby Programming" --author "David Heinemeier Hansson" --pages 300 --description "Learn Ruby with Rails" --isbn "978-0134596882" --published_year 2023
|
20
|
+
BookService.add_book(
|
21
|
+
title,
|
22
|
+
options[:author],
|
23
|
+
options[:pages],
|
24
|
+
options[:description],
|
25
|
+
options[:isbn],
|
26
|
+
options[:published_year]
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
desc 'remove_book ID', 'Xóa sách khỏi danh sách'
|
31
|
+
# ruby bin/book remove_book 1
|
32
|
+
def remove_book(id)
|
33
|
+
BookService.remove_book(id)
|
34
|
+
end
|
35
|
+
|
36
|
+
desc 'progress_book ID --page PAGE', 'Cập nhật tiến độ đọc sách'
|
37
|
+
option :page, required: true, type: :numeric
|
38
|
+
def progress_book(id)
|
39
|
+
# ruby bin/book progress_book 2 --page 150
|
40
|
+
BookService.update_progress(id, options[:page])
|
41
|
+
end
|
42
|
+
|
43
|
+
desc 'list_books', 'Hiển thị danh sách sách'
|
44
|
+
# ruby bin/book list_books
|
45
|
+
def list_books
|
46
|
+
BookService.list_books
|
47
|
+
end
|
48
|
+
|
49
|
+
desc 'stats', 'Hiển thị thống kê sách'
|
50
|
+
# ruby bin/book stats
|
51
|
+
def stats
|
52
|
+
BookService.stats
|
53
|
+
end
|
54
|
+
|
55
|
+
desc 'show_progress ID', 'Xem tiến độ đọc sách'
|
56
|
+
# ruby bin/book show_progress "Ruby Programming"
|
57
|
+
def show_progress(id)
|
58
|
+
BookService.show_progress(id)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Quản lý tác giả
|
62
|
+
desc 'add_author NAME [--biography BIOGRAPHY]', 'Thêm tác giả mới'
|
63
|
+
option :biography, required: false
|
64
|
+
def add_author(name)
|
65
|
+
# ruby bin/book add_author "J.K. Rowling" --biography "Tác giả của Harry Potter"
|
66
|
+
AuthorService.add_author(name, options[:biography])
|
67
|
+
end
|
68
|
+
|
69
|
+
desc 'list_authors', 'Hiển thị danh sách tác giả'
|
70
|
+
def list_authors
|
71
|
+
# ruby bin/book list_authors
|
72
|
+
AuthorService.list_authors
|
73
|
+
end
|
74
|
+
|
75
|
+
# Quản lý danh mục
|
76
|
+
desc 'add_category NAME', 'Thêm danh mục mới'
|
77
|
+
def add_category(name)
|
78
|
+
# ruby bin/book add_category "Fiction"
|
79
|
+
CategoryService.add_category(name)
|
80
|
+
end
|
81
|
+
|
82
|
+
desc 'list_categories', 'Hiển thị danh sách danh mục'
|
83
|
+
# ruby bin/book list_categories
|
84
|
+
def list_categories
|
85
|
+
CategoryService.list_categories
|
86
|
+
end
|
87
|
+
|
88
|
+
# Override Thor's error handling for invalid options
|
89
|
+
def self.exit_on_failure?
|
90
|
+
true
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
class Book < ActiveRecord::Base
|
5
|
+
has_many :book_authors
|
6
|
+
has_many :authors, through: :book_authors
|
7
|
+
|
8
|
+
has_many :book_categories
|
9
|
+
has_many :categories, through: :book_categories
|
10
|
+
|
11
|
+
has_one :reading_progress
|
12
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../models/author'
|
4
|
+
require_relative '../../../config/database_connection'
|
5
|
+
require_relative '../utils/helpers'
|
6
|
+
|
7
|
+
module BookReadingTrackerGem
|
8
|
+
class AuthorService
|
9
|
+
def self.add_author(name, biography = nil)
|
10
|
+
DatabaseConnection.connect
|
11
|
+
Author.create!(author_name: name, biography: biography)
|
12
|
+
puts "Add author: #{name} successfully."
|
13
|
+
rescue StandardError => e
|
14
|
+
puts "Error adding author: #{e.message}"
|
15
|
+
ensure
|
16
|
+
DatabaseConnection.disconnect
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.list_authors
|
20
|
+
DatabaseConnection.connect
|
21
|
+
authors = Author.all
|
22
|
+
|
23
|
+
if authors.empty?
|
24
|
+
puts 'Authors not found.'
|
25
|
+
return
|
26
|
+
end
|
27
|
+
|
28
|
+
header = %w[id author_name biography]
|
29
|
+
|
30
|
+
rows = authors.map do |author|
|
31
|
+
[
|
32
|
+
author.id,
|
33
|
+
author.author_name || 'N/A',
|
34
|
+
author.biography || 'N/A'
|
35
|
+
]
|
36
|
+
end
|
37
|
+
|
38
|
+
CommonUtils.render_table(header, rows)
|
39
|
+
rescue StandardError => e
|
40
|
+
puts "Error listing authors: #{e.message}"
|
41
|
+
ensure
|
42
|
+
DatabaseConnection.disconnect
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../models/book'
|
4
|
+
require_relative '../models/reading_progress'
|
5
|
+
require_relative '../../../config/database_connection'
|
6
|
+
require_relative '../utils/helpers'
|
7
|
+
module BookReadingTrackerGem
|
8
|
+
class BookService
|
9
|
+
def self.add_book(title, author, total_pages, description = nil, isbn = nil, published_year = nil)
|
10
|
+
DatabaseConnection.connect
|
11
|
+
book = Book.create!(
|
12
|
+
title: title,
|
13
|
+
description: description,
|
14
|
+
isbn: isbn,
|
15
|
+
published_year: published_year
|
16
|
+
)
|
17
|
+
book.authors.create!(author_name: author)
|
18
|
+
book.create_reading_progress!(total_pages: total_pages)
|
19
|
+
puts "Add book: #{book.title} successfully"
|
20
|
+
rescue StandardError => e
|
21
|
+
puts "Error adding book: #{e.message}"
|
22
|
+
ensure
|
23
|
+
DatabaseConnection.disconnect
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.remove_book(id)
|
27
|
+
DatabaseConnection.connect
|
28
|
+
book = Book.find_by(id: id)
|
29
|
+
if book
|
30
|
+
book.reading_progress&.destroy
|
31
|
+
book.book_authors.destroy_all
|
32
|
+
book.destroy
|
33
|
+
puts "Removed book: #{id}."
|
34
|
+
else
|
35
|
+
puts "Book not found: #{id}."
|
36
|
+
end
|
37
|
+
rescue StandardError => e
|
38
|
+
puts "Error removing book: #{e.message}"
|
39
|
+
ensure
|
40
|
+
DatabaseConnection.disconnect
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.update_status(id, status)
|
44
|
+
valid_statuses = %w[unread reading read]
|
45
|
+
unless valid_statuses.include?(status)
|
46
|
+
puts "Invalid status. Accepted: #{valid_statuses.join(', ')}"
|
47
|
+
return
|
48
|
+
end
|
49
|
+
|
50
|
+
DatabaseConnection.connect
|
51
|
+
book = Book.find_by(id: id)
|
52
|
+
if book.nil?
|
53
|
+
puts "Book not found: #{id}"
|
54
|
+
return
|
55
|
+
end
|
56
|
+
|
57
|
+
if book.reading_progress.nil?
|
58
|
+
puts "No reading progress for '#{book.title}'."
|
59
|
+
return
|
60
|
+
end
|
61
|
+
|
62
|
+
book.reading_progress.update!(status: status)
|
63
|
+
puts "Updated status of '#{book.title}' to '#{status}'."
|
64
|
+
rescue StandardError => e
|
65
|
+
puts "Error updating status: #{e.message}"
|
66
|
+
ensure
|
67
|
+
DatabaseConnection.disconnect
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.update_progress(id, pages_read)
|
71
|
+
if pages_read.negative?
|
72
|
+
puts 'Pages read cannot be negative.'
|
73
|
+
return
|
74
|
+
end
|
75
|
+
|
76
|
+
DatabaseConnection.connect
|
77
|
+
book = Book.find_by(id: id)
|
78
|
+
|
79
|
+
if book.nil?
|
80
|
+
puts "Book not found: #{id}"
|
81
|
+
return
|
82
|
+
end
|
83
|
+
|
84
|
+
reading_progress = book.reading_progress
|
85
|
+
if reading_progress.nil?
|
86
|
+
puts "No reading progress for '#{book.title}'."
|
87
|
+
return
|
88
|
+
end
|
89
|
+
|
90
|
+
total_pages = reading_progress.total_pages
|
91
|
+
|
92
|
+
if pages_read > total_pages
|
93
|
+
puts "Pages read: #{pages_read} cannot exceed total pages: #{total_pages}."
|
94
|
+
return
|
95
|
+
end
|
96
|
+
|
97
|
+
reading_progress.update!(pages_read: pages_read)
|
98
|
+
puts "Updated reading progress for '#{book.title}' to page #{pages_read}."
|
99
|
+
|
100
|
+
if pages_read.zero?
|
101
|
+
update_status(id, 'unread')
|
102
|
+
elsif pages_read < total_pages
|
103
|
+
update_status(id, 'reading')
|
104
|
+
elsif pages_read == total_pages
|
105
|
+
update_status(id, 'read')
|
106
|
+
end
|
107
|
+
rescue StandardError => e
|
108
|
+
puts "Error updating progress: #{e.message}"
|
109
|
+
ensure
|
110
|
+
DatabaseConnection.disconnect
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.list_books
|
114
|
+
DatabaseConnection.connect
|
115
|
+
books = Book.includes(:authors).all
|
116
|
+
|
117
|
+
if books.empty?
|
118
|
+
puts 'No books in the system.'
|
119
|
+
return
|
120
|
+
end
|
121
|
+
|
122
|
+
header = %w[id title description isbn published_year authors created_at updated_at]
|
123
|
+
|
124
|
+
rows = books.map do |book|
|
125
|
+
# Giới hạn lại thông tin để tránh bảng rộng bị -> dọc
|
126
|
+
author_names = (book.authors.map(&:author_name).join(', ').then { |a| a.empty? ? 'N/A' : "#{a[0..20]}..." })
|
127
|
+
|
128
|
+
# Giới hạn mô tả để tránh bảng quá rộng
|
129
|
+
description = "#{(book.description || 'N/A')[0..30]}..."
|
130
|
+
title = "#{(book.title || 'N/A')[0..20]}..."
|
131
|
+
|
132
|
+
[
|
133
|
+
book.id,
|
134
|
+
title,
|
135
|
+
description,
|
136
|
+
book.isbn || 'N/A',
|
137
|
+
book.published_year || 'N/A',
|
138
|
+
author_names,
|
139
|
+
book.created_at,
|
140
|
+
book.updated_at
|
141
|
+
]
|
142
|
+
end
|
143
|
+
|
144
|
+
# Gọi hàm render_table từ CommonUtils với chế độ ép ngang
|
145
|
+
CommonUtils.render_table(header, rows)
|
146
|
+
rescue StandardError => e
|
147
|
+
puts "Error listing books: #{e.message}"
|
148
|
+
ensure
|
149
|
+
DatabaseConnection.disconnect
|
150
|
+
end
|
151
|
+
|
152
|
+
def self.stats
|
153
|
+
DatabaseConnection.connect
|
154
|
+
total_books = Book.count
|
155
|
+
read_books = ReadingProgress.where(status: 'read').count
|
156
|
+
reading_books = ReadingProgress.where(status: 'reading').count
|
157
|
+
unread_books = ReadingProgress.where(status: 'unread').count
|
158
|
+
|
159
|
+
puts "Total books: #{total_books}"
|
160
|
+
puts "Books read: #{read_books}"
|
161
|
+
puts "Books reading: #{reading_books}"
|
162
|
+
puts "Books unread: #{unread_books}"
|
163
|
+
rescue StandardError => e
|
164
|
+
puts "Error generating stats: #{e.message}"
|
165
|
+
ensure
|
166
|
+
DatabaseConnection.disconnect
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.show_progress(id)
|
170
|
+
DatabaseConnection.connect
|
171
|
+
book = Book.includes(:reading_progress).find_by(id: id)
|
172
|
+
|
173
|
+
if book.nil?
|
174
|
+
puts "Book not found: #{id}"
|
175
|
+
return
|
176
|
+
end
|
177
|
+
|
178
|
+
if book.reading_progress.nil?
|
179
|
+
puts "No reading progress for '#{book.title}'."
|
180
|
+
else
|
181
|
+
header = %w[id book_id status pages_read total_pages read created_at updated_at]
|
182
|
+
progress = book.reading_progress
|
183
|
+
pages_read = progress.pages_read
|
184
|
+
total_pages = progress.total_pages
|
185
|
+
read = "#{pages_read}/#{total_pages}"
|
186
|
+
|
187
|
+
rows = [
|
188
|
+
[
|
189
|
+
book.id,
|
190
|
+
book.title,
|
191
|
+
progress.status,
|
192
|
+
pages_read,
|
193
|
+
total_pages,
|
194
|
+
read,
|
195
|
+
book.reading_progress.created_at,
|
196
|
+
book.reading_progress.updated_at
|
197
|
+
]
|
198
|
+
]
|
199
|
+
|
200
|
+
CommonUtils.render_table(header, rows)
|
201
|
+
end
|
202
|
+
rescue StandardError => e
|
203
|
+
puts "Error showing progress: #{e.message}"
|
204
|
+
ensure
|
205
|
+
DatabaseConnection.disconnect
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../models/category'
|
4
|
+
require_relative '../../../config/database_connection'
|
5
|
+
|
6
|
+
module BookReadingTrackerGem
|
7
|
+
class CategoryService
|
8
|
+
def self.add_category(name)
|
9
|
+
DatabaseConnection.connect
|
10
|
+
Category.create!(category_name: name)
|
11
|
+
puts "Add category '#{name}' successfully."
|
12
|
+
rescue StandardError => e
|
13
|
+
puts "Error adding category: #{e.message}"
|
14
|
+
ensure
|
15
|
+
DatabaseConnection.disconnect
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.list_categories
|
19
|
+
DatabaseConnection.connect
|
20
|
+
categories = Category.all
|
21
|
+
|
22
|
+
if categories.empty?
|
23
|
+
puts 'Categories not found.'
|
24
|
+
return
|
25
|
+
end
|
26
|
+
|
27
|
+
header = %w[id category_name created_at updated_at]
|
28
|
+
rows = categories.map do |category|
|
29
|
+
[
|
30
|
+
category.id,
|
31
|
+
category.category_name || 'N/A',
|
32
|
+
category.created_at,
|
33
|
+
category.updated_at
|
34
|
+
]
|
35
|
+
end
|
36
|
+
|
37
|
+
CommonUtils.render_table(header, rows)
|
38
|
+
rescue StandardError => e
|
39
|
+
puts "Error listing categories: #{e.message}"
|
40
|
+
ensure
|
41
|
+
DatabaseConnection.disconnect
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tty-table'
|
4
|
+
|
5
|
+
module CommonUtils
|
6
|
+
def self.render_table(header, rows)
|
7
|
+
if rows.empty?
|
8
|
+
puts 'No data available.'
|
9
|
+
else
|
10
|
+
table = TTY::Table.new(header, rows)
|
11
|
+
puts table.render(:ascii, resize: false)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'book_reading_tracker_gem/version'
|
4
|
+
require_relative 'book_reading_tracker_gem/clis/cli'
|
5
|
+
require_relative 'book_reading_tracker_gem/services/book_service'
|
6
|
+
require_relative 'book_reading_tracker_gem/services/author_service'
|
7
|
+
require_relative 'book_reading_tracker_gem/services/category_service'
|
8
|
+
require_relative 'book_reading_tracker_gem/models/book'
|
9
|
+
require_relative 'book_reading_tracker_gem/models/author'
|
10
|
+
require_relative 'book_reading_tracker_gem/models/category'
|
11
|
+
require_relative 'book_reading_tracker_gem/models/book_author'
|
12
|
+
require_relative 'book_reading_tracker_gem/models/book_category'
|
13
|
+
require_relative 'book_reading_tracker_gem/models/reading_progress'
|
14
|
+
require_relative '../config/database_connection'
|
15
|
+
|
16
|
+
module BookReadingTrackerGem
|
17
|
+
class Error < StandardError; end
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: book_reading_tracker_gem
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Doan Vo Van Trong
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-03-28 00:00:00.000000000 Z
|
11
|
+
dependencies: []
|
12
|
+
description: BookReadingTrackerGem is a simple command-line Ruby gem that helps users
|
13
|
+
manage their reading progress, books, authors, and categories. Built with Thor and
|
14
|
+
ActiveRecord, and backed by a PostgreSQL (Supabase) database.
|
15
|
+
email:
|
16
|
+
- trong.doan@tomosia.com
|
17
|
+
executables:
|
18
|
+
- book_reading_tracker_gem
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- config/database_connection.rb
|
23
|
+
- db/book_reading_tracker_gem.rake.rb
|
24
|
+
- db/migrate/20250326100001_create_books.rb
|
25
|
+
- db/migrate/20250326100002_create_authors.rb
|
26
|
+
- db/migrate/20250326100003_create_book_authors.rb
|
27
|
+
- db/migrate/20250326100004_create_categories.rb
|
28
|
+
- db/migrate/20250326100005_create_book_categories.rb
|
29
|
+
- db/migrate/20250326100006_create_reading_progresses.rb
|
30
|
+
- db/seeds.rb
|
31
|
+
- exe/book_reading_tracker_gem
|
32
|
+
- lib/book_reading_tracker_gem.rb
|
33
|
+
- lib/book_reading_tracker_gem/clis/cli.rb
|
34
|
+
- lib/book_reading_tracker_gem/models/author.rb
|
35
|
+
- lib/book_reading_tracker_gem/models/book.rb
|
36
|
+
- lib/book_reading_tracker_gem/models/book_author.rb
|
37
|
+
- lib/book_reading_tracker_gem/models/book_category.rb
|
38
|
+
- lib/book_reading_tracker_gem/models/category.rb
|
39
|
+
- lib/book_reading_tracker_gem/models/reading_progress.rb
|
40
|
+
- lib/book_reading_tracker_gem/services/author_service.rb
|
41
|
+
- lib/book_reading_tracker_gem/services/book_service.rb
|
42
|
+
- lib/book_reading_tracker_gem/services/category_service.rb
|
43
|
+
- lib/book_reading_tracker_gem/utils/helpers.rb
|
44
|
+
- lib/book_reading_tracker_gem/version.rb
|
45
|
+
homepage: https://github.com/vantrong24052003/Book-Reading-Tracker-Gem
|
46
|
+
licenses:
|
47
|
+
- MIT
|
48
|
+
metadata:
|
49
|
+
allowed_push_host: https://rubygems.org
|
50
|
+
homepage_uri: https://github.com/vantrong24052003/Book-Reading-Tracker-Gem
|
51
|
+
source_code_uri: https://github.com/vantrong24052003/Book-Reading-Tracker-Gem.git
|
52
|
+
changelog_uri: https://github.com/vantrong24052003/Book-Reading-Tracker-Gem/blob/main/CHANGELOG.md
|
53
|
+
rubygems_mfa_required: 'true'
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 3.1.0
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
requirements: []
|
68
|
+
rubygems_version: 3.6.6
|
69
|
+
specification_version: 4
|
70
|
+
summary: A CLI gem to track your book reading progress, authors, and categories.
|
71
|
+
test_files: []
|