From c794189b0127b72379af6c9769e498c3e2d1607f Mon Sep 17 00:00:00 2001
From: Teal Bauer <git@teal.is>
Date: Sun, 26 May 2024 14:01:40 +0200
Subject: [PATCH] tons of stuff

---
 Gemfile                                       |  2 +
 Gemfile.lock                                  | 37 ++++++++++
 app/assets/stylesheets/application.css        | 12 +---
 .../stylesheets/application.tailwind.css      | 24 ++++++-
 app/controllers/assignments_controller.rb     |  8 ++-
 app/controllers/conferences_controller.rb     |  5 ++
 app/jobs/notification_job.rb                  |  7 ++
 .../republica_2023_or_later/import_job.rb     |  7 +-
 .../telegram_group_chat_notification_job.rb   | 13 ++++
 app/models/notification.rb                    |  2 +
 app/models/notification_channel.rb            |  2 +
 app/models/revision.rb                        |  3 +
 app/models/revision_set.rb                    |  3 +
 app/models/session.rb                         | 19 ++++-
 app/models/speaker.rb                         | 12 ++++
 app/subscribers/notifications_subscriber.rb   | 24 +++++++
 app/subscribers/revision_subscriber.rb        | 24 +++++++
 app/subscribers/telegram_bot_subscriber.rb    |  2 +-
 app/views/application/_user_avatar.html.erb   |  5 +-
 app/views/assignments/by_user.html.erb        | 31 ++++++---
 app/views/assignments/index.html.erb          | 23 +++++--
 app/views/conferences/show.html.erb           | 69 ++++++++++++++++---
 app/views/sessions/_assignment_form.html.erb  | 18 ++---
 app/views/sessions/_session.html.erb          | 22 +++---
 config/routes.rb                              |  3 +-
 .../20240526061128_create_revision_sets.rb    |  9 +++
 db/migrate/20240526061318_create_revisions.rb | 14 ++++
 .../20240526061941_create_notifications.rb    | 14 ++++
 ...0526063139_create_notification_channels.rb | 10 +++
 db/schema.rb                                  | 41 ++++++++++-
 db/seeds.rb                                   | 11 ++-
 test/fixtures/notification_channels.yml       |  9 +++
 test/fixtures/notifications.yml               | 17 +++++
 test/fixtures/revision_sets.yml               |  7 ++
 test/fixtures/revisions.yml                   | 17 +++++
 test/jobs/notification_job_test.rb            |  7 ++
 ...legram_group_chat_notification_job_test.rb |  7 ++
 test/models/notification_channel_test.rb      |  7 ++
 test/models/notification_test.rb              |  7 ++
 test/models/revision_set_test.rb              |  7 ++
 test/models/revision_test.rb                  |  7 ++
 41 files changed, 501 insertions(+), 67 deletions(-)
 create mode 100644 app/jobs/notification_job.rb
 create mode 100644 app/jobs/telegram_group_chat_notification_job.rb
 create mode 100644 app/models/notification.rb
 create mode 100644 app/models/notification_channel.rb
 create mode 100644 app/models/revision.rb
 create mode 100644 app/models/revision_set.rb
 create mode 100644 app/subscribers/notifications_subscriber.rb
 create mode 100644 app/subscribers/revision_subscriber.rb
 create mode 100644 db/migrate/20240526061128_create_revision_sets.rb
 create mode 100644 db/migrate/20240526061318_create_revisions.rb
 create mode 100644 db/migrate/20240526061941_create_notifications.rb
 create mode 100644 db/migrate/20240526063139_create_notification_channels.rb
 create mode 100644 test/fixtures/notification_channels.yml
 create mode 100644 test/fixtures/notifications.yml
 create mode 100644 test/fixtures/revision_sets.yml
 create mode 100644 test/fixtures/revisions.yml
 create mode 100644 test/jobs/notification_job_test.rb
 create mode 100644 test/jobs/telegram_group_chat_notification_job_test.rb
 create mode 100644 test/models/notification_channel_test.rb
 create mode 100644 test/models/notification_test.rb
 create mode 100644 test/models/revision_set_test.rb
 create mode 100644 test/models/revision_test.rb

diff --git a/Gemfile b/Gemfile
index 1ea2342..fc463c2 100644
--- a/Gemfile
+++ b/Gemfile
@@ -67,3 +67,5 @@ gem "hotwire-rails", "~> 0.1.3"
 gem "importmap-rails", "~> 2.0"
 
 gem "icalendar", "~> 2.10"
+
+gem "telegram-bot-ruby", "~> 2.0"
diff --git a/Gemfile.lock b/Gemfile.lock
index a26ea99..8a92be5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -100,9 +100,35 @@ GEM
       irb (~> 1.10)
       reline (>= 0.3.8)
     drb (2.2.1)
+    dry-core (1.0.1)
+      concurrent-ruby (~> 1.0)
+      zeitwerk (~> 2.6)
+    dry-inflector (1.0.0)
+    dry-logic (1.5.0)
+      concurrent-ruby (~> 1.0)
+      dry-core (~> 1.0, < 2)
+      zeitwerk (~> 2.6)
+    dry-struct (1.6.0)
+      dry-core (~> 1.0, < 2)
+      dry-types (>= 1.7, < 2)
+      ice_nine (~> 0.11)
+      zeitwerk (~> 2.6)
+    dry-types (1.7.2)
+      bigdecimal (~> 3.0)
+      concurrent-ruby (~> 1.0)
+      dry-core (~> 1.0)
+      dry-inflector (~> 1.0)
+      dry-logic (~> 1.4)
+      zeitwerk (~> 2.6)
     erubi (1.12.0)
     et-orbi (1.2.11)
       tzinfo
