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 @@
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
# 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
# Rails app lives here
......
source "https://rubygems.org"
ruby "3.3.6"
ruby "3.4.1"
gem "rails", "~> 8.0.1"
......@@ -17,14 +17,18 @@ gem "puma", ">= 5.0"
gem "redis", ">= 4.0.1"
# 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
gem "bootsnap", require: false
group :development, :test do
# 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
group :development do
......@@ -50,8 +54,8 @@ gem "httparty", "> 0"
gem "tailwindcss-rails", "~> 3.1"
gem "turbo-rails", "~> 2.0"
gem "stimulus-rails", "~> 1.3"
gem "turbo-rails", "~> 2.0"
gem "importmap-rails", "~> 2.0"
......@@ -59,6 +63,6 @@ gem "icalendar", "~> 2.10"
gem "telegram-bot-ruby", "~> 2.0"
gem 'devise', '~> 4.9'
gem "devise", "~> 4.9"
gem "crono", "~> 2.1"
......@@ -74,6 +74,7 @@ GEM
uri (>= 0.13.1)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
ast (2.4.2)
base64 (0.2.0)
bcrypt (3.1.20)
benchmark (0.4.0)
......@@ -91,8 +92,8 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
concurrent-ruby (1.3.4)
connection_pool (2.4.1)
concurrent-ruby (1.3.5)
connection_pool (2.5.0)
crass (1.0.6)
crono (2.1.0)
rails (>= 5.2.8)
......@@ -108,21 +109,22 @@ GEM
responders
warden (~> 1.2.3)
drb (2.2.1)
dry-core (1.0.2)
dry-core (1.1.0)
concurrent-ruby (~> 1.0)
logger
zeitwerk (~> 2.6)
dry-inflector (1.1.0)
dry-logic (1.5.0)
dry-inflector (1.2.0)
dry-logic (1.6.0)
bigdecimal
concurrent-ruby (~> 1.0)
dry-core (~> 1.0, < 2)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
dry-struct (1.6.0)
dry-core (~> 1.0, < 2)
dry-types (>= 1.7, < 2)
dry-struct (1.7.1)
dry-core (~> 1.1)
dry-types (~> 1.8, >= 1.8.2)
ice_nine (~> 0.11)
zeitwerk (~> 2.6)
dry-types (1.7.2)
dry-types (1.8.2)
bigdecimal (~> 3.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0)
......@@ -149,7 +151,7 @@ GEM
csv
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
i18n (1.14.6)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
icalendar (2.10.3)
ice_cube (~> 0.16)
......@@ -161,11 +163,14 @@ GEM
activesupport (>= 6.0.0)
railties (>= 6.0.0)
io-console (0.8.0)
irb (1.14.3)
irb (1.15.1)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
json (2.9.1)
logger (1.6.5)
json (2.10.1)
language_server-protocol (3.17.0.4)
lint_roller (1.1.0)
logger (1.6.6)
loofah (2.24.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
......@@ -178,40 +183,48 @@ GEM
matrix (0.4.2)
mini_mime (1.1.5)
minitest (5.25.4)
msgpack (1.7.5)
msgpack (1.8.0)
multi_xml (0.7.1)
bigdecimal (~> 3.1)
multipart-post (2.4.1)
net-http (0.6.0)
uri
net-imap (0.5.4)
net-imap (0.5.6)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.5.0)
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.1-aarch64-linux-gnu)
nokogiri (1.18.3-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.1-arm64-darwin)
nokogiri (1.18.3-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.1-x86_64-linux-gnu)
nokogiri (1.18.3-x86_64-linux-gnu)
racc (~> 1.4)
orm_adapter (0.5.0)
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
stringio
public_suffix (6.0.1)
puma (6.5.0)
puma (6.6.0)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.1.8)
rack-session (2.0.0)
rack (3.1.11)
rack-session (2.1.0)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
rack (>= 1.3)
......@@ -246,12 +259,13 @@ GEM
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
rdoc (6.10.0)
rdoc (6.12.0)
psych (>= 4.0.0)
redis (5.3.0)
redis (5.4.0)
redis-client (>= 0.22.0)
redis-client (0.23.0)
redis-client (0.23.2)
connection_pool
regexp_parser (2.10.0)
reline (0.6.0)
......@@ -259,16 +273,46 @@ GEM
responders (3.1.1)
actionpack (>= 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)
securerandom (0.4.1)
selenium-webdriver (4.28.0)
selenium-webdriver (4.29.1)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
solid_queue (1.1.2)
solid_queue (1.1.3)
activejob (>= 7.1)
activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1)
......@@ -282,31 +326,34 @@ GEM
actionpack (>= 6.1)
activesupport (>= 6.1)
sprockets (>= 3.0.0)
sqlite3 (2.5.0-aarch64-linux-gnu)
sqlite3 (2.5.0-arm64-darwin)
sqlite3 (2.5.0-x86_64-linux-gnu)
sqlite3 (2.6.0-aarch64-linux-gnu)
sqlite3 (2.6.0-arm64-darwin)
sqlite3 (2.6.0-x86_64-linux-gnu)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.1.2)
tailwindcss-rails (3.1.0)
stringio (3.1.5)
tailwindcss-rails (3.3.1)
railties (>= 7.0.0)
tailwindcss-ruby
tailwindcss-ruby (~> 3.0)
tailwindcss-ruby (3.4.17-aarch64-linux)
tailwindcss-ruby (3.4.17-arm64-darwin)
tailwindcss-ruby (3.4.17-x86_64-linux)
telegram-bot-ruby (2.2.0)
telegram-bot-ruby (2.4.0)
dry-struct (~> 1.6)
faraday (~> 2.0)
faraday-multipart (~> 1.0)
zeitwerk (~> 2.6)
thor (1.3.2)
timeout (0.4.3)
turbo-rails (2.0.11)
actionpack (>= 6.0.0)
railties (>= 6.0.0)
turbo-rails (2.0.13)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
tzinfo (2.0.6)
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)
warden (1.2.9)
rack (>= 2.0.9)
......@@ -316,12 +363,13 @@ GEM
bindex (>= 0.4.0)
railties (>= 6.0.0)
websocket (1.2.11)
websocket-driver (0.7.6)
websocket-driver (0.7.7)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.1)
zeitwerk (2.7.2)
PLATFORMS
aarch64-linux
......@@ -341,6 +389,8 @@ DEPENDENCIES
puma (>= 5.0)
rails (~> 8.0.1)
redis (>= 4.0.1)
rubocop-capybara (~> 2.21)
rubocop-rails-omakase (~> 1.1)
selenium-webdriver
solid_queue (~> 1.1)
sprockets-rails (> 0)
......@@ -353,7 +403,7 @@ DEPENDENCIES
web-console (> 0)
RUBY VERSION
ruby 3.3.6p108
ruby 3.4.1p0
BUNDLED WITH
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
protected
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|
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
def authorize_shiftcoordinator
unless current_user.shiftcoordinator?
render plain: 'Forbidden', status: :forbidden
end
authorize_role("shift_coordinator")
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
require 'icalendar/tzinfo'
require "icalendar/tzinfo"
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
def index
@assignments = Assignment.all.joins(:session, :user).order('sessions.starts_at')
if params[:user_id]
@assignments = @assignments.where(user_id: params[:user_id])
end
@assignments = Assignment.all.joins(:session, :user).order("sessions.starts_at")
return unless params[:user_id]
@assignments = @assignments.where(user_id: params[:user_id])
end
def create
params[:user_id] ||= params[:assignment][:user_id]
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|
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
return
end
......@@ -35,13 +35,13 @@ class AssignmentsController < ApplicationController
partial: "sessions/session",
locals: { session: @session }
)
flash.now[:success] = 'User assigned successfully.'
flash.now[:success] = "User assigned successfully."
respond_to do |format|
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
else
flash.now[:alert] = 'Failed to assign user.'
flash.now[:alert] = "Failed to assign user."
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.html { render :show, status: :unprocessable_entity }
......@@ -63,12 +63,12 @@ class AssignmentsController < ApplicationController
)
respond_to do |format|
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
else
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.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
......@@ -80,7 +80,7 @@ class AssignmentsController < ApplicationController
format.ics do
calendar = Icalendar::Calendar.new
tz = TZInfo::Timezone.get('UTC')
tz = TZInfo::Timezone.get("UTC")
calendar.add_timezone tz.ical_timezone Time.now
@user.assignments.each do |assignment|
......@@ -96,18 +96,18 @@ class AssignmentsController < ApplicationController
event = Icalendar::Event.new
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.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.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.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>"))
calendar.add_event(event)
end
calendar.publish
headers['Content-Type'] = 'text/calendar; charset=UTF-8'
headers["Content-Type"] = "text/calendar; charset=UTF-8"
render plain: calendar.to_ical
end
end
......@@ -115,6 +115,10 @@ class AssignmentsController < ApplicationController
private
def authorize_permission
super("manage_assignments")
end
def set_session
conference = Conference.find_by(slug: params[:conference_slug])
@session = Session.find_by(conference:, ref_id: params[:session_ref_id])
......
require 'icalendar/tzinfo'
require "icalendar/tzinfo"
class CandidatesController < ApplicationController
before_action :authorize_shiftcoordinator, except: [:create, :destroy_self]
before_action :authorize_permission, except: %i[create destroy_self]
def create
@conference = Conference.find_by(slug: params[:conference_slug])
......@@ -13,20 +13,20 @@ class CandidatesController < ApplicationController
if @candidate
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(
@session.conference,
target: helpers.dom_id(@session),
partial: "sessions/session",
locals: { session: @session }
)
flash.now[:success] = 'User assigned successfully.'
flash.now[:success] = "User assigned successfully."
respond_to do |format|
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
else
flash.now[:alert] = 'Failed to record candidate.'
flash.now[:alert] = "Failed to record candidate."
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.html { render :show, status: :unprocessable_entity }
......@@ -50,6 +50,10 @@ class CandidatesController < ApplicationController
private
def authorize_permission
super("manage_assignments")
end
def destroy_candidate(session, candidate)
if candidate&.destroy
Rails.logger.debug("destroyed candidate entry")
......@@ -61,12 +65,12 @@ class CandidatesController < ApplicationController
)
respond_to do |format|
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
else
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.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
......
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
@conferences = Conference.all
end
def show
@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]
date = Time.parse(params[:date])
logger.debug(date)
......@@ -20,16 +107,42 @@ class ConferencesController < ApplicationController
@users = User.all
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
# @conference is set by the before_action
end
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
def stats
@conference = Conference.find_by(slug: params[:slug])
@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
@language_stats = @relevant_sessions.group_by { |s| s.language }.transform_values(&:count)
@speakers = @relevant_sessions.map(&:speakers)
......@@ -44,6 +157,10 @@ class ConferencesController < ApplicationController
scheduled_time_min: se.map { |x| (x.ends_at - x.starts_at) / 60.0 }.sum
}
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
class FiledropFilesController < ApplicationController
before_action :authenticate_user!
before_action :set_filedrop_file, only: [:download]
before_action :set_filedrop_file, only: [ :download ]
def download
# Define the file path within the storage directory
......@@ -8,9 +8,9 @@ class FiledropFilesController < ApplicationController
# Send the file to the user
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
render plain: 'File not found', status: :not_found
render plain: "File not found", status: :not_found
end
end
......
class SessionsController < ApplicationController
before_action :authenticate_user!, except: [:index]
before_action :authorize_shiftcoordinator, except: [:index, :show]
before_action :authenticate_user!, except: [ :index ]
before_action :authorize_permission, except: %i[index show]
def index
@conference = Conference.find_by(slug: params[:conference_slug])
......@@ -13,10 +13,8 @@ class SessionsController < ApplicationController
end
# Filter by stage name if provided
if params[:stage].present?
@sessions = @sessions.joins(:stage).where(stages: { name: params[:stage] })
end
return unless params[:stage].present?
@sessions = @sessions.joins(:stage).where(stages: { name: params[:stage] })
# Further filtering options can be added here
end
......@@ -39,13 +37,13 @@ class SessionsController < ApplicationController
partial: "sessions/session",
locals: { session: }
)
flash.now[:success] = 'Notes saved successfully.'
flash.now[:success] = "Notes saved successfully."
respond_to do |format|
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
else
flash.now[:alert] = 'Failed to save notes.'
flash.now[:alert] = "Failed to save notes."
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.html { render :show, status: :unprocessable_entity }
......@@ -55,6 +53,10 @@ class SessionsController < ApplicationController
private
def authorize_permission
super("manage_assignments")
end
def notes_params
params.require(:session).permit(:notes)
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
# See https://c3voc.de/wiki/schedule for more information about the format
......@@ -14,54 +14,54 @@ module Pretalx
schedule = JSON.parse(response.body)
return Rails.logger.error "Incomplete JSON received from #{conference.schedule_url}" unless
schedule.dig('schedule', 'conference', 'rooms') &&
schedule.dig('schedule', 'conference', 'days')
schedule.dig("schedule", "conference", "rooms") &&
schedule.dig("schedule", "conference", "days")
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
stages = {}
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_|
stage_.name = stage_data['name']
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_|
stage_.name = stage_data["name"]
stage_.save!
end
end
# 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_|
stage_.name = 'Canceled talk'
stage_.description = 'A dummy stage where talks move to when they disappear from the Fahrplan'
canceled_stage = Stage.find_or_initialize_by(conference:, ref_id: "c3lingo_canceled").tap do |stage_|
stage_.name = "Canceled talk"
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_.save!
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.
existing_sessions = []
schedule['schedule']['conference']['days'].each do |day_data|
day_data['rooms'].each do |stage_name, stage_data|
schedule["schedule"]["conference"]["days"].each do |day_data|
day_data["rooms"].each do |stage_name, stage_data|
stage = stages[stage_name]
stage_data.each do |session_data|
existing_sessions << session_data['guid']
Session.find_or_initialize_by(conference:, ref_id: session_data['guid']).tap do |session|
existing_sessions << session_data["guid"]
Session.find_or_initialize_by(conference:, ref_id: session_data["guid"]).tap do |session|
session.stage = stage
session.title = session_data['title']
session.language = session_data['language']
session.description = simple_format(session_data['abstract']) + simple_format(session_data['description'])
session.session_format = session_data['type']
session.track = session_data['track']
session.starts_at = session_data['date']
hours, minutes = session_data['duration'].split(":").map(&:to_i)
session.title = session_data["title"]
session.language = session_data["language"]
session.description = simple_format(session_data["abstract"]) + simple_format(session_data["description"])
session.session_format = session_data["type"]
session.track = session_data["track"]
session.starts_at = session_data["date"]
hours, minutes = session_data["duration"].split(":").map(&:to_i)
session.ends_at = session.starts_at + hours.hours + minutes.minutes
session.url = session_data['url']
session.speakers = session_data['persons'].map do |speaker_data|
Speaker.find_or_initialize_by(ref_id: speaker_data['guid'], conference:).tap do |speaker|
speaker.name = speaker_data['name'] || speaker_data['public_name']
speaker.description = simple_format(speaker_data['biography'])
session.url = session_data["url"]
session.speakers = session_data["persons"].map do |speaker_data|
Speaker.find_or_initialize_by(ref_id: speaker_data["guid"], conference:).tap do |speaker|
speaker.name = speaker_data["name"] || speaker_data["public_name"]
speaker.description = simple_format(speaker_data["biography"])
speaker.save!
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]
session.save!
end
......@@ -83,9 +83,9 @@ module Pretalx
return unless data = conference.fetch_engelsystem("angeltypes/#{translation_angel_id}/shifts")
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?
hash[starts_at] = [shift]
hash[starts_at] = [ shift ]
else
hash[starts_at].push(shift)
end
......@@ -93,26 +93,25 @@ module Pretalx
Session.joins(:conference).where(conference:).each do |session|
shifts_at_time = shifts[session.starts_at - 15.minutes]
unless shifts_at_time.nil?
shifts_at_time.each do |shift|
if session.stage.name == shift.dig("location", "name")
session.engelsystem_id = shift["id"]
session.engelsystem_url = shift["url"]
session.save
break
end
end
next if shifts_at_time.nil?
shifts_at_time.each do |shift|
next unless session.stage.name == shift.dig("location", "name")
session.engelsystem_id = shift["id"]
session.engelsystem_url = shift["url"]
session.save
break
end
end
end
def perform(conference_slug, *args)
def perform(conference_slug, *_args)
conference = Conference.find_by(slug: conference_slug)
import_schedule(conference)
import_engelsystem_refs(conference)
RevisionSet.create!(conference:)
heartbeat = conference.data['heartbeat_url']
heartbeat = conference.data["heartbeat_url"]
HTTParty.get(heartbeat) unless heartbeat.blank?
end
......@@ -126,12 +125,13 @@ module Pretalx
filedrop_url,
basic_auth: {
username: fetch_credential("filedrop_user"),
password: fetch_credential("filedrop_password") },
headers: { 'Accept' => 'application/json' },
password: fetch_credential("filedrop_password")
},
headers: { "Accept" => "application/json" },
timeout: 30
)
data = JSON.parse(response.body)
rescue => e
rescue StandardError => e
Rails.logger.warn("Filedrop response for #{session.ref_id} failed: #{e.message}")
return {}
end
......@@ -140,7 +140,7 @@ module Pretalx
return {}
end
return data["talks"].each_with_object({}) do |item, hash|
data["talks"].each_with_object({}) do |item, hash|
hash[item["id"]] = item
end
end
......@@ -156,14 +156,14 @@ module Pretalx
# Add or update comments
filedrop_data["comments"]&.each do |comment_data|
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'])
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.save!
end
end
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
(existing_files - new_files).each do |name, checksum|
......@@ -171,15 +171,15 @@ module Pretalx
end
# Add or update files
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|
file.size = file_data['meta']['size']
file.orig_created = parse_datetime_or_nil(session.conference, file_data['meta']['created'])
unless file_data['url'].blank?
file.download(filedrop_url + file_data['url'].sub(/\A\//, ''))
file.save
else
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|
file.size = file_data["meta"]["size"]
file.orig_created = parse_datetime_or_nil(session.conference, file_data["meta"]["created"])
if file_data["url"].blank?
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
......@@ -187,7 +187,7 @@ module Pretalx
def parse_datetime_or_nil(conference, datetime_string)
DateTime.iso8601(datetime_string).in_time_zone(conference.time_zone)
rescue
rescue StandardError
nil
end
end
......
require 'httparty'
require "httparty"
module Republica2023OrLater
class ImportJob < ApplicationJob
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)
response = HTTParty.get(url)
if response.success?
speakers = JSON.parse(response.body)
speakers.each do |speaker_data|
Speaker.find_or_initialize_by(ref_id: speaker_data['uid'], conference:).tap do |speaker|
speaker.name = speaker_data['name_raw']
speaker.position = speaker_data['position']
speaker.description = speaker_data['bio']
Speaker.find_or_initialize_by(ref_id: speaker_data["uid"], conference:).tap do |speaker|
speaker.name = speaker_data["name_raw"]
speaker.position = speaker_data["position"]
speaker.description = speaker_data["bio"]
speaker.save!
end
end
......@@ -25,30 +44,30 @@ module Republica2023OrLater
response = HTTParty.get(url)
if response.success?
sessions = JSON.parse(response.body)
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']
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!
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.title = session_data['title']
session.title = session_data["title"]
session.language =
case session_data['language']
when 'German', 'Deutsch'
'de'
when 'English', 'Englisch'
'en'
case session_data["language"]
when "German", "Deutsch"
"de"
when "English", "Englisch"
"en"
end
session.status = session_data['status']
session.description = session_data['description']
session.session_format = session_data['format']
session.track = session_data['track']
session.is_interpreted = (session_data['live_translation'] == "1")
session.starts_at = session_data['datetime_start']
session.ends_at = session_data['datetime_end']
session.status = session_data["status"]
session.description = session_data["description"]
session.session_format = session_data["format"]
session.track = session_data["track"]
session.is_interpreted = (session_data["live_translation"] == "1")
session.starts_at = session_data["datetime_start"]
session.ends_at = session_data["datetime_end"]
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!
end
end
......@@ -57,11 +76,11 @@ module Republica2023OrLater
end
end
def perform(conference_slug, *args)
def perform(conference_slug, *_args)
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:)
import_speakers(conference, conference.data["speakers_url"])
import_sessions(conference, conference.data["sessions_url"])
RevisionSet.create!(conference:)
end
end
end
require 'telegram/bot'
require "telegram/bot"
class TelegramGroupChatNotificationJob < NotificationJob
queue_as :notifications
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}")
return unless channel&.data
token = channel.data['token']
token = channel.data["token"]
return unless token
args[:parse_mode] ||= "HTML"
args[:target] ||= Rails.application.config.telegram_default_target
......
......@@ -10,7 +10,7 @@ class TelegramNotifyUpcomingJob < ApplicationJob
if assignees.length.positive?
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(', ')}"
TelegramGroupChatNotificationJob.perform_later(text: message)
......
......@@ -9,10 +9,10 @@ class Assignment < ApplicationRecord
after_create_commit :notify_assignment_created
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 -> {
Rails.logger.debug('Created assignment, broadcasting')
Rails.logger.debug("Created assignment, broadcasting")
broadcast_replace_to "sessions",
target: session,
partial: "sessions/session",
......@@ -33,10 +33,9 @@ class Assignment < ApplicationRecord
)
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.")
end
errors.add(:base, "This assignment overlaps with another assignment for this user.")
end
def notify_assignment_created
......