From bbc29c4b503460af44cfd302c1d892087d90b6d9 Mon Sep 17 00:00:00 2001 From: Teal <git@teal.is> Date: Tue, 4 Mar 2025 18:27:58 +0100 Subject: [PATCH] Add UI for custom data handling, ImportJob class selection --- app/controllers/assignments_controller.rb | 12 +- app/controllers/candidates_controller.rb | 6 +- app/controllers/conferences_controller.rb | 123 +++++++++++- app/controllers/sessions_controller.rb | 12 +- .../controllers/conference_form_controller.js | 181 ++++++++++++++++++ app/jobs/pretalx/import_job.rb | 32 ++-- .../republica_2023_or_later/import_job.rb | 23 ++- app/models/assignment.rb | 5 +- app/models/conference.rb | 80 ++++---- app/subscribers/telegram_bot_subscriber.rb | 2 +- app/views/conferences/edit.html.erb | 158 +++++++++++++++ app/views/conferences/index.html.erb | 59 +++++- app/views/conferences/new.html.erb | 115 +++++++++++ app/views/conferences/show.html.erb | 4 +- .../initializers/filter_parameter_logging.rb | 4 +- config/queue.yml | 36 ++-- config/routes.rb | 7 +- 17 files changed, 759 insertions(+), 100 deletions(-) create mode 100644 app/javascript/controllers/conference_form_controller.js create mode 100644 app/views/conferences/edit.html.erb create mode 100644 app/views/conferences/new.html.erb diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb index 97715b6..8d30fce 100644 --- a/app/controllers/assignments_controller.rb +++ b/app/controllers/assignments_controller.rb @@ -1,14 +1,14 @@ 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 + return unless params[:user_id] + + @assignments = @assignments.where(user_id: params[:user_id]) end def create @@ -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]) diff --git a/app/controllers/candidates_controller.rb b/app/controllers/candidates_controller.rb index 5f1dac0..d599067 100644 --- a/app/controllers/candidates_controller.rb +++ b/app/controllers/candidates_controller.rb @@ -1,7 +1,7 @@ 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]) @@ -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") diff --git a/app/controllers/conferences_controller.rb b/app/controllers/conferences_controller.rb index ebea5e7..d62e63b 100644 --- a/app/controllers/conferences_controller.rb +++ b/app/controllers/conferences_controller.rb @@ -1,11 +1,98 @@ 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 diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 2ea28ce..ec5761a 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,6 +1,6 @@ class SessionsController < ApplicationController before_action :authenticate_user!, except: [ :index ] - before_action :authorize_shiftcoordinator, except: [ :index, :show ] + 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 @@ -55,6 +53,10 @@ class SessionsController < ApplicationController private + def authorize_permission + super("manage_assignments") + end + def notes_params params.require(:session).permit(:notes) end diff --git a/app/javascript/controllers/conference_form_controller.js b/app/javascript/controllers/conference_form_controller.js new file mode 100644 index 0000000..ccb03cd --- /dev/null +++ b/app/javascript/controllers/conference_form_controller.js @@ -0,0 +1,181 @@ +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, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + } +} diff --git a/app/jobs/pretalx/import_job.rb b/app/jobs/pretalx/import_job.rb index ac83733..db4f801 100644 --- a/app/jobs/pretalx/import_job.rb +++ b/app/jobs/pretalx/import_job.rb @@ -93,20 +93,19 @@ 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) @@ -126,12 +125,13 @@ module Pretalx filedrop_url, basic_auth: { username: fetch_credential("filedrop_user"), - password: fetch_credential("filedrop_password") }, + 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 @@ -175,11 +175,11 @@ module Pretalx 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? + 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 - else - Rails.logger.warn("Skipping incomplete file #{file.name} for #{session.ref_id}") 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 diff --git a/app/jobs/republica_2023_or_later/import_job.rb b/app/jobs/republica_2023_or_later/import_job.rb index adef5fc..1e69748 100644 --- a/app/jobs/republica_2023_or_later/import_job.rb +++ b/app/jobs/republica_2023_or_later/import_job.rb @@ -4,6 +4,25 @@ 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? @@ -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:) + RevisionSet.create!(conference:) end end end diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 91f00aa..3089f9a 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -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 diff --git a/app/models/conference.rb b/app/models/conference.rb index 29c4688..95a1914 100644 --- a/app/models/conference.rb +++ b/app/models/conference.rb @@ -43,6 +43,20 @@ class Conference < ApplicationRecord data["heartbeat_url"] end + # Get the required data fields for the import job class + def required_data_fields + return [] if import_job_class.blank? + + begin + # Dynamically query the import job class for its required fields + klass = import_job_class.constantize + klass.respond_to?(:required_data_fields) ? klass.required_data_fields : [] + rescue NameError => e + Rails.logger.error("Could not find import job class #{import_job_class}: #{e}") + [] + end + end + def fetch_translation_angel_id fetch_engelsystem("angeltypes") &.find { |t| t["name"] == "Translation Angel" } @@ -50,22 +64,20 @@ class Conference < ApplicationRecord end def fetch_engelsystem(endpoint) - begin - endpoint_url = engelsystem_url + endpoint - Rails.logger.debug("Querying engelsystem API at #{endpoint_url}") - response = HTTParty.get( - endpoint_url, - headers: { - "Accept" => "application/json", - "x-api-key" => fetch_credential("engelsystem_token") - }, - timeout: 10 - ) - response.success? ? JSON.parse(response.body)["data"] : nil - rescue => e - Rails.logger.warn("Engelsystem query for #{endpoint} failed: #{e.message}") - nil - end + endpoint_url = engelsystem_url + endpoint + Rails.logger.debug("Querying engelsystem API at #{endpoint_url}") + response = HTTParty.get( + endpoint_url, + headers: { + "Accept" => "application/json", + "x-api-key" => fetch_credential("engelsystem_token") + }, + timeout: 10 + ) + response.success? ? JSON.parse(response.body)["data"] : nil + rescue StandardError => e + Rails.logger.warn("Engelsystem query for #{endpoint} failed: #{e.message}") + nil end def compare_engelsystem_shifts(additional_conferences = []) @@ -74,32 +86,32 @@ class Conference < ApplicationRecord engelsystem_shifts = data.each_with_object({}) do |shift, hash| hash[shift["id"]] = shift - &.dig("needed_angel_types") - &.find { |t| t["angel_type"]["id"] == translation_angel_id } - &.dig("entries") - &.map { |t| t["user"]["name"] } + &.dig("needed_angel_types") + &.find { |t| t["angel_type"]["id"] == translation_angel_id } + &.dig("entries") + &.map { |t| t["user"]["name"] } end Session - .where(conference: [ self, *additional_conferences ]) - .where.not(engelsystem_id: nil) - .includes(assignments: :user) - .group_by(&:engelsystem_id) - .each do |engelsystem_id, sessions| + .where(conference: [ self, *additional_conferences ]) + .where.not(engelsystem_id: nil) + .includes(assignments: :user) + .group_by(&:engelsystem_id) + .each do |engelsystem_id, sessions| engelsystem_assigned = engelsystem_shifts[engelsystem_id] - local_assigned = sessions.flat_map(&:assignments).map { |a|a.user.name } + local_assigned = sessions.flat_map(&:assignments).map { |a| a.user.name } only_engelsystem = engelsystem_assigned - local_assigned only_local = local_assigned - engelsystem_assigned - unless only_engelsystem.blank? and only_local.blank? - puts "=============================" - puts "Session: #{sessions[0].title} (#{engelsystem_id})" - puts "=============================" - puts "Not signed up in engelsystem: #{only_local.join(", ")}" unless only_local.blank? - puts "Missing in local assignments: #{only_engelsystem.join(", ")}" unless only_engelsystem.blank? - puts - end + next if only_engelsystem.blank? and only_local.blank? + + puts "=============================" + puts "Session: #{sessions[0].title} (#{engelsystem_id})" + puts "=============================" + puts "Not signed up in engelsystem: #{only_local.join(', ')}" unless only_local.blank? + puts "Missing in local assignments: #{only_engelsystem.join(', ')}" unless only_engelsystem.blank? + puts end true diff --git a/app/subscribers/telegram_bot_subscriber.rb b/app/subscribers/telegram_bot_subscriber.rb index 43f83ae..caef521 100644 --- a/app/subscribers/telegram_bot_subscriber.rb +++ b/app/subscribers/telegram_bot_subscriber.rb @@ -12,7 +12,7 @@ class TelegramBotSubscriber def handle_session_updated(event) Rails.logger.info("session event #{event.inspect}") - model_name, action = event.name.split(".") + _, action = event.name.split(".") session = event.payload[:record] changes = event.payload[:changes] diff --git a/app/views/conferences/edit.html.erb b/app/views/conferences/edit.html.erb new file mode 100644 index 0000000..4acecd2 --- /dev/null +++ b/app/views/conferences/edit.html.erb @@ -0,0 +1,158 @@ +<div class="container mx-auto px-4 py-8"> + <h1 class="text-2xl font-bold mb-6 dark:text-gray-200">Edit Conference: <%= @conference.name %></h1> + + <%= form_with(model: @conference, url: conference_path(slug: @conference.slug), method: :patch, class: "space-y-6") do |form| %> + <% if @conference.errors.any? %> + <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4"> + <h2 class="font-bold"><%= pluralize(@conference.errors.count, "error") %> prohibited this conference from being saved:</h2> + <ul class="list-disc list-inside mt-2"> + <% @conference.errors.full_messages.each do |message| %> + <li><%= message %></li> + <% end %> + </ul> + </div> + <% end %> + + <div class="space-y-4"> + <div> + <%= form.label :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> + <%= form.text_field :name, 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> + + <div> + <%= form.label :slug, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> + <%= form.text_field :slug, 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" %> + <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Used in URLs. Should contain only lowercase letters, numbers, and hyphens.</p> + </div> + + <div> + <%= form.label :url, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> + <%= form.url_field :url, 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> + + <div> + <%= form.label :time_zone, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> + <%= form.time_zone_select :time_zone, nil, {}, { 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> + + <div class="grid grid-cols-1 gap-4 md:grid-cols-2"> + <div> + <%= form.label :starts_at, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> + <%= form.datetime_local_field :starts_at, value: @conference.starts_at&.strftime('%Y-%m-%dT%H:%M'), 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> + + <div> + <%= form.label :ends_at, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> + <%= form.datetime_local_field :ends_at, value: @conference.ends_at&.strftime('%Y-%m-%dT%H:%M'), 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> + </div> + + <div data-controller="dynamic-fields"> + <div> + <%= form.label :import_job_class, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> + <%= form.select :import_job_class, + [["Select Import Job Class", ""]] + + ConferencesController.available_import_job_classes.map { |class_name, display_name| [display_name, class_name] }, + {}, + { 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", + data: { dynamic_fields_target: "importJobClass", action: "change->dynamic-fields#updateRequiredFields" } } %> + <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Select the import job class to see required data fields</p> + </div> + + <fieldset class="mt-6 border border-gray-300 rounded-md p-4 dark:border-gray-600"> + <legend class="px-2 text-sm font-medium text-gray-700 dark:text-gray-300">Required Data Fields</legend> + + <div class="space-y-4" data-dynamic-fields-target="requiredFields"> + <% @conference.required_data_fields.each do |field| %> + <div class="mb-4"> + <%= form.label "data[#{field}]", field.humanize, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> + <%= form.url_field "data[#{field}]", value: @conference.data&.dig(field), 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> + <% end %> + + <% if @conference.required_data_fields.empty? %> + <p class="text-sm text-gray-500 dark:text-gray-400">No required fields for this import job class</p> + <% end %> + </div> + </fieldset> + + <fieldset class="mt-6 border border-gray-300 rounded-md p-4 dark:border-gray-600" data-controller="dynamic-fields"> + <legend class="px-2 text-sm font-medium text-gray-700 dark:text-gray-300">Custom Data Fields</legend> + + <div class="space-y-4" data-dynamic-fields-target="container"> + <% if @conference.data.present? %> + <% @conference.data.except('schedule_url', 'filedrop_url', 'engelsystem_url', 'heartbeat_url').each do |key, value| %> + <div class="flex items-center space-x-2 nested-field"> + <div class="flex-1"> + <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Key</label> + <input type="text" value="<%= key %>" 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" data-field-key> + </div> + <div class="flex-1"> + <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Value</label> + <input type="text" name="data[<%= key %>]" value="<%= value %>" 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> + <div class="flex items-end"> + <button type="button" data-action="click->dynamic-fields#removeField" class="mt-1 p-2 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> + <path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" /> + </svg> + </button> + </div> + </div> + <% end %> + <% end %> + </div> + + <template data-dynamic-fields-target="template"> + <div class="flex items-center space-x-2 nested-field"> + <div class="flex-1"> + <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Key</label> + <input type="text" name="data[custom_key_NEW_RECORD]" placeholder="Enter key" 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" data-field-key> + </div> + <div class="flex-1"> + <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Value</label> + <input type="text" name="data[custom_value_NEW_RECORD]" placeholder="Enter value" 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> + <div class="flex items-end"> + <button type="button" data-action="click->dynamic-fields#removeField" class="mt-1 p-2 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> + <path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" /> + </svg> + </button> + </div> + </div> + </template> + + <div class="mt-4"> + <button type="button" data-action="click->dynamic-fields#addField" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-indigo-900 dark:text-indigo-200 dark:hover:bg-indigo-800"> + <svg xmlns="http://www.w3.org/2000/svg" class="-ml-0.5 mr-2 h-4 w-4" viewBox="0 0 20 20" fill="currentColor"> + <path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" /> + </svg> + Add Custom Field + </button> + </div> + </fieldset> + </div> + + <div class="flex justify-between pt-6"> + <%= link_to "Cancel", conference_path(slug: @conference.slug), class: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-600" %> + + <%= form.submit "Update Conference", class: "inline-flex justify-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-blue-700 dark:hover:bg-blue-800" %> + </div> + <% end %> + + <div class="mt-8 pt-8 border-t border-gray-200 dark:border-gray-700"> + <h2 class="text-xl font-bold mb-4 dark:text-gray-200">Danger Zone</h2> + + <div class="bg-red-50 border border-red-300 rounded-md p-4 dark:bg-red-900/20 dark:border-red-800"> + <h3 class="text-lg font-medium text-red-800 dark:text-red-300">Delete This Conference</h3> + <p class="mt-1 text-sm text-red-700 dark:text-red-400">Once you delete a conference, there is no going back. This will delete all associated data including sessions, speakers, and stages.</p> + + <div class="mt-4"> + <%= button_to "Delete Conference", conference_path(slug: @conference.slug), method: :delete, + data: { confirm: "Are you sure you want to delete this conference? This action cannot be undone." }, + class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 dark:bg-red-700 dark:hover:bg-red-800" %> + </div> + </div> + </div> +</div> diff --git a/app/views/conferences/index.html.erb b/app/views/conferences/index.html.erb index 4fcb2ed..514fc3d 100644 --- a/app/views/conferences/index.html.erb +++ b/app/views/conferences/index.html.erb @@ -1,8 +1,53 @@ -<div> -<h1 class="text-xl my-4 dark:text-red-500">Conferences</h1> -<ul> -<% @conferences.each do |conference| %> -<li><%= link_to conference.name, conference_path(conference.slug), class: "inline-block px-4 py-2 text-slate-500 hover:text-slate-900 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-gray-600 rounded-md" %></li> -<% end %> -</ul> +<div class="container mx-auto px-4 py-8"> + <div class="flex justify-between items-center mb-6"> + <h1 class="text-2xl font-bold dark:text-gray-200">Conferences</h1> + + <% if user_signed_in? && current_user.has_role?("events_admin") %> + <%= link_to new_conference_path, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-blue-700 dark:hover:bg-blue-800" do %> + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor"> + <path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" /> + </svg> + New Conference + <% end %> + <% end %> + </div> + + <div class="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-md"> + <ul class="divide-y divide-gray-200 dark:divide-gray-700"> + <% if @conferences.empty? %> + <li class="px-6 py-4 text-center text-gray-500 dark:text-gray-400">No conferences available.</li> + <% else %> + <% @conferences.each do |conference| %> + <li> + <div class="px-4 py-4 flex items-center justify-between"> + <div class="min-w-0 flex-1"> + <div class="flex items-center"> + <p class="text-lg font-medium text-blue-600 dark:text-blue-400"> + <%= link_to conference.name, conference_path(slug: conference.slug) %> + </p> + </div> + <div class="mt-2 flex items-center text-sm text-gray-500 dark:text-gray-400"> + <% if conference.starts_at && conference.ends_at %> + <span> + <%= conference.starts_at.strftime("%b %d, %Y") %> - <%= conference.ends_at.strftime("%b %d, %Y") %> + </span> + <% end %> + </div> + </div> + + <% if user_signed_in? && current_user.has_role?("events_admin") %> + <div class="flex-shrink-0 flex"> + <%= link_to edit_conference_path(slug: conference.slug), class: "ml-2 text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" do %> + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> + <path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" /> + </svg> + <% end %> + </div> + <% end %> + </div> + </li> + <% end %> + <% end %> + </ul> + </div> </div> diff --git a/app/views/conferences/new.html.erb b/app/views/conferences/new.html.erb new file mode 100644 index 0000000..6f58f4f --- /dev/null +++ b/app/views/conferences/new.html.erb @@ -0,0 +1,115 @@ +<div class="container mx-auto px-4 py-8"> + <h1 class="text-2xl font-bold mb-6 dark:text-gray-200">New Conference</h1> + + <%= form_with(model: @conference, url: conferences_path, class: "space-y-6") do |form| %> + <% if @conference.errors.any? %> + <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4"> + <h2 class="font-bold"><%= pluralize(@conference.errors.count, "error") %> prohibited this conference from being saved:</h2> + <ul class="list-disc list-inside mt-2"> + <% @conference.errors.full_messages.each do |message| %> + <li><%= message %></li> + <% end %> + </ul> + </div> + <% end %> + + <div class="space-y-4"> + <div> + <%= form.label :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> + <%= form.text_field :name, 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> + + <div> + <%= form.label :slug, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> + <%= form.text_field :slug, 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" %> + <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Used in URLs. Should contain only lowercase letters, numbers, and hyphens.</p> + </div> + + <div> + <%= form.label :url, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> + <%= form.url_field :url, 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> + + <div> + <%= form.label :time_zone, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> + <%= form.time_zone_select :time_zone, nil, { include_blank: "Select Time Zone" }, { 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> + + <div class="grid grid-cols-1 gap-4 md:grid-cols-2"> + <div> + <%= form.label :starts_at, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> + <%= form.datetime_local_field :starts_at, 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> + + <div> + <%= form.label :ends_at, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> + <%= form.datetime_local_field :ends_at, 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> + </div> + + <div data-controller="dynamic-fields"> + <div> + <%= form.label :import_job_class, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> + <%= form.select :import_job_class, + [["Select Import Job Class", ""]] + + ConferencesController.available_import_job_classes.map { |class_name, display_name| [display_name, class_name] }, + {}, + { 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", + data: { dynamic_fields_target: "importJobClass", action: "change->dynamic-fields#updateRequiredFields" } } %> + <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Select the import job class to see required data fields</p> + </div> + + <fieldset class="mt-6 border border-gray-300 rounded-md p-4 dark:border-gray-600"> + <legend class="px-2 text-sm font-medium text-gray-700 dark:text-gray-300">Required Data Fields</legend> + + <div class="space-y-4" data-dynamic-fields-target="requiredFields"> + <!-- Required fields will be dynamically added here --> + <p class="text-sm text-gray-500 dark:text-gray-400">Select an import job class to see required fields</p> + </div> + </fieldset> + + <fieldset class="mt-6 border border-gray-300 rounded-md p-4 dark:border-gray-600" data-controller="dynamic-fields"> + <legend class="px-2 text-sm font-medium text-gray-700 dark:text-gray-300">Custom Data Fields</legend> + + <div class="space-y-4" data-dynamic-fields-target="container"> + <!-- Dynamic fields will be added here --> + </div> + + <template data-dynamic-fields-target="template"> + <div class="flex items-center space-x-2 nested-field"> + <div class="flex-1"> + <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Key</label> + <input type="text" name="data[custom_key_NEW_RECORD]" placeholder="Enter key" 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" data-field-key> + </div> + <div class="flex-1"> + <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Value</label> + <input type="text" name="data[custom_value_NEW_RECORD]" placeholder="Enter value" 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> + <div class="flex items-end"> + <button type="button" data-action="click->dynamic-fields#removeField" class="mt-1 p-2 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> + <path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" /> + </svg> + </button> + </div> + </div> + </template> + + <div class="mt-4"> + <button type="button" data-action="click->dynamic-fields#addField" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-indigo-900 dark:text-indigo-200 dark:hover:bg-indigo-800"> + <svg xmlns="http://www.w3.org/2000/svg" class="-ml-0.5 mr-2 h-4 w-4" viewBox="0 0 20 20" fill="currentColor"> + <path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" /> + </svg> + Add Custom Field + </button> + </div> + </fieldset> + </div> + + <div class="flex justify-between pt-6"> + <%= link_to "Cancel", conferences_path, class: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-600" %> + + <%= form.submit "Create Conference", class: "inline-flex justify-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-blue-700 dark:hover:bg-blue-800" %> + </div> + <% end %> +</div> diff --git a/app/views/conferences/show.html.erb b/app/views/conferences/show.html.erb index 0528867..2d8fa31 100644 --- a/app/views/conferences/show.html.erb +++ b/app/views/conferences/show.html.erb @@ -13,8 +13,8 @@ current_time = Time.zone.now.in_time_zone(@conference.time_zone) <%= render partial: 'assignments/filteredlist_option', locals: { user: } %> <% end %> </template> -<div> - <div> +<div class="container mx-auto px-4 py-8"> + <div class="mb-4"> <a href="#now" onclick="document.querySelector('#now')?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' }); return false" class="underline text-blue-500">Jump to current time</a> </div> <h1 class="text-2xl font-bold my-2 dark:text-red-500"><%= @conference.name %></h1> diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index c2d89e2..262e862 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -3,6 +3,6 @@ # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. # Use this to limit dissemination of sensitive information. # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. -Rails.application.config.filter_parameters += [ - :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +Rails.application.config.filter_parameters += %i[ + passw secret token _key crypt salt certificate otp ssn ] diff --git a/config/queue.yml b/config/queue.yml index 6dde0de..0ff67ce 100644 --- a/config/queue.yml +++ b/config/queue.yml @@ -1,18 +1,18 @@ -# default: &default -# dispatchers: -# - polling_interval: 1 -# batch_size: 500 -# workers: -# - queues: "*" -# threads: 3 -# processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %> -# polling_interval: 0.1 -# -# development: -# <<: *default -# -# test: -# <<: *default -# -# production: -# <<: *default +default: &default + dispatchers: + - polling_interval: 1 + batch_size: 500 + workers: + - queues: "*" + threads: 3 + processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %> + polling_interval: 0.1 + +development: + <<: *default + +test: + <<: *default + +production: + <<: *default diff --git a/config/routes.rb b/config/routes.rb index e139691..9fbdf75 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,14 +19,17 @@ Rails.application.routes.draw do root "conferences#index" resources :conferences, param: :slug do + collection do + get "required_fields", to: "conferences#required_fields" + end get "stats", on: :member get ":date", action: :show, on: :member, as: :date, date: /\d{4}-\d{2}-\d{2}/ resources :sessions, param: :ref_id do member do patch :update_notes end - resources :assignments, only: [ :create, :destroy ] - resources :candidates, only: [ :create, :destroy ] + resources :assignments, only: %i[create destroy] + resources :candidates, only: %i[create destroy] delete "candidates", to: "candidates#destroy_self" end resources :speakers, param: :ref_id -- GitLab