+    faraday (2.9.0)
+      faraday-net_http (>= 2.0, < 3.2)
+    faraday-multipart (1.0.4)
+      multipart-post (~> 2)
+    faraday-net_http (3.1.0)
+      net-http
     fugit (1.9.0)
       et-orbi (~> 1, >= 1.2.7)
       raabro (~> 1.4)
@@ -120,6 +146,7 @@ GEM
     icalendar (2.10.1)
       ice_cube (~> 0.16)
     ice_cube (0.16.4)
+    ice_nine (0.11.2)
     importmap-rails (2.0.1)
       actionpack (>= 6.0.0)
       activesupport (>= 6.0.0)
@@ -145,7 +172,10 @@ GEM
     minitest (5.22.3)
     msgpack (1.7.2)
     multi_xml (0.6.0)
+    multipart-post (2.4.1)
     mutex_m (0.2.0)
+    net-http (0.4.1)
+      uri
     net-imap (0.4.10)
       date
       net-protocol
@@ -250,6 +280,11 @@ GEM
       railties (>= 7.0.0)
     tailwindcss-rails (2.6.0-x86_64-linux)
       railties (>= 7.0.0)
+    telegram-bot-ruby (2.0.0)
+      dry-struct (~> 1.6)
+      faraday (~> 2.0)
+      faraday-multipart (~> 1.0)
+      zeitwerk (~> 2.6)
     thor (1.3.1)
     timeout (0.4.1)
     turbo-rails (2.0.5)
@@ -258,6 +293,7 @@ GEM
       railties (>= 6.0.0)
     tzinfo (2.0.6)
       concurrent-ruby (~> 1.0)
+    uri (0.13.0)
     web-console (4.2.1)
       actionview (>= 6.0.0)
       activemodel (>= 6.0.0)
@@ -295,6 +331,7 @@ DEPENDENCIES
   sprockets-rails
   sqlite3 (~> 1.4)
   tailwindcss-rails (~> 2.6)
+  telegram-bot-ruby (~> 2.0)
   tzinfo-data
   web-console
 
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
index 525f70c..9ee413d 100644
--- a/app/assets/stylesheets/application.css
+++ b/app/assets/stylesheets/application.css
@@ -20,15 +20,6 @@
   margin: 0 auto;
 }
 
