From 7d04fddeb952d4480282929c5f080831a7069318 Mon Sep 17 00:00:00 2001
From: Teal Bauer <git@teal.is>
Date: Sun, 26 May 2024 17:15:58 +0200
Subject: [PATCH] add model versioning log and crono

---
 Gemfile                                       |  4 +++-
 Gemfile.lock                                  |  6 ++++++
 .../telegram_group_chat_notification_job.rb   |  4 ++--
 app/models/assignment.rb                      | 13 ++++++++++++
 app/models/model_version.rb                   |  2 ++
 app/models/notification_channel.rb            |  1 +
 app/models/user.rb                            |  8 +++++++
 .../assignment_audit_subscriber.rb            | 15 +++++++++++++
 app/subscribers/telegram_bot_subscriber.rb    | 14 ++++++-------
 config/cronotab.rb                            | 17 +++++++++++++++
 config/initializers/subscribers.rb            |  5 +++--
 config/routes.rb                              |  2 ++
 .../20240526145439_create_model_versions.rb   | 11 ++++++++++
 ...40526145815_add_action_to_model_version.rb |  5 +++++
 .../20240526151339_create_crono_jobs.rb       | 12 +++++++++++
 db/schema.rb                                  | 21 ++++++++++++++++++-
 test/fixtures/model_versions.yml              | 11 ++++++++++
 test/models/model_version_test.rb             |  7 +++++++
 18 files changed, 144 insertions(+), 14 deletions(-)
 create mode 100644 app/models/model_version.rb
 create mode 100644 app/subscribers/assignment_audit_subscriber.rb
 create mode 100644 config/cronotab.rb
 create mode 100644 db/migrate/20240526145439_create_model_versions.rb
 create mode 100644 db/migrate/20240526145815_add_action_to_model_version.rb
 create mode 100644 db/migrate/20240526151339_create_crono_jobs.rb
 create mode 100644 test/fixtures/model_versions.yml
 create mode 100644 test/models/model_version_test.rb

diff --git a/Gemfile b/Gemfile
index fc463c2..1df1a54 100644
--- a/Gemfile
+++ b/Gemfile
@@ -24,7 +24,7 @@ gem "redis", ">= 4.0.1"
 # gem "kredis"
 
 # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
-# gem "bcrypt", "~> 3.1.7"
+gem "bcrypt", "~> 3.1.7"
 
 # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
 gem "tzinfo-data", platforms: %i[ windows jruby ]
@@ -69,3 +69,5 @@ gem "importmap-rails", "~> 2.0"
 gem "icalendar", "~> 2.10"
 
 gem "telegram-bot-ruby", "~> 2.0"
+
+gem "crono", "~> 2.0"
diff --git a/Gemfile.lock b/Gemfile.lock
index 8a92be5..e9f8ce2 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -78,6 +78,7 @@ GEM
     addressable (2.8.6)
       public_suffix (>= 2.0.2, < 6.0)
     base64 (0.2.0)
+    bcrypt (3.1.20)
     bigdecimal (3.1.7)
     bindex (0.8.1)
     bootsnap (1.18.3)
@@ -95,6 +96,9 @@ GEM
     concurrent-ruby (1.2.3)
     connection_pool (2.4.1)
     crass (1.0.6)
+    crono (2.0.1)
+      rails (>= 5.2.8)
+      sprockets-rails
     date (3.3.4)
     debug (1.9.2)
       irb (~> 1.10)
@@ -315,8 +319,10 @@ PLATFORMS
   x86_64-linux
 
 DEPENDENCIES
+  bcrypt (~> 3.1.7)
   bootsnap
   capybara
+  crono (~> 2.0)
   debug
   hotwire-rails (~> 0.1.3)
   httparty
diff --git a/app/jobs/telegram_group_chat_notification_job.rb b/app/jobs/telegram_group_chat_notification_job.rb
index ae907df..ea10213 100644
--- a/app/jobs/telegram_group_chat_notification_job.rb
+++ b/app/jobs/telegram_group_chat_notification_job.rb
@@ -3,11 +3,11 @@ require 'telegram/bot'
 class TelegramGroupChatNotificationJob < NotificationJob
   queue_as :notifications
 
-  def perform(*args)
+  def perform(**args)
     channel = NotificationChannel.find_by(name: 'telegram_group_chat')
     token = channel.data['token']
     Telegram::Bot::Client.run(token) do |bot|
-      bot.api.send_message(chat_id: args[:target], text: content)
+      bot.api.send_message(chat_id: args[:target], text: args[:text])
     end
   end
 end
diff --git a/app/models/assignment.rb b/app/models/assignment.rb
index 3426f22..20d75f3 100644
--- a/app/models/assignment.rb
+++ b/app/models/assignment.rb
@@ -6,6 +6,9 @@ class Assignment < ApplicationRecord
 
   validate :no_overlapping_assignments
 
