ruby_todo 0.3.0 → 0.3.2

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,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "sqlite3"
5
+ require "fileutils"
6
+
7
+ module RubyTodo
8
+ class Database
9
+ class << self
10
+ def setup
11
+ return if ActiveRecord::Base.connected?
12
+
13
+ db_path = File.expand_path("~/.ruby_todo/todo.db")
14
+ FileUtils.mkdir_p(File.dirname(db_path))
15
+
16
+ ActiveRecord::Base.establish_connection(
17
+ adapter: "sqlite3",
18
+ database: db_path
19
+ )
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
+ end
32
+
33
+ def create_tables
34
+ ActiveRecord::Schema.define do
35
+ create_table :notebooks do |t|
36
+ t.string :name, null: false
37
+ t.timestamps
38
+ end
39
+
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
49
+ end
50
+
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
60
+ end
61
+
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
69
+ end
70
+ end
71
+
72
+ # Set initial schema version
73
+ ActiveRecord::Base.connection.execute("INSERT INTO schema_migrations (version) VALUES (2)")
74
+ end
75
+
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
79
+
80
+ # If needed, perform migrations
81
+ if current_version < 1
82
+ upgrade_to_version_1
83
+ end
84
+
85
+ if current_version < 2
86
+ upgrade_to_version_2
87
+ end
88
+ 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
+ end
120
+ end
121
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ module RubyTodo
6
+ class Notebook < ActiveRecord::Base
7
+ has_many :tasks, dependent: :destroy
8
+
9
+ validates :name, presence: true, uniqueness: true
10
+
11
+ def tasks_by_status(status)
12
+ tasks.where(status: status)
13
+ end
14
+
15
+ def tasks_by_priority(priority)
16
+ tasks.where(priority: priority)
17
+ end
18
+
19
+ def tasks_with_tag(tag)
20
+ tasks.select { |task| task.has_tag?(tag) }
21
+ end
22
+
23
+ def overdue_tasks
24
+ tasks.select(&:overdue?)
25
+ end
26
+
27
+ def due_soon_tasks
28
+ tasks.select(&:due_soon?)
29
+ end
30
+
31
+ def todo_tasks
32
+ tasks_by_status("todo")
33
+ end
34
+
35
+ def in_progress_tasks
36
+ tasks_by_status("in_progress")
37
+ end
38
+
39
+ def done_tasks
40
+ tasks_by_status("done")
41
+ end
42
+
43
+ def archived_tasks
44
+ tasks_by_status("archived")
45
+ end
46
+
47
+ def high_priority_tasks
48
+ tasks_by_priority("high")
49
+ end
50
+
51
+ def medium_priority_tasks
52
+ tasks_by_priority("medium")
53
+ end
54
+
55
+ def low_priority_tasks
56
+ tasks_by_priority("low")
57
+ end
58
+
59
+ def statistics
60
+ {
61
+ 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
71
+ }
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ module RubyTodo
6
+ class Task < ActiveRecord::Base
7
+ belongs_to :notebook
8
+
9
+ validates :title, presence: true
10
+ validates :status, presence: true, inclusion: { in: %w[todo in_progress done archived] }
11
+ validates :due_date, presence: false
12
+ validates :priority, inclusion: { in: %w[high medium low], allow_nil: true }
13
+ validate :due_date_cannot_be_in_past, if: :due_date?
14
+ validate :tags_format, if: :tags?
15
+
16
+ before_save :archive_completed_tasks
17
+ before_save :format_tags
18
+
19
+ scope :todo, -> { where(status: "todo") }
20
+ scope :in_progress, -> { where(status: "in_progress") }
21
+ scope :done, -> { where(status: "done") }
22
+ scope :archived, -> { where(status: "archived") }
23
+ scope :high_priority, -> { where(priority: "high") }
24
+ scope :medium_priority, -> { where(priority: "medium") }
25
+ scope :low_priority, -> { where(priority: "low") }
26
+
27
+ def overdue?
28
+ return false unless due_date?
29
+
30
+ due_date < Time.current && status != "done" && status != "archived"
31
+ end
32
+
33
+ def due_soon?
34
+ return false unless due_date?
35
+
36
+ due_date < Time.current + 24 * 60 * 60 && status != "done" && status != "archived"
37
+ end
38
+
39
+ def tag_list
40
+ return [] unless tags
41
+
42
+ tags.split(",").map(&:strip)
43
+ end
44
+
45
+ def has_tag?(tag)
46
+ return false unless tags
47
+
48
+ tag_list.include?(tag.strip)
49
+ end
50
+
51
+ private
52
+
53
+ def archive_completed_tasks
54
+ return unless status_changed? && status == "done"
55
+
56
+ self.status = "archived"
57
+ end
58
+
59
+ def due_date_cannot_be_in_past
60
+ return unless due_date.present? && due_date < Time.current && new_record?
61
+
62
+ errors.add(:due_date, "can't be in the past")
63
+ end
64
+
65
+ def tags_format
66
+ return unless tags.present?
67
+
68
+ unless tags.match?(/^[a-zA-Z0-9,\s\-_]+$/)
69
+ errors.add(:tags, "can only contain letters, numbers, commas, spaces, hyphens and underscores")
70
+ end
71
+ end
72
+
73
+ def format_tags
74
+ return unless tags.present?
75
+
76
+ self.tags = tags.split(",").map(&:strip).join(",")
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ module RubyTodo
6
+ class Template < ActiveRecord::Base
7
+ belongs_to :notebook, optional: true
8
+
9
+ validates :name, presence: true, uniqueness: true
10
+ validates :title_pattern, presence: true
11
+
12
+ # Create a task from this template
13
+ def create_task(notebook, replacements = {})
14
+ # Process the title with replacements
15
+ title = process_pattern(title_pattern, replacements)
16
+
17
+ # Process the description with replacements if it exists
18
+ description = description_pattern ? process_pattern(description_pattern, replacements) : nil
19
+
20
+ # Process tags if they exist
21
+ tags = tags_pattern ? process_pattern(tags_pattern, replacements) : nil
22
+
23
+ # Calculate due date
24
+ due_date = calculate_due_date(due_date_offset) if due_date_offset
25
+
26
+ # Create the task
27
+ Task.create(
28
+ notebook: notebook,
29
+ title: title,
30
+ description: description,
31
+ status: "todo",
32
+ priority: priority,
33
+ tags: tags,
34
+ due_date: due_date
35
+ )
36
+ end
37
+
38
+ private
39
+
40
+ def process_pattern(pattern, replacements)
41
+ result = pattern.dup
42
+
43
+ # Replace placeholders with values
44
+ replacements.each do |key, value|
45
+ placeholder = "{#{key}}"
46
+ result.gsub!(placeholder, value.to_s)
47
+ end
48
+
49
+ # Replace date placeholders
50
+ today = Date.today
51
+ result.gsub!("{today}", today.strftime("%Y-%m-%d"))
52
+ result.gsub!("{tomorrow}", (today + 1).strftime("%Y-%m-%d"))
53
+ result.gsub!("{yesterday}", (today - 1).strftime("%Y-%m-%d"))
54
+ result.gsub!("{weekday}", today.strftime("%A"))
55
+ result.gsub!("{month}", today.strftime("%B"))
56
+ result.gsub!("{year}", today.strftime("%Y"))
57
+
58
+ result
59
+ end
60
+
61
+ def calculate_due_date(offset_string)
62
+ return nil unless offset_string
63
+
64
+ # Parse the offset string (e.g., "2d", "1w", "3h")
65
+ if offset_string =~ /^(\d+)([dwmhy])$/
66
+ amount = ::Regexp.last_match(1).to_i
67
+ unit = ::Regexp.last_match(2)
68
+
69
+ case unit
70
+ when "d" # days
71
+ Time.now + amount * 24 * 60 * 60
72
+ when "w" # weeks
73
+ Time.now + amount * 7 * 24 * 60 * 60
74
+ when "m" # months (approximate)
75
+ Time.now + amount * 30 * 24 * 60 * 60
76
+ when "h" # hours
77
+ Time.now + amount * 60 * 60
78
+ when "y" # years (approximate)
79
+ Time.now + amount * 365 * 24 * 60 * 60
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyTodo
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_todo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremiah Parrack
@@ -167,7 +167,8 @@ dependencies:
167
167
  description: A flexible and powerful todo list management system for the command line