-.session {
-  padding: 8px;
-  background-color: #f9f9f9;
-  border: 1px solid #d4d4d4;
-  border-radius: 4px;
-  width: 100%;
-  display: flex;
-  flex-direction: column;
-}
 .session h4 {
   margin-top: 0;
   margin-bottom: 4px;
@@ -55,7 +46,8 @@
   position: absolute;
   width: 100%;
   z-index: 30;
-  border-top: 1px solid rgba(255, 0, 0, 0.4);
+  border-top: 1.5px solid rgba(255, 0, 0, 0.5);
+  pointer-events: none;
 }
 .stages {
   display: flex;
diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css
index 9c0e8aa..d388dd9 100644
--- a/app/assets/stylesheets/application.tailwind.css
+++ b/app/assets/stylesheets/application.tailwind.css
@@ -16,8 +16,28 @@ h1, h2, h3, h4, h5, h6 {
   @apply font-bold;
 }
 input[type=submit] {
-  @apply border rounded-md px-2 py-1;
+  @apply border bg-gray-400 rounded-md px-2 py-1;
   &.primary {
-    @apply bg-teal-800 text-teal-50;
+    @apply bg-teal-800 text-teal-50 border-teal-600;
+  }
+}
+select {
+  @apply pl-2 pr-6 py-1;
+}
+.session-holder { @apply w-full; }
+.session {
+  @apply border p-2 bg-slate-100 border-slate-300 rounded-md w-full flex flex-col;
+
+  &.translators-needed {
+    @apply bg-red-200 border-red-400;
+  }
+  &.backup-needed {
+    @apply bg-amber-100 border-amber-400;
+  }
+  &.no-assignees {
+    @apply bg-red-300 border-red-800;
+  }
+  &.no-backup-needed {
+    @apply bg-green-100 border-green-300;
   }
 }
diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb
index 78e5178..fd910c3 100644
--- a/app/controllers/assignments_controller.rb
+++ b/app/controllers/assignments_controller.rb
@@ -56,17 +56,19 @@ class AssignmentsController < ApplicationController
 
         @user.assignments.each do |assignment|
           session = assignment.session
+          other_users = session.assignments.map { |a| a.user.name }
+          other_users = other_users - @user
 
           event = Icalendar::Event.new
           event.dtstart = Icalendar::Values::DateTime.new(session.starts_at, tzid: session.starts_at.zone)
           event.dtend = Icalendar::Values::DateTime.new(session.ends_at, tzid: session.ends_at.zone)
-          event.summary = session.title
-          event.description = helpers.strip_tags(session.description)
+          event.summary = [session.title, session.stage.name].join(' @ ')
+          event.description = other_users.join(', ') + "\n\n" + helpers.strip_tags(session.description)
           event.location = [session.stage.name, session.conference.name].join(' @ ')
           event.created = Icalendar::Values::DateTime.new(session.created_at)
           event.last_modified = Icalendar::Values::DateTime.new(session.updated_at)
           event.uid = [session.conference.slug, session.ref_id].join('-')
-          event.append_custom_property("X-ALT-DESC;FMTTYPE=text/html", session.description) 
+          event.append_custom_property("X-ALT-DESC;FMTTYPE=text/html", other_users.join(', ') + "\n\n" + session.description)
           calendar.add_event(event)
         end
 
diff --git a/app/controllers/conferences_controller.rb b/app/controllers/conferences_controller.rb
index 4077705..2e27113 100644
--- a/app/controllers/conferences_controller.rb
+++ b/app/controllers/conferences_controller.rb
@@ -6,6 +6,11 @@ class ConferencesController < ApplicationController
   def show
     @conference = Conference.find_by(slug: params[:slug])
     @sessions = @conference.sessions.where.not(starts_at: nil).includes(:stage, :assignments).where(stage: { name: ["Stage 1", "Stage 2"] }).order(:starts_at)
+    if params[:date]
+      date = Time.parse(params[:date])
+      logger.debug(date)
+      @sessions = @sessions.where(starts_at: date.beginning_of_day..date.end_of_day)
+    end
     @users = User.all
   end
 end
diff --git a/app/jobs/notification_job.rb b/app/jobs/notification_job.rb
new file mode 100644
index 0000000..1ebb4fc
--- /dev/null
+++ b/app/jobs/notification_job.rb
@@ -0,0 +1,7 @@
+class NotificationJob < ApplicationJob
+  queue_as :notifications
+
+  def perform(*args)
+    # Do something later
+  end
+end
diff --git a/app/jobs/republica_2023_or_later/import_job.rb b/app/jobs/republica_2023_or_later/import_job.rb
index 4d20870..f0c40f0 100644
--- a/app/jobs/republica_2023_or_later/import_job.rb
+++ b/app/jobs/republica_2023_or_later/import_job.rb
@@ -8,7 +8,7 @@ module Republica2023OrLater
       response = HTTParty.get(url)
       if response.success?
         sessions = JSON.parse(response.body)
-        sessions.each do |session_data|
+        sessions.reject { |s| s['langcode'] == 'en' }.each do |session_data|
           stage = Stage.find_or_initialize_by(conference:, ref_id: session_data['room_nid']).tap do |stage_|
             stage_.name = session_data['room']
             stage_.save!
@@ -18,9 +18,9 @@ module Republica2023OrLater
             session.title = session_data['title']
             session.language =
               case session_data['language']
-              when 'German'
+              when 'German', 'Deutsch'
                 'de'
-              when 'English'
+              when 'English', 'Englisch'
                 'en'
               end
             session.status = session_data['status']
@@ -60,6 +60,7 @@ module Republica2023OrLater
       conference = Conference.find_by(slug: conference_slug)
       import_speakers(conference, conference.data['speakers_url'])
       import_sessions(conference, conference.data['sessions_url'])
+      revision_set = RevisionSet.create!(conference:)
     end
   end
 end
diff --git a/app/jobs/telegram_group_chat_notification_job.rb b/app/jobs/telegram_group_chat_notification_job.rb
new file mode 100644
index 0000000..ae907df
--- /dev/null
+++ b/app/jobs/telegram_group_chat_notification_job.rb
@@ -0,0 +1,13 @@
+require 'telegram/bot'
+
+class TelegramGroupChatNotificationJob < NotificationJob
+  queue_as :notifications
+
+  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)
+    end
+  end
+end
diff --git a/app/models/notification.rb b/app/models/notification.rb
new file mode 100644
index 0000000..0210dd8
--- /dev/null
+++ b/app/models/notification.rb
@@ -0,0 +1,2 @@
+class Notification < ApplicationRecord
+end
diff --git a/app/models/notification_channel.rb b/app/models/notification_channel.rb
new file mode 100644
index 0000000..fd234e2
--- /dev/null
+++ b/app/models/notification_channel.rb
@@ -0,0 +1,2 @@
+class NotificationChannel < ApplicationRecord
+end
diff --git a/app/models/revision.rb b/app/models/revision.rb
new file mode 100644
index 0000000..cf2eae5
--- /dev/null
+++ b/app/models/revision.rb
@@ -0,0 +1,3 @@
+class Revision < ApplicationRecord
+  belongs_to :conference
+end
diff --git a/app/models/revision_set.rb b/app/models/revision_set.rb
new file mode 100644
index 0000000..27fd66d
--- /dev/null
+++ b/app/models/revision_set.rb
@@ -0,0 +1,3 @@
+class RevisionSet < ApplicationRecord
+  belongs_to :conference
+end
diff --git a/app/models/session.rb b/app/models/session.rb
index ffe1ab5..91b67c8 100644
--- a/app/models/session.rb
+++ b/app/models/session.rb
@@ -4,10 +4,14 @@ class Session < ApplicationRecord
   has_many :assignments
   has_many :users, through: :assignments
 
+  scope :scheduled, -> { where(status: 'scheduled') }
+
   validates :ref_id, uniqueness: { scope: :conference_id }
 
   after_update :notify_if_changed
 
+  self.implicit_order_column = :starts_at
+
   def to_param
     ref_id
   end
@@ -20,11 +24,24 @@ class Session < ApplicationRecord
     super.in_time_zone(conference.time_zone)
   end
 
+  def translators_needed?
+    is_interpreted && assignments.length < 2
+  end
+
+  def backup_needed?
+    is_interpreted && assignments.length < 4
+  end
+
+  def has_assignees?
+    assignments.length > 0
+  end
+
   private
 
   def notify_if_changed
+    return if new_record?
     if saved_changes.present?
-      ActiveSupport::Notifications.instrument("republica.import.session.updated", record: self, changes: saved_changes)
+      ActiveSupport::Notifications.instrument("session.updated", record: self, changes: saved_changes)
     end
   end
 end
diff --git a/app/models/speaker.rb b/app/models/speaker.rb
index bfa5b79..5f40005 100644
--- a/app/models/speaker.rb
+++ b/app/models/speaker.rb
@@ -2,4 +2,16 @@ class Speaker < ApplicationRecord
   belongs_to :conference
 
   validates :ref_id, uniqueness: { scope: :conference_id }
