timet 1.6.2 → 1.6.3

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: 60cf8b709509db9c6a6a5bd27ed60db0c3c08a22f44aeced1d758d0790f74e5a
4
- data.tar.gz: 7dd85ae0a812e4102218c553256740f184313008b694d8478eced986392ecc90
3
+ metadata.gz: 84fa974afe30946a8b292569e74917c3992097e0ae652339320981f2951f298d
4
+ data.tar.gz: dc6cf2d3f5bd30feebb93db4aa07c436203e53bf1d9d0dbba5166695b622b021
5
5
  SHA512:
6
- metadata.gz: cfa8731849d0ac4f4e1b7d3da1d676e3b93a2e3c730d332b33a12bc8c589ef33214a36f5a37c4e6793ff07fdcf534b085a10b6e412c15d4052f2075c5b4fe02a
7
- data.tar.gz: 226ebaef071767418b9ac6257d334e45e1aff0d65fa19968deb5eba198524f3145093b520e2c7e6c69e31ba5b222018aa819e6f03a5f63c5bc41a18d233a1d2b
6
+ metadata.gz: 49cf7cc38f9e44687d27bee93a8de5678963fd92a9589241e38635afcdf4b86f8229d1c401483d2d6e955ee83e5423ca10f832f4beb0101873e60f5d0ff34821
7
+ data.tar.gz: 2098f1a3c297277bd07425961b3e84425a1189ae5984e21c35649a3a2c517a5a175dfd5b477abb59ba49f78925a9c4ec872006d3be025ddc4ee363bf86ff7004
data/.reek.yml CHANGED
@@ -3,4 +3,20 @@ detectors:
3
3
  TooManyStatements:
4
4
  max_statements: 7
5
5
  UncommunicativeVariableName:
6
- enabled: false
6
+ enabled: false
7
+ FeatureEnvy:
8
+ exclude:
9
+ - fetch_last_id
10
+ - last_item
11
+ - update_time_columns
12
+ TooManyMethods:
13
+ max_methods: 20
14
+ exclude:
15
+ - ValidationEditor
16
+ LongParameterList:
17
+ exclude:
18
+ - process_and_update_time_field
19
+ - invalid_time_value?
20
+ DataClump:
21
+ exclude:
22
+ - TimeValidationHelpers
data/.rubocop.yml CHANGED
@@ -6,6 +6,10 @@ AllCops:
6
6
  - timet.gemspec
7
7
  - "vendor/**/*" # Exclude vendor/bundle gems
8
8
 
9
+ Metrics/ModuleLength:
10
+ Exclude:
11
+ - 'lib/timet/tag_distribution.rb'
12
+
9
13
  Metrics/MethodLength:
10
14
  Max: 15
11
15
 
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## [1.6.3] - 2026-02-28
2
+
3
+ **Improvements:**
4
+
5
+ - **Validation Architecture:** Refactored validation logic from `ValidationEditHelper` module to `ValidationEditor` class for better encapsulation and testability.
6
+ - **Synchronization & Storage:** Simplified `DatabaseSyncer` and `S3Supabase` by extracting modular helper methods, introducing `S3Config` module, and `S3ObjectRef` struct.
7
+ - **Reporting Features:** Added `export_csv` and `export_icalendar` methods to `TimeReport` for enhanced data portability.
8
+ - **Database Refactoring:** Converted `Timet::Database` methods to class methods where appropriate, extracted column checks, and simplified item fetching logic.
9
+ - **Tag Distribution:** Split `TagDistribution` into dedicated formatting and logic modules using a Context struct for clearer rendering.
10
+ - **Dependencies:** Updated `aws-sdk-s3` to `~> 1.213`, removed unused `httparty` gem, and bumped RuboCop, parser, and thor dependencies.
11
+ - **Code Quality:** Updated `.reek.yml` and RuboCop configs to accommodate new method structures, exclusions, and added code coverage badge to README.
12
+ - **Cleanup:** Removed obsolete helper modules (`item_data_helper`, `time_report_helper`, `time_update_helper`, `time_validation_helper`).
13
+
14
+ **Bug Fixes:**
15
+
16
+ - **Status Determination:** Added nil guards to `determine_status` to prevent runtime errors during item status checks.
17
+ - **Data Integrity:** Resolved duplicated `get_item_values` logic in syncer to ensure consistency during inserts and updates.
18
+ - **Validation Logic:** Updated validation methods to correctly handle end datetime adjustments for the next day.
19
+
1
20
  ## [1.6.2] - 2025-12-28
