todoro 0.1.3 → 0.1.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9df4d399bddcdec90b476afd3f277a388c9ed04a60c3388cbd339f33f9bc78a
4
- data.tar.gz: 4d56a850c3674c38e6abeaae287afe234f6a6fee73edb09722b8d066f3a61095
3
+ metadata.gz: 5fdd815954054219782d8b2281bced824aca47cb6c7add82408cf6d1fdad3f4d
4
+ data.tar.gz: aa21ff161a69f8d9ae91cc5634871fdb354aeead527c92b8798b6526e576812b
5
5
  SHA512:
6
- metadata.gz: 4b9fd5d72efb5b3a6bb3b8b5c1b024405ab5f43b9f626607ce6ea2b740f8498abe14d045cd0c3d31256132ff80ab042664209985f48b02b72dbd07f0479f9890
7
- data.tar.gz: c2d103b2d0b301652b364ceeecc0e65e057e6431c79386a52e54a8ba26d841988394683337ee09d4f9c8ee8c4395edbc70152f7597427fc2ca0e432695a8abe0
6
+ metadata.gz: d5c69c6f569090184889b7536ea4d380c5a616be36155a70941e7f2cae1095a73780b4844013539d3ff536da18db1f34b48ce62e5dd9d5bcce37e00239c1d1d6
7
+ data.tar.gz: ee5361a8bc6904bba93619563a6b4d504a5e2b359c8c75ad25b44fda556b8b2d79349011a50b6ad32c1af5613459eec36320317584cec7ff753345d68216fe2d
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # Todoro
2
+
2
3
  A lightweight Rails engine that lets any model manage task lists and tasks with a simple, flexible API.
3
4
 
4
5
  ![Totoro](totoro.png)
@@ -13,25 +14,26 @@ gem "todoro"
13
14
 
14
15
  And then execute:
15
16
 
16
- ```bash
17
- $ bundle
17
+ ```sh
18
+ $ bundle install
18
19
  ```
19
20
 
20
21
  Or install it yourself as:
21
22
 
22
- ```bash
23
+ ```sh
23
24
  $ gem install todoro
24
25
  ```
25
26
 
26
27
  ## Usage
27
28
 
28
- Generate the required migrations
29
+ Generate the required migrations:
29
30
 
30
- ```ruby
31
- rails generate todoro:install
31
+ ```sh
32
+ $ rails generate todoro:install
33
+ $ rails db:migrate
32
34
  ```
33
35
 
34
- Include Taskable in the model(s) that can have tasks
36
+ Include `acts_as_taskable` in the model(s) that can have tasks:
35
37
 
36
38
  ```ruby
37
39
  class Project < ApplicationRecord
@@ -39,9 +41,9 @@ class Project < ApplicationRecord
39
41
  end
40
42
  ```
41
43
 
42
- Now, a Project can have task lists and tasks!
44
+ Now, a `Project` can have task lists and tasks:
43
45
 
44
- ```bash
46
+ ```ruby
45
47
  project = Project.create(name: "Website Redesign")
46
48
  task_list = project.create_task_list("Development Tasks")
47
49
  task = project.add_task_to_list(task_list, "Build homepage", "Implement UI components")
@@ -49,6 +51,36 @@ task = project.add_task_to_list(task_list, "Build homepage", "Implement UI compo
49
51
  puts project.tasks # Lists all tasks in all its task lists
50
52
  ```
51
53
 
