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") %> – <%= @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 %> · <%= 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