+
+  after_update :notify_if_changed
+
+
+  private
+
+  def notify_if_changed
+    return if new_record?
+    if saved_changes.present?
+      ActiveSupport::Notifications.instrument("speaker.updated", record: self, changes: saved_changes)
+    end
+  end
 end
diff --git a/app/subscribers/notifications_subscriber.rb b/app/subscribers/notifications_subscriber.rb
new file mode 100644
index 0000000..c6aad96
--- /dev/null
+++ b/app/subscribers/notifications_subscriber.rb
@@ -0,0 +1,24 @@
+class NotificationsSubscriber
+  def self.subscribe
+    ActiveSupport::Notifications.subscribe('session.updated') do |*args|
+      event = ActiveSupport::Notifications::Event.new(*args)
+      new.handle_event(event)
+    end
+  end
+
+  def handle_event(event)
+    model_name = event.name.split(".").first
+    record = event.payload[:record]
+    changes = event.payload[:changes]
+
+    conference = record.conference
+
+    Revision.create!(
+      conference:,
+      target_type: model_name,
+      target_ref_id: record.ref_id,
+      new_data: record.to_json,
+      data_changes: changes.to_json
+    )
+  end
+end
diff --git a/app/subscribers/revision_subscriber.rb b/app/subscribers/revision_subscriber.rb
new file mode 100644
index 0000000..92bb52c
--- /dev/null
+++ b/app/subscribers/revision_subscriber.rb
@@ -0,0 +1,24 @@
+class RevisionSubscriber
+  def self.subscribe
+    ActiveSupport::Notifications.subscribe(/\A(?:session|speaker)\.updated/) do |*args|
+      event = ActiveSupport::Notifications::Event.new(*args)
+      new.handle_event(event)  # Call the instance method
+    end
+  end
+
+  def handle_event(event)
+    model_name = event.name.split(".").first
+    record = event.payload[:record]
+    changes = event.payload[:changes]
+
+    conference = record.conference
+
+    Revision.create!(
+      conference:,
+      target_type: model_name,
+      target_ref_id: record.ref_id,
+      new_data: record.to_json,
+      data_changes: changes.to_json
+    )
+  end
+end
diff --git a/app/subscribers/telegram_bot_subscriber.rb b/app/subscribers/telegram_bot_subscriber.rb
index bbbd606..15fefb0 100644
--- a/app/subscribers/telegram_bot_subscriber.rb
+++ b/app/subscribers/telegram_bot_subscriber.rb
@@ -1,6 +1,6 @@
 class TelegramBotSubscriber
   def self.subscribe
-    ActiveSupport::Notifications.subscribe(/republica\.import/) do |*args|
+    ActiveSupport::Notifications.subscribe(/\A(?:session|speaker)\.updated/) do |*args|
       event = ActiveSupport::Notifications::Event.new(*args)
       new.handle_event(event)  # Call the instance method
     end
diff --git a/app/views/application/_user_avatar.html.erb b/app/views/application/_user_avatar.html.erb
index 6e26c21..f8563fc 100644
--- a/app/views/application/_user_avatar.html.erb
+++ b/app/views/application/_user_avatar.html.erb
@@ -1,3 +1,6 @@
 <span class="inline-flex items-center gap-x-0.5 rounded-full bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10" style="background-color: <%= user.avatar_color %>" title="<%= user.name %>">
-  <span style="color: <%= user.text_color %>"><%= user.name.split(/\s+/).map(&:first).join('') %></span>
+  <span style="color: <%= user.text_color %>"><%= user.name %></span>
 </span>
+<%
+  # user.name.split(/\s+/).map(&:first).join('')
+%>
\ No newline at end of file
diff --git a/app/views/assignments/by_user.html.erb b/app/views/assignments/by_user.html.erb
index 9014b7f..1786c8e 100644
--- a/app/views/assignments/by_user.html.erb
+++ b/app/views/assignments/by_user.html.erb
@@ -1,10 +1,25 @@
-<ul>
+<div>
+  <table class="border *:border">
+    <thead>
+      <tr class="*:font-bold *:border">
+        <th>Conference</th>
+        <th>Date</th>
+        <th>Time</th>
+        <th>Stage</th>
+        <th>Session</th>
+        <th>Collaborators</th>
+      </tr>
+    </thead>
+    <tbody>
   <% @user.assignments.includes(:session, session: :conference).order('sessions.starts_at').each do |assignment| %>
-    <li>
-      <%= assignment.session.conference.name %>
-      <%= assignment.session.starts_at %>
-      <%= assignment.session.stage.name %>
-      <%= assignment.session.title %>
-    </li>
+    <tr class="*:border *:p-1">
+      <td><%= assignment.session.conference.name %></td>
+      <td><%= assignment.session.starts_at.strftime('%Y-%m-%d') %></td>
+      <td><%= assignment.session.starts_at.strftime('%H:%M') %></td>
+      <td><%= assignment.session.stage.name %></td>
+      <td><%= assignment.session.title %></td>
+      <td><% assignment.session.assignments.map(&:user).reject{ |u| u == @user }.each do |other_user| %><%= render partial: 'application/user_avatar', locals: { user: other_user } %><% end %></td>
+    </tr>
   <% end %>
-</ul>
\ No newline at end of file
+  </table>
+</div>
\ No newline at end of file
diff --git a/app/views/assignments/index.html.erb b/app/views/assignments/index.html.erb
index 139f949..9506530 100644
--- a/app/views/assignments/index.html.erb
+++ b/app/views/assignments/index.html.erb
@@ -1,10 +1,21 @@
 <div>
   <% @assignments.group_by(&:user).each do |user, assignments| %>