54
+ ## Automatic Route Generation
55
+
56
+ Todoro automatically generates routes for any model that includes `acts_as_taskable`. For example, if `Project` includes `acts_as_taskable`, the following routes are created:
57
+
58
+ ```sh
59
+ GET /projects/:project_id/task_lists(.:format)
60
+ POST /projects/:project_id/task_lists(.:format)
61
+ GET /projects/:project_id/task_lists/:id(.:format)
62
+ PATCH /projects/:project_id/task_lists/:id(.:format)
63
+ DELETE /projects/:project_id/task_lists/:id(.:format)
64
+ GET /projects/:project_id/task_lists/:task_list_id/tasks(.:format)
65
+ POST /projects/:project_id/task_lists/:task_list_id/tasks(.:format)
66
+ ```
67
+
68
+ These routes are automatically configured, so no additional setup is needed.
69
+
70
+ ## Mounting the Engine
71
+
72
+ To access Todoro's routes, mount the engine in your application's routes file:
73
+
74
+ ```ruby
75
+ # config/routes.rb
76
+ Rails.application.routes.draw do
77
+ mount Todoro::Engine, at: "/"
78
+ # Other routes...
79
+ end
80
+ ```
81
+
82
+ Since Todoro dynamically registers routes for `acts_as_taskable` models, no further route configuration is required.
52
83
 
53
84
  ## License
54
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
85
+
86
+ The gem is available as open source under the terms of the [MIT License](MIT-LICENSE).
@@ -0,0 +1,6 @@
1
+ class TaskableModelsConstraint
2
+ def matches?(request)
3
+ taskable_type = request.params[:taskable_type]&.classify
4
+ Todoro.taskable_models.include?(taskable_type)
5
+ end
6
+ end
@@ -1,4 +1,26 @@
1
1
  module Todoro
2
- class ApplicationController < ActionController::Base
2
+ class ApplicationController < ::ApplicationController
3
+ before_action :set_taskable
4
+ before_action :validate_taskable!
5
+
6
+ private
7
+
8
+ def set_taskable
9
+ taskable_type = params[:taskable_type]&.classify
10
+ taskable_id_param = "#{params[:taskable_type]}_id"
11
+ taskable_id = params[taskable_id_param]
12
+
13
+ if taskable_type.present? && taskable_id.present? && Todoro.taskable_models.include?(taskable_type)
14
+ @taskable = taskable_type.safe_constantize&.find_by(id: taskable_id)
15
+ end
16
+
17
+ render json: { error: "Taskable not found" }, status: :not_found unless @taskable
18
+ end
19
+
20
+ def validate_taskable!
21
+ unless @taskable && Todoro.taskable_models.include?(@taskable.class.name)
22
+ render json: { error: "Unauthorized taskable model" }, status: :forbidden
23
+ end
24
+ end
3
25
  end
4
26
  end
@@ -1,7 +1,6 @@
1
1
  module Todoro
2
2
  class TaskListsController < ApplicationController
3
3
  before_action :set_task_list, only: %i[show edit update destroy]
4
- before_action :set_taskable, only: %i[index new create]
5
4
 
6
5
  def index
7
6
  @task_lists = @taskable.task_lists
@@ -16,7 +15,7 @@ module Todoro
16
15
  def create
17
16
  @task_list = @taskable.task_lists.new(task_list_params)
18
17
  if @task_list.save
19
- redirect_to taskable_task_lists_path(taskable: @taskable), notice: "Task list created successfully."
18
+ redirect_to [ @taskable, @task_list ], notice: "Task list created successfully."
20
19
  else
21
20
  render :new
22
21
  end
@@ -26,7 +25,7 @@ module Todoro
26
25
 
27
26
  def update
28
27
  if @task_list.update(task_list_params)
29
- redirect_to taskable_task_lists_path(taskable: @task_list.taskable), notice: "Task list updated successfully."
28
+ redirect_to [ @taskable, @task_list ], notice: "Task list updated successfully."
30
29
  else
31
30
  render :edit
32
31
  end
@@ -35,23 +34,14 @@ module Todoro
35
34
  def destroy
36
35
  taskable = @task_list.taskable
37
36
  @task_list.destroy
38
- redirect_to taskable_task_lists_path(taskable: taskable), notice: "Task list deleted."
37
+
38
+ redirect_to [ @taskable, :task_list ], notice: "Task list deleted."
39
39
  end
40
40
 
41
41
  private
42
42
 
43
43
  def set_task_list
