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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -54
- data/PRD.md +264 -0
- data/PROGRESS.md +273 -0
- data/README.md +94 -1
- data/exe/ruby_todo +6 -0
- data/lib/ruby_todo/cli.rb +853 -0
- data/lib/ruby_todo/database.rb +121 -0
- data/lib/ruby_todo/models/notebook.rb +74 -0
- data/lib/ruby_todo/models/task.rb +79 -0
- data/lib/ruby_todo/models/template.rb +84 -0
- data/lib/ruby_todo/version.rb +1 -1
- metadata +11 -3
- data/ruby_todo.gemspec +0 -52
@@ -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
|
data/lib/ruby_todo/version.rb
CHANGED
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.
|
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
|