2
21
 
3
22
  **Improvements:**
@@ -440,7 +459,6 @@
440
459
  **Improvements:**
441
460
 
442
461
  - **Refactor `TimeReport` to use `TimeReportHelper` module for utility methods:**
443
-
444
462
  - Extracted utility methods (`add_hashes`, `date_ranges`, `format_item`, `valid_date_format?`) into a new `TimeReportHelper` module.
445
463
  - Updated `TimeReport` class to include `TimeReportHelper` module.
446
464
  - Removed redundant utility methods from `TimeReport` class.
@@ -450,7 +468,6 @@
450
468
  - Adjusted formatting in `total` method for better alignment.
451
469
 
452
470
  - **Refactor `Timet::Formatter` to improve readability and modularity:**
453
-
454
471
  - Introduced a constant `CHAR_MAPPING` to store block characters for different value ranges.
455
472
  - Refactored `format_notes` method to use a more descriptive variable name for the maximum length.
456
473
  - Updated `format_tag_distribution` method to accept `colors` parameter and pass it to `process_and_print_tags`.
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  [![Gem Version](https://badge.fury.io/rb/timet.svg)](https://badge.fury.io/rb/timet)
2
2
  ![timet workflow](https://github.com/frankvielma/timet/actions/workflows/ci.yml/badge.svg)
3
3
  [![Maintainability](https://qlty.sh/gh/frankvielma/projects/timet/maintainability.svg)](https://qlty.sh/gh/frankvielma/projects/timet)
4
+ [![Code Coverage](https://qlty.sh/gh/frankvielma/projects/timet/coverage.svg)](https://qlty.sh/gh/frankvielma/projects/timet)
4
5
 
5
6
  # Timet
6
7
 
@@ -4,7 +4,7 @@ require 'thor'
4
4
  require 'tty-prompt'
5
5
  require 'icalendar'
6
6
  require_relative 's3_supabase'
7
- require_relative 'validation_edit_helper'
7
+ require_relative 'validation_editor'
8
8
  require_relative 'application_helper'
9
9
  require_relative 'time_helper'
10
10
  require_relative 'version'
@@ -22,7 +22,6 @@ module Timet
22
22
  # - delete: Delete a task
23
23
  # - cancel: Cancel active time tracking
24
24
  class Application < Thor
25
- include ValidationEditHelper
26
25
  include ApplicationHelper
27
26
  include TimeHelper
28
27
 
@@ -271,7 +270,8 @@ Update start time => tt edit 12 start 12:33'
271
270
  new_value = prompt_for_new_value(item, field)
272
271
  end
273
272
 
274
- updated_item = validate_and_update(item, field, new_value)
273
+ editor = Timet::ValidationEditor.new(item, @db)
274
+ updated_item = editor.update(field, new_value)
275
275
  @db.update_item(id, field, updated_item[FIELD_INDEX[field]])
276
276
  display_item(updated_item)
277
277
  end
@@ -24,7 +24,7 @@ module Timet
24
24
  # @note The method creates a new SQLite3 database connection and initializes the necessary tables if they
25
25
  # do not already exist.
26
26
  def initialize(database_path = DEFAULT_DATABASE_PATH)
27
- move_old_database_file(database_path)
27
+ self.class.move_old_database_file(database_path)
28
28
 
29
29
  @db = SQLite3::Database.new(database_path)
30
30
  create_table
@@ -76,15 +76,17 @@ module Timet
76
76
  raise 'Invalid table name' unless table_name == 'items'
77
77
  raise 'Invalid column name' unless /\A[a-zA-Z0-9_]+\z/.match?(new_column_name)
78
78
  raise 'Invalid date type' unless %w[INTEGER TEXT BOOLEAN].include?(date_type)
79
-
80
- result = execute_sql("SELECT count(*) FROM pragma_table_info('items') where name=?", [new_column_name])
81
- column_exists = result[0][0].positive?
82
- return if column_exists
79
+ return if column_exists?(new_column_name)
83
80
 
84
81
  execute_sql("ALTER TABLE #{table_name} ADD COLUMN #{new_column_name} #{date_type}")
85
82
  puts "Column '#{new_column_name}' added to table '#{table_name}'."
86
83
  end
87
84
 
85
+ def column_exists?(new_column_name)
86
+ execute_sql("SELECT count(*) FROM pragma_table_info('items') where name=?",
87
+ [new_column_name]).first.first.positive?
88
+ end
89
+
88
90
  # Inserts a new item into the items table.
89
91
  #
90
92
  # @param start [Integer] The start time of the item.
@@ -150,8 +152,8 @@ module Timet
150
152
  #
151
153
  # @note The method executes SQL to fetch the ID of the last inserted item.
152
154
  def fetch_last_id
153
- result = execute_sql('SELECT id FROM items WHERE deleted IS NULL OR deleted = 0 ORDER BY id DESC LIMIT 1')
154
- result.empty? ? nil : result[0][0]
155
+ execute_sql('SELECT id FROM items WHERE deleted IS NULL OR deleted = 0 ORDER BY id DESC LIMIT 1')
156
+ .then { |result| result.empty? ? nil : result.first.first }
155
157
  end
156
158
 
157
159
  # Fetches the last item from the items table.
@@ -163,8 +165,8 @@ module Timet
163
165
  #
164
166
  # @note The method executes SQL to fetch the last item from the 'items' table.
165
167
  def last_item
166
- result = execute_sql('SELECT * FROM items WHERE deleted IS NULL OR deleted = 0 ORDER BY id DESC LIMIT 1')
167
- result.empty? ? nil : result[0]
168
+ execute_sql('SELECT * FROM items WHERE deleted IS NULL OR deleted = 0 ORDER BY id DESC LIMIT 1')
169
+ .then { |result| result.empty? ? nil : result.first }
168
170
  end
169
171
 
170
172
  # Finds an item by its ID.
@@ -180,8 +182,7 @@ module Timet
180
182
  # @note If the item is found, it returns the item as an array.
181
183
  # @note If the item is not found, it returns nil.
182
184
  def find_item(id)
183
- result = execute_sql('SELECT * FROM items WHERE id = ?', [id])
184
- result.first.dup if result.any? # Add .dup to create a copy
185
+ execute_sql('SELECT * FROM items WHERE id = ?', [id]).first&.dup
185
186
  end
186
187
 
187
188
  # Fetches all items from the items table that have a start time greater than or equal to today.
@@ -212,8 +213,8 @@ module Timet
212
213
  #
213
214
  # @see StatusHelper#determine_status
214
215
  def item_status(id = nil)
215
- id = fetch_last_id if id.nil?
216
- determine_status(find_item(id))
216
+ id ||= fetch_last_id
217
+ self.class.determine_status(find_item(id))
217
218
  end
218
219
 
219
220
  # Executes a SQL query and returns the result.
@@ -281,8 +282,8 @@ module Timet
281
282
  # @note The method checks if the result set is empty and returns :no_items if true.
282
283
  # @note If the last item in the result set has no end time, it returns :in_progress.
283
284
  # @note If the last item in the result set has an end time, it returns :complete.
284
- def determine_status(result)
285
- return :no_items if result.nil?
285
+ def self.determine_status(result)
286
+ return :no_items unless result
286
287
 
287
288
  last_item_end = result[2]
288
289
  return :in_progress unless last_item_end
@@ -293,11 +294,12 @@ module Timet
293
294
  # Moves the old database file to the new location if it exists.
294
295
  #
295
296
  # @param database_path [String] The path to the new SQLite database file.
296
- def move_old_database_file(database_path)
297
+ def self.move_old_database_file(database_path)
297
298
  old_file = File.join(Dir.home, '.timet.db')
298
299
  return unless File.exist?(old_file)
299
300
 
300
- FileUtils.mkdir_p(File.dirname(database_path)) unless File.directory?(File.dirname(database_path))
301
+ dir = File.dirname(database_path)
302
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
301
303
  FileUtils.mv(old_file, database_path)
302
304
  end
303
305
 
@@ -317,14 +319,12 @@ module Timet
317
319
  # @raise [StandardError] If there is an issue executing the SQL queries, an error may be raised.
318
320
  #
319
321
  def update_time_columns
320
- result = execute_sql('SELECT * FROM items WHERE updated_at IS NULL OR created_at IS NULL')
321
- result.each do |item|
322
- id = item[0]
323
- start_time = item[1]
324
- end_time = item[2]
325
- fallback_time = end_time || start_time || Time.now.to_i
326
- execute_sql('UPDATE items SET updated_at = ?, created_at = ? WHERE id = ?', [fallback_time, fallback_time, id])
327
- end
322
+ execute_sql('SELECT * FROM items WHERE updated_at IS NULL OR created_at IS NULL')
323
+ .each { |item| update_timestamp_for_item(item[0], item[2] || item[1] || Time.now.to_i) }
324
+ end
325
+
326
+ def update_timestamp_for_item(id, fallback_time)
327
+ execute_sql('UPDATE items SET updated_at = ?, created_at = ? WHERE id = ?', [fallback_time, fallback_time, id])
328
328
  end
329
329
  end
330
330
  end
@@ -3,51 +3,41 @@
3
3
  module Timet
4
4
  # Module responsible for synchronizing local and remote databases
5
5
  module DatabaseSyncer
6
- # Fields used in item operations
7
6
  ITEM_FIELDS = %w[start end tag notes pomodoro updated_at created_at deleted].freeze
8
7
 
9
- # Handles the synchronization process when differences are detected between databases
10
- #
11
- # @param local_db [SQLite3::Database] The local database connection
12
- # @param remote_storage [S3Supabase] The remote storage client for cloud operations
13
- # @param bucket [String] The S3 bucket name
14
- # @param local_db_path [String] Path to the local database file
15
- # @param remote_path [String] Path to the downloaded remote database file
16
- # @return [void]
17
- # @note This method attempts to sync the databases and handles any errors that occur during the process
18
8
  def handle_database_differences(*args)
19
9
  local_db, remote_storage, bucket, local_db_path, remote_path = args
20
10
  puts 'Differences detected between local and remote databases'
21
11
  begin
22
12
  sync_with_remote_database(local_db, remote_path, remote_storage, bucket, local_db_path)
23
13
  rescue SQLite3::Exception => e
24
- handle_sync_error(e, remote_storage, bucket, local_db_path)
14
+ handle_sync_error(e, remote_storage: remote_storage, bucket: bucket, local_db_path: local_db_path)
25
15
  end
26
16
  end
27
17
 
28
- # Handles errors that occur during database synchronization
29
- #
30
- # @param error [SQLite3::Exception] The error that occurred during sync
31
- # @param remote_storage [S3Supabase] The remote storage client for cloud operations
32
- # @param bucket [String] The S3 bucket name
33
- # @param local_db_path [String] Path to the local database file
34
- # @return [void]
35
- # @note When sync fails, this method falls back to uploading the local database
36
- def handle_sync_error(error, remote_storage, bucket, local_db_path)
18
+ def handle_sync_error(error, *args)
19
+ first_arg = args.first
20
+ if first_arg.is_a?(Hash)
21
+ options = first_arg
22
+ remote_storage, bucket, local_db_path = options.values_at(:remote_storage, :bucket, :local_db_path)
23
+ else
24
+ remote_storage, bucket, local_db_path = args
25
+ end
26
+ report_sync_error(error)
27
+ upload_local_database(remote_storage, bucket, local_db_path)
28
+ end
29
+
30
+ def report_sync_error(error)
37
31
  puts "Error opening remote database: #{error.message}"
38
32
  puts 'Uploading local database to replace corrupted remote database'
33
+ end
34
+ module_function :report_sync_error
35
+
36
+ def upload_local_database(remote_storage, bucket, local_db_path)
39
37
  remote_storage.upload_file(bucket, local_db_path, 'timet.db')
40
38
  end
39
+ module_function :upload_local_database
41
40
 
42
- # Performs the actual database synchronization by setting up connections and syncing data
43
- #
44
- # @param local_db [SQLite3::Database] The local database connection
45
- # @param remote_path [String] Path to the remote database file
46
- # @param remote_storage [S3Supabase] The remote storage client for cloud operations
47
- # @param bucket [String] The S3 bucket name
48
- # @param local_db_path [String] Path to the local database file
49
- # @return [void]
50
- # @note Configures both databases to return results as hashes for consistent data handling
51
41
  def sync_with_remote_database(*args)
52
42
  local_db, remote_path, remote_storage, bucket, local_db_path = args
53
43
  db_remote = open_remote_database(remote_path)
@@ -56,12 +46,6 @@ module Timet
56
46
  sync_databases(local_db, db_remote, remote_storage, bucket, local_db_path)
57
47
  end
58
48
 
59
- # Opens and validates a connection to the remote database
60
- #
61
- # @param remote_path [String] Path to the remote database file
62
- # @return [SQLite3::Database] The initialized database connection
63
- # @raise [RuntimeError] If the database connection cannot be established
64
- # @note Validates that the database connection is properly initialized
65
49
  def open_remote_database(remote_path)
66
50
  db_remote = SQLite3::Database.new(remote_path)
67
51
  raise 'Failed to initialize remote database' unless db_remote
@@ -69,15 +53,6 @@ module Timet
69
53
  db_remote
70
54
  end
71
55
 
72
- # Synchronizes the local and remote databases by comparing and merging their items
73
- #
74
- # @param local_db [SQLite3::Database] The local database connection
75
- # @param remote_db [SQLite3::Database] The remote database connection
76
- # @param remote_storage [S3Supabase] The remote storage client for cloud operations
77
- # @param bucket [String] The S3 bucket name
78
- # @param local_db_path [String] Path to the local database file
79
- # @return [void]
80
- # @note This method orchestrates the entire database synchronization process
81
56
  def sync_databases(*args)
82
57
  local_db, remote_db, remote_storage, bucket, local_db_path = args
83
58
  process_database_items(local_db, remote_db)
@@ -85,130 +60,117 @@ module Timet
85
60
  puts 'Database sync completed'
86
61
  end
87
62
 
88
- # Processes items from both databases and syncs them
89
- #
90
- # @param local_db [SQLite3::Database] The local database connection
91
- # @param remote_db [SQLite3::Database] The remote database connection
92
- # @return [void]
93
63
  def process_database_items(local_db, remote_db)
94
64
  remote_items = remote_db.execute('SELECT * FROM items ORDER BY updated_at DESC')
95
65
  local_items = local_db.execute_sql('SELECT * FROM items ORDER BY updated_at DESC')
96
66
 
97
- sync_items_by_id(
98
- local_db,
99
- items_to_hash(local_items),
100
- items_to_hash(remote_items)
101
- )
67
+ sync_items_by_id(local_db, items_to_hash(local_items), items_to_hash(remote_items))
102
68
  end
103
69
 
104
- # Syncs items between local and remote databases based on their IDs
105
- #
106
- # @param local_db [SQLite3::Database] The local database connection
107
- # @param local_items_by_id [Hash] Local items indexed by ID
108
- # @param remote_items_by_id [Hash] Remote items indexed by ID
109
- # @return [void]
110
70
  def sync_items_by_id(local_db, local_items_by_id, remote_items_by_id)
111
71
  all_item_ids = (remote_items_by_id.keys + local_items_by_id.keys).uniq
72
+ all_item_ids.each { |id| sync_single_item(local_db, id, local_items_by_id, remote_items_by_id) }
73
+ end
112
74
 
113
- all_item_ids.each do |id|
114
- if !remote_items_by_id[id]
115
- puts "Local item #{id} will be uploaded"
116
- elsif !local_items_by_id[id]
117
- puts "Adding remote item #{id} to local"
118
- insert_item_from_hash(local_db, remote_items_by_id[id])
119
- else
120
- process_existing_item(id, local_items_by_id[id], remote_items_by_id[id], local_db)
121
- end
75
+ def sync_single_item(*args)
76
+ local_db, id, local_items_by_id, remote_items_by_id = args
77
+ remote_item = remote_items_by_id[id]
78
+ local_item = local_items_by_id[id]
79
+
80
+ if !remote_item
81
+ log_local_only(id)
82
+ elsif !local_item
83
+ add_remote_item(local_db, id, remote_item)
84
+ else
85
+ merge_item(local_db, id, local_item, remote_item)
122
86
  end
123
87
  end
124
88
 
125
- # Inserts a new item into the database from a hash
126
- #
127
- # @param db [SQLite3::Database] The database connection
128
- # @param item [Hash] Hash containing item data
129
- # @return [void]
89
+ def log_local_only(id)
90
+ puts "Local item #{id} will be uploaded"
91
+ end
92
+ module_function :log_local_only
93
+
94
+ def add_remote_item(local_db, id, remote_item)
95
+ puts "Adding remote item #{id} to local"
96
+ insert_item_from_hash(local_db, remote_item)
97
+ end
98
+
99
+ def process_existing_item(id, local_item, remote_item, local_db)
100
+ merge_item(local_db, id, local_item, remote_item)
101
+ end
102
+
103
+ def merge_item(*args)
104
+ local_db, id, local_item, remote_item = args
105
+ local_time = extract_timestamp(local_item)
106
+ remote_time = extract_timestamp(remote_item)
107
+
108
+ return resolve_remote_wins(local_db, id, remote_item) if remote_wins?(remote_item, remote_time, local_time)
109
+
110
+ log_local_wins(id, local_item)
111
+ end
112
+
113
+ def resolve_remote_wins(local_db, id, remote_item)
114
+ puts format_status_message(id, remote_item, 'Remote')
115
+ update_item_from_hash(local_db, remote_item)
116
+ :local_update
117
+ end
118
+
119
+ def log_local_wins(id, local_item)
120
+ puts format_status_message(id, local_item, 'Local')
121
+ :remote_update
122
+ end
123
+
130
124
  def insert_item_from_hash(db, item)
131
125
  fields = ['id', *ITEM_FIELDS].join(', ')
132
126
  placeholders = Array.new(ITEM_FIELDS.length + 1, '?').join(', ')
133
127
  db.execute_sql(
134
128
  "INSERT INTO items (#{fields}) VALUES (#{placeholders})",
135
- get_item_values(item, include_id_at_start: true)
129
+ get_insert_values(item)
136
130
  )
137
131
  end
138
132
 
139
- # Processes an item that exists in both databases
140
- #
141
- # @param id [Integer] Item ID
142
- # @param local_item [Hash] Local database item
143
- # @param remote_item [Hash] Remote database item
144
- # @param local_db [SQLite3::Database] Local database connection
145
- # @return [Symbol] :local_update if local was updated, :remote_update if remote needs update
146
- def process_existing_item(*args)
147
- id, local_item, remote_item, local_db = args
148
- local_time = local_item['updated_at'].to_i
149
- remote_time = remote_item['updated_at'].to_i
150
-
151
- if remote_wins?(remote_item, remote_time, local_time)
152
- puts format_status_message(id, remote_item, 'Remote')
153
- update_item_from_hash(local_db, remote_item)
154
- :local_update
155
- elsif local_time > remote_time
156
- puts format_status_message(id, local_item, 'Local')
157
- :remote_update
158
- end
133
+ def update_item_from_hash(db, item)
134
+ fields = "#{ITEM_FIELDS.join(' = ?, ')} = ?"
135
+ db.execute_sql(
136
+ "UPDATE items SET #{fields} WHERE id = ?",
137
+ get_update_values(item)
138
+ )
139
+ end
140
+
141
+ def extract_timestamp(item)
142
+ item['updated_at'].to_i
159
143
  end
144
+ module_function :extract_timestamp
160
145
 
161
- # Converts database items to a hash indexed by ID
162
- #
163
- # @param items [Array<Hash>] Array of database items
164
- # @return [Hash] Items indexed by ID
165
146
  def items_to_hash(items)
166
147
  items.to_h { |item| [item['id'], item] }
167
148
  end
168
149
 
169
- # Determines if remote item should take precedence
170
- #
171
- # @param remote_item [Hash] Remote database item
172
- # @param remote_time [Integer] Remote item timestamp
173
- # @param local_time [Integer] Local item timestamp
174
- # @return [Boolean] true if remote item should take precedence
175
150
  def remote_wins?(remote_item, remote_time, local_time)
176
- remote_time > local_time && (remote_item['deleted'].to_i == 1 || remote_time > local_time)
151
+ time_diff = remote_time > local_time
152
+ time_diff && (remote_item['deleted'].to_i == 1 || time_diff)
177
153
  end
178
154
 
179
- # Formats item status message
180
- #
181
- # @param id [Integer] Item ID
182
- # @param item [Hash] Database item
183
- # @param source [String] Source of the item ('Remote' or 'Local')
184
- # @return [String] Formatted status message
185
155
  def format_status_message(id, item, source)
186
156
  deleted = item['deleted'].to_i == 1 ? ' and deleted' : ''
187
157
  "#{source} item #{id} is newer#{deleted} - #{source == 'Remote' ? 'updating local' : 'will be uploaded'}"
188
158
  end
159
+ module_function :format_status_message
189
160
 
190
- # Updates an existing item in the database with values from a hash
191
- #
192
- # @param db [SQLite3::Database] The database connection
193
- # @param item [Hash] Hash containing item data
194
- # @return [void]
195
- def update_item_from_hash(db, item)
196
- fields = "#{ITEM_FIELDS.join(' = ?, ')} = ?"
197
- db.execute_sql(
198
- "UPDATE items SET #{fields} WHERE id = ?",
199
- get_item_values(item)
200
- )
161
+ def get_insert_values(item)
162
+ @database_fields ||= ITEM_FIELDS
163
+ values = @database_fields.map { |field| item[field] }
164
+ [item['id'], *values]
201
165
  end
202
166
 
203
- # Gets the values array for database operations
204
- #
205
- # @param item [Hash] Hash containing item data
206
- # @param include_id [Boolean] Whether to include ID at start (insert) or end (update)
207
- # @return [Array] Array of values for database operation
208
- def get_item_values(item, include_id_at_start: false)
167
+ def get_update_values(item)
209
168
  @database_fields ||= ITEM_FIELDS
210
- values = @database_fields.map { |field| item[field] }
211
- include_id_at_start ? [item['id'], *values] : values
169
+ @database_fields.map { |field| item[field] }
170
+ end
171
+
172
+ def get_item_values(item, include_id_at_start: false)
173
+ include_id_at_start ? get_insert_values(item) : get_update_values(item)
212
174
  end
213
175
  end
214
176
  end