44
- @task_list = TaskList.find(params[:id])
45
- end
46
-
47
- def set_taskable
48
- @taskable = find_taskable
49
- end
50
-
51
- def find_taskable
52
- return unless params[:taskable] && params[:taskable_id]
53
-
54
- params[:taskable].constantize.find(params[:taskable_id])
44
+ @task_list = @taskable.task_lists.find(params[:id])
55
45
  end
56
46
 
57
47
  def task_list_params
@@ -8,9 +8,10 @@ module Todoro
8
8
  end
9
9
 
10
10
  def create
11
- @task = @task_list.tasks.new(task_params)
11
+ @task = @task_list.tasks.build(task_params)
12
+
12
13
  if @task.save
13
- redirect_to todoro.taskable_task_list_path(taskable: @task_list.taskable.class.name, taskable_id: @task_list.taskable.id, id: @task_list.id), notice: "Task created successfully."
14
+ redirect_to [ @taskable, @task_list ], notice: "Task created successfully."
14
15
  else
15
16
  render :new
16
17
  end
@@ -20,7 +21,7 @@ module Todoro
20
21
 
21
22
  def update
22
23
  if @task.update(task_params)
23
- redirect_to @task_list, notice: "Task updated successfully."
24
+ redirect_to [ @taskable, @task_list ], notice: "Task updated successfully."
24
25
  else
25
26
  render :edit
26
27
  end
@@ -28,26 +29,26 @@ module Todoro
28
29
 
29
30
  def destroy
30
31
  @task.destroy
31
- redirect_to @task_list, notice: "Task deleted."
32
+ redirect_to [ @taskable, @task_list ], notice: "Task deleted."
32
33
  end
33
34
 
34
35
  def complete
35
36
  @task.update(status: "completed")
36
- redirect_to todoro.taskable_task_list_path(taskable: @task_list.taskable.class.name, taskable_id: @task_list.taskable.id, id: @task_list.id), notice: "Task marked as completed."
37
+ redirect_to [ @taskable, @task_list ], notice: "Task marked as completed."
37
38
  end
38
39
 
39
40
  private
40
41
 
41
- def set_task_list
42
- @task_list = Todoro::TaskList.find(params[:task_list_id])
43
- end
44
-
45
42
  def set_task
46
43
  @task = @task_list.tasks.find(params[:id])
47
44
  end
48
45
 
46
+ def set_task_list
47
+ @task_list = @taskable.task_lists.find(params[:task_list_id])
48
+ end
49
+
49
50
  def task_params
50
- params.require(:task).permit(:title, :description, :expiry_date, :status)
51
+ params.require(:task).permit(:title, :description, :status, :expiry_date)
51
52
  end
52
53
  end
53
54
  end
@@ -4,7 +4,6 @@ module Todoro
4
4
  has_many :reminders, dependent: :destroy
5
5
 
6
6
  validates :title, presence: true
7
- validates :description, presence: true
8
7
  validates :status, presence: true, inclusion: { in: %w[pending completed] }
9
8
 
10
9
  enum :status, { pending: "pending", completed: "completed" }
@@ -1,12 +1,9 @@
1
- <%= form_with model: [taskable, task_list], local: true do |form| %>
1
+ <%= form_with model: task_list, url: polymorphic_path([@taskable, task_list]), local: true do |form| %>
2
2
  <div class="form-group">
3
3
  <%= form.label :name %>
4
4
  <%= form.text_field :name, class: "form-control", required: true %>
5
5
  </div>
6
6
 
7
- <%= form.hidden_field :taskable_id, value: taskable.id %>
8
- <%= form.hidden_field :taskable_type, value: taskable.class.name %>
9
7
 
10
8
  <%= form.submit class: "btn btn-success" %>
11
9
  <% end %>
12
-
@@ -2,4 +2,4 @@
2
2
 
3
3
  <%= render "form", task_list: @task_list %>
4
4
 
5
- <%= link_to "Back to Task Lists", task_lists_path, class: "btn btn-secondary" %>
5
+ <%= link_to "Back to Task Lists", polymorphic_path([@task_list.taskable, :task_lists]), class: "btn btn-secondary" %>
@@ -1,15 +1,15 @@
1
1
  <h1>Task Lists for <%= @taskable.class.name %>: <%= @taskable.try(:name) || "Unknown" %></h1>