+  after_create_commit :notify_assignment_created
+  after_destroy_commit :notify_assignment_destroyed
+
   private
 
   def no_overlapping_assignments
@@ -25,4 +28,14 @@ class Assignment < ApplicationRecord
       errors.add(:base, "This assignment overlaps with another assignment for this user.")
     end
   end
+
+  private
+
+  def notify_assignment_created
+    ActiveSupport::Notifications.instrument("assignment.created", record: self)
+  end
+
+  def notify_assignment_destroyed
+    ActiveSupport::Notifications.instrument("assignment.destroyed", record: self)
+  end
 end
diff --git a/app/models/model_version.rb b/app/models/model_version.rb
new file mode 100644
index 0000000..10660b8
--- /dev/null
+++ b/app/models/model_version.rb
@@ -0,0 +1,2 @@
+class ModelVersion < ApplicationRecord
+end
diff --git a/app/models/notification_channel.rb b/app/models/notification_channel.rb
index fd234e2..074e0c6 100644
--- a/app/models/notification_channel.rb
+++ b/app/models/notification_channel.rb
@@ -1,2 +1,3 @@
 class NotificationChannel < ApplicationRecord
+  serialize :data, coder: JSON
 end
diff --git a/app/models/user.rb b/app/models/user.rb
index 8e3dc5a..f343dde 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,9 +1,17 @@
 class User < ApplicationRecord
   has_many :assignments
+
+  has_secure_password
+
+  validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
   validates :email, uniqueness: { case_sensitive: false, message: "already in use" }
 
   after_initialize :set_avatar_color
 
+  def has_password?
+    !password_digest.nil?
+  end
+
   def text_color
     r, g, b = avatar_color.delete_prefix('#').chars.each_slice(2).map { |hex| hex.join.to_i(16) }
 
diff --git a/app/subscribers/assignment_audit_subscriber.rb b/app/subscribers/assignment_audit_subscriber.rb
new file mode 100644
index 0000000..4bdb724
--- /dev/null
+++ b/app/subscribers/assignment_audit_subscriber.rb
@@ -0,0 +1,15 @@
+class AssignmentAuditSubscriber
+  def self.subscribe
+    ActiveSupport::Notifications.subscribe(/\Aassignment\..*/) do |*args|
+      event = ActiveSupport::Notifications::Event.new(*args)
+      new.handle_event(event)  # Call the instance method
+    end
+  end
+
+  def handle_event(event)
+    action = event.name.split(".").last
+    record = event.payload[:record]
+
+    ModelVersion.create!(model: 'assignment', action:, new_data: record.to_json)
+  end
+end
diff --git a/app/subscribers/telegram_bot_subscriber.rb b/app/subscribers/telegram_bot_subscriber.rb
index 15fefb0..27349ee 100644
--- a/app/subscribers/telegram_bot_subscriber.rb
+++ b/app/subscribers/telegram_bot_subscriber.rb
@@ -7,25 +7,23 @@ class TelegramBotSubscriber
   end
 
   def handle_event(event)
-    # 1. Extract Relevant Information
-    model_name = event.name.split(".").last  # Get "session" or "speaker"
+    model_name, action = event.name.split(".")
     record = event.payload[:record]
     changes = event.payload[:changes]
 
-    # 2. Format Message for Telegram
-    message = format_telegram_message(model_name, record, changes)
+    message = format_telegram_message(model_name, action, record, changes)
 
-    # 3. Send Message via Telegram Bot API
-    # (Replace with your actual Telegram bot API integration)
     # TelegramBotAPI.sendMessage(chat_id: YOUR_CHAT_ID, text: message)
     Rails.logger.info("event: #{message}")
+    #TelegramGroupChatNotificationJob.perform_later(target: "-316096320", text: message)
+    TelegramGroupChatNotificationJob.perform_later(target: "2192297", text: message)
   end
 
   private
 
-  def format_telegram_message(model_name, record, changes)
+  def format_telegram_message(model_name, action, record, changes)
     # Customize this to your desired message format
-    "**#{model_name.capitalize} Updated:**\n\n" +
+    "**#{model_name.capitalize} #{action.capitalize}:**\n\n" +
       changes.map { |attr, (from, to)| "- #{attr}: #{from} -> #{to}" }.join("\n")
   end
 end