168
168
  email:
169
169
  - jeremiah.parrack@gmail.com
170
- executables: []
170
+ executables:
171
+ - ruby_todo
171
172
  extensions: []
172
173
  extra_rdoc_files: []
173
174
  files:
@@ -175,11 +176,18 @@ files:
175
176
  - CHANGELOG.md
176
177
  - CODE_OF_CONDUCT.md
177
178
  - LICENSE.txt
179
+ - PRD.md
180
+ - PROGRESS.md
178
181
  - README.md
179
182
  - Rakefile
183
+ - exe/ruby_todo
180
184
  - lib/ruby_todo.rb
185
+ - lib/ruby_todo/cli.rb
186
+ - lib/ruby_todo/database.rb
187
+ - lib/ruby_todo/models/notebook.rb
188
+ - lib/ruby_todo/models/task.rb
189
+ - lib/ruby_todo/models/template.rb
181
190
  - lib/ruby_todo/version.rb
182
- - ruby_todo.gemspec
183
191
  - sig/ruby_todo.rbs
184
192
  homepage: https://github.com/jtparrack/ruby_todo
185
193
  licenses:
data/ruby_todo.gemspec DELETED
@@ -1,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/ruby_todo/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "ruby_todo"
7
- spec.version = RubyTodo::VERSION
8
- spec.authors = ["Jeremiah Parrack"]
9
- spec.email = ["jeremiah.parrack@gmail.com"]
10
-
11
- spec.summary = "A command-line todo application"
12
- spec.description = "A flexible and powerful todo list management system for the command line"
13
- spec.homepage = "https://github.com/jtparrack/ruby_todo"
14
- spec.license = "MIT"
15
- spec.required_ruby_version = ">= 3.0.0"
16
-
17
- spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
-
19
- spec.metadata["homepage_uri"] = spec.homepage
20
- spec.metadata["source_code_uri"] = "https://github.com/jtparrack/ruby_todo"
21
- spec.metadata["changelog_uri"] = "https://github.com/jtparrack/ruby_todo/blob/main/CHANGELOG.md"
22
-
23
- # Specify which files should be added to the gem when it is released.
24
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
- spec.files = Dir.chdir(__dir__) do
26
- `git ls-files -z`.split("\x0").reject do |f|
27
- (File.expand_path(f) == __FILE__) ||
28
- f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
29
- end
30
- end
31
- spec.bindir = "exe"
32
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
- spec.require_paths = ["lib"]
34
-
35
- # Runtime dependencies
36
- spec.add_dependency "activerecord", "~> 7.1"
37
- spec.add_dependency "colorize", "~> 1.1"
38
- spec.add_dependency "sqlite3", "~> 1.7"
39
- spec.add_dependency "thor", "~> 1.3"
40
- spec.add_dependency "tty-prompt", "~> 0.23"
41
- spec.add_dependency "tty-table", "~> 0.12"
42
-
43
- # Development dependencies
44
- spec.add_development_dependency "minitest", "~> 5.19"
45
- spec.add_development_dependency "rake", "~> 13.0"
46
- spec.add_development_dependency "rubocop", "~> 1.59"
47
- spec.add_development_dependency "rubocop-minitest", "~> 0.34"
48
- spec.add_development_dependency "rubocop-rake", "~> 0.6"
49
-
50
- # For more information and examples about making a new gem, check out our
51
- # guide at: https://bundler.io/guides/creating_gem.html
52
- end