2
2
 
3
- <%= link_to "Create New Task List", new_taskable_task_list_path(taskable: @taskable.class.name, taskable_id: @taskable.id), class: "btn btn-primary" %>
3
+ <%= link_to "Create New Task List", new_polymorphic_path([@taskable, :task_list]), class: "btn btn-primary" %>
4
4
 
5
5
  <ul>
6
6
  <% @task_lists.each do |task_list| %>
7
7
  <li>
8
- <%= link_to task_list.name, taskable_task_list_path(taskable: @taskable.class.name, taskable_id: @taskable.id, id: task_list.id) %>
9
- (<%= link_to "Edit", edit_taskable_task_list_path(taskable: @taskable.class.name, taskable_id: @taskable.id, id: task_list.id) %> |
10
- <%= link_to "Delete", taskable_task_list_path(taskable: @taskable.class.name, taskable_id: @taskable.id, id: task_list.id), method: :delete, data: { confirm: "Are you sure?" } %>)
8
+ <%= link_to task_list.name, polymorphic_path([ @taskable, task_list]) %>
9
+ (<%= link_to "Edit", edit_polymorphic_path([@taskable, task_list ])%> |
10
+ <%= link_to "Delete", polymorphic_path([ @taskable, task_list ]), method: :delete, data: { confirm: "Are you sure?" } %>)
11
11
  </li>
12
12
  <% end %>
13
13
  </ul>
14
14
 
15
- <%= link_to "Back", root_path, class: "btn btn-secondary" %>
15
+ <%= link_to "Back", polymorphic_path(@taskable), class: "btn btn-secondary" %>
@@ -2,4 +2,4 @@
2
2
 
3
3
  <%= render "form", task_list: @task_list %>
4
4
 
5
- <%= link_to "Back to Task Lists", task_lists_path, class: "btn btn-secondary" %>
5
+ <%= link_to "Back to Task Lists", polymorphic_path([@taskable, @task_list]), class: "btn btn-secondary" %>
@@ -1,21 +1,24 @@
1
1
  <h1><%= @task_list.name %></h1>
2
2
 
3
- <%= link_to "Edit", edit_taskable_task_list_path(taskable: @task_list.taskable.class.name, taskable_id: @task_list.taskable.id, id: @task_list.id), class: "btn btn-warning" %>
4
- <%= link_to "Delete", taskable_task_list_path(taskable: @task_list.taskable.class.name, taskable_id: @task_list.taskable.id, id: @task_list.id), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-danger" %>
3
+ <%= link_to "Edit", edit_polymorphic_path([@task_list.taskable, @task_list]), class: "btn btn-warning" %>
4
+
5
+ <%= button_to "Delete", polymorphic_path([@task_list.taskable, @task_list]), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-danger btn-link" %>
5
6
 
6
7
  <h2>Tasks</h2>
7
- <%= link_to "Add Task", new_taskable_task_list_task_path(taskable: @task_list.taskable.class.name, taskable_id: @task_list.taskable.id, task_list_id: @task_list.id), class: "btn btn-primary" %>
8
+ <%= link_to "Add Task", new_polymorphic_path([@task_list.taskable, @task_list, :task]), class: "btn btn-primary" %>
8
9
 
9
10
  <ul>
10
11
  <% @task_list.tasks.each do |task| %>
11
12
  <li>
12
13
  <%= task.title %> - <%= task.status %>
