ruby_todo 0.4.1 → 1.0.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.
@@ -0,0 +1,449 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "json"
5
+ require "openai"
6
+ require "dotenv/load"
7
+
8
+ require_relative "../ai_assistant/task_search"
9
+ require_relative "../ai_assistant/task_management"
10
+ require_relative "../ai_assistant/openai_integration"
11
+ require_relative "../ai_assistant/configuration_management"
12
+ require_relative "../ai_assistant/common_query_handler"
13
+
14
+ module RubyTodo
15
+ class AIAssistantCommand < Thor
16
+ include TaskManagement
17
+ include OpenAIIntegration
18
+ include ConfigurationManagement
19
+ include CommonQueryHandler
20
+
21
+ desc "ai:ask [PROMPT]", "Ask the AI assistant to perform tasks using natural language"
22
+ method_option :api_key, type: :string, desc: "OpenAI API key"
23
+ method_option :verbose, type: :boolean, default: false, desc: "Show detailed response"
24
+ def ask(*prompt_args)
25
+ prompt = prompt_args.join(" ")
26
+ validate_prompt(prompt)
27
+ say "\n=== Starting AI Assistant with prompt: '#{prompt}' ===" if options[:verbose]
28
+
29
+ # Direct handling for common queries
30
+ return if handle_common_query(prompt)
31
+
32
+ process_ai_query(prompt)
33
+ end
34
+
35
+ desc "ai:configure", "Configure the AI assistant settings"
36
+ def configure
37
+ prompt = TTY::Prompt.new
38
+ api_key = prompt.mask("Enter your OpenAI API key:")
39
+ save_config("openai", api_key)
40
+ say "Configuration saved successfully!".green
41
+ end
42
+
43
+ def self.banner(command, _namespace = nil, _subcommand: false)
44
+ "#{basename} #{command.name}"
45
+ end
46
+
47
+ def self.exit_on_failure?
48
+ true
49
+ end
50
+
51
+ private
52
+
53
+ def process_ai_query(prompt)
54
+ api_key = fetch_api_key
55
+ say "\nAPI key loaded successfully" if options[:verbose]
56
+
57
+ context = build_context
58
+ say "\nInitial context built" if options[:verbose]
59
+
60
+ # Process based on query type
61
+ process_query_by_type(prompt, context)
62
+
63
+ # Get AI response for commands and explanation
64
+ say "\n=== Querying OpenAI ===" if options[:verbose]
65
+ response = query_openai(prompt, context, api_key)
66
+ say "\nOpenAI Response received" if options[:verbose]
67
+
68
+ # Execute actions based on response
69
+ execute_actions(response, context)
70
+ end
71
+
72
+ def process_query_by_type(prompt, context)
73
+ if should_handle_task_movement?(prompt)
74
+ # Handle task movement and build context
75
+ say "\n=== Processing Task Movement Request ===" if options[:verbose]
76
+ handle_task_request(prompt, context)
77
+ elsif options[:verbose]
78
+ say "\n=== Processing Non-Movement Request ==="
79
+ end
80
+ end
81
+
82
+ def execute_actions(response, context)
83
+ # If we have tasks to move, do it now
84
+ if context[:matching_tasks]&.any? && context[:target_status]
85
+ say "\n=== Moving Tasks ===" if options[:verbose]
86
+ move_tasks_to_status(context[:matching_tasks], context[:target_status])
87
+ end
88
+
89
+ # Execute any additional commands from the AI
90
+ if response && response["commands"]
91
+ say "\n=== Executing Additional Commands ===" if options[:verbose]
92
+ execute_commands(response)
93
+ end
94
+
95
+ # Display the explanation from the AI
96
+ if response && response["explanation"]
97
+ say "\n=== AI Explanation ===" if options[:verbose]
98
+ say "\n#{response["explanation"]}"
99
+ end
100
+ end
101
+
102
+ def validate_prompt(prompt)
103
+ return if prompt && !prompt.empty?
104
+
105
+ say "Please provide a prompt for the AI assistant".red
106
+ raise ArgumentError, "Empty prompt"
107
+ end
108
+
109
+ def fetch_api_key
110
+ api_key = options[:api_key] || ENV["OPENAI_API_KEY"] || load_api_key_from_config
111
+ return api_key if api_key
112
+
113
+ say "No API key found. Please provide an API key using --api-key or set OPENAI_API_KEY environment variable".red
114
+ raise ArgumentError, "No API key found"
115
+ end
116
+
117
+ def build_context
118
+ { matching_tasks: [] }
119
+ end
120
+
121
+ def execute_commands(response)
122
+ return unless response["commands"].is_a?(Array)
123
+
124
+ response["commands"].each do |command|
125
+ process_command(command)
126
+ end
127
+ end
128
+
129
+ def process_command(command)
130
+ say "\nExecuting command: #{command}".blue if options[:verbose]
131
+
132
+ begin
133
+ # Skip if empty or nil
134
+ return if command.nil? || command.strip.empty?
135
+
136
+ # Ensure command starts with ruby_todo
137
+ return unless command.start_with?("ruby_todo")
138
+
139
+ # Process and execute the command
140
+ process_ruby_todo_command(command)
141
+ rescue StandardError => e
142
+ say "Error executing command: #{e.message}".red
143
+ say e.backtrace.join("\n").red if options[:verbose]
144
+ end
145
+ end
146
+
147
+ def process_ruby_todo_command(command)
148
+ # Remove the ruby_todo prefix
149
+ cmd_without_prefix = command.sub(/^ruby_todo\s+/, "")
150
+ say "\nCommand without prefix: '#{cmd_without_prefix}'".blue if options[:verbose]
151
+
152
+ # Convert underscores to colons for all task commands
153
+ if cmd_without_prefix =~ /^task_\w+/
154
+ cmd_without_prefix = cmd_without_prefix.sub(/^task_(\w+)/, 'task:\1')
155
+ say "\nConverted underscores to colons: '#{cmd_without_prefix}'".blue if options[:verbose]
156
+ end
157
+
158
+ # Convert underscores to colons for notebook commands
159
+ if cmd_without_prefix =~ /^notebook_\w+/
160
+ cmd_without_prefix = cmd_without_prefix.sub(/^notebook_(\w+)/, 'notebook:\1')
161
+ say "\nConverted underscores to colons: '#{cmd_without_prefix}'".blue if options[:verbose]
162
+ end
163
+
164
+ # Convert underscores to colons for template commands
165
+ if cmd_without_prefix =~ /^template_\w+/
166
+ cmd_without_prefix = cmd_without_prefix.sub(/^template_(\w+)/, 'template:\1')
167
+ say "\nConverted underscores to colons: '#{cmd_without_prefix}'".blue if options[:verbose]
168
+ end
169
+
170
+ # Process different command types
171
+ if cmd_without_prefix.start_with?("task:list")
172
+ execute_task_list_command(cmd_without_prefix)
173
+ elsif cmd_without_prefix.start_with?("task:search")
174
+ execute_task_search_command(cmd_without_prefix)
175
+ elsif cmd_without_prefix.start_with?("task:move")
176
+ execute_task_move_command(cmd_without_prefix)
177
+ else
178
+ execute_other_command(cmd_without_prefix)
179
+ end
180
+ end
181
+
182
+ def execute_task_list_command(cmd_without_prefix)
183
+ parts = cmd_without_prefix.split(/\s+/)
184
+ say "\nSplit task:list command into parts: #{parts.inspect}".blue if options[:verbose]
185
+
186
+ if parts.size >= 2
187
+ execute_task_list_with_notebook(parts)
188
+ elsif Notebook.default_notebook
189
+ execute_task_list_with_default_notebook(parts)
190
+ else
191
+ say "\nNo notebook specified for task:list command".yellow
192
+ end
193
+ end
194
+
195
+ def execute_task_list_with_notebook(parts)
196
+ notebook_name = parts[1]
197
+ # Extract any options
198
+ options_args = []
199
+ parts[2..].each do |part|
200
+ options_args << part if part.start_with?("--")
201
+ end
202
+
203
+ # Execute the task list command with the notebook name and any options
204
+ cli_args = ["task:list", notebook_name] + options_args
205
+ say "\nRunning CLI with args: #{cli_args.inspect}".blue if options[:verbose]
206
+ RubyTodo::CLI.start(cli_args)
207
+ end
208
+
209
+ def execute_task_list_with_default_notebook(parts)
210
+ cli_args = ["task:list", Notebook.default_notebook.name]
211
+ if parts.size > 1 && parts[1].start_with?("--")
212
+ cli_args << parts[1]
213
+ end
214
+ say "\nUsing default notebook for task:list with args: #{cli_args.inspect}".blue if options[:verbose]
215
+ RubyTodo::CLI.start(cli_args)
216
+ end
217
+
218
+ def execute_task_search_command(cmd_without_prefix)
219
+ parts = cmd_without_prefix.split(/\s+/, 2) # Split into command and search term
220
+
221
+ if parts.size >= 2
222
+ # Pass the entire search term as a single argument, not individual words
223
+ cli_args = ["task:search", parts[1]]
224
+ say "\nRunning CLI with args: #{cli_args.inspect}".blue if options[:verbose]
225
+ RubyTodo::CLI.start(cli_args)
226
+ else
227
+ say "\nNo search term provided for task:search command".yellow
228
+ end
229
+ end
230
+
231
+ def execute_task_move_command(cmd_without_prefix)
232
+ parts = cmd_without_prefix.split(/\s+/)
233
+
234
+ # Need at least task:move NOTEBOOK TASK_ID STATUS
235
+ if parts.size >= 4
236
+ notebook_name = parts[1]
237
+ task_id = parts[2]
238
+ status = parts[3]
239
+
240
+ cli_args = ["task:move", notebook_name, task_id, status]
241
+ say "\nRunning CLI with args: #{cli_args.inspect}".blue if options[:verbose]
242
+ RubyTodo::CLI.start(cli_args)
243
+ else
244
+ say "\nInvalid task:move command format. Need NOTEBOOK, TASK_ID, and STATUS".yellow
245
+ end
246
+ end
247
+
248
+ def execute_other_command(cmd_without_prefix)
249
+ # Process all other commands
250
+ cli_args = cmd_without_prefix.split(/\s+/)
251
+ say "\nRunning CLI with args: #{cli_args.inspect}".blue if options[:verbose]
252
+ RubyTodo::CLI.start(cli_args)
253
+ end
254
+
255
+ def handle_common_query(prompt)
256
+ prompt_lower = prompt.downcase
257
+
258
+ # Check for different types of common queries
259
+ return handle_task_creation(prompt, prompt_lower) if task_creation_query?(prompt_lower)
260
+ return handle_priority_tasks(prompt_lower, "high") if high_priority_query?(prompt_lower)
261
+ return handle_priority_tasks(prompt_lower, "medium") if medium_priority_query?(prompt_lower)
262
+ return handle_statistics(prompt_lower) if statistics_query?(prompt_lower)
263
+ return handle_status_tasks(prompt_lower) if status_tasks_query?(prompt_lower)
264
+ return handle_notebook_listing(prompt_lower) if notebook_listing_query?(prompt_lower)
265
+
266
+ # Not a common query
267
+ false
268
+ end
269
+
270
+ def task_creation_query?(prompt_lower)
271
+ (prompt_lower.include?("create") || prompt_lower.include?("add")) &&
272
+ (prompt_lower.include?("task") || prompt_lower.include?("todo"))
273
+ end
274
+
275
+ def high_priority_query?(prompt_lower)
276
+ prompt_lower.include?("high priority") ||
277
+ (prompt_lower.include?("priority") && prompt_lower.include?("high"))
278
+ end
279
+
280
+ def medium_priority_query?(prompt_lower)
281
+ prompt_lower.include?("medium priority") ||
282
+ (prompt_lower.include?("priority") && prompt_lower.include?("medium"))
283
+ end
284
+
285
+ def statistics_query?(prompt_lower)
286
+ (prompt_lower.include?("statistics") || prompt_lower.include?("stats")) &&
287
+ (prompt_lower.include?("notebook") || prompt_lower.include?("tasks"))
288
+ end
289
+
290
+ def status_tasks_query?(prompt_lower)
291
+ statuses = { "todo" => "todo", "in progress" => "in_progress", "done" => "done", "archived" => "archived" }
292
+
293
+ statuses.keys.any? do |name|
294
+ prompt_lower.include?("#{name} tasks") || prompt_lower.include?("tasks in #{name}")
295
+ end
296
+ end
297
+
298
+ def notebook_listing_query?(prompt_lower)
299
+ prompt_lower.include?("list notebooks") ||
300
+ prompt_lower.include?("show notebooks") ||
301
+ prompt_lower.include?("all notebooks")
302
+ end
303
+
304
+ def handle_task_creation(prompt, prompt_lower)
305
+ say "\n=== Detecting task creation request ===" if options[:verbose]
306
+
307
+ title = extract_task_title(prompt)
308
+ return false unless title
309
+
310
+ notebook_name = determine_notebook_name(prompt_lower)
311
+ return false unless notebook_name
312
+
313
+ priority = determine_priority(prompt_lower)
314
+
315
+ create_task(notebook_name, title, priority)
316
+ true
317
+ end
318
+
319
+ def extract_task_title(prompt)
320
+ # Try to extract title from quotes first
321
+ title_match = prompt.match(/'([^']+)'|"([^"]+)"/)
322
+
323
+ if title_match
324
+ title_match[1] || title_match[2]
325
+ else
326
+ # If no quoted title found, try extracting from the prompt
327
+ extract_title_from_text(prompt)
328
+ end
329
+ end
330
+
331
+ def extract_title_from_text(prompt)
332
+ potential_title = prompt
333
+ phrases_to_remove = [
334
+ "create a task", "create task", "add a task", "add task",
335
+ "called", "named", "with", "priority", "high", "medium", "low",
336
+ "in", "notebook"
337
+ ]
338
+
339
+ phrases_to_remove.each do |phrase|
340
+ potential_title = potential_title.gsub(/#{phrase}/i, " ")
341
+ end
342
+
343
+ result = potential_title.strip
344
+ result.empty? ? nil : result
345
+ end
346
+
347
+ def determine_notebook_name(prompt_lower)
348
+ return nil unless Notebook.default_notebook
349
+
350
+ notebook_name = Notebook.default_notebook.name
351
+
352
+ # Try to extract a specific notebook name from the prompt
353
+ Notebook.all.each do |notebook|
354
+ if prompt_lower.include?(notebook.name.downcase)
355
+ notebook_name = notebook.name
356
+ break
357
+ end
358
+ end
359
+
360
+ notebook_name
361
+ end
362
+
363
+ def determine_priority(prompt_lower)
364
+ if prompt_lower.include?("high priority") || prompt_lower.match(/priority.*high/)
365
+ "high"
366
+ elsif prompt_lower.include?("medium priority") || prompt_lower.match(/priority.*medium/)
367
+ "medium"
368
+ elsif prompt_lower.include?("low priority") || prompt_lower.match(/priority.*low/)
369
+ "low"
370
+ end
371
+ end
372
+
373
+ def create_task(notebook_name, title, priority)
374
+ say "\nCreating task in notebook: #{notebook_name}" if options[:verbose]
375
+ cli_args = ["task:add", notebook_name, title]
376
+
377
+ # Add priority if specified
378
+ cli_args.push("--priority", priority) if priority
379
+
380
+ RubyTodo::CLI.start(cli_args)
381
+
382
+ # Create a simple explanation
383
+ priority_text = priority ? " with #{priority} priority" : ""
384
+ say "\nCreated task '#{title}'#{priority_text} in the #{notebook_name} notebook"
385
+ end
386
+
387
+ def handle_priority_tasks(_prompt_lower, priority)
388
+ say "\n=== Detecting #{priority} priority task request ===" if options[:verbose]
389
+
390
+ return false unless Notebook.default_notebook
391
+
392
+ say "\nListing #{priority} priority tasks from default notebook" if options[:verbose]
393
+ RubyTodo::CLI.start(["task:list", Notebook.default_notebook.name, "--priority", priority])
394
+
395
+ # Create a simple explanation
396
+ say "\nListing all #{priority} priority tasks in the #{Notebook.default_notebook.name} notebook"
397
+ true
398
+ end
399
+
400
+ def handle_statistics(prompt_lower)
401
+ say "\n=== Detecting statistics request ===" if options[:verbose]
402
+
403
+ notebook_name = determine_notebook_name(prompt_lower)
404
+
405
+ if notebook_name
406
+ say "\nShowing statistics for notebook: #{notebook_name}" if options[:verbose]
407
+ RubyTodo::CLI.start(["stats", notebook_name])
408
+ say "\nDisplaying statistics for the #{notebook_name} notebook"
409
+ else
410
+ # Show global stats if no default notebook
411
+ say "\nShowing global statistics" if options[:verbose]
412
+ RubyTodo::CLI.start(["stats"])
413
+ say "\nDisplaying global statistics for all notebooks"
414
+ end
415
+
416
+ true
417
+ end
418
+
419
+ def handle_status_tasks(prompt_lower)
420
+ statuses = { "todo" => "todo", "in progress" => "in_progress", "done" => "done", "archived" => "archived" }
421
+
422
+ statuses.each do |name, value|
423
+ next unless prompt_lower.include?("#{name} tasks") || prompt_lower.include?("tasks in #{name}")
424
+
425
+ say "\n=== Detecting #{name} task listing request ===" if options[:verbose]
426
+
427
+ return false unless Notebook.default_notebook
428
+
429
+ say "\nListing #{name} tasks from default notebook" if options[:verbose]
430
+ RubyTodo::CLI.start(["task:list", Notebook.default_notebook.name, "--status", value])
431
+
432
+ # Create a simple explanation
433
+ say "\nListing all #{name} tasks in the #{Notebook.default_notebook.name} notebook"
434
+ return true
435
+ end
436
+
437
+ false
438
+ end
439
+
440
+ def handle_notebook_listing(_prompt_lower)
441
+ say "\n=== Detecting notebook listing request ===" if options[:verbose]
442
+ RubyTodo::CLI.start(["notebook:list"])
443
+
444
+ # Create a simple explanation
445
+ say "\nListing all available notebooks"
446
+ true
447
+ end
448
+ end
449
+ end
@@ -10,112 +10,86 @@ module RubyTodo
10
10
  def setup
11
11
  return if ActiveRecord::Base.connected?
12
12
 
13
- db_path = File.expand_path("~/.ruby_todo/todo.db")
14
- FileUtils.mkdir_p(File.dirname(db_path))
13
+ ensure_database_directory
14
+ establish_connection
15
+ create_tables
16
+ end
17
+
18
+ private
15
19
 
20
+ def ensure_database_directory
21
+ db_dir = File.expand_path("~/.ruby_todo")
22
+ FileUtils.mkdir_p(db_dir)
23
+ end
24
+
25
+ def establish_connection
26
+ db_path = File.expand_path("~/.ruby_todo/ruby_todo.db")
16
27
  ActiveRecord::Base.establish_connection(
17
28
  adapter: "sqlite3",
18
29
  database: db_path
19
30
  )
20
-
21
- create_tables unless tables_exist?
22
- check_schema_version
23
- end
24
-
25
- private
26
-
27
- def tables_exist?
28
- ActiveRecord::Base.connection.tables.include?("notebooks") &&
29
- ActiveRecord::Base.connection.tables.include?("tasks") &&
30
- ActiveRecord::Base.connection.tables.include?("schema_migrations")
31
31
  end
32
32
 
33
33
  def create_tables
34
+ connection = ActiveRecord::Base.connection
35
+
34
36
  ActiveRecord::Schema.define do
35
- create_table :notebooks do |t|
36
- t.string :name, null: false
37
- t.timestamps
37
+ unless connection.table_exists?(:notebooks)
38
+ create_table :notebooks do |t|
39
+ t.string :name, null: false
40
+ t.boolean :is_default, default: false
41
+ t.timestamps
42
+ end
38
43
  end
39
44
 
40
- create_table :tasks do |t|
41
- t.references :notebook, null: false, foreign_key: true
42
- t.string :title, null: false
43
- t.string :status, null: false, default: "todo"
44
- t.text :description
45
- t.datetime :due_date
46
- t.string :priority
47
- t.string :tags
48
- t.timestamps
45
+ unless connection.table_exists?(:tasks)
46
+ create_table :tasks do |t|
47
+ t.references :notebook, null: false
48
+ t.string :title, null: false
49
+ t.text :description
50
+ t.string :status, default: "todo"
51
+ t.datetime :due_date
52
+ t.string :priority
53
+ t.string :tags
54
+ t.timestamps
55
+ end
49
56
  end
50
57
 
51
- create_table :templates do |t|
52
- t.references :notebook, foreign_key: true
53
- t.string :name, null: false
54
- t.string :title_pattern, null: false
55
- t.text :description_pattern
56
- t.string :tags_pattern
57
- t.string :priority
58
- t.string :due_date_offset
59
- t.timestamps
58
+ unless connection.table_exists?(:templates)
59
+ create_table :templates do |t|
60
+ t.string :name, null: false
61
+ t.string :title_pattern, null: false
62
+ t.text :description_pattern
63
+ t.string :priority
64
+ t.string :tags_pattern
65
+ t.string :due_date_offset
66
+ t.references :notebook
67
+ t.timestamps
68
+ end
60
69
  end
61
70
 
62
- add_index :tasks, %i[notebook_id status]
63
- add_index :tasks, :priority
64
- add_index :tasks, :due_date
65
- add_index :templates, :name, unique: true
66
-
67
- create_table :schema_migrations do |t|
68
- t.integer :version, null: false
71
+ # Add indexes if they don't exist
72
+ unless connection.index_exists?(:tasks, %i[notebook_id status])
73
+ add_index :tasks, %i[notebook_id status]
69
74
  end
70
- end
71
75
 
72
- # Set initial schema version
73
- ActiveRecord::Base.connection.execute("INSERT INTO schema_migrations (version) VALUES (2)")
74
- end
76
+ unless connection.index_exists?(:tasks, :priority)
77
+ add_index :tasks, :priority
78
+ end
75
79
 
76
- def check_schema_version
77
- # Get current schema version
78
- current_version = ActiveRecord::Base.connection.select_value("SELECT MAX(version) FROM schema_migrations").to_i
80
+ unless connection.index_exists?(:tasks, :due_date)
81
+ add_index :tasks, :due_date
82
+ end
79
83
 
80
- # If needed, perform migrations
81
- if current_version < 1
82
- upgrade_to_version_1
83
- end
84
+ unless connection.index_exists?(:templates, :name)
85
+ add_index :templates, :name, unique: true
86
+ end
84
87
 
85
- if current_version < 2
86
- upgrade_to_version_2
88
+ unless connection.index_exists?(:notebooks, :is_default)
89
+ add_index :notebooks, :is_default
90
+ end
87
91
  end
88
92
  end
89
-
90
- def upgrade_to_version_1
91
- ActiveRecord::Base.connection.execute(<<-SQL)
92
- ALTER TABLE tasks ADD COLUMN priority STRING;
93
- ALTER TABLE tasks ADD COLUMN tags STRING;
94
- SQL
95
-
96
- ActiveRecord::Base.connection.execute("UPDATE schema_migrations SET version = 1")
97
- end
98
-
99
- def upgrade_to_version_2
100
- ActiveRecord::Base.connection.execute(<<-SQL)
101
- CREATE TABLE templates (
102
- id INTEGER PRIMARY KEY AUTOINCREMENT,
103
- notebook_id INTEGER,
104
- name VARCHAR NOT NULL,
105
- title_pattern VARCHAR NOT NULL,
106
- description_pattern TEXT,
107
- tags_pattern VARCHAR,
108
- priority VARCHAR,
109
- due_date_offset VARCHAR,
110
- created_at DATETIME NOT NULL,
111
- updated_at DATETIME NOT NULL,
112
- FOREIGN KEY (notebook_id) REFERENCES notebooks(id)
113
- );
114
- CREATE UNIQUE INDEX index_templates_on_name ON templates (name);
115
- SQL
116
-
117
- ActiveRecord::Base.connection.execute("UPDATE schema_migrations SET version = 2")
118
- end
119
93
  end
120
94
  end
121
95
  end
@@ -5,8 +5,30 @@ require "active_record"
5
5
  module RubyTodo
6
6
  class Notebook < ActiveRecord::Base
7
7
  has_many :tasks, dependent: :destroy
8
+ has_many :templates
8
9
 
9
- validates :name, presence: true, uniqueness: true
10
+ validates :name, presence: true
11
+ validates :name, uniqueness: true
12
+
13
+ scope :default, -> { where(is_default: true).first }
14
+
15
+ before_create :set_default_if_first
16
+ before_save :ensure_only_one_default
17
+
18
+ def self.default_notebook
19
+ find_by(is_default: true)
20
+ end
21
+
22
+ def make_default!
23
+ transaction do
24
+ Notebook.where.not(id: id).update_all(is_default: false)
25
+ update!(is_default: true)
26
+ end
27
+ end
28
+
29
+ def is_default?
30
+ is_default
31
+ end
10
32
 
11
33
  def tasks_by_status(status)
12
34
  tasks.where(status: status)
@@ -59,16 +81,28 @@ module RubyTodo
59
81
  def statistics
60
82
  {
61
83
  total: tasks.count,
62
- todo: todo_tasks.count,
63
- in_progress: in_progress_tasks.count,
64
- done: done_tasks.count,
65
- archived: archived_tasks.count,
66
- overdue: overdue_tasks.count,
67
- due_soon: due_soon_tasks.count,
68
- high_priority: high_priority_tasks.count,
69
- medium_priority: medium_priority_tasks.count,
70
- low_priority: low_priority_tasks.count
84
+ todo: tasks.where(status: "todo").count,
85
+ in_progress: tasks.where(status: "in_progress").count,
86
+ done: tasks.where(status: "done").count,
87
+ archived: tasks.where(status: "archived").count,
88
+ overdue: tasks.select(&:overdue?).count,
89
+ due_soon: tasks.select(&:due_soon?).count,
90
+ high_priority: tasks.where(priority: "high").count,
91
+ medium_priority: tasks.where(priority: "medium").count,
92
+ low_priority: tasks.where(priority: "low").count
71
93
  }
72
94
  end
95
+
96
+ private
97
+
98
+ def set_default_if_first
99
+ self.is_default = true if Notebook.count.zero?
100
+ end
101
+
102
+ def ensure_only_one_default
103
+ return unless is_default_changed? && is_default?
104
+
105
+ Notebook.where.not(id: id).update_all(is_default: false)
106
+ end
73
107
  end
74
108
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyTodo
4
- VERSION = "0.4.1"
4
+ VERSION = "1.0.1"
5
5
  end