-  <h4><%= user.name %></h4>
-  <ul>
-  <% assignments.each do |assignment| %>
-    <li><%= link_to assignment.session.title, assignment.session %></li>
-  <% end %>
-  </ul>
+    <div class="my-8">
+      <h4 class="text-xl my-2"><%= link_to user.name, user_assignments_path(user) %> <span class="font-normal"><%= link_to user_assignments_path(user, format: 'ics') do %><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="ml-2 mb-1 size-4 inline-block stroke-slate-400 fill-slate-400"><path fill-rule="evenodd" d="M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.75z" clip-rule="evenodd"></path></svg><% end %></span></h4>
+        <% assignments.group_by { |a| a.session.starts_at.strftime('%Y-%m-%d') }.each do |date, assignments_on_date| %>
+          <h5><%= date %></h5>
+          <ol class="list-inside">
+            <% assignments_on_date.each do |assignment| %>
+              <li>
+                <%= assignment.session.starts_at.strftime('%H:%M') %> @ <%= assignment.session.stage.name %>:
+                <%= link_to assignment.session.title, assignment.session %>
+                <small><% assignment.session.assignments.map(&:user).reject{ |u| u == user }.each do |other_user| %>
+                  <%= render partial: 'application/user_avatar', locals: { user: other_user } %>
+                <% end %></small>
+              </li>
+            <% end %>
+          </ol>
+        <% end %>
+    </div>
   <% end %>
 </div>
\ No newline at end of file
diff --git a/app/views/conferences/show.html.erb b/app/views/conferences/show.html.erb
index 00b8cd4..bc490b8 100644
--- a/app/views/conferences/show.html.erb
+++ b/app/views/conferences/show.html.erb
@@ -1,23 +1,33 @@
 <%
 pixels_per_hour = 300.0
 timeline_granularity = 15
-# current_time = Time.zone.now
-current_time = Time.parse(@conference.days.first.strftime("%Y-%m-%d 10:37"))
+fudge = 24
+current_time = Time.zone.now.in_time_zone(@conference.time_zone).advance(days: 1, hours: 2)
+# current_time = Time.parse(@conference.days.first.strftime("%Y-%m-%d 11:00"))
 #current_time = @sessions_by_date[@conference.days.first].first.starts_at.advance(minutes: 5)
 %>
 <div>
   <h1 class="text-2xl font-bold my-6"><%= @conference.name %></h1>
+  <p><small><%= @conference.starts_at_in_local_time.strftime("%b %d, %Y") %> &ndash; <%= @conference.ends_at_in_local_time.strftime("%b %d, %Y") %></small></p>
 
-  <nav id="conference-days" class="list-disc list-inside">
+  <div class="text-sm font-medium text-center text-gray-500 border-b border-gray-200 dark:text-gray-400 dark:border-gray-700">
+    <ul class="flex flex-wrap -mb-px">
+      <li class="me-2">
+        <%= link_to "All days", conference_path(@conference), class: "inline-block p-4 border-b-2 rounded-t-lg" + (!params[:date] ? " active text-blue-600 border-blue-600 dark:text-blue-500 dark:border-blue-500" : " border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300") %>
+      </li>
     <% @conference.days.each do |date| %>
-      <li><%= link_to date.strftime("%Y-%m-%d"), "\##{date.strftime('day-%Y-%m-%d')}" %></li>
-    <% end %>
-  </nav>
+      <li class="me-2">
+        <%= link_to date.strftime("%Y-%m-%d"), date_conference_path(@conference, date.strftime('%Y-%m-%d')), class: "inline-block p-4 border-b-2 rounded-t-lg" + ((params[:date] && params[:date] == date.strftime("%Y-%m-%d")) ? " active text-blue-600 border-blue-600 dark:text-blue-500 dark:border-blue-500" : " border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300") %>
+      </li>
+  <% end %>
+    </ul>
+  </div>
 
   <%
   @conference.days.each do |date|
     @sessions_by_date = @sessions.group_by { |x| x.starts_at.to_date }
     @sessions_by_date_and_stage = @sessions_by_date.transform_values{ |sessions| sessions.group_by { |s| s.stage } }
+    next if @sessions_by_date[date].nil?
     day_starts_at = @sessions_by_date[date].first.starts_at
     day_ends_at = @sessions_by_date[date].last.ends_at
     # round to previous interval
@@ -27,15 +37,54 @@ current_time = Time.parse(@conference.days.first.strftime("%Y-%m-%d 10:37"))
   %>
     <div class="conference-day my-8" id="<%= date.strftime('day-%Y-%m-%d') %>">
       <h3 class="text-xl my-4 border-b"><%= date.strftime('%B %d, %Y') %></h3>
+      <%
+=begin%>
+      <ul class="list-disc">
+        <li>Current time: <%= current_time %></li>
+        <li>Timeline starts at <%= timeline_starts_at %></li>
+        <li>Diff s: <%= (current_time - timeline_starts_at) %></li>
+        <li>Diff px: <%= fudge + (current_time - timeline_starts_at) / 3600.0 * pixels_per_hour %></li>
+      </ul>
+      <%
+=end %>
       <div class="day-wrapper flex relative">
 
       <div class="times" style="">
         <h4>Time</h4>
-        <% if current_time.strftime('%Y%m%d') == date.strftime('%Y%m%d')
+        <% # if current_time.strftime('%Y%m%d') == date.strftime('%Y%m%d') && current_time >= timeline_starts_at && current_time <= timeline_ends_at
         %>