13
- (<%= link_to "Edit", edit_taskable_task_list_task_path(taskable: @task_list.taskable.class.name, taskable_id: @task_list.taskable.id, task_list_id: @task_list.id, id: task.id) %> |
14
- <%= link_to "Complete", complete_taskable_task_list_task_path(taskable: @task_list.taskable.class.name, taskable_id: @task_list.taskable.id, task_list_id: @task_list.id, id: task.id), method: :patch if task.status == "pending" %> |
15
- <%= link_to "Delete", taskable_task_list_task_path(taskable: @task_list.taskable.class.name, taskable_id: @task_list.taskable.id, task_list_id: @task_list.id, id: task.id), method: :delete, data: { confirm: "Are you sure?" } %>)
14
+ <%= link_to "Edit", edit_polymorphic_path([@task_list.taskable, @task_list, task]) %> |
15
+
16
+ <%= button_to "Complete", polymorphic_path([@task_list.taskable, @task_list, task], action: :complete), method: :patch, class: "btn btn-success btn-link", data: { turbo: true } if task.status == "pending" %>
17
+
18
+ <%= button_to "Delete", polymorphic_path([@task_list.taskable, @task_list, task]), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-danger btn-link" %>
19
+
16
20
  </li>
17
21
  <% end %>
18
22
  </ul>
19
23
 
20
- <%= link_to "Back to Task Lists", taskable_task_lists_path(taskable: @task_list.taskable.class.name, taskable_id: @task_list.taskable.id), class: "btn btn-secondary" %>
21
-
24
+ <%= link_to "Back to Task Lists", polymorphic_path([@task_list.taskable, :task_lists]), class: "btn btn-secondary" %>
@@ -1,4 +1,4 @@
1
- <%= form_with model: [task.task_list, task], local: true do |form| %>
1
+ <%= form_with model: task, url: polymorphic_path([@taskable, @task_list, @task]), local: true do |form| %>
2
2
  <div class="form-group">
3
3
  <%= form.label :title %>
4
4
  <%= form.text_field :title, class: "form-control", required: true %>
@@ -6,7 +6,7 @@
6
6
 
7
7
  <div class="form-group">
8
8
  <%= form.label :description %>
9
- <%= form.text_area :description, class: "form-control", required: true %>
9
+ <%= form.text_area :description, class: "form-control" %>
10
10
  </div>
11
11
 
12
12
  <div class="form-group">
@@ -2,4 +2,4 @@
2
2
 
3
3
  <%= render "form", task: @task %>
4
4
 
5
- <%= link_to "Back to Task List", task_list_path(@task_list), class: "btn btn-secondary" %>
5
+ <%= link_to "Back to Task List", polymorphic_path([@taskable, @task_list]), class: "btn btn-secondary" %>
@@ -2,4 +2,4 @@
2
2
 
3
3
  <%= render "form", task: @task %>
4
4
 
5
- <%= link_to "Back to Task List", task_list_path(@task_list), class: "btn btn-secondary" %>
5
+ <%= link_to "Back to Task List", polymorphic_path([@taskable, @task_list]), class: "btn btn-secondary" %>
data/config/routes.rb CHANGED
@@ -1,9 +1,11 @@
1
1
  Todoro::Engine.routes.draw do
2
- resources :taskables, only: [] do
3
- resources :task_lists do
4
- resources :tasks do
5
- member do
6
- patch :complete
2
+ Todoro.taskable_models.each do |model|
3
+ resource_name = model.underscore.pluralize.to_sym
4
+
5
+ resources resource_name, defaults: { taskable_type: model.downcase }, constraints: TaskableModelsConstraint.new do
6
+ resources :task_lists do
7
+ resources :tasks, only: [ :new, :edit, :create, :update, :destroy ] do
8
+ patch :complete, on: :member
7
9
  end
8
10
  end
9
11
  end
