Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • c3lingo/rescheduled
1 result
Show changes
Commits on Source (12)
Showing
with 705 additions and 188 deletions
inherit_gem: { rubocop-rails-omakase: rubocop.yml }
require:
- rubocop-capybara
AllCops:
NewCops: enable
ruby 3.3.6 ruby 3.4.1
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile # Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
# renovate: datasource=docker depName=registry.docker.com/library/ruby # renovate: datasource=docker depName=registry.docker.com/library/ruby
ARG RUBY_VERSION=3.3.6 ARG RUBY_VERSION=3.4.1
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base
# Rails app lives here # Rails app lives here
......
source "https://rubygems.org" source "https://rubygems.org"
ruby "3.3.6" ruby "3.4.1"
gem "rails", "~> 8.0.1" gem "rails", "~> 8.0.1"
...@@ -17,14 +17,18 @@ gem "puma", ">= 5.0" ...@@ -17,14 +17,18 @@ gem "puma", ">= 5.0"
gem "redis", ">= 4.0.1" gem "redis", ">= 4.0.1"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ] gem "tzinfo-data", platforms: %i[windows jruby]
# Reduces boot times through caching; required in config/boot.rb # Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false gem "bootsnap", require: false
group :development, :test do group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri windows ] gem "debug", platforms: %i[mri windows]
gem "rubocop-rails-omakase", "~> 1.1"
gem "rubocop-capybara", "~> 2.21"
end end
group :development do group :development do
...@@ -50,8 +54,8 @@ gem "httparty", "> 0" ...@@ -50,8 +54,8 @@ gem "httparty", "> 0"
gem "tailwindcss-rails", "~> 3.1" gem "tailwindcss-rails", "~> 3.1"
gem "turbo-rails", "~> 2.0"
gem "stimulus-rails", "~> 1.3" gem "stimulus-rails", "~> 1.3"
gem "turbo-rails", "~> 2.0"
gem "importmap-rails", "~> 2.0" gem "importmap-rails", "~> 2.0"
...@@ -59,6 +63,6 @@ gem "icalendar", "~> 2.10" ...@@ -59,6 +63,6 @@ gem "icalendar", "~> 2.10"
gem "telegram-bot-ruby", "~> 2.0" gem "telegram-bot-ruby", "~> 2.0"
gem 'devise', '~> 4.9' gem "devise", "~> 4.9"
gem "crono", "~> 2.1" gem "crono", "~> 2.1"
...@@ -74,6 +74,7 @@ GEM ...@@ -74,6 +74,7 @@ GEM
uri (>= 0.13.1) uri (>= 0.13.1)
addressable (2.8.7) addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0) public_suffix (>= 2.0.2, < 7.0)
ast (2.4.2)
base64 (0.2.0) base64 (0.2.0)
bcrypt (3.1.20) bcrypt (3.1.20)
benchmark (0.4.0) benchmark (0.4.0)
...@@ -91,8 +92,8 @@ GEM ...@@ -91,8 +92,8 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
concurrent-ruby (1.3.4) concurrent-ruby (1.3.5)
connection_pool (2.4.1) connection_pool (2.5.0)
crass (1.0.6) crass (1.0.6)
crono (2.1.0) crono (2.1.0)
rails (>= 5.2.8) rails (>= 5.2.8)
...@@ -108,21 +109,22 @@ GEM ...@@ -108,21 +109,22 @@ GEM
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
drb (2.2.1) drb (2.2.1)
dry-core (1.0.2) dry-core (1.1.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
logger logger
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
dry-inflector (1.1.0) dry-inflector (1.2.0)
dry-logic (1.5.0) dry-logic (1.6.0)
bigdecimal
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
dry-core (~> 1.0, < 2) dry-core (~> 1.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
dry-struct (1.6.0) dry-struct (1.7.1)
dry-core (~> 1.0, < 2) dry-core (~> 1.1)
dry-types (>= 1.7, < 2) dry-types (~> 1.8, >= 1.8.2)
ice_nine (~> 0.11) ice_nine (~> 0.11)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
dry-types (1.7.2) dry-types (1.8.2)
bigdecimal (~> 3.0) bigdecimal (~> 3.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
dry-core (~> 1.0) dry-core (~> 1.0)
...@@ -149,7 +151,7 @@ GEM ...@@ -149,7 +151,7 @@ GEM
csv csv
mini_mime (>= 1.0.0) mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
i18n (1.14.6) i18n (1.14.7)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
icalendar (2.10.3) icalendar (2.10.3)
ice_cube (~> 0.16) ice_cube (~> 0.16)
...@@ -161,11 +163,14 @@ GEM ...@@ -161,11 +163,14 @@ GEM
activesupport (>= 6.0.0) activesupport (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
io-console (0.8.0) io-console (0.8.0)
irb (1.14.3) irb (1.15.1)
pp (>= 0.6.0)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
json (2.9.1) json (2.10.1)
logger (1.6.5) language_server-protocol (3.17.0.4)
lint_roller (1.1.0)
logger (1.6.6)
loofah (2.24.0) loofah (2.24.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
...@@ -178,40 +183,48 @@ GEM ...@@ -178,40 +183,48 @@ GEM
matrix (0.4.2) matrix (0.4.2)
mini_mime (1.1.5) mini_mime (1.1.5)
minitest (5.25.4) minitest (5.25.4)
msgpack (1.7.5) msgpack (1.8.0)
multi_xml (0.7.1) multi_xml (0.7.1)
bigdecimal (~> 3.1) bigdecimal (~> 3.1)
multipart-post (2.4.1) multipart-post (2.4.1)
net-http (0.6.0) net-http (0.6.0)
uri uri
net-imap (0.5.4) net-imap (0.5.6)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
net-protocol net-protocol
net-protocol (0.2.2) net-protocol (0.2.2)
timeout timeout
net-smtp (0.5.0) net-smtp (0.5.1)
net-protocol net-protocol
nio4r (2.7.4) nio4r (2.7.4)
nokogiri (1.18.1-aarch64-linux-gnu) nokogiri (1.18.3-aarch64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.1-arm64-darwin) nokogiri (1.18.3-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.1-x86_64-linux-gnu) nokogiri (1.18.3-x86_64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
orm_adapter (0.5.0) orm_adapter (0.5.0)
ostruct (0.6.1) ostruct (0.6.1)
psych (5.2.2) parallel (1.26.3)
parser (3.3.7.1)
ast (~> 2.4.1)
racc
pp (0.6.2)
prettyprint
prettyprint (0.2.0)
psych (5.2.3)
date date
stringio stringio
public_suffix (6.0.1) public_suffix (6.0.1)
puma (6.5.0) puma (6.6.0)
nio4r (~> 2.0) nio4r (~> 2.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.8.1) racc (1.8.1)
rack (3.1.8) rack (3.1.11)
rack-session (2.0.0) rack-session (2.1.0)
base64 (>= 0.1.0)
rack (>= 3.0.0) rack (>= 3.0.0)
rack-test (2.2.0) rack-test (2.2.0)
rack (>= 1.3) rack (>= 1.3)
...@@ -246,12 +259,13 @@ GEM ...@@ -246,12 +259,13 @@ GEM
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0, >= 1.2.2) thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1) rake (13.2.1)
rdoc (6.10.0) rdoc (6.12.0)
psych (>= 4.0.0) psych (>= 4.0.0)
redis (5.3.0) redis (5.4.0)
redis-client (>= 0.22.0) redis-client (>= 0.22.0)
redis-client (0.23.0) redis-client (0.23.2)
connection_pool connection_pool
regexp_parser (2.10.0) regexp_parser (2.10.0)
reline (0.6.0) reline (0.6.0)
...@@ -259,16 +273,46 @@ GEM ...@@ -259,16 +273,46 @@ GEM
responders (3.1.1) responders (3.1.1)
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
rexml (3.4.0) rexml (3.4.1)
rubocop (1.73.2)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.38.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.38.1)
parser (>= 3.3.1.0)
rubocop-capybara (2.21.0)
rubocop (~> 1.41)
rubocop-performance (1.24.0)
lint_roller (~> 1.1)
rubocop (>= 1.72.1, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
rubocop-rails (2.30.3)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
rubocop (>= 1.72.1, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
rubocop-rails-omakase (1.1.0)
rubocop (>= 1.72)
rubocop-performance (>= 1.24)
rubocop-rails (>= 2.30)
ruby-progressbar (1.13.0)
rubyzip (2.4.1) rubyzip (2.4.1)
securerandom (0.4.1) securerandom (0.4.1)
selenium-webdriver (4.28.0) selenium-webdriver (4.29.1)
base64 (~> 0.2) base64 (~> 0.2)
logger (~> 1.4) logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0) rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0) websocket (~> 1.0)
solid_queue (1.1.2) solid_queue (1.1.3)
activejob (>= 7.1) activejob (>= 7.1)
activerecord (>= 7.1) activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1) concurrent-ruby (>= 1.3.1)
...@@ -282,31 +326,34 @@ GEM ...@@ -282,31 +326,34 @@ GEM
actionpack (>= 6.1) actionpack (>= 6.1)
activesupport (>= 6.1) activesupport (>= 6.1)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sqlite3 (2.5.0-aarch64-linux-gnu) sqlite3 (2.6.0-aarch64-linux-gnu)
sqlite3 (2.5.0-arm64-darwin) sqlite3 (2.6.0-arm64-darwin)
sqlite3 (2.5.0-x86_64-linux-gnu) sqlite3 (2.6.0-x86_64-linux-gnu)
stimulus-rails (1.3.4) stimulus-rails (1.3.4)
railties (>= 6.0.0) railties (>= 6.0.0)
stringio (3.1.2) stringio (3.1.5)
tailwindcss-rails (3.1.0) tailwindcss-rails (3.3.1)
railties (>= 7.0.0) railties (>= 7.0.0)
tailwindcss-ruby tailwindcss-ruby (~> 3.0)
tailwindcss-ruby (3.4.17-aarch64-linux) tailwindcss-ruby (3.4.17-aarch64-linux)
tailwindcss-ruby (3.4.17-arm64-darwin) tailwindcss-ruby (3.4.17-arm64-darwin)
tailwindcss-ruby (3.4.17-x86_64-linux) tailwindcss-ruby (3.4.17-x86_64-linux)
telegram-bot-ruby (2.2.0) telegram-bot-ruby (2.4.0)
dry-struct (~> 1.6) dry-struct (~> 1.6)
faraday (~> 2.0) faraday (~> 2.0)
faraday-multipart (~> 1.0) faraday-multipart (~> 1.0)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
thor (1.3.2) thor (1.3.2)
timeout (0.4.3) timeout (0.4.3)
turbo-rails (2.0.11) turbo-rails (2.0.13)
actionpack (>= 6.0.0) actionpack (>= 7.1.0)
railties (>= 6.0.0) railties (>= 7.1.0)
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
uri (1.0.2) unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.3)
useragent (0.16.11) useragent (0.16.11)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
...@@ -316,12 +363,13 @@ GEM ...@@ -316,12 +363,13 @@ GEM
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
websocket (1.2.11) websocket (1.2.11)
websocket-driver (0.7.6) websocket-driver (0.7.7)
base64
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.7.1) zeitwerk (2.7.2)
PLATFORMS PLATFORMS
aarch64-linux aarch64-linux
...@@ -341,6 +389,8 @@ DEPENDENCIES ...@@ -341,6 +389,8 @@ DEPENDENCIES
puma (>= 5.0) puma (>= 5.0)
rails (~> 8.0.1) rails (~> 8.0.1)
redis (>= 4.0.1) redis (>= 4.0.1)
rubocop-capybara (~> 2.21)
rubocop-rails-omakase (~> 1.1)
selenium-webdriver selenium-webdriver
solid_queue (~> 1.1) solid_queue (~> 1.1)
sprockets-rails (> 0) sprockets-rails (> 0)
...@@ -353,7 +403,7 @@ DEPENDENCIES ...@@ -353,7 +403,7 @@ DEPENDENCIES
web-console (> 0) web-console (> 0)
RUBY VERSION RUBY VERSION
ruby 3.3.6p108 ruby 3.4.1p0
BUNDLED WITH BUNDLED WITH
2.6.2 2.6.2
module Admin
class ConferencesController < ApplicationController
before_action :authenticate_user!
before_action :authorize_permission
before_action :set_conference, only: [ :edit, :update, :destroy ]
def index
@conferences = Conference.all
end
def new
@conference = Conference.new
end
def create
@conference = Conference.new(conference_params)
if @conference.save
redirect_to admin_conferences_path, notice: "Conference was successfully created."
else
render :new, status: :unprocessable_entity
end
end
def edit
end
def update
if @conference.update(conference_params)
redirect_to admin_conferences_path, notice: "Conference was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@conference.destroy
redirect_to admin_conferences_path, notice: "Conference was successfully deleted."
end
private
def authorize_permission
super("manage_conferences")
end
def set_conference
@conference = Conference.find_by(slug: params[:slug])
end
def conference_params
all_params = params.require(:conference).permit(:name, :slug, :starts_at, :ends_at, :url, :time_zone, :import_job_class).to_h
data_hash = @conference&.data&.dup || {}
if params[:data].present?
params[:data].each do |key, value|
data_hash[key] = value.presence
end
end
if params[:custom_field_keys].present? && params[:custom_field_values].present?
keys = params[:custom_field_keys]
values = params[:custom_field_values]
keys.each_with_index do |key, index|
next if key.blank?
data_hash[key] = values[index].presence
end
end
all_params[:data] = data_hash
all_params
end
end
end
module Admin
class RolesController < ApplicationController
before_action :authenticate_user!
before_action :authorize_role
before_action :set_role, only: [ :edit, :update ]
def index
@roles = Role.all.includes(:permissions)
end
def edit
@permissions = Permission.all
end
def update
if @role.update(role_params)
redirect_to admin_roles_path, notice: "Role was successfully updated."
else
@permissions = Permission.all
render :edit, status: :unprocessable_entity
end
end
private
def authorize_role
super("events_admin")
end
def set_role
@role = Role.find(params[:id])
end
def role_params
params.require(:role).permit(:name, :description, permission_ids: [])
end
end
end
...@@ -4,15 +4,30 @@ class ApplicationController < ActionController::Base ...@@ -4,15 +4,30 @@ class ApplicationController < ActionController::Base
protected protected
def configure_permitted_parameters def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:invitation_token]) devise_parameter_sanitizer.permit(:sign_up, keys: [ :invitation_token ])
devise_parameter_sanitizer.permit(:account_update) do |u| devise_parameter_sanitizer.permit(:account_update) do |u|
u.permit(:name, :email, :password, :password_confirmation, :avatar_color, :darkmode, :languages_from, :languages_to, :telegram_username, :current_password) u.permit(:name, :email, :password, :password_confirmation, :avatar_color, :darkmode, :languages_from,
:languages_to, :telegram_username, :current_password)
end end
end end
def authorize_shiftcoordinator def authorize_shiftcoordinator
unless current_user.shiftcoordinator? authorize_role("shift_coordinator")
render plain: 'Forbidden', status: :forbidden end
end
def redirect_back_with_error(message)
redirect_back(fallback_location: root_path, alert: message)
end
def authorize_role(role_name)
return if current_user&.has_role?(role_name)
render plain: "Forbidden", status: :forbidden
end
def authorize_permission(permission_name)
return if current_user&.has_permission?(permission_name)
render plain: "Forbidden", status: :forbidden
end end
end end
require 'icalendar/tzinfo' require "icalendar/tzinfo"
class AssignmentsController < ApplicationController class AssignmentsController < ApplicationController
before_action :authorize_shiftcoordinator, except: [:index, :by_user] before_action :authorize_permission, except: %i[index by_user]
before_action :set_session, :set_users before_action :set_session, :set_users
def index def index
@assignments = Assignment.all.joins(:session, :user).order('sessions.starts_at') @assignments = Assignment.all.joins(:session, :user).order("sessions.starts_at")
if params[:user_id] return unless params[:user_id]
@assignments = @assignments.where(user_id: params[:user_id])
end @assignments = @assignments.where(user_id: params[:user_id])
end end
def create def create
params[:user_id] ||= params[:assignment][:user_id] params[:user_id] ||= params[:assignment][:user_id]
if params[:user_id].nil? or params[:user_id].empty? if params[:user_id].nil? or params[:user_id].empty?
flash.now[:alert] = 'Please select a user to assign.' flash.now[:alert] = "Please select a user to assign."
respond_to do |format| respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }), status: :unprocessable_entity } format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }), status: :unprocessable_entity }
format.html { redirect_to conference_session_path(@session.conference, @session), alert: 'Please select a user to assign.' } format.html { redirect_to conference_session_path(@session.conference, @session), alert: "Please select a user to assign." }
end end
return return
end end
...@@ -35,13 +35,13 @@ class AssignmentsController < ApplicationController ...@@ -35,13 +35,13 @@ class AssignmentsController < ApplicationController
partial: "sessions/session", partial: "sessions/session",
locals: { session: @session } locals: { session: @session }
) )
flash.now[:success] = 'User assigned successfully.' flash.now[:success] = "User assigned successfully."
respond_to do |format| respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }) } format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }) }
format.html { redirect_to conference_session_path(@session.conference, @session), success: 'User assigned successfully.' } format.html { redirect_to conference_session_path(@session.conference, @session), success: "User assigned successfully." }
end end
else else
flash.now[:alert] = 'Failed to assign user.' flash.now[:alert] = "Failed to assign user."
respond_to do |format| respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }), status: :unprocessable_entity } format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }), status: :unprocessable_entity }
format.html { render :show, status: :unprocessable_entity } format.html { render :show, status: :unprocessable_entity }
...@@ -63,12 +63,12 @@ class AssignmentsController < ApplicationController ...@@ -63,12 +63,12 @@ class AssignmentsController < ApplicationController
) )
respond_to do |format| respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }) } format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }) }
format.html { redirect_to conference_session_path(@session.conference, @session), notice: 'User removed successfully.' } format.html { redirect_to conference_session_path(@session.conference, @session), notice: "User removed successfully." }
end end
else else
respond_to do |format| respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }), status: :unprocessable_entity } format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }), status: :unprocessable_entity }
format.html { redirect_to conference_session_path(@session.conference, @session), alert: 'Failed to remove user.' } format.html { redirect_to conference_session_path(@session.conference, @session), alert: "Failed to remove user." }
end end
end end
end end
...@@ -80,7 +80,7 @@ class AssignmentsController < ApplicationController ...@@ -80,7 +80,7 @@ class AssignmentsController < ApplicationController
format.ics do format.ics do
calendar = Icalendar::Calendar.new calendar = Icalendar::Calendar.new
tz = TZInfo::Timezone.get('UTC') tz = TZInfo::Timezone.get("UTC")
calendar.add_timezone tz.ical_timezone Time.now calendar.add_timezone tz.ical_timezone Time.now
@user.assignments.each do |assignment| @user.assignments.each do |assignment|
...@@ -96,18 +96,18 @@ class AssignmentsController < ApplicationController ...@@ -96,18 +96,18 @@ class AssignmentsController < ApplicationController
event = Icalendar::Event.new event = Icalendar::Event.new
event.dtstart = Icalendar::Values::DateTime.new(session.starts_at, tzid: session.starts_at.time_zone.tzinfo.name) event.dtstart = Icalendar::Values::DateTime.new(session.starts_at, tzid: session.starts_at.time_zone.tzinfo.name)
event.dtend = Icalendar::Values::DateTime.new(session.ends_at, tzid: session.ends_at.time_zone.tzinfo.name) event.dtend = Icalendar::Values::DateTime.new(session.ends_at, tzid: session.ends_at.time_zone.tzinfo.name)
event.summary = [session.title, session.stage.name].join(' @ ') event.summary = [ session.title, session.stage.name ].join(" @ ")
event.description = desc.map { |l| helpers.strip_tags(l) }.join("\n\n") event.description = desc.map { |l| helpers.strip_tags(l) }.join("\n\n")
event.location = [session.stage.name, session.conference.name].join(' @ ') event.location = [ session.stage.name, session.conference.name ].join(" @ ")
event.created = Icalendar::Values::DateTime.new(session.created_at) event.created = Icalendar::Values::DateTime.new(session.created_at)
event.last_modified = Icalendar::Values::DateTime.new(session.updated_at) event.last_modified = Icalendar::Values::DateTime.new(session.updated_at)
event.uid = [session.conference.slug, session.ref_id].join('-') event.uid = [ session.conference.slug, session.ref_id ].join("-")
event.append_custom_property("X-ALT-DESC;FMTTYPE=text/html", desc.join("<hr>")) event.append_custom_property("X-ALT-DESC;FMTTYPE=text/html", desc.join("<hr>"))
calendar.add_event(event) calendar.add_event(event)
end end
calendar.publish calendar.publish
headers['Content-Type'] = 'text/calendar; charset=UTF-8' headers["Content-Type"] = "text/calendar; charset=UTF-8"
render plain: calendar.to_ical render plain: calendar.to_ical
end end
end end
...@@ -115,6 +115,10 @@ class AssignmentsController < ApplicationController ...@@ -115,6 +115,10 @@ class AssignmentsController < ApplicationController
private private
def authorize_permission
super("manage_assignments")
end
def set_session def set_session
conference = Conference.find_by(slug: params[:conference_slug]) conference = Conference.find_by(slug: params[:conference_slug])
@session = Session.find_by(conference:, ref_id: params[:session_ref_id]) @session = Session.find_by(conference:, ref_id: params[:session_ref_id])
......
require 'icalendar/tzinfo' require "icalendar/tzinfo"
class CandidatesController < ApplicationController class CandidatesController < ApplicationController
before_action :authorize_shiftcoordinator, except: [:create, :destroy_self] before_action :authorize_permission, except: %i[create destroy_self]
def create def create
@conference = Conference.find_by(slug: params[:conference_slug]) @conference = Conference.find_by(slug: params[:conference_slug])
...@@ -13,20 +13,20 @@ class CandidatesController < ApplicationController ...@@ -13,20 +13,20 @@ class CandidatesController < ApplicationController
if @candidate if @candidate
Rails.logger.debug("Saved candidate #{@candidate.inspect}") Rails.logger.debug("Saved candidate #{@candidate.inspect}")
#@session = Session.find_by(ref_id: params[:session_ref_id]) # @session = Session.find_by(ref_id: params[:session_ref_id])
Turbo::StreamsChannel.broadcast_replace_to( Turbo::StreamsChannel.broadcast_replace_to(
@session.conference, @session.conference,
target: helpers.dom_id(@session), target: helpers.dom_id(@session),
partial: "sessions/session", partial: "sessions/session",
locals: { session: @session } locals: { session: @session }
) )
flash.now[:success] = 'User assigned successfully.' flash.now[:success] = "User assigned successfully."
respond_to do |format| respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }) } format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }) }
format.html { redirect_to conference_session_path(@session.conference, @session), success: 'User assigned successfully.' } format.html { redirect_to conference_session_path(@session.conference, @session), success: "User assigned successfully." }
end end
else else
flash.now[:alert] = 'Failed to record candidate.' flash.now[:alert] = "Failed to record candidate."
respond_to do |format| respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }), status: :unprocessable_entity } format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }), status: :unprocessable_entity }
format.html { render :show, status: :unprocessable_entity } format.html { render :show, status: :unprocessable_entity }
...@@ -50,6 +50,10 @@ class CandidatesController < ApplicationController ...@@ -50,6 +50,10 @@ class CandidatesController < ApplicationController
private private
def authorize_permission
super("manage_assignments")
end
def destroy_candidate(session, candidate) def destroy_candidate(session, candidate)
if candidate&.destroy if candidate&.destroy
Rails.logger.debug("destroyed candidate entry") Rails.logger.debug("destroyed candidate entry")
...@@ -61,12 +65,12 @@ class CandidatesController < ApplicationController ...@@ -61,12 +65,12 @@ class CandidatesController < ApplicationController
) )
respond_to do |format| respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(session), partial: "sessions/session", locals: { session: session }) } format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(session), partial: "sessions/session", locals: { session: session }) }
format.html { redirect_to conference_session_path(session.conference, session), notice: 'Candidate removed successfully.' } format.html { redirect_to conference_session_path(session.conference, session), notice: "Candidate removed successfully." }
end end
else else
respond_to do |format| respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(session), partial: "sessions/session", locals: { session: session }), status: :unprocessable_entity } format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(session), partial: "sessions/session", locals: { session: session }), status: :unprocessable_entity }
format.html { redirect_to conference_session_path(session.conference, session), alert: 'Failed to remove candidate.' } format.html { redirect_to conference_session_path(session.conference, session), alert: "Failed to remove candidate." }
end end
end end
end end
......
class ConferencesController < ApplicationController class ConferencesController < ApplicationController
before_action :authenticate_user!, except: [ :index, :show, :stats ]
before_action :authorize_permission, only: [ :new, :create, :edit, :update, :destroy ]
before_action :set_conference, only: [ :show, :edit, :update, :destroy, :stats ]
private
def authorize_permission
super("manage_conferences")
end
def set_conference
@conference = Conference.find_by(slug: params[:slug])
end
def conference_params
all_params = params.require(:conference).permit(:name, :slug, :starts_at, :ends_at, :url, :time_zone, :import_job_class, data: {}).to_h
data_hash = @conference&.data&.dup || {}
if params[:data].present?
params[:data].each do |key, value|
data_hash[key] = value.presence
end
end
if params[:custom_field_keys].present? && params[:custom_field_values].present?
keys = params[:custom_field_keys]
values = params[:custom_field_values]
keys.each_with_index do |key, index|
next if key.blank?
data_hash[key] = values[index].presence
end
end
all_params[:data] = data_hash
all_params
end
public
def self.available_import_job_classes
job_classes = {}
Rails.application.eager_load! if Rails.env.development?
ApplicationJob.descendants.each do |job_class|
if job_class.name.end_with?("ImportJob")
display_name = job_class.name.demodulize.gsub("ImportJob", "")
# If the class is in a module, add the module name
if job_class.name.include?("::")
module_name = job_class.name.deconstantize
display_name = "#{module_name} #{display_name}"
end
display_name = display_name.gsub("::", " ").gsub(/([A-Z])/, ' \1').strip
job_classes[job_class.name] = display_name
end
end
job_classes
end
def required_fields
import_job_class = params[:import_job_class]
result = {
required_fields: [],
metadata: {}
}
if import_job_class.present?
begin
klass = import_job_class.constantize
result[:required_fields] = klass.respond_to?(:required_data_fields) ? klass.required_data_fields : []
result[:metadata] = klass.respond_to?(:field_metadata) ? klass.field_metadata : {}
rescue NameError => e
Rails.logger.error("Could not find import job class #{import_job_class}: #{e}")
end
end
render json: result
end
def index def index
@conferences = Conference.all @conferences = Conference.all
end end
def show def show
@conference = Conference.find_by(slug: params[:slug]) @conference = Conference.find_by(slug: params[:slug])
@sessions = @conference.sessions.where.not(starts_at: nil).includes(:stage, :assignments).where(stage: @conference.relevant_stages).order(:starts_at) @sessions = @conference.sessions.where.not(starts_at: nil).includes(:stage,
:assignments).where(stage: @conference.relevant_stages).order(:starts_at)
if params[:date] if params[:date]
date = Time.parse(params[:date]) date = Time.parse(params[:date])
logger.debug(date) logger.debug(date)
...@@ -20,16 +107,42 @@ class ConferencesController < ApplicationController ...@@ -20,16 +107,42 @@ class ConferencesController < ApplicationController
@users = User.all @users = User.all
end end
def new
@conference = Conference.new
end
def create
@conference = Conference.new(conference_params)
if @conference.save
redirect_to conference_path(slug: @conference.slug), notice: "Conference was successfully created."
else
render :new, status: :unprocessable_entity
end
end
def edit def edit
# @conference is set by the before_action
end end
def update def update
if @conference.update(conference_params)
redirect_to conference_path(slug: @conference.slug), notice: "Conference was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@conference.destroy
redirect_to conferences_path, notice: "Conference was successfully deleted."
end end
def stats def stats
@conference = Conference.find_by(slug: params[:slug]) @conference = Conference.find_by(slug: params[:slug])
@relevant_stages = @conference.relevant_stages @relevant_stages = @conference.relevant_stages
@relevant_sessions = @conference.sessions.includes(:stage, :assignments, :speakers, assignments: :user).where(stage: @conference.relevant_stages).order(:starts_at) @relevant_sessions = @conference.sessions.includes(:stage, :assignments, :speakers,
assignments: :user).where(stage: @conference.relevant_stages).order(:starts_at)
@assignees = @relevant_sessions.map(&:assignments).flatten.map(&:user).uniq @assignees = @relevant_sessions.map(&:assignments).flatten.map(&:user).uniq
@language_stats = @relevant_sessions.group_by { |s| s.language }.transform_values(&:count) @language_stats = @relevant_sessions.group_by { |s| s.language }.transform_values(&:count)
@speakers = @relevant_sessions.map(&:speakers) @speakers = @relevant_sessions.map(&:speakers)
...@@ -44,6 +157,10 @@ class ConferencesController < ApplicationController ...@@ -44,6 +157,10 @@ class ConferencesController < ApplicationController
scheduled_time_min: se.map { |x| (x.ends_at - x.starts_at) / 60.0 }.sum scheduled_time_min: se.map { |x| (x.ends_at - x.starts_at) / 60.0 }.sum
} }
end end
@total_stats = @day_stats.values.inject { |m, x| m.merge(x) { |k, o, n| o + n } }.slice(:sessions_count, :wall_clock_time_min, :scheduled_time_min) @total_stats = @day_stats.values.inject do |m, x|
m.merge(x) do |_k, o, n|
o + n
end
end.slice(:sessions_count, :wall_clock_time_min, :scheduled_time_min)
end end
end end
class FiledropFilesController < ApplicationController class FiledropFilesController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_filedrop_file, only: [:download] before_action :set_filedrop_file, only: [ :download ]
def download def download
# Define the file path within the storage directory # Define the file path within the storage directory
...@@ -8,9 +8,9 @@ class FiledropFilesController < ApplicationController ...@@ -8,9 +8,9 @@ class FiledropFilesController < ApplicationController
# Send the file to the user # Send the file to the user
if File.exist?(file_path) if File.exist?(file_path)
send_file file_path, filename: @filedrop_file.name, disposition: 'attachment' send_file file_path, filename: @filedrop_file.name, disposition: "attachment"
else else
render plain: 'File not found', status: :not_found render plain: "File not found", status: :not_found
end end
end end
......
class SessionsController < ApplicationController class SessionsController < ApplicationController
before_action :authenticate_user!, except: [:index] before_action :authenticate_user!, except: [ :index ]
before_action :authorize_shiftcoordinator, except: [:index, :show] before_action :authorize_permission, except: %i[index show]
def index def index
@conference = Conference.find_by(slug: params[:conference_slug]) @conference = Conference.find_by(slug: params[:conference_slug])
...@@ -13,10 +13,8 @@ class SessionsController < ApplicationController ...@@ -13,10 +13,8 @@ class SessionsController < ApplicationController
end end
# Filter by stage name if provided # Filter by stage name if provided
if params[:stage].present? return unless params[:stage].present?
@sessions = @sessions.joins(:stage).where(stages: { name: params[:stage] }) @sessions = @sessions.joins(:stage).where(stages: { name: params[:stage] })
end
# Further filtering options can be added here # Further filtering options can be added here
end end
...@@ -39,13 +37,13 @@ class SessionsController < ApplicationController ...@@ -39,13 +37,13 @@ class SessionsController < ApplicationController
partial: "sessions/session", partial: "sessions/session",
locals: { session: } locals: { session: }
) )
flash.now[:success] = 'Notes saved successfully.' flash.now[:success] = "Notes saved successfully."
respond_to do |format| respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(session), partial: "sessions/session", locals: { session: }) } format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(session), partial: "sessions/session", locals: { session: }) }
format.html { redirect_to conference_session_path(session.conference, session), success: 'Notes saved successfully.' } format.html { redirect_to conference_session_path(session.conference, session), success: "Notes saved successfully." }
end end
else else
flash.now[:alert] = 'Failed to save notes.' flash.now[:alert] = "Failed to save notes."
respond_to do |format| respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(session), partial: "sessions/session", locals: { session: }), status: :unprocessable_entity } format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(session), partial: "sessions/session", locals: { session: }), status: :unprocessable_entity }
format.html { render :show, status: :unprocessable_entity } format.html { render :show, status: :unprocessable_entity }
...@@ -55,6 +53,10 @@ class SessionsController < ApplicationController ...@@ -55,6 +53,10 @@ class SessionsController < ApplicationController
private private
def authorize_permission
super("manage_assignments")
end
def notes_params def notes_params
params.require(:session).permit(:notes) params.require(:session).permit(:notes)
end end
......
module Admin::RolesHelper
end
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["requiredFields", "customFields", "importJobClass", "customTemplate"]
connect() {
// Initialize with at least one custom field if none exist
if (this.hasCustomFieldsTarget && this.customFieldsTarget.querySelectorAll('.custom-field-row').length === 0) {
this.addCustomField()
}
// Load required fields based on the initial selection
if (this.hasImportJobClassTarget && this.importJobClassTarget.value) {
this.importJobClassChanged()
}
}
importJobClassChanged() {
if (!this.hasImportJobClassTarget || !this.hasRequiredFieldsTarget) return
const selectedClass = this.importJobClassTarget.value
if (!selectedClass) {
this.requiredFieldsTarget.innerHTML = '<p class="text-sm text-gray-500 dark:text-gray-400">Select an import job class to see required fields</p>'
return
}
// Store the current data values
const currentData = this.getCurrentDataValues()
// Fetch the required fields for the selected import job class
fetch(`/conferences/required_fields?import_job_class=${encodeURIComponent(selectedClass)}`)
.then(response => response.json())
.then(data => {
// Clear the required fields container
this.requiredFieldsTarget.innerHTML = ''
if (data.required_fields.length === 0) {
this.requiredFieldsTarget.innerHTML = '<p class="text-sm text-gray-500 dark:text-gray-400">No required fields for this import job class</p>'
return
}
// Add the required fields
data.required_fields.forEach(field => {
const metadata = data.metadata[field] || {}
const fieldValue = currentData[field] || ''
const fieldHtml = this.generateRequiredFieldHtml(
field,
metadata.title || this.humanize(field),
metadata.description || '',
metadata.placeholder || '',
fieldValue,
metadata.required || false
)
this.requiredFieldsTarget.insertAdjacentHTML('beforeend', fieldHtml)
// Remove this field from currentData as it's now a required field
delete currentData[field]
})
// Convert any remaining data fields to custom fields
this.convertToCustomFields(currentData)
})
.catch(error => {
console.error('Error fetching required fields:', error)
this.requiredFieldsTarget.innerHTML = '<p class="text-sm text-red-500">Error loading required fields</p>'
})
}
getCurrentDataValues() {
const dataValues = {}
// Get values from required fields
if (this.hasRequiredFieldsTarget) {
const inputs = this.requiredFieldsTarget.querySelectorAll('input[name^="data["]')
inputs.forEach(input => {
const matches = input.name.match(/data\[(.*?)\]/)
if (matches && matches.length > 1) {
dataValues[matches[1]] = input.value
}
})
}
// Get values from custom fields
if (this.hasCustomFieldsTarget) {
const rows = this.customFieldsTarget.querySelectorAll('.custom-field-row')
rows.forEach(row => {
const keyInput = row.querySelector('input[name="custom_field_keys[]"]')
const valueInput = row.querySelector('input[name="custom_field_values[]"]')
if (keyInput && valueInput && keyInput.value) {
dataValues[keyInput.value] = valueInput.value
}
})
}
// Also include any data fields that might be in hidden inputs
document.querySelectorAll('input[type="hidden"][name^="data["]').forEach(input => {
const matches = input.name.match(/data\[(.*?)\]/)
if (matches && matches.length > 1) {
dataValues[matches[1]] = input.value
}
})
return dataValues
}
convertToCustomFields(dataValues) {
if (!this.hasCustomFieldsTarget) return
// Clear existing custom fields
this.customFieldsTarget.innerHTML = ''
// Add custom fields for each data value
Object.entries(dataValues).forEach(([key, value]) => {
if (key && value) {
this.addCustomField(key, value)
}
})
// Add at least one empty custom field if there are none
if (this.customFieldsTarget.querySelectorAll('.custom-field-row').length === 0) {
this.addCustomField()
}
}
generateRequiredFieldHtml(field, title, description, placeholder, value, required) {
return `
<div class="mb-4">
<label for="data_${field}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
${title}${required ? ' <span class="text-red-500">*</span>' : ''}
</label>
${description ? `<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">${description}</p>` : ''}
<input
type="text"
id="data_${field}"
name="data[${field}]"
value="${this.escapeHtml(value)}"
placeholder="${this.escapeHtml(placeholder)}"
${required ? 'required' : ''}
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
`
}
addCustomField(key = '', value = '') {
if (!this.hasCustomFieldsTarget || !this.hasCustomTemplateTarget) return
const template = this.customTemplateTarget.innerHTML
.replace(/KEY_PLACEHOLDER/g, this.escapeHtml(key))
.replace(/VALUE_PLACEHOLDER/g, this.escapeHtml(value))
this.customFieldsTarget.insertAdjacentHTML('beforeend', template)
}
removeCustomField(event) {
const row = event.target.closest('.custom-field-row')
if (row) {
row.remove()
}
}
// Helper to convert snake_case to Title Case
humanize(str) {
return str
.replace(/_/g, ' ')
.replace(/\b\w/g, l => l.toUpperCase())
}
// Helper to escape HTML special characters
escapeHtml(str) {
if (!str) return ''
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
}
require 'httparty' require "httparty"
module Pretalx module Pretalx
# See https://c3voc.de/wiki/schedule for more information about the format # See https://c3voc.de/wiki/schedule for more information about the format
...@@ -14,54 +14,54 @@ module Pretalx ...@@ -14,54 +14,54 @@ module Pretalx
schedule = JSON.parse(response.body) schedule = JSON.parse(response.body)
return Rails.logger.error "Incomplete JSON received from #{conference.schedule_url}" unless return Rails.logger.error "Incomplete JSON received from #{conference.schedule_url}" unless
schedule.dig('schedule', 'conference', 'rooms') && schedule.dig("schedule", "conference", "rooms") &&
schedule.dig('schedule', 'conference', 'days') schedule.dig("schedule", "conference", "days")
filedrop_index = fetch_filedrop_index(conference.filedrop_url) filedrop_index = fetch_filedrop_index(conference.filedrop_url)
# We keep a local hash of the stages, because the sessions reference stages by name instead of id # We keep a local hash of the stages, because the sessions reference stages by name instead of id
stages = {} stages = {}
schedule['schedule']['conference']['rooms'].each do |stage_data| schedule["schedule"]["conference"]["rooms"].each do |stage_data|
stages[stage_data['name']] = Stage.find_or_initialize_by(conference:, ref_id: stage_data['guid']).tap do |stage_| stages[stage_data["name"]] = Stage.find_or_initialize_by(conference:, ref_id: stage_data["guid"]).tap do |stage_|
stage_.name = stage_data['name'] stage_.name = stage_data["name"]
stage_.save! stage_.save!
end end
end end
# This is where canceled sessions are moved to, so we can still easily access them # This is where canceled sessions are moved to, so we can still easily access them
canceled_stage = Stage.find_or_initialize_by(conference:, ref_id: 'c3lingo_canceled').tap do |stage_| canceled_stage = Stage.find_or_initialize_by(conference:, ref_id: "c3lingo_canceled").tap do |stage_|
stage_.name = 'Canceled talk' stage_.name = "Canceled talk"
stage_.description = 'A dummy stage where talks move to when they disappear from the Fahrplan' stage_.description = "A dummy stage where talks move to when they disappear from the Fahrplan"
stage_.weight = 1000 # Sort it all the way to the right stage_.weight = 1000 # Sort it all the way to the right
stage_.save! stage_.save!
end end
# We build a list of sessions that exist in the current version of the fahrplan. That way we can move removed sessions to the "Canceled session" fake stage. # We build a list of sessions that exist in the current version of the fahrplan. That way we can move removed sessions to the "Canceled session" fake stage.
existing_sessions = [] existing_sessions = []
schedule['schedule']['conference']['days'].each do |day_data| schedule["schedule"]["conference"]["days"].each do |day_data|
day_data['rooms'].each do |stage_name, stage_data| day_data["rooms"].each do |stage_name, stage_data|
stage = stages[stage_name] stage = stages[stage_name]
stage_data.each do |session_data| stage_data.each do |session_data|
existing_sessions << session_data['guid'] existing_sessions << session_data["guid"]
Session.find_or_initialize_by(conference:, ref_id: session_data['guid']).tap do |session| Session.find_or_initialize_by(conference:, ref_id: session_data["guid"]).tap do |session|
session.stage = stage session.stage = stage
session.title = session_data['title'] session.title = session_data["title"]
session.language = session_data['language'] session.language = session_data["language"]
session.description = simple_format(session_data['abstract']) + simple_format(session_data['description']) session.description = simple_format(session_data["abstract"]) + simple_format(session_data["description"])
session.session_format = session_data['type'] session.session_format = session_data["type"]
session.track = session_data['track'] session.track = session_data["track"]
session.starts_at = session_data['date'] session.starts_at = session_data["date"]
hours, minutes = session_data['duration'].split(":").map(&:to_i) hours, minutes = session_data["duration"].split(":").map(&:to_i)
session.ends_at = session.starts_at + hours.hours + minutes.minutes session.ends_at = session.starts_at + hours.hours + minutes.minutes
session.url = session_data['url'] session.url = session_data["url"]
session.speakers = session_data['persons'].map do |speaker_data| session.speakers = session_data["persons"].map do |speaker_data|
Speaker.find_or_initialize_by(ref_id: speaker_data['guid'], conference:).tap do |speaker| Speaker.find_or_initialize_by(ref_id: speaker_data["guid"], conference:).tap do |speaker|
speaker.name = speaker_data['name'] || speaker_data['public_name'] speaker.name = speaker_data["name"] || speaker_data["public_name"]
speaker.description = simple_format(speaker_data['biography']) speaker.description = simple_format(speaker_data["biography"])
speaker.save! speaker.save!
end end
end end
session.recorded = !session_data.fetch('do_not_record', false) session.recorded = !session_data.fetch("do_not_record", false)
update_filedrop_data(session, filedrop_index[session.ref_id], conference.filedrop_url) if filedrop_index[session.ref_id] update_filedrop_data(session, filedrop_index[session.ref_id], conference.filedrop_url) if filedrop_index[session.ref_id]
session.save! session.save!
end end
...@@ -83,9 +83,9 @@ module Pretalx ...@@ -83,9 +83,9 @@ module Pretalx
return unless data = conference.fetch_engelsystem("angeltypes/#{translation_angel_id}/shifts") return unless data = conference.fetch_engelsystem("angeltypes/#{translation_angel_id}/shifts")
shifts = data.each_with_object({}) do |shift, hash| shifts = data.each_with_object({}) do |shift, hash|
starts_at = parse_datetime_or_nil(conference, shift['starts_at']) starts_at = parse_datetime_or_nil(conference, shift["starts_at"])
if hash[starts_at].nil? if hash[starts_at].nil?
hash[starts_at] = [shift] hash[starts_at] = [ shift ]
else else
hash[starts_at].push(shift) hash[starts_at].push(shift)
end end
...@@ -93,26 +93,25 @@ module Pretalx ...@@ -93,26 +93,25 @@ module Pretalx
Session.joins(:conference).where(conference:).each do |session| Session.joins(:conference).where(conference:).each do |session|
shifts_at_time = shifts[session.starts_at - 15.minutes] shifts_at_time = shifts[session.starts_at - 15.minutes]
unless shifts_at_time.nil? next if shifts_at_time.nil?
shifts_at_time.each do |shift|
if session.stage.name == shift.dig("location", "name") shifts_at_time.each do |shift|
session.engelsystem_id = shift["id"] next unless session.stage.name == shift.dig("location", "name")
session.engelsystem_url = shift["url"] session.engelsystem_id = shift["id"]
session.save session.engelsystem_url = shift["url"]
break session.save
end break
end
end end
end end
end end
def perform(conference_slug, *args) def perform(conference_slug, *_args)
conference = Conference.find_by(slug: conference_slug) conference = Conference.find_by(slug: conference_slug)
import_schedule(conference) import_schedule(conference)
import_engelsystem_refs(conference) import_engelsystem_refs(conference)
RevisionSet.create!(conference:) RevisionSet.create!(conference:)
heartbeat = conference.data['heartbeat_url'] heartbeat = conference.data["heartbeat_url"]
HTTParty.get(heartbeat) unless heartbeat.blank? HTTParty.get(heartbeat) unless heartbeat.blank?
end end
...@@ -126,12 +125,13 @@ module Pretalx ...@@ -126,12 +125,13 @@ module Pretalx
filedrop_url, filedrop_url,
basic_auth: { basic_auth: {
username: fetch_credential("filedrop_user"), username: fetch_credential("filedrop_user"),
password: fetch_credential("filedrop_password") }, password: fetch_credential("filedrop_password")
headers: { 'Accept' => 'application/json' }, },
headers: { "Accept" => "application/json" },
timeout: 30 timeout: 30
) )
data = JSON.parse(response.body) data = JSON.parse(response.body)
rescue => e rescue StandardError => e
Rails.logger.warn("Filedrop response for #{session.ref_id} failed: #{e.message}") Rails.logger.warn("Filedrop response for #{session.ref_id} failed: #{e.message}")
return {} return {}
end end
...@@ -140,7 +140,7 @@ module Pretalx ...@@ -140,7 +140,7 @@ module Pretalx
return {} return {}
end end
return data["talks"].each_with_object({}) do |item, hash| data["talks"].each_with_object({}) do |item, hash|
hash[item["id"]] = item hash[item["id"]] = item
end end
end end
...@@ -156,14 +156,14 @@ module Pretalx ...@@ -156,14 +156,14 @@ module Pretalx
# Add or update comments # Add or update comments
filedrop_data["comments"]&.each do |comment_data| filedrop_data["comments"]&.each do |comment_data|
session.filedrop_comments.find_or_initialize_by(body: comment_data['body']).tap do |comment| session.filedrop_comments.find_or_initialize_by(body: comment_data["body"]).tap do |comment|
comment.orig_created = parse_datetime_or_nil(session.conference, comment_data['meta']['created']) comment.orig_created = parse_datetime_or_nil(session.conference, comment_data["meta"]["created"])
comment.save! comment.save!
end end
end end
existing_files = session.filedrop_files.pluck(:name, :checksum) existing_files = session.filedrop_files.pluck(:name, :checksum)
new_files = filedrop_data['files']&.map { |d| [d['name'], d.dig('meta', 'hash')] } || [] new_files = filedrop_data["files"]&.map { |d| [ d["name"], d.dig("meta", "hash") ] } || []
# Remove files not in the JSON file # Remove files not in the JSON file
(existing_files - new_files).each do |name, checksum| (existing_files - new_files).each do |name, checksum|
...@@ -171,15 +171,15 @@ module Pretalx ...@@ -171,15 +171,15 @@ module Pretalx
end end
# Add or update files # Add or update files
filedrop_data['files']&.each do |file_data| filedrop_data["files"]&.each do |file_data|
session.filedrop_files.find_or_initialize_by(name: file_data['name'], checksum: file_data['meta']['hash']).tap do |file| session.filedrop_files.find_or_initialize_by(name: file_data["name"], checksum: file_data["meta"]["hash"]).tap do |file|
file.size = file_data['meta']['size'] file.size = file_data["meta"]["size"]
file.orig_created = parse_datetime_or_nil(session.conference, file_data['meta']['created']) file.orig_created = parse_datetime_or_nil(session.conference, file_data["meta"]["created"])
unless file_data['url'].blank? if file_data["url"].blank?
file.download(filedrop_url + file_data['url'].sub(/\A\//, ''))
file.save
else
Rails.logger.warn("Skipping incomplete file #{file.name} for #{session.ref_id}") Rails.logger.warn("Skipping incomplete file #{file.name} for #{session.ref_id}")
else
file.download(filedrop_url + file_data["url"].sub(/\A\//, ""))
file.save
end end
end end
end end
...@@ -187,7 +187,7 @@ module Pretalx ...@@ -187,7 +187,7 @@ module Pretalx
def parse_datetime_or_nil(conference, datetime_string) def parse_datetime_or_nil(conference, datetime_string)
DateTime.iso8601(datetime_string).in_time_zone(conference.time_zone) DateTime.iso8601(datetime_string).in_time_zone(conference.time_zone)
rescue rescue StandardError
nil nil
end end
end end
......
require 'httparty' require "httparty"
module Republica2023OrLater module Republica2023OrLater
class ImportJob < ApplicationJob class ImportJob < ApplicationJob
queue_as :default queue_as :default
def self.required_data_fields
[ "speakers_url", "sessions_url" ]
end
def self.field_metadata
{
"speakers_url" => {
title: "Speakers Data URL",
description: "URL to the speakers data in JSON format",
placeholder: "https://re-publica.com/sites/default/files/extappdata/<YEAR>/speaker.json"
},
"sessions_url" => {
title: "Sessions Data URL",
description: "URL to the sessions data in JSON format",
placeholder: "https://re-publica.com/sites/default/files/extappdata/<YEAR>/session.json"
}
}
end
def import_speakers(conference, url) def import_speakers(conference, url)
response = HTTParty.get(url) response = HTTParty.get(url)
if response.success? if response.success?
speakers = JSON.parse(response.body) speakers = JSON.parse(response.body)
speakers.each do |speaker_data| speakers.each do |speaker_data|
Speaker.find_or_initialize_by(ref_id: speaker_data['uid'], conference:).tap do |speaker| Speaker.find_or_initialize_by(ref_id: speaker_data["uid"], conference:).tap do |speaker|
speaker.name = speaker_data['name_raw'] speaker.name = speaker_data["name_raw"]
speaker.position = speaker_data['position'] speaker.position = speaker_data["position"]
speaker.description = speaker_data['bio'] speaker.description = speaker_data["bio"]
speaker.save! speaker.save!
end end
end end
...@@ -25,30 +44,30 @@ module Republica2023OrLater ...@@ -25,30 +44,30 @@ module Republica2023OrLater
response = HTTParty.get(url) response = HTTParty.get(url)
if response.success? if response.success?
sessions = JSON.parse(response.body) sessions = JSON.parse(response.body)
sessions.reject { |s| s['langcode'] == 'en' }.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 = Stage.find_or_initialize_by(conference:, ref_id: session_data["room_nid"]).tap do |stage_|
stage_.name = session_data['room'] stage_.name = session_data["room"]
stage_.save! stage_.save!
end end
Session.find_or_initialize_by(conference:, ref_id: session_data['nid']).tap do |session| Session.find_or_initialize_by(conference:, ref_id: session_data["nid"]).tap do |session|
session.stage = stage session.stage = stage
session.title = session_data['title'] session.title = session_data["title"]
session.language = session.language =
case session_data['language'] case session_data["language"]
when 'German', 'Deutsch' when "German", "Deutsch"
'de' "de"
when 'English', 'Englisch' when "English", "Englisch"
'en' "en"
end end
session.status = session_data['status'] session.status = session_data["status"]
session.description = session_data['description'] session.description = session_data["description"]
session.session_format = session_data['format'] session.session_format = session_data["format"]
session.track = session_data['track'] session.track = session_data["track"]
session.is_interpreted = (session_data['live_translation'] == "1") session.is_interpreted = (session_data["live_translation"] == "1")
session.starts_at = session_data['datetime_start'] session.starts_at = session_data["datetime_start"]
session.ends_at = session_data['datetime_end'] session.ends_at = session_data["datetime_end"]
session.url = "https://re-publica.com#{session_data['path']}" session.url = "https://re-publica.com#{session_data['path']}"
session.speakers = session_data['speaker_uid'].map { |speaker_uid| conference.speakers.find_by!(ref_id: speaker_uid) } session.speakers = session_data["speaker_uid"].map { |speaker_uid| conference.speakers.find_by!(ref_id: speaker_uid) }
session.save! session.save!
end end
end end
...@@ -57,11 +76,11 @@ module Republica2023OrLater ...@@ -57,11 +76,11 @@ module Republica2023OrLater
end end
end end
def perform(conference_slug, *args) def perform(conference_slug, *_args)
conference = Conference.find_by(slug: conference_slug) conference = Conference.find_by(slug: conference_slug)
import_speakers(conference, conference.data['speakers_url']) import_speakers(conference, conference.data["speakers_url"])
import_sessions(conference, conference.data['sessions_url']) import_sessions(conference, conference.data["sessions_url"])
revision_set = RevisionSet.create!(conference:) RevisionSet.create!(conference:)
end end
end end
end end
require 'telegram/bot' require "telegram/bot"
class TelegramGroupChatNotificationJob < NotificationJob class TelegramGroupChatNotificationJob < NotificationJob
queue_as :notifications queue_as :notifications
def perform(**args) def perform(**args)
channel = NotificationChannel.find_by(name: 'telegram_group_chat') channel = NotificationChannel.find_by(name: "telegram_group_chat")
Rails.logger.debug("TelegramGroupChatNotificationJob #{args.inspect}") Rails.logger.debug("TelegramGroupChatNotificationJob #{args.inspect}")
return unless channel&.data return unless channel&.data
token = channel.data['token'] token = channel.data["token"]
return unless token return unless token
args[:parse_mode] ||= "HTML" args[:parse_mode] ||= "HTML"
args[:target] ||= Rails.application.config.telegram_default_target args[:target] ||= Rails.application.config.telegram_default_target
......
...@@ -10,7 +10,7 @@ class TelegramNotifyUpcomingJob < ApplicationJob ...@@ -10,7 +10,7 @@ class TelegramNotifyUpcomingJob < ApplicationJob
if assignees.length.positive? if assignees.length.positive?
notify_names = assignees.map { |a| a.telegram_username ? "@#{a.telegram_username}" : a.name } notify_names = assignees.map { |a| a.telegram_username ? "@#{a.telegram_username}" : a.name }
message = notify_names.join(' ') + ": Your scheduled session <i>#{session.title}</i> starts at <b>#{session.starts_at.strftime("%H:%M")}</b> on <b>#{session.stage.name}</b>" message = notify_names.join(" ") + ": Your scheduled session <i>#{session.title}</i> starts at <b>#{session.starts_at.strftime("%H:%M")}</b> on <b>#{session.stage.name}</b>"
message += "<br>Speakers: #{session.speakers.map(&:name).join(', ')}" message += "<br>Speakers: #{session.speakers.map(&:name).join(', ')}"
TelegramGroupChatNotificationJob.perform_later(text: message) TelegramGroupChatNotificationJob.perform_later(text: message)
......
...@@ -9,10 +9,10 @@ class Assignment < ApplicationRecord ...@@ -9,10 +9,10 @@ class Assignment < ApplicationRecord
after_create_commit :notify_assignment_created after_create_commit :notify_assignment_created
after_destroy_commit :notify_assignment_destroyed after_destroy_commit :notify_assignment_destroyed
scope :future, -> { joins(:session).where('sessions.starts_at' => Time.now..) } scope :future, -> { joins(:session).where("sessions.starts_at" => Time.now..) }
after_create_commit -> { after_create_commit -> {
Rails.logger.debug('Created assignment, broadcasting') Rails.logger.debug("Created assignment, broadcasting")
broadcast_replace_to "sessions", broadcast_replace_to "sessions",
target: session, target: session,
partial: "sessions/session", partial: "sessions/session",
...@@ -33,10 +33,9 @@ class Assignment < ApplicationRecord ...@@ -33,10 +33,9 @@ class Assignment < ApplicationRecord
) )
logger.debug(overlapping_assignments) logger.debug(overlapping_assignments)
return unless overlapping_assignments.exists?
if overlapping_assignments.exists? errors.add(:base, "This assignment overlaps with another assignment for this user.")
errors.add(:base, "This assignment overlaps with another assignment for this user.")
end
end end
def notify_assignment_created def notify_assignment_created
......