diff --git a/config/cronotab.rb b/config/cronotab.rb
new file mode 100644
index 0000000..976cc97
--- /dev/null
+++ b/config/cronotab.rb
@@ -0,0 +1,17 @@
+# cronotab.rb — Crono configuration file
+#
+# Here you can specify periodic jobs and schedule.
+# You can use ActiveJob's jobs from `app/jobs/`
+# You can use any class. The only requirement is that
+# class should have a method `perform` without arguments.
+#
+# class TestJob
+#   def perform
+#     puts 'Test!'
+#   end
+# end
+#
+# Crono.perform(TestJob).every 2.days, at: '15:30'
+#
+
+Crono.perform(FetchConferenceDataJob, 'rp2024').every 5.minutes
diff --git a/config/initializers/subscribers.rb b/config/initializers/subscribers.rb
index cc511bf..ad9d4de 100644
--- a/config/initializers/subscribers.rb
+++ b/config/initializers/subscribers.rb
@@ -1,3 +1,4 @@
 Rails.application.config.to_prepare do
-  TelegramBotSubscriber.subscribe  # Subscribe to notifications
-end
\ No newline at end of file
+  TelegramBotSubscriber.subscribe
+  AssignmentAuditSubscriber.subscribe
+end
diff --git a/config/routes.rb b/config/routes.rb
index 80999f2..ebfe923 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,4 +1,6 @@
 Rails.application.routes.draw do
+  mount Crono::Engine, at: '/crono'
+
   get 'login', to: 'users#login', as: :login
   post 'login', to: 'users#login'
   post 'logout', to: 'users#logout', as: :logout
diff --git a/db/migrate/20240526145439_create_model_versions.rb b/db/migrate/20240526145439_create_model_versions.rb
new file mode 100644
index 0000000..8a55348
--- /dev/null
+++ b/db/migrate/20240526145439_create_model_versions.rb
@@ -0,0 +1,11 @@
+class CreateModelVersions < ActiveRecord::Migration[7.1]
+  def change
+    create_table :model_versions do |t|
+      t.string :model
+      t.text :changed_data
+      t.text :new_data
+
+      t.timestamps
+    end
+  end
+end
diff --git a/db/migrate/20240526145815_add_action_to_model_version.rb b/db/migrate/20240526145815_add_action_to_model_version.rb
new file mode 100644
index 0000000..f00bc31
--- /dev/null
+++ b/db/migrate/20240526145815_add_action_to_model_version.rb
@@ -0,0 +1,5 @@
+class AddActionToModelVersion < ActiveRecord::Migration[7.1]
+  def change
+    add_column :model_versions, :action, :string
+  end
+end
diff --git a/db/migrate/20240526151339_create_crono_jobs.rb b/db/migrate/20240526151339_create_crono_jobs.rb
new file mode 100644
index 0000000..e0ed09e
--- /dev/null
+++ b/db/migrate/20240526151339_create_crono_jobs.rb
@@ -0,0 +1,12 @@
+class CreateCronoJobs < ActiveRecord::Migration[6.1]
+  def change
+    create_table :crono_jobs do |t|
+      t.string    :job_id, null: false
+      t.text      :log, limit: 1073741823 # LONGTEXT for MySQL
+      t.datetime  :last_performed_at
+      t.boolean   :healthy
+      t.timestamps null: false
+    end
+    add_index :crono_jobs, [:job_id], unique: true
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c3f6ce6..a593153 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema[7.1].define(version: 2024_05_26_121600) do
+ActiveRecord::Schema[7.1].define(version: 2024_05_26_151339) do
   create_table "assignments", force: :cascade do |t|
     t.integer "user_id", null: false
     t.integer "session_id", null: false
@@ -35,6 +35,25 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_26_121600) do
     t.string "time_zone"
   end
 
+  create_table "crono_jobs", force: :cascade do |t|
+    t.string "job_id", null: false
+    t.text "log", limit: 1073741823
+    t.datetime "last_performed_at", precision: nil
+    t.boolean "healthy"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["job_id"], name: "index_crono_jobs_on_job_id", unique: true
+  end
+
+  create_table "model_versions", force: :cascade do |t|
+    t.string "model"
+    t.text "changed_data"
+    t.text "new_data"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.string "action"
+  end
+
   create_table "notification_channels", force: :cascade do |t|
     t.string "name"
     t.text "data"
diff --git a/test/fixtures/model_versions.yml b/test/fixtures/model_versions.yml
new file mode 100644
index 0000000..8cd61a5
--- /dev/null
+++ b/test/fixtures/model_versions.yml
@@ -0,0 +1,11 @@
+# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+one:
+  model: MyString
+  changed_data: MyText
+  new_data: MyText
+
+two:
+  model: MyString
+  changed_data: MyText
+  new_data: MyText
diff --git a/test/models/model_version_test.rb b/test/models/model_version_test.rb
new file mode 100644
index 0000000..6c23819
--- /dev/null
+++ b/test/models/model_version_test.rb
@@ -0,0 +1,7 @@
+require "test_helper"
+
+class ModelVersionTest < ActiveSupport::TestCase
+  # test "the truth" do
+  #   assert true
+  # end
+end
-- 
GitLab