@@ -0,0 +1,10 @@
1
+ class CreateTodoroTaskLists < ActiveRecord::Migration[ActiveRecord::Migration.current_version]
2
+ def change
3
+ create_table :todoro_task_lists do |t|
4
+ t.string :name, null: false
5
+ t.references :taskable, polymorphic: true, null: false
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ class CreateTodoroTasks < ActiveRecord::Migration[ActiveRecord::Migration.current_version]
2
+ def change
3
+ create_table :todoro_tasks do |t|
4
+ t.string :title
5
+ t.text :description, null: false # Description is required
6
+ t.datetime :expiry_date
7
+ t.string :status, null: false, default: "pending" # Status is required
8
+ t.references :task_list, null: false, foreign_key: { to_table: :todoro_task_lists }
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ # Enforce status constraint
14
+ execute <<-SQL
15
+ ALTER TABLE todoro_tasks
16
+ ADD CONSTRAINT status_check
17
+ CHECK (status IN ('pending', 'completed'));
18
+ SQL
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ class CreateTodoroReminders < ActiveRecord::Migration[ActiveRecord::Migration.current_version]
2
+ def change
3
+ create_table :todoro_reminders do |t|
4
+ t.references :task, null: false, foreign_key: { to_table: :todoro_tasks }
5
+ t.datetime :remind_at, null: false # Enforce remind_at is mandatory
6
+
7
+
8
+
9
+ t.timestamps
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ class AddIndexesToTodoroTasks < ActiveRecord::Migration[ActiveRecord::Migration.current_version]
2
+ def change
3
+ add_index :todoro_task_lists, [ :taskable_type, :taskable_id ]
4
+ add_index :todoro_tasks, :status
5
+ add_index :todoro_tasks, :expiry_date
6
+ end
7
+ end
@@ -1,3 +1,3 @@
1
1
  module Todoro
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.5"
3
3
  end
data/lib/todoro.rb CHANGED
@@ -3,4 +3,11 @@ require "todoro/engine"
3
3
  require "todoro/taskable"
4
4
 
5
5
  module Todoro
6
+ def self.taskable_models
7
+ Rails.application.eager_load!
8
+
9
+ ActiveRecord::Base.descendants.select do |model|
10
+ model.included_modules.include?(Todoro::Taskable)
11
+ end.map(&:name)
12
+ end
6
13
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: todoro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Despo Pentara
8
8
  - Evangelos Giataganas
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-02-15 00:00:00.000000000 Z
11
+ date: 2025-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -37,6 +37,7 @@ files:
37
37
  - README.md
38
38
  - Rakefile
39
39
  - app/assets/stylesheets/todoro/application.css
40
+ - app/constraints/taskable_models_constraint.rb
40
41
  - app/controllers/todoro/application_controller.rb
41
42
  - app/controllers/todoro/task_lists_controller.rb
42
43
  - app/controllers/todoro/tasks_controller.rb
@@ -45,7 +46,6 @@ files:
45
46
  - app/helpers/todoro/tasks_helper.rb
46
47
  - app/jobs/todoro/application_job.rb
47
48
  - app/mailers/todoro/application_mailer.rb
48
- - app/models/concerns/taskable.rb
49
49
  - app/models/todoro/application_record.rb
50
50
  - app/models/todoro/reminder.rb
51
51
  - app/models/todoro/task.rb
@@ -60,6 +60,10 @@ files:
60
60
  - app/views/todoro/tasks/edit.html.erb
61
61
  - app/views/todoro/tasks/new.html.erb
62
62
  - config/routes.rb
63
+ - db/migrate/20250215225042_create_todoro_task_lists.rb
64
+ - db/migrate/20250215225043_create_todoro_tasks.rb
65
+ - db/migrate/20250215225044_create_todoro_reminders.rb
66
+ - db/migrate/20250215225045_add_indexes_to_todoro_tasks.rb
63
67
  - lib/generators/todoro/install_generator.rb
64
68
  - lib/generators/todoro/templates/add_indexes_to_todoro_tasks.rb
65
69
  - lib/generators/todoro/templates/create_todoro_reminders.rb
@@ -1,21 +0,0 @@
1
- module Todoro
2
- module Taskable
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- has_many :task_lists, as: :taskable, class_name: "Todoro::TaskList", dependent: :destroy
7
- end
8
-
9
- def tasks
10
- Todoro::Task.joins(:task_list).where(task_lists: { taskable_id: id, taskable_type: self.class.name })
11
- end
12
-
13
- def create_task_list(name)
14
- task_lists.create(name: name)
15
- end
16
-
17
- def add_task_to_list(task_list, title, description, expiry_date = nil)
18
- task_list.tasks.create(title: title, description: description, expiry_date: expiry_date, status: "pending")
19
- end
20
- end
21
- end