-        <div class="current-time" style="top: <%= (current_time - timeline_starts_at) / 3600.0 * pixels_per_hour %>px"></div>
+        <div class="current-time" style="top: <%= fudge + (current_time - timeline_starts_at) / 3600.0 * pixels_per_hour %>px"></div>
+        <script type="text/javascript">
+          function updateCurrentTime<%= date.strftime('day%Y%m%d') %>() {
+            const currentTime = new Date();
+            currentTime.setTime(currentTime.getTime() + 1 * 86400 * 1000);  /* XXX DEBUG */
+            const timelineStartsAt = new Date(<%= timeline_starts_at.to_json.html_safe %>);
+            const timelineEndsAt = new Date(<%= timeline_ends_at.to_json.html_safe %>);
+            const fudge = <%= fudge %>;
+            const pixelsPerHour = <%= pixels_per_hour %>;
+            const currentTimeDiv = document.querySelector('#<%= date.strftime('day-%Y-%m-%d') %> .current-time');
+
+            if (
+              currentTime.toDateString() === timelineStartsAt.toDateString() &&
+              currentTime >= timelineStartsAt &&
+              currentTime <= timelineEndsAt
+            ) {
+              const timeDiffInSeconds = (currentTime - timelineStartsAt) / 1000; // JavaScript works in milliseconds
+              const hoursPassed = timeDiffInSeconds / 3600;
+              const topPosition = fudge + hoursPassed * pixelsPerHour;
+
+              currentTimeDiv.style.top = `${topPosition}px`;
+              currentTimeDiv.style.display = 'block';
+            } else {
+              currentTimeDiv.style.display = 'none';
+            }
+          }
+
+          updateCurrentTime<%= date.strftime('day%Y%m%d') %>();
+          setInterval(updateCurrentTime<%= date.strftime('day%Y%m%d') %>, 60000);
+        </script>
         <%
-        end
+        #end
         %>
         <%
         time_slot = timeline_starts_at
@@ -55,7 +104,7 @@ current_time = Time.parse(@conference.days.first.strftime("%Y-%m-%d 10:37"))
             <h4><%= stage.name %></h4>
             <div class="stage-sessions">
               <% sessions.each do |session| %>
-                <div class="session text-sm" style="position: absolute; top: <%= (session.starts_at - timeline_starts_at) / 3600.0 * pixels_per_hour %>px; height: <%= (session.ends_at - session.starts_at) / 3600.0 * pixels_per_hour %>px; overflow: scroll;">
+                <div class="session-holder" style="position: absolute; top: <%= (session.starts_at - timeline_starts_at) / 3600.0 * pixels_per_hour %>px; height: <%= (session.ends_at - session.starts_at) / 3600.0 * pixels_per_hour %>px; overflow: scroll;">
                   <%= render partial: "sessions/session", locals: { session: session } %>
                 </div>
               <% end %>
diff --git a/app/views/sessions/_assignment_form.html.erb b/app/views/sessions/_assignment_form.html.erb
index 45ce934..686a20f 100644
--- a/app/views/sessions/_assignment_form.html.erb
+++ b/app/views/sessions/_assignment_form.html.erb
@@ -1,12 +1,14 @@
 <% unassigned_users = @users - session.assignments.collect(&:user) %>
 <% if unassigned_users.length > 0 %>
-  <%= form_with url: conference_session_assignments_path(session.conference, session), method: :post, data: { turbo_frame: dom_id(session) } do |f| %>
-    <%= f.select :user_id, options_from_collection_for_select(unassigned_users, :id, :name) %>
-    <%= f.submit "Assign", class: 'primary' %>
-    <% if @assignment&.errors&.any? %>
-      <div class="alert alert-danger">
-        <%= @assignment.errors.full_messages.join(", ") %>
-      </div>
+  <div class="text-sm">
+    <%= form_with url: conference_session_assignments_path(session.conference, session), method: :post, data: { turbo_frame: dom_id(session) } do |f| %>
+      <%= f.select :user_id, options_from_collection_for_select(unassigned_users, :id, :name), {}, { class: "text-sm" } %>
+      <%= f.submit "Assign", class: 'primary text-sm' %>
+      <% if @assignment&.errors&.any? %>
+        <div class="alert alert-danger">
+          <%= @assignment.errors.full_messages.join(", ") %>
+        </div>
+      <% end %>
     <% end %>
-  <% end %>
+  </div>
 <% end %>
diff --git a/app/views/sessions/_session.html.erb b/app/views/sessions/_session.html.erb
index 1f03f31..0534eb4 100644
--- a/app/views/sessions/_session.html.erb
+++ b/app/views/sessions/_session.html.erb
@@ -1,12 +1,14 @@
 <%= turbo_frame_tag dom_id(session) do %>
-  <h4><%= link_to session.title, [session.conference, session], target: "_top" %></h4>
-  <p class="session-time"><%= session.starts_at.strftime('%H:%M') %> - <%= session.ends_at.strftime('%H:%M') %></p>
-  <ul class="inline-flex flex-wrap gap-1 my-1">
-    <% session.assignments.each do |assignment| %>
-      <li>
-        <span class="assigned-user"><%= render partial: 'assignments/user_avatar', locals: { assignment: assignment } %></span>
-      </li>
-    <% end %>
-  </ul>
-  <%= render partial: "sessions/assignment_form", locals: { session: session } %>
+  <div class="session text-sm w-full h-full <%= session.translators_needed? ? "translators-needed" : "no-translators-needed" %> <%= session.backup_needed? ? "backup-needed" : "no-backup-needed" %> <%= session.has_assignees? ? "has-assignees" : "no-assignees" %>">
+    <h4><small class="text-2xs uppercase font-light bg-black/10 rounded-sm p-1 mr-1 lang-<%= session.language %>"><%= session.language %></small><%= link_to session.title, session.url, target: "_top" %></h4>
+    <p class="session-time"><%= session.starts_at.strftime('%H:%M') %> - <%= session.ends_at.strftime('%H:%M') %> @ <%= session.stage.name %> &middot; <%= session.language %><% if session.is_interpreted %> <strong>(int)</strong><% end %></p>
+    <ul class="inline-flex flex-wrap gap-1 my-1">
+      <% session.assignments.each do |assignment| %>
+        <li>
+          <span class="assigned-user"><%= render partial: 'assignments/user_avatar', locals: { assignment: assignment } %></span>
+        </li>
+      <% end %>
+    </ul>
+    <%= render partial: "sessions/assignment_form", locals: { session: session } %>
+  </div>
 <% end %>
