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 (4)
Showing
with 128 additions and 436 deletions
class ConferencesController < ApplicationController class ConferencesController < ApplicationController
before_action :authenticate_user!, except: [ :index, :show, :stats ] before_action :authenticate_user!, except: [ :index, :show, :stats ]
before_action :authorize_permission, only: [ :new, :create, :edit, :update, :destroy ] before_action :set_conference, only: [ :show, :stats ]
before_action :set_conference, only: [ :show, :edit, :update, :destroy, :stats ]
private private
def authorize_permission
super("manage_conferences")
end
def set_conference def set_conference
@conference = Conference.find_by(slug: params[:slug]) @conference = Conference.find_by(slug: params[:slug])
end 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 public
def self.available_import_job_classes def self.available_import_job_classes
...@@ -107,37 +76,6 @@ class ConferencesController < ApplicationController ...@@ -107,37 +76,6 @@ class ConferencesController < ApplicationController
@users = User.all @users = User.all
end end
def new
@conference = Conference.new
end
def create
@conference = Conference.new(conference_params)
if @conference.save
redirect_to conference_path(slug: @conference.slug), notice: "Conference was successfully created."
else
render :new, status: :unprocessable_entity
end
end
def edit
# @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 def stats
@conference = Conference.find_by(slug: params[:slug]) @conference = Conference.find_by(slug: params[:slug])
@relevant_stages = @conference.relevant_stages @relevant_stages = @conference.relevant_stages
......
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "channels"
import "controllers" import "controllers"
import "@hotwired/turbo-rails" import "@hotwired/turbo-rails"
document.addEventListener("turbo:load", function() { document.addEventListener("turbo:load", function() {
console.log('turbo:load');
applyDarkmode();
const flashMessages = document.querySelectorAll(".flash"); const flashMessages = document.querySelectorAll(".flash");
flashMessages.forEach(flashMessage => { flashMessages.forEach(flashMessage => {
...@@ -16,4 +15,4 @@ document.addEventListener("turbo:load", function() { ...@@ -16,4 +15,4 @@ document.addEventListener("turbo:load", function() {
flashMessage.parentNode.removeChild(flashMessage); flashMessage.parentNode.removeChild(flashMessage);
}, 5000); }, 5000);
}); });
});import "channels" });
...@@ -4,7 +4,7 @@ class FetchConferenceDataJob < ApplicationJob ...@@ -4,7 +4,7 @@ class FetchConferenceDataJob < ApplicationJob
def perform(conference_slug) def perform(conference_slug)
conference = Conference.find_by(slug: conference_slug) conference = Conference.find_by(slug: conference_slug)
# Convert import job class to class path format # Convert import job class to class path format
job_class_path = "#{conference.import_job_class.camelize}::ImportJob" job_class_path = "#{conference.import_job_class.camelize}"
if Object.const_defined?(job_class_path) if Object.const_defined?(job_class_path)
job_class = job_class_path.constantize job_class = job_class_path.constantize
job_class.perform_now(conference_slug) job_class.perform_now(conference_slug)
......
...@@ -8,6 +8,41 @@ module Pretalx ...@@ -8,6 +8,41 @@ module Pretalx
queue_as :default queue_as :default
include ActionView::Helpers include ActionView::Helpers
# Class method to return required data fields
def self.required_data_fields
[ "schedule_url", "filedrop_url", "engelsystem_url", "heartbeat_url" ]
end
# Class method to return field metadata
def self.field_metadata
{
"schedule_url" => {
title: "Schedule URL",
description: "URL to the Pretalx schedule data in JSON format",
placeholder: "https://pretalx.com/api/events/<EVENT>/schedules/latest/",
required: true
},
"filedrop_url" => {
title: "Filedrop URL",
description: "URL to the filedrop service for speaker slides and materials",
placeholder: "https://filedrop.example.com/api/",
required: false
},
"engelsystem_url" => {
title: "Engelsystem URL",
description: "URL to the Engelsystem API for volunteer management",
placeholder: "https://engelsystem.example.com/api/",
required: false
},
"heartbeat_url" => {
title: "Heartbeat URL",
description: "URL to ping when the import is complete",
placeholder: "https://monitoring.example.com/ping/<ID>",
required: false
}
}
end
def import_schedule(conference) def import_schedule(conference)
response = HTTParty.get(conference.schedule_url) response = HTTParty.get(conference.schedule_url)
response.success? or return Rails.logger.error "Failed to fetch schedule from #{conference.schedule_url}" response.success? or return Rails.logger.error "Failed to fetch schedule from #{conference.schedule_url}"
...@@ -62,7 +97,10 @@ module Pretalx ...@@ -62,7 +97,10 @@ module Pretalx
end end
end end
session.recorded = !session_data.fetch("do_not_record", false) session.recorded = !session_data.fetch("do_not_record", false)
update_filedrop_data(session, filedrop_index[session.ref_id], conference.filedrop_url) if filedrop_index[session.ref_id] if filedrop_index[session.ref_id]
update_filedrop_data(session, filedrop_index[session.ref_id],
conference.filedrop_url)
end
session.save! session.save!
end end
end end
...@@ -97,6 +135,7 @@ module Pretalx ...@@ -97,6 +135,7 @@ module Pretalx
shifts_at_time.each do |shift| shifts_at_time.each do |shift|
next unless session.stage.name == shift.dig("location", "name") next unless session.stage.name == shift.dig("location", "name")
session.engelsystem_id = shift["id"] session.engelsystem_id = shift["id"]
session.engelsystem_url = shift["url"] session.engelsystem_url = shift["url"]
session.save session.save
...@@ -129,13 +168,13 @@ module Pretalx ...@@ -129,13 +168,13 @@ module Pretalx
}, },
headers: { "Accept" => "application/json" }, headers: { "Accept" => "application/json" },
timeout: 30 timeout: 30
) )
data = JSON.parse(response.body) data = JSON.parse(response.body)
rescue StandardError => e rescue StandardError => e
Rails.logger.warn("Filedrop response for #{session.ref_id} failed: #{e.message}") Rails.logger.warn("Filedrop response for #{session.ref_id} failed: #{e.message}")
return {} return {}
end end
if !data["talks"].is_a?(Array) unless data["talks"].is_a?(Array)
Rails.logger.warn("Filedrop index was incomplete") Rails.logger.warn("Filedrop index was incomplete")
return {} return {}
end end
...@@ -172,7 +211,8 @@ module Pretalx ...@@ -172,7 +211,8 @@ module Pretalx
# Add or update files # Add or update files
filedrop_data["files"]&.each do |file_data| filedrop_data["files"]&.each do |file_data|
session.filedrop_files.find_or_initialize_by(name: file_data["name"], checksum: file_data["meta"]["hash"]).tap do |file| session.filedrop_files.find_or_initialize_by(name: file_data["name"],
checksum: file_data["meta"]["hash"]).tap do |file|
file.size = file_data["meta"]["size"] file.size = file_data["meta"]["size"]
file.orig_created = parse_datetime_or_nil(session.conference, file_data["meta"]["created"]) file.orig_created = parse_datetime_or_nil(session.conference, file_data["meta"]["created"])
if file_data["url"].blank? if file_data["url"].blank?
......
require "httparty" require "httparty"
require "httparty"
module Republica2023OrLater module Republica2023OrLater
class ImportJob < ApplicationJob class ImportJob < ApplicationJob
...@@ -67,7 +68,9 @@ module Republica2023OrLater ...@@ -67,7 +68,9 @@ module Republica2023OrLater
session.starts_at = session_data["datetime_start"] session.starts_at = session_data["datetime_start"]
session.ends_at = session_data["datetime_end"] session.ends_at = session_data["datetime_end"]
session.url = "https://re-publica.com#{session_data['path']}" session.url = "https://re-publica.com#{session_data['path']}"
session.speakers = session_data["speaker_uid"].map { |speaker_uid| conference.speakers.find_by!(ref_id: speaker_uid) } session.speakers = session_data["speaker_uid"].map do |speaker_uid|
conference.speakers.find_by!(ref_id: speaker_uid)
end
session.save! session.save!
end end
end end
......
class FiledropFile < ApplicationRecord class FiledropFile < ApplicationRecord
belongs_to :session belongs_to :session
validates :checksum, presence: true, format: { with: /\A[0-9a-fA-F]+\z/, message: "only allows hexadecimal characters" } validates :checksum, presence: true,
format: { with: /\A[0-9a-fA-F]+\z/, message: "only allows hexadecimal characters" }
def sanitize_filename(filename) def sanitize_filename(filename)
filename.gsub(/[^\w\s.-]/, "_") filename.gsub(/[^\w\s.-]/, "_")
...@@ -9,11 +10,11 @@ class FiledropFile < ApplicationRecord ...@@ -9,11 +10,11 @@ class FiledropFile < ApplicationRecord
def safe_download_path(download_dir, filename) def safe_download_path(download_dir, filename)
sanitized_filename = sanitize_filename(filename) sanitized_filename = sanitize_filename(filename)
output_path = File.join(download_dir, sanitized_filename) output_path = File.join(download_dir, sanitized_filename)
if File.expand_path(output_path).start_with?(File.expand_path(download_dir)) unless File.expand_path(output_path).start_with?(File.expand_path(download_dir))
output_path
else
raise "Invalid filename, potential directory traversal detected!" raise "Invalid filename, potential directory traversal detected!"
end end
output_path
end end
def download(url) def download(url)
......
...@@ -64,8 +64,9 @@ class Session < ApplicationRecord ...@@ -64,8 +64,9 @@ class Session < ApplicationRecord
def notify_if_changed def notify_if_changed
return if new_record? return if new_record?
if saved_changes.present?
ActiveSupport::Notifications.instrument("session.updated", record: self, changes: saved_changes) return unless saved_changes.present?
end
ActiveSupport::Notifications.instrument("session.updated", record: self, changes: saved_changes)
end end
end end
...@@ -15,8 +15,9 @@ class Speaker < ApplicationRecord ...@@ -15,8 +15,9 @@ class Speaker < ApplicationRecord
def notify_if_changed def notify_if_changed
return if new_record? return if new_record?
if saved_changes.present?
ActiveSupport::Notifications.instrument("speaker.updated", record: self, changes: saved_changes) return unless saved_changes.present?
end
ActiveSupport::Notifications.instrument("speaker.updated", record: self, changes: saved_changes)
end end
end end
...@@ -102,6 +102,11 @@ class User < ApplicationRecord ...@@ -102,6 +102,11 @@ class User < ApplicationRecord
roles.joins(:permissions).exists?(permissions: { name: permission_name }) roles.joins(:permissions).exists?(permissions: { name: permission_name })
end end
# Alias for has_permission? with a more standard Rails name
def can?(permission_name)
has_permission?(permission_name)
end
def shiftcoordinator? def shiftcoordinator?
has_role?("shift_coordinator") has_role?("shift_coordinator")
end end
......
...@@ -2,7 +2,7 @@ class AssignmentAuditSubscriber ...@@ -2,7 +2,7 @@ class AssignmentAuditSubscriber
def self.subscribe def self.subscribe
ActiveSupport::Notifications.subscribe(/\Aassignment\..*/) do |*args| ActiveSupport::Notifications.subscribe(/\Aassignment\..*/) do |*args|
event = ActiveSupport::Notifications::Event.new(*args) event = ActiveSupport::Notifications::Event.new(*args)
new.handle_event(event) # Call the instance method new.handle_event(event) # Call the instance method
end end
end end
......
...@@ -2,7 +2,7 @@ class RevisionSubscriber ...@@ -2,7 +2,7 @@ class RevisionSubscriber
def self.subscribe def self.subscribe
ActiveSupport::Notifications.subscribe(/\A(?:session|speaker)\.updated/) do |*args| ActiveSupport::Notifications.subscribe(/\A(?:session|speaker)\.updated/) do |*args|
event = ActiveSupport::Notifications::Event.new(*args) event = ActiveSupport::Notifications::Event.new(*args)
new.handle_event(event) # Call the instance method new.handle_event(event) # Call the instance method
end end
end end
......
<div class="max-w-full"> <div class="container mx-auto px-4 py-8">
<div class="max-w-full">
<h1 class="text-xl my-4 dark:text-red-500"> <h1 class="text-xl my-4 dark:text-red-500">
Assignments for Assignments for
<%= link_to @user.name, user_assignments_path(@user) %> <%= link_to @user.name, user_assignments_path(@user) %>
......
<% now = Time.now %> <div class="container mx-auto px-4 py-8">
<div class="scroll-smooth"> <% now = Time.now %>
<div class="scroll-smooth">
<h1 class="text-xl my-4 dark:text-red-500">Assignments for all users</h1> <h1 class="text-xl my-4 dark:text-red-500">Assignments for all users</h1>
<p> <p>
Jump to: Jump to:
......
<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>
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold dark:text-gray-200">Conferences</h1> <h1 class="text-2xl font-bold dark:text-gray-200">Conferences</h1>
<% if user_signed_in? && current_user.has_role?("events_admin") %> <% if user_signed_in? && current_user.can?("manage_conferences") %>
<%= 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 %> <%= link_to new_admin_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"> <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" /> <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> </svg>
...@@ -35,9 +35,9 @@ ...@@ -35,9 +35,9 @@
</div> </div>
</div> </div>
<% if user_signed_in? && current_user.has_role?("events_admin") %> <% if user_signed_in? && current_user.can?("manage_conferences") %>
<div class="flex-shrink-0 flex"> <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 %> <%= link_to edit_admin_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"> <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" /> <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> </svg>
......
<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>
<!DOCTYPE html> <!DOCTYPE html>
<html class="md:scroll-pt-16"> <html>
<head> <head>
<title>ReScheduled</title> <title>re:scheduled</title>
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"> <meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %> <%= csrf_meta_tags %>
<%= csp_meta_tag %> <%= csp_meta_tag %>
<%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "application" %> <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %> <%= javascript_importmap_tags %>
<script type="text/javascript">
function applyDarkmode() {
const userTheme = document.body.dataset.darkmode;
document.documentElement.classList.toggle(
'dark',
userTheme === 'dark' || ((userTheme === 'auto') && window.matchMedia('(prefers-color-scheme: dark)').matches)
);
}
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', applyDarkmode);
</script>
</head> </head>
<body <%= tag.attributes(body_data_attributes) %> class="dark:bg-gray-900 dark:text-slate-300"> <body class="bg-gray-100 dark:bg-gray-900 min-h-screen">
<script type="text/javascript"> <header class="bg-white dark:bg-gray-800 shadow">
applyDarkmode(); // Required to prevent flashing on load <div class="container mx-auto px-4 py-4 flex justify-between items-center">
</script> <div class="flex items-center">
<nav class="bg-slate-100 dark:bg-zinc-700 text-gray-700 dark:text-slate-300 shadow w-full relative md:fixed z-50" data-controller="navigation"> <h1 class="text-xl font-bold text-gray-900 dark:text-white">
<div class="relative bg-slate-100 dark:bg-zinc-700 z-50 container mx-auto px-4 py-4 flex justify-between items-center"> <%= link_to "re:scheduled", root_path %>
<div class="flex items-center space-x-4"> </h1>
<div class="text-xl font-bold text-black dark:text-white"><%= link_to 're:scheduled', '/', class: "!no-underline" %></div>
<div class="hidden md:flex space-x-4">
<%= link_to 'Conferences', conferences_path, class: 'hover:text-gray-900 dark:hover:text-slate-200' %>
<%= link_to 'Assignments', assignments_path, class: 'hover:text-gray-900 dark:hover:text-slate-200' %>
</div> </div>
<nav class="flex items-center space-x-4">
<% if user_signed_in? %>
<% if current_user.has_role?("events_admin") %>
<%= link_to "Admin", admin_conferences_path, class: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" %>
<% end %>
<%= link_to "Conferences", conferences_path, class: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" %>
<%= link_to "Assignments", assignments_path, class: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" %>
<%= link_to "Sign Out", destroy_user_session_path, method: :delete, class: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" %>
<div class="flex items-center mr-4">
<div class="w-8 h-8 rounded-full flex items-center justify-center mr-2" style="background-color: <%= current_user.avatar_color %>; color: <%= current_user.text_color %>">
<%= current_user.initials %>
</div>
<span class="text-gray-700 dark:text-gray-300"><%= current_user.name %></span>
</div>
<% else %>
<%= link_to "Sign In", new_user_session_path, class: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" %>
<% end %>
</nav>
</div> </div>
<div class="hidden md:flex items-center space-x-4 ml-0 lg:ml-8"> </header>
<% if user_signed_in? %>
<span class="-mr-2 hidden lg:inline">logged in as</span>
<%= render partial: 'application/user_avatar', locals: { user: current_user } %>
<%= link_to '<span class="hidden lg:inline">My </span>Profile'.html_safe, edit_user_registration_path, class: 'hover:text-gray-900 dark:hover:text-slate-200', aria_label: "My Profile" %>
<%= link_to 'My Assignments', user_assignments_path(current_user), class: 'hover:text-gray-900 dark:hover:text-slate-200' %>
<%= link_to 'Logout', destroy_user_session_path, data: { turbo_method: :delete }, class: 'hover:text-gray-900 dark:hover:text-slate-200' %>
<% else %>
<span class="px-2">not logged in</span>
<%= link_to 'Login', new_user_session_path, class: 'hover:text-gray-900 dark:hover:text-slate-200' %>
<%= link_to 'Sign Up', new_user_registration_path, class: 'hover:text-gray-900 dark:hover:text-slate-200' %>
<% end %>
</div>
<div class="md:hidden">
<button id="menu-button" class="text-gray-700 dark:text-slate-400 hover:text-gray-900 dark:hover:text-white focus:outline-none" data-action="click->navigation#toggleMenu">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
<div id="mobile-menu" class="absolute top-18 bg-slate-100 dark:bg-zinc-700 z-20 container mx-auto hidden md:hidden space-y-4 px-8 pb-4 shadow" data-navigation-target="mobileMenu">
<%= link_to 'Conferences', conferences_path, class: 'block hover:text-gray-900 dark:hover:text-white' %>
<%= link_to 'Assignments', assignments_path, class: 'block hover:text-gray-900 dark:hover:text-white' %>
<hr>
<% if user_signed_in? %>
<div>logged in as <%= render partial: 'application/user_avatar', locals: { user: current_user } %></div>
<%= link_to 'My Profile', edit_user_registration_path, class: 'block hover:text-gray-900 dark:hover:text-white' %>
<%= link_to 'My Assignments', user_assignments_path(current_user), class: 'block hover:text-gray-900 dark:hover:text-white' %>
<%= link_to 'Logout', destroy_user_session_path, data: { turbo_method: :delete }, class: 'block hover:text-gray-900 dark:hover:text-white' %>
<% else %>
<div>not logged in</div>
<%= link_to 'Login', new_user_session_path, class: 'block hover:text-gray-900 dark:hover:text-white' %>
<%= link_to 'Sign Up', new_user_registration_path, class: 'block hover:text-gray-900 dark:hover:text-white ' %>
<% end %>
</div>
</nav>
<main>
<% if notice %>
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4 mx-4 mt-4" role="alert">
<%= render partial: 'shared/flash' %> <span class="block sm:inline"><%= notice %></span>
<main class="container mx-auto mt-8 px-5 flex pb-4 md:mt-24"> </div>
<% end %>
<% if alert %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4 mx-4 mt-4" role="alert">
<span class="block sm:inline"><%= alert %></span>
</div>
<% end %>
<%= yield %> <%= yield %>
</main> </main>
</body> </body>
......
...@@ -75,6 +75,8 @@ Rails.application.configure do ...@@ -75,6 +75,8 @@ Rails.application.configure do
config.action_controller.raise_on_missing_callback_actions = true config.action_controller.raise_on_missing_callback_actions = true
config.telegram_default_target = ENV["TELEGRAM_DEFAULT_TARGET"] || "2192297" config.telegram_default_target = ENV["TELEGRAM_DEFAULT_TARGET"] || "2192297"
config.solid_queue.logger = ActiveSupport::Logger.new(STDOUT)
end end
Rails.application.routes.default_url_options[:host] = "127.0.0.1" Rails.application.routes.default_url_options[:host] = "127.0.0.1"
......
...@@ -15,10 +15,9 @@ Rails.application.routes.draw do ...@@ -15,10 +15,9 @@ Rails.application.routes.draw do
get "up" => "rails/health#show", as: :rails_health_check get "up" => "rails/health#show", as: :rails_health_check
# Defines the root path route ("/") # Defines the root path route ("/")
# root "posts#index"
root "conferences#index" root "conferences#index"
resources :conferences, param: :slug do resources :conferences, param: :slug, only: [ :index, :show ] do
collection do collection do
get "required_fields", to: "conferences#required_fields" get "required_fields", to: "conferences#required_fields"
end end
......