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
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 ]
before_action :set_conference, only: [ :show, :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
......@@ -107,37 +76,6 @@ 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
......
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "channels"
import "controllers"
import "@hotwired/turbo-rails"
document.addEventListener("turbo:load", function() {
console.log('turbo:load');
applyDarkmode();
const flashMessages = document.querySelectorAll(".flash");
flashMessages.forEach(flashMessage => {
......@@ -16,4 +15,4 @@ document.addEventListener("turbo:load", function() {
flashMessage.parentNode.removeChild(flashMessage);
}, 5000);
});
});import "channels"
});
......@@ -4,7 +4,7 @@ class FetchConferenceDataJob < ApplicationJob
def perform(conference_slug)
conference = Conference.find_by(slug: conference_slug)
# 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)
job_class = job_class_path.constantize
job_class.perform_now(conference_slug)
......
......@@ -8,6 +8,41 @@ module Pretalx
queue_as :default
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)
response = HTTParty.get(conference.schedule_url)
response.success? or return Rails.logger.error "Failed to fetch schedule from #{conference.schedule_url}"
......@@ -62,7 +97,10 @@ module Pretalx
end
end
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!
end
end
......@@ -97,6 +135,7 @@ module Pretalx
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
......@@ -129,13 +168,13 @@ module Pretalx
},
headers: { "Accept" => "application/json" },
timeout: 30
)
)
data = JSON.parse(response.body)
rescue StandardError => e
Rails.logger.warn("Filedrop response for #{session.ref_id} failed: #{e.message}")
return {}
end
if !data["talks"].is_a?(Array)
unless data["talks"].is_a?(Array)
Rails.logger.warn("Filedrop index was incomplete")
return {}
end
......@@ -172,7 +211,8 @@ module Pretalx
# 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|
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?
......
require "httparty"
require "httparty"
module Republica2023OrLater
class ImportJob < ApplicationJob
......@@ -67,7 +68,9 @@ module Republica2023OrLater
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 do |speaker_uid|
conference.speakers.find_by!(ref_id: speaker_uid)
end
session.save!
end
end
......
class FiledropFile < ApplicationRecord
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)
filename.gsub(/[^\w\s.-]/, "_")
......@@ -9,11 +10,11 @@ class FiledropFile < ApplicationRecord
def safe_download_path(download_dir, filename)
sanitized_filename = sanitize_filename(filename)
output_path = File.join(download_dir, sanitized_filename)
if File.expand_path(output_path).start_with?(File.expand_path(download_dir))
output_path
else
unless File.expand_path(output_path).start_with?(File.expand_path(download_dir))
raise "Invalid filename, potential directory traversal detected!"
end
output_path
end
def download(url)
......
......@@ -64,8 +64,9 @@ class Session < ApplicationRecord
def notify_if_changed
return if new_record?
if saved_changes.present?
ActiveSupport::Notifications.instrument("session.updated", record: self, changes: saved_changes)
end
return unless saved_changes.present?
ActiveSupport::Notifications.instrument("session.updated", record: self, changes: saved_changes)
end
end
......@@ -15,8 +15,9 @@ class Speaker < ApplicationRecord
def notify_if_changed
return if new_record?
if saved_changes.present?
ActiveSupport::Notifications.instrument("speaker.updated", record: self, changes: saved_changes)
end
return unless saved_changes.present?
ActiveSupport::Notifications.instrument("speaker.updated", record: self, changes: saved_changes)
end
end
......@@ -102,6 +102,11 @@ class User < ApplicationRecord
roles.joins(:permissions).exists?(permissions: { name: permission_name })
end
# Alias for has_permission? with a more standard Rails name
def can?(permission_name)
has_permission?(permission_name)
end
def shiftcoordinator?
has_role?("shift_coordinator")
end
......
......@@ -2,7 +2,7 @@ class AssignmentAuditSubscriber
def self.subscribe
ActiveSupport::Notifications.subscribe(/\Aassignment\..*/) do |*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
......
......@@ -2,7 +2,7 @@ class RevisionSubscriber
def self.subscribe
ActiveSupport::Notifications.subscribe(/\A(?:session|speaker)\.updated/) do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
new.handle_event(event) # Call the instance method
new.handle_event(event) # Call the instance method
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">
Assignments for
<%= link_to @user.name, user_assignments_path(@user) %>
......
<% now = Time.now %>
<div class="scroll-smooth">
<div class="container mx-auto px-4 py-8">
<% now = Time.now %>
<div class="scroll-smooth">
<h1 class="text-xl my-4 dark:text-red-500">Assignments for all users</h1>
<p>
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 @@
<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 %>
<% if user_signed_in? && current_user.can?("manage_conferences") %>
<%= 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">
<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>
......@@ -35,9 +35,9 @@
</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">
<%= 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">
<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>
......
<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>
<html class="md:scroll-pt-16">
<html>
<head>
<title>ReScheduled</title>
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<title>re:scheduled</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= 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 %>
<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>
<body <%= tag.attributes(body_data_attributes) %> class="dark:bg-gray-900 dark:text-slate-300">
<script type="text/javascript">
applyDarkmode(); // Required to prevent flashing on load
</script>
<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">
<div class="relative bg-slate-100 dark:bg-zinc-700 z-50 container mx-auto px-4 py-4 flex justify-between items-center">
<div class="flex items-center space-x-4">
<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' %>
<body class="bg-gray-100 dark:bg-gray-900 min-h-screen">
<header class="bg-white dark:bg-gray-800 shadow">
<div class="container mx-auto px-4 py-4 flex justify-between items-center">
<div class="flex items-center">
<h1 class="text-xl font-bold text-gray-900 dark:text-white">
<%= link_to "re:scheduled", root_path %>
</h1>
</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 class="hidden md:flex items-center space-x-4 ml-0 lg:ml-8">
<% 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>
</header>
<%= render partial: 'shared/flash' %>
<main class="container mx-auto mt-8 px-5 flex pb-4 md:mt-24">
<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">
<span class="block sm:inline"><%= notice %></span>
</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 %>
</main>
</body>
......
......@@ -75,6 +75,8 @@ Rails.application.configure do
config.action_controller.raise_on_missing_callback_actions = true
config.telegram_default_target = ENV["TELEGRAM_DEFAULT_TARGET"] || "2192297"
config.solid_queue.logger = ActiveSupport::Logger.new(STDOUT)
end
Rails.application.routes.default_url_options[:host] = "127.0.0.1"
......
......@@ -15,10 +15,9 @@ Rails.application.routes.draw do
get "up" => "rails/health#show", as: :rails_health_check
# Defines the root path route ("/")
# root "posts#index"
root "conferences#index"
resources :conferences, param: :slug do
resources :conferences, param: :slug, only: [ :index, :show ] do
collection do
get "required_fields", to: "conferences#required_fields"
end
......