diff --git a/config/routes.rb b/config/routes.rb
index ead8585..6f22f91 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -10,13 +10,14 @@ Rails.application.routes.draw do
   root "conferences#index"
 
   resources :conferences, param: :slug do
+    get ':date', action: :show, on: :member, as: :date
     resources :sessions, param: :ref_id do
       resources :assignments, only: [:create, :destroy]
     end
   end
 
   resources :assignments, only: [:index] do
-    get 'for/:user_id', action: 'by_user', on: :collection
+    get 'for/:user_id', action: 'by_user', on: :collection, as: :user
   end
   resources :sessions, param: :ref_id
 
diff --git a/db/migrate/20240526061128_create_revision_sets.rb b/db/migrate/20240526061128_create_revision_sets.rb
new file mode 100644
index 0000000..fc1ccfe
--- /dev/null
+++ b/db/migrate/20240526061128_create_revision_sets.rb
@@ -0,0 +1,9 @@
+class CreateRevisionSets < ActiveRecord::Migration[7.1]
+  def change
+    create_table :revision_sets do |t|
+      t.references :conference, null: false, foreign_key: true
+
+      t.timestamps
+    end
+  end
+end
diff --git a/db/migrate/20240526061318_create_revisions.rb b/db/migrate/20240526061318_create_revisions.rb
new file mode 100644
index 0000000..a0242da
--- /dev/null
+++ b/db/migrate/20240526061318_create_revisions.rb
@@ -0,0 +1,14 @@
+class CreateRevisions < ActiveRecord::Migration[7.1]
+  def change
+    create_table :revisions do |t|
+      t.references :conference, null: false, foreign_key: true
+      t.string :target_type
+      t.string :target_ref_id
+      t.text :previous_data
+      t.text :new_data
+      t.text :data_changes
+
+      t.timestamps
+    end
+  end
+end
diff --git a/db/migrate/20240526061941_create_notifications.rb b/db/migrate/20240526061941_create_notifications.rb
new file mode 100644
index 0000000..729e439
--- /dev/null
+++ b/db/migrate/20240526061941_create_notifications.rb
@@ -0,0 +1,14 @@
+class CreateNotifications < ActiveRecord::Migration[7.1]
+  def change
+    create_table :notifications do |t|
+      t.string :channel
+      t.string :target
+      t.text :content
+      t.text :content_formatted
+      t.string :status
+      t.text :data
+
+      t.timestamps
+    end
+  end
+end
diff --git a/db/migrate/20240526063139_create_notification_channels.rb b/db/migrate/20240526063139_create_notification_channels.rb
new file mode 100644
index 0000000..3f88f02
--- /dev/null
+++ b/db/migrate/20240526063139_create_notification_channels.rb
@@ -0,0 +1,10 @@
+class CreateNotificationChannels < ActiveRecord::Migration[7.1]
+  def change
+    create_table :notification_channels do |t|
+      t.string :name
+      t.text :data
+
+      t.timestamps
+    end
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b6e8e04..31489c2 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_25_190720) do
+ActiveRecord::Schema[7.1].define(version: 2024_05_26_063139) do
   create_table "assignments", force: :cascade do |t|
     t.integer "user_id", null: false
     t.integer "session_id", null: false
@@ -35,6 +35,43 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_25_190720) do
     t.string "time_zone"
   end
 
+  create_table "notification_channels", force: :cascade do |t|
+    t.string "name"
+    t.text "data"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+  end
+
+  create_table "notifications", force: :cascade do |t|
+    t.string "channel"
+    t.string "target"
+    t.text "content"
+    t.text "content_formatted"
+    t.string "status"
+    t.text "data"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+  end
+
+  create_table "revision_sets", force: :cascade do |t|
+    t.integer "conference_id", null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["conference_id"], name: "index_revision_sets_on_conference_id"
+  end
+
+  create_table "revisions", force: :cascade do |t|
+    t.integer "conference_id", null: false
+    t.string "target_type"
+    t.string "target_ref_id"
+    t.text "previous_data"
+    t.text "new_data"
+    t.text "data_changes"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["conference_id"], name: "index_revisions_on_conference_id"
+  end
+
   create_table "sessions", force: :cascade do |t|
     t.string "title"
     t.string "language"
@@ -193,6 +230,8 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_25_190720) do
 
   add_foreign_key "assignments", "sessions"
   add_foreign_key "assignments", "users"
+  add_foreign_key "revision_sets", "conferences"
+  add_foreign_key "revisions", "conferences"
   add_foreign_key "sessions", "conferences"
   add_foreign_key "sessions", "stages"
   add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
diff --git a/db/seeds.rb b/db/seeds.rb
index cf19d0f..de2afc9 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -10,6 +10,7 @@
 
 Conference.find_or_create_by!(slug: "rp2023") do |c|
   c.name = "re:publica 2023"
+  c.time_zone = "Berlin"
   c.starts_at = DateTime.parse("Mon, 05 Jun 2023 08:30:00.000000000 UTC +00:00")
   c.ends_at = DateTime.parse("Wed, 07 Jun 2023 20:50:00.000000000 UTC +00:00")
   c.data = {
@@ -17,11 +18,12 @@ Conference.find_or_create_by!(slug: "rp2023") do |c|
     "sessions_url" => "https://re-publica.com/sites/default/files/extappdata/2023/session.json"
   }
   c.import_job_class = "republica_2023_or_later"
-  c.time_zone = "Berlin"
+  c.location = "Arena Berlin"
 end
 
 Conference.find_or_create_by!(slug: "rp2024") do |c|
   c.name = "re:publica 2024"
+  c.time_zone = "Berlin"
   c.starts_at = DateTime.parse("27 May 2024 10:30 CEST")
   c.ends_at = DateTime.parse("29 May 2024 19:45 CEST")
   c.data = {
@@ -29,9 +31,14 @@ Conference.find_or_create_by!(slug: "rp2024") do |c|
     "sessions_url" => "https://re-publica.com/sites/default/files/extappdata/2024/session.json"
   }
   c.import_job_class = "republica_2023_or_later"
-  c.time_zone = "Berlin"
+  c.location = "STATION Berlin"
 end
 
 %w[Teal hdsjulian Sophie bergpiratin sblsg Max aerowaffle ningwie Senana ToniHDS].each do |username|
   User.find_or_create_by!(name: username, email: "c3lingo+#{username}@x.moeffju.net")
 end
+
+NotificationChannel.create!(
+  name: 'telegram_group_chat',
+  data: { token: '6001822848:AAGR0hPl3upppQAQy2VrJBHud466QVsBnyQ' }
+)
diff --git a/test/fixtures/notification_channels.yml b/test/fixtures/notification_channels.yml
new file mode 100644
index 0000000..8121056
--- /dev/null
+++ b/test/fixtures/notification_channels.yml
@@ -0,0 +1,9 @@
+# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+one:
+  name: MyString
+  data: MyText
+
+two:
+  name: MyString
+  data: MyText
diff --git a/test/fixtures/notifications.yml b/test/fixtures/notifications.yml
new file mode 100644
index 0000000..75fc0f9
--- /dev/null
+++ b/test/fixtures/notifications.yml
@@ -0,0 +1,17 @@
+# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+one:
+  channel: MyString
+  target: MyString
+  content: MyText
+  content_formatted: MyText
+  status: MyString
+  data: MyText
+
+two:
+  channel: MyString
+  target: MyString
+  content: MyText
+  content_formatted: MyText
+  status: MyString
+  data: MyText
diff --git a/test/fixtures/revision_sets.yml b/test/fixtures/revision_sets.yml
new file mode 100644
index 0000000..8bac6c8
--- /dev/null
+++ b/test/fixtures/revision_sets.yml
@@ -0,0 +1,7 @@
+# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+one:
+  conference: one
+
+two:
+  conference: two
diff --git a/test/fixtures/revisions.yml b/test/fixtures/revisions.yml
new file mode 100644
index 0000000..d0df92a
--- /dev/null
+++ b/test/fixtures/revisions.yml
@@ -0,0 +1,17 @@
+# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+one:
+  conference: one
+  target_type: MyString
+  target_ref_id: MyString
+  previous_data: MyText
+  new_data: MyText
+  data_changes: MyText
+
+two:
+  conference: two
+  target_type: MyString
+  target_ref_id: MyString
+  previous_data: MyText
+  new_data: MyText
+  data_changes: MyText
diff --git a/test/jobs/notification_job_test.rb b/test/jobs/notification_job_test.rb
new file mode 100644
index 0000000..26306bc
--- /dev/null
+++ b/test/jobs/notification_job_test.rb
@@ -0,0 +1,7 @@
+require "test_helper"
+
+class NotificationJobTest < ActiveJob::TestCase
+  # test "the truth" do
+  #   assert true
+  # end
+end
diff --git a/test/jobs/telegram_group_chat_notification_job_test.rb b/test/jobs/telegram_group_chat_notification_job_test.rb
new file mode 100644
index 0000000..bb41759
--- /dev/null
+++ b/test/jobs/telegram_group_chat_notification_job_test.rb
@@ -0,0 +1,7 @@
+require "test_helper"
+
+class TelegramGroupChatNotificationJobTest < ActiveJob::TestCase
+  # test "the truth" do
+  #   assert true
+  # end
+end
diff --git a/test/models/notification_channel_test.rb b/test/models/notification_channel_test.rb
new file mode 100644
index 0000000..d56092b
--- /dev/null
+++ b/test/models/notification_channel_test.rb
@@ -0,0 +1,7 @@
+require "test_helper"
+
+class NotificationChannelTest < ActiveSupport::TestCase
+  # test "the truth" do
+  #   assert true
+  # end
+end
diff --git a/test/models/notification_test.rb b/test/models/notification_test.rb
new file mode 100644
index 0000000..a76e08d
--- /dev/null
+++ b/test/models/notification_test.rb
@@ -0,0 +1,7 @@
+require "test_helper"
+
+class NotificationTest < ActiveSupport::TestCase
+  # test "the truth" do
+  #   assert true
+  # end
+end
diff --git a/test/models/revision_set_test.rb b/test/models/revision_set_test.rb
new file mode 100644
index 0000000..2b4c50a
--- /dev/null
+++ b/test/models/revision_set_test.rb
@@ -0,0 +1,7 @@
+require "test_helper"
+
+class RevisionSetTest < ActiveSupport::TestCase
+  # test "the truth" do
+  #   assert true
+  # end
+end
diff --git a/test/models/revision_test.rb b/test/models/revision_test.rb
new file mode 100644
index 0000000..a9ab620
--- /dev/null
+++ b/test/models/revision_test.rb
@@ -0,0 +1,7 @@
+require "test_helper"
+
+class RevisionTest < ActiveSupport::TestCase
+  # test "the truth" do
+  #   assert true
+  # end
+end
-- 
GitLab